mirror of
https://github.com/flavioribeiro/donut.git
synced 2025-09-27 03:15:54 +08:00
start transcoding audio
This commit is contained in:
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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"`
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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",
|
||||
|
Reference in New Issue
Block a user