From f32e37b8cd6c341fdfe6af65c969f8d6bbbe7b52 Mon Sep 17 00:00:00 2001 From: Josh Allmann Date: Fri, 16 Aug 2019 03:33:52 -0700 Subject: [PATCH] ffmpeg: Return stats on decoded stream from transcoder. --- cmd/transcoding/transcoding.go | 3 +- ffmpeg/ffmpeg.go | 16 ++++-- ffmpeg/ffmpeg_test.go | 99 ++++++++++++++++++++++++++++++++-- ffmpeg/lpms_ffmpeg.c | 7 ++- ffmpeg/lpms_ffmpeg.h | 2 +- 5 files changed, 116 insertions(+), 11 deletions(-) diff --git a/cmd/transcoding/transcoding.go b/cmd/transcoding/transcoding.go index 0afd5557c0..633d40d491 100644 --- a/cmd/transcoding/transcoding.go +++ b/cmd/transcoding/transcoding.go @@ -75,7 +75,8 @@ func main() { if err != nil { panic(err) } - for i, r := range res { + fmt.Printf("profile=input frames=%v pixels=%v\n", res.Decoded.Frames, res.Decoded.Pixels) + for i, r := range res.Encoded { fmt.Printf("profile=%v frames=%v pixels=%v\n", profiles[i].Name, r.Frames, r.Pixels) } } diff --git a/ffmpeg/ffmpeg.go b/ffmpeg/ffmpeg.go index 4876a327a5..fe65f6e713 100644 --- a/ffmpeg/ffmpeg.go +++ b/ffmpeg/ffmpeg.go @@ -46,6 +46,11 @@ type MediaInfo struct { Pixels int64 } +type TranscodeResults struct { + Decoded MediaInfo + Encoded []MediaInfo +} + func RTMPToHLS(localRTMPUrl string, outM3U8 string, tmpl string, seglen_secs string, seg_start int) error { inp := C.CString(localRTMPUrl) outp := C.CString(outM3U8) @@ -128,7 +133,7 @@ func Transcode2(input *TranscodeOptionsIn, ps []TranscodeOptions) error { return err } -func Transcode3(input *TranscodeOptionsIn, ps []TranscodeOptions) ([]MediaInfo, error) { +func Transcode3(input *TranscodeOptionsIn, ps []TranscodeOptions) (*TranscodeResults, error) { if input == nil { return nil, ErrTranscoderInp } @@ -182,7 +187,8 @@ func Transcode3(input *TranscodeOptionsIn, ps []TranscodeOptions) ([]MediaInfo, } inp := &C.input_params{fname: fname, hw_type: hw_type, device: device} results := make([]C.output_results, len(ps)) - ret := int(C.lpms_transcode(inp, (*C.output_params)(¶ms[0]), (*C.output_results)(&results[0]), C.int(len(params)))) + decoded := &C.output_results{} + ret := int(C.lpms_transcode(inp, (*C.output_params)(¶ms[0]), (*C.output_results)(&results[0]), C.int(len(params)), decoded)) if 0 != ret { glog.Infof("Transcoder Return : %v\n", Strerror(ret)) return nil, ErrorMap[ret] @@ -194,7 +200,11 @@ func Transcode3(input *TranscodeOptionsIn, ps []TranscodeOptions) ([]MediaInfo, Pixels: int64(r.pixels), } } - return tr, nil + dec := MediaInfo{ + Frames: int(decoded.frames), + Pixels: int64(decoded.pixels), + } + return &TranscodeResults{Encoded: tr, Decoded: dec}, nil } func InitFFmpeg() { diff --git a/ffmpeg/ffmpeg_test.go b/ffmpeg/ffmpeg_test.go index 868eb200de..4ee3fb88f9 100644 --- a/ffmpeg/ffmpeg_test.go +++ b/ffmpeg/ffmpeg_test.go @@ -377,8 +377,97 @@ func TestTranscoder_Timestamp(t *testing.T) { run(cmd) } -func TestTranscoder_Statistics(t *testing.T) { - // Checks the stats returned after transcoding +func TestTranscoderStatistics_Decoded(t *testing.T) { + // Checks the decoded stats returned after transcoding + + var ( + totalPixels int64 + totalFrames int + ) + + run, dir := setupTest(t) + defer os.RemoveAll(dir) + + // segment using our muxer. This should produce 4 segments. + err := RTMPToHLS("../transcoder/test.ts", dir+"/test.m3u8", dir+"/test_%d.ts", "1", 0) + if err != nil { + t.Error(err) + } + + // Use various resolutions to test input + // Quickcheck style tests would be nice here one day? + profiles := []VideoProfile{P144p30fps16x9, P240p30fps16x9, P360p30fps16x9, P576p30fps16x9} + + // Transcode some data, save encoded statistics, then attempt to re-transcode + // Ensure decoded re-transcode stats match original transcoded statistics + for i, p := range profiles { + oname := fmt.Sprintf("%s/out_%d.ts", dir, i) + out := []TranscodeOptions{TranscodeOptions{Profile: p, Oname: oname}} + in := &TranscodeOptionsIn{Fname: fmt.Sprintf("%s/test_%d.ts", dir, i)} + res, err := Transcode3(in, out) + if err != nil { + t.Error(err) + } + info := res.Encoded[0] + + // Now attempt to re-encode the transcoded data + // and check decoded results from *that* + p.Framerate = 10 + in = &TranscodeOptionsIn{Fname: oname} + out = []TranscodeOptions{TranscodeOptions{Profile: p, Oname: "out.ts"}} + res, err = Transcode3(in, out) + if err != nil { + t.Error(err) + } + w, h, err := VideoProfileResolution(p) + if err != nil { + t.Error(err) + } + + // Check pixel counts + if info.Pixels != res.Decoded.Pixels { + t.Error("Mismatched pixel counts") + } + if info.Pixels != int64(w*h*res.Decoded.Frames) { + t.Error("Mismatched pixel counts") + } + // Check frame counts + if info.Frames != res.Decoded.Frames { + t.Error("Mismatched frame counts") + } + if info.Frames != int(res.Decoded.Pixels/int64(w*h)) { + t.Error("Mismatched frame counts") + } + totalPixels += info.Pixels + totalFrames += info.Frames + } + + // Now for something fun. Concatenate our segments of various resolutions + // Run them through the transcoder, and check the sum of pixels / frames match + // Ensures we can properly accommodate mid-stream resolution changes. + cmd := ` + set -eux + cd "$0" + cat out_0.ts out_1.ts out_2.ts out_3.ts > combined.ts + ` + run(cmd) + in := &TranscodeOptionsIn{Fname: dir + "/combined.ts"} + out := []TranscodeOptions{TranscodeOptions{Profile: P240p30fps16x9, Oname: "out.ts"}} + + res, err := Transcode3(in, out) + if err != nil { + t.Error(err) + } + if totalPixels != res.Decoded.Pixels { + t.Error("Mismatched total pixel counts") + } + if totalFrames != res.Decoded.Frames { + t.Errorf("Mismatched total frame counts - %d vs %d", totalFrames, res.Decoded.Frames) + } +} + +func TestTranscoder_Statistics_Encoded(t *testing.T) { + // Checks the encoded stats returned after transcoding run, dir := setupTest(t) defer os.RemoveAll(dir) @@ -412,7 +501,7 @@ func TestTranscoder_Statistics(t *testing.T) { t.Error(err) } - for i, r := range res { + for i, r := range res.Encoded { w, h, err := VideoProfileResolution(out[i].Profile) if err != nil { t.Error(err) @@ -481,10 +570,10 @@ func TestTranscoder_StatisticsAspectRatio(t *testing.T) { pAdj := VideoProfile{Resolution: "124x456", Framerate: 15, Bitrate: "100k"} out := []TranscodeOptions{TranscodeOptions{Profile: pAdj, Oname: dir + "/adj.mp4"}} res, err := Transcode3(&TranscodeOptionsIn{Fname: dir + "/test.ts"}, out) - if err != nil || len(res) <= 0 { + if err != nil || len(res.Encoded) <= 0 { t.Error(err) } - r := res[0] + r := res.Encoded[0] if r.Frames != int(pAdj.Framerate) || r.Pixels != int64(r.Frames*124*70) { t.Error(fmt.Errorf("Results did not match: %v ", r)) } diff --git a/ffmpeg/lpms_ffmpeg.c b/ffmpeg/lpms_ffmpeg.c index 3a25fa27ea..6d9ed96333 100644 --- a/ffmpeg/lpms_ffmpeg.c +++ b/ffmpeg/lpms_ffmpeg.c @@ -759,7 +759,7 @@ proc_cleanup: #define MAX_OUTPUT_SIZE 10 int lpms_transcode(input_params *inp, output_params *params, - output_results *results, int nb_outputs) + output_results *results, int nb_outputs, output_results *decoded_results) { #define main_err(msg) { \ if (!ret) ret = AVERROR(EINVAL); \ @@ -821,6 +821,11 @@ int lpms_transcode(input_params *inp, output_params *params, else if (ret < 0) main_err("transcoder: Could not decode; stopping\n"); ist = ictx.ic->streams[ipkt.stream_index]; + if (AVMEDIA_TYPE_VIDEO == ist->codecpar->codec_type) { + decoded_results->frames++; + decoded_results->pixels += dframe->width * dframe->height; + } + for (i = 0; i < nb_outputs; i++) { struct output_ctx *octx = &outputs[i]; struct filter_ctx *filter = NULL; diff --git a/ffmpeg/lpms_ffmpeg.h b/ffmpeg/lpms_ffmpeg.h index b5702cdf23..a4c942310c 100644 --- a/ffmpeg/lpms_ffmpeg.h +++ b/ffmpeg/lpms_ffmpeg.h @@ -31,6 +31,6 @@ typedef struct { void lpms_init(); int lpms_rtmp2hls(char *listen, char *outf, char *ts_tmpl, char *seg_time, char *seg_start); -int lpms_transcode(input_params *inp, output_params *params, output_results *results, int nb_outputs); +int lpms_transcode(input_params *inp, output_params *params, output_results *results, int nb_outputs, output_results *decoded_results); #endif // _LPMS_FFMPEG_H_