Skip to content

Commit

Permalink
ffmpeg: Estimate duration for some audio formats in GetCodecInfoBytes
Browse files Browse the repository at this point in the history
  • Loading branch information
j0sh committed Aug 9, 2024
1 parent e67ff9f commit c333041
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 11 deletions.
Binary file added data/audio.mp3
Binary file not shown.
Binary file added data/audio.ogg
Binary file not shown.
4 changes: 4 additions & 0 deletions ffmpeg/extras.c
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ int lpms_get_codec_info(char *fname, pcodec_info out)
out->dur = ic->duration / AV_TIME_BASE;
}
// Return
if (ic->iformat && ic->iformat->name) {
strncpy(out->format_name, ic->iformat->name, MIN(strlen(out->format_name), strlen(ic->iformat->name)) + 1);
}
if (video_present && vc->name) {
strncpy(out->video_codec, vc->name, MIN(strlen(out->video_codec), strlen(vc->name))+1);
// If video track is present extract pixel format info
Expand All @@ -186,6 +189,7 @@ int lpms_get_codec_info(char *fname, pcodec_info out)
}
if (audio_present && ac->name) {
strncpy(out->audio_codec, ac->name, MIN(strlen(out->audio_codec), strlen(ac->name))+1);
out->audio_bit_rate = ic->streams[astream]->codecpar->bit_rate;
} else {
// Indicate failure to extract audio codec from given container
out->audio_codec[0] = 0;
Expand Down
2 changes: 2 additions & 0 deletions ffmpeg/extras.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
#define _LPMS_EXTRAS_H_

typedef struct s_codec_info {
char * format_name;
char * video_codec;
char * audio_codec;
int audio_bit_rate;
int pixel_format;
int width;
int height;
Expand Down
22 changes: 22 additions & 0 deletions ffmpeg/ffmpeg.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,13 @@ const (
)

type MediaFormatInfo struct {
Format string
Acodec, Vcodec string
PixFormat PixelFormat
Width, Height int
FPS float32
DurSecs int64
AudioBitrate int
}

func (f *MediaFormatInfo) ScaledHeight(width int) int {
Expand All @@ -261,15 +263,21 @@ func GetCodecInfo(fname string) (CodecStatus, MediaFormatInfo, error) {
format := MediaFormatInfo{}
cfname := C.CString(fname)
defer C.free(unsafe.Pointer(cfname))
fmtname := C.CString(strings.Repeat("0", 255))
acodec_c := C.CString(strings.Repeat("0", 255))
vcodec_c := C.CString(strings.Repeat("0", 255))
defer C.free(unsafe.Pointer(fmtname))
defer C.free(unsafe.Pointer(acodec_c))
defer C.free(unsafe.Pointer(vcodec_c))
var params_c C.codec_info
params_c.format_name = fmtname
params_c.video_codec = vcodec_c
params_c.audio_codec = acodec_c
params_c.pixel_format = C.AV_PIX_FMT_NONE
status := CodecStatus(C.lpms_get_codec_info(cfname, &params_c))
if C.strlen(fmtname) < 255 {
format.Format = C.GoString(fmtname)
}
if C.strlen(acodec_c) < 255 {
format.Acodec = C.GoString(acodec_c)
}
Expand All @@ -281,6 +289,7 @@ func GetCodecInfo(fname string) (CodecStatus, MediaFormatInfo, error) {
format.Height = int(params_c.height)
format.FPS = float32(params_c.fps)
format.DurSecs = int64(params_c.dur)
format.AudioBitrate = int(params_c.audio_bit_rate)
return status, format, nil
}

Expand All @@ -300,6 +309,19 @@ func GetCodecInfoBytes(data []byte) (CodecStatus, MediaFormatInfo, error) {
}
fname := fmt.Sprintf("pipe:%d", or.Fd())
status, format, err = GetCodecInfo(fname)

// estimate duration from bitrate and filesize for audio
// some formats do not have built-in track duration metadata,
// and pipes do not have a filesize on their own which breaks ffmpeg's own
// duration estimates. So do the estimation calculation ourselves
// NB : mpegts has the same problem but may contain video so let's not handle that
// some other formats, eg ogg, show zero bitrate
//
// ffmpeg estimation of duration from bitrate:
// https://github.com/FFmpeg/FFmpeg/blob/8280ec7a3213c9b7bad88aac3695be2dedd2c00b/libavformat/demux.c#L1798
if format.DurSecs == 0 && format.AudioBitrate > 0 && (format.Format == "mp3" || format.Format == "wav" || format.Format == "aac") {
format.DurSecs = int64(len(data) * 8 / format.AudioBitrate)
}
return status, format, err
}

Expand Down
62 changes: 51 additions & 11 deletions ffmpeg/ffmpeg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1987,28 +1987,68 @@ func TestDurationFPS_GetCodecInfo(t *testing.T) {
ffprobe -loglevel warning -show_format test.m4a | grep duration=2.042993
ffmpeg -loglevel warning -i test-short.mp4 -vn -c:a flac test.flac
ffprobe -loglevel warning -show_format test.flac | grep duration=2.043356
ffmpeg -loglevel warning -i test.mp4 -vn -c:a copy stereo-audio.aac
ffprobe -show_entries stream=channels,channel_layout -of csv stereo-audio.aac | grep stream,2,stereo
ffprobe -show_format stereo-audio.aac | grep duration=52.440083
ffmpeg -i test.mp4 -vn stereo-audio.wav
ffprobe -show_format stereo-audio.wav | grep duration=60.139683
cp $1/../data/audio.mp3 test.mp3
ffprobe -show_format test.mp3 | grep duration=1.968000
cp $1/../data/audio.ogg test.ogg
ffprobe -show_format test.ogg | grep duration=1.974500
`
run(cmd)

files := []struct {
Filename string
Format string
Duration int64
FPS float32

// skip check if bytes version is known to fail duration
BytesSkipDuration bool
}{
{Filename: "test-short.mp4", Duration: 2, FPS: 24},
{Filename: "test.ts", Duration: 2, FPS: 30.0},
{Filename: "test.flac", Duration: 2, FPS: 0.0},
{Filename: "test.webm", Duration: 2, FPS: 24},
{Filename: "test.m4a", Duration: 2, FPS: 0.0},
{Filename: "test-short.mp4", Format: "mov,mp4,m4a,3gp,3g2,mj2", Duration: 2, FPS: 24},
{Filename: "test.ts", Format: "mpegts", Duration: 2, FPS: 30.0, BytesSkipDuration: true},
{Filename: "test.flac", Format: "flac", Duration: 2},
{Filename: "test.webm", Format: "matroska,webm", Duration: 2, FPS: 24},
{Filename: "test.m4a", Format: "mov,mp4,m4a,3gp,3g2,mj2", Duration: 2},
{Filename: "stereo-audio.aac", Format: "aac", Duration: 52},
{Filename: "stereo-audio.wav", Format: "wav", Duration: 60},
{Filename: "test.mp3", Format: "mp3", Duration: 1},
{Filename: "test.ogg", Format: "ogg", Duration: 1, BytesSkipDuration: true},
}
for _, file := range files {
t.Run(file.Filename, func(t *testing.T) {
assert := assert.New(t)
status, format, err := GetCodecInfo(path.Join(dir, file.Filename))
assert.Nil(err, "getcodecinfo error")
assert.Equal(CodecStatusOk, status, "status not ok")
assert.Equal(file.Duration, format.DurSecs, "duration mismatch")
assert.Equal(file.FPS, format.FPS, "fps mismatch")
fname := path.Join(dir, file.Filename)
// use 'bytes' prefix to prevent test runner regex matching
for _, tt := range []string{"GetCodecInfo", "BytesGetCodecInfo"} {
t.Run(tt, func(t *testing.T) {
assert := assert.New(t)
f := func() (CodecStatus, MediaFormatInfo, error) {
if tt == "GetCodecInfo" {
return GetCodecInfo(fname)
}
d, err := os.ReadFile(fname)
assert.Nil(err, "reading file")
return GetCodecInfoBytes(d)
}
status, format, err := f()
assert.Nil(err, "getcodecinfo error")
assert.Equal(CodecStatusOk, status, "status not ok")
assert.Equal(file.Format, format.Format, "format mismatch")
if tt == "BytesGetCodecInfo" && file.BytesSkipDuration {
assert.Equal(int64(0), format.DurSecs, "special duration mismatch")
} else {
assert.Equal(file.Duration, format.DurSecs, "duration mismatch")
}
assert.Equal(file.FPS, format.FPS, "fps mismatch")
})
}
})
}
}

0 comments on commit c333041

Please sign in to comment.