add always available streams (#5335)

When the publisher or source of a stream is offline, the server can be
configured to fill gaps in the stream with a video that is played on
repeat until a publisher comes back online. This allows readers to stay
connected regardless of the state of the stream. The offline video and
any future online stream are concatenated without decoding or
re-encoding packets, using the original codec.
This commit is contained in:
Alessandro Ros
2026-01-31 14:44:58 +01:00
committed by GitHub
parent 06c7cc3d7f
commit a56408db19
95 changed files with 2028 additions and 641 deletions
+1
View File
@@ -32,6 +32,7 @@ _MediaMTX_ is a ready-to-use and zero-dependency real-time media server and medi
- Streams are automatically converted from a protocol to another
- Serve several streams at once in separate paths
- Reload the configuration without disconnecting existing clients (hot reloading)
- [Serve always-available streams](https://mediamtx.org/docs/usage/always-available) even when the publisher is offline
- [Record](https://mediamtx.org/docs/usage/record) streams to disk in fMP4 or MPEG-TS format
- [Playback](https://mediamtx.org/docs/usage/playback) recorded streams
- [Authenticate](https://mediamtx.org/docs/usage/authentication) users with internal, HTTP or JWT authentication
+24
View File
@@ -377,6 +377,16 @@ components:
useAbsoluteTimestamp:
type: boolean
# Always available
alwaysAvailable:
type: boolean
alwaysAvailableFile:
type: string
alwaysAvailableTracks:
type: array
items:
$ref: '#/components/schemas/AlwaysAvailableTrack'
# Record
record:
type: boolean
@@ -1088,6 +1098,20 @@ components:
items:
$ref: '#/components/schemas/SRTConn'
AlwaysAvailableTrack:
type: object
properties:
codec:
type: string
sampleRate:
type: integer
format: int64
channelCount:
type: integer
format: int64
muLaw:
type: boolean
WebRTCICEServer:
type: object
properties:
+1
View File
@@ -11,6 +11,7 @@ Main features:
- Streams are automatically converted from a protocol to another
- Serve several streams at once in separate paths
- Reload the configuration without disconnecting existing clients (hot reloading)
- [Serve always-available streams](/docs/usage/always-available) even when the publisher is offline
- [Record](/docs/usage/record) streams to disk in fMP4 or MPEG-TS format
- [Playback](/docs/usage/playback) recorded streams
- [Authenticate](/docs/usage/authentication) users with internal, HTTP or JWT authentication
+2
View File
@@ -59,6 +59,8 @@ COPY --from=mediamtx.yml /
RUN apt update && apt install -y \
(insert here additional utilities)
ENTRYPOINT [ "/mediamtx" ]
```
And then build it:
+29
View File
@@ -0,0 +1,29 @@
# Always-available streams
When the publisher or source of a stream is offline, the server can be configured to fill gaps in the stream with a video that is played on repeat until a publisher comes back online. This allows readers to stay connected regardless of the state of the stream. The offline video and any future online stream are concatenated without decoding or re-encoding packets, using the original codec.
This feature can be enabled by toggling the `alwaysAvailable` flag and filling `alwaysAvailableTracks`:
```yml
paths:
mypath:
alwaysAvailable: true
alwaysAvailableTracks:
# Available values are: AV1, VP9, H265, H264, Opus, MPEG4Audio, G711, LPCM
- codec: H264
# in case of MPEG4Audio, G711, LPCM, sampleRate and ChannelCount must be provided too.
# sampleRate: 48000
# channelCount: 2
# in case of G711, muLaw must be provided too.
# muLaw: false
```
By default, the server uses a default offline video with the text "STREAM IS OFFLINE". This can be changed by importing the video from a MP4 file:
```yml
paths:
mypath:
alwaysAvailable: true
# Path to the MP4 file that is played on repeat. If not provided, a default video will be used instead.
alwaysAvailableFile: "./h264.mp4"
```
+1 -1
View File
@@ -13,7 +13,7 @@ require (
github.com/bluenviron/gohlslib/v2 v2.2.5-0.20260117214804-b8c1ff42629d
github.com/bluenviron/gortmplib v0.2.0
github.com/bluenviron/gortsplib/v5 v5.2.2
github.com/bluenviron/mediacommon/v2 v2.6.0
github.com/bluenviron/mediacommon/v2 v2.6.1-0.20260130191353-17b8857753e7
github.com/datarhei/gosrt v0.9.0
github.com/fsnotify/fsnotify v1.9.0
github.com/gin-contrib/pprof v1.5.3
+2 -2
View File
@@ -41,8 +41,8 @@ github.com/bluenviron/gortmplib v0.2.0 h1:j15eeHrgVh6Avg9oAx+r4w0HugTqrIqLBsYnhs
github.com/bluenviron/gortmplib v0.2.0/go.mod h1:yzobxBF8zusF2nKbEOF69zIIL429j0kaCWc/euNdvO4=
github.com/bluenviron/gortsplib/v5 v5.2.2 h1:5q2viB8PGxWOSXNhVvj8buyr1wighLbHqRZ0U7MLM3o=
github.com/bluenviron/gortsplib/v5 v5.2.2/go.mod h1:xkVBOAnR4fzaerPN650CBb7N+zUUsj7PI2HiY1TP7Co=
github.com/bluenviron/mediacommon/v2 v2.6.0 h1:wZAPXwv7V78Cx2x7cToYIHOLToHl6APcvHbdQT+gOkg=
github.com/bluenviron/mediacommon/v2 v2.6.0/go.mod h1:5V15TiOfeaNVmZPVuOqAwqQSWyvMV86/dijDKu5q9Zs=
github.com/bluenviron/mediacommon/v2 v2.6.1-0.20260130191353-17b8857753e7 h1:Cqh4LLMu+yMhdPwzqppj64iMog+A7bKVnYFXshO6MR8=
github.com/bluenviron/mediacommon/v2 v2.6.1-0.20260130191353-17b8857753e7/go.mod h1:5V15TiOfeaNVmZPVuOqAwqQSWyvMV86/dijDKu5q9Zs=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
+67
View File
@@ -0,0 +1,67 @@
package conf
import (
"fmt"
"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper"
)
// Codec is a codec of AlwaysAvailableTrack.
type Codec string
// available codecs.
const (
CodecAV1 Codec = "AV1"
CodecVP9 Codec = "VP9"
CodecH265 Codec = "H265"
CodecH264 Codec = "H264"
CodecMPEG4Audio Codec = "MPEG4Audio"
CodecOpus Codec = "Opus"
CodecG711 Codec = "G711"
CodecLPCM Codec = "LPCM"
)
// UnmarshalEnv implements env.Unmarshaler.
func (d *Codec) UnmarshalEnv(_ string, v string) error {
return jsonwrapper.Unmarshal([]byte(`"`+v+`"`), d)
}
// AlwaysAvailableTrack is an item of alwaysAvailableTracks.
type AlwaysAvailableTrack struct {
Codec Codec `json:"codec"`
SampleRate int `json:"sampleRate"`
ChannelCount int `json:"channelCount"`
MULaw bool `json:"muLaw"`
}
// UnmarshalJSON implements json.Unmarshaler.
func (t *AlwaysAvailableTrack) UnmarshalJSON(b []byte) error {
type alias AlwaysAvailableTrack
err := jsonwrapper.Unmarshal(b, (*alias)(t))
if err != nil {
return err
}
switch t.Codec {
case CodecAV1, CodecVP9, CodecH265, CodecH264, CodecOpus:
if t.SampleRate != 0 {
return fmt.Errorf("sampleRate must not be specified for codec '%s'", t.Codec)
}
if t.ChannelCount != 0 {
return fmt.Errorf("channelCount must not be specified for codec '%s'", t.Codec)
}
case CodecMPEG4Audio, CodecG711, CodecLPCM:
if t.SampleRate == 0 {
return fmt.Errorf("sampleRate is mandatory for codec '%s'", t.Codec)
}
if t.ChannelCount == 0 {
return fmt.Errorf("channelCount is mandatory for codec '%s'", t.Codec)
}
default:
return fmt.Errorf("unsupported codec '%s'", t.Codec)
}
return nil
}
+9 -35
View File
@@ -1,59 +1,33 @@
package conf
import (
"encoding/json"
"fmt"
"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper"
)
// AuthMethod is an authentication method.
type AuthMethod int
type AuthMethod string
// authentication methods.
const (
AuthMethodInternal AuthMethod = iota
AuthMethodHTTP
AuthMethodJWT
AuthMethodInternal AuthMethod = "internal"
AuthMethodHTTP AuthMethod = "http"
AuthMethodJWT AuthMethod = "jwt"
)
// MarshalJSON implements json.Marshaler.
func (d AuthMethod) MarshalJSON() ([]byte, error) {
var out string
switch d {
case AuthMethodInternal:
out = "internal"
case AuthMethodHTTP:
out = "http"
default:
out = "jwt"
}
return json.Marshal(out)
}
// UnmarshalJSON implements json.Unmarshaler.
func (d *AuthMethod) UnmarshalJSON(b []byte) error {
var in string
if err := jsonwrapper.Unmarshal(b, &in); err != nil {
type alias AuthMethod
if err := jsonwrapper.Unmarshal(b, (*alias)(d)); err != nil {
return err
}
switch in {
case "internal":
*d = AuthMethodInternal
case "http":
*d = AuthMethodHTTP
case "jwt":
*d = AuthMethodJWT
switch *d {
case AuthMethodInternal, AuthMethodHTTP, AuthMethodJWT:
default:
return fmt.Errorf("invalid authMethod: '%s'", in)
return fmt.Errorf("invalid authMethod: '%s'", *d)
}
return nil
+3
View File
@@ -420,6 +420,7 @@ func (conf *Conf) setDefaults() {
conf.UDPMaxPayloadSize = 1472
// Authentication
conf.AuthMethod = AuthMethodInternal
conf.AuthInternalUsers = defaultAuthInternalUsers
conf.AuthHTTPExclude = []AuthInternalUserPermission{
{
@@ -461,6 +462,7 @@ func (conf *Conf) setDefaults() {
// RTSP server
conf.RTSP = true
conf.RTSPEncryption = EncryptionNo
conf.RTSPTransports = RTSPTransports{
gortsplib.ProtocolUDP: {},
gortsplib.ProtocolUDPMulticast: {},
@@ -483,6 +485,7 @@ func (conf *Conf) setDefaults() {
// RTMP server
conf.RTMP = true
conf.RTMPEncryption = EncryptionNo
conf.RTMPAddress = ":1935"
conf.RTMPSAddress = ":1936"
conf.RTMPServerKey = "server.key"
+7 -4
View File
@@ -48,10 +48,13 @@ func TestConfFromFile(t *testing.T) {
pa, ok := conf.Paths["cam1"]
require.Equal(t, true, ok)
require.Equal(t, &Path{
Name: "cam1",
Source: "publisher",
SourceOnDemandStartTimeout: 10 * Duration(time.Second),
SourceOnDemandCloseAfter: 10 * Duration(time.Second),
Name: "cam1",
Source: "publisher",
SourceOnDemandStartTimeout: 10 * Duration(time.Second),
SourceOnDemandCloseAfter: 10 * Duration(time.Second),
AlwaysAvailableTracks: []AlwaysAvailableTrack{
{Codec: "H264"},
},
RecordPath: "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f",
RecordFormat: RecordFormatFMP4,
RecordPartDuration: Duration(1 * time.Second),
+14 -33
View File
@@ -1,59 +1,40 @@
package conf
import (
"encoding/json"
"fmt"
"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper"
)
// Encryption is the rtspEncryption / rtmpEncryption parameter.
type Encryption int
type Encryption string
// values.
const (
EncryptionNo Encryption = iota
EncryptionOptional
EncryptionStrict
EncryptionNo Encryption = "no"
EncryptionOptional Encryption = "optional"
EncryptionStrict Encryption = "strict"
)
// MarshalJSON implements json.Marshaler.
func (d Encryption) MarshalJSON() ([]byte, error) {
var out string
switch d {
case EncryptionNo:
out = "no"
case EncryptionOptional:
out = "optional"
default:
out = "strict"
}
return json.Marshal(out)
}
// UnmarshalJSON implements json.Unmarshaler.
func (d *Encryption) UnmarshalJSON(b []byte) error {
var in string
if err := jsonwrapper.Unmarshal(b, &in); err != nil {
type alias Encryption
if err := jsonwrapper.Unmarshal(b, (*alias)(d)); err != nil {
return err
}
switch in {
case "no", "false":
switch *d {
case "false":
*d = EncryptionNo
case "optional":
*d = EncryptionOptional
case "strict", "yes", "true":
case "true", "yes":
*d = EncryptionStrict
}
switch *d {
case EncryptionNo, EncryptionOptional, EncryptionStrict:
default:
return fmt.Errorf("invalid encryption: '%s'", in)
return fmt.Errorf("invalid encryption: '%s'", *d)
}
return nil
+83 -11
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"net"
"net/url"
"os"
"reflect"
"regexp"
"sort"
@@ -11,6 +12,8 @@ import (
"time"
"github.com/bluenviron/gortsplib/v5/pkg/base"
"github.com/bluenviron/mediacommon/v2/pkg/formats/mp4/codecs"
"github.com/bluenviron/mediacommon/v2/pkg/formats/pmp4"
"github.com/bluenviron/mediamtx/internal/logger"
)
@@ -63,6 +66,34 @@ func checkRedirect(v string) error {
return nil
}
func checkAlwaysAvailableFile(fpath string) error {
f, err := os.Open(fpath)
if err != nil {
return err
}
defer f.Close()
var presentation pmp4.Presentation
err = presentation.Unmarshal(f)
if err != nil {
return err
}
if len(presentation.Tracks) == 0 {
return fmt.Errorf("file does not contain any track")
}
for _, track := range presentation.Tracks {
switch track.Codec.(type) {
case *codecs.AV1, *codecs.VP9, *codecs.H265, *codecs.H264, *codecs.Opus, *codecs.MPEG4Audio, *codecs.LPCM:
default:
return fmt.Errorf("unsupported codec %T", track.Codec)
}
}
return nil
}
// FindPathConf returns the configuration corresponding to the given path name.
func FindPathConf(pathConfs map[string]*Path, name string) (*Path, []string, error) {
// normal path
@@ -120,6 +151,11 @@ type Path struct {
Fallback string `json:"fallback"`
UseAbsoluteTimestamp bool `json:"useAbsoluteTimestamp"`
// Always available
AlwaysAvailable bool `json:"alwaysAvailable"`
AlwaysAvailableFile string `json:"alwaysAvailableFile"`
AlwaysAvailableTracks []AlwaysAvailableTrack `json:"alwaysAvailableTracks"`
// Record
Record bool `json:"record"`
Playback *bool `json:"playback,omitempty"` // deprecated
@@ -235,6 +271,11 @@ func (pconf *Path) setDefaults() {
pconf.SourceOnDemandStartTimeout = 10 * Duration(time.Second)
pconf.SourceOnDemandCloseAfter = 10 * Duration(time.Second)
// Always available
pconf.AlwaysAvailableTracks = []AlwaysAvailableTrack{
{Codec: "H264"},
}
// Record
pconf.RecordPath = "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f"
pconf.RecordFormat = RecordFormatFMP4
@@ -317,25 +358,15 @@ func (pconf *Path) validate(
// common configuration errors
if pconf.Source != "publisher" && pconf.Source != "redirect" &&
pconf.Regexp != nil && !pconf.SourceOnDemand {
return fmt.Errorf("a path with a regular expression (or path 'all') and a static source" +
" must have 'sourceOnDemand' set to true")
}
if pconf.SRTPublishPassphrase != "" && pconf.Source != "publisher" {
return fmt.Errorf("'srtPublishPassphase' can only be used when source is 'publisher'")
}
if pconf.SourceOnDemand && pconf.Source == "publisher" {
return fmt.Errorf("'sourceOnDemand' is useless when source is 'publisher'")
}
if pconf.Source != "redirect" && pconf.SourceRedirect != "" {
return fmt.Errorf("'sourceRedirect' is useless when source is not 'redirect'")
}
// source-dependent settings
// General
switch {
case pconf.Source == "publisher":
@@ -613,6 +644,17 @@ func (pconf *Path) validate(
return fmt.Errorf("invalid source: '%s'", pconf.Source)
}
if pconf.SourceOnDemand {
if pconf.Source == "publisher" {
return fmt.Errorf("'sourceOnDemand' is useless when source is 'publisher'")
}
} else {
if pconf.Source != "publisher" && pconf.Source != "redirect" && pconf.Regexp != nil {
return fmt.Errorf("a path with a regular expression (or path 'all') and a static source" +
" must have 'sourceOnDemand' set to true")
}
}
if pconf.SRTReadPassphrase != "" {
err := checkSRTPassphrase(pconf.SRTReadPassphrase)
if err != nil {
@@ -627,6 +669,35 @@ func (pconf *Path) validate(
}
}
// Always available
if pconf.AlwaysAvailable {
if pconf.Regexp != nil {
return fmt.Errorf("'alwaysAvailable' cannot be used in a path with a regular expression (or path 'all')")
}
if pconf.SourceOnDemand {
return fmt.Errorf("'sourceOnDemand' is not compatible with 'alwaysAvailable'")
}
if pconf.RunOnDemand != "" || pconf.RunOnUnDemand != "" {
return fmt.Errorf("'runOnDemand' and 'runOnUnDemand' cannot be used with 'alwaysAvailable'")
}
if pconf.AlwaysAvailableFile != "" {
err := checkAlwaysAvailableFile(pconf.AlwaysAvailableFile)
if err != nil {
return fmt.Errorf("invalid 'alwaysAvailableVideo': %w", err)
}
} else if len(pconf.AlwaysAvailableTracks) == 0 {
return fmt.Errorf("'alwaysAvailableTracks' must contain at least one track")
}
if pconf.UseAbsoluteTimestamp {
return fmt.Errorf("'useAbsoluteTimestamp' cannot be used with 'alwaysAvailable'")
}
}
// Record
if pconf.Playback != nil {
@@ -733,6 +804,7 @@ func (pconf *Path) validate(
return fmt.Errorf("a path with a regular expression (or path 'all')" +
" does not support option 'runOnInit'; use another path")
}
if (pconf.RunOnDemand != "" || pconf.RunOnUnDemand != "") && pconf.Source != "publisher" {
return fmt.Errorf("'runOnDemand' and 'runOnUnDemand' can be used only when source is 'publisher'")
}
+8 -28
View File
@@ -1,52 +1,32 @@
package conf
import (
"encoding/json"
"fmt"
"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper"
)
// RecordFormat is the recordFormat parameter.
type RecordFormat int
type RecordFormat string
// supported values.
const (
RecordFormatFMP4 RecordFormat = iota
RecordFormatMPEGTS
RecordFormatFMP4 RecordFormat = "fmp4"
RecordFormatMPEGTS RecordFormat = "mpegts"
)
// MarshalJSON implements json.Marshaler.
func (d RecordFormat) MarshalJSON() ([]byte, error) {
var out string
switch d {
case RecordFormatMPEGTS:
out = "mpegts"
default:
out = "fmp4"
}
return json.Marshal(out)
}
// UnmarshalJSON implements json.Unmarshaler.
func (d *RecordFormat) UnmarshalJSON(b []byte) error {
var in string
if err := jsonwrapper.Unmarshal(b, &in); err != nil {
type alias RecordFormat
if err := jsonwrapper.Unmarshal(b, (*alias)(d)); err != nil {
return err
}
switch in {
case "mpegts":
*d = RecordFormatMPEGTS
case "fmp4":
*d = RecordFormatFMP4
switch *d {
case RecordFormatFMP4, RecordFormatMPEGTS:
default:
return fmt.Errorf("invalid record format '%s'", in)
return fmt.Errorf("invalid record format '%s'", *d)
}
return nil
+10 -42
View File
@@ -1,66 +1,34 @@
package conf
import (
"encoding/json"
"fmt"
"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper"
)
// RTSPRangeType is the type used in the Range header.
type RTSPRangeType int
type RTSPRangeType string
// supported values.
const (
RTSPRangeTypeUndefined RTSPRangeType = iota
RTSPRangeTypeClock
RTSPRangeTypeNPT
RTSPRangeTypeSMPTE
RTSPRangeTypeUndefined RTSPRangeType = ""
RTSPRangeTypeClock RTSPRangeType = "clock"
RTSPRangeTypeNPT RTSPRangeType = "npt"
RTSPRangeTypeSMPTE RTSPRangeType = "smpte"
)
// MarshalJSON implements json.Marshaler.
func (d RTSPRangeType) MarshalJSON() ([]byte, error) {
var out string
switch d {
case RTSPRangeTypeClock:
out = "clock"
case RTSPRangeTypeNPT:
out = "npt"
case RTSPRangeTypeSMPTE:
out = "smpte"
default:
out = ""
}
return json.Marshal(out)
}
// UnmarshalJSON implements json.Unmarshaler.
func (d *RTSPRangeType) UnmarshalJSON(b []byte) error {
var in string
if err := jsonwrapper.Unmarshal(b, &in); err != nil {
type alias RTSPRangeType
if err := jsonwrapper.Unmarshal(b, (*alias)(d)); err != nil {
return err
}
switch in {
case "clock":
*d = RTSPRangeTypeClock
case "npt":
*d = RTSPRangeTypeNPT
case "smpte":
*d = RTSPRangeTypeSMPTE
case "":
*d = RTSPRangeTypeUndefined
switch *d {
case RTSPRangeTypeUndefined, RTSPRangeTypeClock, RTSPRangeTypeNPT, RTSPRangeTypeSMPTE:
default:
return fmt.Errorf("invalid rtsp range type: '%s'", in)
return fmt.Errorf("invalid rtsp range type: '%s'", *d)
}
return nil
+117 -38
View File
@@ -86,7 +86,8 @@ type path struct {
publisherQuery string
stream *stream.Stream
recorder *recorder.Recorder
readyTime time.Time
availableTime time.Time
onlineTime time.Time
onUnDemandHook func(string)
onNotReadyHook func()
readers map[defs.Reader]struct{}
@@ -158,7 +159,7 @@ func (pa *path) Name() string {
return pa.name
}
func (pa *path) isReady() bool {
func (pa *path) isAvailable() bool {
return pa.stream != nil
}
@@ -166,6 +167,13 @@ func (pa *path) run() {
defer close(pa.done)
defer pa.wg.Done()
if pa.conf.AlwaysAvailable {
err := pa.setAvailable(nil, true)
if err != nil {
panic(err)
}
}
if pa.conf.Source == "redirect" {
pa.source = &sourceRedirect{}
} else if pa.conf.HasStaticSource() {
@@ -218,7 +226,7 @@ func (pa *path) run() {
}
if pa.stream != nil {
pa.setNotReady()
pa.setNotAvailable()
}
if pa.source != nil {
@@ -329,7 +337,10 @@ func (pa *path) doOnDemandStaticSourceReadyTimer() {
}
func (pa *path) doOnDemandStaticSourceCloseTimer() {
pa.setNotReady()
if pa.conf.AlwaysAvailable {
panic("should not happen")
}
pa.setNotAvailable()
pa.onDemandStaticSourceStop("not needed by anyone")
}
@@ -379,12 +390,31 @@ func (pa *path) doReloadConf(newConf *conf.Path) {
}
func (pa *path) doSourceStaticSetReady(req defs.PathSourceStaticSetReadyReq) {
err := pa.setReady(req.Desc, req.UseRTPPackets, req.ReplaceNTP)
if !pa.conf.AlwaysAvailable {
err := pa.setAvailable(req.Desc, req.ReplaceNTP)
if err != nil {
req.Res <- defs.PathSourceStaticSetReadyRes{Err: err}
return
}
}
subStream := &stream.SubStream{
Stream: pa.stream,
UseRTPPackets: req.UseRTPPackets,
}
if pa.conf.AlwaysAvailable {
subStream.CurDesc = req.Desc
}
err := subStream.Initialize()
if err != nil {
req.Res <- defs.PathSourceStaticSetReadyRes{Err: err}
return
}
if pa.conf.AlwaysAvailable {
pa.onlineTime = time.Now()
}
if pa.conf.HasOnDemandStaticSource() {
pa.onDemandStaticSourceReadyTimer.Stop()
pa.onDemandStaticSourceReadyTimer = emptyTimer()
@@ -393,11 +423,18 @@ func (pa *path) doSourceStaticSetReady(req defs.PathSourceStaticSetReadyReq) {
pa.consumeOnHoldRequests()
req.Res <- defs.PathSourceStaticSetReadyRes{Stream: pa.stream}
req.Res <- defs.PathSourceStaticSetReadyRes{SubStream: subStream}
}
func (pa *path) doSourceStaticSetNotReady(req defs.PathSourceStaticSetNotReadyReq) {
pa.setNotReady()
if !pa.conf.AlwaysAvailable {
pa.setNotAvailable()
} else {
err := pa.stream.StartOfflineSubStream()
if err != nil {
panic("should not happen")
}
}
// send response before calling onDemandStaticSourceStop()
// in order to avoid a deadlock due to staticsources.Handler.stop()
@@ -476,16 +513,34 @@ func (pa *path) doAddPublisher(req defs.PathAddPublisherReq) {
pa.source = req.Author
pa.publisherQuery = req.AccessRequest.Query
err := pa.setReady(req.Desc, req.UseRTPPackets, req.ReplaceNTP)
req.Author.Log(logger.Info, "is publishing to path '%s'",
pa.name)
if !pa.conf.AlwaysAvailable {
err := pa.setAvailable(req.Desc, req.ReplaceNTP)
if err != nil {
pa.source = nil
req.Res <- defs.PathAddPublisherRes{Err: err}
return
}
}
subStream := &stream.SubStream{
Stream: pa.stream,
UseRTPPackets: req.UseRTPPackets,
}
if pa.conf.AlwaysAvailable {
subStream.CurDesc = req.Desc
}
err := subStream.Initialize()
if err != nil {
pa.source = nil
req.Res <- defs.PathAddPublisherRes{Err: err}
return
}
req.Author.Log(logger.Info, "is publishing to path '%s', %s",
pa.name,
defs.MediasInfo(req.Desc.Medias))
if pa.conf.AlwaysAvailable {
pa.onlineTime = time.Now()
}
if pa.conf.HasOnDemandPublisher() && pa.onDemandPublisherState != pathOnDemandStateInitial {
pa.onDemandPublisherReadyTimer.Stop()
@@ -496,8 +551,8 @@ func (pa *path) doAddPublisher(req defs.PathAddPublisherReq) {
pa.consumeOnHoldRequests()
req.Res <- defs.PathAddPublisherRes{
Path: pa,
Stream: pa.stream,
Path: pa,
SubStream: subStream,
}
}
@@ -550,6 +605,14 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) {
data: &defs.APIPath{
Name: pa.name,
ConfName: pa.conf.Name,
Ready: pa.isAvailable(),
ReadyTime: func() *time.Time {
if !pa.isAvailable() {
return nil
}
v := pa.availableTime
return &v
}(),
Source: func() *defs.APIPathSource {
if pa.source == nil {
return nil
@@ -557,28 +620,20 @@ func (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) {
v := pa.source.APISourceDescribe()
return v
}(),
Ready: pa.isReady(),
ReadyTime: func() *time.Time {
if !pa.isReady() {
return nil
}
v := pa.readyTime
return &v
}(),
Tracks: func() []string {
if !pa.isReady() {
if !pa.isAvailable() {
return []string{}
}
return defs.MediasToCodecs(pa.stream.Desc.Medias)
}(),
BytesReceived: func() uint64 {
if !pa.isReady() {
if !pa.isAvailable() {
return 0
}
return pa.stream.BytesReceived()
}(),
BytesSent: func() uint64 {
if !pa.isReady() {
if !pa.isAvailable() {
return 0
}
return pa.stream.BytesSent()
@@ -688,35 +743,52 @@ func (pa *path) onDemandPublisherStop(reason string) {
pa.onDemandPublisherState = pathOnDemandStateInitial
}
func (pa *path) setReady(desc *description.Session, useRTPPackets bool, replaceNTP bool) error {
func (pa *path) setAvailable(desc *description.Session, replaceNTP bool) error {
pa.stream = &stream.Stream{
Desc: desc,
UseRTPPackets: useRTPPackets,
WriteQueueSize: pa.writeQueueSize,
RTPMaxPayloadSize: pa.rtpMaxPayloadSize,
ReplaceNTP: replaceNTP,
Parent: pa.source,
Desc: desc,
AlwaysAvailable: pa.conf.AlwaysAvailable,
AlwaysAvailableFile: pa.conf.AlwaysAvailableFile,
AlwaysAvailableTracks: pa.conf.AlwaysAvailableTracks,
WriteQueueSize: pa.writeQueueSize,
RTPMaxPayloadSize: pa.rtpMaxPayloadSize,
ReplaceNTP: replaceNTP,
Parent: pa,
}
err := pa.stream.Initialize()
if err != nil {
return err
}
pa.readyTime = time.Now()
pa.availableTime = time.Now()
if !pa.conf.AlwaysAvailable {
pa.onlineTime = time.Now()
}
if pa.conf.Record {
pa.startRecording()
}
var sourceDesc *defs.APIPathSource
if pa.source != nil {
sourceDesc = pa.source.APISourceDescribe()
}
pa.onNotReadyHook = hooks.OnReady(hooks.OnReadyParams{
Logger: pa,
ExternalCmdPool: pa.externalCmdPool,
Conf: pa.conf,
ExternalCmdEnv: pa.ExternalCmdEnv(),
Desc: *pa.source.APISourceDescribe(),
Desc: sourceDesc,
Query: pa.publisherQuery,
})
if pa.conf.AlwaysAvailable {
pa.Log(logger.Info, "stream is available, %s", defs.MediasInfo(pa.stream.Desc.Medias))
} else {
pa.Log(logger.Info, "stream is available and online, %s", defs.MediasInfo(pa.stream.Desc.Medias))
}
pa.parent.pathReady(pa)
return nil
@@ -736,7 +808,7 @@ func (pa *path) consumeOnHoldRequests() {
pa.readerAddRequestsOnHold = nil
}
func (pa *path) setNotReady() {
func (pa *path) setNotAvailable() {
pa.parent.pathNotReady(pa)
for r := range pa.readers {
@@ -805,7 +877,14 @@ func (pa *path) executeRemoveReader(r defs.Reader) {
}
func (pa *path) executeRemovePublisher() {
pa.setNotReady()
if !pa.conf.AlwaysAvailable {
pa.setNotAvailable()
} else {
err := pa.stream.StartOfflineSubStream()
if err != nil {
panic("should not happen")
}
}
pa.source = nil
}
@@ -900,11 +979,11 @@ func (pa *path) describe(req defs.PathDescribeReq) defs.PathDescribeRes {
}
// addPublisher is called by a publisher through pathManager.
func (pa *path) addPublisher(req defs.PathAddPublisherReq) (defs.Path, *stream.Stream, error) {
func (pa *path) addPublisher(req defs.PathAddPublisherReq) (defs.Path, *stream.SubStream, error) {
select {
case pa.chAddPublisher <- req:
res := <-req.Res
return res.Path, res.Stream, res.Err
return res.Path, res.SubStream, res.Err
case <-pa.ctx.Done():
return nil, nil, fmt.Errorf("terminated")
}
+1 -1
View File
@@ -533,7 +533,7 @@ func (pm *pathManager) Describe(req defs.PathDescribeReq) defs.PathDescribeRes {
}
// AddPublisher is called by a publisher.
func (pm *pathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, *stream.Stream, error) {
func (pm *pathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, *stream.SubStream, error) {
req.Res = make(chan defs.PathAddPublisherRes)
select {
case pm.chAddPublisher <- req:
+6 -6
View File
@@ -57,9 +57,9 @@ type PathDescribeReq struct {
// PathAddPublisherRes contains the response of AddPublisher().
type PathAddPublisherRes struct {
Path Path
Stream *stream.Stream
Err error
Path Path
SubStream *stream.SubStream
Err error
}
// PathAddPublisherReq contains arguments of AddPublisher().
@@ -99,10 +99,10 @@ type PathRemoveReaderReq struct {
Res chan struct{}
}
// PathSourceStaticSetReadyRes contains the response of SetReadu().
// PathSourceStaticSetReadyRes contains the response of SetReady().
type PathSourceStaticSetReadyRes struct {
Stream *stream.Stream
Err error
SubStream *stream.SubStream
Err error
}
// PathSourceStaticSetReadyReq contains arguments of SetReady().
+5 -3
View File
@@ -13,7 +13,7 @@ type OnReadyParams struct {
ExternalCmdPool *externalcmd.Pool
Conf *conf.Path
ExternalCmdEnv externalcmd.Environment
Desc defs.APIPathSource
Desc *defs.APIPathSource
Query string
}
@@ -25,8 +25,10 @@ func OnReady(params OnReadyParams) func() {
if params.Conf.RunOnReady != "" || params.Conf.RunOnNotReady != "" {
env = params.ExternalCmdEnv
env["MTX_QUERY"] = params.Query
env["MTX_SOURCE_TYPE"] = params.Desc.Type
env["MTX_SOURCE_ID"] = params.Desc.ID
if params.Desc != nil {
env["MTX_SOURCE_TYPE"] = params.Desc.Type
env["MTX_SOURCE_ID"] = params.Desc.ID
}
}
if params.Conf.RunOnReady != "" {
+16 -11
View File
@@ -488,8 +488,9 @@ func TestOnGet(t *testing.T) {
WriteTimeout: conf.Duration(10 * time.Second),
PathConfs: map[string]*conf.Path{
"mypath": {
Name: "mypath",
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
Name: "mypath",
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
RecordFormat: conf.RecordFormatFMP4,
},
},
AuthManager: test.NilAuthManager,
@@ -660,9 +661,10 @@ func TestOnGet(t *testing.T) {
TimeOffset: 48000,
Codec: &mcodecs.MPEG4Audio{
Config: mpeg4audio.AudioSpecificConfig{
Type: mpeg4audio.ObjectTypeAACLC,
SampleRate: 48000,
ChannelCount: 2,
Type: mpeg4audio.ObjectTypeAACLC,
SampleRate: 48000,
ChannelCount: 2,
ChannelConfig: 2,
},
},
Samples: []*pmp4.Sample{
@@ -714,8 +716,9 @@ func TestOnGetDifferentInit(t *testing.T) {
WriteTimeout: conf.Duration(10 * time.Second),
PathConfs: map[string]*conf.Path{
"mypath": {
Name: "mypath",
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
Name: "mypath",
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
RecordFormat: conf.RecordFormatFMP4,
},
},
AuthManager: test.NilAuthManager,
@@ -836,8 +839,9 @@ func TestOnGetInMiddleOfLastSample(t *testing.T) {
WriteTimeout: conf.Duration(10 * time.Second),
PathConfs: map[string]*conf.Path{
"mypath": {
Name: "mypath",
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
Name: "mypath",
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
RecordFormat: conf.RecordFormatFMP4,
},
},
AuthManager: test.NilAuthManager,
@@ -963,8 +967,9 @@ func TestOnGetBetweenSegments(t *testing.T) {
WriteTimeout: conf.Duration(10 * time.Second),
PathConfs: map[string]*conf.Path{
"mypath": {
Name: "mypath",
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
Name: "mypath",
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
RecordFormat: conf.RecordFormatFMP4,
},
},
AuthManager: test.NilAuthManager,
+6 -4
View File
@@ -64,8 +64,9 @@ func TestOnList(t *testing.T) {
WriteTimeout: conf.Duration(10 * time.Second),
PathConfs: map[string]*conf.Path{
"mypath": {
Name: "mypath",
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
Name: "mypath",
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
RecordFormat: conf.RecordFormatFMP4,
},
},
AuthManager: &test.AuthManager{
@@ -294,8 +295,9 @@ func TestOnListCachedDuration(t *testing.T) {
WriteTimeout: conf.Duration(10 * time.Second),
PathConfs: map[string]*conf.Path{
"mypath": {
Name: "mypath",
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
Name: "mypath",
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
RecordFormat: conf.RecordFormatFMP4,
},
},
AuthManager: test.NilAuthManager,
+5 -5
View File
@@ -146,28 +146,28 @@ func segmentFMP4ReadHeader(r io.ReadSeeker) (*fmp4.Init, time.Duration, error) {
// read mvhd
var mvhd amp4.Mvhd
mvhdSize, err := amp4.Unmarshal(r, uint64(moovSize-8), &mvhd, amp4.Context{})
_, err = amp4.Unmarshal(r, uint64(moovSize-8), &mvhd, amp4.Context{})
if err != nil {
return nil, 0, err
}
d := time.Duration(mvhd.DurationV0) * time.Second / time.Duration(mvhd.Timescale)
// read moov
// read ftyp and moov
_, err = r.Seek(int64(-mvhdSize-8-8), io.SeekCurrent)
_, err = r.Seek(0, io.SeekStart)
if err != nil {
return nil, 0, err
}
buf = make([]byte, uint64(moovSize))
buf = make([]byte, uint64(ftypSize+moovSize))
_, err = io.ReadFull(r, buf)
if err != nil {
return nil, 0, err
}
// pass moov to fmp4.Init
// pass ftyp and moov to fmp4.Init
var init fmp4.Init
err = init.Unmarshal(bytes.NewReader(buf))
+8 -9
View File
@@ -9,7 +9,6 @@ import (
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/ntpestimator"
"github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/unit"
@@ -35,7 +34,7 @@ func ToStream(
c *gohlslib.Client,
tracks []*gohlslib.Track,
pathConf *conf.Path,
strm **stream.Stream,
subStream **stream.SubStream,
) ([]*description.Media, error) {
var ntpStat ntpState
var ntpStatMutex sync.Mutex
@@ -74,7 +73,7 @@ func ToStream(
case ntpStateUnavailable:
_, avail := c.AbsoluteTime(ctrack)
if avail {
(*strm).Parent.Log(logger.Warn, "absolute timestamp appeared after stream started, we are not using it")
// absolute timestamp appeared after stream started, we are not using it
ntpStat = ntpStateReplace
}
return ntpEstimator.Estimate(pts)
@@ -97,7 +96,7 @@ func ToStream(
newClockRate := medi.Formats[0].ClockRate()
c.OnDataAV1(ctrack, func(pts int64, tu [][]byte) {
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
NTP: handleNTP(pts),
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),
Payload: unit.PayloadAV1(tu),
@@ -114,7 +113,7 @@ func ToStream(
newClockRate := medi.Formats[0].ClockRate()
c.OnDataVP9(ctrack, func(pts int64, frame []byte) {
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
NTP: handleNTP(pts),
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),
Payload: unit.PayloadVP9(frame),
@@ -134,7 +133,7 @@ func ToStream(
newClockRate := medi.Formats[0].ClockRate()
c.OnDataH26x(ctrack, func(pts int64, _ int64, au [][]byte) {
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
NTP: handleNTP(pts),
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),
Payload: unit.PayloadH265(au),
@@ -154,7 +153,7 @@ func ToStream(
newClockRate := medi.Formats[0].ClockRate()
c.OnDataH26x(ctrack, func(pts int64, _ int64, au [][]byte) {
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
NTP: handleNTP(pts),
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),
Payload: unit.PayloadH264(au),
@@ -172,7 +171,7 @@ func ToStream(
newClockRate := medi.Formats[0].ClockRate()
c.OnDataOpus(ctrack, func(pts int64, packets [][]byte) {
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
NTP: handleNTP(pts),
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),
Payload: unit.PayloadOpus(packets),
@@ -193,7 +192,7 @@ func ToStream(
newClockRate := medi.Formats[0].ClockRate()
c.OnDataMPEG4Audio(ctrack, func(pts int64, aus [][]byte) {
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
NTP: handleNTP(pts),
PTS: multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),
Payload: unit.PayloadMPEG4Audio(aus),
+9 -2
View File
@@ -86,6 +86,7 @@ func TestToStream(t *testing.T) {
defer s.Shutdown(context.Background())
var strm *stream.Stream
var subStream *stream.SubStream
done := make(chan struct{})
r := &stream.Reader{Parent: test.NilLogger}
@@ -96,7 +97,7 @@ func TestToStream(t *testing.T) {
OnTracks: func(tracks []*gohlslib.Track) error {
medias, err2 := ToStream(c, tracks, &conf.Path{
UseAbsoluteTimestamp: true,
}, &strm)
}, &subStream)
require.NoError(t, err2)
require.Equal(t, []*description.Media{{
@@ -109,7 +110,6 @@ func TestToStream(t *testing.T) {
strm = &stream.Stream{
Desc: &description.Session{Medias: medias},
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -117,6 +117,13 @@ func TestToStream(t *testing.T) {
err2 = strm.Initialize()
require.NoError(t, err2)
subStream = &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err2 = subStream.Initialize()
require.NoError(t, err2)
n := 0
r.OnData(
+11 -11
View File
@@ -24,7 +24,7 @@ var errNoSupportedCodecs = errors.New(
// ToStream maps a MPEG-TS stream to a MediaMTX stream.
func ToStream(
r *EnhancedReader,
strm **stream.Stream,
subStream **stream.SubStream,
l logger.Writer,
) ([]*description.Media, error) {
var medias []*description.Media //nolint:prealloc
@@ -48,7 +48,7 @@ func ToStream(
r.OnDataH265(track, func(pts int64, _ int64, au [][]byte) error {
pts = td.Decode(pts)
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
PTS: pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP
Payload: unit.PayloadH265(au),
})
@@ -67,7 +67,7 @@ func ToStream(
r.OnDataH264(track, func(pts int64, _ int64, au [][]byte) error {
pts = td.Decode(pts)
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
PTS: pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP
Payload: unit.PayloadH264(au),
})
@@ -85,7 +85,7 @@ func ToStream(
r.OnDataMPEGxVideo(track, func(pts int64, frame []byte) error {
pts = td.Decode(pts)
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
PTS: pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP
Payload: unit.PayloadMPEG4Video(frame),
})
@@ -101,7 +101,7 @@ func ToStream(
r.OnDataMPEGxVideo(track, func(pts int64, frame []byte) error {
pts = td.Decode(pts)
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
PTS: pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP
Payload: unit.PayloadMPEG1Video(frame),
})
@@ -120,7 +120,7 @@ func ToStream(
r.OnDataOpus(track, func(pts int64, packets [][]byte) error {
pts = td.Decode(pts)
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
PTS: multiplyAndDivide(pts, int64(medi.Formats[0].ClockRate()), 90000),
Payload: unit.PayloadOpus(packets),
})
@@ -137,7 +137,7 @@ func ToStream(
r.OnDataKLV(track, func(pts int64, uni []byte) error {
pts = td.Decode(pts)
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
PTS: pts,
Payload: unit.PayloadKLV(uni),
})
@@ -159,7 +159,7 @@ func ToStream(
r.OnDataMPEG4Audio(track, func(pts int64, aus [][]byte) error {
pts = td.Decode(pts)
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
PTS: multiplyAndDivide(pts, int64(medi.Formats[0].ClockRate()), 90000),
Payload: unit.PayloadMPEG4Audio(aus),
})
@@ -210,7 +210,7 @@ func ToStream(
return err
}
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
PTS: pts,
Payload: unit.PayloadMPEG4AudioLATM(buf),
})
@@ -230,7 +230,7 @@ func ToStream(
r.OnDataMPEG1Audio(track, func(pts int64, frames [][]byte) error {
pts = td.Decode(pts)
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
PTS: pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP
Payload: unit.PayloadMPEG1Audio(frames),
})
@@ -250,7 +250,7 @@ func ToStream(
r.OnDataAC3(track, func(pts int64, frame []byte) error {
pts = td.Decode(pts)
(*strm).WriteUnit(medi, medi.Formats[0], &unit.Unit{
(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{
PTS: multiplyAndDivide(pts, int64(medi.Formats[0].ClockRate()), 90000),
Payload: unit.PayloadAC3{frame},
})
+4 -3
View File
@@ -134,9 +134,10 @@ func TestToStream(t *testing.T) {
Programs: []*mpeg4audio.StreamMuxConfigProgram{{
Layers: []*mpeg4audio.StreamMuxConfigLayer{{
AudioSpecificConfig: &mpeg4audio.AudioSpecificConfig{
Type: 2,
SampleRate: 48000,
ChannelCount: 2,
Type: 2,
SampleRate: 48000,
ChannelCount: 2,
ChannelConfig: 2,
},
LatmBufferFullness: 255,
}},
+41 -35
View File
@@ -72,7 +72,7 @@ func TestFromStream(t *testing.T) {
name string
medias []*description.Media
expectedTracks []*gortmplib.Track
writeUnits func([]*description.Media, *stream.Stream)
writeUnits func([]*description.Media, *stream.SubStream)
}{
{
name: "h264 + aac",
@@ -93,15 +93,15 @@ func TestFromStream(t *testing.T) {
Config: test.FormatMPEG4Audio.Config,
}},
},
writeUnits: func(medias []*description.Media, strm *stream.Stream) {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
writeUnits: func(medias []*description.Media, subStream *stream.SubStream) {
subStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
PTS: 0,
Payload: unit.PayloadH264{
{5, 2}, // IDR
},
})
strm.WriteUnit(medias[1], medias[1].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[1], medias[1].Formats[0], &unit.Unit{
PTS: 90000 * 5,
Payload: unit.PayloadMPEG4Audio{
{3, 4},
@@ -121,9 +121,9 @@ func TestFromStream(t *testing.T) {
expectedTracks: []*gortmplib.Track{
{Codec: &codecs.AV1{}},
},
writeUnits: func(medias []*description.Media, strm *stream.Stream) {
writeUnits: func(medias []*description.Media, subStream *stream.SubStream) {
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
PTS: 90000 * 2 * int64(i),
Payload: unit.PayloadAV1{{
0x0a, 0x0e, 0x00, 0x00, 0x00, 0x4a, 0xab, 0xbf,
@@ -145,9 +145,9 @@ func TestFromStream(t *testing.T) {
expectedTracks: []*gortmplib.Track{
{Codec: &codecs.VP9{}},
},
writeUnits: func(medias []*description.Media, strm *stream.Stream) {
writeUnits: func(medias []*description.Media, subStream *stream.SubStream) {
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
PTS: 90000 * 2 * int64(i),
Payload: unit.PayloadVP9{1, 2},
})
@@ -175,9 +175,9 @@ func TestFromStream(t *testing.T) {
PPS: h265PPS,
}},
},
writeUnits: func(medias []*description.Media, strm *stream.Stream) {
writeUnits: func(medias []*description.Media, subStream *stream.SubStream) {
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
PTS: 90000 * 2 * int64(i),
Payload: unit.PayloadH265{{
0x2a, 0x01, 0xad, 0xe0, 0xf5, 0x34, 0x11, 0x0b,
@@ -200,9 +200,9 @@ func TestFromStream(t *testing.T) {
PPS: test.FormatH264.PPS,
}},
},
writeUnits: func(medias []*description.Media, strm *stream.Stream) {
writeUnits: func(medias []*description.Media, subStream *stream.SubStream) {
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
PTS: 90000 * 2 * int64(i),
Payload: unit.PayloadH264{
{5, 2}, // IDR
@@ -226,9 +226,9 @@ func TestFromStream(t *testing.T) {
ChannelCount: 2,
}},
},
writeUnits: func(medias []*description.Media, strm *stream.Stream) {
writeUnits: func(medias []*description.Media, subStream *stream.SubStream) {
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
PTS: 90000 * 5 * int64(i),
Payload: unit.PayloadOpus{
{3, 4},
@@ -249,9 +249,9 @@ func TestFromStream(t *testing.T) {
Config: test.FormatMPEG4Audio.Config,
}},
},
writeUnits: func(medias []*description.Media, strm *stream.Stream) {
writeUnits: func(medias []*description.Media, subStream *stream.SubStream) {
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
PTS: 90000 * 5 * int64(i),
Payload: unit.PayloadMPEG4Audio{
{3, 4},
@@ -270,9 +270,9 @@ func TestFromStream(t *testing.T) {
expectedTracks: []*gortmplib.Track{
{Codec: &codecs.MPEG1Audio{}},
},
writeUnits: func(medias []*description.Media, strm *stream.Stream) {
writeUnits: func(medias []*description.Media, subStream *stream.SubStream) {
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
PTS: 90000 * 5 * int64(i),
Payload: unit.PayloadMPEG1Audio{
{
@@ -299,9 +299,9 @@ func TestFromStream(t *testing.T) {
ChannelCount: 1,
}},
},
writeUnits: func(medias []*description.Media, strm *stream.Stream) {
writeUnits: func(medias []*description.Media, subStream *stream.SubStream) {
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
PTS: 90000 * 5 * int64(i),
Payload: unit.PayloadAC3{
{
@@ -377,9 +377,9 @@ func TestFromStream(t *testing.T) {
SampleRate: 8000,
}},
},
writeUnits: func(medias []*description.Media, strm *stream.Stream) {
writeUnits: func(medias []*description.Media, subStream *stream.SubStream) {
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
PTS: 90000 * 5 * int64(i),
Payload: unit.PayloadG711{
3, 4,
@@ -406,9 +406,9 @@ func TestFromStream(t *testing.T) {
SampleRate: 8000,
}},
},
writeUnits: func(medias []*description.Media, strm *stream.Stream) {
writeUnits: func(medias []*description.Media, subStream *stream.SubStream) {
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
PTS: 90000 * 5 * int64(i),
Payload: unit.PayloadG711{
3, 4,
@@ -435,9 +435,9 @@ func TestFromStream(t *testing.T) {
ChannelCount: 2,
}},
},
writeUnits: func(medias []*description.Media, strm *stream.Stream) {
writeUnits: func(medias []*description.Media, subStream *stream.SubStream) {
for i := range 2 {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
PTS: 90000 * 5 * int64(i),
Payload: unit.PayloadLPCM{
3, 4, 5, 6,
@@ -496,8 +496,8 @@ func TestFromStream(t *testing.T) {
Config: test.FormatMPEG4Audio.Config,
}},
},
writeUnits: func(medias []*description.Media, strm *stream.Stream) {
strm.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
writeUnits: func(medias []*description.Media, subStream *stream.SubStream) {
subStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{
Payload: unit.PayloadH265{
{
0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60,
@@ -522,7 +522,7 @@ func TestFromStream(t *testing.T) {
},
})
strm.WriteUnit(medias[1], medias[1].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[1], medias[1].Formats[0], &unit.Unit{
Payload: unit.PayloadH264{
h264DefaultSPS,
h264DefaultPPS,
@@ -530,24 +530,24 @@ func TestFromStream(t *testing.T) {
},
})
strm.WriteUnit(medias[2], medias[2].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[2], medias[2].Formats[0], &unit.Unit{
Payload: unit.PayloadVP9{1, 2},
})
strm.WriteUnit(medias[3], medias[3].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[3], medias[3].Formats[0], &unit.Unit{
Payload: unit.PayloadAV1{{
0x0a, 0x0e, 0x00, 0x00, 0x00, 0x4a, 0xab, 0xbf,
0xc3, 0x77, 0x6b, 0xe4, 0x40, 0x40, 0x40, 0x41,
}},
})
strm.WriteUnit(medias[4], medias[4].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[4], medias[4].Formats[0], &unit.Unit{
Payload: unit.PayloadOpus{
{3, 4},
},
})
strm.WriteUnit(medias[5], medias[5].Formats[0], &unit.Unit{
subStream.WriteUnit(medias[5], medias[5].Formats[0], &unit.Unit{
PTS: 90000 * 5,
Payload: unit.PayloadMPEG4Audio{
{3, 4},
@@ -563,7 +563,6 @@ func TestFromStream(t *testing.T) {
strm := &stream.Stream{
Desc: &description.Session{Medias: medias},
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -571,6 +570,13 @@ func TestFromStream(t *testing.T) {
err := strm.Initialize()
require.NoError(t, err)
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:9121")
require.NoError(t, err)
defer ln.Close()
@@ -619,7 +625,7 @@ func TestFromStream(t *testing.T) {
strm.AddReader(r)
defer strm.RemoveReader(r)
tc.writeUnits(medias, strm)
tc.writeUnits(medias, subStream)
<-done
})
+11 -11
View File
@@ -34,7 +34,7 @@ func fourCCToString(c message.FourCC) string {
// ToStream maps a RTMP stream to a MediaMTX stream.
func ToStream(
r *gortmplib.Reader,
strm **stream.Stream,
subStream **stream.SubStream,
) ([]*description.Media, error) {
var medias []*description.Media
@@ -51,7 +51,7 @@ func ToStream(
medias = append(medias, medi)
r.OnDataAV1(track, func(pts time.Duration, tu [][]byte) {
(*strm).WriteUnit(medi, forma, &unit.Unit{
(*subStream).WriteUnit(medi, forma, &unit.Unit{
PTS: durationToTimestamp(pts, forma.ClockRate()),
Payload: unit.PayloadAV1(tu),
})
@@ -68,7 +68,7 @@ func ToStream(
medias = append(medias, medi)
r.OnDataVP9(track, func(pts time.Duration, frame []byte) {
(*strm).WriteUnit(medi, forma, &unit.Unit{
(*subStream).WriteUnit(medi, forma, &unit.Unit{
PTS: durationToTimestamp(pts, forma.ClockRate()),
Payload: unit.PayloadVP9(frame),
})
@@ -88,7 +88,7 @@ func ToStream(
medias = append(medias, medi)
r.OnDataH265(track, func(pts time.Duration, _ time.Duration, au [][]byte) {
(*strm).WriteUnit(medi, forma, &unit.Unit{
(*subStream).WriteUnit(medi, forma, &unit.Unit{
PTS: durationToTimestamp(pts, forma.ClockRate()),
Payload: unit.PayloadH265(au),
})
@@ -108,7 +108,7 @@ func ToStream(
medias = append(medias, medi)
r.OnDataH264(track, func(pts time.Duration, _ time.Duration, au [][]byte) {
(*strm).WriteUnit(medi, forma, &unit.Unit{
(*subStream).WriteUnit(medi, forma, &unit.Unit{
PTS: durationToTimestamp(pts, forma.ClockRate()),
Payload: unit.PayloadH264(au),
})
@@ -126,7 +126,7 @@ func ToStream(
medias = append(medias, medi)
r.OnDataOpus(track, func(pts time.Duration, packet []byte) {
(*strm).WriteUnit(medi, forma, &unit.Unit{
(*subStream).WriteUnit(medi, forma, &unit.Unit{
PTS: durationToTimestamp(pts, forma.ClockRate()),
Payload: unit.PayloadOpus{packet},
})
@@ -147,7 +147,7 @@ func ToStream(
medias = append(medias, medi)
r.OnDataMPEG4Audio(track, func(pts time.Duration, au []byte) {
(*strm).WriteUnit(medi, forma, &unit.Unit{
(*subStream).WriteUnit(medi, forma, &unit.Unit{
PTS: durationToTimestamp(pts, forma.ClockRate()),
Payload: unit.PayloadMPEG4Audio{au},
})
@@ -162,7 +162,7 @@ func ToStream(
medias = append(medias, medi)
r.OnDataMPEG1Audio(track, func(pts time.Duration, frame []byte) {
(*strm).WriteUnit(medi, forma, &unit.Unit{
(*subStream).WriteUnit(medi, forma, &unit.Unit{
PTS: durationToTimestamp(pts, forma.ClockRate()),
Payload: unit.PayloadMPEG1Audio{frame},
})
@@ -181,7 +181,7 @@ func ToStream(
medias = append(medias, medi)
r.OnDataAC3(track, func(pts time.Duration, frame []byte) {
(*strm).WriteUnit(medi, forma, &unit.Unit{
(*subStream).WriteUnit(medi, forma, &unit.Unit{
PTS: durationToTimestamp(pts, forma.ClockRate()),
Payload: unit.PayloadAC3{frame},
})
@@ -210,7 +210,7 @@ func ToStream(
medias = append(medias, medi)
r.OnDataG711(track, func(pts time.Duration, samples []byte) {
(*strm).WriteUnit(medi, forma, &unit.Unit{
(*subStream).WriteUnit(medi, forma, &unit.Unit{
PTS: durationToTimestamp(pts, forma.ClockRate()),
Payload: unit.PayloadG711(samples),
})
@@ -230,7 +230,7 @@ func ToStream(
medias = append(medias, medi)
r.OnDataLPCM(track, func(pts time.Duration, samples []byte) {
(*strm).WriteUnit(medi, forma, &unit.Unit{
(*subStream).WriteUnit(medi, forma, &unit.Unit{
PTS: durationToTimestamp(pts, forma.ClockRate()),
Payload: unit.PayloadLPCM(samples),
})
+2 -2
View File
@@ -33,7 +33,7 @@ func ToStream(
source rtspSource,
medias []*description.Media,
pathConf *conf.Path,
strm **stream.Stream,
subStream **stream.SubStream,
log logger.Writer,
) {
for _, medi := range medias {
@@ -83,7 +83,7 @@ func ToStream(
return
}
(*strm).WriteUnit(cmedi, cforma, &unit.Unit{
(*subStream).WriteUnit(cmedi, cforma, &unit.Unit{
PTS: pts,
NTP: ntp,
RTPPackets: []*rtp.Packet{pkt},
@@ -96,7 +96,6 @@ func TestFromStreamResampleOpus(t *testing.T) {
}},
},
}},
UseRTPPackets: true,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
ReplaceNTP: false,
@@ -105,6 +104,13 @@ func TestFromStreamResampleOpus(t *testing.T) {
err := strm.Initialize()
require.NoError(t, err)
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: true,
}
err = subStream.Initialize()
require.NoError(t, err)
pc1 := &PeerConnection{
LocalRandomUDP: true,
IPsFromInterfaces: true,
@@ -153,7 +159,7 @@ func TestFromStreamResampleOpus(t *testing.T) {
strm.AddReader(r)
defer strm.RemoveReader(r)
strm.WriteUnit(strm.Desc.Medias[0], strm.Desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(strm.Desc.Medias[0], strm.Desc.Medias[0].Formats[0], &unit.Unit{
PTS: 0,
NTP: time.Now(),
RTPPackets: []*rtp.Packet{{
@@ -169,7 +175,7 @@ func TestFromStreamResampleOpus(t *testing.T) {
}},
})
strm.WriteUnit(strm.Desc.Medias[0], strm.Desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(strm.Desc.Medias[0], strm.Desc.Medias[0].Formats[0], &unit.Unit{
PTS: 0,
NTP: time.Now(),
RTPPackets: []*rtp.Packet{{
+2 -2
View File
@@ -33,7 +33,7 @@ var errNoSupportedCodecsTo = errors.New(
func ToStream(
pc *PeerConnection,
pathConf *conf.Path,
strm **stream.Stream,
subStream **stream.SubStream,
log logger.Writer,
) ([]*description.Media, error) {
var medias []*description.Media //nolint:prealloc
@@ -197,7 +197,7 @@ func ToStream(
return
}
(*strm).WriteUnit(medi, forma, &unit.Unit{
(*subStream).WriteUnit(medi, forma, &unit.Unit{
PTS: pts,
NTP: ntp,
RTPPackets: []*rtp.Packet{pkt},
+2 -2
View File
@@ -405,8 +405,8 @@ func TestToStream(t *testing.T) {
err = pc2.GatherIncomingTracks()
require.NoError(t, err)
var strm *stream.Stream
medias, err := ToStream(pc2, &conf.Path{}, &strm, nil)
var subStream *stream.SubStream
medias, err := ToStream(pc2, &conf.Path{}, &subStream, nil)
require.NoError(t, err)
require.Equal(t, ca.out, medias[0].Formats[0])
})
-1
View File
@@ -816,7 +816,6 @@ func (f *formatFMP4) initialize() bool {
parsed = true
codec.SampleRate = syncInfo.SampleRate()
codec.ChannelCount = bsi.ChannelCount()
codec.Fscod = syncInfo.Fscod
codec.Bsid = bsi.Bsid
codec.Bsmod = bsi.Bsmod
codec.Acmod = bsi.Acmod
+81 -38
View File
@@ -72,12 +72,12 @@ func TestRecorder(t *testing.T) {
},
}}
writeToStream := func(strm *stream.Stream, startDTS int64, startNTP time.Time) {
writeToStream := func(subStream *stream.SubStream, startDTS int64, startNTP time.Time) {
for i := range 2 {
pts := startDTS + int64(i)*100*90000/1000
ntp := startNTP.Add(time.Duration(i*100) * time.Millisecond)
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: pts,
NTP: ntp,
Payload: unit.PayloadH264{
@@ -87,7 +87,7 @@ func TestRecorder(t *testing.T) {
},
})
strm.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{
PTS: pts,
Payload: unit.PayloadH265{
{
@@ -110,17 +110,17 @@ func TestRecorder(t *testing.T) {
},
})
strm.WriteUnit(desc.Medias[2], desc.Medias[2].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[2], desc.Medias[2].Formats[0], &unit.Unit{
PTS: pts * int64(desc.Medias[2].Formats[0].ClockRate()) / 90000,
Payload: unit.PayloadMPEG4Audio{{1, 2, 3, 4}},
})
strm.WriteUnit(desc.Medias[3], desc.Medias[3].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[3], desc.Medias[3].Formats[0], &unit.Unit{
PTS: pts * int64(desc.Medias[3].Formats[0].ClockRate()) / 90000,
Payload: unit.PayloadG711{1, 2, 3, 4},
})
strm.WriteUnit(desc.Medias[4], desc.Medias[4].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[4], desc.Medias[4].Formats[0], &unit.Unit{
PTS: pts * int64(desc.Medias[4].Formats[0].ClockRate()) / 90000,
Payload: unit.PayloadLPCM{1, 2, 3, 4},
})
@@ -131,7 +131,6 @@ func TestRecorder(t *testing.T) {
t.Run(ca, func(t *testing.T) {
strm := &stream.Stream{
Desc: desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -140,6 +139,13 @@ func TestRecorder(t *testing.T) {
require.NoError(t, err)
defer strm.Close()
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
dir, err := os.MkdirTemp("", "mediamtx-agent")
require.NoError(t, err)
defer os.RemoveAll(dir)
@@ -204,16 +210,16 @@ func TestRecorder(t *testing.T) {
}
w.Initialize()
writeToStream(strm,
writeToStream(subStream,
50*90000,
time.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC))
writeToStream(strm,
writeToStream(subStream,
52*90000,
time.Date(2008, 5, 20, 22, 15, 27, 0, time.UTC))
// simulate a write error
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: 0,
Payload: unit.PayloadH264{
{5}, // IDR
@@ -274,9 +280,10 @@ func TestRecorder(t *testing.T) {
TimeScale: 44100,
Codec: &mcodecs.MPEG4Audio{
Config: mpeg4audio.AudioSpecificConfig{
Type: 2,
SampleRate: 44100,
ChannelCount: 2,
Type: 2,
SampleRate: 44100,
ChannelCount: 2,
ChannelConfig: 2,
},
},
},
@@ -320,7 +327,7 @@ func TestRecorder(t *testing.T) {
time.Sleep(50 * time.Millisecond)
writeToStream(strm,
writeToStream(subStream,
300*90000,
time.Date(2010, 5, 20, 22, 15, 25, 0, time.UTC))
@@ -419,7 +426,6 @@ func TestRecorderFMP4NegativeInitialDTS(t *testing.T) {
strm := &stream.Stream{
Desc: desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -428,6 +434,13 @@ func TestRecorderFMP4NegativeInitialDTS(t *testing.T) {
require.NoError(t, err)
defer strm.Close()
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
dir, err := os.MkdirTemp("", "mediamtx-agent")
require.NoError(t, err)
defer os.RemoveAll(dir)
@@ -447,7 +460,7 @@ func TestRecorderFMP4NegativeInitialDTS(t *testing.T) {
w.Initialize()
for i := range 3 {
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: -50*90000/1000 + (int64(i) * 200 * 90000 / 1000),
NTP: time.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC),
Payload: unit.PayloadH264{
@@ -457,7 +470,7 @@ func TestRecorderFMP4NegativeInitialDTS(t *testing.T) {
},
})
strm.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{
PTS: -100*44100/1000 + (int64(i) * 200 * 44100 / 1000),
Payload: unit.PayloadMPEG4Audio{{1, 2, 3, 4}},
})
@@ -508,7 +521,6 @@ func TestRecorderFMP4NegativeDTSDiff(t *testing.T) {
strm := &stream.Stream{
Desc: desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -517,6 +529,13 @@ func TestRecorderFMP4NegativeDTSDiff(t *testing.T) {
require.NoError(t, err)
defer strm.Close()
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
dir, err := os.MkdirTemp("", "mediamtx-agent")
require.NoError(t, err)
defer os.RemoveAll(dir)
@@ -535,25 +554,25 @@ func TestRecorderFMP4NegativeDTSDiff(t *testing.T) {
}
w.Initialize()
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: 44100,
NTP: time.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC),
Payload: unit.PayloadMPEG4Audio{{1, 2}},
})
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: 3 * 44100,
NTP: time.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC),
Payload: unit.PayloadMPEG4Audio{{1, 2}},
})
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: 2 * 44100,
NTP: time.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC),
Payload: unit.PayloadMPEG4Audio{{1, 2}},
})
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: 4 * 44100,
NTP: time.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC),
Payload: unit.PayloadMPEG4Audio{{1, 2}},
@@ -602,7 +621,6 @@ func TestRecorderSkipTracksPartial(t *testing.T) {
strm := &stream.Stream{
Desc: desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -611,6 +629,13 @@ func TestRecorderSkipTracksPartial(t *testing.T) {
require.NoError(t, err)
defer strm.Close()
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
dir, err := os.MkdirTemp("", "mediamtx-agent")
require.NoError(t, err)
defer os.RemoveAll(dir)
@@ -664,7 +689,6 @@ func TestRecorderSkipTracksFull(t *testing.T) {
strm := &stream.Stream{
Desc: desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -673,6 +697,13 @@ func TestRecorderSkipTracksFull(t *testing.T) {
require.NoError(t, err)
defer strm.Close()
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
dir, err := os.MkdirTemp("", "mediamtx-agent")
require.NoError(t, err)
defer os.RemoveAll(dir)
@@ -728,7 +759,6 @@ func TestRecorderFMP4SegmentSwitch(t *testing.T) {
strm := &stream.Stream{
Desc: desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -737,6 +767,13 @@ func TestRecorderFMP4SegmentSwitch(t *testing.T) {
require.NoError(t, err)
defer strm.Close()
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
dir, err := os.MkdirTemp("", "mediamtx-agent")
require.NoError(t, err)
defer os.RemoveAll(dir)
@@ -767,7 +804,7 @@ func TestRecorderFMP4SegmentSwitch(t *testing.T) {
pts := 50 * time.Second
ntp := time.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC)
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: int64(pts) * 90000 / int64(time.Second),
NTP: ntp,
Payload: unit.PayloadH264{
@@ -778,7 +815,7 @@ func TestRecorderFMP4SegmentSwitch(t *testing.T) {
pts += 700 * time.Millisecond
ntp = ntp.Add(700 * time.Millisecond)
strm.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{ // segment switch should happen here
subStream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{ // segment switch should happen here
PTS: int64(pts) * 44100 / int64(time.Second),
NTP: ntp,
Payload: unit.PayloadMPEG4Audio{{1, 2}},
@@ -787,7 +824,7 @@ func TestRecorderFMP4SegmentSwitch(t *testing.T) {
pts += 400 * time.Millisecond
ntp = ntp.Add(400 * time.Millisecond)
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: int64(pts) * 90000 / int64(time.Second),
NTP: ntp,
Payload: unit.PayloadH264{
@@ -798,7 +835,7 @@ func TestRecorderFMP4SegmentSwitch(t *testing.T) {
pts += 100 * time.Millisecond
ntp = ntp.Add(100 * time.Millisecond)
strm.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{
PTS: int64(pts) * 44100 / int64(time.Second),
NTP: ntp,
Payload: unit.PayloadMPEG4Audio{{3, 4}},
@@ -807,7 +844,7 @@ func TestRecorderFMP4SegmentSwitch(t *testing.T) {
pts += 400 * time.Millisecond
ntp = ntp.Add(400 * time.Millisecond)
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: int64(pts) * 90000 / int64(time.Second),
NTP: ntp,
Payload: unit.PayloadH264{
@@ -851,7 +888,6 @@ func TestRecorderTimeDriftDetector(t *testing.T) {
strm := &stream.Stream{
Desc: desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -860,6 +896,13 @@ func TestRecorderTimeDriftDetector(t *testing.T) {
require.NoError(t, err)
defer strm.Close()
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
dir, err := os.MkdirTemp("", "mediamtx-agent")
require.NoError(t, err)
defer os.RemoveAll(dir)
@@ -916,7 +959,7 @@ func TestRecorderTimeDriftDetector(t *testing.T) {
pts := startDTS + int64(i)*100*90000/1000
ntp := startNTP.Add(time.Duration(i*100) * time.Millisecond)
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: pts,
NTP: ntp,
Payload: unit.PayloadH264{
@@ -926,7 +969,7 @@ func TestRecorderTimeDriftDetector(t *testing.T) {
},
})
strm.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{
PTS: pts * int64(desc.Medias[1].Formats[0].ClockRate()) / 90000,
NTP: ntp,
Payload: unit.PayloadMPEG4Audio{{1, 2, 3, 4}},
@@ -945,7 +988,7 @@ func TestRecorderTimeDriftDetector(t *testing.T) {
pts := startDTS + int64(i)*100*90000/1000
ntp := startNTP.Add(time.Duration(i*100) * time.Millisecond)
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: pts,
NTP: ntp,
Payload: unit.PayloadH264{
@@ -955,7 +998,7 @@ func TestRecorderTimeDriftDetector(t *testing.T) {
},
})
strm.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{
PTS: pts * int64(desc.Medias[1].Formats[0].ClockRate()) / 90000,
NTP: ntp,
Payload: unit.PayloadMPEG4Audio{{1, 2, 3, 4}},
@@ -967,7 +1010,7 @@ func TestRecorderTimeDriftDetector(t *testing.T) {
driftedPTS := startDTS + 15*100*90000/1000
driftedNTP := startNTP.Add(15*100*time.Millisecond + 6*time.Second) // 6 second drift
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: driftedPTS,
NTP: driftedNTP,
Payload: unit.PayloadH264{
@@ -995,7 +1038,7 @@ func TestRecorderTimeDriftDetector(t *testing.T) {
pts := restartDTS + int64(i)*100*90000/1000
ntp := restartNTP.Add(time.Duration(i*100) * time.Millisecond)
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: pts,
NTP: ntp,
Payload: unit.PayloadH264{
@@ -1005,7 +1048,7 @@ func TestRecorderTimeDriftDetector(t *testing.T) {
},
})
strm.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{
PTS: pts * int64(desc.Medias[1].Formats[0].ClockRate()) / 90000,
NTP: ntp,
Payload: unit.PayloadMPEG4Audio{{1, 2, 3, 4}},
+33 -13
View File
@@ -186,7 +186,6 @@ func TestServerRead(t *testing.T) {
strm := &stream.Stream{
Desc: desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
ReplaceNTP: false,
@@ -195,6 +194,13 @@ func TestServerRead(t *testing.T) {
err := strm.Initialize()
require.NoError(t, err)
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
pm := &dummyPathManager{
findPathConfImpl: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
@@ -251,9 +257,10 @@ func TestServerRead(t *testing.T) {
{
Codec: &codecs.MPEG4Audio{
Config: mpeg4audio.AudioSpecificConfig{
Type: 2,
ChannelCount: 2,
SampleRate: 44100,
Type: 2,
ChannelCount: 2,
ChannelConfig: 2,
SampleRate: 44100,
},
},
ClockRate: 90000,
@@ -287,14 +294,14 @@ func TestServerRead(t *testing.T) {
strm.WaitForReaders()
for i := range 2 {
strm.WriteUnit(test.MediaH264, test.FormatH264, &unit.Unit{
subStream.WriteUnit(test.MediaH264, test.FormatH264, &unit.Unit{
NTP: time.Time{},
PTS: int64(i) * 90000,
Payload: unit.PayloadH264{
{5, 1}, // IDR
},
})
strm.WriteUnit(test.MediaMPEG4Audio, test.FormatMPEG4Audio, &unit.Unit{
subStream.WriteUnit(test.MediaMPEG4Audio, test.FormatMPEG4Audio, &unit.Unit{
NTP: time.Time{},
PTS: int64(i) * 44100,
Payload: unit.PayloadMPEG4Audio{{1, 2}},
@@ -328,14 +335,14 @@ func TestServerRead(t *testing.T) {
strm.WaitForReaders()
for i := range 2 {
strm.WriteUnit(test.MediaH264, test.FormatH264, &unit.Unit{
subStream.WriteUnit(test.MediaH264, test.FormatH264, &unit.Unit{
NTP: time.Time{},
PTS: int64(i) * 90000,
Payload: unit.PayloadH264{
{5, 1}, // IDR
},
})
strm.WriteUnit(test.MediaMPEG4Audio, test.FormatMPEG4Audio, &unit.Unit{
subStream.WriteUnit(test.MediaMPEG4Audio, test.FormatMPEG4Audio, &unit.Unit{
NTP: time.Time{},
PTS: int64(i) * 44100,
Payload: unit.PayloadMPEG4Audio{{1, 2}},
@@ -359,9 +366,10 @@ func TestServerRead(t *testing.T) {
{
Codec: &codecs.MPEG4Audio{
Config: mpeg4audio.AudioSpecificConfig{
Type: 2,
ChannelCount: 2,
SampleRate: 44100,
Type: 2,
ChannelCount: 2,
ChannelConfig: 2,
SampleRate: 44100,
},
},
ClockRate: 90000,
@@ -408,7 +416,6 @@ func TestServerDirectory(t *testing.T) {
strm := &stream.Stream{
Desc: desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -416,6 +423,13 @@ func TestServerDirectory(t *testing.T) {
err = strm.Initialize()
require.NoError(t, err)
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
pm := &dummyPathManager{
addReaderImpl: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
return &dummyPath{}, strm, nil
@@ -457,7 +471,6 @@ func TestServerDynamicAlwaysRemux(t *testing.T) {
strm := &stream.Stream{
Desc: desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -465,6 +478,13 @@ func TestServerDynamicAlwaysRemux(t *testing.T) {
err := strm.Initialize()
require.NoError(t, err)
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
done := make(chan struct{})
pm := &dummyPathManager{
+3 -3
View File
@@ -229,15 +229,15 @@ func (c *conn) runPublish() error {
return err
}
var strm *stream.Stream
var subStream *stream.SubStream
medias, err := rtmp.ToStream(r, &strm)
medias, err := rtmp.ToStream(r, &subStream)
if err != nil {
return err
}
var path defs.Path
path, strm, err = c.pathManager.AddPublisher(defs.PathAddPublisherReq{
path, subStream, err = c.pathManager.AddPublisher(defs.PathAddPublisherReq{
Author: c,
Desc: &description.Session{Medias: medias},
UseRTPPackets: false,
+1 -1
View File
@@ -63,7 +63,7 @@ type serverMetrics interface {
}
type serverPathManager interface {
AddPublisher(req defs.PathAddPublisherReq) (defs.Path, *stream.Stream, error)
AddPublisher(req defs.PathAddPublisherReq) (defs.Path, *stream.SubStream, error)
AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
}
+19 -7
View File
@@ -69,7 +69,7 @@ func TestServerPublish(t *testing.T) {
n := 0
pathManager := &test.PathManager{
AddPublisherImpl: func(req defs.PathAddPublisherReq) (defs.Path, *stream.Stream, error) {
AddPublisherImpl: func(req defs.PathAddPublisherReq) (defs.Path, *stream.SubStream, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "user=myuser&pass=mypass&param=value", req.AccessRequest.Query)
require.Equal(t, "myuser", req.AccessRequest.Credentials.User)
@@ -77,7 +77,6 @@ func TestServerPublish(t *testing.T) {
strm = &stream.Stream{
Desc: req.Desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -85,6 +84,13 @@ func TestServerPublish(t *testing.T) {
err := strm.Initialize()
require.NoError(t, err)
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
reader = &stream.Reader{Parent: test.NilLogger}
reader.OnData(
@@ -112,7 +118,7 @@ func TestServerPublish(t *testing.T) {
strm.AddReader(reader)
return &dummyPath{}, strm, nil
return &dummyPath{}, subStream, nil
},
}
@@ -207,7 +213,6 @@ func TestServerRead(t *testing.T) {
strm := &stream.Stream{
Desc: desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -215,6 +220,13 @@ func TestServerRead(t *testing.T) {
err := strm.Initialize()
require.NoError(t, err)
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
pathManager := &test.PathManager{
AddReaderImpl: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
@@ -269,14 +281,14 @@ func TestServerRead(t *testing.T) {
strm.WaitForReaders()
go func() {
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
NTP: time.Time{},
Payload: unit.PayloadH264{
{5, 2, 3, 4}, // IDR
},
})
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
NTP: time.Time{},
PTS: 2 * 90000,
Payload: unit.PayloadH264{
@@ -284,7 +296,7 @@ func TestServerRead(t *testing.T) {
},
})
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
NTP: time.Time{},
PTS: 3 * 90000,
Payload: unit.PayloadH264{
+1 -1
View File
@@ -78,7 +78,7 @@ type serverMetrics interface {
type serverPathManager interface {
FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error)
Describe(req defs.PathDescribeReq) defs.PathDescribeRes
AddPublisher(_ defs.PathAddPublisherReq) (defs.Path, *stream.Stream, error)
AddPublisher(_ defs.PathAddPublisherReq) (defs.Path, *stream.SubStream, error)
AddReader(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
}
+24 -6
View File
@@ -81,14 +81,13 @@ func TestServerPublish(t *testing.T) {
return &conf.Path{}, nil
},
AddPublisherImpl: func(req defs.PathAddPublisherReq) (defs.Path, *stream.Stream, error) {
AddPublisherImpl: func(req defs.PathAddPublisherReq) (defs.Path, *stream.SubStream, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "param=value", req.AccessRequest.Query)
require.True(t, req.AccessRequest.SkipAuth)
strm = &stream.Stream{
Desc: req.Desc,
UseRTPPackets: true,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -96,6 +95,13 @@ func TestServerPublish(t *testing.T) {
err := strm.Initialize()
require.NoError(t, err)
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: true,
}
err = subStream.Initialize()
require.NoError(t, err)
reader = &stream.Reader{Parent: test.NilLogger}
reader.OnData(
@@ -113,7 +119,7 @@ func TestServerPublish(t *testing.T) {
strm.AddReader(reader)
return &dummyPath{}, strm, nil
return &dummyPath{}, subStream, nil
},
}
@@ -176,7 +182,6 @@ func TestServerRead(t *testing.T) {
strm := &stream.Stream{
Desc: desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -184,6 +189,13 @@ func TestServerRead(t *testing.T) {
err := strm.Initialize()
require.NoError(t, err)
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
n := 0
pathManager := &test.PathManager{
@@ -301,7 +313,7 @@ func TestServerRead(t *testing.T) {
_, err = reader.Play(nil)
require.NoError(t, err)
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
NTP: time.Time{},
Payload: unit.PayloadH264{
{5, 2, 3, 4}, // IDR
@@ -320,7 +332,6 @@ func TestServerRedirect(t *testing.T) {
strm := &stream.Stream{
Desc: desc,
UseRTPPackets: true,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -328,6 +339,13 @@ func TestServerRedirect(t *testing.T) {
err := strm.Initialize()
require.NoError(t, err)
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: true,
}
err = subStream.Initialize()
require.NoError(t, err)
pathManager := &test.PathManager{
DescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes {
if req.AccessRequest.Name == "path1" {
+5 -3
View File
@@ -51,6 +51,7 @@ type session struct {
pathConf *conf.Path // record only
path defs.Path
stream *stream.Stream
subStream *stream.SubStream
onUnreadHook func()
packetsLost *counterdumper.Dumper
decodeErrors *errordumper.Dumper
@@ -136,6 +137,7 @@ func (s *session) onClose(err error) {
s.path = nil
s.stream = nil
s.subStream = nil
s.discardedFrames.Stop()
s.decodeErrors.Stop()
@@ -310,7 +312,7 @@ func (s *session) onPlay(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, e
// onRecord is called by rtspServer.
func (s *session) onRecord(_ *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) {
path, stream, err := s.pathManager.AddPublisher(defs.PathAddPublisherReq{
path, subStream, err := s.pathManager.AddPublisher(defs.PathAddPublisherReq{
Author: s,
Desc: s.rsession.AnnouncedDescription(),
UseRTPPackets: true,
@@ -333,11 +335,11 @@ func (s *session) onRecord(_ *gortsplib.ServerHandlerOnRecordCtx) (*base.Respons
s.rsession,
s.rsession.AnnouncedDescription().Medias,
path.SafeConf(),
&s.stream,
&s.subStream,
s)
s.path = path
s.stream = stream
s.subStream = subStream
return &base.Response{
StatusCode: base.StatusOK,
+3 -3
View File
@@ -210,15 +210,15 @@ func (c *conn) runPublishReader(sconn srt.Conn, streamID *streamID, pathConf *co
decodeErrors.Add(err)
})
var strm *stream.Stream
var subStream *stream.SubStream
medias, err := mpegts.ToStream(r, &strm, c)
medias, err := mpegts.ToStream(r, &subStream, c)
if err != nil {
return err
}
var path defs.Path
path, strm, err = c.pathManager.AddPublisher(defs.PathAddPublisherReq{
path, subStream, err = c.pathManager.AddPublisher(defs.PathAddPublisherReq{
Author: c,
Desc: &description.Session{Medias: medias},
UseRTPPackets: false,
+1 -1
View File
@@ -65,7 +65,7 @@ type serverMetrics interface {
type serverPathManager interface {
FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error)
AddPublisher(req defs.PathAddPublisherReq) (defs.Path, *stream.Stream, error)
AddPublisher(req defs.PathAddPublisherReq) (defs.Path, *stream.SubStream, error)
AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
}
+18 -6
View File
@@ -60,14 +60,13 @@ func TestServerPublish(t *testing.T) {
require.Equal(t, "mypass", req.AccessRequest.Credentials.Pass)
return &conf.Path{}, nil
},
AddPublisherImpl: func(req defs.PathAddPublisherReq) (defs.Path, *stream.Stream, error) {
AddPublisherImpl: func(req defs.PathAddPublisherReq) (defs.Path, *stream.SubStream, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "param=value", req.AccessRequest.Query)
require.True(t, req.AccessRequest.SkipAuth)
strm = &stream.Stream{
Desc: req.Desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -75,6 +74,13 @@ func TestServerPublish(t *testing.T) {
err := strm.Initialize()
require.NoError(t, err)
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
reader = &stream.Reader{Parent: test.NilLogger}
reader.OnData(
@@ -107,7 +113,7 @@ func TestServerPublish(t *testing.T) {
strm.AddReader(reader)
return &dummyPath{}, strm, nil
return &dummyPath{}, subStream, nil
},
}
@@ -182,7 +188,6 @@ func TestServerRead(t *testing.T) {
strm := &stream.Stream{
Desc: desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -190,6 +195,13 @@ func TestServerRead(t *testing.T) {
err := strm.Initialize()
require.NoError(t, err)
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
pathManager := &test.PathManager{
AddReaderImpl: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
@@ -232,7 +244,7 @@ func TestServerRead(t *testing.T) {
strm.WaitForReaders()
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
NTP: time.Time{},
Payload: unit.PayloadH264{
{5, 1}, // IDR
@@ -262,7 +274,7 @@ func TestServerRead(t *testing.T) {
return nil
})
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
NTP: time.Time{},
Payload: unit.PayloadH264{
{5, 2},
+1 -1
View File
@@ -176,7 +176,7 @@ type serverMetrics interface {
type serverPathManager interface {
FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error)
AddPublisher(req defs.PathAddPublisherReq) (defs.Path, *stream.Stream, error)
AddPublisher(req defs.PathAddPublisherReq) (defs.Path, *stream.SubStream, error)
AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
}
+18 -6
View File
@@ -211,14 +211,13 @@ func TestServerPublish(t *testing.T) {
require.Equal(t, "mypass", req.AccessRequest.Credentials.Pass)
return &conf.Path{}, nil
},
AddPublisherImpl: func(req defs.PathAddPublisherReq) (defs.Path, *stream.Stream, error) {
AddPublisherImpl: func(req defs.PathAddPublisherReq) (defs.Path, *stream.SubStream, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "param=value", req.AccessRequest.Query)
require.True(t, req.AccessRequest.SkipAuth)
strm = &stream.Stream{
Desc: req.Desc,
UseRTPPackets: true,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -226,6 +225,13 @@ func TestServerPublish(t *testing.T) {
err := strm.Initialize()
require.NoError(t, err)
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: true,
}
err = subStream.Initialize()
require.NoError(t, err)
reader = &stream.Reader{Parent: test.NilLogger}
reader.OnData(
@@ -246,7 +252,7 @@ func TestServerPublish(t *testing.T) {
strm.AddReader(reader)
return &dummyPath{}, strm, nil
return &dummyPath{}, subStream, nil
},
}
@@ -467,7 +473,6 @@ func TestServerRead(t *testing.T) {
strm := &stream.Stream{
Desc: desc,
UseRTPPackets: (ca.unit.Payload == nil),
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
@@ -475,6 +480,13 @@ func TestServerRead(t *testing.T) {
err := strm.Initialize()
require.NoError(t, err)
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: (ca.unit.Payload == nil),
}
err = subStream.Initialize()
require.NoError(t, err)
pathManager := &test.PathManager{
FindPathConfImpl: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
@@ -537,13 +549,13 @@ func TestServerRead(t *testing.T) {
if ca.unit.Payload == nil {
clone := *ca.unit.RTPPackets[0]
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: 0,
NTP: time.Time{},
RTPPackets: []*rtp.Packet{&clone},
})
} else {
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], r.Interface().(*unit.Unit))
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], r.Interface().(*unit.Unit))
}
}()
+3 -3
View File
@@ -233,15 +233,15 @@ func (s *session) runPublish() (int, error) {
return 0, err
}
var strm *stream.Stream
var subStream *stream.SubStream
medias, err := webrtc.ToStream(pc, pathConf, &strm, s)
medias, err := webrtc.ToStream(pc, pathConf, &subStream, s)
if err != nil {
return 0, err
}
var path defs.Path
path, strm, err = s.pathManager.AddPublisher(defs.PathAddPublisherReq{
path, subStream, err = s.pathManager.AddPublisher(defs.PathAddPublisherReq{
Author: s,
Desc: &description.Session{Medias: medias},
UseRTPPackets: true,
+1 -7
View File
@@ -309,13 +309,7 @@ func (s *Handler) SetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSource
req.Res = make(chan defs.PathSourceStaticSetReadyRes)
select {
case s.chInstanceSetReady <- req:
res := <-req.Res
if res.Err == nil {
s.instance.Log(logger.Info, "ready: %s", defs.MediasInfo(req.Desc.Medias))
}
return res
return <-req.Res
case <-s.ctx.Done():
return defs.PathSourceStaticSetReadyRes{Err: fmt.Errorf("terminated")}
+4 -4
View File
@@ -37,10 +37,10 @@ func (s *Source) Log(level logger.Level, format string, args ...any) {
// Run implements StaticSource.
func (s *Source) Run(params defs.StaticSourceRunParams) error {
var strm *stream.Stream
var subStream *stream.SubStream
defer func() {
if strm != nil {
if subStream != nil {
s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})
}
}()
@@ -91,7 +91,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
decodeErrors.Add(err)
},
OnTracks: func(tracks []*gohlslib.Track) error {
medias, err2 := hls.ToStream(c, tracks, params.Conf, &strm)
medias, err2 := hls.ToStream(c, tracks, params.Conf, &subStream)
if err2 != nil {
return err2
}
@@ -105,7 +105,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
return res.Err
}
strm = res.Stream
subStream = res.SubStream
return nil
},
+3 -3
View File
@@ -113,9 +113,9 @@ func (s *Source) runReader(nc net.Conn) error {
decodeErrors.Add(err)
})
var strm *stream.Stream
var subStream *stream.SubStream
medias, err := mpegts.ToStream(mr, &strm, s)
medias, err := mpegts.ToStream(mr, &subStream, s)
if err != nil {
return err
}
@@ -131,7 +131,7 @@ func (s *Source) runReader(nc net.Conn) error {
defer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})
strm = res.Stream
subStream = res.SubStream
for {
nc.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))
+14 -14
View File
@@ -153,10 +153,10 @@ func (s *Source) runPrimary(params defs.StaticSourceRunParams) error {
medias = append(medias, mediaSecondary)
}
var strm *stream.Stream
var subStream *stream.SubStream
initializeStream := func() {
if strm == nil {
if subStream == nil {
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
Desc: &description.Session{Medias: medias},
UseRTPPackets: true,
@@ -166,7 +166,7 @@ func (s *Source) runPrimary(params defs.StaticSourceRunParams) error {
panic("should not happen")
}
strm = res.Stream
subStream = res.SubStream
}
}
@@ -190,7 +190,7 @@ func (s *Source) runPrimary(params defs.StaticSourceRunParams) error {
for _, pkt := range pkts {
pkt.Timestamp = uint32(pts)
strm.WriteUnit(medi, medi.Formats[0], &unit.Unit{
subStream.WriteUnit(medi, medi.Formats[0], &unit.Unit{
PTS: pts,
NTP: ntp,
RTPPackets: []*rtp.Packet{pkt},
@@ -221,7 +221,7 @@ func (s *Source) runPrimary(params defs.StaticSourceRunParams) error {
for _, pkt := range pkts {
pkt.Timestamp = uint32(pts)
pkt.PayloadType = 96
strm.WriteUnit(mediaSecondary, mediaSecondary.Formats[0], &unit.Unit{
subStream.WriteUnit(mediaSecondary, mediaSecondary.Formats[0], &unit.Unit{
PTS: pts,
NTP: ntp,
RTPPackets: []*rtp.Packet{pkt},
@@ -231,7 +231,7 @@ func (s *Source) runPrimary(params defs.StaticSourceRunParams) error {
}
defer func() {
if strm != nil {
if subStream != nil {
s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})
}
}()
@@ -271,7 +271,7 @@ func (s *Source) runSecondary(params defs.StaticSourceRunParams) error {
r.ctx, r.ctxCancel = context.WithCancel(context.Background())
defer r.ctxCancel()
path, origStream, err := s.waitForPrimary(r, params)
path, primaryStream, err := s.waitForPrimary(r, params)
if err != nil {
return err
}
@@ -294,8 +294,8 @@ func (s *Source) runSecondary(params defs.StaticSourceRunParams) error {
rdr := &stream.Reader{Parent: s}
rdr.OnData(
origStream.Desc.Medias[1],
origStream.Desc.Medias[1].Formats[0],
primaryStream.Desc.Medias[1],
primaryStream.Desc.Medias[1].Formats[0],
func(u *unit.Unit) error {
pkt := u.RTPPackets[0]
@@ -305,7 +305,7 @@ func (s *Source) runSecondary(params defs.StaticSourceRunParams) error {
}
newPkt.PayloadType = 26
res.Stream.WriteUnit(media, media.Formats[0], &unit.Unit{
res.SubStream.WriteUnit(media, media.Formats[0], &unit.Unit{
PTS: u.PTS,
NTP: u.NTP,
RTPPackets: []*rtp.Packet{newPkt},
@@ -313,8 +313,8 @@ func (s *Source) runSecondary(params defs.StaticSourceRunParams) error {
return nil
})
origStream.AddReader(rdr)
defer origStream.RemoveReader(rdr)
primaryStream.AddReader(rdr)
defer primaryStream.RemoveReader(rdr)
select {
case err = <-rdr.Error():
@@ -333,7 +333,7 @@ func (s *Source) waitForPrimary(
params defs.StaticSourceRunParams,
) (defs.Path, *stream.Stream, error) {
for {
path, origStream, err := s.Parent.AddReader(defs.PathAddReaderReq{
path, primaryStream, err := s.Parent.AddReader(defs.PathAddReaderReq{
Author: r,
AccessRequest: defs.PathAccessRequest{
Name: params.Conf.RPICameraPrimaryName,
@@ -354,7 +354,7 @@ func (s *Source) waitForPrimary(
return nil, nil, err
}
return path, origStream, nil
return path, primaryStream, nil
}
}
+3 -3
View File
@@ -101,9 +101,9 @@ func (s *Source) runReader(conn *gortmplib.Client) error {
return err
}
var strm *stream.Stream
var subStream *stream.SubStream
medias, err := rtmp.ToStream(r, &strm)
medias, err := rtmp.ToStream(r, &subStream)
if err != nil {
return err
}
@@ -123,7 +123,7 @@ func (s *Source) runReader(conn *gortmplib.Client) error {
defer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})
strm = res.Stream
subStream = res.SubStream
conn.NetConn().SetWriteDeadline(time.Time{})
+5 -5
View File
@@ -132,7 +132,7 @@ func (s *Source) runReader(desc *description.Session, nc net.Conn) error {
decodeErrors.Start()
defer decodeErrors.Stop()
var strm *stream.Stream
var subStream *stream.SubStream
timeDecoder := &rtptime.GlobalDecoder{}
timeDecoder.Initialize()
@@ -167,14 +167,14 @@ func (s *Source) runReader(desc *description.Session, nc net.Conn) error {
var pkt rtp.Packet
err = pkt.Unmarshal(buf[:n])
if err != nil {
if strm != nil {
if subStream != nil {
decodeErrors.Add(err)
continue
}
return err
}
if strm == nil {
if subStream == nil {
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
Desc: desc,
UseRTPPackets: true,
@@ -186,7 +186,7 @@ func (s *Source) runReader(desc *description.Session, nc net.Conn) error {
defer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})
strm = res.Stream
subStream = res.SubStream
}
media, ok := mediasByPayloadType[pkt.PayloadType]
@@ -208,7 +208,7 @@ func (s *Source) runReader(desc *description.Session, nc net.Conn) error {
continue
}
strm.WriteUnit(media.desc, forma.desc, &unit.Unit{
subStream.WriteUnit(media.desc, forma.desc, &unit.Unit{
PTS: pts,
RTPPackets: []*rtp.Packet{pkt},
})
+3 -3
View File
@@ -234,13 +234,13 @@ func (s *Source) runInner(c *gortsplib.Client, u *base.URL, pathConf *conf.Path)
Medias: medias,
}
var strm *stream.Stream
var subStream *stream.SubStream
rtsp.ToStream(
c,
desc2.Medias,
pathConf,
&strm,
&subStream,
s)
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
@@ -254,7 +254,7 @@ func (s *Source) runInner(c *gortsplib.Client, u *base.URL, pathConf *conf.Path)
defer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})
strm = res.Stream
subStream = res.SubStream
rangeHeader, err := createRangeHeader(pathConf)
if err != nil {
+3 -3
View File
@@ -98,9 +98,9 @@ func (s *Source) runReader(sconn srt.Conn) error {
decodeErrors.Add(err)
})
var strm *stream.Stream
var subStream *stream.SubStream
medias, err := mpegts.ToStream(r, &strm, s)
medias, err := mpegts.ToStream(r, &subStream, s)
if err != nil {
return err
}
@@ -116,7 +116,7 @@ func (s *Source) runReader(sconn srt.Conn) error {
defer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})
strm = res.Stream
subStream = res.SubStream
for {
sconn.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))
+3 -3
View File
@@ -67,9 +67,9 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
return err
}
var strm *stream.Stream
var subStream *stream.SubStream
medias, err := webrtc.ToStream(client.PeerConnection(), params.Conf, &strm, s)
medias, err := webrtc.ToStream(client.PeerConnection(), params.Conf, &subStream, s)
if err != nil {
client.Close() //nolint:errcheck
return err
@@ -87,7 +87,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
defer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})
strm = rres.Stream
subStream = rres.SubStream
client.StartReading()
Binary file not shown.
Binary file not shown.
Binary file not shown.
+68
View File
@@ -0,0 +1,68 @@
package stream
import (
"context"
"sync"
"time"
)
func multiplyAndDivide2(v, m, d time.Duration) time.Duration {
secs := v / d
dec := v % d
return (secs*m + dec*m/d)
}
type offlineSubStream struct {
stream *Stream
subStream *SubStream
ctx context.Context
ctxCancel func()
wg sync.WaitGroup
tracks []*offlineSubStreamTrack
}
func (o *offlineSubStream) initialize() error {
o.subStream = &SubStream{
Stream: o.stream,
CurDesc: o.stream.Desc,
UseRTPPackets: false,
}
err := o.subStream.Initialize()
if err != nil {
return err
}
o.ctx, o.ctxCancel = context.WithCancel(context.Background())
pos := 0
o.tracks = make([]*offlineSubStreamTrack, len(o.subStream.CurDesc.Medias))
for _, media := range o.subStream.CurDesc.Medias {
for _, forma := range media.Formats {
t := &offlineSubStreamTrack{
wg: &o.wg,
file: o.stream.AlwaysAvailableFile,
pos: pos,
ctx: o.ctx,
subStream: o.subStream,
media: media,
format: forma,
}
t.initialize()
o.tracks[pos] = t
pos++
}
}
return nil
}
func (o *offlineSubStream) close(waitLastSample bool) {
for _, track := range o.tracks {
track.waitLastSample = waitLastSample
}
o.ctxCancel()
o.wg.Wait()
}
+327
View File
@@ -0,0 +1,327 @@
package stream
import (
"bytes"
"context"
_ "embed"
"io"
"os"
"sync"
"time"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/av1"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
mcodecs "github.com/bluenviron/mediacommon/v2/pkg/formats/mp4/codecs"
"github.com/bluenviron/mediacommon/v2/pkg/formats/pmp4"
"github.com/bluenviron/mediamtx/internal/unit"
)
//go:embed offline_av1.mp4
var offlineAV1 []byte
//go:embed offline_vp9.mp4
var offlineVP9 []byte
//go:embed offline_h265.mp4
var offlineH265 []byte
//go:embed offline_h264.mp4
var offlineH264 []byte
type offlineSubStreamTrack struct {
wg *sync.WaitGroup
file string
pos int
ctx context.Context
subStream *SubStream
media *description.Media
format format.Format
waitLastSample bool
}
func (t *offlineSubStreamTrack) initialize() {
t.wg.Add(1)
go t.run()
}
func (t *offlineSubStreamTrack) run() {
defer t.wg.Done()
var pts int64
systemTime := time.Now()
if t.file != "" {
f, err := os.Open(t.file)
if err != nil {
panic(err)
}
defer f.Close()
err = t.runFile(pts, systemTime, f, t.pos)
if err != nil {
panic(err)
}
return
}
const audioWritesPerSecond = 10
switch forma := t.format.(type) {
case *format.Opus:
unitsPerWrite := (forma.ClockRate() / 960) / audioWritesPerSecond
writeDuration := 960 * int64(unitsPerWrite)
writeDurationGo := multiplyAndDivide2(time.Duration(writeDuration), time.Second, 48000)
for {
payload := make(unit.PayloadOpus, unitsPerWrite)
for i := range payload {
payload[i] = []byte{0xF8, 0xFF, 0xFE} // DTX frame
}
t.subStream.WriteUnit(t.media, t.format, &unit.Unit{
PTS: pts,
NTP: time.Time{},
Payload: payload,
})
pts += writeDuration
systemTime = systemTime.Add(writeDurationGo)
if !t.sleep(systemTime) {
return
}
}
case *format.MPEG4Audio:
unitsPerWrite := (forma.ClockRate() / mpeg4audio.SamplesPerAccessUnit) / audioWritesPerSecond
writeDuration := mpeg4audio.SamplesPerAccessUnit * int64(unitsPerWrite)
writeDurationGo := multiplyAndDivide2(time.Duration(writeDuration), time.Second, time.Duration(forma.ClockRate()))
for {
var frame []byte
switch forma.Config.ChannelConfig {
case 1:
frame = []byte{0x01, 0x18, 0x20, 0x07}
default:
frame = []byte{0x21, 0x10, 0x04, 0x60, 0x8c, 0x1c}
}
payload := make(unit.PayloadMPEG4Audio, unitsPerWrite)
for i := range payload {
payload[i] = frame
}
t.subStream.WriteUnit(t.media, t.format, &unit.Unit{
PTS: pts,
NTP: time.Time{},
Payload: payload,
})
pts += writeDuration
systemTime = systemTime.Add(writeDurationGo)
if !t.sleep(systemTime) {
return
}
}
case *format.G711:
samplesPerWrite := forma.ClockRate() / audioWritesPerSecond
writeDuration := samplesPerWrite
writeDurationGo := multiplyAndDivide2(time.Duration(writeDuration), time.Second, time.Duration(forma.ClockRate()))
for {
var sample byte
if forma.MULaw {
sample = 0xFF
} else {
sample = 0xD5
}
payload := make(unit.PayloadG711, samplesPerWrite*forma.ChannelCount)
for i := range payload {
payload[i] = sample
}
t.subStream.WriteUnit(t.media, t.format, &unit.Unit{
PTS: pts,
NTP: time.Time{},
Payload: payload,
})
pts += int64(writeDuration)
systemTime = systemTime.Add(writeDurationGo)
if !t.sleep(systemTime) {
return
}
}
case *format.LPCM:
samplesPerWrite := forma.ClockRate() / audioWritesPerSecond
writeDuration := samplesPerWrite
writeDurationGo := multiplyAndDivide2(time.Duration(writeDuration), time.Second, time.Duration(forma.ClockRate()))
for {
payload := make(unit.PayloadLPCM, samplesPerWrite*forma.ChannelCount*(forma.BitDepth/8))
t.subStream.WriteUnit(t.media, t.format, &unit.Unit{
PTS: pts,
NTP: time.Time{},
Payload: payload,
})
pts += int64(writeDuration)
systemTime = systemTime.Add(writeDurationGo)
if !t.sleep(systemTime) {
return
}
}
default:
var buf []byte
switch t.format.(type) {
case *format.AV1:
buf = offlineAV1
case *format.VP9:
buf = offlineVP9
case *format.H265:
buf = offlineH265
case *format.H264:
buf = offlineH264
default:
panic("should not happen")
}
r := bytes.NewReader(buf)
err := t.runFile(pts, systemTime, r, 0)
if err != nil {
panic(err)
}
}
}
func (t *offlineSubStreamTrack) runFile(pts int64, systemTime time.Time, r io.ReadSeeker, pos int) error {
var presentation pmp4.Presentation
err := presentation.Unmarshal(r)
if err != nil {
return err
}
track := presentation.Tracks[pos]
for {
// in case of the embedded video, codec parameters are not in the description
// and must be sent manually
if t.file == "" {
switch codec := track.Codec.(type) {
case *mcodecs.H265:
if codec.SPS != nil && codec.PPS != nil && codec.VPS != nil {
t.subStream.WriteUnit(t.media, t.format, &unit.Unit{
PTS: pts,
NTP: time.Time{},
Payload: unit.PayloadH265([][]byte{codec.SPS, codec.PPS, codec.VPS}),
})
}
case *mcodecs.H264:
if codec.SPS != nil && codec.PPS != nil {
t.subStream.WriteUnit(t.media, t.format, &unit.Unit{
PTS: pts,
NTP: time.Time{},
Payload: unit.PayloadH264([][]byte{codec.SPS, codec.PPS}),
})
}
}
}
for _, sample := range track.Samples {
var payload []byte
payload, err = sample.GetPayload()
if err != nil {
return err
}
switch track.Codec.(type) {
case *mcodecs.AV1:
var bs av1.Bitstream
err = bs.Unmarshal(payload)
if err != nil {
return err
}
t.subStream.WriteUnit(t.media, t.format, &unit.Unit{
PTS: pts,
NTP: time.Time{},
Payload: unit.PayloadAV1(bs),
})
case *mcodecs.VP9:
t.subStream.WriteUnit(t.media, t.format, &unit.Unit{
PTS: pts,
NTP: time.Time{},
Payload: unit.PayloadVP9(payload),
})
case *mcodecs.H265:
var avcc h264.AVCC
err = avcc.Unmarshal(payload)
if err != nil {
return err
}
t.subStream.WriteUnit(t.media, t.format, &unit.Unit{
PTS: pts,
NTP: time.Time{},
Payload: unit.PayloadH265(avcc),
})
case *mcodecs.H264:
var avcc h264.AVCC
err = avcc.Unmarshal(payload)
if err != nil {
return err
}
t.subStream.WriteUnit(t.media, t.format, &unit.Unit{
PTS: pts,
NTP: time.Time{},
Payload: unit.PayloadH264(avcc),
})
}
pts += multiplyAndDivide(int64(sample.Duration)+int64(sample.PTSOffset),
int64(t.format.ClockRate()), int64(track.TimeScale))
durationGo := multiplyAndDivide2(time.Duration(int64(sample.Duration)+int64(sample.PTSOffset)),
time.Second, time.Duration(track.TimeScale))
systemTime = systemTime.Add(durationGo)
if !t.sleep(systemTime) {
return nil
}
}
}
}
func (t *offlineSubStreamTrack) sleep(systemTime time.Time) bool {
select {
case <-time.After(time.Until(systemTime)):
case <-t.ctx.Done():
if t.waitLastSample {
time.Sleep(time.Until(systemTime))
}
return false
}
return true
}
Binary file not shown.
+11 -1
View File
@@ -1,6 +1,8 @@
package stream
import (
"fmt"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/gortsplib/v5/pkg/format/rtpac3"
"github.com/bluenviron/gortsplib/v5/pkg/format/rtpav1"
@@ -25,6 +27,14 @@ func ptrOf[T any](v T) *T {
return &v
}
type rtpEncoderNotAvailableError struct {
format format.Format
}
func (e rtpEncoderNotAvailableError) Error() string {
return fmt.Sprintf("RTP encoder not available for format %T", e.format)
}
type rtpEncoder interface {
encode(unit.Payload) ([]*rtp.Packet, error)
}
@@ -377,6 +387,6 @@ func newRTPEncoder(
return (*rtpEncoderKLV)(wrapped), nil
default:
return nil, nil
return nil, rtpEncoderNotAvailableError{forma}
}
}
+269 -22
View File
@@ -2,6 +2,7 @@
package stream
import (
"os"
"sync"
"sync/atomic"
"time"
@@ -9,27 +10,224 @@ import (
"github.com/bluenviron/gortsplib/v5"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediacommon/v2/pkg/formats/mp4/codecs"
"github.com/bluenviron/mediacommon/v2/pkg/formats/pmp4"
"github.com/pion/rtp"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/errordumper"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/unit"
)
func mediasFromAlwaysAvailableFile(alwaysAvailableFile string) ([]*description.Media, error) {
f, err := os.Open(alwaysAvailableFile)
if err != nil {
return nil, err
}
defer f.Close()
var presentation pmp4.Presentation
err = presentation.Unmarshal(f)
if err != nil {
return nil, err
}
var medias []*description.Media
for _, track := range presentation.Tracks {
switch codec := track.Codec.(type) {
case *codecs.AV1:
medias = append(medias, &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.AV1{
PayloadTyp: 96,
}},
})
case *codecs.VP9:
medias = append(medias, &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.VP9{
PayloadTyp: 96,
}},
})
case *codecs.H265:
medias = append(medias, &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.H265{
PayloadTyp: 96,
}},
})
case *codecs.H264:
medias = append(medias, &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.H264{
PayloadTyp: 96,
PacketizationMode: 1,
}},
})
case *codecs.Opus:
medias = append(medias, &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.Opus{
PayloadTyp: 96,
ChannelCount: 2,
}},
})
case *codecs.MPEG4Audio:
medias = append(medias, &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.MPEG4Audio{
PayloadTyp: 96,
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
Config: &mpeg4audio.AudioSpecificConfig{
Type: mpeg4audio.ObjectTypeAACLC,
SampleRate: codec.Config.SampleRate,
ChannelConfig: codec.Config.ChannelConfig,
},
}},
})
case *codecs.LPCM:
medias = append(medias, &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.LPCM{
PayloadTyp: 96,
BitDepth: codec.BitDepth,
SampleRate: codec.SampleRate,
ChannelCount: codec.ChannelCount,
}},
})
}
}
return medias, nil
}
func mediasFromAlwaysAvailableTracks(alwaysAvailableTracks []conf.AlwaysAvailableTrack) []*description.Media {
var medias []*description.Media
for _, track := range alwaysAvailableTracks {
switch track.Codec {
case conf.CodecAV1:
medias = append(medias, &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.AV1{
PayloadTyp: 96,
}},
})
case conf.CodecVP9:
medias = append(medias, &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.VP9{
PayloadTyp: 96,
}},
})
case conf.CodecH265:
medias = append(medias, &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.H265{
PayloadTyp: 96,
}},
})
case conf.CodecH264:
medias = append(medias, &description.Media{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.H264{
PayloadTyp: 96,
PacketizationMode: 1,
}},
})
case conf.CodecOpus:
medias = append(medias, &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.Opus{
PayloadTyp: 96,
ChannelCount: 2,
}},
})
case conf.CodecMPEG4Audio:
medias = append(medias, &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.MPEG4Audio{
PayloadTyp: 96,
SizeLength: 13,
IndexLength: 3,
IndexDeltaLength: 3,
Config: &mpeg4audio.AudioSpecificConfig{
Type: mpeg4audio.ObjectTypeAACLC,
SampleRate: track.SampleRate,
ChannelConfig: uint8(track.ChannelCount),
},
}},
})
case conf.CodecG711:
medias = append(medias, &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.G711{
PayloadTyp: func() uint8 {
switch {
case track.ChannelCount == 1 && track.MULaw:
return 0
case track.ChannelCount == 1 && !track.MULaw:
return 8
default:
return 96
}
}(),
MULaw: track.MULaw,
SampleRate: track.SampleRate,
ChannelCount: track.ChannelCount,
}},
})
case conf.CodecLPCM:
medias = append(medias, &description.Media{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.LPCM{
PayloadTyp: 96,
BitDepth: 16,
SampleRate: track.SampleRate,
ChannelCount: track.ChannelCount,
}},
})
}
}
return medias
}
// Stream is a media stream.
// It stores tracks, readers and allows to write data to readers, remuxing it when needed.
type Stream struct {
Desc *description.Session
UseRTPPackets bool
WriteQueueSize int
RTPMaxPayloadSize int
ReplaceNTP bool
Parent logger.Writer
Desc *description.Session
AlwaysAvailable bool
AlwaysAvailableFile string
AlwaysAvailableTracks []conf.AlwaysAvailableTrack
WriteQueueSize int
RTPMaxPayloadSize int
ReplaceNTP bool
Parent logger.Writer
mutex sync.RWMutex
subStream *SubStream
offlineSubStream *offlineSubStream
bytesReceived *uint64
bytesSent *uint64
medias map[*description.Media]*streamMedia
mutex sync.RWMutex
rtspStream *gortsplib.ServerStream
rtspsStream *gortsplib.ServerStream
readers map[*Reader]struct{}
@@ -40,6 +238,31 @@ type Stream struct {
// Initialize initializes a Stream.
func (s *Stream) Initialize() error {
if s.AlwaysAvailable {
if s.Desc != nil {
panic("should not happen")
}
if !s.ReplaceNTP {
panic("should not happen")
}
var medias []*description.Media
if s.AlwaysAvailableFile != "" {
var err error
medias, err = mediasFromAlwaysAvailableFile(s.AlwaysAvailableFile)
if err != nil {
return err
}
} else {
medias = mediasFromAlwaysAvailableTracks(s.AlwaysAvailableTracks)
}
s.Desc = &description.Session{
Medias: medias,
}
}
s.bytesReceived = new(uint64)
s.bytesSent = new(uint64)
s.medias = make(map[*description.Media]*streamMedia)
@@ -58,9 +281,9 @@ func (s *Stream) Initialize() error {
s.processingErrors.Start()
for _, media := range s.Desc.Medias {
s.medias[media] = &streamMedia{
sm := &streamMedia{
media: media,
useRTPPackets: s.UseRTPPackets,
alwaysAvailable: s.AlwaysAvailable,
rtpMaxPayloadSize: s.RTPMaxPayloadSize,
replaceNTP: s.ReplaceNTP,
onBytesReceived: s.onBytesReceived,
@@ -69,7 +292,15 @@ func (s *Stream) Initialize() error {
processingErrors: s.processingErrors,
parent: s.Parent,
}
err := s.medias[media].initialize()
err := sm.initialize()
if err != nil {
return err
}
s.medias[media] = sm
}
if s.AlwaysAvailable {
err := s.StartOfflineSubStream()
if err != nil {
return err
}
@@ -80,6 +311,10 @@ func (s *Stream) Initialize() error {
// Close closes all resources of the stream.
func (s *Stream) Close() {
if s.offlineSubStream != nil {
s.offlineSubStream.close(false)
}
s.processingErrors.Stop()
if s.rtspStream != nil {
@@ -90,6 +325,29 @@ func (s *Stream) Close() {
}
}
// StartOfflineSubStream starts the offline substream.
func (s *Stream) StartOfflineSubStream() error {
if !s.AlwaysAvailable {
panic("should not happen")
}
oss := &offlineSubStream{
stream: s,
}
err := oss.initialize()
if err != nil {
return err
}
if s.offlineSubStream != nil {
s.Parent.Log(logger.Info, "stream is offline")
}
s.offlineSubStream = oss
return nil
}
// BytesReceived returns received bytes.
func (s *Stream) BytesReceived() uint64 {
return atomic.LoadUint64(s.bytesReceived)
@@ -202,17 +460,6 @@ func (s *Stream) WaitForReaders() {
<-s.hasReaders
}
// WriteUnit writes a Unit.
func (s *Stream) WriteUnit(medi *description.Media, forma format.Format, u *unit.Unit) {
sm := s.medias[medi]
sf := sm.formats[forma]
s.mutex.RLock()
defer s.mutex.RUnlock()
sf.writeUnit(u)
}
func (s *Stream) onBytesReceived(v uint64) {
atomic.AddUint64(s.bytesReceived, v)
}
+19 -134
View File
@@ -2,7 +2,6 @@ package stream
import (
"crypto/rand"
"fmt"
"time"
"github.com/bluenviron/gortsplib/v5/pkg/description"
@@ -15,6 +14,12 @@ import (
"github.com/bluenviron/mediamtx/internal/unit"
)
func multiplyAndDivide(v, m, d int64) int64 {
secs := v / d
dec := v % d
return (secs*m + dec*m/d)
}
func unitSize(u *unit.Unit) uint64 {
n := uint64(0)
for _, pkt := range u.RTPPackets {
@@ -35,7 +40,7 @@ func randUint32() (uint32, error) {
type streamFormat struct {
format format.Format
media *description.Media
useRTPPackets bool
alwaysAvailable bool
rtpMaxPayloadSize int
replaceNTP bool
processingErrors *errordumper.Dumper
@@ -44,151 +49,31 @@ type streamFormat struct {
writeRTSP func(*description.Media, []*rtp.Packet, time.Time)
parent logger.Writer
rtpDecoder rtpDecoder
formatUpdater formatUpdater
unitRemuxer unitRemuxer
rtpEncoder rtpEncoder
ptsOffset uint32
ntpEstimator *ntpestimator.Estimator
onDatas map[*Reader]OnDataFunc
firstReceived bool
lastPTS int64
lastSystemTime time.Time
ptsOffset int64
formatUpdater formatUpdater
unitRemuxer unitRemuxer
rtpEncoder rtpEncoder
rtpTimeOffset uint32
ntpEstimator *ntpestimator.Estimator
onDatas map[*Reader]OnDataFunc
}
func (sf *streamFormat) initialize() error {
sf.onDatas = make(map[*Reader]OnDataFunc)
if sf.useRTPPackets {
var err error
sf.rtpDecoder, err = newRTPDecoder(sf.format)
if err != nil {
return err
}
}
sf.lastSystemTime = time.Now()
sf.formatUpdater = newFormatUpdater(sf.format)
sf.unitRemuxer = newUnitRemuxer(sf.format)
if !sf.useRTPPackets {
var err error
sf.rtpEncoder, err = newRTPEncoder(sf.format, sf.rtpMaxPayloadSize, nil, nil)
if err != nil {
return err
}
if sf.rtpEncoder == nil {
return fmt.Errorf("RTP encoder not available for format %T", sf.format)
}
sf.ptsOffset, err = randUint32()
if err != nil {
return err
}
}
if sf.replaceNTP {
sf.ntpEstimator = &ntpestimator.Estimator{
ClockRate: sf.format.ClockRate(),
}
}
return nil
}
func (sf *streamFormat) writeUnit(u *unit.Unit) {
err := sf.writeUnitInner(u)
if err != nil {
sf.processingErrors.Add(err)
return
}
}
func (sf *streamFormat) writeUnitInner(u *unit.Unit) error {
if sf.useRTPPackets {
if len(u.RTPPackets) != 1 {
panic("should not happen")
}
if !u.NilPayload() {
panic("should not happen")
}
if sf.rtpDecoder != nil {
var err error
u.Payload, err = sf.rtpDecoder.decode(u.RTPPackets[0])
if err != nil {
return err
}
}
if sf.rtpEncoder == nil {
for _, pkt := range u.RTPPackets {
if len(pkt.Payload) > sf.rtpMaxPayloadSize {
var err error
sf.rtpEncoder, err = newRTPEncoder(sf.format, sf.rtpMaxPayloadSize, ptrOf(pkt.SSRC), ptrOf(pkt.SequenceNumber))
if err != nil {
return err
}
if sf.rtpEncoder == nil {
return fmt.Errorf("RTP payload size (%d) is greater than maximum allowed (%d)",
len(pkt.Payload), sf.rtpMaxPayloadSize)
}
sf.ptsOffset = pkt.Timestamp - uint32(u.PTS)
sf.parent.Log(logger.Info, "RTP packets are too big, remuxing them into smaller ones")
break
}
}
}
if sf.rtpEncoder != nil {
u.RTPPackets = nil
}
} else {
if len(u.RTPPackets) != 0 {
panic("should not happen")
}
if u.NilPayload() {
panic("should not happen")
}
}
if !u.NilPayload() {
sf.formatUpdater(sf.format, u.Payload)
u.Payload = sf.unitRemuxer(sf.format, u.Payload)
if sf.rtpEncoder != nil && !u.NilPayload() {
var err error
u.RTPPackets, err = sf.rtpEncoder.encode(u.Payload)
if err != nil {
return err
}
for _, pkt := range u.RTPPackets {
pkt.Timestamp += sf.ptsOffset + uint32(u.PTS)
}
}
}
if sf.replaceNTP {
u.NTP = sf.ntpEstimator.Estimate(u.PTS)
}
size := unitSize(u)
sf.onBytesReceived(size)
sf.writeRTSP(sf.media, u.RTPPackets, u.NTP)
for sr, onData := range sf.onDatas {
csr := sr
cOnData := onData
sr.push(func() error {
if !csr.SkipBytesSent {
sf.onBytesSent(size)
}
return cOnData(u)
})
}
sf.onDatas = make(map[*Reader]OnDataFunc)
return nil
}
+2 -2
View File
@@ -12,7 +12,7 @@ import (
type streamMedia struct {
media *description.Media
useRTPPackets bool
alwaysAvailable bool
rtpMaxPayloadSize int
replaceNTP bool
onBytesReceived func(uint64)
@@ -31,7 +31,7 @@ func (sm *streamMedia) initialize() error {
sf := &streamFormat{
format: forma,
media: sm.media,
useRTPPackets: sm.useRTPPackets,
alwaysAvailable: sm.alwaysAvailable,
rtpMaxPayloadSize: sm.rtpMaxPayloadSize,
replaceNTP: sm.replaceNTP,
processingErrors: sm.processingErrors,
+127 -12
View File
@@ -1,10 +1,12 @@
package stream
import (
"bytes"
"testing"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/unit"
"github.com/pion/rtp"
@@ -30,7 +32,6 @@ func TestStream(t *testing.T) {
strm := &Stream{
Desc: desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
}
@@ -38,6 +39,13 @@ func TestStream(t *testing.T) {
require.NoError(t, err)
defer strm.Close()
subStream := &SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
r := &Reader{}
recv := make(chan struct{})
@@ -50,7 +58,7 @@ func TestStream(t *testing.T) {
strm.AddReader(r)
defer strm.RemoveReader(r)
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: 30000 * 2,
Payload: unit.PayloadH264{
{5, 2}, // IDR
@@ -84,6 +92,13 @@ func TestStreamSkipBytesSent(t *testing.T) {
require.NoError(t, err)
defer strm.Close()
subStream := &SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
r := &Reader{
SkipBytesSent: true,
}
@@ -98,7 +113,7 @@ func TestStreamSkipBytesSent(t *testing.T) {
strm.AddReader(r)
defer strm.RemoveReader(r)
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: 30000 * 2,
Payload: unit.PayloadH264{
{5, 2}, // IDR
@@ -128,7 +143,6 @@ func TestStreamResizeOversizedRTPPackets(t *testing.T) {
strm := &Stream{
Desc: desc,
UseRTPPackets: true,
WriteQueueSize: 512,
RTPMaxPayloadSize: 400,
Parent: &nilLogger{},
@@ -137,6 +151,13 @@ func TestStreamResizeOversizedRTPPackets(t *testing.T) {
require.NoError(t, err)
defer strm.Close()
subStream := &SubStream{
Stream: strm,
UseRTPPackets: true,
}
err = subStream.Initialize()
require.NoError(t, err)
r := &Reader{}
recv := make(chan *unit.Unit)
@@ -157,7 +178,7 @@ func TestStreamResizeOversizedRTPPackets(t *testing.T) {
strm.AddReader(r)
defer strm.RemoveReader(r)
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: 90000,
RTPPackets: []*rtp.Packet{
{
@@ -179,7 +200,7 @@ func TestStreamResizeOversizedRTPPackets(t *testing.T) {
oversizedPayload[i] = byte(i % 256)
}
strm.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: 90000,
RTPPackets: []*rtp.Packet{
{
@@ -310,7 +331,6 @@ func TestStreamUpdateFormatParams(t *testing.T) {
strm := &Stream{
Desc: desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
}
@@ -318,6 +338,13 @@ func TestStreamUpdateFormatParams(t *testing.T) {
require.NoError(t, err)
defer strm.Close()
subStream := &SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
r := &Reader{}
recv := make(chan struct{})
@@ -329,7 +356,8 @@ func TestStreamUpdateFormatParams(t *testing.T) {
strm.AddReader(r)
defer strm.RemoveReader(r)
strm.WriteUnit(media, forma, u)
subStream.WriteUnit(media, forma, u)
<-recv
// Verify that format parameters were updated
@@ -1198,7 +1226,6 @@ func TestStreamDecode(t *testing.T) {
strm := &Stream{
Desc: desc,
UseRTPPackets: true,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: &nilLogger{},
@@ -1207,6 +1234,13 @@ func TestStreamDecode(t *testing.T) {
require.NoError(t, err)
defer strm.Close()
subStream := &SubStream{
Stream: strm,
UseRTPPackets: true,
}
err = subStream.Initialize()
require.NoError(t, err)
r := &Reader{}
recv := make(chan *unit.Unit)
@@ -1222,7 +1256,7 @@ func TestStreamDecode(t *testing.T) {
defer strm.RemoveReader(r)
for _, pkt := range ca.encoded {
strm.WriteUnit(desc.Medias[0], ca.format, &unit.Unit{
subStream.WriteUnit(desc.Medias[0], ca.format, &unit.Unit{
RTPPackets: []*rtp.Packet{pkt},
})
}
@@ -1242,7 +1276,6 @@ func TestStreamEncode(t *testing.T) {
strm := &Stream{
Desc: desc,
UseRTPPackets: false,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: &nilLogger{},
@@ -1251,6 +1284,13 @@ func TestStreamEncode(t *testing.T) {
require.NoError(t, err)
defer strm.Close()
subStream := &SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
r := &Reader{}
recv := make(chan struct{})
@@ -1268,7 +1308,7 @@ func TestStreamEncode(t *testing.T) {
strm.AddReader(r)
defer strm.RemoveReader(r)
strm.WriteUnit(desc.Medias[0], ca.format, &unit.Unit{
subStream.WriteUnit(desc.Medias[0], ca.format, &unit.Unit{
Payload: ca.decoded,
})
@@ -1276,3 +1316,78 @@ func TestStreamEncode(t *testing.T) {
})
}
}
func TestStreamAlwaysAvailable(t *testing.T) {
strm := &Stream{
AlwaysAvailable: true,
AlwaysAvailableTracks: []conf.AlwaysAvailableTrack{
{Codec: conf.CodecH264},
{Codec: conf.CodecOpus, SampleRate: 48000, ChannelCount: 2},
},
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
ReplaceNTP: true,
Parent: &nilLogger{},
}
err := strm.Initialize()
require.NoError(t, err)
defer strm.Close()
r := &Reader{
Parent: &nilLogger{},
}
recv1 := make(chan struct{})
recv2 := make(chan struct{})
var lastPTS int64
r.OnData(strm.Desc.Medias[0], strm.Desc.Medias[0].Formats[0], func(u *unit.Unit) error {
require.GreaterOrEqual(t, u.PTS, lastPTS)
lastPTS = u.PTS
select {
case <-recv1:
default:
close(recv1)
}
if len(u.Payload.(unit.PayloadH264)) == 3 && bytes.Equal(u.Payload.(unit.PayloadH264)[2], []byte{5, 2}) {
close(recv2)
}
return nil
})
strm.AddReader(r)
defer strm.RemoveReader(r)
<-recv1
desc := &description.Session{Medias: []*description.Media{
{
Type: description.MediaTypeVideo,
Formats: []format.Format{&format.H264{}},
},
{
Type: description.MediaTypeAudio,
Formats: []format.Format{&format.Opus{}},
},
}}
subStream := &SubStream{
Stream: strm,
CurDesc: desc,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: 90000,
Payload: unit.PayloadH264{
{5, 2}, // IDR
},
})
<-recv2
}
+157
View File
@@ -0,0 +1,157 @@
package stream
import (
"fmt"
"reflect"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/unit"
)
// FormatsToCodecs returns the name of codecs of given formats.
func FormatsToCodecs(formats []format.Format) []string {
ret := make([]string, len(formats))
for i, forma := range formats {
ret[i] = forma.Codec()
}
return ret
}
func gatherFormats(medias []*description.Media) []format.Format {
n := 0
for _, media := range medias {
n += len(media.Formats)
}
if n == 0 {
return nil
}
formats := make([]format.Format, n)
n = 0
for _, media := range medias {
n += copy(formats[n:], media.Formats)
}
return formats
}
func mediasToCodecs(medias []*description.Media) []string {
return FormatsToCodecs(gatherFormats(medias))
}
func mediasAreCompatible(medias1 []*description.Media, medias2 []*description.Media) bool {
if len(medias1) != len(medias2) {
return false
}
for i := range medias1 {
if len(medias1[i].Formats) != len(medias2[i].Formats) {
return false
}
for j := range medias1[i].Formats {
if reflect.TypeOf(medias1[i].Formats[j]) != reflect.TypeOf(medias2[i].Formats[j]) {
return false
}
}
}
return true
}
// SubStream is a Stream without interruptions.
type SubStream struct {
Stream *Stream
CurDesc *description.Session
UseRTPPackets bool
medias map[*description.Media]*subStreamMedia
}
// Initialize initializes the SubStream.
func (ss *SubStream) Initialize() error {
if !ss.Stream.AlwaysAvailable {
if ss.Stream.subStream != nil {
panic("should not happen")
}
if ss.CurDesc != nil {
panic("should not happen")
}
} else {
if ss.CurDesc == nil {
panic("should not happen")
}
if !mediasAreCompatible(ss.Stream.Desc.Medias, ss.CurDesc.Medias) {
return fmt.Errorf("want to publish %v, but stream expects %v",
mediasToCodecs(ss.CurDesc.Medias), mediasToCodecs(ss.Stream.Desc.Medias))
}
}
if !ss.Stream.AlwaysAvailable {
ss.CurDesc = ss.Stream.Desc
}
ss.medias = make(map[*description.Media]*subStreamMedia)
for i, curMedia := range ss.CurDesc.Medias {
media := ss.Stream.Desc.Medias[i]
ssm := &subStreamMedia{
curMedia: curMedia,
streamMedia: ss.Stream.medias[media],
useRTPPackets: ss.UseRTPPackets,
}
err := ssm.initialize()
if err != nil {
return err
}
ss.medias[curMedia] = ssm
}
if ss.Stream.AlwaysAvailable {
if ss.Stream.offlineSubStream != nil {
ss.Stream.Parent.Log(logger.Info, "stream is online")
// wait for the entire duration of the last sample of the offline sub stream
// to minimize errors in clients.
// TODO: it would be better in the future to wait for the last sample
// of normal sub streams as well (this is currently impossible because
// we don't know the duration of their samples).
ss.Stream.offlineSubStream.close(true)
ss.Stream.offlineSubStream = nil
}
}
ss.Stream.mutex.Lock()
ss.Stream.subStream = ss
ss.Stream.mutex.Unlock()
for _, ssm := range ss.medias {
for _, ssf := range ssm.formats {
ssf.initialize2()
}
}
return nil
}
// WriteUnit writes a Unit.
func (ss *SubStream) WriteUnit(medi *description.Media, forma format.Format, u *unit.Unit) {
ss.Stream.mutex.RLock()
defer ss.Stream.mutex.RUnlock()
if ss.Stream.subStream != ss {
return
}
ssm := ss.medias[medi]
ssf := ssm.formats[forma]
ssf.writeUnit(u)
}
+188
View File
@@ -0,0 +1,188 @@
package stream
import (
"errors"
"fmt"
"time"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/unit"
)
type subStreamFormat struct {
curFormat format.Format
streamFormat *streamFormat
useRTPPackets bool
rtpDecoder rtpDecoder
tempRTPEncoder rtpEncoder
tempRTPTimeOffset uint32
}
func (ssf *subStreamFormat) initialize() error {
if ssf.useRTPPackets {
var err error
ssf.rtpDecoder, err = newRTPDecoder(ssf.curFormat)
if err != nil {
return err
}
}
if ssf.streamFormat.rtpEncoder == nil && (!ssf.useRTPPackets || ssf.streamFormat.alwaysAvailable) {
var err error
ssf.tempRTPEncoder, err = newRTPEncoder(ssf.curFormat, ssf.streamFormat.rtpMaxPayloadSize, nil, nil)
if err != nil {
return err
}
ssf.tempRTPTimeOffset, err = randUint32()
if err != nil {
return err
}
}
return nil
}
func (ssf *subStreamFormat) initialize2() {
if ssf.tempRTPEncoder != nil {
if ssf.streamFormat.rtpEncoder == nil {
ssf.streamFormat.rtpEncoder = ssf.tempRTPEncoder
ssf.streamFormat.rtpTimeOffset = ssf.tempRTPTimeOffset
}
ssf.tempRTPEncoder = nil
ssf.tempRTPTimeOffset = 0
}
if ssf.streamFormat.alwaysAvailable {
if ssf.streamFormat.firstReceived {
deltaT := max(1, multiplyAndDivide(
int64(time.Since(ssf.streamFormat.lastSystemTime)), int64(ssf.streamFormat.format.ClockRate()), int64(time.Second)))
ssf.streamFormat.ptsOffset = ssf.streamFormat.lastPTS + deltaT
}
switch curFormat := ssf.curFormat.(type) {
case *format.H265:
sps, pps, vps := curFormat.SafeParams()
if sps != nil && pps != nil && vps != nil {
ssf.writeUnit(&unit.Unit{
PTS: 0,
NTP: time.Time{},
RTPPackets: nil,
Payload: unit.PayloadH265([][]byte{sps, pps, vps}),
})
}
case *format.H264:
sps, pps := curFormat.SafeParams()
if sps != nil && pps != nil {
ssf.writeUnit(&unit.Unit{
PTS: 0,
NTP: time.Time{},
RTPPackets: nil,
Payload: unit.PayloadH264([][]byte{sps, pps}),
})
}
}
}
}
func (ssf *subStreamFormat) writeUnit(u *unit.Unit) {
err := ssf.writeUnitInner(u)
if err != nil {
ssf.streamFormat.processingErrors.Add(err)
return
}
}
func (ssf *subStreamFormat) writeUnitInner(u *unit.Unit) error {
if ssf.streamFormat.alwaysAvailable {
ssf.streamFormat.firstReceived = true
u.PTS += ssf.streamFormat.ptsOffset
if u.PTS > ssf.streamFormat.lastPTS {
ssf.streamFormat.lastPTS = u.PTS
}
ssf.streamFormat.lastSystemTime = time.Now()
}
if ssf.streamFormat.replaceNTP {
u.NTP = ssf.streamFormat.ntpEstimator.Estimate(u.PTS)
}
if len(u.RTPPackets) != 0 {
if ssf.rtpDecoder != nil {
var err error
u.Payload, err = ssf.rtpDecoder.decode(u.RTPPackets[0])
if err != nil {
return err
}
}
if ssf.streamFormat.rtpEncoder == nil {
for _, pkt := range u.RTPPackets {
if len(pkt.Payload) > ssf.streamFormat.rtpMaxPayloadSize {
var err error
ssf.streamFormat.rtpEncoder, err = newRTPEncoder(ssf.streamFormat.format, ssf.streamFormat.rtpMaxPayloadSize,
ptrOf(pkt.SSRC), ptrOf(pkt.SequenceNumber))
if err != nil {
var err2 rtpEncoderNotAvailableError
if errors.As(err, &err2) {
return fmt.Errorf("RTP payload size (%d) is greater than maximum allowed (%d)",
len(pkt.Payload), ssf.streamFormat.rtpMaxPayloadSize)
}
return err
}
ssf.streamFormat.rtpTimeOffset = pkt.Timestamp - uint32(u.PTS)
ssf.streamFormat.parent.Log(logger.Info, "RTP packets are too big, remuxing them into smaller ones")
break
}
}
}
if ssf.streamFormat.rtpEncoder != nil {
u.RTPPackets = nil
}
}
if !u.NilPayload() {
ssf.streamFormat.formatUpdater(ssf.streamFormat.format, u.Payload)
u.Payload = ssf.streamFormat.unitRemuxer(ssf.streamFormat.format, u.Payload)
if ssf.streamFormat.rtpEncoder != nil && !u.NilPayload() {
var err error
u.RTPPackets, err = ssf.streamFormat.rtpEncoder.encode(u.Payload)
if err != nil {
return err
}
for _, pkt := range u.RTPPackets {
pkt.Timestamp += ssf.streamFormat.rtpTimeOffset + uint32(u.PTS)
}
}
}
size := unitSize(u)
ssf.streamFormat.onBytesReceived(size)
ssf.streamFormat.writeRTSP(ssf.streamFormat.media, u.RTPPackets, u.NTP)
for sr, onData := range ssf.streamFormat.onDatas {
csr := sr
cOnData := onData
sr.push(func() error {
if !csr.SkipBytesSent {
ssf.streamFormat.onBytesSent(size)
}
return cOnData(u)
})
}
return nil
}
+35
View File
@@ -0,0 +1,35 @@
package stream
import (
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
)
type subStreamMedia struct {
curMedia *description.Media
streamMedia *streamMedia
useRTPPackets bool
formats map[format.Format]*subStreamFormat
}
func (ssm *subStreamMedia) initialize() error {
ssm.formats = make(map[format.Format]*subStreamFormat)
for i, curFormat := range ssm.curMedia.Formats {
forma := ssm.streamMedia.media.Formats[i]
ssf := &subStreamFormat{
curFormat: curFormat,
streamFormat: ssm.streamMedia.formats[forma],
useRTPPackets: ssm.useRTPPackets,
}
err := ssf.initialize()
if err != nil {
return err
}
ssm.formats[curFormat] = ssf
}
return nil
}
+4 -3
View File
@@ -44,9 +44,10 @@ var FormatH265 = &format.H265{
var FormatMPEG4Audio = &format.MPEG4Audio{
PayloadTyp: 96,
Config: &mpeg4audio.AudioSpecificConfig{
Type: 2,
SampleRate: 44100,
ChannelCount: 2,
Type: 2,
SampleRate: 44100,
ChannelCount: 2,
ChannelConfig: 2,
},
SizeLength: 13,
IndexLength: 3,
+2 -2
View File
@@ -10,7 +10,7 @@ import (
type PathManager struct {
FindPathConfImpl func(req defs.PathFindPathConfReq) (*conf.Path, error)
DescribeImpl func(req defs.PathDescribeReq) defs.PathDescribeRes
AddPublisherImpl func(req defs.PathAddPublisherReq) (defs.Path, *stream.Stream, error)
AddPublisherImpl func(req defs.PathAddPublisherReq) (defs.Path, *stream.SubStream, error)
AddReaderImpl func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
}
@@ -25,7 +25,7 @@ func (pm *PathManager) Describe(req defs.PathDescribeReq) defs.PathDescribeRes {
}
// AddPublisher implements PathManager.
func (pm *PathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, *stream.Stream, error) {
func (pm *PathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, *stream.SubStream, error) {
return pm.AddPublisherImpl(req)
}
+10 -2
View File
@@ -31,7 +31,6 @@ func (p *StaticSourceParent) Close() {
func (p *StaticSourceParent) SetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes {
p.stream = &stream.Stream{
Desc: req.Desc,
UseRTPPackets: req.UseRTPPackets,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
ReplaceNTP: req.ReplaceNTP,
@@ -42,6 +41,15 @@ func (p *StaticSourceParent) SetReady(req defs.PathSourceStaticSetReadyReq) defs
panic(err)
}
subStream := &stream.SubStream{
Stream: p.stream,
UseRTPPackets: req.UseRTPPackets,
}
err = subStream.Initialize()
if err != nil {
panic("should not happen")
}
p.reader = &stream.Reader{Parent: NilLogger}
p.reader.OnData(
@@ -55,7 +63,7 @@ func (p *StaticSourceParent) SetReady(req defs.PathSourceStaticSetReadyReq) defs
p.stream.AddReader(p.reader)
return defs.PathSourceStaticSetReadyRes{Stream: p.stream}
return defs.PathSourceStaticSetReadyRes{SubStream: subStream}
}
// SetNotReady implements parent.
+26 -21
View File
@@ -72,21 +72,22 @@ func fillProperty(t *testing.T, rt reflect.Type, existing openAPIProperty) openA
rt == reflect.TypeOf(conf.Duration(0)) ||
rt == reflect.TypeOf(conf.IPNetwork{}) ||
rt == reflect.TypeOf(conf.Credential("")) ||
rt == reflect.TypeOf(conf.RecordFormat(0)) ||
rt == reflect.TypeOf(conf.RecordFormat("")) ||
rt == reflect.TypeOf(conf.AuthAction("")) ||
rt == reflect.TypeOf(conf.Encryption(0)) ||
rt == reflect.TypeOf(conf.Encryption("")) ||
rt == reflect.TypeOf(conf.RTSPTransport{}) ||
rt == reflect.TypeOf(conf.StringSize(0)) ||
rt == reflect.TypeOf(conf.RTSPRangeType(0)) ||
rt == reflect.TypeOf(conf.RTSPRangeType("")) ||
rt == reflect.TypeOf(conf.LogLevel(0)) ||
rt == reflect.TypeOf(conf.AuthMethod(0)) ||
rt == reflect.TypeOf(conf.AuthMethod("")) ||
rt == reflect.TypeOf(conf.LogDestination(0)) ||
rt == reflect.TypeOf(conf.RTSPAuthMethod(0)) ||
rt == reflect.TypeOf(conf.HLSVariant(0)) ||
rt == reflect.TypeOf(defs.APIRTMPConnState("")) ||
rt == reflect.TypeOf(defs.APIWebRTCSessionState("")) ||
rt == reflect.TypeOf(defs.APISRTConnState("")) ||
rt == reflect.TypeOf(defs.APIRTSPSessionState("")):
rt == reflect.TypeOf(defs.APIRTSPSessionState("")) ||
rt == reflect.TypeOf(conf.Codec("")):
return openAPIProperty{Type: "string"}
case rt == reflect.TypeOf(conf.RTSPTransports{}):
@@ -138,8 +139,8 @@ func TestAPIDocs(t *testing.T) {
goStruct any
}{
{
"Info",
defs.APIInfo{},
"AlwaysAvailableTrack",
conf.AlwaysAvailableTrack{},
},
{
"AuthInternalUser",
@@ -153,6 +154,22 @@ func TestAPIDocs(t *testing.T) {
"GlobalConf",
conf.Conf{},
},
{
"HLSMuxer",
defs.APIHLSMuxer{},
},
{
"HLSMuxerList",
defs.APIHLSMuxerList{},
},
{
"Info",
defs.APIInfo{},
},
{
"Path",
defs.APIPath{},
},
{
"PathConf",
conf.Path{},
@@ -161,29 +178,17 @@ func TestAPIDocs(t *testing.T) {
"PathConfList",
defs.APIPathConfList{},
},
{
"Path",
defs.APIPath{},
},
{
"PathList",
defs.APIPathList{},
},
{
"PathSource",
defs.APIPathSource{},
},
{
"PathReader",
defs.APIPathReader{},
},
{
"HLSMuxer",
defs.APIHLSMuxer{},
},
{
"HLSMuxerList",
defs.APIHLSMuxerList{},
"PathSource",
defs.APIPathSource{},
},
{
"Recording",
+17
View File
@@ -486,6 +486,23 @@ pathDefaults:
# Use absolute timestamp of frames, instead of replacing them with the current time.
useAbsoluteTimestamp: false
###############################################
# Default path settings -> Always available
# Enable always-available mode, in which a file is played on repeat when the stream is not available.
alwaysAvailable: false
# Path to the MP4 file that is played on repeat. If not provided, a default file will be used.
alwaysAvailableFile: ''
# If alwaysAvailableFile is not provided, these are the tracks of the default file.
alwaysAvailableTracks:
# Available values are: AV1, VP9, H265, H264, Opus, MPEG4Audio, G711, LPCM
- codec: H264
# in case of MPEG4Audio, G711, LPCM, sampleRate and ChannelCount must be provided too.
# sampleRate: 48000
# channelCount: 2
# in case of G711, muLaw must be provided too.
# muLaw: false
###############################################
# Default path settings -> Record