diff --git a/README.md b/README.md index 34087c3..e11d716 100644 --- a/README.md +++ b/README.md @@ -41,12 +41,34 @@ imagorvideo then converts the selected frame to RGB image data, forwards to the imagorvideo supports the following filters, which can be used in conjunction with [imagor filters](https://github.com/cshum/imagor#filters): -- `frame(n)` specifying the time position, duration or frame index for imaging, which skips the default automatic selection: +- `frame(n)` specify the position or time duration for imaging, which skips the automatic best frame selection: - Float between `0.0` and `1.0` indices position of the video. Example `frame(0.5)`, `frame(1.0)` - Time duration indices the elasped time since the start of video. Example `frame(5m1s)`, `frame(200s)` - - Number starts from 1 indices frame index, example `frame(1)`, `frame(10)` +- `seek(n)` seeks to the approximate position or time duration, then perform automatic best frame selection around that point: + - Float between `0.0` and `1.0` indices position of the video. Example `seek(0.5)` + - Time duration indices the elasped time since the start of video. Example `seek(5m1s)`, `seek(200s)` - `max_frames(n)` restrict the maximum number of frames allocated for image selection. The smaller the number, the faster the processing time. +#### `frame(n)` vs `seek(n)` + +There are differences you may want to choose one over the other. +`frame(n)` gives you the precise time frame specified. However, precise may not be the best in some circumstances: +``` +http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 +``` +Retrieving the frame at 5 minutes elapsed time of this video: +``` +http://localhost:8000/unsafe/300x0/filters:frame(5m)/http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 +``` +It results a complete black frame. + +![black](https://raw.githubusercontent.com/cshum/imagorvideo/master/testdata/black.jpg) + +This is where `seek(n)` comes handy. It seeks to the key frame before the 5 minutes elapsed time, then perform best frame selection starting from that point using root-mean-square error (RMSE). +The result is a reasonable image that sits close to the specified time: + +![seek 5m](https://raw.githubusercontent.com/cshum/imagorvideo/master/testdata/seek5m.jpg) + ### Metadata imagorvideo provides metadata endpoint that extracts video metadata, including dimension, duration and FPS data. It processes header only, without extracting the frame data for better processing speed. diff --git a/ffmpeg/ffmpeg.go b/ffmpeg/ffmpeg.go index 8aa0635..ffe86d9 100644 --- a/ffmpeg/ffmpeg.go +++ b/ffmpeg/ffmpeg.go @@ -107,13 +107,21 @@ func (av *AVContext) SelectPosition(f float64) (err error) { func (av *AVContext) SelectDuration(ts time.Duration) (err error) { if ts > 0 { av.selectedDuration = ts - if err = seekDuration(av, ts); err != nil { + if err = av.SeekDuration(ts); err != nil { return } } return av.ProcessFrames(-1) } +func (av *AVContext) SeekPosition(f float64) error { + return av.SeekDuration(av.positionToDuration(f)) +} + +func (av *AVContext) SeekDuration(ts time.Duration) error { + return seekDuration(av, ts) +} + func (av *AVContext) Export(bands int) (buf []byte, err error) { if err = av.ProcessFrames(-1); err != nil { return diff --git a/ffmpeg/ffmpeg_test.go b/ffmpeg/ffmpeg_test.go index b9bbd9a..f6a8d36 100644 --- a/ffmpeg/ffmpeg_test.go +++ b/ffmpeg/ffmpeg_test.go @@ -67,17 +67,6 @@ func TestAVContext(t *testing.T) { stats, err := os.Stat(path) require.NoError(t, err) av, err := LoadAVContext(reader, stats.Size()) - require.NoError(t, err) - defer av.Close() - if n == 10 { - require.NoError(t, av.ProcessFrames(n)) - } else if n == 99999 { - require.NoError(t, av.SelectDuration(time.Second)) - } else if n == 9999 { - require.NoError(t, av.SelectPosition(0.7)) - } else if n > -1 { - require.NoError(t, av.SelectFrame(n)) - } meta := av.Metadata() metaBuf, err := json.Marshal(meta) require.NoError(t, err) @@ -87,6 +76,19 @@ func TestAVContext(t *testing.T) { } else { require.NoError(t, os.WriteFile(goldenFile, metaBuf, 0666)) } + require.NoError(t, err) + defer av.Close() + if n == 10 { + require.NoError(t, av.ProcessFrames(n)) + } else if n == 99999 { + require.NoError(t, av.SelectDuration(time.Second)) + } else if n == 9999 { + require.NoError(t, av.SelectPosition(0.7)) + } else if n == 5 { + require.NoError(t, av.SelectFrame(n)) + } else { + require.NoError(t, av.SeekPosition(0.7)) + } bands := 4 if n == 99999 { bands = 999 diff --git a/processor.go b/processor.go index 87e2efe..2b9c5e7 100644 --- a/processor.go +++ b/processor.go @@ -136,6 +136,16 @@ func (p *Processor) Process(ctx context.Context, in *imagor.Blob, params imagorp } } } + case "seek": + if ts, e := time.ParseDuration(filter.Args); e == nil { + if err = av.SeekDuration(ts); err != nil { + return + } + } else if f, e := strconv.ParseFloat(filter.Args, 64); e == nil { + if err = av.SeekPosition(f); err != nil { + return + } + } case "max_frames": n, _ := strconv.Atoi(filter.Args) if err = av.ProcessFrames(n); err != nil { diff --git a/processor_test.go b/processor_test.go index 0f21c0b..9b45767 100644 --- a/processor_test.go +++ b/processor_test.go @@ -32,7 +32,6 @@ type test struct { name string path string expectCode int - sizeOnly bool } func TestProcessor(t *testing.T) { @@ -55,8 +54,10 @@ func TestProcessor(t *testing.T) { {name: "mp4 orient 270", path: "200x100/schizo_270.mp4"}, {name: "image", path: "fit-in/100x100/demo.png"}, {name: "alpha", path: "fit-in/filters:format(png)/alpha-webm.webm"}, - {name: "alpha frame duration", path: "500x/filters:frame(5s):format(png)/alpha-webm.webm", sizeOnly: true}, - {name: "alpha frame position", path: "500x/filters:frame(0.5):format(png)/alpha-webm.webm", sizeOnly: true}, + {name: "alpha frame duration", path: "500x/filters:frame(5s):format(png)/alpha-webm.webm"}, + {name: "alpha frame position", path: "500x/filters:frame(0.5):format(png)/alpha-webm.webm"}, + {name: "alpha seek duration", path: "500x/filters:seek(5s):format(png)/alpha-webm.webm"}, + {name: "alpha seek position", path: "500x/filters:seek(0.5):format(png)/alpha-webm.webm"}, {name: "corrupted", path: "fit-in/100x100/corrupt/everybody-betray-me.mkv", expectCode: 406}, {name: "no cover meta", path: "meta/no_cover.mp3"}, {name: "no cover 406", path: "fit-in/100x100/no_cover.mp3", expectCode: 406}, @@ -108,9 +109,6 @@ func doGoldenTests(t *testing.T, resultDir string, tests []test, opts ...Option) assert.NoError(t, app.Shutdown(context.Background())) }) for _, tt := range tests { - if i == 1 && tt.sizeOnly { - continue - } t.Run(fmt.Sprintf("%s-%d", tt.name, i+1), func(t *testing.T) { w := httptest.NewRecorder() ctx, cancel := context.WithCancel(context.Background()) diff --git a/testdata/black.jpg b/testdata/black.jpg new file mode 100644 index 0000000..3bc9832 Binary files /dev/null and b/testdata/black.jpg differ diff --git a/testdata/golden/export/alpha-webm.webm.jpg b/testdata/golden/export/alpha-webm.webm.jpg index d887ab1..3312405 100644 Binary files a/testdata/golden/export/alpha-webm.webm.jpg and b/testdata/golden/export/alpha-webm.webm.jpg differ diff --git a/testdata/golden/export/everybody-betray-me.mkv.jpg b/testdata/golden/export/everybody-betray-me.mkv.jpg index 02a94a0..5c2f430 100644 Binary files a/testdata/golden/export/everybody-betray-me.mkv.jpg and b/testdata/golden/export/everybody-betray-me.mkv.jpg differ diff --git a/testdata/golden/result/500x/filters%3Aseek%280.5%29%3Aformat%28png%29/alpha-webm.webm b/testdata/golden/result/500x/filters%3Aseek%280.5%29%3Aformat%28png%29/alpha-webm.webm new file mode 100644 index 0000000..8f039c9 Binary files /dev/null and b/testdata/golden/result/500x/filters%3Aseek%280.5%29%3Aformat%28png%29/alpha-webm.webm differ diff --git a/testdata/golden/result/500x/filters%3Aseek%285s%29%3Aformat%28png%29/alpha-webm.webm b/testdata/golden/result/500x/filters%3Aseek%285s%29%3Aformat%28png%29/alpha-webm.webm new file mode 100644 index 0000000..752872d Binary files /dev/null and b/testdata/golden/result/500x/filters%3Aseek%285s%29%3Aformat%28png%29/alpha-webm.webm differ diff --git a/testdata/seek5m.jpg b/testdata/seek5m.jpg new file mode 100644 index 0000000..d800142 Binary files /dev/null and b/testdata/seek5m.jpg differ