mirror of
https://github.com/flavioribeiro/donut.git
synced 2025-09-27 03:15:54 +08:00
introduce new api
This commit is contained in:
19
.dockerignore
Normal file
19
.dockerignore
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# The .dockerignore file excludes files from the container build process.
|
||||||
|
#
|
||||||
|
# https://docs.docker.com/engine/reference/builder/#dockerignore-file
|
||||||
|
|
||||||
|
# Exclude locally vendored dependencies.
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# Exclude "build-time" ignore files.
|
||||||
|
.dockerignore
|
||||||
|
.gcloudignore
|
||||||
|
.github
|
||||||
|
.vscode
|
||||||
|
.git
|
||||||
|
|
||||||
|
# Exclude git history and configuration.
|
||||||
|
.gitignore
|
||||||
|
tags
|
||||||
|
probers.test
|
||||||
|
coverage.out
|
@@ -27,6 +27,8 @@ go donutEngine.Stream(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
ref https://wiki.xiph.org/Opus_Recommended_Settings 48000 webrtc
|
||||||
|
|
||||||
## Date: 2/4/24
|
## Date: 2/4/24
|
||||||
### Summary: Adding audio track
|
### Summary: Adding audio track
|
||||||
|
|
||||||
|
@@ -3,6 +3,6 @@ package streamers
|
|||||||
import "github.com/flavioribeiro/donut/internal/entities"
|
import "github.com/flavioribeiro/donut/internal/entities"
|
||||||
|
|
||||||
type DonutStreamer interface {
|
type DonutStreamer interface {
|
||||||
Stream(sp *entities.StreamParameters)
|
Stream(p *entities.DonutParameters)
|
||||||
Match(req *entities.RequestParams) bool
|
Match(req *entities.RequestParams) bool
|
||||||
}
|
}
|
||||||
|
@@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/asticode/go-astiav"
|
"github.com/asticode/go-astiav"
|
||||||
"github.com/asticode/go-astikit"
|
"github.com/asticode/go-astikit"
|
||||||
"github.com/flavioribeiro/donut/internal/entities"
|
"github.com/flavioribeiro/donut/internal/entities"
|
||||||
"github.com/pion/webrtc/v3/pkg/media"
|
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -19,15 +18,14 @@ type LibAVFFmpegStreamer struct {
|
|||||||
c *entities.Config
|
c *entities.Config
|
||||||
l *zap.SugaredLogger
|
l *zap.SugaredLogger
|
||||||
|
|
||||||
middlewares []entities.StreamMiddleware
|
lastAudioFrameDTS float64
|
||||||
|
currentAudioFrameSize float64
|
||||||
}
|
}
|
||||||
|
|
||||||
type LibAVFFmpegStreamerParams struct {
|
type LibAVFFmpegStreamerParams struct {
|
||||||
fx.In
|
fx.In
|
||||||
C *entities.Config
|
C *entities.Config
|
||||||
L *zap.SugaredLogger
|
L *zap.SugaredLogger
|
||||||
|
|
||||||
Middlewares []entities.StreamMiddleware `group:"middlewares"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResultLibAVFFmpegStreamer struct {
|
type ResultLibAVFFmpegStreamer struct {
|
||||||
@@ -38,18 +36,14 @@ type ResultLibAVFFmpegStreamer struct {
|
|||||||
func NewLibAVFFmpegStreamer(p LibAVFFmpegStreamerParams) ResultLibAVFFmpegStreamer {
|
func NewLibAVFFmpegStreamer(p LibAVFFmpegStreamerParams) ResultLibAVFFmpegStreamer {
|
||||||
return ResultLibAVFFmpegStreamer{
|
return ResultLibAVFFmpegStreamer{
|
||||||
LibAVFFmpegStreamer: &LibAVFFmpegStreamer{
|
LibAVFFmpegStreamer: &LibAVFFmpegStreamer{
|
||||||
c: p.C,
|
c: p.C,
|
||||||
l: p.L,
|
l: p.L,
|
||||||
middlewares: p.Middlewares,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LibAVFFmpegStreamer) Match(req *entities.RequestParams) bool {
|
func (c *LibAVFFmpegStreamer) Match(req *entities.RequestParams) bool {
|
||||||
if req.SRTHost != "" {
|
return req.SRTHost != ""
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type streamContext struct {
|
type streamContext struct {
|
||||||
@@ -64,20 +58,18 @@ type params struct {
|
|||||||
streams map[int]*streamContext
|
streams map[int]*streamContext
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LibAVFFmpegStreamer) Stream(sp *entities.StreamParameters) {
|
func (c *LibAVFFmpegStreamer) Stream(donut *entities.DonutParameters) {
|
||||||
c.l.Infow("streaming has started")
|
c.l.Infow("streaming has started")
|
||||||
|
|
||||||
closer := astikit.NewCloser()
|
closer := astikit.NewCloser()
|
||||||
defer closer.Close()
|
defer closer.Close()
|
||||||
defer sp.WebRTCConn.Close()
|
|
||||||
defer sp.Cancel()
|
|
||||||
|
|
||||||
p := ¶ms{
|
p := ¶ms{
|
||||||
streams: make(map[int]*streamContext),
|
streams: make(map[int]*streamContext),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.prepareInput(p, closer, sp); err != nil {
|
if err := c.prepareInput(p, closer, donut); err != nil {
|
||||||
c.l.Errorf("ffmpeg/libav: failed at prepareInput %s", err.Error())
|
c.onError(err, donut)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,14 +78,12 @@ func (c *LibAVFFmpegStreamer) Stream(sp *entities.StreamParameters) {
|
|||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-sp.Ctx.Done():
|
case <-donut.Ctx.Done():
|
||||||
if errors.Is(sp.Ctx.Err(), context.Canceled) {
|
if errors.Is(donut.Ctx.Err(), context.Canceled) {
|
||||||
c.l.Infow("streaming has stopped due cancellation")
|
c.l.Infow("streaming has stopped due cancellation")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.l.Errorw("streaming has stopped due errors",
|
c.onError(donut.Ctx.Err(), donut)
|
||||||
"error", sp.Ctx.Err(),
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
|
|
||||||
@@ -101,7 +91,7 @@ func (c *LibAVFFmpegStreamer) Stream(sp *entities.StreamParameters) {
|
|||||||
if errors.Is(err, astiav.ErrEof) {
|
if errors.Is(err, astiav.ErrEof) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
c.l.Fatalf("ffmpeg/libav: reading frame failed %s", err.Error())
|
c.onError(err, donut)
|
||||||
}
|
}
|
||||||
|
|
||||||
s, ok := p.streams[pkt.StreamIndex()]
|
s, ok := p.streams[pkt.StreamIndex()]
|
||||||
@@ -110,33 +100,43 @@ func (c *LibAVFFmpegStreamer) Stream(sp *entities.StreamParameters) {
|
|||||||
}
|
}
|
||||||
pkt.RescaleTs(s.inputStream.TimeBase(), s.decCodecContext.TimeBase())
|
pkt.RescaleTs(s.inputStream.TimeBase(), s.decCodecContext.TimeBase())
|
||||||
|
|
||||||
|
audioDuration := c.defineAudioDuration(s, pkt)
|
||||||
|
videoDuration := c.defineVideoDuration(s, pkt)
|
||||||
|
|
||||||
if s.inputStream.CodecParameters().MediaType() == astiav.MediaTypeVideo {
|
if s.inputStream.CodecParameters().MediaType() == astiav.MediaTypeVideo {
|
||||||
if err := sp.VideoTrack.WriteSample(media.Sample{Data: pkt.Data(), Duration: time.Second / 30}); err != nil {
|
if donut.OnVideoFrame != nil {
|
||||||
c.l.Errorw("ffmpeg/libav: failed to write video to web rtc",
|
if err := donut.OnVideoFrame(pkt.Data(), entities.MediaFrameContext{
|
||||||
"error", err,
|
PTS: int(pkt.Pts()),
|
||||||
)
|
DTS: int(pkt.Dts()),
|
||||||
return
|
Duration: videoDuration,
|
||||||
|
}); err != nil {
|
||||||
|
c.onError(err, donut)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if err := s.decCodecContext.SendPacket(pkt); err != nil {
|
if s.inputStream.CodecParameters().MediaType() == astiav.MediaTypeAudio {
|
||||||
// c.l.Fatalf("ffmpeg/libav: sending packet failed %s", err.Error())
|
if donut.OnAudioFrame != nil {
|
||||||
// }
|
donut.OnAudioFrame(pkt.Data(), entities.MediaFrameContext{
|
||||||
|
PTS: int(pkt.Pts()),
|
||||||
// for {
|
DTS: int(pkt.Dts()),
|
||||||
// if err := s.decCodecContext.ReceiveFrame(s.decFrame); err != nil {
|
Duration: audioDuration,
|
||||||
// if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) {
|
})
|
||||||
// break
|
}
|
||||||
// }
|
}
|
||||||
// c.l.Fatalf("ffmpeg/libav: receiving frame failed %s", err.Error())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *LibAVFFmpegStreamer) prepareInput(p *params, closer *astikit.Closer, sp *entities.StreamParameters) error {
|
func (c *LibAVFFmpegStreamer) onError(err error, p *entities.DonutParameters) {
|
||||||
|
if p.OnError != nil {
|
||||||
|
p.OnError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LibAVFFmpegStreamer) prepareInput(p *params, closer *astikit.Closer, donut *entities.DonutParameters) error {
|
||||||
|
// good for debugging
|
||||||
astiav.SetLogLevel(astiav.LogLevelDebug)
|
astiav.SetLogLevel(astiav.LogLevelDebug)
|
||||||
astiav.SetLogCallback(func(l astiav.LogLevel, fmt, msg, parent string) {
|
astiav.SetLogCallback(func(l astiav.LogLevel, fmt, msg, parent string) {
|
||||||
c.l.Infof("ffmpeg log: %s (level: %d)", strings.TrimSpace(msg), l)
|
c.l.Infof("ffmpeg log: %s (level: %d)", strings.TrimSpace(msg), l)
|
||||||
@@ -147,27 +147,15 @@ func (c *LibAVFFmpegStreamer) prepareInput(p *params, closer *astikit.Closer, sp
|
|||||||
}
|
}
|
||||||
closer.Add(p.inputFormatContext.Free)
|
closer.Add(p.inputFormatContext.Free)
|
||||||
|
|
||||||
// TODO: add an UI element for sub-type (format) when input is srt:// (defaulting to mpeg-ts)
|
inputFormat, err := c.defineInputFormat(donut.StreamFormat)
|
||||||
// We're assuming that SRT is carrying mpegts.
|
if err != nil {
|
||||||
userProvidedInputFormat := "mpegts"
|
return err
|
||||||
|
|
||||||
inputFormat := astiav.FindInputFormat(userProvidedInputFormat)
|
|
||||||
if inputFormat == nil {
|
|
||||||
return errors.New(fmt.Sprintf("ffmpeg/libav: could not find %s", userProvidedInputFormat))
|
|
||||||
}
|
}
|
||||||
|
inputOptions := c.defineInputOptions(donut, closer)
|
||||||
d := &astiav.Dictionary{}
|
if err := p.inputFormatContext.OpenInput(donut.StreamURL, inputFormat, inputOptions); err != nil {
|
||||||
// ref https://ffmpeg.org/ffmpeg-all.html#srt
|
|
||||||
// flags (the zeroed 3rd value) https://github.com/FFmpeg/FFmpeg/blob/n5.0/libavutil/dict.h#L67C9-L77
|
|
||||||
d.Set("srt_streamid", sp.RequestParams.SRTStreamID, 0)
|
|
||||||
d.Set("smoother", "live", 0)
|
|
||||||
d.Set("transtype", "live", 0)
|
|
||||||
|
|
||||||
inputURL := fmt.Sprintf("srt://%s:%d", sp.RequestParams.SRTHost, sp.RequestParams.SRTPort)
|
|
||||||
|
|
||||||
if err := p.inputFormatContext.OpenInput(inputURL, inputFormat, d); err != nil {
|
|
||||||
return errors.New(fmt.Sprintf("ffmpeg/libav: opening input failed %s", err.Error()))
|
return errors.New(fmt.Sprintf("ffmpeg/libav: opening input failed %s", err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
closer.Add(p.inputFormatContext.CloseInput)
|
closer.Add(p.inputFormatContext.CloseInput)
|
||||||
|
|
||||||
if err := p.inputFormatContext.FindStreamInfo(nil); err != nil {
|
if err := p.inputFormatContext.FindStreamInfo(nil); err != nil {
|
||||||
@@ -211,3 +199,64 @@ func (c *LibAVFFmpegStreamer) prepareInput(p *params, closer *astikit.Closer, sp
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *LibAVFFmpegStreamer) defineInputFormat(streamFormat string) (*astiav.InputFormat, error) {
|
||||||
|
if streamFormat != "" {
|
||||||
|
inputFormat := astiav.FindInputFormat(streamFormat)
|
||||||
|
if inputFormat == nil {
|
||||||
|
return nil, errors.New(fmt.Sprintf("ffmpeg/libav: could not find %s input format", streamFormat))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LibAVFFmpegStreamer) defineInputOptions(p *entities.DonutParameters, closer *astikit.Closer) *astiav.Dictionary {
|
||||||
|
if strings.Contains(strings.ToLower(p.StreamURL), "srt:") {
|
||||||
|
d := &astiav.Dictionary{}
|
||||||
|
closer.Add(d.Free)
|
||||||
|
|
||||||
|
// ref https://ffmpeg.org/ffmpeg-all.html#srt
|
||||||
|
// flags (the zeroed 3rd value) https://github.com/FFmpeg/FFmpeg/blob/n5.0/libavutil/dict.h#L67C9-L77
|
||||||
|
d.Set("srt_streamid", p.StreamID, 0)
|
||||||
|
d.Set("smoother", "live", 0)
|
||||||
|
d.Set("transtype", "live", 0)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LibAVFFmpegStreamer) defineAudioDuration(s *streamContext, pkt *astiav.Packet) time.Duration {
|
||||||
|
audioDuration := time.Duration(0)
|
||||||
|
if s.inputStream.CodecParameters().MediaType() == astiav.MediaTypeAudio {
|
||||||
|
// Audio
|
||||||
|
//
|
||||||
|
// dur = 0,023219954648526078
|
||||||
|
// sample = 44100
|
||||||
|
// frameSize = 1024 (or 960 for aac, but it could be variable for opus)
|
||||||
|
// 1s = dur * (sample/frameSize)
|
||||||
|
// ref https://developer.apple.com/documentation/coreaudiotypes/audiostreambasicdescription/1423257-mframesperpacket
|
||||||
|
|
||||||
|
// TODO: handle wraparound
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
return audioDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LibAVFFmpegStreamer) defineVideoDuration(s *streamContext, pkt *astiav.Packet) time.Duration {
|
||||||
|
videoDuration := time.Duration(0)
|
||||||
|
if s.inputStream.CodecParameters().MediaType() == astiav.MediaTypeVideo {
|
||||||
|
// Video
|
||||||
|
//
|
||||||
|
// dur = 0,033333
|
||||||
|
// sample = 30
|
||||||
|
// frameSize = 1
|
||||||
|
// 1s = dur * (sample/frameSize)
|
||||||
|
|
||||||
|
// we're assuming fixed video frame rate
|
||||||
|
videoDuration = time.Duration((float64(1) / float64(s.inputStream.AvgFrameRate().Num())) * float64(time.Second))
|
||||||
|
}
|
||||||
|
return videoDuration
|
||||||
|
}
|
||||||
|
@@ -1,187 +0,0 @@
|
|||||||
package streamers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
astisrt "github.com/asticode/go-astisrt/pkg"
|
|
||||||
"github.com/asticode/go-astits"
|
|
||||||
"github.com/flavioribeiro/donut/internal/entities"
|
|
||||||
"github.com/pion/webrtc/v3/pkg/media"
|
|
||||||
"go.uber.org/fx"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SRTMpegTSStreamer struct {
|
|
||||||
c *entities.Config
|
|
||||||
l *zap.SugaredLogger
|
|
||||||
|
|
||||||
middlewares []entities.StreamMiddleware
|
|
||||||
}
|
|
||||||
|
|
||||||
type SRTMpegTSStreamerParams struct {
|
|
||||||
fx.In
|
|
||||||
C *entities.Config
|
|
||||||
L *zap.SugaredLogger
|
|
||||||
|
|
||||||
Middlewares []entities.StreamMiddleware `group:"middlewares"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResultSRTMpegTSStreamer struct {
|
|
||||||
fx.Out
|
|
||||||
SRTMpegTSStreamer DonutStreamer `group:"streamers"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSRTMpegTSStreamer(p SRTMpegTSStreamerParams) ResultSRTMpegTSStreamer {
|
|
||||||
return ResultSRTMpegTSStreamer{
|
|
||||||
SRTMpegTSStreamer: &SRTMpegTSStreamer{
|
|
||||||
c: p.C,
|
|
||||||
l: p.L,
|
|
||||||
middlewares: p.Middlewares,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *SRTMpegTSStreamer) Match(req *entities.RequestParams) bool {
|
|
||||||
if req.SRTHost != "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *SRTMpegTSStreamer) Stream(sp *entities.StreamParameters) {
|
|
||||||
srtConnection, err := c.connect(sp.Cancel, sp.RequestParams)
|
|
||||||
if err != nil {
|
|
||||||
c.l.Errorw("streaming has stopped due errors",
|
|
||||||
"error", sp.Ctx.Err(),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r, w := io.Pipe()
|
|
||||||
|
|
||||||
defer r.Close()
|
|
||||||
defer w.Close()
|
|
||||||
defer srtConnection.Close()
|
|
||||||
defer sp.WebRTCConn.Close()
|
|
||||||
defer sp.Cancel()
|
|
||||||
|
|
||||||
go c.readFromSRTIntoWriterPipe(srtConnection, w)
|
|
||||||
|
|
||||||
// reading from reader pipe to the mpeg-ts demuxer
|
|
||||||
mpegTSDemuxer := astits.NewDemuxer(sp.Ctx, r)
|
|
||||||
|
|
||||||
c.l.Infow("streaming has started")
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-sp.Ctx.Done():
|
|
||||||
if errors.Is(sp.Ctx.Err(), context.Canceled) {
|
|
||||||
c.l.Infow("streaming has stopped due cancellation")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.l.Errorw("streaming has stopped due errors",
|
|
||||||
"error", sp.Ctx.Err(),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
// fetching mpeg-ts data
|
|
||||||
// ref https://tsduck.io/download/docs/mpegts-introduction.pdf
|
|
||||||
mpegTSDemuxData, err := mpegTSDemuxer.NextData()
|
|
||||||
if err != nil {
|
|
||||||
c.l.Errorw("failed to demux mpeg-ts",
|
|
||||||
"error", err,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// writing mpeg-ts video to webrtc channels
|
|
||||||
for _, v := range sp.ServerStreamInfo.VideoStreams() {
|
|
||||||
if v.Id != mpegTSDemuxData.PID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sp.VideoTrack.WriteSample(media.Sample{Data: mpegTSDemuxData.PES.Data, Duration: time.Second / 30}); err != nil {
|
|
||||||
c.l.Errorw("failed to write an mpeg-ts to web rtc",
|
|
||||||
"error", err,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// calling all registered middlewares
|
|
||||||
for _, m := range c.middlewares {
|
|
||||||
err = m.Act(mpegTSDemuxData, sp)
|
|
||||||
if err != nil {
|
|
||||||
c.l.Errorw("middleware error",
|
|
||||||
"error", err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *SRTMpegTSStreamer) readFromSRTIntoWriterPipe(srtConnection *astisrt.Connection, w *io.PipeWriter) {
|
|
||||||
defer srtConnection.Close()
|
|
||||||
|
|
||||||
inboundMpegTsPacket := make([]byte, c.c.SRTReadBufferSizeBytes)
|
|
||||||
|
|
||||||
for {
|
|
||||||
n, err := srtConnection.Read(inboundMpegTsPacket)
|
|
||||||
if err != nil {
|
|
||||||
c.l.Errorw("str conn failed to write data to buffer",
|
|
||||||
"error", err,
|
|
||||||
)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := w.Write(inboundMpegTsPacket[:n]); err != nil {
|
|
||||||
c.l.Errorw("failed to write mpeg-ts into the pipe",
|
|
||||||
"error", err,
|
|
||||||
)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move to its own component later dup streamer.srt_mpegts, prober.srt_mpegts
|
|
||||||
func (c *SRTMpegTSStreamer) connect(cancel context.CancelFunc, params *entities.RequestParams) (*astisrt.Connection, error) {
|
|
||||||
c.l.Info("trying to connect srt")
|
|
||||||
|
|
||||||
if err := params.Valid(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.l.Infow("Connecting to SRT ",
|
|
||||||
"offer", params.String(),
|
|
||||||
)
|
|
||||||
|
|
||||||
conn, err := astisrt.Dial(astisrt.DialOptions{
|
|
||||||
ConnectionOptions: []astisrt.ConnectionOption{
|
|
||||||
astisrt.WithLatency(c.c.SRTConnectionLatencyMS),
|
|
||||||
astisrt.WithStreamid(params.SRTStreamID),
|
|
||||||
astisrt.WithCongestion("live"),
|
|
||||||
astisrt.WithTranstype(astisrt.Transtype(astisrt.TranstypeLive)),
|
|
||||||
},
|
|
||||||
|
|
||||||
OnDisconnect: func(conn *astisrt.Connection, err error) {
|
|
||||||
c.l.Infow("Canceling SRT",
|
|
||||||
"error", err,
|
|
||||||
)
|
|
||||||
cancel()
|
|
||||||
},
|
|
||||||
|
|
||||||
Host: params.SRTHost,
|
|
||||||
Port: params.SRTPort,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
c.l.Errorw("failed to connect srt",
|
|
||||||
"error", err,
|
|
||||||
)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.l.Infow("Connected to SRT")
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
@@ -1,86 +1,87 @@
|
|||||||
package streammiddlewares
|
package streammiddlewares
|
||||||
|
|
||||||
import (
|
// import (
|
||||||
"encoding/json"
|
// "encoding/json"
|
||||||
|
|
||||||
"github.com/asticode/go-astits"
|
// "github.com/asticode/go-astits"
|
||||||
"github.com/flavioribeiro/donut/internal/entities"
|
// "github.com/flavioribeiro/donut/internal/entities"
|
||||||
"github.com/flavioribeiro/donut/internal/mapper"
|
// "github.com/flavioribeiro/donut/internal/mapper"
|
||||||
"go.uber.org/fx"
|
// "go.uber.org/fx"
|
||||||
)
|
// )
|
||||||
|
|
||||||
type eia608Middleware struct{}
|
// type eia608Middleware struct{}
|
||||||
|
|
||||||
type EIA608Response struct {
|
// type EIA608Response struct {
|
||||||
fx.Out
|
// fx.Out
|
||||||
EIA608Middleware entities.StreamMiddleware `group:"middlewares"`
|
// EIA608Middleware entities.StreamMiddleware `group:"middlewares"`
|
||||||
}
|
// }
|
||||||
|
|
||||||
// NewEIA608 creates a new EIA608 middleware
|
// // TODO: migrate to donutparameters.onEvent api
|
||||||
func NewEIA608() EIA608Response {
|
// // NewEIA608 creates a new EIA608 middleware
|
||||||
return EIA608Response{
|
// func NewEIA608() EIA608Response {
|
||||||
EIA608Middleware: &eia608Middleware{},
|
// return EIA608Response{
|
||||||
}
|
// EIA608Middleware: &eia608Middleware{},
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
// Act parses and send eia608 data from mpeg-ts to metadata channel
|
// // Act parses and send eia608 data from mpeg-ts to metadata channel
|
||||||
func (*eia608Middleware) Act(mpegTSDemuxData *astits.DemuxerData, sp *entities.StreamParameters) error {
|
// func (*eia608Middleware) Act(mpegTSDemuxData *astits.DemuxerData, sp *entities.StreamParameters) error {
|
||||||
vs := sp.ServerStreamInfo.VideoStreams()
|
// vs := sp.ServerStreamInfo.VideoStreams()
|
||||||
eia608Reader := newEIA608Reader()
|
// eia608Reader := newEIA608Reader()
|
||||||
|
|
||||||
for _, v := range vs {
|
// for _, v := range vs {
|
||||||
if mpegTSDemuxData.PES != nil && v.Codec == entities.H264 {
|
// if mpegTSDemuxData.PES != nil && v.Codec == entities.H264 {
|
||||||
captions, err := eia608Reader.parse(mpegTSDemuxData.PES)
|
// captions, err := eia608Reader.parse(mpegTSDemuxData.PES)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
|
||||||
if captions != "" {
|
// if captions != "" {
|
||||||
captionsMsg, err := eia608Reader.buildCaptionsMessage(mpegTSDemuxData.PES.Header.OptionalHeader.PTS, captions)
|
// captionsMsg, err := eia608Reader.buildCaptionsMessage(mpegTSDemuxData.PES.Header.OptionalHeader.PTS, captions)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
sp.MetadataTrack.SendText(captionsMsg)
|
// sp.MetadataTrack.SendText(captionsMsg)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
type streamInfoMiddleware struct {
|
// type streamInfoMiddleware struct {
|
||||||
m *mapper.Mapper
|
// m *mapper.Mapper
|
||||||
}
|
// }
|
||||||
|
|
||||||
type StreamInfoResponse struct {
|
// type StreamInfoResponse struct {
|
||||||
fx.Out
|
// fx.Out
|
||||||
StreamInfoMiddleware entities.StreamMiddleware `group:"middlewares"`
|
// StreamInfoMiddleware entities.StreamMiddleware `group:"middlewares"`
|
||||||
}
|
// }
|
||||||
|
|
||||||
// NewStreamInfo creates a new StreamInfo middleware
|
// // NewStreamInfo creates a new StreamInfo middleware
|
||||||
func NewStreamInfo(m *mapper.Mapper) StreamInfoResponse {
|
// func NewStreamInfo(m *mapper.Mapper) StreamInfoResponse {
|
||||||
return StreamInfoResponse{
|
// return StreamInfoResponse{
|
||||||
StreamInfoMiddleware: &streamInfoMiddleware{m: m},
|
// StreamInfoMiddleware: &streamInfoMiddleware{m: m},
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Act parses and send StreamInfo data from mpeg-ts to metadata channel
|
// // Act parses and send StreamInfo data from mpeg-ts to metadata channel
|
||||||
func (s *streamInfoMiddleware) Act(mpegTSDemuxData *astits.DemuxerData, sp *entities.StreamParameters) error {
|
// func (s *streamInfoMiddleware) Act(mpegTSDemuxData *astits.DemuxerData, sp *entities.StreamParameters) error {
|
||||||
var streams []entities.Stream
|
// var streams []entities.Stream
|
||||||
// TODO: check if it makes sense to move this code to a mapper
|
// // TODO: check if it makes sense to move this code to a mapper
|
||||||
if mpegTSDemuxData.PMT != nil {
|
// if mpegTSDemuxData.PMT != nil {
|
||||||
for _, es := range mpegTSDemuxData.PMT.ElementaryStreams {
|
// for _, es := range mpegTSDemuxData.PMT.ElementaryStreams {
|
||||||
streams = append(streams, s.m.FromStreamTypeToEntityStream(es))
|
// streams = append(streams, s.m.FromStreamTypeToEntityStream(es))
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
msgs := s.m.FromStreamInfoToEntityMessages(&entities.StreamInfo{Streams: streams})
|
// msgs := s.m.FromStreamInfoToEntityMessages(&entities.StreamInfo{Streams: streams})
|
||||||
for _, m := range msgs {
|
// for _, m := range msgs {
|
||||||
msg, err := json.Marshal(m)
|
// msg, err := json.Marshal(m)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
sp.MetadataTrack.SendText(string(msg))
|
// sp.MetadataTrack.SendText(string(msg))
|
||||||
}
|
// }
|
||||||
|
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
|
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/flavioribeiro/donut/internal/entities"
|
"github.com/flavioribeiro/donut/internal/entities"
|
||||||
"github.com/flavioribeiro/donut/internal/mapper"
|
"github.com/flavioribeiro/donut/internal/mapper"
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v3"
|
||||||
|
"github.com/pion/webrtc/v3/pkg/media"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -118,6 +119,13 @@ func (c *WebRTCController) GatheringWebRTC(peer *webrtc.PeerConnection) (*webrtc
|
|||||||
return peer.LocalDescription(), nil
|
return peer.LocalDescription(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *WebRTCController) SendVideoSample(videoTrack *webrtc.TrackLocalStaticSample, data []byte, mediaCtx entities.MediaFrameContext) error {
|
||||||
|
if err := videoTrack.WriteSample(media.Sample{Data: data, Duration: mediaCtx.Duration}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func NewWebRTCSettingsEngine(c *entities.Config, tcpListener net.Listener, udpListener net.PacketConn) webrtc.SettingEngine {
|
func NewWebRTCSettingsEngine(c *entities.Config, tcpListener net.Listener, udpListener net.PacketConn) webrtc.SettingEngine {
|
||||||
settingEngine := webrtc.SettingEngine{}
|
settingEngine := webrtc.SettingEngine{}
|
||||||
|
|
||||||
|
@@ -3,8 +3,8 @@ package entities
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/asticode/go-astits"
|
|
||||||
"github.com/pion/webrtc/v3"
|
"github.com/pion/webrtc/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,6 +83,15 @@ type Stream struct {
|
|||||||
Index uint16
|
Index uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MediaFrameContext struct {
|
||||||
|
// DTS decoding timestamp
|
||||||
|
DTS int
|
||||||
|
// PTS presentation timestamp
|
||||||
|
PTS int
|
||||||
|
// Media frame duration
|
||||||
|
Duration time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
type StreamInfo struct {
|
type StreamInfo struct {
|
||||||
Streams []Stream
|
Streams []Stream
|
||||||
}
|
}
|
||||||
@@ -113,22 +122,22 @@ type Cue struct {
|
|||||||
Text string
|
Text string
|
||||||
}
|
}
|
||||||
|
|
||||||
type StreamParameters struct {
|
type DonutParameters struct {
|
||||||
WebRTCConn *webrtc.PeerConnection
|
Cancel context.CancelFunc
|
||||||
Cancel context.CancelFunc
|
Ctx context.Context
|
||||||
Ctx context.Context
|
|
||||||
RequestParams *RequestParams
|
|
||||||
VideoTrack *webrtc.TrackLocalStaticSample
|
|
||||||
MetadataTrack *webrtc.DataChannel
|
|
||||||
ServerStreamInfo *StreamInfo
|
|
||||||
ClientStreamInfo *StreamInfo
|
|
||||||
StreamMiddlewares []StreamMiddleware
|
|
||||||
}
|
|
||||||
|
|
||||||
// StreamMiddleware is a component to act while streaming.
|
StreamID string // ie: live001, channel01
|
||||||
// Most implementations are at /internal/controllers/streammiddlewares/
|
StreamFormat string // ie: flv, mpegts
|
||||||
type StreamMiddleware interface {
|
StreamURL string // ie: srt://host:9080, rtmp://host:4991
|
||||||
Act(mpegTSDemuxData *astits.DemuxerData, sp *StreamParameters) error
|
|
||||||
|
TranscodeVideoCodec Codec // ie: vp8
|
||||||
|
TranscodeAudioCodec Codec // ie: opus
|
||||||
|
|
||||||
|
OnClose func()
|
||||||
|
OnError func(err error)
|
||||||
|
OnStream func(st *Stream)
|
||||||
|
OnVideoFrame func(data []byte, c MediaFrameContext) error
|
||||||
|
OnAudioFrame func(data []byte, c MediaFrameContext) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/flavioribeiro/donut/internal/controllers/engine"
|
"github.com/flavioribeiro/donut/internal/controllers/engine"
|
||||||
"github.com/flavioribeiro/donut/internal/controllers/probers"
|
"github.com/flavioribeiro/donut/internal/controllers/probers"
|
||||||
"github.com/flavioribeiro/donut/internal/controllers/streamers"
|
"github.com/flavioribeiro/donut/internal/controllers/streamers"
|
||||||
"github.com/flavioribeiro/donut/internal/controllers/streammiddlewares"
|
|
||||||
"github.com/flavioribeiro/donut/internal/entities"
|
"github.com/flavioribeiro/donut/internal/entities"
|
||||||
"github.com/flavioribeiro/donut/internal/mapper"
|
"github.com/flavioribeiro/donut/internal/mapper"
|
||||||
"github.com/flavioribeiro/donut/internal/web/handlers"
|
"github.com/flavioribeiro/donut/internal/web/handlers"
|
||||||
@@ -44,15 +43,14 @@ func Dependencies(enableICEMux bool) fx.Option {
|
|||||||
fx.Provide(controllers.NewWebRTCSettingsEngine),
|
fx.Provide(controllers.NewWebRTCSettingsEngine),
|
||||||
fx.Provide(controllers.NewWebRTCMediaEngine),
|
fx.Provide(controllers.NewWebRTCMediaEngine),
|
||||||
fx.Provide(controllers.NewWebRTCAPI),
|
fx.Provide(controllers.NewWebRTCAPI),
|
||||||
fx.Provide(streamers.NewSRTMpegTSStreamer),
|
|
||||||
fx.Provide(streamers.NewLibAVFFmpegStreamer),
|
fx.Provide(streamers.NewLibAVFFmpegStreamer),
|
||||||
fx.Provide(probers.NewLibAVFFmpeg),
|
fx.Provide(probers.NewLibAVFFmpeg),
|
||||||
|
|
||||||
fx.Provide(engine.NewDonutEngineController),
|
fx.Provide(engine.NewDonutEngineController),
|
||||||
|
|
||||||
// Stream middlewares
|
// // Stream middlewares
|
||||||
fx.Provide(streammiddlewares.NewStreamInfo),
|
// fx.Provide(streammiddlewares.NewStreamInfo),
|
||||||
fx.Provide(streammiddlewares.NewEIA608),
|
// fx.Provide(streammiddlewares.NewEIA608),
|
||||||
|
|
||||||
// Mappers
|
// Mappers
|
||||||
fx.Provide(mapper.NewMapper),
|
fx.Provide(mapper.NewMapper),
|
||||||
|
@@ -3,6 +3,7 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/flavioribeiro/donut/internal/controllers"
|
"github.com/flavioribeiro/donut/internal/controllers"
|
||||||
@@ -80,7 +81,7 @@ func (h *SignalingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) err
|
|||||||
h.l.Info("we must transcode")
|
h.l.Info("we must transcode")
|
||||||
}
|
}
|
||||||
|
|
||||||
if compatibleStreams == nil || len(compatibleStreams) == 0 {
|
if len(compatibleStreams) == 0 {
|
||||||
return entities.ErrMissingCompatibleStreams
|
return entities.ErrMissingCompatibleStreams
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,15 +119,33 @@ func (h *SignalingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go donutEngine.Streamer().Stream(&entities.StreamParameters{
|
go donutEngine.Streamer().Stream(&entities.DonutParameters{
|
||||||
Cancel: cancel,
|
Cancel: cancel,
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
WebRTCConn: peer,
|
|
||||||
RequestParams: ¶ms,
|
// TODO: add an UI element for the sub-type (format) when input is srt://
|
||||||
VideoTrack: videoTrack,
|
// We're assuming that SRT is carrying mpegts.
|
||||||
MetadataTrack: metadataSender,
|
StreamFormat: "mpegts",
|
||||||
ServerStreamInfo: serverStreamInfo,
|
StreamID: params.SRTStreamID,
|
||||||
ClientStreamInfo: clientStreamInfo,
|
StreamURL: fmt.Sprintf("srt://%s:%d", params.SRTHost, params.SRTPort),
|
||||||
|
|
||||||
|
OnClose: func() {
|
||||||
|
cancel()
|
||||||
|
peer.Close()
|
||||||
|
},
|
||||||
|
OnError: func(err error) {
|
||||||
|
h.l.Errorw("error while streaming", "error", err)
|
||||||
|
},
|
||||||
|
OnStream: func(st *entities.Stream) {
|
||||||
|
h.sendStreamInfoToMetadata(st, metadataSender)
|
||||||
|
},
|
||||||
|
OnVideoFrame: func(data []byte, c entities.MediaFrameContext) error {
|
||||||
|
return h.webRTCController.SendVideoSample(videoTrack, data, c)
|
||||||
|
},
|
||||||
|
OnAudioFrame: func(data []byte, c entities.MediaFrameContext) error {
|
||||||
|
// TODO: implement
|
||||||
|
return nil
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -140,6 +159,16 @@ func (h *SignalingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *SignalingHandler) sendStreamInfoToMetadata(st *entities.Stream, md *webrtc.DataChannel) {
|
||||||
|
msg := h.mapper.FromStreamToEntityMessage(*st)
|
||||||
|
msgBytes, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
h.l.Errorw("error marshalling stream message", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
md.SendText(string(msgBytes))
|
||||||
|
}
|
||||||
|
|
||||||
func (h *SignalingHandler) createAndValidateParams(w http.ResponseWriter, r *http.Request) (entities.RequestParams, error) {
|
func (h *SignalingHandler) createAndValidateParams(w http.ResponseWriter, r *http.Request) (entities.RequestParams, error) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
return entities.RequestParams{}, entities.ErrHTTPPostOnly
|
return entities.RequestParams{}, entities.ErrHTTPPostOnly
|
||||||
|
Reference in New Issue
Block a user