mirror of
https://github.com/bluenviron/mediamtx.git
synced 2025-12-20 02:00:05 -08:00
estimate absolute timestamp more precisely (#5078)
When the absolute timestamp of incoming frames was not available, it was filled with the current timestamp, which is influenced by latency over time. This mechanism is replaced by an algorithm that detects when latency is the lowest, stores the current timestamp and uses it as reference throughout the rest of the stream.
This commit is contained in:
parent
f5f03562d3
commit
0cdae40fe3
30 changed files with 296 additions and 178 deletions
|
|
@ -377,7 +377,7 @@ func (pa *path) doReloadConf(newConf *conf.Path) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pa *path) doSourceStaticSetReady(req defs.PathSourceStaticSetReadyReq) {
|
func (pa *path) doSourceStaticSetReady(req defs.PathSourceStaticSetReadyReq) {
|
||||||
err := pa.setReady(req.Desc, req.GenerateRTPPackets)
|
err := pa.setReady(req.Desc, req.GenerateRTPPackets, req.FillNTP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
req.Res <- defs.PathSourceStaticSetReadyRes{Err: err}
|
req.Res <- defs.PathSourceStaticSetReadyRes{Err: err}
|
||||||
return
|
return
|
||||||
|
|
@ -474,7 +474,7 @@ func (pa *path) doAddPublisher(req defs.PathAddPublisherReq) {
|
||||||
pa.source = req.Author
|
pa.source = req.Author
|
||||||
pa.publisherQuery = req.AccessRequest.Query
|
pa.publisherQuery = req.AccessRequest.Query
|
||||||
|
|
||||||
err := pa.setReady(req.Desc, req.GenerateRTPPackets)
|
err := pa.setReady(req.Desc, req.GenerateRTPPackets, req.FillNTP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pa.source = nil
|
pa.source = nil
|
||||||
req.Res <- defs.PathAddPublisherRes{Err: err}
|
req.Res <- defs.PathAddPublisherRes{Err: err}
|
||||||
|
|
@ -684,12 +684,13 @@ func (pa *path) onDemandPublisherStop(reason string) {
|
||||||
pa.onDemandPublisherState = pathOnDemandStateInitial
|
pa.onDemandPublisherState = pathOnDemandStateInitial
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pa *path) setReady(desc *description.Session, allocateEncoder bool) error {
|
func (pa *path) setReady(desc *description.Session, generateRTPPackets bool, fillNTP bool) error {
|
||||||
pa.stream = &stream.Stream{
|
pa.stream = &stream.Stream{
|
||||||
WriteQueueSize: pa.writeQueueSize,
|
WriteQueueSize: pa.writeQueueSize,
|
||||||
RTPMaxPayloadSize: pa.rtpMaxPayloadSize,
|
RTPMaxPayloadSize: pa.rtpMaxPayloadSize,
|
||||||
Desc: desc,
|
Desc: desc,
|
||||||
GenerateRTPPackets: allocateEncoder,
|
GenerateRTPPackets: generateRTPPackets,
|
||||||
|
FillNTP: fillNTP,
|
||||||
Parent: pa.source,
|
Parent: pa.source,
|
||||||
}
|
}
|
||||||
err := pa.stream.Initialize()
|
err := pa.stream.Initialize()
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ type PathAddPublisherReq struct {
|
||||||
Author Publisher
|
Author Publisher
|
||||||
Desc *description.Session
|
Desc *description.Session
|
||||||
GenerateRTPPackets bool
|
GenerateRTPPackets bool
|
||||||
|
FillNTP bool
|
||||||
ConfToCompare *conf.Path
|
ConfToCompare *conf.Path
|
||||||
AccessRequest PathAccessRequest
|
AccessRequest PathAccessRequest
|
||||||
Res chan PathAddPublisherRes
|
Res chan PathAddPublisherRes
|
||||||
|
|
@ -108,6 +109,7 @@ type PathSourceStaticSetReadyRes struct {
|
||||||
type PathSourceStaticSetReadyReq struct {
|
type PathSourceStaticSetReadyReq struct {
|
||||||
Desc *description.Session
|
Desc *description.Session
|
||||||
GenerateRTPPackets bool
|
GenerateRTPPackets bool
|
||||||
|
FillNTP bool
|
||||||
Res chan PathSourceStaticSetReadyRes
|
Res chan PathSourceStaticSetReadyRes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
45
internal/ntpestimator/estimator.go
Normal file
45
internal/ntpestimator/estimator.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Package ntpestimator contains a NTP estimator.
|
||||||
|
package ntpestimator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var timeNow = time.Now
|
||||||
|
|
||||||
|
func multiplyAndDivide(v, m, d time.Duration) time.Duration {
|
||||||
|
secs := v / d
|
||||||
|
dec := v % d
|
||||||
|
return (secs*m + dec*m/d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estimator is a NTP estimator.
|
||||||
|
type Estimator struct {
|
||||||
|
ClockRate int
|
||||||
|
|
||||||
|
refNTP time.Time
|
||||||
|
refPTS int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var zero = time.Time{}
|
||||||
|
|
||||||
|
// Estimate returns estimated NTP.
|
||||||
|
func (e *Estimator) Estimate(pts int64) time.Time {
|
||||||
|
now := timeNow()
|
||||||
|
|
||||||
|
if e.refNTP.Equal(zero) {
|
||||||
|
e.refNTP = now
|
||||||
|
e.refPTS = pts
|
||||||
|
return now
|
||||||
|
}
|
||||||
|
|
||||||
|
computed := e.refNTP.Add((multiplyAndDivide(time.Duration(pts-e.refPTS), time.Second, time.Duration(e.ClockRate))))
|
||||||
|
|
||||||
|
if computed.After(now) {
|
||||||
|
e.refNTP = now
|
||||||
|
e.refPTS = pts
|
||||||
|
return now
|
||||||
|
}
|
||||||
|
|
||||||
|
return computed
|
||||||
|
}
|
||||||
32
internal/ntpestimator/estimator_test.go
Normal file
32
internal/ntpestimator/estimator_test.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
package ntpestimator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEstimator(t *testing.T) {
|
||||||
|
e := &Estimator{ClockRate: 90000}
|
||||||
|
|
||||||
|
timeNow = func() time.Time { return time.Date(2003, 11, 4, 23, 15, 7, 0, time.UTC) }
|
||||||
|
ntp := e.Estimate(90000)
|
||||||
|
require.Equal(t, time.Date(2003, 11, 4, 23, 15, 7, 0, time.UTC), ntp)
|
||||||
|
|
||||||
|
timeNow = func() time.Time { return time.Date(2003, 11, 4, 23, 15, 8, 0, time.UTC) }
|
||||||
|
ntp = e.Estimate(2 * 90000)
|
||||||
|
require.Equal(t, time.Date(2003, 11, 4, 23, 15, 8, 0, time.UTC), ntp)
|
||||||
|
|
||||||
|
timeNow = func() time.Time { return time.Date(2003, 11, 4, 23, 15, 10, 0, time.UTC) }
|
||||||
|
ntp = e.Estimate(3 * 90000)
|
||||||
|
require.Equal(t, time.Date(2003, 11, 4, 23, 15, 9, 0, time.UTC), ntp)
|
||||||
|
|
||||||
|
timeNow = func() time.Time { return time.Date(2003, 11, 4, 23, 15, 9, 0, time.UTC) }
|
||||||
|
ntp = e.Estimate(4 * 90000)
|
||||||
|
require.Equal(t, time.Date(2003, 11, 4, 23, 15, 9, 0, time.UTC), ntp)
|
||||||
|
|
||||||
|
timeNow = func() time.Time { return time.Date(2003, 11, 4, 23, 15, 15, 0, time.UTC) }
|
||||||
|
ntp = e.Estimate(5 * 90000)
|
||||||
|
require.Equal(t, time.Date(2003, 11, 4, 23, 15, 10, 0, time.UTC), ntp)
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
||||||
"github.com/bluenviron/gortsplib/v5/pkg/format"
|
"github.com/bluenviron/gortsplib/v5/pkg/format"
|
||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/ntpestimator"
|
||||||
"github.com/bluenviron/mediamtx/internal/stream"
|
"github.com/bluenviron/mediamtx/internal/stream"
|
||||||
"github.com/bluenviron/mediamtx/internal/unit"
|
"github.com/bluenviron/mediamtx/internal/unit"
|
||||||
)
|
)
|
||||||
|
|
@ -32,55 +33,54 @@ func multiplyAndDivide(v, m, d int64) int64 {
|
||||||
func ToStream(
|
func ToStream(
|
||||||
c *gohlslib.Client,
|
c *gohlslib.Client,
|
||||||
tracks []*gohlslib.Track,
|
tracks []*gohlslib.Track,
|
||||||
stream **stream.Stream,
|
strm **stream.Stream,
|
||||||
) ([]*description.Media, error) {
|
) ([]*description.Media, error) {
|
||||||
var ntpStat ntpState
|
var ntpStat ntpState
|
||||||
var ntpStatMutex sync.Mutex
|
var ntpStatMutex sync.Mutex
|
||||||
|
|
||||||
handleNTP := func(track *gohlslib.Track) time.Time {
|
|
||||||
ntpStatMutex.Lock()
|
|
||||||
defer ntpStatMutex.Unlock()
|
|
||||||
|
|
||||||
switch ntpStat {
|
|
||||||
case ntpStateInitial:
|
|
||||||
ntp, avail := c.AbsoluteTime(track)
|
|
||||||
if !avail {
|
|
||||||
ntpStat = ntpStateUnavailable
|
|
||||||
return time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
ntpStat = ntpStateAvailable
|
|
||||||
return ntp
|
|
||||||
|
|
||||||
case ntpStateAvailable:
|
|
||||||
ntp, avail := c.AbsoluteTime(track)
|
|
||||||
if !avail {
|
|
||||||
panic("should not happen")
|
|
||||||
}
|
|
||||||
|
|
||||||
return ntp
|
|
||||||
|
|
||||||
case ntpStateUnavailable:
|
|
||||||
_, avail := c.AbsoluteTime(track)
|
|
||||||
if avail {
|
|
||||||
(*stream).Parent.Log(logger.Warn, "absolute timestamp appeared after stream started, we are not using it")
|
|
||||||
ntpStat = ntpStateDegraded
|
|
||||||
}
|
|
||||||
|
|
||||||
return time.Now()
|
|
||||||
|
|
||||||
default: // ntpStateDegraded
|
|
||||||
return time.Now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var medias []*description.Media //nolint:prealloc
|
var medias []*description.Media //nolint:prealloc
|
||||||
|
|
||||||
for _, track := range tracks {
|
for _, track := range tracks {
|
||||||
var medi *description.Media
|
ctrack := track
|
||||||
clockRate := track.ClockRate
|
ntpEstimator := &ntpestimator.Estimator{ClockRate: track.ClockRate}
|
||||||
|
|
||||||
switch tcodec := track.Codec.(type) {
|
handleNTP := func(pts int64) time.Time {
|
||||||
|
ntpStatMutex.Lock()
|
||||||
|
defer ntpStatMutex.Unlock()
|
||||||
|
|
||||||
|
switch ntpStat {
|
||||||
|
case ntpStateInitial:
|
||||||
|
ntp, avail := c.AbsoluteTime(ctrack)
|
||||||
|
if !avail {
|
||||||
|
ntpStat = ntpStateUnavailable
|
||||||
|
return ntpEstimator.Estimate(pts)
|
||||||
|
}
|
||||||
|
ntpStat = ntpStateAvailable
|
||||||
|
return ntp
|
||||||
|
|
||||||
|
case ntpStateAvailable:
|
||||||
|
ntp, avail := c.AbsoluteTime(ctrack)
|
||||||
|
if !avail {
|
||||||
|
panic("should not happen")
|
||||||
|
}
|
||||||
|
return ntp
|
||||||
|
|
||||||
|
case ntpStateUnavailable:
|
||||||
|
_, avail := c.AbsoluteTime(ctrack)
|
||||||
|
if avail {
|
||||||
|
(*strm).Parent.Log(logger.Warn, "absolute timestamp appeared after stream started, we are not using it")
|
||||||
|
ntpStat = ntpStateDegraded
|
||||||
|
}
|
||||||
|
return ntpEstimator.Estimate(pts)
|
||||||
|
|
||||||
|
default: // ntpStateDegraded
|
||||||
|
return ntpEstimator.Estimate(pts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var medi *description.Media
|
||||||
|
|
||||||
|
switch tcodec := ctrack.Codec.(type) {
|
||||||
case *codecs.AV1:
|
case *codecs.AV1:
|
||||||
medi = &description.Media{
|
medi = &description.Media{
|
||||||
Type: description.MediaTypeVideo,
|
Type: description.MediaTypeVideo,
|
||||||
|
|
@ -90,10 +90,10 @@ func ToStream(
|
||||||
}
|
}
|
||||||
newClockRate := medi.Formats[0].ClockRate()
|
newClockRate := medi.Formats[0].ClockRate()
|
||||||
|
|
||||||
c.OnDataAV1(track, func(pts int64, tu [][]byte) {
|
c.OnDataAV1(ctrack, func(pts int64, tu [][]byte) {
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
||||||
NTP: handleNTP(track),
|
NTP: handleNTP(pts),
|
||||||
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(clockRate)),
|
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),
|
||||||
Payload: unit.PayloadAV1(tu),
|
Payload: unit.PayloadAV1(tu),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -107,10 +107,10 @@ func ToStream(
|
||||||
}
|
}
|
||||||
newClockRate := medi.Formats[0].ClockRate()
|
newClockRate := medi.Formats[0].ClockRate()
|
||||||
|
|
||||||
c.OnDataVP9(track, func(pts int64, frame []byte) {
|
c.OnDataVP9(ctrack, func(pts int64, frame []byte) {
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
||||||
NTP: handleNTP(track),
|
NTP: handleNTP(pts),
|
||||||
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(clockRate)),
|
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),
|
||||||
Payload: unit.PayloadVP9(frame),
|
Payload: unit.PayloadVP9(frame),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -127,10 +127,10 @@ func ToStream(
|
||||||
}
|
}
|
||||||
newClockRate := medi.Formats[0].ClockRate()
|
newClockRate := medi.Formats[0].ClockRate()
|
||||||
|
|
||||||
c.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) {
|
c.OnDataH26x(ctrack, func(pts int64, _ int64, au [][]byte) {
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
||||||
NTP: handleNTP(track),
|
NTP: handleNTP(pts),
|
||||||
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(clockRate)),
|
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),
|
||||||
Payload: unit.PayloadH265(au),
|
Payload: unit.PayloadH265(au),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -147,10 +147,10 @@ func ToStream(
|
||||||
}
|
}
|
||||||
newClockRate := medi.Formats[0].ClockRate()
|
newClockRate := medi.Formats[0].ClockRate()
|
||||||
|
|
||||||
c.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) {
|
c.OnDataH26x(ctrack, func(pts int64, _ int64, au [][]byte) {
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
||||||
NTP: handleNTP(track),
|
NTP: handleNTP(pts),
|
||||||
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(clockRate)),
|
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),
|
||||||
Payload: unit.PayloadH264(au),
|
Payload: unit.PayloadH264(au),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -165,10 +165,10 @@ func ToStream(
|
||||||
}
|
}
|
||||||
newClockRate := medi.Formats[0].ClockRate()
|
newClockRate := medi.Formats[0].ClockRate()
|
||||||
|
|
||||||
c.OnDataOpus(track, func(pts int64, packets [][]byte) {
|
c.OnDataOpus(ctrack, func(pts int64, packets [][]byte) {
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
||||||
NTP: handleNTP(track),
|
NTP: handleNTP(pts),
|
||||||
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(clockRate)),
|
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),
|
||||||
Payload: unit.PayloadOpus(packets),
|
Payload: unit.PayloadOpus(packets),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -186,10 +186,10 @@ func ToStream(
|
||||||
}
|
}
|
||||||
newClockRate := medi.Formats[0].ClockRate()
|
newClockRate := medi.Formats[0].ClockRate()
|
||||||
|
|
||||||
c.OnDataMPEG4Audio(track, func(pts int64, aus [][]byte) {
|
c.OnDataMPEG4Audio(ctrack, func(pts int64, aus [][]byte) {
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
||||||
NTP: handleNTP(track),
|
NTP: handleNTP(pts),
|
||||||
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(clockRate)),
|
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),
|
||||||
Payload: unit.PayloadMPEG4Audio(aus),
|
Payload: unit.PayloadMPEG4Audio(aus),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
||||||
"github.com/bluenviron/gortsplib/v5/pkg/format"
|
"github.com/bluenviron/gortsplib/v5/pkg/format"
|
||||||
|
|
@ -24,7 +23,7 @@ var errNoSupportedCodecs = errors.New(
|
||||||
// ToStream maps a MPEG-TS stream to a MediaMTX stream.
|
// ToStream maps a MPEG-TS stream to a MediaMTX stream.
|
||||||
func ToStream(
|
func ToStream(
|
||||||
r *EnhancedReader,
|
r *EnhancedReader,
|
||||||
stream **stream.Stream,
|
strm **stream.Stream,
|
||||||
l logger.Writer,
|
l logger.Writer,
|
||||||
) ([]*description.Media, error) {
|
) ([]*description.Media, error) {
|
||||||
var medias []*description.Media //nolint:prealloc
|
var medias []*description.Media //nolint:prealloc
|
||||||
|
|
@ -48,8 +47,7 @@ func ToStream(
|
||||||
r.OnDataH265(track, func(pts int64, _ int64, au [][]byte) error {
|
r.OnDataH265(track, func(pts int64, _ int64, au [][]byte) error {
|
||||||
pts = td.Decode(pts)
|
pts = td.Decode(pts)
|
||||||
|
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP
|
PTS: pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP
|
||||||
Payload: unit.PayloadH265(au),
|
Payload: unit.PayloadH265(au),
|
||||||
})
|
})
|
||||||
|
|
@ -68,8 +66,7 @@ func ToStream(
|
||||||
r.OnDataH264(track, func(pts int64, _ int64, au [][]byte) error {
|
r.OnDataH264(track, func(pts int64, _ int64, au [][]byte) error {
|
||||||
pts = td.Decode(pts)
|
pts = td.Decode(pts)
|
||||||
|
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP
|
PTS: pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP
|
||||||
Payload: unit.PayloadH264(au),
|
Payload: unit.PayloadH264(au),
|
||||||
})
|
})
|
||||||
|
|
@ -87,8 +84,7 @@ func ToStream(
|
||||||
r.OnDataMPEGxVideo(track, func(pts int64, frame []byte) error {
|
r.OnDataMPEGxVideo(track, func(pts int64, frame []byte) error {
|
||||||
pts = td.Decode(pts)
|
pts = td.Decode(pts)
|
||||||
|
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP
|
PTS: pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP
|
||||||
Payload: unit.PayloadMPEG4Video(frame),
|
Payload: unit.PayloadMPEG4Video(frame),
|
||||||
})
|
})
|
||||||
|
|
@ -104,8 +100,7 @@ func ToStream(
|
||||||
r.OnDataMPEGxVideo(track, func(pts int64, frame []byte) error {
|
r.OnDataMPEGxVideo(track, func(pts int64, frame []byte) error {
|
||||||
pts = td.Decode(pts)
|
pts = td.Decode(pts)
|
||||||
|
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP
|
PTS: pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP
|
||||||
Payload: unit.PayloadMPEG1Video(frame),
|
Payload: unit.PayloadMPEG1Video(frame),
|
||||||
})
|
})
|
||||||
|
|
@ -124,8 +119,7 @@ func ToStream(
|
||||||
r.OnDataOpus(track, func(pts int64, packets [][]byte) error {
|
r.OnDataOpus(track, func(pts int64, packets [][]byte) error {
|
||||||
pts = td.Decode(pts)
|
pts = td.Decode(pts)
|
||||||
|
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: multiplyAndDivide(pts, int64(medi.Formats[0].ClockRate()), 90000),
|
PTS: multiplyAndDivide(pts, int64(medi.Formats[0].ClockRate()), 90000),
|
||||||
Payload: unit.PayloadOpus(packets),
|
Payload: unit.PayloadOpus(packets),
|
||||||
})
|
})
|
||||||
|
|
@ -142,8 +136,7 @@ func ToStream(
|
||||||
r.OnDataKLV(track, func(pts int64, uni []byte) error {
|
r.OnDataKLV(track, func(pts int64, uni []byte) error {
|
||||||
pts = td.Decode(pts)
|
pts = td.Decode(pts)
|
||||||
|
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: pts,
|
PTS: pts,
|
||||||
Payload: unit.PayloadKLV(uni),
|
Payload: unit.PayloadKLV(uni),
|
||||||
})
|
})
|
||||||
|
|
@ -165,8 +158,7 @@ func ToStream(
|
||||||
r.OnDataMPEG4Audio(track, func(pts int64, aus [][]byte) error {
|
r.OnDataMPEG4Audio(track, func(pts int64, aus [][]byte) error {
|
||||||
pts = td.Decode(pts)
|
pts = td.Decode(pts)
|
||||||
|
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: multiplyAndDivide(pts, int64(medi.Formats[0].ClockRate()), 90000),
|
PTS: multiplyAndDivide(pts, int64(medi.Formats[0].ClockRate()), 90000),
|
||||||
Payload: unit.PayloadMPEG4Audio(aus),
|
Payload: unit.PayloadMPEG4Audio(aus),
|
||||||
})
|
})
|
||||||
|
|
@ -217,8 +209,7 @@ func ToStream(
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: pts,
|
PTS: pts,
|
||||||
Payload: unit.PayloadMPEG4AudioLATM(buf),
|
Payload: unit.PayloadMPEG4AudioLATM(buf),
|
||||||
})
|
})
|
||||||
|
|
@ -238,8 +229,7 @@ func ToStream(
|
||||||
r.OnDataMPEG1Audio(track, func(pts int64, frames [][]byte) error {
|
r.OnDataMPEG1Audio(track, func(pts int64, frames [][]byte) error {
|
||||||
pts = td.Decode(pts)
|
pts = td.Decode(pts)
|
||||||
|
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP
|
PTS: pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP
|
||||||
Payload: unit.PayloadMPEG1Audio(frames),
|
Payload: unit.PayloadMPEG1Audio(frames),
|
||||||
})
|
})
|
||||||
|
|
@ -259,8 +249,7 @@ func ToStream(
|
||||||
r.OnDataAC3(track, func(pts int64, frame []byte) error {
|
r.OnDataAC3(track, func(pts int64, frame []byte) error {
|
||||||
pts = td.Decode(pts)
|
pts = td.Decode(pts)
|
||||||
|
|
||||||
(*stream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: multiplyAndDivide(pts, int64(medi.Formats[0].ClockRate()), 90000),
|
PTS: multiplyAndDivide(pts, int64(medi.Formats[0].ClockRate()), 90000),
|
||||||
Payload: unit.PayloadAC3{frame},
|
Payload: unit.PayloadAC3{frame},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -267,8 +267,6 @@ func FromStream(
|
||||||
}
|
}
|
||||||
|
|
||||||
case *format.MPEG1Audio:
|
case *format.MPEG1Audio:
|
||||||
// TODO: check sample rate and layer,
|
|
||||||
// unfortunately they are not available at this stage.
|
|
||||||
r.OnData(
|
r.OnData(
|
||||||
media,
|
media,
|
||||||
forma,
|
forma,
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,10 @@ func fourCCToString(c message.FourCC) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToStream maps a RTMP stream to a MediaMTX stream.
|
// ToStream maps a RTMP stream to a MediaMTX stream.
|
||||||
func ToStream(r *gortmplib.Reader, stream **stream.Stream) ([]*description.Media, error) {
|
func ToStream(
|
||||||
|
r *gortmplib.Reader,
|
||||||
|
strm **stream.Stream,
|
||||||
|
) ([]*description.Media, error) {
|
||||||
var medias []*description.Media
|
var medias []*description.Media
|
||||||
|
|
||||||
for _, track := range r.Tracks() {
|
for _, track := range r.Tracks() {
|
||||||
|
|
@ -46,8 +49,7 @@ func ToStream(r *gortmplib.Reader, stream **stream.Stream) ([]*description.Media
|
||||||
medias = append(medias, medi)
|
medias = append(medias, medi)
|
||||||
|
|
||||||
r.OnDataAV1(ttrack, func(pts time.Duration, tu [][]byte) {
|
r.OnDataAV1(ttrack, func(pts time.Duration, tu [][]byte) {
|
||||||
(*stream).WriteUnit(medi, ctrack, &unit.Unit{
|
(*strm).WriteUnit(medi, ctrack, &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
||||||
Payload: unit.PayloadAV1(tu),
|
Payload: unit.PayloadAV1(tu),
|
||||||
})
|
})
|
||||||
|
|
@ -61,8 +63,7 @@ func ToStream(r *gortmplib.Reader, stream **stream.Stream) ([]*description.Media
|
||||||
medias = append(medias, medi)
|
medias = append(medias, medi)
|
||||||
|
|
||||||
r.OnDataVP9(ttrack, func(pts time.Duration, frame []byte) {
|
r.OnDataVP9(ttrack, func(pts time.Duration, frame []byte) {
|
||||||
(*stream).WriteUnit(medi, ctrack, &unit.Unit{
|
(*strm).WriteUnit(medi, ctrack, &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
||||||
Payload: unit.PayloadVP9(frame),
|
Payload: unit.PayloadVP9(frame),
|
||||||
})
|
})
|
||||||
|
|
@ -76,8 +77,7 @@ func ToStream(r *gortmplib.Reader, stream **stream.Stream) ([]*description.Media
|
||||||
medias = append(medias, medi)
|
medias = append(medias, medi)
|
||||||
|
|
||||||
r.OnDataH265(ttrack, func(pts time.Duration, _ time.Duration, au [][]byte) {
|
r.OnDataH265(ttrack, func(pts time.Duration, _ time.Duration, au [][]byte) {
|
||||||
(*stream).WriteUnit(medi, ctrack, &unit.Unit{
|
(*strm).WriteUnit(medi, ctrack, &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
||||||
Payload: unit.PayloadH265(au),
|
Payload: unit.PayloadH265(au),
|
||||||
})
|
})
|
||||||
|
|
@ -91,8 +91,7 @@ func ToStream(r *gortmplib.Reader, stream **stream.Stream) ([]*description.Media
|
||||||
medias = append(medias, medi)
|
medias = append(medias, medi)
|
||||||
|
|
||||||
r.OnDataH264(ttrack, func(pts time.Duration, _ time.Duration, au [][]byte) {
|
r.OnDataH264(ttrack, func(pts time.Duration, _ time.Duration, au [][]byte) {
|
||||||
(*stream).WriteUnit(medi, ctrack, &unit.Unit{
|
(*strm).WriteUnit(medi, ctrack, &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
||||||
Payload: unit.PayloadH264(au),
|
Payload: unit.PayloadH264(au),
|
||||||
})
|
})
|
||||||
|
|
@ -106,8 +105,7 @@ func ToStream(r *gortmplib.Reader, stream **stream.Stream) ([]*description.Media
|
||||||
medias = append(medias, medi)
|
medias = append(medias, medi)
|
||||||
|
|
||||||
r.OnDataOpus(ttrack, func(pts time.Duration, packet []byte) {
|
r.OnDataOpus(ttrack, func(pts time.Duration, packet []byte) {
|
||||||
(*stream).WriteUnit(medi, ctrack, &unit.Unit{
|
(*strm).WriteUnit(medi, ctrack, &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
||||||
Payload: unit.PayloadOpus{packet},
|
Payload: unit.PayloadOpus{packet},
|
||||||
})
|
})
|
||||||
|
|
@ -121,8 +119,7 @@ func ToStream(r *gortmplib.Reader, stream **stream.Stream) ([]*description.Media
|
||||||
medias = append(medias, medi)
|
medias = append(medias, medi)
|
||||||
|
|
||||||
r.OnDataMPEG4Audio(ttrack, func(pts time.Duration, au []byte) {
|
r.OnDataMPEG4Audio(ttrack, func(pts time.Duration, au []byte) {
|
||||||
(*stream).WriteUnit(medi, ctrack, &unit.Unit{
|
(*strm).WriteUnit(medi, ctrack, &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
||||||
Payload: unit.PayloadMPEG4Audio{au},
|
Payload: unit.PayloadMPEG4Audio{au},
|
||||||
})
|
})
|
||||||
|
|
@ -136,8 +133,7 @@ func ToStream(r *gortmplib.Reader, stream **stream.Stream) ([]*description.Media
|
||||||
medias = append(medias, medi)
|
medias = append(medias, medi)
|
||||||
|
|
||||||
r.OnDataMPEG1Audio(ttrack, func(pts time.Duration, frame []byte) {
|
r.OnDataMPEG1Audio(ttrack, func(pts time.Duration, frame []byte) {
|
||||||
(*stream).WriteUnit(medi, ctrack, &unit.Unit{
|
(*strm).WriteUnit(medi, ctrack, &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
||||||
Payload: unit.PayloadMPEG1Audio{frame},
|
Payload: unit.PayloadMPEG1Audio{frame},
|
||||||
})
|
})
|
||||||
|
|
@ -151,8 +147,7 @@ func ToStream(r *gortmplib.Reader, stream **stream.Stream) ([]*description.Media
|
||||||
medias = append(medias, medi)
|
medias = append(medias, medi)
|
||||||
|
|
||||||
r.OnDataAC3(ttrack, func(pts time.Duration, frame []byte) {
|
r.OnDataAC3(ttrack, func(pts time.Duration, frame []byte) {
|
||||||
(*stream).WriteUnit(medi, ctrack, &unit.Unit{
|
(*strm).WriteUnit(medi, ctrack, &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
||||||
Payload: unit.PayloadAC3{frame},
|
Payload: unit.PayloadAC3{frame},
|
||||||
})
|
})
|
||||||
|
|
@ -166,8 +161,7 @@ func ToStream(r *gortmplib.Reader, stream **stream.Stream) ([]*description.Media
|
||||||
medias = append(medias, medi)
|
medias = append(medias, medi)
|
||||||
|
|
||||||
r.OnDataG711(ttrack, func(pts time.Duration, samples []byte) {
|
r.OnDataG711(ttrack, func(pts time.Duration, samples []byte) {
|
||||||
(*stream).WriteUnit(medi, ctrack, &unit.Unit{
|
(*strm).WriteUnit(medi, ctrack, &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
||||||
Payload: unit.PayloadG711(samples),
|
Payload: unit.PayloadG711(samples),
|
||||||
})
|
})
|
||||||
|
|
@ -181,8 +175,7 @@ func ToStream(r *gortmplib.Reader, stream **stream.Stream) ([]*description.Media
|
||||||
medias = append(medias, medi)
|
medias = append(medias, medi)
|
||||||
|
|
||||||
r.OnDataLPCM(ttrack, func(pts time.Duration, samples []byte) {
|
r.OnDataLPCM(ttrack, func(pts time.Duration, samples []byte) {
|
||||||
(*stream).WriteUnit(medi, ctrack, &unit.Unit{
|
(*strm).WriteUnit(medi, ctrack, &unit.Unit{
|
||||||
NTP: time.Now(),
|
|
||||||
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
PTS: durationToTimestamp(pts, ctrack.ClockRate()),
|
||||||
Payload: unit.PayloadLPCM(samples),
|
Payload: unit.PayloadLPCM(samples),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ func ToStream(
|
||||||
handleNTP := func(pkt *rtp.Packet) (time.Time, bool) {
|
handleNTP := func(pkt *rtp.Packet) (time.Time, bool) {
|
||||||
switch ntpStat {
|
switch ntpStat {
|
||||||
case ntpStateReplace:
|
case ntpStateReplace:
|
||||||
return time.Now(), true
|
return time.Time{}, true
|
||||||
|
|
||||||
case ntpStateInitial:
|
case ntpStateInitial:
|
||||||
ntp, avail := source.PacketNTP(cmedi, pkt)
|
ntp, avail := source.PacketNTP(cmedi, pkt)
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@ func TestFromStreamResampleOpus(t *testing.T) {
|
||||||
n := 0
|
n := 0
|
||||||
var ts uint32
|
var ts uint32
|
||||||
|
|
||||||
tracks[0].OnPacketRTP = func(pkt *rtp.Packet, _ time.Time) {
|
tracks[0].OnPacketRTP = func(pkt *rtp.Packet) {
|
||||||
n++
|
n++
|
||||||
|
|
||||||
switch n {
|
switch n {
|
||||||
|
|
|
||||||
|
|
@ -237,22 +237,21 @@ var incomingAudioCodecs = []webrtc.RTPCodecParameters{
|
||||||
|
|
||||||
// IncomingTrack is an incoming track.
|
// IncomingTrack is an incoming track.
|
||||||
type IncomingTrack struct {
|
type IncomingTrack struct {
|
||||||
OnPacketRTP func(*rtp.Packet, time.Time)
|
OnPacketRTP func(*rtp.Packet)
|
||||||
|
|
||||||
useAbsoluteTimestamp bool
|
track *webrtc.TrackRemote
|
||||||
track *webrtc.TrackRemote
|
receiver *webrtc.RTPReceiver
|
||||||
receiver *webrtc.RTPReceiver
|
writeRTCP func([]rtcp.Packet) error
|
||||||
writeRTCP func([]rtcp.Packet) error
|
log logger.Writer
|
||||||
log logger.Writer
|
rtpPacketsReceived *uint64
|
||||||
rtpPacketsReceived *uint64
|
rtpPacketsLost *uint64
|
||||||
rtpPacketsLost *uint64
|
|
||||||
|
|
||||||
packetsLost *counterdumper.CounterDumper
|
packetsLost *counterdumper.CounterDumper
|
||||||
rtcpReceiver *rtpreceiver.Receiver
|
rtcpReceiver *rtpreceiver.Receiver
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *IncomingTrack) initialize() {
|
func (t *IncomingTrack) initialize() {
|
||||||
t.OnPacketRTP = func(*rtp.Packet, time.Time) {}
|
t.OnPacketRTP = func(*rtp.Packet) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Codec returns the track codec.
|
// Codec returns the track codec.
|
||||||
|
|
@ -361,30 +360,23 @@ func (t *IncomingTrack) start() {
|
||||||
|
|
||||||
atomic.AddUint64(t.rtpPacketsReceived, uint64(len(packets)))
|
atomic.AddUint64(t.rtpPacketsReceived, uint64(len(packets)))
|
||||||
|
|
||||||
var ntp time.Time
|
|
||||||
if t.useAbsoluteTimestamp {
|
|
||||||
var avail bool
|
|
||||||
ntp, avail = t.rtcpReceiver.PacketNTP(pkt.Timestamp)
|
|
||||||
if !avail {
|
|
||||||
t.log.Log(logger.Warn, "received RTP packet without absolute time, skipping it")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ntp = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pkt := range packets {
|
for _, pkt := range packets {
|
||||||
// sometimes Chrome sends empty RTP packets. ignore them.
|
// sometimes Chrome sends empty RTP packets. ignore them.
|
||||||
if len(pkt.Payload) == 0 {
|
if len(pkt.Payload) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
t.OnPacketRTP(pkt, ntp)
|
t.OnPacketRTP(pkt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PacketNTP returns the packet NTP.
|
||||||
|
func (t *IncomingTrack) PacketNTP(pkt *rtp.Packet) (time.Time, bool) {
|
||||||
|
return t.rtcpReceiver.PacketNTP(pkt.Timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *IncomingTrack) close() {
|
func (t *IncomingTrack) close() {
|
||||||
if t.packetsLost != nil {
|
if t.packetsLost != nil {
|
||||||
t.packetsLost.Stop()
|
t.packetsLost.Stop()
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,6 @@ type PeerConnection struct {
|
||||||
STUNGatherTimeout conf.Duration
|
STUNGatherTimeout conf.Duration
|
||||||
Publish bool
|
Publish bool
|
||||||
OutgoingTracks []*OutgoingTrack
|
OutgoingTracks []*OutgoingTrack
|
||||||
UseAbsoluteTimestamp bool
|
|
||||||
Log logger.Writer
|
Log logger.Writer
|
||||||
|
|
||||||
wr *webrtc.PeerConnection
|
wr *webrtc.PeerConnection
|
||||||
|
|
@ -698,13 +697,12 @@ func (co *PeerConnection) GatherIncomingTracks() error {
|
||||||
|
|
||||||
case pair := <-co.incomingTrack:
|
case pair := <-co.incomingTrack:
|
||||||
t := &IncomingTrack{
|
t := &IncomingTrack{
|
||||||
useAbsoluteTimestamp: co.UseAbsoluteTimestamp,
|
track: pair.track,
|
||||||
track: pair.track,
|
receiver: pair.receiver,
|
||||||
receiver: pair.receiver,
|
writeRTCP: co.wr.WriteRTCP,
|
||||||
writeRTCP: co.wr.WriteRTCP,
|
log: co.Log,
|
||||||
log: co.Log,
|
rtpPacketsReceived: co.rtpPacketsReceived,
|
||||||
rtpPacketsReceived: co.rtpPacketsReceived,
|
rtpPacketsLost: co.rtpPacketsLost,
|
||||||
rtpPacketsLost: co.rtpPacketsLost,
|
|
||||||
}
|
}
|
||||||
t.initialize()
|
t.initialize()
|
||||||
co.incomingTracks = append(co.incomingTracks, t)
|
co.incomingTracks = append(co.incomingTracks, t)
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,21 @@ import (
|
||||||
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
||||||
"github.com/bluenviron/gortsplib/v5/pkg/format"
|
"github.com/bluenviron/gortsplib/v5/pkg/format"
|
||||||
"github.com/bluenviron/gortsplib/v5/pkg/rtptime"
|
"github.com/bluenviron/gortsplib/v5/pkg/rtptime"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
"github.com/bluenviron/mediamtx/internal/stream"
|
"github.com/bluenviron/mediamtx/internal/stream"
|
||||||
"github.com/pion/rtp"
|
"github.com/pion/rtp"
|
||||||
"github.com/pion/webrtc/v4"
|
"github.com/pion/webrtc/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ntpState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ntpStateInitial ntpState = iota
|
||||||
|
ntpStateReplace
|
||||||
|
ntpStateAvailable
|
||||||
|
)
|
||||||
|
|
||||||
var errNoSupportedCodecsTo = errors.New(
|
var errNoSupportedCodecsTo = errors.New(
|
||||||
"the stream doesn't contain any supported codec, which are currently " +
|
"the stream doesn't contain any supported codec, which are currently " +
|
||||||
"AV1, VP9, VP8, H265, H264, Opus, G722, G711, LPCM")
|
"AV1, VP9, VP8, H265, H264, Opus, G722, G711, LPCM")
|
||||||
|
|
@ -21,7 +31,9 @@ var errNoSupportedCodecsTo = errors.New(
|
||||||
// ToStream maps a WebRTC connection to a MediaMTX stream.
|
// ToStream maps a WebRTC connection to a MediaMTX stream.
|
||||||
func ToStream(
|
func ToStream(
|
||||||
pc *PeerConnection,
|
pc *PeerConnection,
|
||||||
stream **stream.Stream,
|
pathConf *conf.Path,
|
||||||
|
strm **stream.Stream,
|
||||||
|
log logger.Writer,
|
||||||
) ([]*description.Media, error) {
|
) ([]*description.Media, error) {
|
||||||
var medias []*description.Media //nolint:prealloc
|
var medias []*description.Media //nolint:prealloc
|
||||||
timeDecoder := &rtptime.GlobalDecoder{}
|
timeDecoder := &rtptime.GlobalDecoder{}
|
||||||
|
|
@ -142,13 +154,49 @@ func ToStream(
|
||||||
Formats: []format.Format{forma},
|
Formats: []format.Format{forma},
|
||||||
}
|
}
|
||||||
|
|
||||||
track.OnPacketRTP = func(pkt *rtp.Packet, ntp time.Time) {
|
var ntpStat ntpState
|
||||||
|
|
||||||
|
if !pathConf.UseAbsoluteTimestamp {
|
||||||
|
ntpStat = ntpStateReplace
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNTP := func(pkt *rtp.Packet) (time.Time, bool) {
|
||||||
|
switch ntpStat {
|
||||||
|
case ntpStateReplace:
|
||||||
|
return time.Time{}, true
|
||||||
|
|
||||||
|
case ntpStateInitial:
|
||||||
|
ntp, avail := track.PacketNTP(pkt)
|
||||||
|
if !avail {
|
||||||
|
log.Log(logger.Warn, "received RTP packet without absolute time, skipping it")
|
||||||
|
return time.Time{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
ntpStat = ntpStateAvailable
|
||||||
|
return ntp, true
|
||||||
|
|
||||||
|
default: // ntpStateAvailable
|
||||||
|
ntp, avail := track.PacketNTP(pkt)
|
||||||
|
if !avail {
|
||||||
|
panic("should not happen")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ntp, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
track.OnPacketRTP = func(pkt *rtp.Packet) {
|
||||||
pts, ok := timeDecoder.Decode(track, pkt)
|
pts, ok := timeDecoder.Decode(track, pkt)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
(*stream).WriteRTPPacket(medi, forma, pkt, ntp, pts)
|
ntp, ok := handleNTP(pkt)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
(*strm).WriteRTPPacket(medi, forma, pkt, ntp, pts)
|
||||||
}
|
}
|
||||||
|
|
||||||
medias = append(medias, medi)
|
medias = append(medias, medi)
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import (
|
||||||
|
|
||||||
func TestToStreamNoSupportedCodecs(t *testing.T) {
|
func TestToStreamNoSupportedCodecs(t *testing.T) {
|
||||||
pc := &PeerConnection{}
|
pc := &PeerConnection{}
|
||||||
_, err := ToStream(pc, nil)
|
_, err := ToStream(pc, &conf.Path{}, nil, nil)
|
||||||
require.Equal(t, errNoSupportedCodecsTo, err)
|
require.Equal(t, errNoSupportedCodecsTo, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -406,7 +406,7 @@ func TestToStream(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var stream *stream.Stream
|
var stream *stream.Stream
|
||||||
medias, err := ToStream(pc2, &stream)
|
medias, err := ToStream(pc2, &conf.Path{}, &stream, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, ca.out, medias[0].Formats[0])
|
require.Equal(t, ca.out, medias[0].Formats[0])
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,11 @@ const (
|
||||||
|
|
||||||
// Client is a WHIP client.
|
// Client is a WHIP client.
|
||||||
type Client struct {
|
type Client struct {
|
||||||
URL *url.URL
|
URL *url.URL
|
||||||
Publish bool
|
Publish bool
|
||||||
OutgoingTracks []*webrtc.OutgoingTrack
|
OutgoingTracks []*webrtc.OutgoingTrack
|
||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
UseAbsoluteTimestamp bool
|
Log logger.Writer
|
||||||
Log logger.Writer
|
|
||||||
|
|
||||||
pc *webrtc.PeerConnection
|
pc *webrtc.PeerConnection
|
||||||
patchIsSupported bool
|
patchIsSupported bool
|
||||||
|
|
@ -45,15 +44,14 @@ func (c *Client) Initialize(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
c.pc = &webrtc.PeerConnection{
|
c.pc = &webrtc.PeerConnection{
|
||||||
LocalRandomUDP: true,
|
LocalRandomUDP: true,
|
||||||
ICEServers: iceServers,
|
ICEServers: iceServers,
|
||||||
IPsFromInterfaces: true,
|
IPsFromInterfaces: true,
|
||||||
HandshakeTimeout: conf.Duration(10 * time.Second),
|
HandshakeTimeout: conf.Duration(10 * time.Second),
|
||||||
TrackGatherTimeout: conf.Duration(2 * time.Second),
|
TrackGatherTimeout: conf.Duration(2 * time.Second),
|
||||||
Publish: c.Publish,
|
Publish: c.Publish,
|
||||||
OutgoingTracks: c.OutgoingTracks,
|
OutgoingTracks: c.OutgoingTracks,
|
||||||
UseAbsoluteTimestamp: c.UseAbsoluteTimestamp,
|
Log: c.Log,
|
||||||
Log: c.Log,
|
|
||||||
}
|
}
|
||||||
err = c.pc.Start()
|
err = c.pc.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,7 @@ func TestClientRead(t *testing.T) {
|
||||||
|
|
||||||
for i, track := range cl.IncomingTracks() {
|
for i, track := range cl.IncomingTracks() {
|
||||||
ci := i
|
ci := i
|
||||||
track.OnPacketRTP = func(_ *rtp.Packet, _ time.Time) {
|
track.OnPacketRTP = func(_ *rtp.Packet) {
|
||||||
close(recv[ci])
|
close(recv[ci])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -340,7 +340,7 @@ func TestClientPublish(t *testing.T) {
|
||||||
|
|
||||||
for i, track := range pc.IncomingTracks() {
|
for i, track := range pc.IncomingTracks() {
|
||||||
ci := i
|
ci := i
|
||||||
track.OnPacketRTP = func(_ *rtp.Packet, _ time.Time) {
|
track.OnPacketRTP = func(_ *rtp.Packet) {
|
||||||
close(recv[ci])
|
close(recv[ci])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@ func (c *conn) runPublish() error {
|
||||||
Author: c,
|
Author: c,
|
||||||
Desc: &description.Session{Medias: medias},
|
Desc: &description.Session{Medias: medias},
|
||||||
GenerateRTPPackets: true,
|
GenerateRTPPackets: true,
|
||||||
|
FillNTP: true,
|
||||||
AccessRequest: defs.PathAccessRequest{
|
AccessRequest: defs.PathAccessRequest{
|
||||||
Name: pathName,
|
Name: pathName,
|
||||||
Query: c.rconn.URL.RawQuery,
|
Query: c.rconn.URL.RawQuery,
|
||||||
|
|
|
||||||
|
|
@ -311,6 +311,7 @@ func (s *session) onRecord(_ *gortsplib.ServerHandlerOnRecordCtx) (*base.Respons
|
||||||
Author: s,
|
Author: s,
|
||||||
Desc: s.rsession.AnnouncedDescription(),
|
Desc: s.rsession.AnnouncedDescription(),
|
||||||
GenerateRTPPackets: false,
|
GenerateRTPPackets: false,
|
||||||
|
FillNTP: !s.pathConf.UseAbsoluteTimestamp,
|
||||||
ConfToCompare: s.pathConf,
|
ConfToCompare: s.pathConf,
|
||||||
AccessRequest: defs.PathAccessRequest{
|
AccessRequest: defs.PathAccessRequest{
|
||||||
Name: s.rsession.Path()[1:],
|
Name: s.rsession.Path()[1:],
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,7 @@ func (c *conn) runPublishReader(sconn srt.Conn, streamID *streamID, pathConf *co
|
||||||
Author: c,
|
Author: c,
|
||||||
Desc: &description.Session{Medias: medias},
|
Desc: &description.Session{Medias: medias},
|
||||||
GenerateRTPPackets: true,
|
GenerateRTPPackets: true,
|
||||||
|
FillNTP: true,
|
||||||
ConfToCompare: pathConf,
|
ConfToCompare: pathConf,
|
||||||
AccessRequest: defs.PathAccessRequest{
|
AccessRequest: defs.PathAccessRequest{
|
||||||
Name: streamID.path,
|
Name: streamID.path,
|
||||||
|
|
|
||||||
|
|
@ -567,7 +567,7 @@ func TestServerRead(t *testing.T) {
|
||||||
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
|
||||||
wc.IncomingTracks()[0].OnPacketRTP = func(pkt *rtp.Packet, _ time.Time) {
|
wc.IncomingTracks()[0].OnPacketRTP = func(pkt *rtp.Packet) {
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-done:
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,6 @@ func (s *session) runPublish() (int, error) {
|
||||||
TrackGatherTimeout: s.trackGatherTimeout,
|
TrackGatherTimeout: s.trackGatherTimeout,
|
||||||
STUNGatherTimeout: s.stunGatherTimeout,
|
STUNGatherTimeout: s.stunGatherTimeout,
|
||||||
Publish: false,
|
Publish: false,
|
||||||
UseAbsoluteTimestamp: pathConf.UseAbsoluteTimestamp,
|
|
||||||
Log: s,
|
Log: s,
|
||||||
}
|
}
|
||||||
err = pc.Start()
|
err = pc.Start()
|
||||||
|
|
@ -234,7 +233,7 @@ func (s *session) runPublish() (int, error) {
|
||||||
|
|
||||||
var stream *stream.Stream
|
var stream *stream.Stream
|
||||||
|
|
||||||
medias, err := webrtc.ToStream(pc, &stream)
|
medias, err := webrtc.ToStream(pc, pathConf, &stream, s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
@ -244,6 +243,7 @@ func (s *session) runPublish() (int, error) {
|
||||||
Author: s,
|
Author: s,
|
||||||
Desc: &description.Session{Medias: medias},
|
Desc: &description.Session{Medias: medias},
|
||||||
GenerateRTPPackets: false,
|
GenerateRTPPackets: false,
|
||||||
|
FillNTP: !pathConf.UseAbsoluteTimestamp,
|
||||||
ConfToCompare: pathConf,
|
ConfToCompare: pathConf,
|
||||||
AccessRequest: defs.PathAccessRequest{
|
AccessRequest: defs.PathAccessRequest{
|
||||||
Name: s.req.pathName,
|
Name: s.req.pathName,
|
||||||
|
|
@ -312,7 +312,6 @@ func (s *session) runRead() (int, error) {
|
||||||
TrackGatherTimeout: s.trackGatherTimeout,
|
TrackGatherTimeout: s.trackGatherTimeout,
|
||||||
STUNGatherTimeout: s.stunGatherTimeout,
|
STUNGatherTimeout: s.stunGatherTimeout,
|
||||||
Publish: true,
|
Publish: true,
|
||||||
UseAbsoluteTimestamp: path.SafeConf().UseAbsoluteTimestamp,
|
|
||||||
Log: s,
|
Log: s,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ func (s *Source) runReader(nc net.Conn) error {
|
||||||
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
|
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
|
||||||
Desc: &description.Session{Medias: medias},
|
Desc: &description.Session{Medias: medias},
|
||||||
GenerateRTPPackets: true,
|
GenerateRTPPackets: true,
|
||||||
|
FillNTP: true,
|
||||||
})
|
})
|
||||||
if res.Err != nil {
|
if res.Err != nil {
|
||||||
return res.Err
|
return res.Err
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ func (s *Source) runReader(ctx context.Context, u *url.URL, fingerprint string)
|
||||||
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
|
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
|
||||||
Desc: &description.Session{Medias: medias},
|
Desc: &description.Session{Medias: medias},
|
||||||
GenerateRTPPackets: true,
|
GenerateRTPPackets: true,
|
||||||
|
FillNTP: true,
|
||||||
})
|
})
|
||||||
if res.Err != nil {
|
if res.Err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ func (s *Source) runReader(desc *description.Session, nc net.Conn) error {
|
||||||
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
|
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
|
||||||
Desc: desc,
|
Desc: desc,
|
||||||
GenerateRTPPackets: false,
|
GenerateRTPPackets: false,
|
||||||
|
FillNTP: true,
|
||||||
})
|
})
|
||||||
if res.Err != nil {
|
if res.Err != nil {
|
||||||
return res.Err
|
return res.Err
|
||||||
|
|
@ -171,7 +172,7 @@ func (s *Source) runReader(desc *description.Session, nc net.Conn) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.WriteRTPPacket(media, forma, &pkt, time.Now(), pts)
|
stream.WriteRTPPacket(media, forma, &pkt, time.Time{}, pts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
|
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
|
||||||
Desc: desc,
|
Desc: desc,
|
||||||
GenerateRTPPackets: false,
|
GenerateRTPPackets: false,
|
||||||
|
FillNTP: !params.Conf.UseAbsoluteTimestamp,
|
||||||
})
|
})
|
||||||
if res.Err != nil {
|
if res.Err != nil {
|
||||||
return res.Err
|
return res.Err
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ func (s *Source) runReader(sconn srt.Conn) error {
|
||||||
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
|
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
|
||||||
Desc: &description.Session{Medias: medias},
|
Desc: &description.Session{Medias: medias},
|
||||||
GenerateRTPPackets: true,
|
GenerateRTPPackets: true,
|
||||||
|
FillNTP: true,
|
||||||
})
|
})
|
||||||
if res.Err != nil {
|
if res.Err != nil {
|
||||||
return res.Err
|
return res.Err
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
Timeout: time.Duration(s.ReadTimeout),
|
Timeout: time.Duration(s.ReadTimeout),
|
||||||
Transport: tr,
|
Transport: tr,
|
||||||
},
|
},
|
||||||
UseAbsoluteTimestamp: params.Conf.UseAbsoluteTimestamp,
|
Log: s,
|
||||||
Log: s,
|
|
||||||
}
|
}
|
||||||
err = client.Initialize(params.Context)
|
err = client.Initialize(params.Context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -68,7 +67,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
|
|
||||||
var stream *stream.Stream
|
var stream *stream.Stream
|
||||||
|
|
||||||
medias, err := webrtc.ToStream(client.PeerConnection(), &stream)
|
medias, err := webrtc.ToStream(client.PeerConnection(), params.Conf, &stream, s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
client.Close() //nolint:errcheck
|
client.Close() //nolint:errcheck
|
||||||
return err
|
return err
|
||||||
|
|
@ -77,6 +76,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
rres := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
|
rres := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
|
||||||
Desc: &description.Session{Medias: medias},
|
Desc: &description.Session{Medias: medias},
|
||||||
GenerateRTPPackets: true,
|
GenerateRTPPackets: true,
|
||||||
|
FillNTP: !params.Conf.UseAbsoluteTimestamp,
|
||||||
})
|
})
|
||||||
if rres.Err != nil {
|
if rres.Err != nil {
|
||||||
client.Close() //nolint:errcheck
|
client.Close() //nolint:errcheck
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ type Stream struct {
|
||||||
RTPMaxPayloadSize int
|
RTPMaxPayloadSize int
|
||||||
Desc *description.Session
|
Desc *description.Session
|
||||||
GenerateRTPPackets bool
|
GenerateRTPPackets bool
|
||||||
|
FillNTP bool
|
||||||
Parent logger.Writer
|
Parent logger.Writer
|
||||||
|
|
||||||
bytesReceived *uint64
|
bytesReceived *uint64
|
||||||
|
|
@ -61,6 +62,7 @@ func (s *Stream) Initialize() error {
|
||||||
rtpMaxPayloadSize: s.RTPMaxPayloadSize,
|
rtpMaxPayloadSize: s.RTPMaxPayloadSize,
|
||||||
media: media,
|
media: media,
|
||||||
generateRTPPackets: s.GenerateRTPPackets,
|
generateRTPPackets: s.GenerateRTPPackets,
|
||||||
|
fillNTP: s.FillNTP,
|
||||||
processingErrors: s.processingErrors,
|
processingErrors: s.processingErrors,
|
||||||
parent: s.Parent,
|
parent: s.Parent,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/bluenviron/mediamtx/internal/codecprocessor"
|
"github.com/bluenviron/mediamtx/internal/codecprocessor"
|
||||||
"github.com/bluenviron/mediamtx/internal/counterdumper"
|
"github.com/bluenviron/mediamtx/internal/counterdumper"
|
||||||
"github.com/bluenviron/mediamtx/internal/logger"
|
"github.com/bluenviron/mediamtx/internal/logger"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/ntpestimator"
|
||||||
"github.com/bluenviron/mediamtx/internal/unit"
|
"github.com/bluenviron/mediamtx/internal/unit"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -26,11 +27,13 @@ type streamFormat struct {
|
||||||
rtpMaxPayloadSize int
|
rtpMaxPayloadSize int
|
||||||
format format.Format
|
format format.Format
|
||||||
generateRTPPackets bool
|
generateRTPPackets bool
|
||||||
|
fillNTP bool
|
||||||
processingErrors *counterdumper.CounterDumper
|
processingErrors *counterdumper.CounterDumper
|
||||||
parent logger.Writer
|
parent logger.Writer
|
||||||
|
|
||||||
proc codecprocessor.Processor
|
proc codecprocessor.Processor
|
||||||
onDatas map[*Reader]OnDataFunc
|
ntpEstimator *ntpestimator.Estimator
|
||||||
|
onDatas map[*Reader]OnDataFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sf *streamFormat) initialize() error {
|
func (sf *streamFormat) initialize() error {
|
||||||
|
|
@ -42,6 +45,10 @@ func (sf *streamFormat) initialize() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sf.ntpEstimator = &ntpestimator.Estimator{
|
||||||
|
ClockRate: sf.format.ClockRate(),
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,6 +87,10 @@ func (sf *streamFormat) writeRTPPacket(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sf *streamFormat) writeUnitInner(s *Stream, medi *description.Media, u *unit.Unit) {
|
func (sf *streamFormat) writeUnitInner(s *Stream, medi *description.Media, u *unit.Unit) {
|
||||||
|
if sf.fillNTP {
|
||||||
|
u.NTP = sf.ntpEstimator.Estimate(u.PTS)
|
||||||
|
}
|
||||||
|
|
||||||
size := unitSize(u)
|
size := unitSize(u)
|
||||||
|
|
||||||
atomic.AddUint64(s.bytesReceived, size)
|
atomic.AddUint64(s.bytesReceived, size)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ type streamMedia struct {
|
||||||
rtpMaxPayloadSize int
|
rtpMaxPayloadSize int
|
||||||
media *description.Media
|
media *description.Media
|
||||||
generateRTPPackets bool
|
generateRTPPackets bool
|
||||||
|
fillNTP bool
|
||||||
processingErrors *counterdumper.CounterDumper
|
processingErrors *counterdumper.CounterDumper
|
||||||
parent logger.Writer
|
parent logger.Writer
|
||||||
|
|
||||||
|
|
@ -25,6 +26,7 @@ func (sm *streamMedia) initialize() error {
|
||||||
rtpMaxPayloadSize: sm.rtpMaxPayloadSize,
|
rtpMaxPayloadSize: sm.rtpMaxPayloadSize,
|
||||||
format: forma,
|
format: forma,
|
||||||
generateRTPPackets: sm.generateRTPPackets,
|
generateRTPPackets: sm.generateRTPPackets,
|
||||||
|
fillNTP: sm.fillNTP,
|
||||||
processingErrors: sm.processingErrors,
|
processingErrors: sm.processingErrors,
|
||||||
parent: sm.parent,
|
parent: sm.parent,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue