diff --git a/doc/DEV_LOG.md b/doc/DEV_LOG.md index e4a5dec..b652eef 100644 --- a/doc/DEV_LOG.md +++ b/doc/DEV_LOG.md @@ -28,6 +28,7 @@ go donutEngine.Stream( ``` ref https://wiki.xiph.org/Opus_Recommended_Settings 48000 webrtc +ref https://ffmpeg.org/ffmpeg-codecs.html#libopus-1 opus ## Date: 2/4/24 ### Summary: Adding audio track diff --git a/internal/controllers/engine/donut_engine_controller.go b/internal/controllers/engine/donut_engine_controller.go index 5a8cbb0..ea3860b 100644 --- a/internal/controllers/engine/donut_engine_controller.go +++ b/internal/controllers/engine/donut_engine_controller.go @@ -12,7 +12,7 @@ import ( type DonutEngine interface { Prober() probers.DonutProber Streamer() streamers.DonutStreamer - CompatibleStreamsFor(server, client *entities.StreamInfo) ([]entities.Stream, bool) + RecipeFor(server, client *entities.StreamInfo) *entities.DonutTransformRecipe } type DonutEngineParams struct { @@ -79,7 +79,22 @@ func (d *donutEngine) Streamer() streamers.DonutStreamer { return d.streamer } -func (d *donutEngine) CompatibleStreamsFor(server, client *entities.StreamInfo) ([]entities.Stream, bool) { +func (d *donutEngine) RecipeFor(server, client *entities.StreamInfo) *entities.DonutTransformRecipe { // TODO: implement proper matching - return server.Streams, true + r := &entities.DonutTransformRecipe{ + Video: entities.DonutMediaTask{ + Action: entities.DonutBypass, + }, + Audio: entities.DonutMediaTask{ + Action: entities.DonutTranscode, + Codec: entities.Opus, + // TODO: create method list options per Codec + CodecContextOptions: []entities.LibAVOptionsCodecContext{ + // opus specifically works under 48000 Hz + entities.SetSampleRate(48000), + }, + }, + } + + return r } diff --git a/internal/controllers/streamers/libav_ffmpeg.go b/internal/controllers/streamers/libav_ffmpeg.go index bef7dc7..4c470cc 100644 --- a/internal/controllers/streamers/libav_ffmpeg.go +++ b/internal/controllers/streamers/libav_ffmpeg.go @@ -51,13 +51,19 @@ func (c *LibAVFFmpegStreamer) Match(req *entities.RequestParams) bool { } type streamContext struct { + // IN inputStream *astiav.Stream decCodec *astiav.Codec decCodecContext *astiav.CodecContext decFrame *astiav.Frame + + // OUT + encCodec *astiav.Codec + encCodecContext *astiav.CodecContext + encPkt *astiav.Packet } -type params struct { +type libAVParams struct { inputFormatContext *astiav.FormatContext streams map[int]*streamContext } @@ -68,17 +74,36 @@ func (c *LibAVFFmpegStreamer) Stream(donut *entities.DonutParameters) { closer := astikit.NewCloser() defer closer.Close() - p := ¶ms{ + p := &libAVParams{ streams: make(map[int]*streamContext), } + // it's really useful for debugging + astiav.SetLogLevel(astiav.LogLevelDebug) + astiav.SetLogCallback(func(l astiav.LogLevel, fmt, msg, parent string) { + c.l.Infof("ffmpeg %s: - %s", c.libAVLogToString(l), strings.TrimSpace(msg)) + }) + if err := c.prepareInput(p, closer, donut); err != nil { c.onError(err, donut) return } - pkt := astiav.AllocPacket() - closer.Add(pkt.Free) + // the audio codec opus expects 48000 (for webrtc), therefore filters are needed + // so one can upscale 44100 to 48000 frames/samples through filters + // https://ffmpeg.org/ffmpeg-filters.html#aformat + // https://ffmpeg.org/ffmpeg-filters.html#aresample-1 + // https://github.com/FFmpeg/FFmpeg/blob/8b6219a99d80cabf87c50170c009fe93092e32bd/doc/examples/resample_audio.c#L133 + // https://github.com/FFmpeg/FFmpeg/blob/8b6219a99d80cabf87c50170c009fe93092e32bd/doc/examples/mux.c#L295 + // ffmpeg error: more samples than frame size + + if err := c.prepareOutput(p, closer, donut); err != nil { + c.onError(err, donut) + return + } + + inPkt := astiav.AllocPacket() + closer.Add(inPkt.Free) for { select { @@ -91,42 +116,71 @@ func (c *LibAVFFmpegStreamer) Stream(donut *entities.DonutParameters) { return default: - if err := p.inputFormatContext.ReadFrame(pkt); err != nil { + if err := p.inputFormatContext.ReadFrame(inPkt); err != nil { if errors.Is(err, astiav.ErrEof) { break } c.onError(err, donut) } - s, ok := p.streams[pkt.StreamIndex()] + s, ok := p.streams[inPkt.StreamIndex()] if !ok { continue } - pkt.RescaleTs(s.inputStream.TimeBase(), s.decCodecContext.TimeBase()) + // TODO: understand why it's necessary + inPkt.RescaleTs(s.inputStream.TimeBase(), s.decCodecContext.TimeBase()) - audioDuration := c.defineAudioDuration(s, pkt) - videoDuration := c.defineVideoDuration(s, pkt) - - if s.inputStream.CodecParameters().MediaType() == astiav.MediaTypeVideo { + isVideo := s.decCodecContext.MediaType() == astiav.MediaTypeVideo + isVideoBypass := donut.Recipe.Video.Action == entities.DonutBypass + if isVideo && isVideoBypass { if donut.OnVideoFrame != nil { - if err := donut.OnVideoFrame(pkt.Data(), entities.MediaFrameContext{ - PTS: int(pkt.Pts()), - DTS: int(pkt.Dts()), - Duration: videoDuration, + if err := donut.OnVideoFrame(inPkt.Data(), entities.MediaFrameContext{ + PTS: int(inPkt.Pts()), + DTS: int(inPkt.Dts()), + Duration: c.defineVideoDuration(s, inPkt), }); err != nil { c.onError(err, donut) return } } + continue } - if s.inputStream.CodecParameters().MediaType() == astiav.MediaTypeAudio { + isAudio := s.decCodecContext.MediaType() == astiav.MediaTypeAudio + isAudioBypass := donut.Recipe.Audio.Action == entities.DonutBypass + if isAudio && isAudioBypass { if donut.OnAudioFrame != nil { - donut.OnAudioFrame(pkt.Data(), entities.MediaFrameContext{ - PTS: int(pkt.Pts()), - DTS: int(pkt.Dts()), - Duration: audioDuration, - }) + if err := donut.OnAudioFrame(inPkt.Data(), entities.MediaFrameContext{ + PTS: int(inPkt.Pts()), + DTS: int(inPkt.Dts()), + Duration: c.defineAudioDuration(s, inPkt), + }); err != nil { + c.onError(err, donut) + return + } + } + continue + } + + // send the coded packet (compressed/encoded frame) to the decoder + if err := s.decCodecContext.SendPacket(inPkt); err != nil { + c.onError(err, donut) + return + } + + for { + // receive the raw frame from the decoder + if err := s.decCodecContext.ReceiveFrame(s.decFrame); err != nil { + if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) { + break + } + c.onError(err, donut) + return + } + // send the raw frame to the encoder + if err := c.encodeFrame(s.decFrame, s, donut); err != nil { + c.onError(err, donut) + return } } } @@ -139,13 +193,7 @@ func (c *LibAVFFmpegStreamer) onError(err error, p *entities.DonutParameters) { } } -func (c *LibAVFFmpegStreamer) prepareInput(p *params, closer *astikit.Closer, donut *entities.DonutParameters) error { - // good for debugging - astiav.SetLogLevel(astiav.LogLevelDebug) - astiav.SetLogCallback(func(l astiav.LogLevel, fmt, msg, parent string) { - c.l.Infof("ffmpeg log: %s (level: %d)", strings.TrimSpace(msg), l) - }) - +func (c *LibAVFFmpegStreamer) prepareInput(p *libAVParams, closer *astikit.Closer, donut *entities.DonutParameters) error { if p.inputFormatContext = astiav.AllocFormatContext(); p.inputFormatContext == nil { return errors.New("ffmpeg/libav: input format context is nil") } @@ -159,7 +207,6 @@ func (c *LibAVFFmpegStreamer) prepareInput(p *params, closer *astikit.Closer, do if err := p.inputFormatContext.OpenInput(donut.StreamURL, inputFormat, inputOptions); err != nil { return fmt.Errorf("ffmpeg/libav: opening input failed %w", err) } - closer.Add(p.inputFormatContext.CloseInput) if err := p.inputFormatContext.FindStreamInfo(nil); err != nil { @@ -209,6 +256,181 @@ func (c *LibAVFFmpegStreamer) prepareInput(p *params, closer *astikit.Closer, do return nil } +func (c *LibAVFFmpegStreamer) prepareOutput(p *libAVParams, closer *astikit.Closer, donut *entities.DonutParameters) error { + for _, is := range p.inputFormatContext.Streams() { + s, ok := p.streams[is.Index()] + if !ok { + c.l.Infof("skipping stream index = %d", is.Index()) + continue + } + + isVideo := s.decCodecContext.MediaType() == astiav.MediaTypeVideo + isVideoBypass := donut.Recipe.Video.Action == entities.DonutBypass + if isVideo && isVideoBypass { + c.l.Infof("skipping video transcoding for %+v", s.inputStream) + continue + } + + isAudio := s.decCodecContext.MediaType() == astiav.MediaTypeAudio + isAudioBypass := donut.Recipe.Audio.Action == entities.DonutBypass + if isAudio && isAudioBypass { + c.l.Infof("skipping audio transcoding for %+v", s.inputStream) + continue + } + + var codecID astiav.CodecID + if isAudio { + audioCodecID, err := c.m.FromStreamCodecToLibAVCodecID(donut.Recipe.Audio.Codec) + if err != nil { + return err + } + codecID = audioCodecID + } + if isVideo { + videoCodecID, err := c.m.FromStreamCodecToLibAVCodecID(donut.Recipe.Video.Codec) + if err != nil { + return err + } + codecID = videoCodecID + } + + if s.encCodec = astiav.FindEncoder(codecID); s.encCodec == nil { + // TODO: migrate error to entity + return fmt.Errorf("cannot find a libav encoder for %+v", codecID) + } + + if s.encCodecContext = astiav.AllocCodecContext(s.encCodec); s.encCodecContext == nil { + // TODO: migrate error to entity + return errors.New("ffmpeg/libav: codec context is nil") + } + closer.Add(s.encCodecContext.Free) + + if isAudio { + if v := s.encCodec.ChannelLayouts(); len(v) > 0 { + s.encCodecContext.SetChannelLayout(v[0]) + } else { + s.encCodecContext.SetChannelLayout(s.decCodecContext.ChannelLayout()) + } + s.encCodecContext.SetChannels(s.decCodecContext.Channels()) + s.encCodecContext.SetSampleRate(s.decCodecContext.SampleRate()) + if v := s.encCodec.SampleFormats(); len(v) > 0 { + s.encCodecContext.SetSampleFormat(v[0]) + } else { + s.encCodecContext.SetSampleFormat(s.decCodecContext.SampleFormat()) + } + s.encCodecContext.SetTimeBase(s.decCodecContext.TimeBase()) + + // supplying custom config + if len(donut.Recipe.Audio.CodecContextOptions) > 0 { + for _, opt := range donut.Recipe.Audio.CodecContextOptions { + opt(s.encCodecContext) + } + } + } + + if isVideo { + if v := s.encCodec.PixelFormats(); len(v) > 0 { + s.encCodecContext.SetPixelFormat(v[0]) + } else { + s.encCodecContext.SetPixelFormat(s.decCodecContext.PixelFormat()) + } + s.encCodecContext.SetSampleAspectRatio(s.decCodecContext.SampleAspectRatio()) + s.encCodecContext.SetTimeBase(s.decCodecContext.TimeBase()) + s.encCodecContext.SetHeight(s.decCodecContext.Height()) + s.encCodecContext.SetWidth(s.decCodecContext.Width()) + // s.encCodecContext.SetFramerate(p.inputFormatContext.GuessFrameRate(s.inputStream, nil)) + s.encCodecContext.SetFramerate(s.inputStream.AvgFrameRate()) + + // supplying custom config + if len(donut.Recipe.Audio.CodecContextOptions) > 0 { + for _, opt := range donut.Recipe.Audio.CodecContextOptions { + opt(s.encCodecContext) + } + } + } + + if s.decCodecContext.Flags().Has(astiav.CodecContextFlagGlobalHeader) { + s.encCodecContext.SetFlags(s.encCodecContext.Flags().Add(astiav.CodecContextFlagGlobalHeader)) + } + + if err := s.encCodecContext.Open(s.encCodec, nil); err != nil { + return fmt.Errorf("opening encoder context failed: %w", err) + } + + s.encPkt = astiav.AllocPacket() + closer.Add(s.encPkt.Free) + + // // Update codec parameters + // if err = s.outputStream.CodecParameters().FromCodecContext(s.encCodecContext); err != nil { + // err = fmt.Errorf("main: updating codec parameters failed: %w", err) + // return + // } + + // // Update stream + // s.outputStream.SetTimeBase(s.encCodecContext.TimeBase()) + } + return nil +} + +func (c *LibAVFFmpegStreamer) encodeFrame(f *astiav.Frame, s *streamContext, donut *entities.DonutParameters) (err error) { + // Reset picture type + f.SetPictureType(astiav.PictureTypeNone) + + s.encPkt.Unref() + + // Send frame + if err = s.encCodecContext.SendFrame(f); err != nil { + err = fmt.Errorf("main: sending frame failed: %w", err) + return + } + + // Loop + for { + // Receive packet + if err = s.encCodecContext.ReceivePacket(s.encPkt); err != nil { + if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) { + err = nil + break + } + err = fmt.Errorf("main: receiving packet failed: %w", err) + return + } + + // Update pkt + // s.encPkt.RescaleTs(s.encCodecContext.TimeBase(), s.outputStream.TimeBase()) + s.encPkt.RescaleTs(s.encCodecContext.TimeBase(), s.decCodecContext.TimeBase()) + + isVideo := s.decCodecContext.MediaType() == astiav.MediaTypeVideo + if isVideo { + if donut.OnVideoFrame != nil { + if err := donut.OnVideoFrame(s.encPkt.Data(), entities.MediaFrameContext{ + PTS: int(s.encPkt.Pts()), + DTS: int(s.encPkt.Dts()), + Duration: c.defineVideoDuration(s, s.encPkt), + }); err != nil { + return err + } + } + } + + isAudio := s.decCodecContext.MediaType() == astiav.MediaTypeAudio + if isAudio { + if donut.OnAudioFrame != nil { + if err := donut.OnAudioFrame(s.encPkt.Data(), entities.MediaFrameContext{ + PTS: int(s.encPkt.Pts()), + DTS: int(s.encPkt.Dts()), + Duration: c.defineAudioDuration(s, s.encPkt), + }); err != nil { + return err + } + } + } + + } + + return nil +} + func (c *LibAVFFmpegStreamer) defineInputFormat(streamFormat string) (*astiav.InputFormat, error) { if streamFormat != "" { inputFormat := astiav.FindInputFormat(streamFormat) @@ -246,14 +468,19 @@ func (c *LibAVFFmpegStreamer) defineAudioDuration(s *streamContext, pkt *astiav. // ref https://developer.apple.com/documentation/coreaudiotypes/audiostreambasicdescription/1423257-mframesperpacket // TODO: properly handle wraparound / roll over - c.currentAudioFrameSize = float64(pkt.Dts()) - c.lastAudioFrameDTS - if c.currentAudioFrameSize < 0 { - c.currentAudioFrameSize = c.lastAudioFrameDTS*2 - c.lastAudioFrameDTS + // or explore av frame_size https://ffmpeg.org/doxygen/trunk/structAVCodecContext.html#aec57f0d859a6df8b479cd93ca3a44a33 + // and libAV pts roll over + if float64(pkt.Dts())-c.lastAudioFrameDTS > 0 { + c.currentAudioFrameSize = float64(pkt.Dts()) - c.lastAudioFrameDTS } c.lastAudioFrameDTS = float64(pkt.Dts()) sampleRate := float64(s.inputStream.CodecParameters().SampleRate()) audioDuration = time.Duration((c.currentAudioFrameSize / sampleRate) * float64(time.Second)) + c.l.Infow("audio duration", + "framesize", s.inputStream.CodecParameters().FrameSize(), + "audioDuration", audioDuration, + ) } return audioDuration } @@ -270,6 +497,42 @@ func (c *LibAVFFmpegStreamer) defineVideoDuration(s *streamContext, pkt *astiav. // we're assuming fixed video frame rate videoDuration = time.Duration((float64(1) / float64(s.inputStream.AvgFrameRate().Num())) * float64(time.Second)) + c.l.Infow("video duration", + "framesize", s.inputStream.CodecParameters().FrameSize(), + "videoDuration", videoDuration, + ) } return videoDuration } + +// TODO: move this either to a mapper or make a PR for astiav +func (*LibAVFFmpegStreamer) libAVLogToString(l astiav.LogLevel) string { + const _Ciconst_AV_LOG_DEBUG = 0x30 + const _Ciconst_AV_LOG_ERROR = 0x10 + const _Ciconst_AV_LOG_FATAL = 0x8 + const _Ciconst_AV_LOG_INFO = 0x20 + const _Ciconst_AV_LOG_PANIC = 0x0 + const _Ciconst_AV_LOG_QUIET = -0x8 + const _Ciconst_AV_LOG_VERBOSE = 0x28 + const _Ciconst_AV_LOG_WARNING = 0x18 + switch l { + case _Ciconst_AV_LOG_WARNING: + return "WARN" + case _Ciconst_AV_LOG_VERBOSE: + return "VERBOSE" + case _Ciconst_AV_LOG_QUIET: + return "QUIET" + case _Ciconst_AV_LOG_PANIC: + return "PANIC" + case _Ciconst_AV_LOG_INFO: + return "INFO" + case _Ciconst_AV_LOG_FATAL: + return "FATAL" + case _Ciconst_AV_LOG_DEBUG: + return "DEBUG" + case _Ciconst_AV_LOG_ERROR: + return "ERROR" + default: + return "UNKNOWN LEVEL" + } +} diff --git a/internal/controllers/webrtc_controller.go b/internal/controllers/webrtc_controller.go index b1d1010..65cc681 100644 --- a/internal/controllers/webrtc_controller.go +++ b/internal/controllers/webrtc_controller.go @@ -74,8 +74,8 @@ func (c *WebRTCController) CreatePeerConnection(cancel context.CancelFunc) (*web return peerConnection, nil } -func (c *WebRTCController) CreateTrack(peer *webrtc.PeerConnection, track entities.Stream, id string, streamId string) (*webrtc.TrackLocalStaticSample, error) { - codecCapability := c.m.FromTrackToRTPCodecCapability(track) +func (c *WebRTCController) CreateTrack(peer *webrtc.PeerConnection, codec entities.Codec, id string, streamId string) (*webrtc.TrackLocalStaticSample, error) { + codecCapability := c.m.FromTrackToRTPCodecCapability(codec) webRTCtrack, err := webrtc.NewTrackLocalStaticSample(codecCapability, id, streamId) if err != nil { return nil, err diff --git a/internal/entities/entities.go b/internal/entities/entities.go index 85668d1..ec073fb 100644 --- a/internal/entities/entities.go +++ b/internal/entities/entities.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/asticode/go-astiav" "github.com/pion/webrtc/v3" ) @@ -131,8 +132,7 @@ type DonutParameters struct { StreamFormat string // ie: flv, mpegts StreamURL string // ie: srt://host:9080, rtmp://host:4991 - TranscodeVideoCodec Codec // ie: vp8 - TranscodeAudioCodec Codec // ie: opus + Recipe *DonutTransformRecipe OnClose func() OnError func(err error) @@ -141,6 +141,45 @@ type DonutParameters struct { OnAudioFrame func(data []byte, c MediaFrameContext) error } +type DonutMediaTaskAction string + +var DonutTranscode DonutMediaTaskAction = "transcode" +var DonutBypass DonutMediaTaskAction = "bypass" + +// TODO: split entities per domain or files avoiding cluttered names. + +// DonutMediaTask is a transformation template to apply over a media. +type DonutMediaTask struct { + // Action the action that needs to be performed + Action DonutMediaTaskAction + // Codec is the main codec, it might be used depending on the action. + Codec Codec + // CodecContextOptions is a list of options to be applied on codec context. + // If no value is provided ffmpeg will use defaults. + // For instance, if one does not provide bit rate, it'll fallback to 64000 bps (opus) + CodecContextOptions []LibAVOptionsCodecContext +} + +// DonutTransformRecipe is a recipe to run on medias +type DonutTransformRecipe struct { + Video DonutMediaTask + Audio DonutMediaTask +} + +// LibAVOptionsCodecContext is option pattern to change codec context +type LibAVOptionsCodecContext func(c *astiav.CodecContext) + +func SetSampleRate(sampleRate int) LibAVOptionsCodecContext { + return func(c *astiav.CodecContext) { + c.SetSampleRate(sampleRate) + } +} + +// TODO: implement proper matching +// DonutTransformRecipe +// AudioTask: {Action: Transcode, From: AAC, To: Opus} +// VideoTask: {Action: Bypass, From: H264, To: H264} + type Config struct { HTTPPort int32 `required:"true" default:"8080"` HTTPHost string `required:"true" default:"0.0.0.0"` diff --git a/internal/mapper/mapper.go b/internal/mapper/mapper.go index d50854e..75ef3a7 100644 --- a/internal/mapper/mapper.go +++ b/internal/mapper/mapper.go @@ -1,6 +1,7 @@ package mapper import ( + "fmt" "strings" "github.com/asticode/go-astiav" @@ -19,15 +20,18 @@ func NewMapper(l *zap.SugaredLogger) *Mapper { return &Mapper{l: l} } -func (m *Mapper) FromTrackToRTPCodecCapability(track entities.Stream) webrtc.RTPCodecCapability { +func (m *Mapper) FromTrackToRTPCodecCapability(codec entities.Codec) webrtc.RTPCodecCapability { + // TODO: enrich codec capability, check if it's necessary response := webrtc.RTPCodecCapability{} - if track.Codec == entities.H264 { + if codec == entities.H264 { response.MimeType = webrtc.MimeTypeH264 - } else if track.Codec == entities.H265 { + } else if codec == entities.H265 { response.MimeType = webrtc.MimeTypeH265 + } else if codec == entities.Opus { + response.MimeType = webrtc.MimeTypeOpus } else { - m.l.Info("[[[[TODO: mapper not implemented]]]] for ", track) + m.l.Info("[[[[TODO: mapper not implemented]]]] for ", codec) } return response @@ -203,3 +207,16 @@ func (m *Mapper) FromLibAVStreamToEntityStream(libavStream *astiav.Stream) entit return st } + +func (m *Mapper) FromStreamCodecToLibAVCodecID(codec entities.Codec) (astiav.CodecID, error) { + if codec == entities.H264 { + return astiav.CodecIDH264, nil + } else if codec == entities.H265 { + return astiav.CodecIDHevc, nil + } else if codec == entities.Opus { + return astiav.CodecIDOpus, nil + } + + // TODO: port error to entities + return astiav.CodecIDH264, fmt.Errorf("cannot find a libav codec id for donut codec id %+v", codec) +} diff --git a/internal/web/handlers/signaling.go b/internal/web/handlers/signaling.go index eecea17..8920c28 100644 --- a/internal/web/handlers/signaling.go +++ b/internal/web/handlers/signaling.go @@ -70,41 +70,19 @@ func (h *SignalingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) err return err } - // TODO: introduce a mode to deal with transcoding recipes - // selects proper media that client and server has adverted. - // donutEngine preferable vp8, ogg??? - // From: [] To: [] or Transcode:[], Bypass: [] - // libav_streamer.go, libav_streamer_format.go, libav_streamer_codec.go... - // reads from Server (input) and generates h264 raw, and ogg and send it with timing attributes - compatibleStreams, ok := donutEngine.CompatibleStreamsFor(serverStreamInfo, clientStreamInfo) - if !ok { - h.l.Info("we must transcode") - } - - if len(compatibleStreams) == 0 { + donutRecipe := donutEngine.RecipeFor(serverStreamInfo, clientStreamInfo) + if donutRecipe == nil { return entities.ErrMissingCompatibleStreams } var videoTrack *webrtc.TrackLocalStaticSample - // var audioTrack *webrtc.TrackLocalStaticSample - - for _, st := range compatibleStreams { - // TODO: make the mapping less dependent on type - if st.Type == entities.VideoType { - videoTrack, err = h.webRTCController.CreateTrack( - peer, - st, - string(st.Type), // "video" or "audio" - params.SRTStreamID, - ) - if err != nil { - return err - } - - } - // if st.Type == entities.AudioType { + videoTrack, err = h.webRTCController.CreateTrack(peer, donutRecipe.Video.Codec, string(entities.VideoType), params.SRTStreamID) + if err != nil { + return err } + // var audioTrack *webrtc.TrackLocalStaticSample + metadataSender, err := h.webRTCController.CreateDataChannel(peer, entities.MetadataChannelID) if err != nil { return err @@ -123,6 +101,8 @@ func (h *SignalingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) err Cancel: cancel, Ctx: ctx, + Recipe: donutRecipe, + // TODO: add an UI element for the sub-type (format) when input is srt:// // We're assuming that SRT is carrying mpegts. StreamFormat: "mpegts",