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:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -59,6 +59,8 @@ COPY --from=mediamtx.yml /
|
||||
|
||||
RUN apt update && apt install -y \
|
||||
(insert here additional utilities)
|
||||
|
||||
ENTRYPOINT [ "/mediamtx" ]
|
||||
```
|
||||
|
||||
And then build it:
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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'")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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().
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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},
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}},
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
@@ -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{{
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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])
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}},
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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¶m=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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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" {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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{})
|
||||
|
||||
|
||||
@@ -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},
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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.
@@ -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()
|
||||
}
|
||||
@@ -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.
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user