diff --git a/ffmpeg/ffmpeg.go b/ffmpeg/ffmpeg.go index 2e61683..9b8e818 100644 --- a/ffmpeg/ffmpeg.go +++ b/ffmpeg/ffmpeg.go @@ -23,15 +23,16 @@ const ( ) type Metadata struct { - Orientation int `json:"orientation"` - Duration int `json:"duration,omitempty"` - Width int `json:"width,omitempty"` - Height int `json:"height,omitempty"` - Title string `json:"title,omitempty"` - Artist string `json:"artist,omitempty"` - FPS int `json:"fps,omitempty"` - HasVideo bool `json:"has_video"` - HasAudio bool `json:"has_audio"` + Orientation int `json:"orientation"` + Duration int `json:"duration,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + Title string `json:"title,omitempty"` + Artist string `json:"artist,omitempty"` + FPS int `json:"fps,omitempty"` + SelectedFrame int `json:"selected_frame,omitempty"` + HasVideo bool `json:"has_video"` + HasAudio bool `json:"has_audio"` } type AVContext struct { @@ -49,8 +50,8 @@ type AVContext struct { orientation int size int64 duration time.Duration - indexAt C.int - durationAt time.Duration + availableIndex C.int + availableDuration time.Duration width, height int title, artist string hasVideo, hasAudio bool @@ -90,13 +91,19 @@ func (av *AVContext) ProcessFrames() (err error) { return } +func (av *AVContext) SelectFrame(n int) (err error) { + nn := C.int(n) + if av.thumbContext != nil && nn >= av.thumbContext.n { + nn = av.thumbContext.n - 1 + } + av.selectedIndex = nn + return nil +} + func (av *AVContext) Export(bands int) (buf []byte, err error) { if err = av.ProcessFrames(); err != nil { return } - if av.selectedIndex < 0 { - findBestFrameIndex(av) - } if bands < 3 || bands > 4 { bands = 3 } @@ -112,19 +119,24 @@ func (av *AVContext) Close() { func (av *AVContext) Metadata() *Metadata { var fps float64 - if av.durationAt > 0 { - fps = float64(av.indexAt) * float64(time.Second) / float64(av.durationAt) + if av.availableDuration > 0 { + fps = float64(av.availableIndex) * float64(time.Second) / float64(av.availableDuration) + } + var selectedFrame int + if av.availableIndex > 0 && av.selectedIndex > -1 { + selectedFrame = int(av.selectedIndex) } return &Metadata{ - Orientation: av.orientation, - Duration: int(av.duration / time.Millisecond), - Width: av.width, - Height: av.height, - Title: av.title, - Artist: av.artist, - FPS: int(math.Round(fps)), - HasVideo: av.hasVideo, - HasAudio: av.hasAudio, + Orientation: av.orientation, + Duration: int(av.duration / time.Millisecond), + Width: av.width, + Height: av.height, + Title: av.title, + Artist: av.artist, + FPS: int(math.Round(fps)), + SelectedFrame: selectedFrame, + HasVideo: av.hasVideo, + HasAudio: av.hasAudio, } } @@ -207,11 +219,11 @@ func createDecoder(av *AVContext) error { } func incrementDuration(av *AVContext, frame *C.AVFrame, i C.int) { - av.indexAt = i + av.availableIndex = i if frame.pts != C.AV_NOPTS_VALUE { ptsToNano := C.int64_t(1000000000 * av.stream.time_base.num / av.stream.time_base.den) newDuration := time.Duration(frame.pts * ptsToNano) - av.durationAt = newDuration + av.availableDuration = newDuration if !av.durationInFormat && newDuration > av.duration { av.duration = newDuration } @@ -253,20 +265,24 @@ func createThumbContext(av *AVContext) error { } return avError(err) } - frames := make(chan *C.AVFrame, av.thumbContext.max_frames) + n := av.thumbContext.max_frames + if av.selectedIndex > -1 && n > av.selectedIndex+1 { + n = av.selectedIndex + 1 + } + frames := make(chan *C.AVFrame, n) done := populateHistogram(av, frames) frames <- frame if pkt.buf != nil { C.av_packet_unref(&pkt) } - return populateThumbContext(av, frames, done) + return populateThumbContext(av, frames, n, done) } -func populateThumbContext(av *AVContext, frames chan *C.AVFrame, done <-chan struct{}) error { +func populateThumbContext(av *AVContext, frames chan *C.AVFrame, n C.int, done <-chan struct{}) error { pkt := C.create_packet() var frame *C.AVFrame var err C.int - for i := C.int(1); i < av.thumbContext.max_frames; i++ { + for i := C.int(1); i < n; i++ { err = C.obtain_next_frame(av.formatContext, av.codecContext, av.stream.index, &pkt, &frame) if err < 0 { break @@ -275,6 +291,9 @@ func populateThumbContext(av *AVContext, frames chan *C.AVFrame, done <-chan str frames <- frame frame = nil } + if av.selectedIndex > av.availableIndex { + av.selectedIndex = av.availableIndex + } close(frames) if pkt.buf != nil { C.av_packet_unref(&pkt) @@ -286,13 +305,12 @@ func populateThumbContext(av *AVContext, frames chan *C.AVFrame, done <-chan str if err != 0 && err != C.int(ErrEOF) { return avError(err) } + if av.selectedIndex < 0 { + av.selectedIndex = C.find_best_frame_index(av.thumbContext) + } return nil } -func findBestFrameIndex(av *AVContext) { - av.selectedIndex = C.find_best_frame_index(av.thumbContext) -} - func convertFrameToRGB(av *AVContext, bands int) error { var alpha int if bands == 4 { diff --git a/processor.go b/processor.go index ae1cf3b..8b0ce60 100644 --- a/processor.go +++ b/processor.go @@ -8,6 +8,7 @@ import ( "github.com/gabriel-vasile/mimetype" "go.uber.org/zap" "io" + "strconv" "strings" ) @@ -127,6 +128,11 @@ func (p *Processor) Process(ctx context.Context, in *imagor.Blob, params imagorp if err = av.ProcessFrames(); err != nil { return } + case "frame": + n, _ := strconv.Atoi(filter.Args) + if err = av.SelectFrame(n); err != nil { + return + } } } meta := av.Metadata() diff --git a/testdata/golden/meta/alpha-webm.webm.meta.json b/testdata/golden/meta/alpha-webm.webm.meta.json index e45c027..466a08c 100644 --- a/testdata/golden/meta/alpha-webm.webm.meta.json +++ b/testdata/golden/meta/alpha-webm.webm.meta.json @@ -1 +1 @@ -{"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"has_video":true,"has_audio":false} \ No newline at end of file +{"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"selected_frame":45,"has_video":true,"has_audio":false} \ No newline at end of file diff --git a/testdata/golden/meta/everybody-betray-me.mkv.meta.json b/testdata/golden/meta/everybody-betray-me.mkv.meta.json index cb2195e..ddbd058 100644 --- a/testdata/golden/meta/everybody-betray-me.mkv.meta.json +++ b/testdata/golden/meta/everybody-betray-me.mkv.meta.json @@ -1 +1 @@ -{"orientation":1,"duration":7407,"width":640,"height":480,"fps":30,"has_video":true,"has_audio":true} \ No newline at end of file +{"orientation":1,"duration":7407,"width":640,"height":480,"fps":30,"selected_frame":43,"has_video":true,"has_audio":true} \ No newline at end of file diff --git a/testdata/golden/meta/macabre.mp4.meta.json b/testdata/golden/meta/macabre.mp4.meta.json index 419bf8d..0db3f5b 100644 --- a/testdata/golden/meta/macabre.mp4.meta.json +++ b/testdata/golden/meta/macabre.mp4.meta.json @@ -1 +1 @@ -{"orientation":1,"duration":3925,"width":492,"height":360,"fps":30,"has_video":true,"has_audio":true} \ No newline at end of file +{"orientation":1,"duration":3925,"width":492,"height":360,"fps":30,"selected_frame":11,"has_video":true,"has_audio":true} \ No newline at end of file diff --git a/testdata/golden/meta/schizo.flv.meta.json b/testdata/golden/meta/schizo.flv.meta.json index 3e24c24..7414915 100644 --- a/testdata/golden/meta/schizo.flv.meta.json +++ b/testdata/golden/meta/schizo.flv.meta.json @@ -1 +1 @@ -{"orientation":1,"duration":2560,"width":480,"height":360,"fps":30,"has_video":true,"has_audio":true} \ No newline at end of file +{"orientation":1,"duration":2560,"width":480,"height":360,"fps":30,"selected_frame":28,"has_video":true,"has_audio":true} \ No newline at end of file diff --git a/testdata/golden/meta/schizo_0.mp4.meta.json b/testdata/golden/meta/schizo_0.mp4.meta.json index 3dda84e..1f01e08 100644 --- a/testdata/golden/meta/schizo_0.mp4.meta.json +++ b/testdata/golden/meta/schizo_0.mp4.meta.json @@ -1 +1 @@ -{"orientation":1,"duration":2544,"width":480,"height":360,"fps":30,"has_video":true,"has_audio":true} \ No newline at end of file +{"orientation":1,"duration":2544,"width":480,"height":360,"fps":30,"selected_frame":11,"has_video":true,"has_audio":true} \ No newline at end of file diff --git a/testdata/golden/meta/schizo_180.mp4.meta.json b/testdata/golden/meta/schizo_180.mp4.meta.json index f992f3e..d92073e 100644 --- a/testdata/golden/meta/schizo_180.mp4.meta.json +++ b/testdata/golden/meta/schizo_180.mp4.meta.json @@ -1 +1 @@ -{"orientation":3,"duration":2544,"width":480,"height":360,"fps":30,"has_video":true,"has_audio":true} \ No newline at end of file +{"orientation":3,"duration":2544,"width":480,"height":360,"fps":30,"selected_frame":8,"has_video":true,"has_audio":true} \ No newline at end of file diff --git a/testdata/golden/meta/schizo_270.mp4.meta.json b/testdata/golden/meta/schizo_270.mp4.meta.json index 10bc562..fd715a3 100644 --- a/testdata/golden/meta/schizo_270.mp4.meta.json +++ b/testdata/golden/meta/schizo_270.mp4.meta.json @@ -1 +1 @@ -{"orientation":6,"duration":2544,"width":360,"height":480,"fps":30,"has_video":true,"has_audio":true} \ No newline at end of file +{"orientation":6,"duration":2544,"width":360,"height":480,"fps":30,"selected_frame":11,"has_video":true,"has_audio":true} \ No newline at end of file diff --git a/testdata/golden/meta/schizo_90.mp4.meta.json b/testdata/golden/meta/schizo_90.mp4.meta.json index ee3a3aa..4f357a2 100644 --- a/testdata/golden/meta/schizo_90.mp4.meta.json +++ b/testdata/golden/meta/schizo_90.mp4.meta.json @@ -1 +1 @@ -{"orientation":8,"duration":2544,"width":360,"height":480,"fps":30,"has_video":true,"has_audio":true} \ No newline at end of file +{"orientation":8,"duration":2544,"width":360,"height":480,"fps":30,"selected_frame":9,"has_video":true,"has_audio":true} \ No newline at end of file diff --git a/testdata/golden/result/meta/filters%3Aprocess_frames%28%29/everybody-betray-me.mkv b/testdata/golden/result/meta/filters%3Aprocess_frames%28%29/everybody-betray-me.mkv index 87a8093..11c4528 100644 --- a/testdata/golden/result/meta/filters%3Aprocess_frames%28%29/everybody-betray-me.mkv +++ b/testdata/golden/result/meta/filters%3Aprocess_frames%28%29/everybody-betray-me.mkv @@ -1 +1 @@ -{"format":"mkv","content_type":"video/x-matroska","orientation":1,"duration":7407,"width":640,"height":480,"fps":30,"has_video":true,"has_audio":true} \ No newline at end of file +{"format":"mkv","content_type":"video/x-matroska","orientation":1,"duration":7407,"width":640,"height":480,"fps":30,"selected_frame":43,"has_video":true,"has_audio":true} \ No newline at end of file