Make photo library offline-first and fix video thumbnails
This commit is contained in:
@@ -4,15 +4,20 @@ import (
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"image"
|
||||
"image/draw"
|
||||
"image/gif"
|
||||
_ "image/jpeg"
|
||||
"io"
|
||||
"math"
|
||||
"mime"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/image/font"
|
||||
@@ -41,6 +46,59 @@ func (i GifDecoder) Convert(r io.Reader) (any, error) {
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// VideoDecoder extracts a bounded preview frame with ffmpeg. The input is first persisted to a
|
||||
// temporary file so ffmpeg can seek to MP4/MOV metadata stored at the end of large camera files.
|
||||
// The file lives on disk and is never retained in process memory.
|
||||
type VideoDecoder struct{}
|
||||
|
||||
func (VideoDecoder) Convert(r io.Reader) (any, error) {
|
||||
input, err := os.CreateTemp("", "qsfera-video-*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inputPath := input.Name()
|
||||
defer os.Remove(inputPath)
|
||||
if _, err = io.Copy(input, r); err != nil {
|
||||
input.Close()
|
||||
return nil, errors.Wrap(err, "could not persist video for preview extraction")
|
||||
}
|
||||
if err = input.Sync(); err != nil {
|
||||
input.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err = input.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), videoFrameExtractionTimeout)
|
||||
defer cancel()
|
||||
frame, err := extractVideoFrame(ctx, inputPath, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not extract video preview frame")
|
||||
}
|
||||
|
||||
decoded, _, err := image.Decode(bytes.NewReader(frame))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not decode video preview frame")
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
const videoFrameExtractionTimeout = 90 * time.Second
|
||||
|
||||
func extractVideoFrame(ctx context.Context, input string, stdin io.Reader) ([]byte, error) {
|
||||
cmd := exec.CommandContext(
|
||||
ctx,
|
||||
"ffmpeg",
|
||||
"-hide_banner", "-loglevel", "error", "-i", input,
|
||||
"-map", "0:v:0", "-an", "-sn", "-frames:v", "1",
|
||||
"-vf", "thumbnail=30,scale=1920:-2:force_original_aspect_ratio=decrease",
|
||||
"-f", "image2pipe", "-vcodec", "png", "pipe:1",
|
||||
)
|
||||
cmd.Stdin = stdin
|
||||
return cmd.Output()
|
||||
}
|
||||
|
||||
// GgsDecoder is a converter for the geogebra slides file
|
||||
type GgsDecoder struct{ thumbnailpath string }
|
||||
|
||||
@@ -303,6 +361,9 @@ func ForType(mimeType string, opts map[string]any) FileConverter {
|
||||
// return the service call. So we should only get here when the mimeType parses fine.
|
||||
mimeType, _, _ = mime.ParseMediaType(mimeType)
|
||||
switch mimeType {
|
||||
case "video/mp4", "video/quicktime", "video/webm", "video/x-matroska", "video/x-msvideo", "video/mpeg", "video/3gpp",
|
||||
"video/x-m4v", "video/mp2t", "video/ogg", "video/x-ms-wmv", "video/x-flv", "video/hevc":
|
||||
return VideoDecoder{}
|
||||
case "text/plain":
|
||||
fontFileMap := ""
|
||||
fontFaceOpts := &opentype.FaceOptions{
|
||||
|
||||
Reference in New Issue
Block a user