mirror of
https://github.com/bluenviron/mediamtx.git
synced 2025-12-20 02:00:05 -08:00
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.
407 lines
9.4 KiB
Go
407 lines
9.4 KiB
Go
// Package rtmp provides RTMP utilities.
|
|
package rtmp
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"slices"
|
|
"time"
|
|
|
|
"github.com/bluenviron/gortmplib"
|
|
"github.com/bluenviron/gortmplib/pkg/message"
|
|
"github.com/bluenviron/gortsplib/v5/pkg/description"
|
|
"github.com/bluenviron/gortsplib/v5/pkg/format"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/ac3"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h265"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
|
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/opus"
|
|
"github.com/bluenviron/mediamtx/internal/logger"
|
|
"github.com/bluenviron/mediamtx/internal/stream"
|
|
"github.com/bluenviron/mediamtx/internal/unit"
|
|
)
|
|
|
|
var errNoSupportedCodecsFrom = errors.New(
|
|
"the stream doesn't contain any supported codec, which are currently " +
|
|
"AV1, VP9, H265, H264, Opus, MPEG-4 Audio, MPEG-1/2 Audio, AC-3, G711, LPCM")
|
|
|
|
func multiplyAndDivide2(v, m, d time.Duration) time.Duration {
|
|
secs := v / d
|
|
dec := v % d
|
|
return (secs*m + dec*m/d)
|
|
}
|
|
|
|
func timestampToDuration(t int64, clockRate int) time.Duration {
|
|
return multiplyAndDivide2(time.Duration(t), time.Second, time.Duration(clockRate))
|
|
}
|
|
|
|
// FromStream maps a MediaMTX stream to a RTMP stream.
|
|
func FromStream(
|
|
desc *description.Session,
|
|
r *stream.Reader,
|
|
conn *gortmplib.ServerConn,
|
|
nconn net.Conn,
|
|
writeTimeout time.Duration,
|
|
) error {
|
|
var tracks []format.Format
|
|
var w *gortmplib.Writer
|
|
|
|
for _, media := range desc.Medias {
|
|
for _, forma := range media.Formats {
|
|
switch forma := forma.(type) {
|
|
case *format.AV1:
|
|
if slices.Contains(conn.FourCcList, interface{}(fourCCToString(message.FourCCAV1))) {
|
|
r.OnData(
|
|
media,
|
|
forma,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
|
return (*w).WriteAV1(
|
|
forma,
|
|
timestampToDuration(u.PTS, forma.ClockRate()),
|
|
u.Payload.(unit.PayloadAV1))
|
|
})
|
|
|
|
tracks = append(tracks, forma)
|
|
}
|
|
|
|
case *format.VP9:
|
|
if slices.Contains(conn.FourCcList, interface{}(fourCCToString(message.FourCCVP9))) {
|
|
r.OnData(
|
|
media,
|
|
forma,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
|
return (*w).WriteVP9(
|
|
forma,
|
|
timestampToDuration(u.PTS, forma.ClockRate()),
|
|
u.Payload.(unit.PayloadVP9))
|
|
})
|
|
|
|
tracks = append(tracks, forma)
|
|
}
|
|
|
|
case *format.H265:
|
|
if slices.Contains(conn.FourCcList, interface{}(fourCCToString(message.FourCCHEVC))) {
|
|
var videoDTSExtractor *h265.DTSExtractor
|
|
|
|
r.OnData(
|
|
media,
|
|
forma,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
if videoDTSExtractor == nil {
|
|
if !h265.IsRandomAccess(u.Payload.(unit.PayloadH265)) {
|
|
return nil
|
|
}
|
|
videoDTSExtractor = &h265.DTSExtractor{}
|
|
videoDTSExtractor.Initialize()
|
|
}
|
|
|
|
dts, err := videoDTSExtractor.Extract(u.Payload.(unit.PayloadH265), u.PTS)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
|
return (*w).WriteH265(
|
|
forma,
|
|
timestampToDuration(u.PTS, forma.ClockRate()),
|
|
timestampToDuration(dts, forma.ClockRate()),
|
|
u.Payload.(unit.PayloadH265))
|
|
})
|
|
|
|
tracks = append(tracks, forma)
|
|
}
|
|
|
|
case *format.H264:
|
|
var videoDTSExtractor *h264.DTSExtractor
|
|
|
|
r.OnData(
|
|
media,
|
|
forma,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
idrPresent := false
|
|
nonIDRPresent := false
|
|
|
|
for _, nalu := range u.Payload.(unit.PayloadH264) {
|
|
typ := h264.NALUType(nalu[0] & 0x1F)
|
|
switch typ {
|
|
case h264.NALUTypeIDR:
|
|
idrPresent = true
|
|
|
|
case h264.NALUTypeNonIDR:
|
|
nonIDRPresent = true
|
|
}
|
|
}
|
|
|
|
// wait until we receive an IDR
|
|
if videoDTSExtractor == nil {
|
|
if !idrPresent {
|
|
return nil
|
|
}
|
|
|
|
videoDTSExtractor = &h264.DTSExtractor{}
|
|
videoDTSExtractor.Initialize()
|
|
} else if !idrPresent && !nonIDRPresent {
|
|
return nil
|
|
}
|
|
|
|
dts, err := videoDTSExtractor.Extract(u.Payload.(unit.PayloadH264), u.PTS)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
|
return (*w).WriteH264(
|
|
forma,
|
|
timestampToDuration(u.PTS, forma.ClockRate()),
|
|
timestampToDuration(dts, forma.ClockRate()),
|
|
u.Payload.(unit.PayloadH264))
|
|
})
|
|
|
|
tracks = append(tracks, forma)
|
|
|
|
case *format.Opus:
|
|
if slices.Contains(conn.FourCcList, interface{}(fourCCToString(message.FourCCOpus))) {
|
|
r.OnData(
|
|
media,
|
|
forma,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
pts := u.PTS
|
|
|
|
for _, pkt := range u.Payload.(unit.PayloadOpus) {
|
|
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
|
err := (*w).WriteOpus(
|
|
forma,
|
|
timestampToDuration(pts, forma.ClockRate()),
|
|
pkt,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pts += opus.PacketDuration2(pkt)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
tracks = append(tracks, forma)
|
|
}
|
|
|
|
case *format.MPEG4Audio:
|
|
r.OnData(
|
|
media,
|
|
forma,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
for i, au := range u.Payload.(unit.PayloadMPEG4Audio) {
|
|
pts := u.PTS + int64(i)*mpeg4audio.SamplesPerAccessUnit
|
|
|
|
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
|
err := (*w).WriteMPEG4Audio(
|
|
forma,
|
|
timestampToDuration(pts, forma.ClockRate()),
|
|
au,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
tracks = append(tracks, forma)
|
|
|
|
case *format.MPEG4AudioLATM:
|
|
if !forma.CPresent {
|
|
r.OnData(
|
|
media,
|
|
forma,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
var ame mpeg4audio.AudioMuxElement
|
|
ame.StreamMuxConfig = forma.StreamMuxConfig
|
|
err := ame.Unmarshal(u.Payload.(unit.PayloadMPEG4AudioLATM))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
|
return (*w).WriteMPEG4Audio(
|
|
forma,
|
|
timestampToDuration(u.PTS, forma.ClockRate()),
|
|
ame.Payloads[0][0][0],
|
|
)
|
|
})
|
|
|
|
tracks = append(tracks, forma)
|
|
}
|
|
|
|
case *format.MPEG1Audio:
|
|
r.OnData(
|
|
media,
|
|
forma,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
pts := u.PTS
|
|
|
|
for _, frame := range u.Payload.(unit.PayloadMPEG1Audio) {
|
|
var h mpeg1audio.FrameHeader
|
|
err := h.Unmarshal(frame)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
|
err = (*w).WriteMPEG1Audio(
|
|
forma,
|
|
timestampToDuration(pts, forma.ClockRate()),
|
|
frame)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
pts += int64(h.SampleCount()) *
|
|
int64(forma.ClockRate()) / int64(h.SampleRate)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
tracks = append(tracks, forma)
|
|
|
|
case *format.AC3:
|
|
if slices.Contains(conn.FourCcList, interface{}(fourCCToString(message.FourCCAC3))) {
|
|
r.OnData(
|
|
media,
|
|
forma,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
for i, frame := range u.Payload.(unit.PayloadAC3) {
|
|
pts := u.PTS + int64(i)*ac3.SamplesPerFrame
|
|
|
|
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
|
err := (*w).WriteAC3(
|
|
forma,
|
|
timestampToDuration(pts, forma.ClockRate()),
|
|
frame)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
tracks = append(tracks, forma)
|
|
}
|
|
|
|
case *format.G711:
|
|
if forma.SampleRate == 8000 {
|
|
r.OnData(
|
|
media,
|
|
forma,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
|
return (*w).WriteG711(
|
|
forma,
|
|
timestampToDuration(u.PTS, forma.ClockRate()),
|
|
u.Payload.(unit.PayloadG711),
|
|
)
|
|
})
|
|
|
|
tracks = append(tracks, forma)
|
|
}
|
|
|
|
case *format.LPCM:
|
|
if (forma.ChannelCount == 1 || forma.ChannelCount == 2) &&
|
|
(forma.SampleRate == 5512 ||
|
|
forma.SampleRate == 11025 ||
|
|
forma.SampleRate == 22050 ||
|
|
forma.SampleRate == 44100) {
|
|
r.OnData(
|
|
media,
|
|
forma,
|
|
func(u *unit.Unit) error {
|
|
if u.NilPayload() {
|
|
return nil
|
|
}
|
|
|
|
nconn.SetWriteDeadline(time.Now().Add(writeTimeout))
|
|
return (*w).WriteLPCM(
|
|
forma,
|
|
timestampToDuration(u.PTS, forma.ClockRate()),
|
|
u.Payload.(unit.PayloadLPCM),
|
|
)
|
|
})
|
|
|
|
tracks = append(tracks, forma)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(tracks) == 0 {
|
|
return errNoSupportedCodecsFrom
|
|
}
|
|
|
|
w = &gortmplib.Writer{
|
|
Conn: conn,
|
|
Tracks: tracks,
|
|
}
|
|
err := w.Initialize()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
n := 1
|
|
for _, media := range desc.Medias {
|
|
for _, forma := range media.Formats {
|
|
if !slices.Contains(tracks, forma) {
|
|
r.Parent.Log(logger.Warn, "skipping track %d (%s)", n, forma.Codec())
|
|
}
|
|
n++
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|