diff --git a/README.md b/README.md index 85010774..7fc8b761 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/api/openapi.yaml b/api/openapi.yaml index 2a7fddaa..855cffd4 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -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: diff --git a/docs/1-kickoff/1-introduction.md b/docs/1-kickoff/1-introduction.md index 623ff2c7..10082f19 100644 --- a/docs/1-kickoff/1-introduction.md +++ b/docs/1-kickoff/1-introduction.md @@ -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 diff --git a/docs/1-kickoff/2-install.md b/docs/1-kickoff/2-install.md index 97bc0dcf..37f2889c 100644 --- a/docs/1-kickoff/2-install.md +++ b/docs/1-kickoff/2-install.md @@ -59,6 +59,8 @@ COPY --from=mediamtx.yml / RUN apt update && apt install -y \ (insert here additional utilities) + +ENTRYPOINT [ "/mediamtx" ] ``` And then build it: diff --git a/docs/2-usage/07-always-available.md b/docs/2-usage/07-always-available.md new file mode 100644 index 00000000..fecaac9c --- /dev/null +++ b/docs/2-usage/07-always-available.md @@ -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" +``` diff --git a/docs/2-usage/07-record.md b/docs/2-usage/08-record.md similarity index 100% rename from docs/2-usage/07-record.md rename to docs/2-usage/08-record.md diff --git a/docs/2-usage/08-playback.md b/docs/2-usage/09-playback.md similarity index 100% rename from docs/2-usage/08-playback.md rename to docs/2-usage/09-playback.md diff --git a/docs/2-usage/09-forward.md b/docs/2-usage/10-forward.md similarity index 100% rename from docs/2-usage/09-forward.md rename to docs/2-usage/10-forward.md diff --git a/docs/2-usage/10-proxy.md b/docs/2-usage/11-proxy.md similarity index 100% rename from docs/2-usage/10-proxy.md rename to docs/2-usage/11-proxy.md diff --git a/docs/2-usage/11-extract-snapshots.md b/docs/2-usage/12-extract-snapshots.md similarity index 100% rename from docs/2-usage/11-extract-snapshots.md rename to docs/2-usage/12-extract-snapshots.md diff --git a/docs/2-usage/12-on-demand-publishing.md b/docs/2-usage/13-on-demand-publishing.md similarity index 100% rename from docs/2-usage/12-on-demand-publishing.md rename to docs/2-usage/13-on-demand-publishing.md diff --git a/docs/2-usage/13-route-absolute-timestamps.md b/docs/2-usage/14-route-absolute-timestamps.md similarity index 100% rename from docs/2-usage/13-route-absolute-timestamps.md rename to docs/2-usage/14-route-absolute-timestamps.md diff --git a/docs/2-usage/14-expose-the-server-in-a-subfolder.md b/docs/2-usage/15-expose-the-server-in-a-subfolder.md similarity index 100% rename from docs/2-usage/14-expose-the-server-in-a-subfolder.md rename to docs/2-usage/15-expose-the-server-in-a-subfolder.md diff --git a/docs/2-usage/15-embed-streams-in-a-website.md b/docs/2-usage/16-embed-streams-in-a-website.md similarity index 100% rename from docs/2-usage/15-embed-streams-in-a-website.md rename to docs/2-usage/16-embed-streams-in-a-website.md diff --git a/docs/2-usage/16-start-on-boot.md b/docs/2-usage/17-start-on-boot.md similarity index 100% rename from docs/2-usage/16-start-on-boot.md rename to docs/2-usage/17-start-on-boot.md diff --git a/docs/2-usage/17-logging.md b/docs/2-usage/18-logging.md similarity index 100% rename from docs/2-usage/17-logging.md rename to docs/2-usage/18-logging.md diff --git a/docs/2-usage/18-hooks.md b/docs/2-usage/19-hooks.md similarity index 100% rename from docs/2-usage/18-hooks.md rename to docs/2-usage/19-hooks.md diff --git a/docs/2-usage/19-control-api.md b/docs/2-usage/20-control-api.md similarity index 100% rename from docs/2-usage/19-control-api.md rename to docs/2-usage/20-control-api.md diff --git a/docs/2-usage/20-metrics.md b/docs/2-usage/21-metrics.md similarity index 100% rename from docs/2-usage/20-metrics.md rename to docs/2-usage/21-metrics.md diff --git a/docs/2-usage/21-performance.md b/docs/2-usage/22-performance.md similarity index 100% rename from docs/2-usage/21-performance.md rename to docs/2-usage/22-performance.md diff --git a/docs/2-usage/22-srt-specific-features.md b/docs/2-usage/23-srt-specific-features.md similarity index 100% rename from docs/2-usage/22-srt-specific-features.md rename to docs/2-usage/23-srt-specific-features.md diff --git a/docs/2-usage/23-webrtc-specific-features.md b/docs/2-usage/24-webrtc-specific-features.md similarity index 100% rename from docs/2-usage/23-webrtc-specific-features.md rename to docs/2-usage/24-webrtc-specific-features.md diff --git a/docs/2-usage/24-rtsp-specific-features.md b/docs/2-usage/25-rtsp-specific-features.md similarity index 100% rename from docs/2-usage/24-rtsp-specific-features.md rename to docs/2-usage/25-rtsp-specific-features.md diff --git a/docs/2-usage/25-rtmp-specific-features.md b/docs/2-usage/26-rtmp-specific-features.md similarity index 100% rename from docs/2-usage/25-rtmp-specific-features.md rename to docs/2-usage/26-rtmp-specific-features.md diff --git a/docs/2-usage/26-decrease-packet-loss.md b/docs/2-usage/27-decrease-packet-loss.md similarity index 100% rename from docs/2-usage/26-decrease-packet-loss.md rename to docs/2-usage/27-decrease-packet-loss.md diff --git a/go.mod b/go.mod index 439667f7..4cb6d807 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 745b7a67..231539a4 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/conf/always_available_track.go b/internal/conf/always_available_track.go new file mode 100644 index 00000000..29efc64c --- /dev/null +++ b/internal/conf/always_available_track.go @@ -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 +} diff --git a/internal/conf/auth_method.go b/internal/conf/auth_method.go index f22506ef..ade90ff7 100644 --- a/internal/conf/auth_method.go +++ b/internal/conf/auth_method.go @@ -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 diff --git a/internal/conf/conf.go b/internal/conf/conf.go index 8dbe1007..9e554f25 100644 --- a/internal/conf/conf.go +++ b/internal/conf/conf.go @@ -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" diff --git a/internal/conf/conf_test.go b/internal/conf/conf_test.go index 67160ae8..ff79dd00 100644 --- a/internal/conf/conf_test.go +++ b/internal/conf/conf_test.go @@ -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), diff --git a/internal/conf/encryption.go b/internal/conf/encryption.go index 2f915b5f..bdd26753 100644 --- a/internal/conf/encryption.go +++ b/internal/conf/encryption.go @@ -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 diff --git a/internal/conf/path.go b/internal/conf/path.go index 8644b0de..fe31a47e 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -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'") } diff --git a/internal/conf/record_format.go b/internal/conf/record_format.go index 91425b6d..a5a3cdc7 100644 --- a/internal/conf/record_format.go +++ b/internal/conf/record_format.go @@ -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 diff --git a/internal/conf/rtsp_range_type.go b/internal/conf/rtsp_range_type.go index 01e97eab..cd5c9aaa 100644 --- a/internal/conf/rtsp_range_type.go +++ b/internal/conf/rtsp_range_type.go @@ -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 diff --git a/internal/core/path.go b/internal/core/path.go index 066187fa..b482f51f 100644 --- a/internal/core/path.go +++ b/internal/core/path.go @@ -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") } diff --git a/internal/core/path_manager.go b/internal/core/path_manager.go index dce1eb98..c3cc1d98 100644 --- a/internal/core/path_manager.go +++ b/internal/core/path_manager.go @@ -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: diff --git a/internal/defs/path.go b/internal/defs/path.go index b774af5c..51202128 100644 --- a/internal/defs/path.go +++ b/internal/defs/path.go @@ -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(). diff --git a/internal/hooks/on_ready.go b/internal/hooks/on_ready.go index f125f0a9..67327232 100644 --- a/internal/hooks/on_ready.go +++ b/internal/hooks/on_ready.go @@ -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 != "" { diff --git a/internal/playback/on_get_test.go b/internal/playback/on_get_test.go index 540d1e6d..656db5b7 100644 --- a/internal/playback/on_get_test.go +++ b/internal/playback/on_get_test.go @@ -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, diff --git a/internal/playback/on_list_test.go b/internal/playback/on_list_test.go index 6a92f0ca..ddf532f0 100644 --- a/internal/playback/on_list_test.go +++ b/internal/playback/on_list_test.go @@ -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, diff --git a/internal/playback/segment_fmp4.go b/internal/playback/segment_fmp4.go index afa7c30e..4cadb349 100644 --- a/internal/playback/segment_fmp4.go +++ b/internal/playback/segment_fmp4.go @@ -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)) diff --git a/internal/protocols/hls/to_stream.go b/internal/protocols/hls/to_stream.go index aa12027d..d84e71e5 100644 --- a/internal/protocols/hls/to_stream.go +++ b/internal/protocols/hls/to_stream.go @@ -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), diff --git a/internal/protocols/hls/to_stream_test.go b/internal/protocols/hls/to_stream_test.go index 5492a7c4..2f502fb0 100644 --- a/internal/protocols/hls/to_stream_test.go +++ b/internal/protocols/hls/to_stream_test.go @@ -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( diff --git a/internal/protocols/mpegts/to_stream.go b/internal/protocols/mpegts/to_stream.go index 8ebd3857..adee2167 100644 --- a/internal/protocols/mpegts/to_stream.go +++ b/internal/protocols/mpegts/to_stream.go @@ -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}, }) diff --git a/internal/protocols/mpegts/to_stream_test.go b/internal/protocols/mpegts/to_stream_test.go index d8be208d..efbb169e 100644 --- a/internal/protocols/mpegts/to_stream_test.go +++ b/internal/protocols/mpegts/to_stream_test.go @@ -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, }}, diff --git a/internal/protocols/rtmp/from_stream_test.go b/internal/protocols/rtmp/from_stream_test.go index b70b99f6..435e9ca2 100644 --- a/internal/protocols/rtmp/from_stream_test.go +++ b/internal/protocols/rtmp/from_stream_test.go @@ -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 }) diff --git a/internal/protocols/rtmp/to_stream.go b/internal/protocols/rtmp/to_stream.go index 53a36017..b2538203 100644 --- a/internal/protocols/rtmp/to_stream.go +++ b/internal/protocols/rtmp/to_stream.go @@ -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), }) diff --git a/internal/protocols/rtsp/to_stream.go b/internal/protocols/rtsp/to_stream.go index 97f5da81..b45577be 100644 --- a/internal/protocols/rtsp/to_stream.go +++ b/internal/protocols/rtsp/to_stream.go @@ -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}, diff --git a/internal/protocols/webrtc/from_stream_test.go b/internal/protocols/webrtc/from_stream_test.go index b03a3d2a..52fe9493 100644 --- a/internal/protocols/webrtc/from_stream_test.go +++ b/internal/protocols/webrtc/from_stream_test.go @@ -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{{ diff --git a/internal/protocols/webrtc/to_stream.go b/internal/protocols/webrtc/to_stream.go index 920f4a2f..850a62d3 100644 --- a/internal/protocols/webrtc/to_stream.go +++ b/internal/protocols/webrtc/to_stream.go @@ -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}, diff --git a/internal/protocols/webrtc/to_stream_test.go b/internal/protocols/webrtc/to_stream_test.go index b259e54c..abbd8945 100644 --- a/internal/protocols/webrtc/to_stream_test.go +++ b/internal/protocols/webrtc/to_stream_test.go @@ -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]) }) diff --git a/internal/recorder/format_fmp4.go b/internal/recorder/format_fmp4.go index 5670ef1b..5aad8f84 100644 --- a/internal/recorder/format_fmp4.go +++ b/internal/recorder/format_fmp4.go @@ -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 diff --git a/internal/recorder/recorder_test.go b/internal/recorder/recorder_test.go index 8ef2b1ae..e9af32da 100644 --- a/internal/recorder/recorder_test.go +++ b/internal/recorder/recorder_test.go @@ -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}}, diff --git a/internal/servers/hls/server_test.go b/internal/servers/hls/server_test.go index f824c7b3..ec246395 100644 --- a/internal/servers/hls/server_test.go +++ b/internal/servers/hls/server_test.go @@ -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{ diff --git a/internal/servers/rtmp/conn.go b/internal/servers/rtmp/conn.go index 6c675289..18f94d65 100644 --- a/internal/servers/rtmp/conn.go +++ b/internal/servers/rtmp/conn.go @@ -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, diff --git a/internal/servers/rtmp/server.go b/internal/servers/rtmp/server.go index 6a16acb7..c024497a 100644 --- a/internal/servers/rtmp/server.go +++ b/internal/servers/rtmp/server.go @@ -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) } diff --git a/internal/servers/rtmp/server_test.go b/internal/servers/rtmp/server_test.go index defa53c5..b249ea63 100644 --- a/internal/servers/rtmp/server_test.go +++ b/internal/servers/rtmp/server_test.go @@ -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{ diff --git a/internal/servers/rtsp/server.go b/internal/servers/rtsp/server.go index 4c583cd9..4fb916a1 100644 --- a/internal/servers/rtsp/server.go +++ b/internal/servers/rtsp/server.go @@ -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) } diff --git a/internal/servers/rtsp/server_test.go b/internal/servers/rtsp/server_test.go index 17d863e1..e6a71a65 100644 --- a/internal/servers/rtsp/server_test.go +++ b/internal/servers/rtsp/server_test.go @@ -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" { diff --git a/internal/servers/rtsp/session.go b/internal/servers/rtsp/session.go index b634ed77..d02aee87 100644 --- a/internal/servers/rtsp/session.go +++ b/internal/servers/rtsp/session.go @@ -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, diff --git a/internal/servers/srt/conn.go b/internal/servers/srt/conn.go index d0696e1b..b4c74b64 100644 --- a/internal/servers/srt/conn.go +++ b/internal/servers/srt/conn.go @@ -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, diff --git a/internal/servers/srt/server.go b/internal/servers/srt/server.go index 2001941f..a68228fb 100644 --- a/internal/servers/srt/server.go +++ b/internal/servers/srt/server.go @@ -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) } diff --git a/internal/servers/srt/server_test.go b/internal/servers/srt/server_test.go index e3d6b2b1..1bae58cf 100644 --- a/internal/servers/srt/server_test.go +++ b/internal/servers/srt/server_test.go @@ -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}, diff --git a/internal/servers/webrtc/server.go b/internal/servers/webrtc/server.go index cdfa68aa..61b5a3ad 100644 --- a/internal/servers/webrtc/server.go +++ b/internal/servers/webrtc/server.go @@ -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) } diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index 2759b01c..8bd30c79 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -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)) } }() diff --git a/internal/servers/webrtc/session.go b/internal/servers/webrtc/session.go index 26016a04..4387ca06 100644 --- a/internal/servers/webrtc/session.go +++ b/internal/servers/webrtc/session.go @@ -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, diff --git a/internal/staticsources/handler.go b/internal/staticsources/handler.go index 75dca01d..1d436575 100644 --- a/internal/staticsources/handler.go +++ b/internal/staticsources/handler.go @@ -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")} diff --git a/internal/staticsources/hls/source.go b/internal/staticsources/hls/source.go index eafa7ac2..6b4d642d 100644 --- a/internal/staticsources/hls/source.go +++ b/internal/staticsources/hls/source.go @@ -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 }, diff --git a/internal/staticsources/mpegts/source.go b/internal/staticsources/mpegts/source.go index d9812fc6..f5d3b09e 100644 --- a/internal/staticsources/mpegts/source.go +++ b/internal/staticsources/mpegts/source.go @@ -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))) diff --git a/internal/staticsources/rpicamera/source.go b/internal/staticsources/rpicamera/source.go index 71bd1d06..c288eb60 100644 --- a/internal/staticsources/rpicamera/source.go +++ b/internal/staticsources/rpicamera/source.go @@ -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 } } diff --git a/internal/staticsources/rtmp/source.go b/internal/staticsources/rtmp/source.go index 630bd05d..ad5c7ae7 100644 --- a/internal/staticsources/rtmp/source.go +++ b/internal/staticsources/rtmp/source.go @@ -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{}) diff --git a/internal/staticsources/rtp/source.go b/internal/staticsources/rtp/source.go index 547b998c..ebfb532b 100644 --- a/internal/staticsources/rtp/source.go +++ b/internal/staticsources/rtp/source.go @@ -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}, }) diff --git a/internal/staticsources/rtsp/source.go b/internal/staticsources/rtsp/source.go index 2186d972..c5f75036 100644 --- a/internal/staticsources/rtsp/source.go +++ b/internal/staticsources/rtsp/source.go @@ -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 { diff --git a/internal/staticsources/srt/source.go b/internal/staticsources/srt/source.go index 08f19dd6..8dcd67c3 100644 --- a/internal/staticsources/srt/source.go +++ b/internal/staticsources/srt/source.go @@ -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))) diff --git a/internal/staticsources/webrtc/source.go b/internal/staticsources/webrtc/source.go index 067e1c9c..6fa70be6 100644 --- a/internal/staticsources/webrtc/source.go +++ b/internal/staticsources/webrtc/source.go @@ -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() diff --git a/internal/stream/offline_av1.mp4 b/internal/stream/offline_av1.mp4 new file mode 100644 index 00000000..f79cce0e Binary files /dev/null and b/internal/stream/offline_av1.mp4 differ diff --git a/internal/stream/offline_h264.mp4 b/internal/stream/offline_h264.mp4 new file mode 100644 index 00000000..e3d8858c Binary files /dev/null and b/internal/stream/offline_h264.mp4 differ diff --git a/internal/stream/offline_h265.mp4 b/internal/stream/offline_h265.mp4 new file mode 100644 index 00000000..1df8c366 Binary files /dev/null and b/internal/stream/offline_h265.mp4 differ diff --git a/internal/stream/offline_sub_stream.go b/internal/stream/offline_sub_stream.go new file mode 100644 index 00000000..02ee7f9f --- /dev/null +++ b/internal/stream/offline_sub_stream.go @@ -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() +} diff --git a/internal/stream/offline_sub_stream_track.go b/internal/stream/offline_sub_stream_track.go new file mode 100644 index 00000000..dbf6dfe7 --- /dev/null +++ b/internal/stream/offline_sub_stream_track.go @@ -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 +} diff --git a/internal/stream/offline_vp9.mp4 b/internal/stream/offline_vp9.mp4 new file mode 100644 index 00000000..e2afc206 Binary files /dev/null and b/internal/stream/offline_vp9.mp4 differ diff --git a/internal/stream/rtp_encoder.go b/internal/stream/rtp_encoder.go index ca32063a..65485f73 100644 --- a/internal/stream/rtp_encoder.go +++ b/internal/stream/rtp_encoder.go @@ -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} } } diff --git a/internal/stream/stream.go b/internal/stream/stream.go index bdd46dc0..7e206558 100644 --- a/internal/stream/stream.go +++ b/internal/stream/stream.go @@ -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) } diff --git a/internal/stream/stream_format.go b/internal/stream/stream_format.go index ff221976..cc46a0ed 100644 --- a/internal/stream/stream_format.go +++ b/internal/stream/stream_format.go @@ -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 } diff --git a/internal/stream/stream_media.go b/internal/stream/stream_media.go index c5933e96..b92d401c 100644 --- a/internal/stream/stream_media.go +++ b/internal/stream/stream_media.go @@ -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, diff --git a/internal/stream/stream_test.go b/internal/stream/stream_test.go index 559a13d3..cfba0b63 100644 --- a/internal/stream/stream_test.go +++ b/internal/stream/stream_test.go @@ -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 +} diff --git a/internal/stream/sub_stream.go b/internal/stream/sub_stream.go new file mode 100644 index 00000000..fc81e99c --- /dev/null +++ b/internal/stream/sub_stream.go @@ -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) +} diff --git a/internal/stream/sub_stream_format.go b/internal/stream/sub_stream_format.go new file mode 100644 index 00000000..fdf55fff --- /dev/null +++ b/internal/stream/sub_stream_format.go @@ -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 +} diff --git a/internal/stream/sub_stream_media.go b/internal/stream/sub_stream_media.go new file mode 100644 index 00000000..e767a26f --- /dev/null +++ b/internal/stream/sub_stream_media.go @@ -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 +} diff --git a/internal/test/formats.go b/internal/test/formats.go index 11639756..fc4234a4 100644 --- a/internal/test/formats.go +++ b/internal/test/formats.go @@ -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, diff --git a/internal/test/path_manager.go b/internal/test/path_manager.go index 80bdbbb8..8deb8522 100644 --- a/internal/test/path_manager.go +++ b/internal/test/path_manager.go @@ -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) } diff --git a/internal/test/static_source_parent.go b/internal/test/static_source_parent.go index c50fda04..2d8bd047 100644 --- a/internal/test/static_source_parent.go +++ b/internal/test/static_source_parent.go @@ -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. diff --git a/internal/testapidocs/apidocs_test.go b/internal/testapidocs/apidocs_test.go index f7743024..ef0aa491 100644 --- a/internal/testapidocs/apidocs_test.go +++ b/internal/testapidocs/apidocs_test.go @@ -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", diff --git a/mediamtx.yml b/mediamtx.yml index 1724a779..fd9d7b13 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -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