From ca6e1259fb61d7a116d840617d9f1cf13ce58562 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 2 Jun 2024 23:08:54 +0200 Subject: [PATCH] webrtc: support reading and proxying stereo PCMU/PCMA tracks (#3402) --- go.mod | 10 +- go.sum | 19 ++-- internal/protocols/webrtc/incoming_track.go | 86 +++++++++++++--- internal/protocols/webrtc/outgoing_track.go | 38 ++++++- internal/servers/webrtc/publish_index.html | 14 ++- internal/servers/webrtc/read_index.html | 104 ++++++++++++++++++-- internal/servers/webrtc/server_test.go | 2 +- internal/servers/webrtc/session.go | 12 --- 8 files changed, 221 insertions(+), 64 deletions(-) diff --git a/go.mod b/go.mod index 211b046f..abb7e09b 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/gorilla/websocket v1.5.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/matthewhartstonge/argon2 v1.0.0 - github.com/pion/ice/v2 v2.3.11 + github.com/pion/ice/v2 v2.3.24 github.com/pion/interceptor v0.1.29 github.com/pion/logging v0.2.2 github.com/pion/rtcp v1.2.14 @@ -58,12 +58,12 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pion/datachannel v1.5.5 // indirect github.com/pion/dtls/v2 v2.2.7 // indirect - github.com/pion/mdns v0.0.9 // indirect + github.com/pion/mdns v0.0.12 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/sctp v1.8.8 // indirect + github.com/pion/sctp v1.8.16 // indirect github.com/pion/srtp/v2 v2.0.18 // indirect github.com/pion/stun v0.6.1 // indirect - github.com/pion/transport/v2 v2.2.3 // indirect + github.com/pion/transport/v2 v2.2.4 // indirect github.com/pion/turn/v2 v2.1.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect @@ -81,4 +81,4 @@ replace code.cloudfoundry.org/bytefmt => github.com/cloudfoundry/bytefmt v0.0.0- replace github.com/pion/ice/v2 => github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1 -replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6 +replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20240527213244-d3ff0bd9230f diff --git a/go.sum b/go.sum index 8ef3b0a0..4f5183ca 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1 h1:fD6eZt+3/t8bzFn6ZZA2eP63xBP06v3EPfPJu8DO8ys= github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1/go.mod h1:lT3kv5uUIlHfXHU/ZRD7uKD/ufM202+eTa3C/umgGf4= -github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6 h1:wMd3D1mLghoYYh31STig8Kwm2qi8QyQKUy09qUUZrVw= -github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs= +github.com/aler9/webrtc/v3 v3.0.0-20240527213244-d3ff0bd9230f h1:IcGmhy+mh/cDCHR8BBDI8iUGeV2/O1A/qqoR3yi62Z0= +github.com/aler9/webrtc/v3 v3.0.0-20240527213244-d3ff0bd9230f/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs= github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA= github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= @@ -143,8 +143,8 @@ github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/ github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8= -github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4= -github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc= +github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= +github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= @@ -156,8 +156,9 @@ github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0 h1:yPAphilskTN7U3URvBVxlVr0PzheMeWqo7PaOqh//Hg= github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= -github.com/pion/sctp v1.8.8 h1:5EdnnKI4gpyR1a1TwbiS/wxEgcUWBHsc7ILAjARJB+U= github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs= +github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY= +github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE= github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= @@ -169,8 +170,9 @@ github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40 github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= -github.com/pion/transport/v2 v2.2.3 h1:XcOE3/x41HOSKbl1BfyY1TF1dERx7lVvlMCbXU7kfvA= github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo= +github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4= github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0= @@ -215,7 +217,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -236,7 +237,6 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -268,7 +268,6 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -280,7 +279,6 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -293,7 +291,6 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= diff --git a/internal/protocols/webrtc/incoming_track.go b/internal/protocols/webrtc/incoming_track.go index 26986340..9146e808 100644 --- a/internal/protocols/webrtc/incoming_track.go +++ b/internal/protocols/webrtc/incoming_track.go @@ -22,16 +22,16 @@ const ( var incomingVideoCodecs = []webrtc.RTPCodecParameters{ { RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypeAV1, - ClockRate: 90000, + MimeType: webrtc.MimeTypeAV1, + ClockRate: 90000, + SDPFmtpLine: "profile-id=1", }, PayloadType: 96, }, { RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypeVP9, - ClockRate: 90000, - SDPFmtpLine: "profile-id=0", + MimeType: webrtc.MimeTypeAV1, + ClockRate: 90000, }, PayloadType: 97, }, @@ -39,16 +39,40 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypeVP9, ClockRate: 90000, - SDPFmtpLine: "profile-id=1", + SDPFmtpLine: "profile-id=3", }, PayloadType: 98, }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypeVP9, + ClockRate: 90000, + SDPFmtpLine: "profile-id=2", + }, + PayloadType: 99, + }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypeVP9, + ClockRate: 90000, + SDPFmtpLine: "profile-id=1", + }, + PayloadType: 100, + }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypeVP9, + ClockRate: 90000, + SDPFmtpLine: "profile-id=0", + }, + PayloadType: 101, + }, { RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypeVP8, ClockRate: 90000, }, - PayloadType: 99, + PayloadType: 102, }, { RTPCodecCapability: webrtc.RTPCodecCapability{ @@ -56,7 +80,7 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{ ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", }, - PayloadType: 100, + PayloadType: 103, }, { RTPCodecCapability: webrtc.RTPCodecCapability{ @@ -64,7 +88,7 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{ ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", }, - PayloadType: 101, + PayloadType: 104, }, } @@ -85,6 +109,22 @@ var incomingAudioCodecs = []webrtc.RTPCodecParameters{ }, PayloadType: 9, }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypePCMU, + ClockRate: 8000, + Channels: 2, + }, + PayloadType: 118, + }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypePCMA, + ClockRate: 8000, + Channels: 2, + }, + PayloadType: 119, + }, { RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypePCMU, @@ -166,19 +206,39 @@ func newIncomingTrack( t.format = &format.G722{} case strings.ToLower(webrtc.MimeTypePCMU): + channels := track.Codec().Channels + if channels == 0 { + channels = 1 + } + + payloadType := uint8(0) + if channels > 1 { + payloadType = 118 + } + t.format = &format.G711{ - PayloadTyp: 0, + PayloadTyp: payloadType, MULaw: true, SampleRate: 8000, - ChannelCount: 1, + ChannelCount: int(channels), } case strings.ToLower(webrtc.MimeTypePCMA): + channels := track.Codec().Channels + if channels == 0 { + channels = 1 + } + + payloadType := uint8(8) + if channels > 1 { + payloadType = 119 + } + t.format = &format.G711{ - PayloadTyp: 8, + PayloadTyp: payloadType, MULaw: false, SampleRate: 8000, - ChannelCount: 1, + ChannelCount: int(channels), } default: diff --git a/internal/protocols/webrtc/outgoing_track.go b/internal/protocols/webrtc/outgoing_track.go index f02c66fe..9c67d0d7 100644 --- a/internal/protocols/webrtc/outgoing_track.go +++ b/internal/protocols/webrtc/outgoing_track.go @@ -33,7 +33,7 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { ClockRate: 90000, SDPFmtpLine: "profile-id=1", }, - PayloadType: 98, + PayloadType: 96, }, nil case *format.VP8: @@ -42,7 +42,7 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { MimeType: webrtc.MimeTypeVP8, ClockRate: 90000, }, - PayloadType: 99, + PayloadType: 96, }, nil case *format.H264: @@ -52,17 +52,21 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", }, - PayloadType: 101, + PayloadType: 96, }, nil case *format.Opus: + if forma.ChannelCount > 2 { + return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported Opus channel count: %d", forma.ChannelCount) + } + return webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 2, }, - PayloadType: 111, + PayloadType: 96, }, nil case *format.G722: @@ -75,7 +79,22 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { }, nil case *format.G711: + if forma.SampleRate != 8000 { + return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported G711 sample rate") + } + if forma.MULaw { + if forma.ChannelCount != 1 { + return webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypePCMU, + ClockRate: uint32(forma.SampleRate), + Channels: uint16(forma.ChannelCount), + }, + PayloadType: 96, + }, nil + } + return webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypePCMU, @@ -85,6 +104,17 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { }, nil } + if forma.ChannelCount != 1 { + return webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypePCMA, + ClockRate: uint32(forma.SampleRate), + Channels: uint16(forma.ChannelCount), + }, + PayloadType: 96, + }, nil + } + return webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypePCMA, diff --git a/internal/servers/webrtc/publish_index.html b/internal/servers/webrtc/publish_index.html index 8176a664..21e1fbe3 100644 --- a/internal/servers/webrtc/publish_index.html +++ b/internal/servers/webrtc/publish_index.html @@ -376,11 +376,10 @@ const editOffer = (sdp) => { const sections = sdp.split('m='); for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - if (section.startsWith('video')) { - sections[i] = setCodec(section, videoForm.codec.value); - } else if (section.startsWith('audio')) { - sections[i] = setAudioBitrate(setCodec(section, audioForm.codec.value), audioForm.bitrate.value, audioForm.voice.checked); + if (sections[i].startsWith('video')) { + sections[i] = setCodec(sections[i], videoForm.codec.value); + } else if (sections[i].startsWith('audio')) { + sections[i] = setAudioBitrate(setCodec(sections[i], audioForm.codec.value), audioForm.bitrate.value, audioForm.voice.checked); } } @@ -391,9 +390,8 @@ const editAnswer = (sdp) => { const sections = sdp.split('m='); for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - if (section.startsWith('video')) { - sections[i] = setVideoBitrate(section, videoForm.bitrate.value); + if (sections[i].startsWith('video')) { + sections[i] = setVideoBitrate(sections[i], videoForm.bitrate.value); } } diff --git a/internal/servers/webrtc/read_index.html b/internal/servers/webrtc/read_index.html index 78a2026c..c42e8267 100644 --- a/internal/servers/webrtc/read_index.html +++ b/internal/servers/webrtc/read_index.html @@ -50,6 +50,7 @@ const retryPause = 2000; const video = document.getElementById('video'); const message = document.getElementById('message'); +let nonAdvertisedCodecs = []; let pc = null; let restartTimeout = null; let sessionUrl = ''; @@ -87,14 +88,14 @@ const linkToIceServers = (links) => ( }) : [] ); -const parseOffer = (offer) => { +const parseOffer = (sdp) => { const ret = { iceUfrag: '', icePwd: '', medias: [], }; - for (const line of offer.split('\r\n')) { + for (const line of sdp.split('\r\n')) { if (line.startsWith('m=')) { ret.medias.push(line.slice('m='.length)); } else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) { @@ -107,6 +108,20 @@ const parseOffer = (offer) => { return ret; }; +const enableStereoPcmau = (section) => { + let lines = section.split('\r\n'); + + lines[0] += ' 118'; + lines.splice(lines.length - 1, 0, 'a=rtpmap:118 PCMU/8000/2'); + lines.splice(lines.length - 1, 0, 'a=rtcp-fb:118 transport-cc'); + + lines[0] += ' 119'; + lines.splice(lines.length - 1, 0, 'a=rtpmap:119 PCMA/8000/2'); + lines.splice(lines.length - 1, 0, 'a=rtcp-fb:119 transport-cc'); + + return lines.join('\r\n'); +}; + const enableStereoOpus = (section) => { let opusPayloadFormat = ''; let lines = section.split('\r\n'); @@ -136,17 +151,22 @@ const enableStereoOpus = (section) => { return lines.join('\r\n'); }; -const editOffer = (offer) => { - const sections = offer.sdp.split('m='); +const editOffer = (sdp) => { + const sections = sdp.split('m='); for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - if (section.startsWith('audio')) { - sections[i] = enableStereoOpus(section); + if (sections[i].startsWith('audio')) { + sections[i] = enableStereoOpus(sections[i]); + + if (nonAdvertisedCodecs.includes('pcma/8000/2')) { + sections[i] = enableStereoPcmau(sections[i]); + } + + break; } } - offer.sdp = sections.join('m='); + return sections.join('m='); }; const generateSdpFragment = (od, candidates) => { @@ -183,6 +203,70 @@ const loadStream = () => { requestICEServers(); }; +const supportsNonAdvertisedCodec = (codec, fmtp) => ( + new Promise((resolve, reject) => { + const pc = new RTCPeerConnection({ iceServers: [] }); + pc.addTransceiver('audio', { direction: 'recvonly' }); + pc.createOffer() + .then((offer) => { + if (offer.sdp.includes(' ' + codec)) { // codec is advertised, there's no need to add it manually + resolve(false); + return; + } + const sections = offer.sdp.split('m=audio'); + const lines = sections[1].split('\r\n'); + lines[0] += ' 118'; + lines.splice(lines.length - 1, 0, 'a=rtpmap:118 ' + codec); + if (fmtp !== undefined) { + lines.splice(lines.length - 1, 0, 'a=fmtp:118 ' + fmtp); + } + sections[1] = lines.join('\r\n'); + offer.sdp = sections.join('m=audio'); + return pc.setLocalDescription(offer); + }) + .then(() => { + return pc.setRemoteDescription(new RTCSessionDescription({ + type: 'answer', + sdp: 'v=0\r\n' + + 'o=- 6539324223450680508 0 IN IP4 0.0.0.0\r\n' + + 's=-\r\n' + + 't=0 0\r\n' + + 'a=fingerprint:sha-256 0D:9F:78:15:42:B5:4B:E6:E2:94:3E:5B:37:78:E1:4B:54:59:A3:36:3A:E5:05:EB:27:EE:8F:D2:2D:41:29:25\r\n' + + 'm=audio 9 UDP/TLS/RTP/SAVPF 118\r\n' + + 'c=IN IP4 0.0.0.0\r\n' + + 'a=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\r\n' + + 'a=ice-ufrag:29e036dc\r\n' + + 'a=sendonly\r\n' + + 'a=rtcp-mux\r\n' + + 'a=rtpmap:118 ' + codec + '\r\n' + + ((fmtp !== undefined) ? 'a=fmtp:118 ' + fmtp + '\r\n' : ''), + })); + }) + .then(() => { + resolve(true); + }) + .catch((err) => { + resolve(false); + }) + .finally(() => { + pc.close(); + }); + }) +); + +const getNonAdvertisedCodecs = () => { + Promise.all([ + ['pcma/8000/2'], + ['multiopus/48000/6', 'channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2'], + ['L16/48000/2'] + ].map((c) => supportsNonAdvertisedCodec(c[0], c[1]).then((r) => (r) ? c[0] : false))) + .then((c) => c.filter((e) => e !== false)) + .then((codecs) => { + nonAdvertisedCodecs = codecs; + loadStream(); + }); +}; + const onError = (err) => { if (restartTimeout === null) { setMessage(err + ', retrying in some seconds'); @@ -295,7 +379,7 @@ const sendOffer = (offer) => { const createOffer = () => { pc.createOffer() .then((offer) => { - editOffer(offer); + offer.sdp = editOffer(offer.sdp); offerData = parseOffer(offer.sdp); pc.setLocalDescription(offer); sendOffer(offer); @@ -366,7 +450,7 @@ const loadAttributesFromQuery = () => { const init = () => { loadAttributesFromQuery(); - loadStream(); + getNonAdvertisedCodecs(); }; window.addEventListener('DOMContentLoaded', init); diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index 38727792..b87e63c0 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -423,7 +423,7 @@ func TestServerRead(t *testing.T) { Header: rtp.Header{ Version: 2, Marker: true, - PayloadType: 101, + PayloadType: 104, SequenceNumber: pkt.SequenceNumber, Timestamp: pkt.Timestamp, SSRC: pkt.SSRC, diff --git a/internal/servers/webrtc/session.go b/internal/servers/webrtc/session.go index e921a4b0..4d582582 100644 --- a/internal/servers/webrtc/session.go +++ b/internal/servers/webrtc/session.go @@ -216,10 +216,6 @@ func findAudioTrack( if opusFormat != nil { return opusFormat, func(track *webrtc.OutgoingTrack) error { - if opusFormat.ChannelCount > 2 { - return fmt.Errorf("unsupported Opus channel count: %d", opusFormat.ChannelCount) - } - stream.AddReader(writer, media, opusFormat, func(u unit.Unit) error { for _, pkt := range u.GetRTPPackets() { track.WriteRTP(pkt) //nolint:errcheck @@ -252,14 +248,6 @@ func findAudioTrack( if g711Format != nil { return g711Format, func(track *webrtc.OutgoingTrack) error { - if g711Format.SampleRate != 8000 { - return fmt.Errorf("unsupported G711 sample rate") - } - - if g711Format.ChannelCount != 1 { - return fmt.Errorf("unsupported G711 channel count") - } - stream.AddReader(writer, media, g711Format, func(u unit.Unit) error { for _, pkt := range u.GetRTPPackets() { track.WriteRTP(pkt) //nolint:errcheck