From aa14cc5a661022cbe52c4a3bfdd85e64748a17c6 Mon Sep 17 00:00:00 2001 From: Josh Allmann Date: Thu, 5 Sep 2024 00:54:49 +0000 Subject: [PATCH 1/2] ffmpeg: Add a metadata option to each output This will allow us to identify the software version and who transcoded a given segment. --- ffmpeg/encoder.c | 2 ++ ffmpeg/ffmpeg.go | 7 ++++++- ffmpeg/ffmpeg_test.go | 9 ++++++++- ffmpeg/filter.h | 1 + ffmpeg/transcoder.c | 1 + ffmpeg/transcoder.h | 1 + 6 files changed, 19 insertions(+), 2 deletions(-) diff --git a/ffmpeg/encoder.c b/ffmpeg/encoder.c index 3ce1ab4527..c930b1362b 100755 --- a/ffmpeg/encoder.c +++ b/ffmpeg/encoder.c @@ -286,6 +286,8 @@ int open_output(struct output_ctx *octx, struct input_ctx *ictx) if (ret < 0) LPMS_ERR(open_output_err, "Error opening output file"); } + if (octx->metadata) av_dict_copy(&oc->metadata, octx->metadata, 0); + ret = avformat_write_header(oc, &octx->muxer->opts); if (ret < 0) LPMS_ERR(open_output_err, "Error writing header"); diff --git a/ffmpeg/ffmpeg.go b/ffmpeg/ffmpeg.go index 76c743fdb0..201f6aa161 100755 --- a/ffmpeg/ffmpeg.go +++ b/ffmpeg/ffmpeg.go @@ -112,6 +112,7 @@ type TranscodeOptions struct { Muxer ComponentOptions VideoEncoder ComponentOptions AudioEncoder ComponentOptions + Metadata map[string]string } type MediaInfo struct { @@ -778,6 +779,7 @@ func createCOutputParams(input *TranscodeOptionsIn, ps []TranscodeOptions) ([]C. name: C.CString(audioEncoder), opts: newAVOpts(p.AudioEncoder.Opts), } + metadata := newAVOpts(p.Metadata) fromMs := int(p.From.Milliseconds()) toMs := int(p.To.Milliseconds()) vfilt := C.CString(filters) @@ -786,7 +788,7 @@ func createCOutputParams(input *TranscodeOptionsIn, ps []TranscodeOptions) ([]C. params[i] = C.output_params{fname: oname, fps: fps, w: C.int(w), h: C.int(h), bitrate: C.int(bitrate), gop_time: C.int(gopMs), from: C.int(fromMs), to: C.int(toMs), - muxer: muxOpts, audio: audioOpts, video: vidOpts, + muxer: muxOpts, audio: audioOpts, video: vidOpts, metadata: metadata, vfilters: vfilt, sfilters: nil, xcoderParams: xcoderOutParams} if p.CalcSign { //signfilter string @@ -841,6 +843,9 @@ func destroyCOutputParams(params []C.output_params) { if p.video.opts != nil { C.av_dict_free(&p.video.opts) } + if p.metadata != nil { + C.av_dict_free(&p.metadata) + } } } diff --git a/ffmpeg/ffmpeg_test.go b/ffmpeg/ffmpeg_test.go index 93e184b9cc..14bdd67da9 100644 --- a/ffmpeg/ffmpeg_test.go +++ b/ffmpeg/ffmpeg_test.go @@ -1628,6 +1628,9 @@ func TestTranscoder_FormatOptions(t *testing.T) { Oname: dir + "/test.flv", VideoEncoder: ComponentOptions{Name: "copy"}, AudioEncoder: ComponentOptions{Name: "copy"}, + Metadata: map[string]string{ + "encoded_by": "Livepeer Media Server", + }, }} if out[0].Profile.Format != FormatNone { t.Error("Expected empty profile for output option") @@ -1637,7 +1640,7 @@ func TestTranscoder_FormatOptions(t *testing.T) { t.Error(err) } cmd = ` - ffprobe -loglevel warning -show_format test.flv | grep format_name=flv + ffprobe -loglevel warning -show_format test.flv | grep 'format_name=flv\|encoded_by=Livepeer Media Server' ` run(cmd) @@ -1646,6 +1649,9 @@ func TestTranscoder_FormatOptions(t *testing.T) { out[0].Muxer = ComponentOptions{Name: "hls", Opts: map[string]string{ "hls_segment_filename": dir + "/test_segment_%d.ts", }} + out[0].Metadata = map[string]string{ + "service_provider": "Livepeer Media Server", + } _, err = Transcode3(in, out) if err != nil { t.Error(err) @@ -1660,6 +1666,7 @@ func TestTranscoder_FormatOptions(t *testing.T) { ffprobe -loglevel warning -show_entries format=format_name,duration test.ts > test.out diff -u segment.out test.out wc -l test.out | grep 4 # sanity check output file length + ffprobe segment.ts 2>&1 | grep 'service_provider: Livepeer Media Server' ` run(cmd) diff --git a/ffmpeg/filter.h b/ffmpeg/filter.h index 59d9e8c945..412f185d0f 100755 --- a/ffmpeg/filter.h +++ b/ffmpeg/filter.h @@ -65,6 +65,7 @@ struct output_ctx { component_opts *muxer; component_opts *video; component_opts *audio; + AVDictionary *metadata; int64_t drop_ts; // preroll audio ts to drop diff --git a/ffmpeg/transcoder.c b/ffmpeg/transcoder.c index dfc0807aa5..b27a7e02e5 100755 --- a/ffmpeg/transcoder.c +++ b/ffmpeg/transcoder.c @@ -216,6 +216,7 @@ int transcode_init(struct transcode_thread *h, input_params *inp, octx->muxer = ¶ms[i].muxer; octx->audio = ¶ms[i].audio; octx->video = ¶ms[i].video; + octx->metadata = params[i].metadata; octx->vfilters = params[i].vfilters; octx->sfilters = params[i].sfilters; octx->xcoderParams = params[i].xcoderParams; diff --git a/ffmpeg/transcoder.h b/ffmpeg/transcoder.h index 8c1c4888b4..e0cca743b4 100755 --- a/ffmpeg/transcoder.h +++ b/ffmpeg/transcoder.h @@ -35,6 +35,7 @@ typedef struct { component_opts muxer; component_opts audio; component_opts video; + AVDictionary *metadata; } output_params; typedef struct { From beda797c2cbd393eb65d81e503419835da06d5a4 Mon Sep 17 00:00:00 2001 From: Josh Allmann Date: Fri, 6 Sep 2024 03:55:51 +0000 Subject: [PATCH 2/2] Also set metadata when reopening only the muxer under nvenc. --- ffmpeg/encoder.c | 2 ++ ffmpeg/ffmpeg_test.go | 44 +++++++++++++++++++++++++++++++++++++++++++ ffmpeg/nvidia_test.go | 5 +++++ 3 files changed, 51 insertions(+) diff --git a/ffmpeg/encoder.c b/ffmpeg/encoder.c index c930b1362b..0620ff02e2 100755 --- a/ffmpeg/encoder.c +++ b/ffmpeg/encoder.c @@ -326,6 +326,8 @@ int reopen_output(struct output_ctx *octx, struct input_ctx *ictx) ret = avio_open(&octx->oc->pb, octx->fname, AVIO_FLAG_WRITE); if (ret < 0) LPMS_ERR(reopen_out_err, "Error re-opening output file"); } + + if (octx->metadata) av_dict_copy(&octx->oc->metadata, octx->metadata, 0); ret = avformat_write_header(octx->oc, &octx->muxer->opts); if (ret < 0) LPMS_ERR(reopen_out_err, "Error re-writing header"); diff --git a/ffmpeg/ffmpeg_test.go b/ffmpeg/ffmpeg_test.go index 14bdd67da9..1b3f1e6280 100644 --- a/ffmpeg/ffmpeg_test.go +++ b/ffmpeg/ffmpeg_test.go @@ -1759,6 +1759,50 @@ func TestTranscoder_FormatOptions(t *testing.T) { } } +func TestTranscoder_Metadata(t *testing.T) { + runTestTranscoder_Metadata(t, Software) +} + +func runTestTranscoder_Metadata(t *testing.T, accel Acceleration) { + // check that metadata is there in all segments + run, dir := setupTest(t) + defer os.RemoveAll(dir) + + err := RTMPToHLS("../transcoder/test.ts", dir+"/in.m3u8", dir+"/in_%d.ts", "2", 0) + if err != nil { + t.Error(err) + } + tc := NewTranscoder() + defer tc.StopTranscoder() + for i := 0; i < 4; i++ { + in := &TranscodeOptionsIn{ + Fname: fmt.Sprintf("%s/in_%d.ts", dir, i), + Accel: accel, + } + out := []TranscodeOptions{{ + Accel: accel, + Oname: fmt.Sprintf("%s/out_%d.ts", dir, i), + Profile: P144p30fps16x9, + Metadata: map[string]string{ + "service_name": fmt.Sprintf("lpms-test-%d", i), + }, + }} + _, err := tc.Transcode(in, out) + if err != nil { + t.Error(err) + } + } + + cmd := ` + ffprobe -hide_banner -i out_1.ts + ffprobe -i out_0.ts 2>&1 | grep 'service_name : lpms-test-0' + ffprobe -i out_1.ts 2>&1 | grep 'service_name : lpms-test-1' + ffprobe -i out_2.ts 2>&1 | grep 'service_name : lpms-test-2' + ffprobe -i out_3.ts 2>&1 | grep 'service_name : lpms-test-3' + ` + run(cmd) +} + func TestTranscoder_IgnoreUnknown(t *testing.T) { run, dir := setupTest(t) defer os.RemoveAll(dir) diff --git a/ffmpeg/nvidia_test.go b/ffmpeg/nvidia_test.go index 86c0f76a88..0206c98285 100755 --- a/ffmpeg/nvidia_test.go +++ b/ffmpeg/nvidia_test.go @@ -801,3 +801,8 @@ func TestNvidia_DiscontinuityAudioSegment(t *testing.T) { func TestNvidia_Rotation(t *testing.T) { runRotationTests(t, Nvidia) } + +func TestNvidia_Metadata(t *testing.T) { + // with nvenc we reopen the outputs so exercise that + runTestTranscoder_Metadata(t, Nvidia) +}