feat: ffmpeg select specific frame and frame(n) filter

* select frame after process

* restrict frame selected

* selected frame meta

* test: update golden files

* available index
This commit is contained in:
Adrian Shum 2022-10-13 16:52:59 +08:00 committed by GitHub
parent 4d71837fb8
commit a4f23c6560
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 68 additions and 44 deletions

View file

@ -23,15 +23,16 @@ const (
) )
type Metadata struct { type Metadata struct {
Orientation int `json:"orientation"` Orientation int `json:"orientation"`
Duration int `json:"duration,omitempty"` Duration int `json:"duration,omitempty"`
Width int `json:"width,omitempty"` Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"` Height int `json:"height,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
Artist string `json:"artist,omitempty"` Artist string `json:"artist,omitempty"`
FPS int `json:"fps,omitempty"` FPS int `json:"fps,omitempty"`
HasVideo bool `json:"has_video"` SelectedFrame int `json:"selected_frame,omitempty"`
HasAudio bool `json:"has_audio"` HasVideo bool `json:"has_video"`
HasAudio bool `json:"has_audio"`
} }
type AVContext struct { type AVContext struct {
@ -49,8 +50,8 @@ type AVContext struct {
orientation int orientation int
size int64 size int64
duration time.Duration duration time.Duration
indexAt C.int availableIndex C.int
durationAt time.Duration availableDuration time.Duration
width, height int width, height int
title, artist string title, artist string
hasVideo, hasAudio bool hasVideo, hasAudio bool
@ -90,13 +91,19 @@ func (av *AVContext) ProcessFrames() (err error) {
return 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) { func (av *AVContext) Export(bands int) (buf []byte, err error) {
if err = av.ProcessFrames(); err != nil { if err = av.ProcessFrames(); err != nil {
return return
} }
if av.selectedIndex < 0 {
findBestFrameIndex(av)
}
if bands < 3 || bands > 4 { if bands < 3 || bands > 4 {
bands = 3 bands = 3
} }
@ -112,19 +119,24 @@ func (av *AVContext) Close() {
func (av *AVContext) Metadata() *Metadata { func (av *AVContext) Metadata() *Metadata {
var fps float64 var fps float64
if av.durationAt > 0 { if av.availableDuration > 0 {
fps = float64(av.indexAt) * float64(time.Second) / float64(av.durationAt) 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{ return &Metadata{
Orientation: av.orientation, Orientation: av.orientation,
Duration: int(av.duration / time.Millisecond), Duration: int(av.duration / time.Millisecond),
Width: av.width, Width: av.width,
Height: av.height, Height: av.height,
Title: av.title, Title: av.title,
Artist: av.artist, Artist: av.artist,
FPS: int(math.Round(fps)), FPS: int(math.Round(fps)),
HasVideo: av.hasVideo, SelectedFrame: selectedFrame,
HasAudio: av.hasAudio, 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) { func incrementDuration(av *AVContext, frame *C.AVFrame, i C.int) {
av.indexAt = i av.availableIndex = i
if frame.pts != C.AV_NOPTS_VALUE { if frame.pts != C.AV_NOPTS_VALUE {
ptsToNano := C.int64_t(1000000000 * av.stream.time_base.num / av.stream.time_base.den) ptsToNano := C.int64_t(1000000000 * av.stream.time_base.num / av.stream.time_base.den)
newDuration := time.Duration(frame.pts * ptsToNano) newDuration := time.Duration(frame.pts * ptsToNano)
av.durationAt = newDuration av.availableDuration = newDuration
if !av.durationInFormat && newDuration > av.duration { if !av.durationInFormat && newDuration > av.duration {
av.duration = newDuration av.duration = newDuration
} }
@ -253,20 +265,24 @@ func createThumbContext(av *AVContext) error {
} }
return avError(err) 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) done := populateHistogram(av, frames)
frames <- frame frames <- frame
if pkt.buf != nil { if pkt.buf != nil {
C.av_packet_unref(&pkt) 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() pkt := C.create_packet()
var frame *C.AVFrame var frame *C.AVFrame
var err C.int 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) err = C.obtain_next_frame(av.formatContext, av.codecContext, av.stream.index, &pkt, &frame)
if err < 0 { if err < 0 {
break break
@ -275,6 +291,9 @@ func populateThumbContext(av *AVContext, frames chan *C.AVFrame, done <-chan str
frames <- frame frames <- frame
frame = nil frame = nil
} }
if av.selectedIndex > av.availableIndex {
av.selectedIndex = av.availableIndex
}
close(frames) close(frames)
if pkt.buf != nil { if pkt.buf != nil {
C.av_packet_unref(&pkt) 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) { if err != 0 && err != C.int(ErrEOF) {
return avError(err) return avError(err)
} }
if av.selectedIndex < 0 {
av.selectedIndex = C.find_best_frame_index(av.thumbContext)
}
return nil return nil
} }
func findBestFrameIndex(av *AVContext) {
av.selectedIndex = C.find_best_frame_index(av.thumbContext)
}
func convertFrameToRGB(av *AVContext, bands int) error { func convertFrameToRGB(av *AVContext, bands int) error {
var alpha int var alpha int
if bands == 4 { if bands == 4 {

View file

@ -8,6 +8,7 @@ import (
"github.com/gabriel-vasile/mimetype" "github.com/gabriel-vasile/mimetype"
"go.uber.org/zap" "go.uber.org/zap"
"io" "io"
"strconv"
"strings" "strings"
) )
@ -127,6 +128,11 @@ func (p *Processor) Process(ctx context.Context, in *imagor.Blob, params imagorp
if err = av.ProcessFrames(); err != nil { if err = av.ProcessFrames(); err != nil {
return return
} }
case "frame":
n, _ := strconv.Atoi(filter.Args)
if err = av.SelectFrame(n); err != nil {
return
}
} }
} }
meta := av.Metadata() meta := av.Metadata()

View file

@ -1 +1 @@
{"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"has_video":true,"has_audio":false} {"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"selected_frame":45,"has_video":true,"has_audio":false}

View file

@ -1 +1 @@
{"orientation":1,"duration":7407,"width":640,"height":480,"fps":30,"has_video":true,"has_audio":true} {"orientation":1,"duration":7407,"width":640,"height":480,"fps":30,"selected_frame":43,"has_video":true,"has_audio":true}

View file

@ -1 +1 @@
{"orientation":1,"duration":3925,"width":492,"height":360,"fps":30,"has_video":true,"has_audio":true} {"orientation":1,"duration":3925,"width":492,"height":360,"fps":30,"selected_frame":11,"has_video":true,"has_audio":true}

View file

@ -1 +1 @@
{"orientation":1,"duration":2560,"width":480,"height":360,"fps":30,"has_video":true,"has_audio":true} {"orientation":1,"duration":2560,"width":480,"height":360,"fps":30,"selected_frame":28,"has_video":true,"has_audio":true}

View file

@ -1 +1 @@
{"orientation":1,"duration":2544,"width":480,"height":360,"fps":30,"has_video":true,"has_audio":true} {"orientation":1,"duration":2544,"width":480,"height":360,"fps":30,"selected_frame":11,"has_video":true,"has_audio":true}

View file

@ -1 +1 @@
{"orientation":3,"duration":2544,"width":480,"height":360,"fps":30,"has_video":true,"has_audio":true} {"orientation":3,"duration":2544,"width":480,"height":360,"fps":30,"selected_frame":8,"has_video":true,"has_audio":true}

View file

@ -1 +1 @@
{"orientation":6,"duration":2544,"width":360,"height":480,"fps":30,"has_video":true,"has_audio":true} {"orientation":6,"duration":2544,"width":360,"height":480,"fps":30,"selected_frame":11,"has_video":true,"has_audio":true}

View file

@ -1 +1 @@
{"orientation":8,"duration":2544,"width":360,"height":480,"fps":30,"has_video":true,"has_audio":true} {"orientation":8,"duration":2544,"width":360,"height":480,"fps":30,"selected_frame":9,"has_video":true,"has_audio":true}

View file

@ -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} {"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}