feat(ffmpeg): add fps metadata (#26)
* refactor ffmpeg * cleanup * feat(ffmpeg): add fps metadata * reset golden * test: update golden files
This commit is contained in:
parent
f103bd4d01
commit
c86b39430d
10 changed files with 71 additions and 42 deletions
|
|
@ -28,6 +28,7 @@ type Metadata struct {
|
||||||
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"`
|
||||||
HasVideo bool `json:"has_video"`
|
HasVideo bool `json:"has_video"`
|
||||||
HasAudio bool `json:"has_audio"`
|
HasAudio bool `json:"has_audio"`
|
||||||
HasAlpha bool `json:"has_alpha"`
|
HasAlpha bool `json:"has_alpha"`
|
||||||
|
|
@ -42,16 +43,20 @@ type AVContext struct {
|
||||||
stream *C.AVStream
|
stream *C.AVStream
|
||||||
codecContext *C.AVCodecContext
|
codecContext *C.AVCodecContext
|
||||||
thumbContext *C.ThumbContext
|
thumbContext *C.ThumbContext
|
||||||
frame *C.AVFrame
|
selectedFrame *C.AVFrame
|
||||||
|
outputFrame *C.AVFrame
|
||||||
durationInFormat bool
|
durationInFormat bool
|
||||||
|
|
||||||
orientation int
|
orientation int
|
||||||
size int64
|
size int64
|
||||||
duration time.Duration
|
duration time.Duration
|
||||||
|
frameAt int
|
||||||
|
durationAt time.Duration
|
||||||
width, height int
|
width, height int
|
||||||
title, artist string
|
title, artist string
|
||||||
hasVideo, hasAudio bool
|
hasVideo, hasAudio bool
|
||||||
hasFrame, hasAlpha bool
|
hasAlpha bool
|
||||||
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadAVContext(ctx context.Context, reader io.Reader, size int64) (*AVContext, error) {
|
func LoadAVContext(ctx context.Context, reader io.Reader, size int64) (*AVContext, error) {
|
||||||
|
|
@ -67,31 +72,56 @@ func LoadAVContext(ctx context.Context, reader io.Reader, size int64) (*AVContex
|
||||||
if av.seeker != nil {
|
if av.seeker != nil {
|
||||||
flags |= seekPacketFlag
|
flags |= seekPacketFlag
|
||||||
}
|
}
|
||||||
err := createFormatContext(av, flags)
|
if err := createFormatContext(av, flags); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !av.hasVideo {
|
if !av.hasVideo {
|
||||||
return av, nil
|
return av, nil
|
||||||
}
|
}
|
||||||
if err = createDecoder(av); err == ErrTooBig || err == ErrDecoderNotFound {
|
if err := createDecoder(av); err != nil {
|
||||||
|
return av, err
|
||||||
|
}
|
||||||
|
if err := createThumbContext(av); err != nil {
|
||||||
|
return av, err
|
||||||
|
}
|
||||||
|
if err := convertFrameToRGB(av); err != nil {
|
||||||
return av, err
|
return av, err
|
||||||
}
|
}
|
||||||
return av, nil
|
return av, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func closeAVContext(av *AVContext) {
|
||||||
|
if !av.closed {
|
||||||
|
if av.outputFrame != nil {
|
||||||
|
C.av_frame_free(&av.outputFrame)
|
||||||
|
}
|
||||||
|
if av.thumbContext != nil {
|
||||||
|
C.free_thumb_context(av.thumbContext)
|
||||||
|
av.selectedFrame = nil
|
||||||
|
}
|
||||||
|
if av.codecContext != nil {
|
||||||
|
C.avcodec_free_context(&av.codecContext)
|
||||||
|
}
|
||||||
|
if av.formatContext != nil {
|
||||||
|
C.free_format_context(av.formatContext)
|
||||||
|
}
|
||||||
|
pointer.Unref(av.opaque)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (av *AVContext) Export() (buf []byte, err error) {
|
func (av *AVContext) Export() (buf []byte, err error) {
|
||||||
return exportBuffer(av)
|
return exportBuffer(av)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (av *AVContext) Close() {
|
func (av *AVContext) Close() {
|
||||||
if av.hasFrame {
|
closeAVContext(av)
|
||||||
C.av_frame_free(&av.frame)
|
|
||||||
}
|
|
||||||
freeFormatContext(av)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (av *AVContext) Metadata() *Metadata {
|
func (av *AVContext) Metadata() *Metadata {
|
||||||
|
var fps float64
|
||||||
|
if av.durationAt > 0 {
|
||||||
|
fps = float64(av.frameAt) * float64(time.Second) / float64(av.durationAt)
|
||||||
|
}
|
||||||
return &Metadata{
|
return &Metadata{
|
||||||
Orientation: av.orientation,
|
Orientation: av.orientation,
|
||||||
Duration: int(av.duration / time.Millisecond),
|
Duration: int(av.duration / time.Millisecond),
|
||||||
|
|
@ -99,17 +129,13 @@ func (av *AVContext) Metadata() *Metadata {
|
||||||
Height: av.height,
|
Height: av.height,
|
||||||
Title: av.title,
|
Title: av.title,
|
||||||
Artist: av.artist,
|
Artist: av.artist,
|
||||||
|
FPS: int(fps),
|
||||||
HasVideo: av.hasVideo,
|
HasVideo: av.hasVideo,
|
||||||
HasAudio: av.hasAudio,
|
HasAudio: av.hasAudio,
|
||||||
HasAlpha: av.hasAlpha,
|
HasAlpha: av.hasAlpha,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func freeFormatContext(av *AVContext) {
|
|
||||||
C.free_format_context(av.formatContext)
|
|
||||||
pointer.Unref(av.opaque)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createFormatContext(av *AVContext, callbackFlags C.int) error {
|
func createFormatContext(av *AVContext, callbackFlags C.int) error {
|
||||||
intErr := C.allocate_format_context(&av.formatContext)
|
intErr := C.allocate_format_context(&av.formatContext)
|
||||||
if intErr < 0 {
|
if intErr < 0 {
|
||||||
|
|
@ -125,7 +151,8 @@ func createFormatContext(av *AVContext, callbackFlags C.int) error {
|
||||||
duration(av)
|
duration(av)
|
||||||
err := findStreams(av)
|
err := findStreams(av)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
freeFormatContext(av)
|
C.free_format_context(av.formatContext)
|
||||||
|
pointer.Unref(av.opaque)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -165,15 +192,16 @@ func createDecoder(av *AVContext) error {
|
||||||
if err < 0 {
|
if err < 0 {
|
||||||
return avError(err)
|
return avError(err)
|
||||||
}
|
}
|
||||||
defer C.avcodec_free_context(&av.codecContext)
|
return nil
|
||||||
return createThumbContext(av)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func incrementDuration(av *AVContext, frame *C.AVFrame) {
|
func incrementDuration(av *AVContext, frame *C.AVFrame, i int) {
|
||||||
if !av.durationInFormat && frame.pts != C.AV_NOPTS_VALUE {
|
av.frameAt = i
|
||||||
|
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)
|
||||||
if newDuration > av.duration {
|
av.durationAt = newDuration
|
||||||
|
if !av.durationInFormat && newDuration > av.duration {
|
||||||
av.duration = newDuration
|
av.duration = newDuration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -199,7 +227,7 @@ func createThumbContext(av *AVContext) error {
|
||||||
var frame *C.AVFrame
|
var frame *C.AVFrame
|
||||||
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 {
|
||||||
incrementDuration(av, frame)
|
incrementDuration(av, frame, 0)
|
||||||
av.thumbContext = C.create_thumb_context(av.stream, frame)
|
av.thumbContext = C.create_thumb_context(av.stream, frame)
|
||||||
if av.thumbContext == nil {
|
if av.thumbContext == nil {
|
||||||
err = C.int(ErrNoMem)
|
err = C.int(ErrNoMem)
|
||||||
|
|
@ -214,7 +242,6 @@ func createThumbContext(av *AVContext) error {
|
||||||
}
|
}
|
||||||
return avError(err)
|
return avError(err)
|
||||||
}
|
}
|
||||||
defer C.free_thumb_context(av.thumbContext)
|
|
||||||
frames := make(chan *C.AVFrame, av.thumbContext.max_frames)
|
frames := make(chan *C.AVFrame, av.thumbContext.max_frames)
|
||||||
done := populateHistogram(av, frames)
|
done := populateHistogram(av, frames)
|
||||||
frames <- frame
|
frames <- frame
|
||||||
|
|
@ -233,7 +260,7 @@ func populateThumbContext(av *AVContext, frames chan *C.AVFrame, done <-chan str
|
||||||
if err < 0 {
|
if err < 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
incrementDuration(av, frame)
|
incrementDuration(av, frame, int(i))
|
||||||
frames <- frame
|
frames <- frame
|
||||||
frame = nil
|
frame = nil
|
||||||
}
|
}
|
||||||
|
|
@ -248,22 +275,24 @@ 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)
|
||||||
}
|
}
|
||||||
return convertFrameToRGB(av)
|
av.selectedFrame = C.process_frames(av.thumbContext)
|
||||||
}
|
if av.selectedFrame == nil {
|
||||||
|
|
||||||
func convertFrameToRGB(av *AVContext) error {
|
|
||||||
outputFrame := C.convert_frame_to_rgb(C.process_frames(av.thumbContext), av.thumbContext.alpha)
|
|
||||||
if outputFrame == nil {
|
|
||||||
return ErrNoMem
|
return ErrNoMem
|
||||||
}
|
}
|
||||||
av.frame = outputFrame
|
|
||||||
av.hasFrame = true
|
|
||||||
av.hasAlpha = av.thumbContext.alpha != 0
|
av.hasAlpha = av.thumbContext.alpha != 0
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertFrameToRGB(av *AVContext) error {
|
||||||
|
av.outputFrame = C.convert_frame_to_rgb(av.selectedFrame, av.thumbContext.alpha)
|
||||||
|
if av.outputFrame == nil {
|
||||||
|
return ErrNoMem
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func exportBuffer(av *AVContext) ([]byte, error) {
|
func exportBuffer(av *AVContext) ([]byte, error) {
|
||||||
if !av.hasFrame {
|
if av.outputFrame == nil {
|
||||||
return nil, ErrInvalidData
|
return nil, ErrInvalidData
|
||||||
}
|
}
|
||||||
size := av.height * av.width
|
size := av.height * av.width
|
||||||
|
|
@ -272,6 +301,6 @@ func exportBuffer(av *AVContext) ([]byte, error) {
|
||||||
} else {
|
} else {
|
||||||
size *= 3
|
size *= 3
|
||||||
}
|
}
|
||||||
buf := C.GoBytes(unsafe.Pointer(av.frame.data[0]), C.int(size))
|
buf := C.GoBytes(unsafe.Pointer(av.outputFrame.data[0]), C.int(size))
|
||||||
return buf, nil
|
return buf, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"orientation":1,"duration":12040,"width":720,"height":576,"has_video":true,"has_audio":false,"has_alpha":true}
|
{"orientation":1,"duration":12040,"width":720,"height":576,"fps":25,"has_video":true,"has_audio":false,"has_alpha":true}
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"orientation":1,"duration":7407,"width":640,"height":480,"has_video":true,"has_audio":true,"has_alpha":false}
|
{"orientation":1,"duration":7407,"width":640,"height":480,"fps":29,"has_video":true,"has_audio":true,"has_alpha":false}
|
||||||
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,"has_video":true,"has_audio":true,"has_alpha":false}
|
{"orientation":1,"duration":3925,"width":492,"height":360,"fps":29,"has_video":true,"has_audio":true,"has_alpha":false}
|
||||||
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,"has_video":true,"has_audio":true,"has_alpha":false}
|
{"orientation":1,"duration":2560,"width":480,"height":360,"fps":29,"has_video":true,"has_audio":true,"has_alpha":false}
|
||||||
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,"has_video":true,"has_audio":true,"has_alpha":false}
|
{"orientation":1,"duration":2544,"width":480,"height":360,"fps":29,"has_video":true,"has_audio":true,"has_alpha":false}
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"orientation":3,"duration":2544,"width":480,"height":360,"has_video":true,"has_audio":true,"has_alpha":false}
|
{"orientation":3,"duration":2544,"width":480,"height":360,"fps":29,"has_video":true,"has_audio":true,"has_alpha":false}
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"orientation":6,"duration":2544,"width":360,"height":480,"has_video":true,"has_audio":true,"has_alpha":false}
|
{"orientation":6,"duration":2544,"width":360,"height":480,"fps":29,"has_video":true,"has_audio":true,"has_alpha":false}
|
||||||
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,"has_video":true,"has_audio":true,"has_alpha":false}
|
{"orientation":8,"duration":2544,"width":360,"height":480,"fps":29,"has_video":true,"has_audio":true,"has_alpha":false}
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"format":"mkv","content_type":"video/x-matroska","orientation":1,"duration":7407,"width":640,"height":480,"has_video":true,"has_audio":true,"has_alpha":false}
|
{"format":"mkv","content_type":"video/x-matroska","orientation":1,"duration":7407,"width":640,"height":480,"fps":29,"has_video":true,"has_audio":true,"has_alpha":false}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue