diff --git a/ffmpeg/parse/parser.go b/ffmpeg/parse/parser.go index 1089378b..9ad3f4c4 100644 --- a/ffmpeg/parse/parser.go +++ b/ffmpeg/parse/parser.go @@ -204,7 +204,9 @@ func (p *parser) Parse(line string) uint64 { isDefaultProgress := strings.HasPrefix(line, "frame=") isFFmpegInputs := strings.HasPrefix(line, "ffmpeg.inputs:") isFFmpegOutputs := strings.HasPrefix(line, "ffmpeg.outputs:") + isFFmpegMapping := strings.HasPrefix(line, "ffmpeg.mapping:") isFFmpegProgress := strings.HasPrefix(line, "ffmpeg.progress:") + isHLSStreamMap := strings.HasPrefix(line, "hls.streammap:") isAVstreamProgress := strings.HasPrefix(line, "avstream.progress:") p.lock.log.Lock() @@ -233,7 +235,7 @@ func (p *parser) Parse(line string) uint64 { } if isFFmpegInputs { - if err := p.parseIO("input", strings.TrimPrefix(line, "ffmpeg.inputs:")); err != nil { + if err := p.parseFFmpegIO("input", strings.TrimPrefix(line, "ffmpeg.inputs:")); err != nil { p.logger.WithFields(log.Fields{ "line": line, "error": err, @@ -243,8 +245,17 @@ func (p *parser) Parse(line string) uint64 { return 0 } + if isHLSStreamMap { + if err := p.parseHLSStreamMap(strings.TrimPrefix(line, "hls.streammap:")); err != nil { + p.logger.WithFields(log.Fields{ + "line": line, + "error": err, + }).Error().Log("Failed parsing HSL stream mapping") + } + } + if isFFmpegOutputs { - if err := p.parseIO("output", strings.TrimPrefix(line, "ffmpeg.outputs:")); err != nil { + if err := p.parseFFmpegIO("output", strings.TrimPrefix(line, "ffmpeg.outputs:")); err != nil { p.logger.WithFields(log.Fields{ "line": line, "error": err, @@ -273,6 +284,17 @@ func (p *parser) Parse(line string) uint64 { } } + if isFFmpegMapping { + if err := p.parseFFmpegMapping(strings.TrimPrefix(line, "ffmpeg.mapping:")); err != nil { + p.logger.WithFields(log.Fields{ + "line": line, + "error": err, + }).Error().Log("Failed parsing mapping") + } + + return 0 + } + if !isDefaultProgress && !isFFmpegProgress && !isAVstreamProgress { // Write the current non-progress line to the log p.addLog(line) @@ -491,7 +513,7 @@ func (p *parser) parseDefaultProgress(line string) error { return nil } -func (p *parser) parseIO(kind, line string) error { +func (p *parser) parseFFmpegIO(kind, line string) error { processIO := []ffmpegProcessIO{} err := json.Unmarshal([]byte(line), &processIO) @@ -518,6 +540,32 @@ func (p *parser) parseIO(kind, line string) error { return nil } +func (p *parser) parseFFmpegMapping(line string) error { + mapping := ffmpegStreamMapping{} + + err := json.Unmarshal([]byte(line), &mapping) + if err != nil { + return err + } + + p.process.mapping = mapping + + return nil +} + +func (p *parser) parseHLSStreamMap(line string) error { + mapping := ffmpegHLSStreamMap{} + + err := json.Unmarshal([]byte(line), &mapping) + if err != nil { + return err + } + + p.process.hlsMapping = &mapping + + return nil +} + func (p *parser) parseFFmpegProgress(line string) error { progress := ffmpegProgress{} diff --git a/ffmpeg/parse/parser_test.go b/ffmpeg/parse/parser_test.go index 581c918d..88890288 100644 --- a/ffmpeg/parse/parser_test.go +++ b/ffmpeg/parse/parser_test.go @@ -922,6 +922,165 @@ func TestParserProgressPlayout(t *testing.T) { }, progress) } +func TestParserStreamMapping(t *testing.T) { + parser := New(Config{ + LogLines: 20, + }).(*parser) + + parser.Parse(`ffmpeg.inputs:[{"url":"https://cdn.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8","format":"hls","index":0,"stream":0,"type":"video","codec":"h264","coder":"h264","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":25.000000,"pix_fmt":"yuvj420p","width":1280,"height":720},{"url":"https://cdn.livespotting.com/vpu/e9slfpe3/z60wzayk_1440.m3u8","format":"hls","index":1,"stream":0,"type":"video","codec":"h264","coder":"h264","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":25.000000,"pix_fmt":"yuvj420p","width":2560,"height":1440},{"url":"anullsrc=r=44100:cl=mono","format":"lavfi","index":2,"stream":0,"type":"audio","codec":"pcm_u8","coder":"pcm_u8","bitrate_kbps":352,"duration_sec":0.000000,"language":"und","sampling_hz":44100,"layout":"mono","channels":1}]`) + parser.Parse(`hls.streammap:{"address":"http://127.0.0.1:8080/memfs/live/%v.m3u8","variants":[{"variant":0,"address":"http://127.0.0.1:8080/memfs/live/0.m3u8","streams":[0,2]},{"variant":1,"address":"http://127.0.0.1:8080/memfs/live/1.m3u8","streams":[1,3]}]}`) + parser.Parse(`ffmpeg.outputs:[{"url":"http://127.0.0.1:8080/memfs/live/%v.m3u8","format":"hls","index":0,"stream":0,"type":"video","codec":"h264","coder":"copy","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":25.000000,"pix_fmt":"yuvj420p","width":1280,"height":720},{"url":"http://127.0.0.1:8080/memfs/live/%v.m3u8","format":"hls","index":0,"stream":1,"type":"video","codec":"h264","coder":"copy","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":25.000000,"pix_fmt":"yuvj420p","width":2560,"height":1440},{"url":"http://127.0.0.1:8080/memfs/live/%v.m3u8","format":"hls","index":0,"stream":2,"type":"audio","codec":"aac","coder":"aac","bitrate_kbps":69,"duration_sec":0.000000,"language":"und","sampling_hz":44100,"layout":"mono","channels":1},{"url":"http://127.0.0.1:8080/memfs/live/%v.m3u8","format":"hls","index":0,"stream":3,"type":"audio","codec":"aac","coder":"aac","bitrate_kbps":69,"duration_sec":0.000000,"language":"und","sampling_hz":44100,"layout":"mono","channels":1}]`) + parser.Parse(`ffmpeg.mapping:{"graphs":[{"index":0,"graph":[{"src_name":"Parsed_null_0","src_filter":"null","dst_name":"format","dst_filter":"format","inpad":"default","outpad":"default","timebase": "1/90000","type":"video","format":"yuvj420p","width":1280,"height":720},{"src_name":"graph 0 input from stream 0:0","src_filter":"buffer","dst_name":"Parsed_null_0","dst_filter":"null","inpad":"default","outpad":"default","timebase": "1/90000","type":"video","format":"yuvj420p","width":1280,"height":720},{"src_name":"format","src_filter":"format","dst_name":"out_0_0","dst_filter":"buffersink","inpad":"default","outpad":"default","timebase": "1/90000","type":"video","format":"yuvj420p","width":1280,"height":720}]},{"index":1,"graph":[{"src_name":"Parsed_anull_0","src_filter":"anull","dst_name":"auto_aresample_0","dst_filter":"aresample","inpad":"default","outpad":"default","timebase": "1/44100","type":"audio","format":"u8","sampling_hz":44100,"layout":"mono"},{"src_name":"graph_1_in_2_0","src_filter":"abuffer","dst_name":"Parsed_anull_0","dst_filter":"anull","inpad":"default","outpad":"default","timebase": "1/44100","type":"audio","format":"u8","sampling_hz":44100,"layout":"mono"},{"src_name":"format_out_0_2","src_filter":"aformat","dst_name":"out_0_2","dst_filter":"abuffersink","inpad":"default","outpad":"default","timebase": "1/44100","type":"audio","format":"fltp","sampling_hz":44100,"layout":"mono"},{"src_name":"auto_aresample_0","src_filter":"aresample","dst_name":"format_out_0_2","dst_filter":"aformat","inpad":"default","outpad":"default","timebase": "1/44100","type":"audio","format":"fltp","sampling_hz":44100,"layout":"mono"}]},{"index":2,"graph":[{"src_name":"Parsed_anull_0","src_filter":"anull","dst_name":"auto_aresample_0","dst_filter":"aresample","inpad":"default","outpad":"default","timebase": "1/44100","type":"audio","format":"u8","sampling_hz":44100,"layout":"mono"},{"src_name":"graph_2_in_2_0","src_filter":"abuffer","dst_name":"Parsed_anull_0","dst_filter":"anull","inpad":"default","outpad":"default","timebase": "1/44100","type":"audio","format":"u8","sampling_hz":44100,"layout":"mono"},{"src_name":"format_out_0_3","src_filter":"aformat","dst_name":"out_0_3","dst_filter":"abuffersink","inpad":"default","outpad":"default","timebase": "1/44100","type":"audio","format":"fltp","sampling_hz":44100,"layout":"mono"},{"src_name":"auto_aresample_0","src_filter":"aresample","dst_name":"format_out_0_3","dst_filter":"aformat","inpad":"default","outpad":"default","timebase": "1/44100","type":"audio","format":"fltp","sampling_hz":44100,"layout":"mono"}]}],"mapping":[{"input":{"index":0,"stream":0},"graph":{"index":0,"name":"graph 0 input from stream 0:0"},"output":null},{"input":{"index":2,"stream":0},"graph":{"index":1,"name":"graph_1_in_2_0"},"output":null},{"input":{"index":2,"stream":0},"graph":{"index":2,"name":"graph_2_in_2_0"},"output":null},{"input":null,"graph":{"index":0,"name":"out_0_0"},"output":{"index":0,"stream":0}},{"input":{"index":1,"stream":0},"output":{"index":0,"stream":1},"copy":true},{"input":null,"graph":{"index":1,"name":"out_0_2"},"output":{"index":0,"stream":2}},{"input":null,"graph":{"index":2,"name":"out_0_3"},"output":{"index":0,"stream":3}}]}`) + parser.Parse(`ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"framerate":{"min":24.975,"max":24.975,"avg":24.975},"frame":149,"keyframe":3,"packet":149,"size_kb":1467,"size_bytes":1501854},{"index":1,"stream":0,"framerate":{"min":24.975,"max":24.975,"avg":24.975},"frame":149,"keyframe":3,"packet":149,"size_kb":4428,"size_bytes":4534541},{"index":2,"stream":0,"framerate":{"min":43.066,"max":43.068,"avg":43.066},"frame":257,"keyframe":257,"packet":257,"size_kb":257,"size_bytes":263168}],"outputs":[{"index":0,"stream":0,"frame":149,"keyframe":3,"packet":149,"q":-1.0,"size_kb":1467,"size_bytes":1501923,"extradata_size_bytes":69},{"index":0,"stream":1,"frame":149,"keyframe":3,"packet":149,"q":-1.0,"size_kb":4428,"size_bytes":4534612,"extradata_size_bytes":71},{"index":0,"stream":2,"frame":257,"keyframe":256,"packet":256,"size_kb":1,"size_bytes":1046,"extradata_size_bytes":5},{"index":0,"stream":3,"frame":257,"keyframe":256,"packet":256,"size_kb":1,"size_bytes":1046,"extradata_size_bytes":5}],"frame":149,"packet":149,"q":-1.0,"size_kb":5897,"size_bytes":6038627,"time":"0h0m5.96s","speed":4.79,"dup":0,"drop":0}`) + + progress := parser.Progress() + + require.Equal(t, 3, len(progress.Input)) + require.Equal(t, 4, len(progress.Output)) + + require.Equal(t, StreamMapping{ + Graphs: []Graph{ + { + Index: 0, + Graph: []GraphElement{ + {SrcName: "Parsed_null_0", SrcFilter: "null", DstName: "format", DstFilter: "format", Inpad: "default", Outpad: "default", Timebase: "1/90000", Type: "video", Format: "yuvj420p", Sampling: 0, Layout: "", Width: 1280, Height: 720}, + {SrcName: "graph 0 input from stream 0:0", SrcFilter: "buffer", DstName: "Parsed_null_0", DstFilter: "null", Inpad: "default", Outpad: "default", Timebase: "1/90000", Type: "video", Format: "yuvj420p", Sampling: 0, Layout: "", Width: 1280, Height: 720}, + {SrcName: "format", SrcFilter: "format", DstName: "out_0_0", DstFilter: "buffersink", Inpad: "default", Outpad: "default", Timebase: "1/90000", Type: "video", Format: "yuvj420p", Sampling: 0, Layout: "", Width: 1280, Height: 720}, + }, + }, { + Index: 1, + Graph: []GraphElement{ + {SrcName: "Parsed_anull_0", SrcFilter: "anull", DstName: "auto_aresample_0", DstFilter: "aresample", Inpad: "default", Outpad: "default", Timebase: "1/44100", Type: "audio", Format: "u8", Sampling: 44100, Layout: "mono", Width: 0, Height: 0}, + {SrcName: "graph_1_in_2_0", SrcFilter: "abuffer", DstName: "Parsed_anull_0", DstFilter: "anull", Inpad: "default", Outpad: "default", Timebase: "1/44100", Type: "audio", Format: "u8", Sampling: 44100, Layout: "mono", Width: 0, Height: 0}, + {SrcName: "format_out_0_2", SrcFilter: "aformat", DstName: "out_0_2", DstFilter: "abuffersink", Inpad: "default", Outpad: "default", Timebase: "1/44100", Type: "audio", Format: "fltp", Sampling: 44100, Layout: "mono", Width: 0, Height: 0}, + {SrcName: "auto_aresample_0", SrcFilter: "aresample", DstName: "format_out_0_2", DstFilter: "aformat", Inpad: "default", Outpad: "default", Timebase: "1/44100", Type: "audio", Format: "fltp", Sampling: 44100, Layout: "mono", Width: 0, Height: 0}, + }, + }, { + Index: 2, + Graph: []GraphElement{ + {SrcName: "Parsed_anull_0", SrcFilter: "anull", DstName: "auto_aresample_0", DstFilter: "aresample", Inpad: "default", Outpad: "default", Timebase: "1/44100", Type: "audio", Format: "u8", Sampling: 44100, Layout: "mono", Width: 0, Height: 0}, + {SrcName: "graph_2_in_2_0", SrcFilter: "abuffer", DstName: "Parsed_anull_0", DstFilter: "anull", Inpad: "default", Outpad: "default", Timebase: "1/44100", Type: "audio", Format: "u8", Sampling: 44100, Layout: "mono", Width: 0, Height: 0}, + {SrcName: "format_out_0_3", SrcFilter: "aformat", DstName: "out_0_3", DstFilter: "abuffersink", Inpad: "default", Outpad: "default", Timebase: "1/44100", Type: "audio", Format: "fltp", Sampling: 44100, Layout: "mono", Width: 0, Height: 0}, + {SrcName: "auto_aresample_0", SrcFilter: "aresample", DstName: "format_out_0_3", DstFilter: "aformat", Inpad: "default", Outpad: "default", Timebase: "1/44100", Type: "audio", Format: "fltp", Sampling: 44100, Layout: "mono", Width: 0, Height: 0}, + }, + }, + }, + Mapping: []Mapping{ + { + Input: 0, + Output: -1, + Graph: MappingGraph{ + Index: 0, + Name: "graph 0 input from stream 0:0", + }, + Copy: false, + }, + { + Input: 2, + Output: -1, + Graph: MappingGraph{ + Index: 1, + Name: "graph_1_in_2_0", + }, + Copy: false, + }, + { + Input: 2, + Output: -1, + Graph: MappingGraph{ + Index: 2, + Name: "graph_2_in_2_0", + }, + Copy: false, + }, + { + Input: -1, + Output: 0, + Graph: MappingGraph{ + Index: 0, + Name: "out_0_0", + }, + Copy: false, + }, + { + Input: 1, + Output: 1, + Graph: MappingGraph{ + Index: -1, + Name: "", + }, + Copy: true, + }, + { + Input: -1, + Output: 2, + Graph: MappingGraph{ + Index: 1, + Name: "out_0_2", + }, + Copy: false, + }, + { + Input: -1, + Output: 3, + Graph: MappingGraph{ + Index: 2, + Name: "out_0_3", + }, + Copy: false, + }, + }, + }, progress.Mapping) + + require.Equal(t, "http://127.0.0.1:8080/memfs/live/0.m3u8", progress.Output[0].Address) + require.Equal(t, uint64(0), progress.Output[0].Index) + require.Equal(t, uint64(0), progress.Output[0].Stream) + + require.Equal(t, "http://127.0.0.1:8080/memfs/live/1.m3u8", progress.Output[1].Address) + require.Equal(t, uint64(1), progress.Output[1].Index) + require.Equal(t, uint64(0), progress.Output[1].Stream) + + require.Equal(t, "http://127.0.0.1:8080/memfs/live/0.m3u8", progress.Output[2].Address) + require.Equal(t, uint64(0), progress.Output[2].Index) + require.Equal(t, uint64(1), progress.Output[2].Stream) + + require.Equal(t, "http://127.0.0.1:8080/memfs/live/1.m3u8", progress.Output[3].Address) + require.Equal(t, uint64(1), progress.Output[3].Index) + require.Equal(t, uint64(1), progress.Output[3].Stream) +} + +func TestParserHLSMapping(t *testing.T) { + parser := New(Config{ + LogLines: 20, + }).(*parser) + + parser.Parse(`ffmpeg.inputs:[{"url":"https://cdn.livespotting.com/vpu/e9slfpe3/z60wzayk_720.m3u8","format":"hls","index":0,"stream":0,"type":"video","codec":"h264","coder":"h264","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":25.000000,"pix_fmt":"yuvj420p","width":1280,"height":720},{"url":"https://cdn.livespotting.com/vpu/e9slfpe3/z60wzayk_1440.m3u8","format":"hls","index":1,"stream":0,"type":"video","codec":"h264","coder":"h264","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":25.000000,"pix_fmt":"yuvj420p","width":2560,"height":1440},{"url":"anullsrc=r=44100:cl=mono","format":"lavfi","index":2,"stream":0,"type":"audio","codec":"pcm_u8","coder":"pcm_u8","bitrate_kbps":352,"duration_sec":0.000000,"language":"und","sampling_hz":44100,"layout":"mono","channels":1}]`) + parser.Parse(`hls.streammap:{"address":"http://127.0.0.1:8080/memfs/live/%v.m3u8","variants":[{"variant":0,"address":"http://127.0.0.1:8080/memfs/live/0.m3u8","streams":[0,2]},{"variant":1,"address":"http://127.0.0.1:8080/memfs/live/1.m3u8","streams":[1,3]}]}`) + parser.Parse(`ffmpeg.outputs:[{"url":"http://127.0.0.1:8080/memfs/live/%v.m3u8","format":"hls","index":0,"stream":0,"type":"video","codec":"h264","coder":"copy","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":25.000000,"pix_fmt":"yuvj420p","width":1280,"height":720},{"url":"http://127.0.0.1:8080/memfs/live/%v.m3u8","format":"hls","index":0,"stream":1,"type":"video","codec":"h264","coder":"copy","bitrate_kbps":0,"duration_sec":0.000000,"language":"und","fps":25.000000,"pix_fmt":"yuvj420p","width":2560,"height":1440},{"url":"http://127.0.0.1:8080/memfs/live/%v.m3u8","format":"hls","index":0,"stream":2,"type":"audio","codec":"aac","coder":"aac","bitrate_kbps":69,"duration_sec":0.000000,"language":"und","sampling_hz":44100,"layout":"mono","channels":1},{"url":"http://127.0.0.1:8080/memfs/live/%v.m3u8","format":"hls","index":0,"stream":3,"type":"audio","codec":"aac","coder":"aac","bitrate_kbps":69,"duration_sec":0.000000,"language":"und","sampling_hz":44100,"layout":"mono","channels":1}]`) + parser.Parse(`ffmpeg.progress:{"inputs":[{"index":0,"stream":0,"framerate":{"min":24.975,"max":24.975,"avg":24.975},"frame":149,"keyframe":3,"packet":149,"size_kb":1467,"size_bytes":1501854},{"index":1,"stream":0,"framerate":{"min":24.975,"max":24.975,"avg":24.975},"frame":149,"keyframe":3,"packet":149,"size_kb":4428,"size_bytes":4534541},{"index":2,"stream":0,"framerate":{"min":43.066,"max":43.068,"avg":43.066},"frame":257,"keyframe":257,"packet":257,"size_kb":257,"size_bytes":263168}],"outputs":[{"index":0,"stream":0,"frame":149,"keyframe":3,"packet":149,"q":-1.0,"size_kb":1467,"size_bytes":1501923,"extradata_size_bytes":69},{"index":0,"stream":1,"frame":149,"keyframe":3,"packet":149,"q":-1.0,"size_kb":4428,"size_bytes":4534612,"extradata_size_bytes":71},{"index":0,"stream":2,"frame":257,"keyframe":256,"packet":256,"size_kb":1,"size_bytes":1046,"extradata_size_bytes":5},{"index":0,"stream":3,"frame":257,"keyframe":256,"packet":256,"size_kb":1,"size_bytes":1046,"extradata_size_bytes":5}],"frame":149,"packet":149,"q":-1.0,"size_kb":5897,"size_bytes":6038627,"time":"0h0m5.96s","speed":4.79,"dup":0,"drop":0}`) + + progress := parser.Progress() + + require.Equal(t, 3, len(progress.Input)) + require.Equal(t, 4, len(progress.Output)) + + require.Equal(t, "http://127.0.0.1:8080/memfs/live/0.m3u8", progress.Output[0].Address) + require.Equal(t, uint64(0), progress.Output[0].Index) + require.Equal(t, uint64(0), progress.Output[0].Stream) + + require.Equal(t, "http://127.0.0.1:8080/memfs/live/1.m3u8", progress.Output[1].Address) + require.Equal(t, uint64(1), progress.Output[1].Index) + require.Equal(t, uint64(0), progress.Output[1].Stream) + + require.Equal(t, "http://127.0.0.1:8080/memfs/live/0.m3u8", progress.Output[2].Address) + require.Equal(t, uint64(0), progress.Output[2].Index) + require.Equal(t, uint64(1), progress.Output[2].Stream) + + require.Equal(t, "http://127.0.0.1:8080/memfs/live/1.m3u8", progress.Output[3].Address) + require.Equal(t, uint64(1), progress.Output[3].Index) + require.Equal(t, uint64(1), progress.Output[3].Stream) +} + func TestParserPatterns(t *testing.T) { p := New(Config{ LogHistory: 3, diff --git a/ffmpeg/parse/types.go b/ffmpeg/parse/types.go index 83be8970..9ea7d40d 100644 --- a/ffmpeg/parse/types.go +++ b/ffmpeg/parse/types.go @@ -115,8 +115,6 @@ type ffmpegProgressIO struct { } func (io *ffmpegProgressIO) exportTo(progress *ProgressIO) { - progress.Index = io.Index - progress.Stream = io.Stream progress.Frame = io.Frame progress.Keyframe = io.Keyframe progress.Framerate.Min = io.Framerate.Min @@ -228,9 +226,132 @@ func (io *ffmpegProcessIO) export() ProgressIO { } } +type ffmpegGraphElement struct { + SrcName string `json:"src_name"` + SrcFilter string `json:"src_filter"` + DstName string `json:"dst_name"` + DstFilter string `json:"dst_filter"` + Inpad string `json:"inpad"` + Outpad string `json:"outpad"` + Timebase string `json:"timebase"` + Type string `json:"type"` + Format string `json:"format"` + Sampling uint64 `json:"sampling_hz"` + Layout string `json:"layout"` + Width uint64 `json:"width"` + Height uint64 `json:"height"` +} + +func (f *ffmpegGraphElement) Export() GraphElement { + return GraphElement{ + SrcName: f.SrcName, + SrcFilter: f.SrcFilter, + DstName: f.DstName, + DstFilter: f.DstFilter, + Inpad: f.Inpad, + Outpad: f.Outpad, + Timebase: f.Timebase, + Type: f.Type, + Format: f.Format, + Sampling: f.Sampling, + Layout: f.Layout, + Width: f.Width, + Height: f.Height, + } +} + +type ffmpegGraph struct { + Index uint64 `json:"index"` + Graph []ffmpegGraphElement `json:"graph"` +} + +func (f *ffmpegGraph) Export() Graph { + g := Graph{ + Index: f.Index, + } + + for _, e := range f.Graph { + g.Graph = append(g.Graph, e.Export()) + } + + return g +} + +type ffmpegMapping struct { + Input *ffmpegMappingIO `json:"input"` + Output *ffmpegMappingIO `json:"output"` + Graph struct { + Index uint64 `json:"index"` + Name string `json:"name"` + } `json:"graph"` + Copy bool `json:"copy"` +} + +type ffmpegMappingIO struct { + Index uint64 `json:"index"` + Stream uint64 `json:"stream"` +} + +type ffmpegStreamMapping struct { + Graphs []ffmpegGraph `json:"graphs"` + Mapping []ffmpegMapping `json:"mapping"` +} + type ffmpegProcess struct { - input []ffmpegProcessIO - output []ffmpegProcessIO + input []ffmpegProcessIO + output []ffmpegProcessIO + mapping ffmpegStreamMapping + hlsMapping *ffmpegHLSStreamMap +} + +func (f *ffmpegProcess) ExportMapping() StreamMapping { + sm := StreamMapping{} + + for _, g := range f.mapping.Graphs { + sm.Graphs = append(sm.Graphs, g.Export()) + } + + for _, fm := range f.mapping.Mapping { + m := Mapping{ + Input: -1, + Output: -1, + Graph: MappingGraph{ + Index: int(fm.Graph.Index), + Name: fm.Graph.Name, + }, + Copy: fm.Copy, + } + + if len(m.Graph.Name) == 0 { + m.Graph.Index = -1 + } + + if fm.Input != nil { + for i, in := range f.input { + if in.Index != fm.Input.Index || in.Stream != fm.Input.Stream { + continue + } + + m.Input = i + break + } + } + + if fm.Output != nil { + for i, out := range f.output { + if out.Index != fm.Output.Index || out.Stream != fm.Output.Stream { + continue + } + + m.Output = i + break + } + } + + sm.Mapping = append(sm.Mapping, m) + } + + return sm } func (p *ffmpegProcess) export() Progress { @@ -248,9 +369,40 @@ func (p *ffmpegProcess) export() Progress { progress.Output = append(progress.Output, aio) } + progress.Mapping = p.ExportMapping() + + if p.hlsMapping != nil { + for _, variant := range p.hlsMapping.Variants { + for s, stream := range variant.Streams { + if stream >= len(progress.Output) { + continue + } + + output := progress.Output[stream] + + output.Address = variant.Address + output.Index = variant.Variant + output.Stream = uint64(s) + + progress.Output[stream] = output + } + } + } + return progress } +type ffmpegHLSStreamMap struct { + Address string `json:"address"` + Variants []ffmpegHLSVariant `json:"variants"` +} + +type ffmpegHLSVariant struct { + Variant uint64 `json:"variant"` + Address string `json:"address"` + Streams []int `json:"streams"` +} + type ProgressIO struct { Address string @@ -293,6 +445,7 @@ type ProgressIO struct { type Progress struct { Input []ProgressIO Output []ProgressIO + Mapping StreamMapping Frame uint64 Packet uint64 FPS float64 @@ -341,3 +494,41 @@ type Usage struct { Limit uint64 } } + +type GraphElement struct { + SrcName string + SrcFilter string + DstName string + DstFilter string + Inpad string + Outpad string + Timebase string + Type string // audio or video + Format string + Sampling uint64 // Hz + Layout string + Width uint64 + Height uint64 +} + +type Graph struct { + Index uint64 + Graph []GraphElement +} + +type MappingGraph struct { + Index int + Name string +} + +type Mapping struct { + Input int + Output int + Graph MappingGraph + Copy bool +} + +type StreamMapping struct { + Graphs []Graph + Mapping []Mapping +}