moq: support draft-19 of the specification (#5968)

* support draft-19 of the specification

* support subscribing the same track multiple times
This commit is contained in:
Alessandro Ros
2026-07-21 00:24:03 +02:00
committed by GitHub
parent be610a6a04
commit 6da35dc389
17 changed files with 702 additions and 505 deletions
+8
View File
@@ -95,6 +95,12 @@ components:
- read - read
- publish - publish
MoQVersion:
type: string
enum:
- moqt-18
- moqt-19
OKStatus: OKStatus:
type: string type: string
enum: enum:
@@ -805,6 +811,8 @@ components:
$ref: "#/components/schemas/MoQSessionState" $ref: "#/components/schemas/MoQSessionState"
userAgent: userAgent:
type: string type: string
version:
$ref: "#/components/schemas/MoQVersion"
MoQSessionList: MoQSessionList:
type: object type: object
+1 -1
View File
@@ -9,7 +9,7 @@ Media-over-QUIC is a streaming protocol built upon cutting edge protocols (QUIC,
Media-over-QUIC has a wide range of features and variants, most of them in active development. We currently support the following: Media-over-QUIC has a wide range of features and variants, most of them in active development. We currently support the following:
- We support `draft-18` of the [main specification](https://datatracker.ietf.org/doc/html/draft-ietf-moq-transport-18). - The server supports `draft-19` and `draft-18` of the [main specification](https://datatracker.ietf.org/doc/html/draft-ietf-moq-transport-19), and prefers `draft-19` when both are offered during negotiation.
- We only support using Media-over-QUIC through browsers and in particular through the WebTransport API. We do not support using QUIC directly. - We only support using Media-over-QUIC through browsers and in particular through the WebTransport API. We do not support using QUIC directly.
- We support the `PUBLISH` and `SUBSCRIBE` messages only, which are the ones meant to be used with a routing solution like _MediaMTX_. - We support the `PUBLISH` and `SUBSCRIBE` messages only, which are the ones meant to be used with a routing solution like _MediaMTX_.
- We use the MOQT Streaming Format (MSF) to advertise tracks, described in [this specification](https://datatracker.ietf.org/doc/html/draft-ietf-moq-msf-00). - We use the MOQT Streaming Format (MSF) to advertise tracks, described in [this specification](https://datatracker.ietf.org/doc/html/draft-ietf-moq-msf-00).
+4
View File
@@ -56,6 +56,7 @@ func TestMoQSessionsList(t *testing.T) {
State: defs.APIMoQSessionStatePublish, State: defs.APIMoQSessionStatePublish,
Path: "stream1", Path: "stream1",
Query: "token=abc", Query: "token=abc",
Version: defs.APIMoQVersionDraft19,
InboundBytes: 1000, InboundBytes: 1000,
OutboundBytes: 2000, OutboundBytes: 2000,
}, },
@@ -66,6 +67,7 @@ func TestMoQSessionsList(t *testing.T) {
State: defs.APIMoQSessionStateRead, State: defs.APIMoQSessionStateRead,
Path: "stream2", Path: "stream2",
Query: "", Query: "",
Version: defs.APIMoQVersionDraft18,
InboundBytes: 500, InboundBytes: 500,
OutboundBytes: 1500, OutboundBytes: 1500,
}, },
@@ -109,6 +111,7 @@ func TestMoQSessionsGet(t *testing.T) {
State: defs.APIMoQSessionStatePublish, State: defs.APIMoQSessionStatePublish,
Path: "mystream", Path: "mystream",
Query: "key=value", Query: "key=value",
Version: defs.APIMoQVersionDraft19,
InboundBytes: 999999, InboundBytes: 999999,
OutboundBytes: 888888, OutboundBytes: 888888,
}, },
@@ -155,6 +158,7 @@ func TestMoQSessionsKick(t *testing.T) {
State: defs.APIMoQSessionStatePublish, State: defs.APIMoQSessionStatePublish,
Path: "mystream", Path: "mystream",
Query: "", Query: "",
Version: defs.APIMoQVersionDraft19,
InboundBytes: 1000, InboundBytes: 1000,
OutboundBytes: 2000, OutboundBytes: 2000,
}, },
+5
View File
@@ -47,6 +47,11 @@ var enums = []struct {
internalName: "APIMoQSessionState", internalName: "APIMoQSessionState",
File: filepath.Join("internal", "defs", "api_moq.go"), File: filepath.Join("internal", "defs", "api_moq.go"),
}, },
{
externalName: "MoQVersion",
internalName: "APIMoQVersion",
File: filepath.Join("internal", "defs", "api_moq.go"),
},
{ {
externalName: "OKStatus", externalName: "OKStatus",
internalName: "APIOKStatus", internalName: "APIOKStatus",
+3
View File
@@ -366,6 +366,9 @@ func isStructEnum(rt reflect.Type) bool {
case reflect.TypeOf(defs.APIMoQSessionState("")): case reflect.TypeOf(defs.APIMoQSessionState("")):
return true return true
case reflect.TypeOf(defs.APIMoQVersion("")):
return true
case reflect.TypeOf(defs.APISRTConnState("")): case reflect.TypeOf(defs.APISRTConnState("")):
return true return true
} }
+10
View File
@@ -23,6 +23,15 @@ const (
APIMoQSessionStatePublish APIMoQSessionState = "publish" APIMoQSessionStatePublish APIMoQSessionState = "publish"
) )
// APIMoQVersion is a MoQ protocol version.
type APIMoQVersion string
// protocol versions.
const (
APIMoQVersionDraft18 APIMoQVersion = "moqt-18"
APIMoQVersionDraft19 APIMoQVersion = "moqt-19"
)
// APIMoQSessionList is a list of MoQ sessions. // APIMoQSessionList is a list of MoQ sessions.
type APIMoQSessionList struct { type APIMoQSessionList struct {
ItemCount int `json:"itemCount"` ItemCount int `json:"itemCount"`
@@ -39,6 +48,7 @@ type APIMoQSession struct {
Path string `json:"path"` Path string `json:"path"`
Query string `json:"query"` Query string `json:"query"`
UserAgent string `json:"userAgent"` UserAgent string `json:"userAgent"`
Version APIMoQVersion `json:"version"`
InboundBytes uint64 `json:"inboundBytes"` InboundBytes uint64 `json:"inboundBytes"`
OutboundBytes uint64 `json:"outboundBytes"` OutboundBytes uint64 `json:"outboundBytes"`
} }
+1
View File
@@ -339,6 +339,7 @@ func (dummyMoQServer) APISessionsList() (*defs.APIMoQSessionList, error) {
RemoteAddr: "127.0.0.2:3456", RemoteAddr: "127.0.0.2:3456",
State: defs.APIMoQSessionStatePublish, State: defs.APIMoQSessionStatePublish,
Path: "mypath", Path: "mypath",
Version: defs.APIMoQVersionDraft19,
InboundBytes: 321, InboundBytes: 321,
OutboundBytes: 654, OutboundBytes: 654,
}}, }},
+298 -234
View File
@@ -4,6 +4,7 @@ package moq
import ( import (
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"strconv"
"strings" "strings"
"github.com/bluenviron/gortsplib/v5/pkg/description" "github.com/bluenviron/gortsplib/v5/pkg/description"
@@ -16,262 +17,322 @@ import (
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/vp8" "github.com/bluenviron/mediacommon/v2/pkg/codecs/vp8"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/vp9" "github.com/bluenviron/mediacommon/v2/pkg/codecs/vp9"
"github.com/bluenviron/mediamtx/internal/protocols/moq/catalog"
"github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/unit" "github.com/bluenviron/mediamtx/internal/unit"
) )
// FromStream maps a MediaMTX stream to a Media-over-QUIC stream. type writeDataFunc func(payload []byte, pts int64) error
func FromStream(desc *description.Session) ([]*Track, error) {
var tracks []*Track // SetupTrackFunc is a function that sets up a track in a MediaMTX stream.
type SetupTrackFunc func(r *stream.Reader, writeData writeDataFunc)
// FromStream maps a MediaMTX stream to a Media-over-QUIC catalog and subscribed tracks.
func FromStream(desc *description.Session) (*catalog.Catalog, []SetupTrackFunc, error) {
cat := &catalog.Catalog{
Version: 1,
}
var setupTracks []SetupTrackFunc
addTrack := func(
media *description.Media,
forma format.Format,
track catalog.Track,
genParsePayload func(writeData writeDataFunc) func(u *unit.Unit) error,
) {
track.Name = strconv.Itoa(len(cat.Tracks))
track.Packaging = "loc"
track.IsLive = true
setup := func(r *stream.Reader, writeData writeDataFunc) {
parsePayload := genParsePayload(writeData)
r.OnData(media, forma, func(u *unit.Unit) error {
return parsePayload(u)
})
}
cat.Tracks = append(cat.Tracks, track)
setupTracks = append(setupTracks, setup)
}
for _, media := range desc.Medias { for _, media := range desc.Medias {
for _, forma := range media.Formats { for _, forma := range media.Formats {
switch forma := forma.(type) { switch forma := forma.(type) {
case *format.AV1: case *format.AV1:
firstRandomAccess := false addTrack(
media,
track := &Track{ forma,
Codec: "av01.0.04M.08", catalog.Track{
Media: media, Codec: "av01.0.04M.08",
Format: forma,
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error {
if u.NilPayload() {
return nil
}
if !firstRandomAccess && !av1.IsRandomAccess2(u.Payload.(unit.PayloadAV1)) {
return nil
}
firstRandomAccess = true
payload, err := av1.Bitstream([][]byte(u.Payload.(unit.PayloadAV1))).Marshal()
if err != nil {
return err
}
return wrapped(payload, u.PTS)
}, },
} func(writeData writeDataFunc) func(u *unit.Unit) error {
tracks = append(tracks, track) firstRandomAccess := false
case *format.VP9: return func(u *unit.Unit) error {
firstRandomAccess := false if u.NilPayload() {
return nil
}
track := &Track{ if !firstRandomAccess && !av1.IsRandomAccess2(u.Payload.(unit.PayloadAV1)) {
Codec: "vp09.00.10.08", return nil
Media: media, }
Format: forma, firstRandomAccess = true
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error {
if u.NilPayload() {
return nil
}
if !firstRandomAccess && !vp9.IsRandomAccess(u.Payload.(unit.PayloadVP9)) { payload, err := av1.Bitstream([][]byte(u.Payload.(unit.PayloadAV1))).Marshal()
return nil
}
firstRandomAccess = true
return wrapped(u.Payload.(unit.PayloadVP9), u.PTS)
},
}
tracks = append(tracks, track)
case *format.VP8:
firstRandomAccess := false
track := &Track{
Codec: "vp8",
Media: media,
Format: forma,
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error {
if u.NilPayload() {
return nil
}
if !firstRandomAccess && !vp8.IsRandomAccess(u.Payload.(unit.PayloadVP8)) {
return nil
}
firstRandomAccess = true
return wrapped(u.Payload.(unit.PayloadVP8), u.PTS)
},
}
tracks = append(tracks, track)
case *format.H265:
firstRandomAccess := false
track := &Track{
Codec: "hev1.1.6.L93.B0",
Media: media,
Format: forma,
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error {
if u.NilPayload() {
return nil
}
if !firstRandomAccess && !h265.IsRandomAccess(u.Payload.(unit.PayloadH265)) {
return nil
}
firstRandomAccess = true
payload, err := h264.AVCC(u.Payload.(unit.PayloadH265)).Marshal()
if err != nil {
return err
}
return wrapped(payload, u.PTS)
},
}
tracks = append(tracks, track)
case *format.H264:
firstRandomAccess := false
track := &Track{
Codec: "avc3.640028",
Media: media,
Format: forma,
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error {
if u.NilPayload() {
return nil
}
if !firstRandomAccess && !h264.IsRandomAccess(u.Payload.(unit.PayloadH264)) {
return nil
}
firstRandomAccess = true
payload, err := h264.AVCC(u.Payload.(unit.PayloadH264)).Marshal()
if err != nil {
return err
}
return wrapped(payload, u.PTS)
},
}
tracks = append(tracks, track)
case *format.Opus:
track := &Track{
Codec: "opus",
Samplerate: 48000,
Channels: forma.ChannelCount,
Media: media,
Format: forma,
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error {
if u.NilPayload() {
return nil
}
for _, pkt := range u.Payload.(unit.PayloadOpus) {
err := wrapped(pkt, u.PTS)
if err != nil { if err != nil {
return err return err
} }
return writeData(payload, u.PTS)
} }
return nil
}, },
} )
tracks = append(tracks, track)
case *format.VP9:
addTrack(
media,
forma,
catalog.Track{
Codec: "vp09.00.10.08",
},
func(writeData writeDataFunc) func(u *unit.Unit) error {
firstRandomAccess := false
return func(u *unit.Unit) error {
if u.NilPayload() {
return nil
}
if !firstRandomAccess && !vp9.IsRandomAccess(u.Payload.(unit.PayloadVP9)) {
return nil
}
firstRandomAccess = true
return writeData(u.Payload.(unit.PayloadVP9), u.PTS)
}
},
)
case *format.VP8:
addTrack(
media,
forma,
catalog.Track{
Codec: "vp8",
},
func(writeData writeDataFunc) func(u *unit.Unit) error {
firstRandomAccess := false
return func(u *unit.Unit) error {
if u.NilPayload() {
return nil
}
if !firstRandomAccess && !vp8.IsRandomAccess(u.Payload.(unit.PayloadVP8)) {
return nil
}
firstRandomAccess = true
return writeData(u.Payload.(unit.PayloadVP8), u.PTS)
}
},
)
case *format.H265:
addTrack(
media,
forma,
catalog.Track{
Codec: "hev1.1.6.L93.B0",
},
func(writeData writeDataFunc) func(u *unit.Unit) error {
firstRandomAccess := false
return func(u *unit.Unit) error {
if u.NilPayload() {
return nil
}
if !firstRandomAccess && !h265.IsRandomAccess(u.Payload.(unit.PayloadH265)) {
return nil
}
firstRandomAccess = true
payload, err := h264.AVCC(u.Payload.(unit.PayloadH265)).Marshal()
if err != nil {
return err
}
return writeData(payload, u.PTS)
}
},
)
case *format.H264:
addTrack(
media,
forma,
catalog.Track{
Codec: "avc3.640028",
},
func(writeData writeDataFunc) func(u *unit.Unit) error {
firstRandomAccess := false
return func(u *unit.Unit) error {
if u.NilPayload() {
return nil
}
if !firstRandomAccess && !h264.IsRandomAccess(u.Payload.(unit.PayloadH264)) {
return nil
}
firstRandomAccess = true
payload, err := h264.AVCC(u.Payload.(unit.PayloadH264)).Marshal()
if err != nil {
return err
}
return writeData(payload, u.PTS)
}
},
)
case *format.Opus:
addTrack(
media,
forma,
catalog.Track{
Codec: "opus",
Samplerate: 48000,
Channels: forma.ChannelCount,
},
func(writeData writeDataFunc) func(u *unit.Unit) error {
return func(u *unit.Unit) error {
if u.NilPayload() {
return nil
}
for _, pkt := range u.Payload.(unit.PayloadOpus) {
err := writeData(pkt, u.PTS)
if err != nil {
return err
}
}
return nil
}
},
)
case *format.Generic: case *format.Generic:
if strings.HasPrefix(strings.ToLower(forma.RTPMap()), "flac/") { if strings.HasPrefix(strings.ToLower(forma.RTPMap()), "flac/") {
enc, err := hex.DecodeString(forma.FMT["streaminfo"]) enc, err := hex.DecodeString(forma.FMT["streaminfo"])
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
var streamInfo flac.StreamInfo var streamInfo flac.StreamInfo
err = streamInfo.Unmarshal(enc) err = streamInfo.Unmarshal(enc)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
track := &Track{ addTrack(
Codec: "flac", media,
Samplerate: int(streamInfo.SampleRate), forma,
Channels: int(streamInfo.ChannelCount), catalog.Track{
InitData: base64.StdEncoding.EncodeToString(enc), Codec: "flac",
Media: media, Samplerate: int(streamInfo.SampleRate),
Format: forma, Channels: int(streamInfo.ChannelCount),
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error { InitData: base64.StdEncoding.EncodeToString(enc),
if u.NilPayload() {
return nil
}
return wrapped(u.Payload.(unit.PayloadFLAC), u.PTS)
}, },
} func(writeData writeDataFunc) func(u *unit.Unit) error {
tracks = append(tracks, track) return func(u *unit.Unit) error {
if u.NilPayload() {
return nil
}
return writeData(u.Payload.(unit.PayloadFLAC), u.PTS)
}
},
)
} }
case *format.MPEG4Audio: case *format.MPEG4Audio:
if forma.Config != nil { if forma.Config != nil {
enc, err := forma.Config.Marshal() enc, err := forma.Config.Marshal()
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
track := &Track{ addTrack(
Codec: "mp4a.40.2", media,
Samplerate: forma.Config.SampleRate, forma,
Channels: int(forma.Config.ChannelConfig), catalog.Track{
InitData: base64.StdEncoding.EncodeToString(enc), Codec: "mp4a.40.2",
Media: media, Samplerate: forma.Config.SampleRate,
Format: forma, Channels: int(forma.Config.ChannelConfig),
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error { InitData: base64.StdEncoding.EncodeToString(enc),
},
func(writeData writeDataFunc) func(u *unit.Unit) error {
return func(u *unit.Unit) error {
if u.NilPayload() {
return nil
}
pts := u.PTS
for _, au := range u.Payload.(unit.PayloadMPEG4Audio) {
err2 := writeData(au, pts)
if err2 != nil {
return err2
}
pts += mpeg4audio.SamplesPerAccessUnit
}
return nil
}
},
)
}
case *format.G711:
addTrack(
media,
forma,
catalog.Track{
Codec: "pcm-s16",
Samplerate: forma.SampleRate,
Channels: forma.ChannelCount,
},
func(writeData writeDataFunc) func(u *unit.Unit) error {
return func(u *unit.Unit) error {
if u.NilPayload() { if u.NilPayload() {
return nil return nil
} }
pts := u.PTS var bigEndian []byte
if forma.MULaw {
for _, au := range u.Payload.(unit.PayloadMPEG4Audio) { var mu g711.Mulaw
err2 := wrapped(au, pts) mu.Unmarshal(u.Payload.(unit.PayloadG711))
if err2 != nil { bigEndian = mu
return err2 } else {
} var al g711.Alaw
al.Unmarshal(u.Payload.(unit.PayloadG711))
pts += mpeg4audio.SamplesPerAccessUnit bigEndian = al
} }
return nil
},
}
tracks = append(tracks, track)
}
case *format.G711: swapped := make([]byte, len(bigEndian))
track := &Track{ for i := 0; i+2 <= len(bigEndian); i += 2 {
Codec: "pcm-s16", swapped[i], swapped[i+1] = bigEndian[i+1], bigEndian[i]
Samplerate: forma.SampleRate, }
Channels: forma.ChannelCount,
Media: media, return writeData(swapped, u.PTS)
Format: forma,
OnData: func(u *unit.Unit, wrapped func([]byte, int64) error) error {
if u.NilPayload() {
return nil
} }
var bigEndian []byte
if forma.MULaw {
var mu g711.Mulaw
mu.Unmarshal(u.Payload.(unit.PayloadG711))
bigEndian = mu
} else {
var al g711.Alaw
al.Unmarshal(u.Payload.(unit.PayloadG711))
bigEndian = al
}
swapped := make([]byte, len(bigEndian))
for i := 0; i+2 <= len(bigEndian); i += 2 {
swapped[i], swapped[i+1] = bigEndian[i+1], bigEndian[i]
}
return wrapped(swapped, u.PTS)
}, },
} )
tracks = append(tracks, track)
case *format.LPCM: case *format.LPCM:
var codec string var codec string
@@ -286,33 +347,36 @@ func FromStream(desc *description.Session) ([]*Track, error) {
codec = "pcm-s32" codec = "pcm-s32"
} }
track := &Track{ addTrack(
Codec: codec, media,
Samplerate: forma.SampleRate, forma,
Channels: forma.ChannelCount, catalog.Track{
Media: media, Codec: codec,
Format: forma, Samplerate: forma.SampleRate,
OnData: func(u *unit.Unit, onData func([]byte, int64) error) error { Channels: forma.ChannelCount,
if u.NilPayload() {
return nil
}
src := []byte(u.Payload.(unit.PayloadLPCM))
byteDepth := forma.BitDepth / 8
swapped := make([]byte, len(src))
for i := 0; i+byteDepth <= len(src); i += byteDepth {
for j := range byteDepth {
swapped[i+j] = src[i+byteDepth-1-j]
}
}
return onData(swapped, u.PTS)
}, },
} func(writeData writeDataFunc) func(u *unit.Unit) error {
tracks = append(tracks, track) return func(u *unit.Unit) error {
if u.NilPayload() {
return nil
}
src := []byte(u.Payload.(unit.PayloadLPCM))
byteDepth := forma.BitDepth / 8
swapped := make([]byte, len(src))
for i := 0; i+byteDepth <= len(src); i += byteDepth {
for j := range byteDepth {
swapped[i+j] = src[i+byteDepth-1-j]
}
}
return writeData(swapped, u.PTS)
}
},
)
} }
} }
} }
return tracks, nil return cat, setupTracks, nil
} }
+78 -46
View File
@@ -9,6 +9,7 @@ import (
"github.com/bluenviron/gortsplib/v5/pkg/format" "github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/flac" "github.com/bluenviron/mediacommon/v2/pkg/codecs/flac"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediamtx/internal/protocols/moq/catalog"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -59,53 +60,84 @@ func TestFromStream(t *testing.T) {
}, },
} }
tracks, err := FromStream(desc) cat, _, err := FromStream(desc)
require.NoError(t, err) require.NoError(t, err)
for _, track := range tracks { require.Equal(t, &catalog.Catalog{
track.OnData = nil Version: 1,
} Tracks: []catalog.Track{
{
require.Equal(t, []*Track{ Name: "0",
{ Packaging: "loc",
Codec: "av01.0.04M.08", Media: desc.Medias[0], IsLive: true,
Format: desc.Medias[0].Formats[0], Codec: "av01.0.04M.08",
},
{
Name: "1",
Packaging: "loc",
IsLive: true,
Codec: "vp09.00.10.08",
},
{
Name: "2",
Packaging: "loc",
IsLive: true,
Codec: "vp8",
},
{
Name: "3",
Packaging: "loc",
IsLive: true,
Codec: "hev1.1.6.L93.B0",
},
{
Name: "4",
Packaging: "loc",
IsLive: true,
Codec: "avc3.640028",
},
{
Name: "5",
Packaging: "loc",
IsLive: true,
Codec: "opus",
Samplerate: 48000,
Channels: 2,
},
{
Name: "6",
Packaging: "loc",
IsLive: true,
Codec: "flac",
Samplerate: 44100,
Channels: 2,
InitData: base64.StdEncoding.EncodeToString(flacEnc),
},
{
Name: "7",
Packaging: "loc",
IsLive: true,
Codec: "mp4a.40.2",
Samplerate: 44100,
Channels: 2,
InitData: base64.StdEncoding.EncodeToString(mpeg4Enc),
},
{
Name: "8",
Packaging: "loc",
IsLive: true,
Codec: "pcm-s16",
Samplerate: 8000,
Channels: 1,
},
{
Name: "9",
Packaging: "loc",
IsLive: true,
Codec: "pcm-s16",
Samplerate: 44100,
Channels: 2,
},
}, },
{ }, cat)
Codec: "vp09.00.10.08", Media: desc.Medias[1],
Format: desc.Medias[1].Formats[0],
},
{
Codec: "vp8", Media: desc.Medias[2],
Format: desc.Medias[2].Formats[0],
},
{
Codec: "hev1.1.6.L93.B0", Media: desc.Medias[3],
Format: desc.Medias[3].Formats[0],
},
{
Codec: "avc3.640028", Media: desc.Medias[4],
Format: desc.Medias[4].Formats[0],
},
{
Codec: "opus", Samplerate: 48000, Channels: 2,
Media: desc.Medias[5], Format: desc.Medias[5].Formats[0],
},
{
Codec: "flac", Samplerate: 44100, Channels: 2,
InitData: base64.StdEncoding.EncodeToString(flacEnc), Media: desc.Medias[6], Format: desc.Medias[6].Formats[0],
},
{
Codec: "mp4a.40.2", Samplerate: 44100, Channels: 2,
InitData: base64.StdEncoding.EncodeToString(mpeg4Enc), Media: desc.Medias[7], Format: desc.Medias[7].Formats[0],
},
{
Codec: "pcm-s16", Samplerate: 8000, Channels: 1,
Media: desc.Medias[8], Format: desc.Medias[8].Formats[0],
},
{
Codec: "pcm-s16", Samplerate: 44100, Channels: 2,
Media: desc.Medias[9], Format: desc.Medias[9].Formats[0],
},
}, tracks)
} }
+2 -5
View File
@@ -25,11 +25,8 @@ func findTimestamp(props []property.Property) (int64, bool) {
return 0, false return 0, false
} }
// ToStream maps a Media-over-QUIC stream to a MediaMTX stream. // ToStream maps a Media-over-QUIC catalog and published tracks to a MediaMTX stream.
func ToStream( func ToStream(cat *catalog.Catalog, subStream **stream.SubStream) (
cat *catalog.Catalog,
subStream **stream.SubStream,
) (
[]*description.Media, []*description.Media,
map[uint64]func(sg *subgroup.SubGroup) error, map[uint64]func(sg *subgroup.SubGroup) error,
error, error,
-18
View File
@@ -1,18 +0,0 @@
package moq
import (
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediamtx/internal/unit"
)
// Track is a Media-over-QUIC track.
type Track struct {
Codec string
Samplerate int
Channels int
InitData string
Media *description.Media
Format format.Format
OnData func(u *unit.Unit, wrapped func([]byte, int64) error) error
}
+33 -15
View File
@@ -37,27 +37,41 @@ var readerJS []byte
var publisherJS []byte var publisherJS []byte
const ( const (
moqtVersion = "moqt-18"
wtProtocolHeader = "WT-Protocol" wtProtocolHeader = "WT-Protocol"
wtAvailableProtocolsHeader = "WT-Available-Protocols" wtAvailableProtocolsHeader = "WT-Available-Protocols"
) )
// ordered from most preferred to least preferred
var supportedMoqtVersions = []defs.APIMoQVersion{
defs.APIMoQVersionDraft19,
defs.APIMoQVersionDraft18,
}
type ginUnwrapper interface { type ginUnwrapper interface {
Unwrap() http.ResponseWriter Unwrap() http.ResponseWriter
} }
func containsMoqtVersion(header string) bool { func selectMoqtVersion(header string) defs.APIMoQVersion {
available := make(map[string]struct{})
for item := range strings.SplitSeq(header, ",") { for item := range strings.SplitSeq(header, ",") {
item = strings.TrimSpace(item) item = strings.TrimSpace(item)
if i := strings.IndexByte(item, ';'); i >= 0 { if i := strings.IndexByte(item, ';'); i >= 0 {
item = item[:i] item = item[:i]
} }
item = strings.Trim(strings.TrimSpace(item), `"`) item = strings.Trim(strings.TrimSpace(item), `"`)
if item == moqtVersion { if item != "" {
return true available[item] = struct{}{}
} }
} }
return false
for _, version := range supportedMoqtVersions {
if _, ok := available[string(version)]; ok {
return version
}
}
return ""
} }
func trailingSlashLocation(rawPath string, rawQuery string) string { func trailingSlashLocation(rawPath string, rawQuery string) string {
@@ -308,19 +322,22 @@ func (s *httpServer) onRequestHTTPS3(ctx *gin.Context) {
return return
} }
if offered := ctx.Request.Header.Get(wtAvailableProtocolsHeader); offered != "" { offered := ctx.Request.Header.Get(wtAvailableProtocolsHeader)
if !containsMoqtVersion(offered) { if offered == "" {
s.writeErrorNoLog(ctx, http.StatusBadRequest, s.writeErrorNoLog(ctx, http.StatusBadRequest,
fmt.Errorf("no supported MoQ version in %s", wtAvailableProtocolsHeader)) fmt.Errorf("missing %s header", wtAvailableProtocolsHeader))
return return
} }
version := selectMoqtVersion(offered)
if version == "" {
s.writeErrorNoLog(ctx, http.StatusBadRequest,
fmt.Errorf("no supported MoQ version in %s", wtAvailableProtocolsHeader))
return
} }
w := ctx.Writer.(ginUnwrapper).Unwrap() w := ctx.Writer.(ginUnwrapper).Unwrap()
w.Header().Set(wtProtocolHeader, `"`+string(version)+`"`)
if offered := ctx.Request.Header.Get(wtAvailableProtocolsHeader); offered != "" {
w.Header().Set(wtProtocolHeader, `"`+moqtVersion+`"`)
}
wt, err := s.innerHTTP3.Upgrade(w, ctx.Request) wt, err := s.innerHTTP3.Upgrade(w, ctx.Request)
if err != nil { if err != nil {
@@ -332,6 +349,7 @@ func (s *httpServer) onRequestHTTPS3(ctx *gin.Context) {
pathName: pathName, pathName: pathName,
query: ctx.Request.URL.RawQuery, query: ctx.Request.URL.RawQuery,
userAgent: ctx.Request.Header.Get("User-Agent"), userAgent: ctx.Request.Header.Get("User-Agent"),
version: version,
wt: wt, wt: wt,
}) })
if res.err != nil { if res.err != nil {
+1 -1
View File
@@ -34,7 +34,7 @@
class MediaMTXMoQPublisher { class MediaMTXMoQPublisher {
static #RETRY_PAUSE = 2000; static #RETRY_PAUSE = 2000;
static #MOQT_VERSION = "moqt-18"; static #MOQT_VERSION = "moqt-19";
static #SETUP_TYPE = 0x2f00n; static #SETUP_TYPE = 0x2f00n;
static #MSG_PUBLISH = 0x1dn; static #MSG_PUBLISH = 0x1dn;
+1 -1
View File
@@ -33,7 +33,7 @@
class MediaMTXMoQReader { class MediaMTXMoQReader {
static #RETRY_PAUSE = 2000; static #RETRY_PAUSE = 2000;
static #MOQT_VERSION = "moqt-18"; static #MOQT_VERSION = "moqt-19";
static #SETUP_TYPE = 0x2f00n; static #SETUP_TYPE = 0x2f00n;
static #MSG_SUBSCRIBE = 0x03n; static #MSG_SUBSCRIBE = 0x03n;
+2
View File
@@ -37,6 +37,7 @@ type newSessionReq struct {
pathName string pathName string
query string query string
userAgent string userAgent string
version defs.APIMoQVersion
wt *webtransport.Session wt *webtransport.Session
res chan newSessionRes res chan newSessionRes
} }
@@ -180,6 +181,7 @@ outer:
pathName: req.pathName, pathName: req.pathName,
query: req.query, query: req.query,
userAgent: req.userAgent, userAgent: req.userAgent,
version: req.version,
pathManager: s.PathManager, pathManager: s.PathManager,
parent: s, parent: s,
} }
+225 -114
View File
@@ -153,7 +153,7 @@ func TestAuthError(t *testing.T) {
EnableDatagrams: true, EnableDatagrams: true,
EnableStreamResetPartialDelivery: true, EnableStreamResetPartialDelivery: true,
}, },
ApplicationProtocols: []string{"moqt-18"}, ApplicationProtocols: []string{"moqt-19"},
} }
defer d.Close() //nolint:errcheck defer d.Close() //nolint:errcheck
@@ -263,33 +263,226 @@ func TestAuthError(t *testing.T) {
} }
func TestServer(t *testing.T) { func TestServer(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.UniqueMediaH264()}} for _, ca := range []struct {
strm := &stream.Stream{ name string
OrigDesc: desc, clientProtocols []string
WriteQueueSize: 512, expectedVersion defs.APIMoQVersion
RTPMaxPayloadSize: 1450, }{
Parent: test.NilLogger, {
} name: "draft-18",
err := strm.Initialize() clientProtocols: []string{"moqt-18"},
require.NoError(t, err) expectedVersion: defs.APIMoQVersionDraft18,
defer strm.Close()
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
pm := &test.PathManager{
FindPathConfImpl: func(_ defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {
return &defs.PathFindPathConfRes{Conf: &conf.Path{}}, nil
}, },
AddReaderImpl: func(_ defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) { {
return &defs.PathAddReaderRes{Path: &serverDummyPath{}, Stream: strm}, nil name: "draft-19",
clientProtocols: []string{"moqt-19"},
expectedVersion: defs.APIMoQVersionDraft19,
}, },
} {
name: "draft-19-preferred",
clientProtocols: []string{"moqt-19", "moqt-18"},
expectedVersion: defs.APIMoQVersionDraft19,
},
} {
t.Run(ca.name, func(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.UniqueMediaH264()}}
strm := &stream.Stream{
OrigDesc: desc,
WriteQueueSize: 512,
RTPMaxPayloadSize: 1450,
Parent: test.NilLogger,
}
err := strm.Initialize()
require.NoError(t, err)
defer strm.Close()
subStream := &stream.SubStream{
Stream: strm,
UseRTPPackets: false,
}
err = subStream.Initialize()
require.NoError(t, err)
pm := &test.PathManager{
FindPathConfImpl: func(_ defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {
return &defs.PathFindPathConfRes{Conf: &conf.Path{}}, nil
},
AddReaderImpl: func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {
require.Equal(t, ca.expectedVersion, req.Author.(*session).version)
return &defs.PathAddReaderRes{Path: &serverDummyPath{}, Stream: strm}, nil
},
}
serverCertFile := test.CreateTempFile(t, test.TLSCertPub)
serverKeyFile := test.CreateTempFile(t, test.TLSCertKey)
s := &Server{
HTTP2Address: "127.0.0.1:19895",
HTTP3Address: "127.0.0.1:19896",
ServerCert: serverCertFile,
ServerKey: serverKeyFile,
AllowOrigins: []string{"*"},
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.Duration(10 * time.Second),
WriteTimeout: conf.Duration(10 * time.Second),
PathManager: pm,
Parent: test.NilLogger,
}
err = s.Initialize()
require.NoError(t, err)
defer s.Close()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
d := &webtransport.Dialer{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, //nolint:gosec
},
QUICConfig: &quic.Config{
EnableDatagrams: true,
EnableStreamResetPartialDelivery: true,
},
ApplicationProtocols: ca.clientProtocols,
}
defer d.Close() //nolint:errcheck
res, sx, err := d.Dial(ctx, "https://127.0.0.1:19896/teststream/moq", nil)
require.NoError(t, err)
defer sx.CloseWithError(0, "") //nolint:errcheck
defer res.Body.Close() //nolint:errcheck
require.Equal(t, `"`+string(ca.expectedVersion)+`"`, res.Header.Get("WT-Protocol"))
setupStream, err := sx.AcceptUniStream(ctx)
require.NoError(t, err)
setupMsg, err := controlmessage.Read(setupStream)
require.NoError(t, err)
require.Equal(t, &controlmessage.Setup{}, setupMsg)
clientSetup, err := sx.OpenUniStreamSync(ctx)
require.NoError(t, err)
_, err = clientSetup.Write(controlmessage.Setup{}.Marshal())
require.NoError(t, err)
catalogBidi, err := sx.OpenStreamSync(ctx)
require.NoError(t, err)
_, err = catalogBidi.Write(controlmessage.Subscribe{
RequestID: 1,
TrackName: ".catalog",
}.Marshal())
require.NoError(t, err)
catalogOkMsg, err := controlmessage.Read(catalogBidi)
require.NoError(t, err)
require.Equal(t, &controlmessage.SubscribeOk{TrackAlias: 1}, catalogOkMsg)
catalogDataStream, err := sx.AcceptUniStream(ctx)
require.NoError(t, err)
var catalogSG subgroup.SubGroup
err = catalogSG.Read(catalogDataStream)
require.NoError(t, err)
var cat catalog.Catalog
err = json.Unmarshal(catalogSG.Objects[0].Payload, &cat)
require.NoError(t, err)
require.Equal(t, catalog.Catalog{
Version: 1,
Tracks: []catalog.Track{{
Name: "0",
Packaging: "loc",
IsLive: true,
Codec: "avc3.640028",
}},
}, cat)
trackBidi, err := sx.OpenStreamSync(ctx)
require.NoError(t, err)
_, err = trackBidi.Write(controlmessage.Subscribe{
RequestID: 2,
TrackName: "0",
}.Marshal())
require.NoError(t, err)
trackOkMsg, err := controlmessage.Read(trackBidi)
require.NoError(t, err)
require.Equal(t, &controlmessage.SubscribeOk{TrackAlias: 2}, trackOkMsg)
trackBidi2, err := sx.OpenStreamSync(ctx)
require.NoError(t, err)
_, err = trackBidi2.Write(controlmessage.Subscribe{
RequestID: 3,
TrackName: "0",
}.Marshal())
require.NoError(t, err)
trackOkMsg2, err := controlmessage.Read(trackBidi2)
require.NoError(t, err)
require.Equal(t, &controlmessage.SubscribeOk{TrackAlias: 3}, trackOkMsg2)
go func() {
time.Sleep(200 * time.Millisecond)
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: 0,
Payload: unit.PayloadH264{{5, 1}},
})
}()
frameStream, err := sx.AcceptUniStream(ctx)
require.NoError(t, err)
var frameSG subgroup.SubGroup
err = frameSG.Read(frameStream)
require.NoError(t, err)
frameStream2, err := sx.AcceptUniStream(ctx)
require.NoError(t, err)
var frameSG2 subgroup.SubGroup
err = frameSG2.Read(frameStream2)
require.NoError(t, err)
expectedPayload, err2 := mch264.AVCC([][]byte{test.FormatH264.SPS, test.FormatH264.PPS, {5, 1}}).Marshal()
require.NoError(t, err2)
require.Equal(t, expectedPayload, frameSG.Objects[0].Payload)
require.Equal(t, expectedPayload, frameSG2.Objects[0].Payload)
require.ElementsMatch(t, []uint64{2, 3}, []uint64{frameSG.Header.TrackAlias, frameSG2.Header.TrackAlias})
trackBidi.Close() //nolint:errcheck
time.Sleep(100 * time.Millisecond)
go func() {
time.Sleep(200 * time.Millisecond)
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: 1,
Payload: unit.PayloadH264{{5, 2}},
})
}()
frameStream3, err := sx.AcceptUniStream(ctx)
require.NoError(t, err)
var frameSG3 subgroup.SubGroup
err = frameSG3.Read(frameStream3)
require.NoError(t, err)
expectedPayload2, err2 := mch264.AVCC([][]byte{test.FormatH264.SPS, test.FormatH264.PPS, {5, 2}}).Marshal()
require.NoError(t, err2)
require.Equal(t, expectedPayload2, frameSG3.Objects[0].Payload)
require.Equal(t, uint64(3), frameSG3.Header.TrackAlias)
trackBidi2.Close() //nolint:errcheck
})
}
}
func TestServerUnsupportedVersion(t *testing.T) {
serverCertFile := test.CreateTempFile(t, test.TLSCertPub) serverCertFile := test.CreateTempFile(t, test.TLSCertPub)
serverKeyFile := test.CreateTempFile(t, test.TLSCertKey) serverKeyFile := test.CreateTempFile(t, test.TLSCertKey)
@@ -302,10 +495,10 @@ func TestServer(t *testing.T) {
TrustedProxies: conf.IPNetworks{}, TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.Duration(10 * time.Second), ReadTimeout: conf.Duration(10 * time.Second),
WriteTimeout: conf.Duration(10 * time.Second), WriteTimeout: conf.Duration(10 * time.Second),
PathManager: pm, PathManager: &test.PathManager{},
Parent: test.NilLogger, Parent: test.NilLogger,
} }
err = s.Initialize() err := s.Initialize()
require.NoError(t, err) require.NoError(t, err)
defer s.Close() defer s.Close()
@@ -313,98 +506,16 @@ func TestServer(t *testing.T) {
defer cancel() defer cancel()
d := &webtransport.Dialer{ d := &webtransport.Dialer{
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, //nolint:gosec
InsecureSkipVerify: true, //nolint:gosec
},
QUICConfig: &quic.Config{ QUICConfig: &quic.Config{
EnableDatagrams: true, EnableDatagrams: true,
EnableStreamResetPartialDelivery: true, EnableStreamResetPartialDelivery: true,
}, },
ApplicationProtocols: []string{"moqt-18"}, ApplicationProtocols: []string{"moqt-20"},
} }
defer d.Close() //nolint:errcheck defer d.Close() //nolint:errcheck
res, sx, err := d.Dial(ctx, "https://127.0.0.1:19896/teststream/moq", nil) res, _, err := d.Dial(ctx, "https://127.0.0.1:19896/teststream/moq", nil)
require.NoError(t, err) require.Error(t, err)
defer sx.CloseWithError(0, "") //nolint:errcheck defer res.Body.Close()
defer res.Body.Close() //nolint:errcheck
setupStream, err := sx.AcceptUniStream(ctx)
require.NoError(t, err)
setupMsg, err := controlmessage.Read(setupStream)
require.NoError(t, err)
require.Equal(t, &controlmessage.Setup{}, setupMsg)
clientSetup, err := sx.OpenUniStreamSync(ctx)
require.NoError(t, err)
_, err = clientSetup.Write(controlmessage.Setup{}.Marshal())
require.NoError(t, err)
catalogBidi, err := sx.OpenStreamSync(ctx)
require.NoError(t, err)
_, err = catalogBidi.Write(controlmessage.Subscribe{
RequestID: 1,
TrackName: ".catalog",
}.Marshal())
require.NoError(t, err)
catalogOkMsg, err := controlmessage.Read(catalogBidi)
require.NoError(t, err)
require.Equal(t, &controlmessage.SubscribeOk{TrackAlias: 1}, catalogOkMsg)
catalogDataStream, err := sx.AcceptUniStream(ctx)
require.NoError(t, err)
var catalogSG subgroup.SubGroup
err = catalogSG.Read(catalogDataStream)
require.NoError(t, err)
var cat catalog.Catalog
err = json.Unmarshal(catalogSG.Objects[0].Payload, &cat)
require.NoError(t, err)
require.Equal(t, catalog.Catalog{
Version: 1,
Tracks: []catalog.Track{{
Name: "0",
Packaging: "loc",
IsLive: true,
Codec: "avc3.640028",
}},
}, cat)
trackBidi, err := sx.OpenStreamSync(ctx)
require.NoError(t, err)
_, err = trackBidi.Write(controlmessage.Subscribe{
RequestID: 2,
TrackName: "0",
}.Marshal())
require.NoError(t, err)
trackOkMsg, err := controlmessage.Read(trackBidi)
require.NoError(t, err)
require.Equal(t, &controlmessage.SubscribeOk{TrackAlias: 2}, trackOkMsg)
go func() {
time.Sleep(200 * time.Millisecond)
subStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{
PTS: 0,
Payload: unit.PayloadH264{{5, 1}},
})
}()
frameStream, err := sx.AcceptUniStream(ctx)
require.NoError(t, err)
var frameSG subgroup.SubGroup
err = frameSG.Read(frameStream)
require.NoError(t, err)
expectedPayload, err2 := mch264.AVCC([][]byte{test.FormatH264.SPS, test.FormatH264.PPS, {5, 1}}).Marshal()
require.NoError(t, err2)
require.Equal(t, expectedPayload, frameSG.Objects[0].Payload)
} }
+30 -70
View File
@@ -27,7 +27,6 @@ import (
"github.com/bluenviron/mediamtx/internal/protocols/moq/property" "github.com/bluenviron/mediamtx/internal/protocols/moq/property"
"github.com/bluenviron/mediamtx/internal/protocols/moq/subgroup" "github.com/bluenviron/mediamtx/internal/protocols/moq/subgroup"
"github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/unit"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/quic-go/webtransport-go" "github.com/quic-go/webtransport-go"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@@ -72,28 +71,6 @@ func credentialsFromAuthorizationToken(authorization *parameter.AuthorizationTok
return &auth.Credentials{} return &auth.Credentials{}
} }
func tracksToCatalog(tracks []*moq.Track) (catalog.Catalog, error) {
cat := catalog.Catalog{
Version: 1,
Tracks: make([]catalog.Track, len(tracks)),
}
for i, track := range tracks {
ct := catalog.Track{
Name: strconv.Itoa(i),
Packaging: "loc",
IsLive: true,
Codec: track.Codec,
Samplerate: track.Samplerate,
Channels: track.Channels,
InitData: track.InitData,
}
cat.Tracks[i] = ct
}
return cat, nil
}
func isSubGroupStream(b byte) bool { func isSubGroupStream(b byte) bool {
return (b & 0x90) == 0x10 return (b & 0x90) == 0x10
} }
@@ -108,22 +85,22 @@ type session struct {
pathName string pathName string
query string query string
userAgent string userAgent string
version defs.APIMoQVersion
pathManager serverPathManager pathManager serverPathManager
parent sessionParent parent sessionParent
ctx context.Context ctx context.Context
ctxCancel context.CancelFunc ctxCancel context.CancelFunc
created time.Time created time.Time
uuid uuid.UUID uuid uuid.UUID
mutex sync.Mutex mutex sync.Mutex
state defs.APIMoQSessionState state defs.APIMoQSessionState
path defs.Path path defs.Path
stream *stream.Stream // read only stream *stream.Stream // read only
tracks []*moq.Track // read only setupTracks []moq.SetupTrackFunc // read only
trackSubscriptions map[int]struct{} // read only catalogReceived chan *catalog.Catalog // publish only
catalogReceived chan []byte // publish only publishReady chan struct{} // publish only
publishReady chan struct{} // publish only inboundTracks map[uint64]*inboundTrack // publish only
inboundTracks map[uint64]*inboundTrack // publish only
inboundBytes atomic.Uint64 inboundBytes atomic.Uint64
outboundBytes atomic.Uint64 outboundBytes atomic.Uint64
@@ -137,9 +114,8 @@ func (s *session) initialize() {
s.created = time.Now() s.created = time.Now()
s.uuid = uuid.New() s.uuid = uuid.New()
s.state = defs.APIMoQSessionStateIdle s.state = defs.APIMoQSessionStateIdle
s.trackSubscriptions = make(map[int]struct{})
s.catalogReceived = make(chan []byte, 1) s.catalogReceived = make(chan *catalog.Catalog, 1)
s.publishReady = make(chan struct{}) s.publishReady = make(chan struct{})
s.setupReceived = make(chan struct{}) s.setupReceived = make(chan struct{})
@@ -379,9 +355,7 @@ func (s *session) onSubscribeCatalog(wstream *webtransport.Stream, m *controlmes
return err return err
} }
tracks, err := moq.FromStream( cat, setupTracks, err := moq.FromStream(addRes.Stream.OrigDesc)
addRes.Stream.OrigDesc,
)
if err != nil { if err != nil {
addRes.Path.RemoveReader(defs.PathRemoveReaderReq{Author: s}) addRes.Path.RemoveReader(defs.PathRemoveReaderReq{Author: s})
return err return err
@@ -390,7 +364,7 @@ func (s *session) onSubscribeCatalog(wstream *webtransport.Stream, m *controlmes
s.mutex.Lock() s.mutex.Lock()
s.path = addRes.Path s.path = addRes.Path
s.stream = addRes.Stream s.stream = addRes.Stream
s.tracks = tracks s.setupTracks = setupTracks
s.mutex.Unlock() s.mutex.Unlock()
s.Log(logger.Info, "is reading from path %s", s.pathName) s.Log(logger.Info, "is reading from path %s", s.pathName)
@@ -400,11 +374,6 @@ func (s *session) onSubscribeCatalog(wstream *webtransport.Stream, m *controlmes
return err return err
} }
cat, err := tracksToCatalog(tracks)
if err != nil {
return err
}
enc, err := json.Marshal(cat) enc, err := json.Marshal(cat)
if err != nil { if err != nil {
return err return err
@@ -456,17 +425,10 @@ func (s *session) onSubscribeTrack(wstream *webtransport.Stream, m *controlmessa
return fmt.Errorf("stream not ready") return fmt.Errorf("stream not ready")
} }
if trackID >= len(s.tracks) { if trackID >= len(s.setupTracks) {
return fmt.Errorf("track index %d out of range", trackID) return fmt.Errorf("track index %d out of range", trackID)
} }
_, ok := s.trackSubscriptions[trackID]
if ok {
return fmt.Errorf("already subscribed to track %d", trackID)
}
s.trackSubscriptions[trackID] = struct{}{}
return nil return nil
}() }()
if err != nil { if err != nil {
@@ -475,10 +437,9 @@ func (s *session) onSubscribeTrack(wstream *webtransport.Stream, m *controlmessa
r := &stream.Reader{Parent: s} r := &stream.Reader{Parent: s}
track := s.tracks[trackID]
groupID := uint64(0) groupID := uint64(0)
wrapped := func(payload []byte, pts int64) error { writeData := func(payload []byte, pts int64) error {
wstream, err2 := s.wt.OpenUniStreamSync(context.Background()) wstream, err2 := s.wt.OpenUniStreamSync(context.Background())
if err2 != nil { if err2 != nil {
return err2 return err2
@@ -509,9 +470,7 @@ func (s *session) onSubscribeTrack(wstream *webtransport.Stream, m *controlmessa
return err2 return err2
} }
r.OnData(track.Media, track.Format, func(u *unit.Unit) error { s.setupTracks[trackID](r, writeData)
return track.OnData(u, wrapped)
})
s.stream.AddReader(r) s.stream.AddReader(r)
defer s.stream.RemoveReader(r) defer s.stream.RemoveReader(r)
@@ -531,7 +490,7 @@ func (s *session) onSubscribeTrack(wstream *webtransport.Stream, m *controlmessa
case err = <-r.Error(): case err = <-r.Error():
return err return err
case <-streamClosed: case <-streamClosed:
return fmt.Errorf("SUBSCRIBE track stream closed") return nil
case <-s.ctx.Done(): case <-s.ctx.Done():
return fmt.Errorf("terminated") return fmt.Errorf("terminated")
} }
@@ -547,16 +506,10 @@ func (s *session) onPublishCatalog(wstream *webtransport.Stream, m *controlmessa
s.mutex.Unlock() s.mutex.Unlock()
select { select {
case catalogData := <-s.catalogReceived: case cat := <-s.catalogReceived:
var cat catalog.Catalog
err := json.Unmarshal(catalogData, &cat)
if err != nil {
return fmt.Errorf("failed to parse catalog JSON: %w", err)
}
var subStream *stream.SubStream var subStream *stream.SubStream
medias, writeFuncs, err := moq.ToStream(&cat, &subStream) medias, writeFuncs, err := moq.ToStream(cat, &subStream)
if err != nil { if err != nil {
return err return err
} }
@@ -666,8 +619,14 @@ func (s *session) onUniSubGroup(r io.Reader) error {
} }
func (s *session) onDataCatalog(r io.Reader, sg *subgroup.SubGroup) error { func (s *session) onDataCatalog(r io.Reader, sg *subgroup.SubGroup) error {
var cat catalog.Catalog
err := json.Unmarshal(sg.Objects[0].Payload, &cat)
if err != nil {
return fmt.Errorf("failed to parse catalog JSON: %w", err)
}
select { select {
case s.catalogReceived <- sg.Objects[0].Payload: case s.catalogReceived <- &cat:
default: default:
return fmt.Errorf("catalog already received") return fmt.Errorf("catalog already received")
} }
@@ -714,6 +673,7 @@ func (s *session) apiItem() defs.APIMoQSession {
Path: s.pathName, Path: s.pathName,
Query: s.query, Query: s.query,
UserAgent: s.userAgent, UserAgent: s.userAgent,
Version: s.version,
InboundBytes: s.inboundBytes.Load(), InboundBytes: s.inboundBytes.Load(),
OutboundBytes: s.outboundBytes.Load(), OutboundBytes: s.outboundBytes.Load(),
} }