Skip to content

Commit 8402a72

Browse files
authored
moq: support forwarding streams (#6145)
1 parent 7ade2fb commit 8402a72

17 files changed

Lines changed: 1252 additions & 171 deletions

File tree

api/openapi.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ components:
152152
- rtsp
153153
- rtsps
154154
- srt
155+
- moq
155156
- whip
156157
- whips
157158

@@ -1275,6 +1276,8 @@ components:
12751276
type: string
12761277
destFingerprint:
12771278
type: string
1279+
moqTransport:
1280+
type: string
12781281
whipBearerToken:
12791282
type: string
12801283

docs/2-features/11-forward.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
Incoming streams can be natively forwarded to other servers with the following protocols:
44

5+
- [Media-over-QUIC](#media-over-quic)
56
- [SRT](#srt)
67
- [WebRTC](#webrtc)
78
- [RTSP](#rtsp)
@@ -14,6 +15,24 @@ We provide instructions to forward streams to the following services:
1415
- [YouTube](#youtube)
1516
- [Twitch](#twitch)
1617

18+
## Media-over-QUIC
19+
20+
Add the target URL inside `dest` of a `forward` entry:
21+
22+
```yml
23+
paths:
24+
mypath:
25+
forward:
26+
- dest: moqt://user:pass@host:port/path
27+
# Transport protocol used to forward the stream. available values are "quic", "webtransport".
28+
moqTransport: quic
29+
# If the destination TLS certificate is self-signed or invalid, you can provide the
30+
# fingerprint of the certificate in order to validate it anyway. It can be obtained by running:
31+
# openssl s_client -connect dest_ip:dest_port </dev/null 2>/dev/null | sed -n '/BEGIN/,/END/p' > server.crt
32+
# openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':'
33+
destFingerprint:
34+
```
35+
1736
## SRT
1837
1938
Add the target URL inside `dest` of a `forward` entry:
@@ -35,14 +54,14 @@ paths:
3554
forward:
3655
# use whip:// for HTTP and whips:// for HTTPS.
3756
- dest: whip://host:port/mystream/whip
57+
# Token to insert in the Authorization: Bearer header.
58+
whipBearerToken: ""
3859
# If the destination is HTTPS and the destination TLS certificate is self-signed
3960
# or invalid, you can provide the fingerprint of the certificate in order to
4061
# validate it anyway. It can be obtained by running:
4162
# openssl s_client -connect dest_ip:dest_port </dev/null 2>/dev/null | sed -n '/BEGIN/,/END/p' > server.crt
4263
# openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':'
4364
destFingerprint:
44-
# Token to insert in the Authorization: Bearer header.
45-
whipBearerToken: ""
4665
```
4766

4867
If the remote server is a _MediaMTX_ instance, remember to add a `/whip` suffix after the stream name, since in _MediaMTX_ [it's part of the WHIP URL](../3-publish/05-webrtc-clients.md).

internal/api/api_forward_test.go

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func (m *testForwardPathManager) APIForwardDestGet(path string, id uuid.UUID) (*
5454

5555
func TestForward(t *testing.T) {
5656
rtmpID := uuid.New()
57+
moqID := uuid.New()
5758
whipID := uuid.New()
5859
pathManager := &testForwardPathManager{
5960
items: map[uuid.UUID]*defs.APIForwardDest{
@@ -67,9 +68,21 @@ func TestForward(t *testing.T) {
6768
LastError: "connection refused",
6869
OutboundBytes: 123,
6970
},
71+
moqID: {
72+
ID: moqID,
73+
Pos: 2,
74+
Created: time.Date(2026, 6, 18, 9, 0, 30, 0, time.UTC),
75+
Conf: conf.ForwardDest{
76+
Dest: "moqt://localhost/live/stream",
77+
MoQTransport: conf.MoQTransportWebTransport,
78+
},
79+
Protocol: defs.APIForwardDestProtocolMoQ,
80+
State: defs.APIForwardDestStateForwarding,
81+
OutboundBytes: 234,
82+
},
7083
whipID: {
7184
ID: whipID,
72-
Pos: 2,
85+
Pos: 3,
7386
Created: time.Date(2026, 6, 18, 9, 1, 0, 0, time.UTC),
7487
Conf: conf.ForwardDest{Dest: "whip://localhost/live/stream/whip", WHIPBearerToken: "mytoken"},
7588
Protocol: defs.APIForwardDestProtocolWHIP,
@@ -98,26 +111,45 @@ func TestForward(t *testing.T) {
98111
var list defs.APIForwardDestList
99112
httpRequest(t, hc, http.MethodGet,
100113
"http://localhost:9997/v3/paths/forward/list?path=my%2Fnested%2Fstream", nil, &list)
101-
require.Equal(t, 2, list.ItemCount)
114+
require.Equal(t, 3, list.ItemCount)
102115
require.Equal(t, 1, list.PageCount)
103-
require.Len(t, list.Items, 2)
116+
require.Len(t, list.Items, 3)
104117

105118
require.ElementsMatch(t, []defs.APIForwardDest{
106119
{
107-
ID: rtmpID,
108-
Pos: 1,
109-
Created: time.Date(2026, 6, 18, 9, 0, 0, 0, time.UTC),
110-
Conf: conf.ForwardDest{Dest: "rtmp://localhost/live/stream"},
120+
ID: rtmpID,
121+
Pos: 1,
122+
Created: time.Date(2026, 6, 18, 9, 0, 0, 0, time.UTC),
123+
Conf: conf.ForwardDest{
124+
Dest: "rtmp://localhost/live/stream",
125+
MoQTransport: conf.MoQTransportQUIC,
126+
},
111127
Protocol: defs.APIForwardDestProtocolRTMP,
112128
State: defs.APIForwardDestStateError,
113129
LastError: "connection refused",
114130
OutboundBytes: 123,
115131
},
116132
{
117-
ID: whipID,
118-
Pos: 2,
119-
Created: time.Date(2026, 6, 18, 9, 1, 0, 0, time.UTC),
120-
Conf: conf.ForwardDest{Dest: "whip://localhost/live/stream/whip", WHIPBearerToken: "mytoken"},
133+
ID: moqID,
134+
Pos: 2,
135+
Created: time.Date(2026, 6, 18, 9, 0, 30, 0, time.UTC),
136+
Conf: conf.ForwardDest{
137+
Dest: "moqt://localhost/live/stream",
138+
MoQTransport: conf.MoQTransportWebTransport,
139+
},
140+
Protocol: defs.APIForwardDestProtocolMoQ,
141+
State: defs.APIForwardDestStateForwarding,
142+
OutboundBytes: 234,
143+
},
144+
{
145+
ID: whipID,
146+
Pos: 3,
147+
Created: time.Date(2026, 6, 18, 9, 1, 0, 0, time.UTC),
148+
Conf: conf.ForwardDest{
149+
Dest: "whip://localhost/live/stream/whip",
150+
WHIPBearerToken: "mytoken",
151+
MoQTransport: conf.MoQTransportQUIC,
152+
},
121153
Protocol: defs.APIForwardDestProtocolWHIP,
122154
State: defs.APIForwardDestStateForwarding,
123155
OutboundBytes: 456,
@@ -126,10 +158,10 @@ func TestForward(t *testing.T) {
126158

127159
var item defs.APIForwardDest
128160
httpRequest(t, hc, http.MethodGet,
129-
"http://localhost:9997/v3/paths/forward/get?path=my%2Fnested%2Fstream&id="+whipID.String(), nil, &item)
130-
require.Equal(t, "whip://localhost/live/stream/whip", item.Conf.Dest)
131-
require.Equal(t, "mytoken", item.Conf.WHIPBearerToken)
132-
require.Equal(t, defs.APIForwardDestProtocolWHIP, item.Protocol)
161+
"http://localhost:9997/v3/paths/forward/get?path=my%2Fnested%2Fstream&id="+moqID.String(), nil, &item)
162+
require.Equal(t, "moqt://localhost/live/stream", item.Conf.Dest)
163+
require.Equal(t, conf.MoQTransportWebTransport, item.Conf.MoQTransport)
164+
require.Equal(t, defs.APIForwardDestProtocolMoQ, item.Protocol)
133165
require.Equal(t, defs.APIForwardDestStateForwarding, item.State)
134-
require.Equal(t, uint64(456), item.OutboundBytes)
166+
require.Equal(t, uint64(234), item.OutboundBytes)
135167
}

internal/conf/conf_test.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,16 @@ func TestConfErrors(t *testing.T) {
904904
" source: rtsp://user@localhost/stream\n",
905905
"username and password must be both provided",
906906
},
907+
{
908+
"valid moq forward destination",
909+
"paths:\n" +
910+
" mypath:\n" +
911+
" forward:\n" +
912+
" - dest: moqt://localhost/stream\n" +
913+
" destFingerprint: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n" +
914+
" moqTransport: webtransport\n",
915+
"",
916+
},
907917
{
908918
"valid whip forward destination",
909919
"paths:\n" +
@@ -921,7 +931,7 @@ func TestConfErrors(t *testing.T) {
921931
" forward:\n" +
922932
" - dest: http://localhost/stream\n",
923933
"invalid 'forward': entry 0: unsupported scheme 'http', supported ones are " +
924-
"rtmp, rtmps, rtsp, rtsps, srt, whip and whips",
934+
"rtmp, rtmps, rtsp, rtsps, srt, moqt, whip and whips",
925935
},
926936
} {
927937
t.Run(ca.name, func(t *testing.T) {

internal/conf/forward_dest.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import (
88

99
// ForwardDest is a destination to which a path is forwarded.
1010
type ForwardDest struct {
11-
Dest string `json:"dest"`
12-
DestFingerprint string `json:"destFingerprint"`
13-
WHIPBearerToken string `json:"whipBearerToken"`
11+
Dest string `json:"dest"`
12+
DestFingerprint string `json:"destFingerprint"`
13+
MoQTransport MoQTransport `json:"moqTransport"`
14+
WHIPBearerToken string `json:"whipBearerToken"`
1415
}
1516

1617
func validateForwardDest(dest string) (*url.URL, error) {
@@ -31,10 +32,10 @@ func (p *ForwardDest) Validate() error {
3132
}
3233

3334
switch u.Scheme {
34-
case "rtmp", "rtmps", "rtsp", "rtsps", "srt", "whip", "whips":
35+
case "rtmp", "rtmps", "rtsp", "rtsps", "srt", "moqt", "whip", "whips":
3536
default:
3637
return fmt.Errorf(
37-
"unsupported scheme '%s', supported ones are rtmp, rtmps, rtsp, rtsps, srt, whip and whips",
38+
"unsupported scheme '%s', supported ones are rtmp, rtmps, rtsp, rtsps, srt, moqt, whip and whips",
3839
u.Scheme)
3940
}
4041

internal/conf/moq_transport.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ func (d *MoQTransport) UnmarshalJSON(b []byte) error {
2323
}
2424

2525
switch *d {
26+
case "":
27+
*d = MoQTransportQUIC
28+
2629
case MoQTransportQUIC, MoQTransportWebTransport:
2730

2831
default:

0 commit comments

Comments
 (0)