22 changed files with 2164 additions and 193 deletions
@ -0,0 +1,810 @@
@@ -0,0 +1,810 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"bufio" |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
"net" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/bluenviron/gortsplib/v3/pkg/formats" |
||||
"github.com/bluenviron/gortsplib/v3/pkg/media" |
||||
"github.com/bluenviron/gortsplib/v3/pkg/ringbuffer" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/h264" |
||||
"github.com/bluenviron/mediacommon/pkg/codecs/h265" |
||||
"github.com/bluenviron/mediacommon/pkg/formats/mpegts" |
||||
"github.com/datarhei/gosrt" |
||||
"github.com/google/uuid" |
||||
|
||||
"github.com/bluenviron/mediamtx/internal/conf" |
||||
"github.com/bluenviron/mediamtx/internal/formatprocessor" |
||||
"github.com/bluenviron/mediamtx/internal/logger" |
||||
"github.com/bluenviron/mediamtx/internal/stream" |
||||
) |
||||
|
||||
func durationGoToMPEGTS(v time.Duration) int64 { |
||||
return int64(v.Seconds() * 90000) |
||||
} |
||||
|
||||
func h265RandomAccessPresent(au [][]byte) bool { |
||||
for _, nalu := range au { |
||||
typ := h265.NALUType((nalu[0] >> 1) & 0b111111) |
||||
switch typ { |
||||
case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT: |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
type srtConnState int |
||||
|
||||
const ( |
||||
srtConnStateRead srtConnState = iota + 1 |
||||
srtConnStatePublish |
||||
) |
||||
|
||||
type srtConnPathManager interface { |
||||
addReader(req pathAddReaderReq) pathAddReaderRes |
||||
addPublisher(req pathAddPublisherReq) pathAddPublisherRes |
||||
} |
||||
|
||||
type srtConnParent interface { |
||||
logger.Writer |
||||
closeConn(*srtConn) |
||||
} |
||||
|
||||
type srtConn struct { |
||||
readTimeout conf.StringDuration |
||||
writeTimeout conf.StringDuration |
||||
readBufferCount int |
||||
udpMaxPayloadSize int |
||||
connReq srt.ConnRequest |
||||
wg *sync.WaitGroup |
||||
pathManager srtConnPathManager |
||||
parent srtConnParent |
||||
|
||||
ctx context.Context |
||||
ctxCancel func() |
||||
created time.Time |
||||
uuid uuid.UUID |
||||
mutex sync.RWMutex |
||||
state srtConnState |
||||
pathName string |
||||
conn srt.Conn |
||||
|
||||
chNew chan srtNewConnReq |
||||
chSetConn chan srt.Conn |
||||
} |
||||
|
||||
func newSRTConn( |
||||
parentCtx context.Context, |
||||
readTimeout conf.StringDuration, |
||||
writeTimeout conf.StringDuration, |
||||
readBufferCount int, |
||||
udpMaxPayloadSize int, |
||||
connReq srt.ConnRequest, |
||||
wg *sync.WaitGroup, |
||||
pathManager srtConnPathManager, |
||||
parent srtConnParent, |
||||
) *srtConn { |
||||
ctx, ctxCancel := context.WithCancel(parentCtx) |
||||
|
||||
c := &srtConn{ |
||||
readTimeout: readTimeout, |
||||
writeTimeout: writeTimeout, |
||||
readBufferCount: readBufferCount, |
||||
udpMaxPayloadSize: udpMaxPayloadSize, |
||||
connReq: connReq, |
||||
wg: wg, |
||||
pathManager: pathManager, |
||||
parent: parent, |
||||
ctx: ctx, |
||||
ctxCancel: ctxCancel, |
||||
created: time.Now(), |
||||
uuid: uuid.New(), |
||||
chNew: make(chan srtNewConnReq), |
||||
chSetConn: make(chan srt.Conn), |
||||
} |
||||
|
||||
c.Log(logger.Info, "opened") |
||||
|
||||
c.wg.Add(1) |
||||
go c.run() |
||||
|
||||
return c |
||||
} |
||||
|
||||
func (c *srtConn) close() { |
||||
c.ctxCancel() |
||||
} |
||||
|
||||
func (c *srtConn) Log(level logger.Level, format string, args ...interface{}) { |
||||
c.parent.Log(level, "[conn %v] "+format, append([]interface{}{c.connReq.RemoteAddr()}, args...)...) |
||||
} |
||||
|
||||
func (c *srtConn) ip() net.IP { |
||||
return c.connReq.RemoteAddr().(*net.UDPAddr).IP |
||||
} |
||||
|
||||
func (c *srtConn) run() { |
||||
defer c.wg.Done() |
||||
|
||||
err := c.runInner() |
||||
|
||||
c.ctxCancel() |
||||
|
||||
c.parent.closeConn(c) |
||||
|
||||
c.Log(logger.Info, "closed (%v)", err) |
||||
} |
||||
|
||||
func (c *srtConn) runInner() error { |
||||
var req srtNewConnReq |
||||
select { |
||||
case req = <-c.chNew: |
||||
case <-c.ctx.Done(): |
||||
return errors.New("terminated") |
||||
} |
||||
|
||||
answerSent, err := c.runInner2(req) |
||||
|
||||
if !answerSent { |
||||
req.res <- nil |
||||
} |
||||
|
||||
return err |
||||
} |
||||
|
||||
func (c *srtConn) runInner2(req srtNewConnReq) (bool, error) { |
||||
parts := strings.Split(req.connReq.StreamId(), ":") |
||||
if (len(parts) != 2 && len(parts) != 4) || (parts[0] != "read" && parts[0] != "publish") { |
||||
return false, fmt.Errorf("invalid streamid '%s':"+ |
||||
" it must be 'action:pathname' or 'action:pathname:user:pass', "+ |
||||
"where action is either read or publish, pathname is the path name, user and pass are the credentials", |
||||
req.connReq.StreamId()) |
||||
} |
||||
|
||||
pathName := parts[1] |
||||
user := "" |
||||
pass := "" |
||||
|
||||
if len(parts) == 4 { |
||||
user, pass = parts[2], parts[3] |
||||
} |
||||
|
||||
if parts[0] == "publish" { |
||||
return c.runPublish(req, pathName, user, pass) |
||||
} |
||||
return c.runRead(req, pathName, user, pass) |
||||
} |
||||
|
||||
func (c *srtConn) runPublish(req srtNewConnReq, pathName string, user string, pass string) (bool, error) { |
||||
res := c.pathManager.addPublisher(pathAddPublisherReq{ |
||||
author: c, |
||||
pathName: pathName, |
||||
credentials: authCredentials{ |
||||
ip: c.ip(), |
||||
user: user, |
||||
pass: pass, |
||||
proto: authProtocolSRT, |
||||
id: &c.uuid, |
||||
}, |
||||
}) |
||||
|
||||
if res.err != nil { |
||||
if terr, ok := res.err.(*errAuthentication); ok { |
||||
// TODO: re-enable. Currently this freezes the listener.
|
||||
// wait some seconds to stop brute force attacks
|
||||
// <-time.After(srtPauseAfterAuthError)
|
||||
return false, terr |
||||
} |
||||
return false, res.err |
||||
} |
||||
|
||||
defer res.path.removePublisher(pathRemovePublisherReq{author: c}) |
||||
|
||||
sconn, err := c.exchangeRequestWithConn(req) |
||||
if err != nil { |
||||
return true, err |
||||
} |
||||
|
||||
c.mutex.Lock() |
||||
c.state = srtConnStatePublish |
||||
c.pathName = pathName |
||||
c.conn = sconn |
||||
c.mutex.Unlock() |
||||
|
||||
readerErr := make(chan error) |
||||
go func() { |
||||
readerErr <- c.runPublishReader(sconn, res.path) |
||||
}() |
||||
|
||||
select { |
||||
case err := <-readerErr: |
||||
sconn.Close() |
||||
return true, err |
||||
|
||||
case <-c.ctx.Done(): |
||||
sconn.Close() |
||||
<-readerErr |
||||
return true, errors.New("terminated") |
||||
} |
||||
} |
||||
|
||||
func (c *srtConn) runPublishReader(sconn srt.Conn, path *path) error { |
||||
sconn.SetReadDeadline(time.Now().Add(time.Duration(c.readTimeout))) |
||||
r, err := mpegts.NewReader(mpegts.NewBufferedReader(sconn)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
var medias media.Medias |
||||
var stream *stream.Stream |
||||
|
||||
var td *mpegts.TimeDecoder |
||||
decodeTime := func(t int64) time.Duration { |
||||
if td == nil { |
||||
td = mpegts.NewTimeDecoder(t) |
||||
} |
||||
return td.Decode(t) |
||||
} |
||||
|
||||
for _, track := range r.Tracks() { //nolint:dupl
|
||||
var medi *media.Media |
||||
|
||||
switch tcodec := track.Codec.(type) { |
||||
case *mpegts.CodecH264: |
||||
medi = &media.Media{ |
||||
Type: media.TypeVideo, |
||||
Formats: []formats.Format{&formats.H264{ |
||||
PayloadTyp: 96, |
||||
PacketizationMode: 1, |
||||
}}, |
||||
} |
||||
|
||||
r.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) error { |
||||
stream.WriteUnit(medi, medi.Formats[0], &formatprocessor.UnitH264{ |
||||
BaseUnit: formatprocessor.BaseUnit{ |
||||
NTP: time.Now(), |
||||
}, |
||||
PTS: decodeTime(pts), |
||||
AU: au, |
||||
}) |
||||
return nil |
||||
}) |
||||
|
||||
case *mpegts.CodecH265: |
||||
medi = &media.Media{ |
||||
Type: media.TypeVideo, |
||||
Formats: []formats.Format{&formats.H265{ |
||||
PayloadTyp: 96, |
||||
}}, |
||||
} |
||||
|
||||
r.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) error { |
||||
stream.WriteUnit(medi, medi.Formats[0], &formatprocessor.UnitH265{ |
||||
BaseUnit: formatprocessor.BaseUnit{ |
||||
NTP: time.Now(), |
||||
}, |
||||
PTS: decodeTime(pts), |
||||
AU: au, |
||||
}) |
||||
return nil |
||||
}) |
||||
|
||||
case *mpegts.CodecMPEG4Audio: |
||||
medi = &media.Media{ |
||||
Type: media.TypeAudio, |
||||
Formats: []formats.Format{&formats.MPEG4Audio{ |
||||
PayloadTyp: 96, |
||||
SizeLength: 13, |
||||
IndexLength: 3, |
||||
IndexDeltaLength: 3, |
||||
Config: &tcodec.Config, |
||||
}}, |
||||
} |
||||
|
||||
r.OnDataMPEG4Audio(track, func(pts int64, _ int64, aus [][]byte) error { |
||||
stream.WriteUnit(medi, medi.Formats[0], &formatprocessor.UnitMPEG4AudioGeneric{ |
||||
BaseUnit: formatprocessor.BaseUnit{ |
||||
NTP: time.Now(), |
||||
}, |
||||
PTS: decodeTime(pts), |
||||
AUs: aus, |
||||
}) |
||||
return nil |
||||
}) |
||||
|
||||
case *mpegts.CodecOpus: |
||||
medi = &media.Media{ |
||||
Type: media.TypeAudio, |
||||
Formats: []formats.Format{&formats.Opus{ |
||||
PayloadTyp: 96, |
||||
IsStereo: (tcodec.ChannelCount == 2), |
||||
}}, |
||||
} |
||||
|
||||
r.OnDataOpus(track, func(pts int64, _ int64, packets [][]byte) error { |
||||
stream.WriteUnit(medi, medi.Formats[0], &formatprocessor.UnitOpus{ |
||||
BaseUnit: formatprocessor.BaseUnit{ |
||||
NTP: time.Now(), |
||||
}, |
||||
PTS: decodeTime(pts), |
||||
Packets: packets, |
||||
}) |
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
medias = append(medias, medi) |
||||
} |
||||
|
||||
rres := path.startPublisher(pathStartPublisherReq{ |
||||
author: c, |
||||
medias: medias, |
||||
generateRTPPackets: true, |
||||
}) |
||||
if rres.err != nil { |
||||
return rres.err |
||||
} |
||||
|
||||
c.Log(logger.Info, "is publishing to path '%s', %s", |
||||
path.name, |
||||
sourceMediaInfo(medias)) |
||||
|
||||
stream = rres.stream |
||||
|
||||
for { |
||||
err := r.Read() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (c *srtConn) runRead(req srtNewConnReq, pathName string, user string, pass string) (bool, error) { |
||||
res := c.pathManager.addReader(pathAddReaderReq{ |
||||
author: c, |
||||
pathName: pathName, |
||||
credentials: authCredentials{ |
||||
ip: c.ip(), |
||||
user: user, |
||||
pass: pass, |
||||
proto: authProtocolSRT, |
||||
id: &c.uuid, |
||||
}, |
||||
}) |
||||
|
||||
if res.err != nil { |
||||
if terr, ok := res.err.(*errAuthentication); ok { |
||||
// TODO: re-enable. Currently this freezes the listener.
|
||||
// wait some seconds to stop brute force attacks
|
||||
// <-time.After(srtPauseAfterAuthError)
|
||||
return false, terr |
||||
} |
||||
return false, res.err |
||||
} |
||||
|
||||
defer res.path.removeReader(pathRemoveReaderReq{author: c}) |
||||
|
||||
sconn, err := c.exchangeRequestWithConn(req) |
||||
if err != nil { |
||||
return true, err |
||||
} |
||||
defer sconn.Close() |
||||
|
||||
c.mutex.Lock() |
||||
c.state = srtConnStateRead |
||||
c.pathName = pathName |
||||
c.conn = sconn |
||||
c.mutex.Unlock() |
||||
|
||||
ringBuffer, _ := ringbuffer.New(uint64(c.readBufferCount)) |
||||
go func() { |
||||
<-c.ctx.Done() |
||||
ringBuffer.Close() |
||||
}() |
||||
|
||||
var w *mpegts.Writer |
||||
nextPID := uint16(256) |
||||
var tracks []*mpegts.Track |
||||
var medias media.Medias |
||||
bw := bufio.NewWriterSize(sconn, srtMaxPayloadSize(c.udpMaxPayloadSize)) |
||||
|
||||
leadingTrackChosen := false |
||||
leadingTrackInitialized := false |
||||
var leadingTrackStartDTS time.Duration |
||||
|
||||
for _, medi := range res.stream.Medias() { |
||||
for _, format := range medi.Formats { |
||||
switch format := format.(type) { |
||||
case *formats.H265: |
||||
track := &mpegts.Track{ |
||||
PID: nextPID, |
||||
Codec: &mpegts.CodecH265{}, |
||||
} |
||||
tracks = append(tracks, track) |
||||
medias = append(medias, medi) |
||||
nextPID++ |
||||
|
||||
var startPTS time.Duration |
||||
startPTSFilled := false |
||||
|
||||
var isLeadingTrack bool |
||||
if !leadingTrackChosen { |
||||
isLeadingTrack = true |
||||
} else { |
||||
isLeadingTrack = false |
||||
} |
||||
|
||||
randomAccessReceived := false |
||||
dtsExtractor := h265.NewDTSExtractor() |
||||
|
||||
res.stream.AddReader(c, medi, format, func(unit formatprocessor.Unit) { |
||||
ringBuffer.Push(func() error { |
||||
tunit := unit.(*formatprocessor.UnitH265) |
||||
if tunit.AU == nil { |
||||
return nil |
||||
} |
||||
|
||||
if !startPTSFilled { |
||||
startPTS = tunit.PTS |
||||
startPTSFilled = true |
||||
} |
||||
|
||||
randomAccessPresent := h265RandomAccessPresent(tunit.AU) |
||||
|
||||
if !randomAccessReceived { |
||||
if !randomAccessPresent { |
||||
return nil |
||||
} |
||||
randomAccessReceived = true |
||||
} |
||||
|
||||
pts := tunit.PTS - startPTS |
||||
dts, err := dtsExtractor.Extract(tunit.AU, pts) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if !leadingTrackInitialized { |
||||
if isLeadingTrack { |
||||
leadingTrackStartDTS = dts |
||||
leadingTrackInitialized = true |
||||
} else { |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
dts -= leadingTrackStartDTS |
||||
pts -= leadingTrackStartDTS |
||||
|
||||
sconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout))) |
||||
err = w.WriteH26x(track, durationGoToMPEGTS(pts), durationGoToMPEGTS(dts), randomAccessPresent, tunit.AU) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return bw.Flush() |
||||
}) |
||||
}) |
||||
|
||||
case *formats.H264: |
||||
track := &mpegts.Track{ |
||||
PID: nextPID, |
||||
Codec: &mpegts.CodecH264{}, |
||||
} |
||||
tracks = append(tracks, track) |
||||
medias = append(medias, medi) |
||||
nextPID++ |
||||
|
||||
var startPTS time.Duration |
||||
startPTSFilled := false |
||||
|
||||
var isLeadingTrack bool |
||||
if !leadingTrackChosen { |
||||
isLeadingTrack = true |
||||
} else { |
||||
isLeadingTrack = false |
||||
} |
||||
|
||||
firstIDRReceived := false |
||||
dtsExtractor := h264.NewDTSExtractor() |
||||
|
||||
res.stream.AddReader(c, medi, format, func(unit formatprocessor.Unit) { |
||||
ringBuffer.Push(func() error { |
||||
tunit := unit.(*formatprocessor.UnitH264) |
||||
if tunit.AU == nil { |
||||
return nil |
||||
} |
||||
|
||||
if !startPTSFilled { |
||||
startPTS = tunit.PTS |
||||
startPTSFilled = true |
||||
} |
||||
|
||||
idrPresent := h264.IDRPresent(tunit.AU) |
||||
|
||||
if !firstIDRReceived { |
||||
if !idrPresent { |
||||
return nil |
||||
} |
||||
firstIDRReceived = true |
||||
} |
||||
|
||||
pts := tunit.PTS - startPTS |
||||
dts, err := dtsExtractor.Extract(tunit.AU, pts) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if !leadingTrackInitialized { |
||||
if isLeadingTrack { |
||||
leadingTrackStartDTS = dts |
||||
leadingTrackInitialized = true |
||||
} else { |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
dts -= leadingTrackStartDTS |
||||
pts -= leadingTrackStartDTS |
||||
|
||||
sconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout))) |
||||
err = w.WriteH26x(track, durationGoToMPEGTS(pts), durationGoToMPEGTS(dts), idrPresent, tunit.AU) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return bw.Flush() |
||||
}) |
||||
}) |
||||
|
||||
case *formats.MPEG4AudioGeneric: |
||||
track := &mpegts.Track{ |
||||
PID: nextPID, |
||||
Codec: &mpegts.CodecMPEG4Audio{ |
||||
Config: *format.Config, |
||||
}, |
||||
} |
||||
tracks = append(tracks, track) |
||||
medias = append(medias, medi) |
||||
nextPID++ |
||||
|
||||
var startPTS time.Duration |
||||
startPTSFilled := false |
||||
|
||||
res.stream.AddReader(c, medi, format, func(unit formatprocessor.Unit) { |
||||
ringBuffer.Push(func() error { |
||||
tunit := unit.(*formatprocessor.UnitMPEG4AudioGeneric) |
||||
if tunit.AUs == nil { |
||||
return nil |
||||
} |
||||
|
||||
if !startPTSFilled { |
||||
startPTS = tunit.PTS |
||||
startPTSFilled = true |
||||
} |
||||
|
||||
if leadingTrackChosen && !leadingTrackInitialized { |
||||
return nil |
||||
} |
||||
|
||||
pts := tunit.PTS |
||||
pts -= startPTS |
||||
pts -= leadingTrackStartDTS |
||||
|
||||
sconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout))) |
||||
err = w.WriteMPEG4Audio(track, durationGoToMPEGTS(pts), tunit.AUs) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return bw.Flush() |
||||
}) |
||||
}) |
||||
|
||||
case *formats.MPEG4AudioLATM: |
||||
if format.Config != nil && |
||||
len(format.Config.Programs) == 1 && |
||||
len(format.Config.Programs[0].Layers) == 1 { |
||||
track := &mpegts.Track{ |
||||
PID: nextPID, |
||||
Codec: &mpegts.CodecMPEG4Audio{ |
||||
Config: *format.Config.Programs[0].Layers[0].AudioSpecificConfig, |
||||
}, |
||||
} |
||||
tracks = append(tracks, track) |
||||
medias = append(medias, medi) |
||||
nextPID++ |
||||
|
||||
var startPTS time.Duration |
||||
startPTSFilled := false |
||||
|
||||
res.stream.AddReader(c, medi, format, func(unit formatprocessor.Unit) { |
||||
ringBuffer.Push(func() error { |
||||
tunit := unit.(*formatprocessor.UnitMPEG4AudioLATM) |
||||
if tunit.AU == nil { |
||||
return nil |
||||
} |
||||
|
||||
if !startPTSFilled { |
||||
startPTS = tunit.PTS |
||||
startPTSFilled = true |
||||
} |
||||
|
||||
if leadingTrackChosen && !leadingTrackInitialized { |
||||
return nil |
||||
} |
||||
|
||||
pts := tunit.PTS |
||||
pts -= startPTS |
||||
pts -= leadingTrackStartDTS |
||||
|
||||
sconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout))) |
||||
err = w.WriteMPEG4Audio(track, durationGoToMPEGTS(pts), [][]byte{tunit.AU}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return bw.Flush() |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
case *formats.Opus: |
||||
track := &mpegts.Track{ |
||||
PID: nextPID, |
||||
Codec: &mpegts.CodecOpus{ |
||||
ChannelCount: func() int { |
||||
if format.IsStereo { |
||||
return 2 |
||||
} |
||||
return 1 |
||||
}(), |
||||
}, |
||||
} |
||||
tracks = append(tracks, track) |
||||
medias = append(medias, medi) |
||||
nextPID++ |
||||
|
||||
var startPTS time.Duration |
||||
startPTSFilled := false |
||||
|
||||
res.stream.AddReader(c, medi, format, func(unit formatprocessor.Unit) { |
||||
ringBuffer.Push(func() error { |
||||
tunit := unit.(*formatprocessor.UnitOpus) |
||||
if tunit.Packets == nil { |
||||
return nil |
||||
} |
||||
|
||||
if !startPTSFilled { |
||||
startPTS = tunit.PTS |
||||
startPTSFilled = true |
||||
} |
||||
|
||||
if leadingTrackChosen && !leadingTrackInitialized { |
||||
return nil |
||||
} |
||||
|
||||
pts := tunit.PTS |
||||
pts -= startPTS |
||||
pts -= leadingTrackStartDTS |
||||
|
||||
sconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout))) |
||||
err = w.WriteOpus(track, durationGoToMPEGTS(pts), tunit.Packets) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return bw.Flush() |
||||
}) |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
|
||||
if len(tracks) == 0 { |
||||
return true, fmt.Errorf( |
||||
"the stream doesn't contain any supported codec, which are currently H265, H264, Opus, MPEG4-Audio") |
||||
} |
||||
|
||||
c.Log(logger.Info, "is reading from path '%s', %s", |
||||
res.path.name, sourceMediaInfo(medias)) |
||||
|
||||
w = mpegts.NewWriter(bw, tracks) |
||||
|
||||
// disable read deadline
|
||||
sconn.SetReadDeadline(time.Time{}) |
||||
|
||||
for { |
||||
item, ok := ringBuffer.Pull() |
||||
if !ok { |
||||
return true, fmt.Errorf("terminated") |
||||
} |
||||
|
||||
err := item.(func() error)() |
||||
if err != nil { |
||||
return true, err |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (c *srtConn) exchangeRequestWithConn(req srtNewConnReq) (srt.Conn, error) { |
||||
req.res <- c |
||||
|
||||
select { |
||||
case sconn := <-c.chSetConn: |
||||
return sconn, nil |
||||
|
||||
case <-c.ctx.Done(): |
||||
return nil, errors.New("terminated") |
||||
} |
||||
} |
||||
|
||||
// new is called by srtListener through srtServer.
|
||||
func (c *srtConn) new(req srtNewConnReq) *srtConn { |
||||
select { |
||||
case c.chNew <- req: |
||||
return <-req.res |
||||
|
||||
case <-c.ctx.Done(): |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
// setConn is called by srtListener .
|
||||
func (c *srtConn) setConn(sconn srt.Conn) { |
||||
select { |
||||
case c.chSetConn <- sconn: |
||||
case <-c.ctx.Done(): |
||||
} |
||||
} |
||||
|
||||
// apiReaderDescribe implements reader.
|
||||
func (c *srtConn) apiReaderDescribe() pathAPISourceOrReader { |
||||
return pathAPISourceOrReader{ |
||||
Type: "srtConn", |
||||
ID: c.uuid.String(), |
||||
} |
||||
} |
||||
|
||||
// apiSourceDescribe implements source.
|
||||
func (c *srtConn) apiSourceDescribe() pathAPISourceOrReader { |
||||
return c.apiReaderDescribe() |
||||
} |
||||
|
||||
func (c *srtConn) apiItem() *apiSRTConn { |
||||
c.mutex.RLock() |
||||
defer c.mutex.RUnlock() |
||||
|
||||
bytesReceived := uint64(0) |
||||
bytesSent := uint64(0) |
||||
|
||||
if c.conn != nil { |
||||
var s srt.Statistics |
||||
c.conn.Stats(&s) |
||||
bytesReceived = s.Accumulated.ByteRecv |
||||
bytesSent = s.Accumulated.ByteSent |
||||
} |
||||
|
||||
return &apiSRTConn{ |
||||
ID: c.uuid, |
||||
Created: c.created, |
||||
RemoteAddr: c.connReq.RemoteAddr().String(), |
||||
State: func() string { |
||||
switch c.state { |
||||
case srtConnStateRead: |
||||
return "read" |
||||
|
||||
case srtConnStatePublish: |
||||
return "publish" |
||||
|
||||
default: |
||||
return "idle" |
||||
} |
||||
}(), |
||||
Path: c.pathName, |
||||
BytesReceived: bytesReceived, |
||||
BytesSent: bytesSent, |
||||
} |
||||
} |
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"sync" |
||||
|
||||
"github.com/datarhei/gosrt" |
||||
) |
||||
|
||||
type srtListener struct { |
||||
ln srt.Listener |
||||
wg *sync.WaitGroup |
||||
parent *srtServer |
||||
} |
||||
|
||||
func newSRTListener( |
||||
ln srt.Listener, |
||||
wg *sync.WaitGroup, |
||||
parent *srtServer, |
||||
) *srtListener { |
||||
l := &srtListener{ |
||||
ln: ln, |
||||
wg: wg, |
||||
parent: parent, |
||||
} |
||||
|
||||
l.wg.Add(1) |
||||
go l.run() |
||||
|
||||
return l |
||||
} |
||||
|
||||
func (l *srtListener) run() { |
||||
defer l.wg.Done() |
||||
|
||||
err := func() error { |
||||
for { |
||||
var sconn *srtConn |
||||
conn, _, err := l.ln.Accept(func(req srt.ConnRequest) srt.ConnType { |
||||
sconn = l.parent.newConnRequest(req) |
||||
if sconn == nil { |
||||
return srt.REJECT |
||||
} |
||||
|
||||
// currently it's the same to return SUBSCRIBE or PUBLISH
|
||||
return srt.SUBSCRIBE |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if conn == nil { |
||||
continue |
||||
} |
||||
|
||||
sconn.setConn(conn) |
||||
} |
||||
}() |
||||
|
||||
l.parent.acceptError(err) |
||||
} |
@ -0,0 +1,308 @@
@@ -0,0 +1,308 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"sort" |
||||
"sync" |
||||
"time" |
||||
|
||||
"github.com/datarhei/gosrt" |
||||
"github.com/google/uuid" |
||||
|
||||
"github.com/bluenviron/mediamtx/internal/conf" |
||||
"github.com/bluenviron/mediamtx/internal/logger" |
||||
) |
||||
|
||||
func srtMaxPayloadSize(u int) int { |
||||
return ((u - 16) / 188) * 188 // 16 = SRT header, 188 = MPEG-TS packet
|
||||
} |
||||
|
||||
type srtNewConnReq struct { |
||||
connReq srt.ConnRequest |
||||
res chan *srtConn |
||||
} |
||||
|
||||
type srtServerAPIConnsListRes struct { |
||||
data *apiSRTConnsList |
||||
err error |
||||
} |
||||
|
||||
type srtServerAPIConnsListReq struct { |
||||
res chan srtServerAPIConnsListRes |
||||
} |
||||
|
||||
type srtServerAPIConnsGetRes struct { |
||||
data *apiSRTConn |
||||
err error |
||||
} |
||||
|
||||
type srtServerAPIConnsGetReq struct { |
||||
uuid uuid.UUID |
||||
res chan srtServerAPIConnsGetRes |
||||
} |
||||
|
||||
type srtServerAPIConnsKickRes struct { |
||||
err error |
||||
} |
||||
|
||||
type srtServerAPIConnsKickReq struct { |
||||
uuid uuid.UUID |
||||
res chan srtServerAPIConnsKickRes |
||||
} |
||||
|
||||
type srtServerParent interface { |
||||
logger.Writer |
||||
} |
||||
|
||||
type srtServer struct { |
||||
readTimeout conf.StringDuration |
||||
writeTimeout conf.StringDuration |
||||
readBufferCount int |
||||
udpMaxPayloadSize int |
||||
pathManager *pathManager |
||||
parent srtServerParent |
||||
|
||||
ctx context.Context |
||||
ctxCancel func() |
||||
wg sync.WaitGroup |
||||
ln srt.Listener |
||||
conns map[*srtConn]struct{} |
||||
|
||||
// in
|
||||
chNewConnRequest chan srtNewConnReq |
||||
chAcceptErr chan error |
||||
chCloseConn chan *srtConn |
||||
chAPIConnsList chan srtServerAPIConnsListReq |
||||
chAPIConnsGet chan srtServerAPIConnsGetReq |
||||
chAPIConnsKick chan srtServerAPIConnsKickReq |
||||
} |
||||
|
||||
func newSRTServer( |
||||
address string, |
||||
readTimeout conf.StringDuration, |
||||
writeTimeout conf.StringDuration, |
||||
readBufferCount int, |
||||
udpMaxPayloadSize int, |
||||
pathManager *pathManager, |
||||
parent srtServerParent, |
||||
) (*srtServer, error) { |
||||
conf := srt.DefaultConfig() |
||||
conf.ConnectionTimeout = time.Duration(readTimeout) |
||||
conf.PayloadSize = uint32(srtMaxPayloadSize(udpMaxPayloadSize)) |
||||
|
||||
ln, err := srt.Listen("srt", address, conf) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
ctx, ctxCancel := context.WithCancel(context.Background()) |
||||
|
||||
s := &srtServer{ |
||||
readTimeout: readTimeout, |
||||
writeTimeout: writeTimeout, |
||||
readBufferCount: readBufferCount, |
||||
udpMaxPayloadSize: udpMaxPayloadSize, |
||||
pathManager: pathManager, |
||||
parent: parent, |
||||
ctx: ctx, |
||||
ctxCancel: ctxCancel, |
||||
ln: ln, |
||||
conns: make(map[*srtConn]struct{}), |
||||
chNewConnRequest: make(chan srtNewConnReq), |
||||
chAcceptErr: make(chan error), |
||||
chCloseConn: make(chan *srtConn), |
||||
chAPIConnsList: make(chan srtServerAPIConnsListReq), |
||||
chAPIConnsGet: make(chan srtServerAPIConnsGetReq), |
||||
chAPIConnsKick: make(chan srtServerAPIConnsKickReq), |
||||
} |
||||
|
||||
s.Log(logger.Info, "listener opened on "+address+" (UDP)") |
||||
|
||||
newSRTListener( |
||||
s.ln, |
||||
&s.wg, |
||||
s, |
||||
) |
||||
|
||||
s.wg.Add(1) |
||||
go s.run() |
||||
|
||||
return s, nil |
||||
} |
||||
|
||||
// Log is the main logging function.
|
||||
func (s *srtServer) Log(level logger.Level, format string, args ...interface{}) { |
||||
s.parent.Log(level, "[SRT] "+format, append([]interface{}{}, args...)...) |
||||
} |
||||
|
||||
func (s *srtServer) close() { |
||||
s.Log(logger.Info, "listener is closing") |
||||
s.ctxCancel() |
||||
s.wg.Wait() |
||||
} |
||||
|
||||
func (s *srtServer) run() { |
||||
defer s.wg.Done() |
||||
|
||||
outer: |
||||
for { |
||||
select { |
||||
case err := <-s.chAcceptErr: |
||||
s.Log(logger.Error, "%s", err) |
||||
break outer |
||||
|
||||
case req := <-s.chNewConnRequest: |
||||
c := newSRTConn( |
||||
s.ctx, |
||||
s.readTimeout, |
||||
s.writeTimeout, |
||||
s.readBufferCount, |
||||
s.udpMaxPayloadSize, |
||||
req.connReq, |
||||
&s.wg, |
||||
s.pathManager, |
||||
s) |
||||
s.conns[c] = struct{}{} |
||||
req.res <- c |
||||
|
||||
case c := <-s.chCloseConn: |
||||
delete(s.conns, c) |
||||
|
||||
case req := <-s.chAPIConnsList: |
||||
data := &apiSRTConnsList{ |
||||
Items: []*apiSRTConn{}, |
||||
} |
||||
|
||||
for c := range s.conns { |
||||
data.Items = append(data.Items, c.apiItem()) |
||||
} |
||||
|
||||
sort.Slice(data.Items, func(i, j int) bool { |
||||
return data.Items[i].Created.Before(data.Items[j].Created) |
||||
}) |
||||
|
||||
req.res <- srtServerAPIConnsListRes{data: data} |
||||
|
||||
case req := <-s.chAPIConnsGet: |
||||
c := s.findConnByUUID(req.uuid) |
||||
if c == nil { |
||||
req.res <- srtServerAPIConnsGetRes{err: errAPINotFound} |
||||
continue |
||||
} |
||||
|
||||
req.res <- srtServerAPIConnsGetRes{data: c.apiItem()} |
||||
|
||||
case req := <-s.chAPIConnsKick: |
||||
c := s.findConnByUUID(req.uuid) |
||||
if c == nil { |
||||
req.res <- srtServerAPIConnsKickRes{err: errAPINotFound} |
||||
continue |
||||
} |
||||
|
||||
delete(s.conns, c) |
||||
c.close() |
||||
req.res <- srtServerAPIConnsKickRes{} |
||||
|
||||
case <-s.ctx.Done(): |
||||
break outer |
||||
} |
||||
} |
||||
|
||||
s.ctxCancel() |
||||
|
||||
s.ln.Close() |
||||
} |
||||
|
||||
func (s *srtServer) findConnByUUID(uuid uuid.UUID) *srtConn { |
||||
for sx := range s.conns { |
||||
if sx.uuid == uuid { |
||||
return sx |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// newConnRequest is called by srtListener.
|
||||
func (s *srtServer) newConnRequest(connReq srt.ConnRequest) *srtConn { |
||||
req := srtNewConnReq{ |
||||
connReq: connReq, |
||||
res: make(chan *srtConn), |
||||
} |
||||
|
||||
select { |
||||
case s.chNewConnRequest <- req: |
||||
c := <-req.res |
||||
|
||||
return c.new(req) |
||||
|
||||
case <-s.ctx.Done(): |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
// acceptError is called by srtListener.
|
||||
func (s *srtServer) acceptError(err error) { |
||||
select { |
||||
case s.chAcceptErr <- err: |
||||
case <-s.ctx.Done(): |
||||
} |
||||
} |
||||
|
||||
// closeConn is called by srtConn.
|
||||
func (s *srtServer) closeConn(c *srtConn) { |
||||
select { |
||||
case s.chCloseConn <- c: |
||||
case <-s.ctx.Done(): |
||||
} |
||||
} |
||||
|
||||
// apiConnsList is called by api.
|
||||
func (s *srtServer) apiConnsList() (*apiSRTConnsList, error) { |
||||
req := srtServerAPIConnsListReq{ |
||||
res: make(chan srtServerAPIConnsListRes), |
||||
} |
||||
|
||||
select { |
||||
case s.chAPIConnsList <- req: |
||||
res := <-req.res |
||||
return res.data, res.err |
||||
|
||||
case <-s.ctx.Done(): |
||||
return nil, fmt.Errorf("terminated") |
||||
} |
||||
} |
||||
|
||||
// apiConnsGet is called by api.
|
||||
func (s *srtServer) apiConnsGet(uuid uuid.UUID) (*apiSRTConn, error) { |
||||
req := srtServerAPIConnsGetReq{ |
||||
uuid: uuid, |
||||
res: make(chan srtServerAPIConnsGetRes), |
||||
} |
||||
|
||||
select { |
||||
case s.chAPIConnsGet <- req: |
||||
res := <-req.res |
||||
return res.data, res.err |
||||
|
||||
case <-s.ctx.Done(): |
||||
return nil, fmt.Errorf("terminated") |
||||
} |
||||
} |
||||
|
||||
// apiConnsKick is called by api.
|
||||
func (s *srtServer) apiConnsKick(uuid uuid.UUID) error { |
||||
req := srtServerAPIConnsKickReq{ |
||||
uuid: uuid, |
||||
res: make(chan srtServerAPIConnsKickRes), |
||||
} |
||||
|
||||
select { |
||||
case s.chAPIConnsKick <- req: |
||||
res := <-req.res |
||||
return res.err |
||||
|
||||
case <-s.ctx.Done(): |
||||
return fmt.Errorf("terminated") |
||||
} |
||||
} |
@ -0,0 +1,115 @@
@@ -0,0 +1,115 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"bufio" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/bluenviron/mediacommon/pkg/formats/mpegts" |
||||
"github.com/datarhei/gosrt" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestSRTServer(t *testing.T) { |
||||
p, ok := newInstance("paths:\n" + |
||||
" all:\n") |
||||
require.Equal(t, true, ok) |
||||
defer p.Close() |
||||
|
||||
conf := srt.DefaultConfig() |
||||
address, err := conf.UnmarshalURL("srt://localhost:8890?streamid=publish:mypath") |
||||
require.NoError(t, err) |
||||
|
||||
err = conf.Validate() |
||||
require.NoError(t, err) |
||||
|
||||
publisher, err := srt.Dial("srt", address, conf) |
||||
require.NoError(t, err) |
||||
defer publisher.Close() |
||||
|
||||
track := &mpegts.Track{ |
||||
PID: 256, |
||||
Codec: &mpegts.CodecH264{}, |
||||
} |
||||
|
||||
bw := bufio.NewWriter(publisher) |
||||
w := mpegts.NewWriter(bw, []*mpegts.Track{track}) |
||||
require.NoError(t, err) |
||||
|
||||
err = w.WriteH26x(track, 0, 0, true, [][]byte{ |
||||
{ // SPS
|
||||
0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, |
||||
0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, |
||||
0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, |
||||
0x20, |
||||
}, |
||||
{ // PPS
|
||||
0x08, 0x06, 0x07, 0x08, |
||||
}, |
||||
{ // IDR
|
||||
0x05, 1, |
||||
}, |
||||
}) |
||||
require.NoError(t, err) |
||||
bw.Flush() |
||||
|
||||
time.Sleep(500 * time.Millisecond) |
||||
|
||||
conf = srt.DefaultConfig() |
||||
address, err = conf.UnmarshalURL("srt://localhost:8890?streamid=read:mypath") |
||||
require.NoError(t, err) |
||||
|
||||
err = conf.Validate() |
||||
require.NoError(t, err) |
||||
|
||||
reader, err := srt.Dial("srt", address, conf) |
||||
require.NoError(t, err) |
||||
defer reader.Close() |
||||
|
||||
err = w.WriteH26x(track, 2*90000, 1*90000, true, [][]byte{ |
||||
{ // IDR
|
||||
0x05, 2, |
||||
}, |
||||
}) |
||||
require.NoError(t, err) |
||||
bw.Flush() |
||||
|
||||
r, err := mpegts.NewReader(reader) |
||||
require.NoError(t, err) |
||||
|
||||
require.Equal(t, []*mpegts.Track{{ |
||||
PID: 256, |
||||
Codec: &mpegts.CodecH264{}, |
||||
}}, r.Tracks()) |
||||
|
||||
received := false |
||||
|
||||
r.OnDataH26x(r.Tracks()[0], func(pts int64, dts int64, au [][]byte) error { |
||||
require.Equal(t, int64(0), pts) |
||||
require.Equal(t, int64(0), dts) |
||||
require.Equal(t, [][]byte{ |
||||
{ // SPS
|
||||
0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, |
||||
0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04, |
||||
0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, |
||||
0x20, |
||||
}, |
||||
{ // PPS
|
||||
0x08, 0x06, 0x07, 0x08, |
||||
}, |
||||
{ // IDR
|
||||
0x05, 1, |
||||
}, |
||||
}, au) |
||||
received = true |
||||
return nil |
||||
}) |
||||
|
||||
for { |
||||
err = r.Read() |
||||
require.NoError(t, err) |
||||
if received { |
||||
break |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,221 @@
@@ -0,0 +1,221 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"context" |
||||
"time" |
||||
|
||||
"github.com/bluenviron/gortsplib/v3/pkg/formats" |
||||
"github.com/bluenviron/gortsplib/v3/pkg/media" |
||||
"github.com/bluenviron/mediacommon/pkg/formats/mpegts" |
||||
"github.com/datarhei/gosrt" |
||||
|
||||
"github.com/bluenviron/mediamtx/internal/conf" |
||||
"github.com/bluenviron/mediamtx/internal/formatprocessor" |
||||
"github.com/bluenviron/mediamtx/internal/logger" |
||||
"github.com/bluenviron/mediamtx/internal/stream" |
||||
) |
||||
|
||||
type srtSourceParent interface { |
||||
logger.Writer |
||||
sourceStaticImplSetReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes |
||||
sourceStaticImplSetNotReady(req pathSourceStaticSetNotReadyReq) |
||||
} |
||||
|
||||
type srtSource struct { |
||||
readTimeout conf.StringDuration |
||||
parent srtSourceParent |
||||
} |
||||
|
||||
func newSRTSource( |
||||
readTimeout conf.StringDuration, |
||||
parent srtSourceParent, |
||||
) *srtSource { |
||||
s := &srtSource{ |
||||
readTimeout: readTimeout, |
||||
parent: parent, |
||||
} |
||||
|
||||
return s |
||||
} |
||||
|
||||
func (s *srtSource) Log(level logger.Level, format string, args ...interface{}) { |
||||
s.parent.Log(level, "[srt source] "+format, args...) |
||||
} |
||||
|
||||
// run implements sourceStaticImpl.
|
||||
func (s *srtSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf chan *conf.PathConf) error { |
||||
s.Log(logger.Debug, "connecting") |
||||
|
||||
conf := srt.DefaultConfig() |
||||
address, err := conf.UnmarshalURL(cnf.Source) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
err = conf.Validate() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
sconn, err := srt.Dial("srt", address, conf) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
readDone := make(chan error) |
||||
go func() { |
||||
readDone <- s.runReader(sconn) |
||||
}() |
||||
|
||||
for { |
||||
select { |
||||
case err := <-readDone: |
||||
sconn.Close() |
||||
return err |
||||
|
||||
case <-reloadConf: |
||||
|
||||
case <-ctx.Done(): |
||||
sconn.Close() |
||||
<-readDone |
||||
return nil |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *srtSource) runReader(sconn srt.Conn) error { |
||||
sconn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout))) |
||||
r, err := mpegts.NewReader(mpegts.NewBufferedReader(sconn)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
var medias media.Medias |
||||
var stream *stream.Stream |
||||
|
||||
var td *mpegts.TimeDecoder |
||||
decodeTime := func(t int64) time.Duration { |
||||
if td == nil { |
||||
td = mpegts.NewTimeDecoder(t) |
||||
} |
||||
return td.Decode(t) |
||||
} |
||||
|
||||
for _, track := range r.Tracks() { //nolint:dupl
|
||||
var medi *media.Media |
||||
|
||||
switch tcodec := track.Codec.(type) { |
||||
case *mpegts.CodecH264: |
||||
medi = &media.Media{ |
||||
Type: media.TypeVideo, |
||||
Formats: []formats.Format{&formats.H264{ |
||||
PayloadTyp: 96, |
||||
PacketizationMode: 1, |
||||
}}, |
||||
} |
||||
|
||||
r.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) error { |
||||
stream.WriteUnit(medi, medi.Formats[0], &formatprocessor.UnitH264{ |
||||
BaseUnit: formatprocessor.BaseUnit{ |
||||
NTP: time.Now(), |
||||
}, |
||||
PTS: decodeTime(pts), |
||||
AU: au, |
||||
}) |
||||
return nil |
||||
}) |
||||
|
||||
case *mpegts.CodecH265: |
||||
medi = &media.Media{ |
||||
Type: media.TypeVideo, |
||||
Formats: []formats.Format{&formats.H265{ |
||||
PayloadTyp: 96, |
||||
}}, |
||||
} |
||||
|
||||
r.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) error { |
||||
stream.WriteUnit(medi, medi.Formats[0], &formatprocessor.UnitH265{ |
||||
BaseUnit: formatprocessor.BaseUnit{ |
||||
NTP: time.Now(), |
||||
}, |
||||
PTS: decodeTime(pts), |
||||
AU: au, |
||||
}) |
||||
return nil |
||||
}) |
||||
|
||||
case *mpegts.CodecMPEG4Audio: |
||||
medi = &media.Media{ |
||||
Type: media.TypeAudio, |
||||
Formats: []formats.Format{&formats.MPEG4Audio{ |
||||
PayloadTyp: 96, |
||||
SizeLength: 13, |
||||
IndexLength: 3, |
||||
IndexDeltaLength: 3, |
||||
Config: &tcodec.Config, |
||||
}}, |
||||
} |
||||
|
||||
r.OnDataMPEG4Audio(track, func(pts int64, _ int64, aus [][]byte) error { |
||||
stream.WriteUnit(medi, medi.Formats[0], &formatprocessor.UnitMPEG4AudioGeneric{ |
||||
BaseUnit: formatprocessor.BaseUnit{ |
||||
NTP: time.Now(), |
||||
}, |
||||
PTS: decodeTime(pts), |
||||
AUs: aus, |
||||
}) |
||||
return nil |
||||
}) |
||||
|
||||
case *mpegts.CodecOpus: |
||||
medi = &media.Media{ |
||||
Type: media.TypeAudio, |
||||
Formats: []formats.Format{&formats.Opus{ |
||||
PayloadTyp: 96, |
||||
IsStereo: (tcodec.ChannelCount == 2), |
||||
}}, |
||||
} |
||||
|
||||
r.OnDataOpus(track, func(pts int64, _ int64, packets [][]byte) error { |
||||
stream.WriteUnit(medi, medi.Formats[0], &formatprocessor.UnitOpus{ |
||||
BaseUnit: formatprocessor.BaseUnit{ |
||||
NTP: time.Now(), |
||||
}, |
||||
PTS: decodeTime(pts), |
||||
Packets: packets, |
||||
}) |
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
medias = append(medias, medi) |
||||
} |
||||
|
||||
res := s.parent.sourceStaticImplSetReady(pathSourceStaticSetReadyReq{ |
||||
medias: medias, |
||||
generateRTPPackets: true, |
||||
}) |
||||
if res.err != nil { |
||||
return res.err |
||||
} |
||||
|
||||
s.Log(logger.Info, "ready: %s", sourceMediaInfo(medias)) |
||||
|
||||
stream = res.stream |
||||
|
||||
for { |
||||
sconn.SetReadDeadline(time.Now().Add(time.Duration(s.readTimeout))) |
||||
err := r.Read() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
|
||||
// apiSourceDescribe implements sourceStaticImpl.
|
||||
func (*srtSource) apiSourceDescribe() pathAPISourceOrReader { |
||||
return pathAPISourceOrReader{ |
||||
Type: "srtSource", |
||||
ID: "", |
||||
} |
||||
} |
@ -0,0 +1,98 @@
@@ -0,0 +1,98 @@
|
||||
package core |
||||
|
||||
import ( |
||||
"bufio" |
||||
"testing" |
||||
|
||||
"github.com/bluenviron/gortsplib/v3" |
||||
"github.com/bluenviron/gortsplib/v3/pkg/url" |
||||
"github.com/bluenviron/mediacommon/pkg/formats/mpegts" |
||||
"github.com/datarhei/gosrt" |
||||
"github.com/pion/rtp" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestSRTSource(t *testing.T) { |
||||
ln, err := srt.Listen("srt", "localhost:9999", srt.DefaultConfig()) |
||||
require.NoError(t, err) |
||||
defer ln.Close() |
||||
|
||||
connected := make(chan struct{}) |
||||
received := make(chan struct{}) |
||||
done := make(chan struct{}) |
||||
|
||||
go func() { |
||||
conn, _, err := ln.Accept(func(req srt.ConnRequest) srt.ConnType { |
||||
require.Equal(t, "sidname", req.StreamId()) |
||||
|
||||
err := req.SetPassphrase("ttest1234567") |
||||
if err != nil { |
||||
return srt.REJECT |
||||
} |
||||
|
||||
return srt.SUBSCRIBE |
||||
}) |
||||
require.NoError(t, err) |
||||
require.NotNil(t, conn) |
||||
defer conn.Close() |
||||
|
||||
track := &mpegts.Track{ |
||||
PID: 256, |
||||
Codec: &mpegts.CodecH264{}, |
||||
} |
||||
|
||||
bw := bufio.NewWriter(conn) |
||||
w := mpegts.NewWriter(bw, []*mpegts.Track{track}) |
||||
require.NoError(t, err) |
||||
|
||||
err = w.WriteH26x(track, 0, 0, true, [][]byte{ |
||||
{ // IDR
|
||||
0x05, 1, |
||||
}, |
||||
}) |
||||
require.NoError(t, err) |
||||
bw.Flush() |
||||
|
||||
<-connected |
||||
|
||||
err = w.WriteH26x(track, 0, 0, true, [][]byte{{5, 2}}) |
||||
require.NoError(t, err) |
||||
bw.Flush() |
||||
|
||||
<-done |
||||
}() |
||||
|
||||
p, ok := newInstance("paths:\n" + |
||||
" proxied:\n" + |
||||
" source: srt://localhost:9999?streamid=sidname&passphrase=ttest1234567\n" + |
||||
" sourceOnDemand: yes\n") |
||||
require.Equal(t, true, ok) |
||||
defer p.Close() |
||||
|
||||
c := gortsplib.Client{} |
||||
|
||||
u, err := url.Parse("rtsp://127.0.0.1:8554/proxied") |
||||
require.NoError(t, err) |
||||
|
||||
err = c.Start(u.Scheme, u.Host) |
||||
require.NoError(t, err) |
||||
defer c.Close() |
||||
|
||||
medias, baseURL, _, err := c.Describe(u) |
||||
require.NoError(t, err) |
||||
|
||||
err = c.SetupAll(medias, baseURL) |
||||
require.NoError(t, err) |
||||
|
||||
c.OnPacketRTP(medias[0], medias[0].Formats[0], func(pkt *rtp.Packet) { |
||||
require.Equal(t, []byte{5, 1}, pkt.Payload) |
||||
close(received) |
||||
}) |
||||
|
||||
_, err = c.Play(nil) |
||||
require.NoError(t, err) |
||||
|
||||
close(connected) |
||||
<-received |
||||
close(done) |
||||
} |
Loading…
Reference in new issue