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:
parent
4d71837fb8
commit
a4f23c6560
11 changed files with 68 additions and 44 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
2
testdata/golden/meta/macabre.mp4.meta.json
vendored
2
testdata/golden/meta/macabre.mp4.meta.json
vendored
|
|
@ -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}
|
||||||
2
testdata/golden/meta/schizo.flv.meta.json
vendored
2
testdata/golden/meta/schizo.flv.meta.json
vendored
|
|
@ -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}
|
||||||
2
testdata/golden/meta/schizo_0.mp4.meta.json
vendored
2
testdata/golden/meta/schizo_0.mp4.meta.json
vendored
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
2
testdata/golden/meta/schizo_90.mp4.meta.json
vendored
2
testdata/golden/meta/schizo_90.mp4.meta.json
vendored
|
|
@ -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}
|
||||||
|
|
@ -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}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue