webrtc: move codec and bitrate settings on client side (#1990)

This commit is contained in:
Alessandro Ros 2023-06-27 22:37:06 +02:00 committed by GitHub
parent 79ee4e06f3
commit 20a3b07d0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 224 additions and 256 deletions

View File

@ -297,15 +297,10 @@ func (s *webRTCHTTPServer) onRequest(ctx *gin.Context) {
}
res := s.parent.sessionNew(webRTCSessionNewReq{
pathName: dir,
remoteAddr: ctx.ClientIP(),
offer: offer,
publish: (fname == "whip"),
videoCodec: ctx.Query("video_codec"),
audioCodec: ctx.Query("audio_codec"),
videoBitrate: ctx.Query("video_bitrate"),
audioBitrate: ctx.Query("audio_bitrate"),
audioVoice: ctx.Query("audio_voice") == "true",
pathName: dir,
remoteAddr: ctx.ClientIP(),
offer: offer,
publish: (fname == "whip"),
})
if res.err != nil {
if res.errStatusCode != 0 {

View File

@ -130,16 +130,11 @@ type webRTCSessionNewRes struct {
}
type webRTCSessionNewReq struct {
pathName string
remoteAddr string
offer []byte
publish bool
videoCodec string
audioCodec string
videoBitrate string
audioBitrate string
audioVoice bool
res chan webRTCSessionNewRes
pathName string
remoteAddr string
offer []byte
publish bool
res chan webRTCSessionNewRes
}
type webRTCSessionAddCandidatesRes struct {

View File

@ -11,51 +11,57 @@ import (
"github.com/bluenviron/mediamtx/internal/logger"
)
var videoCodecs = map[string][]webrtc.RTPCodecParameters{
"av1": {{
var videoCodecs = []webrtc.RTPCodecParameters{
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeAV1,
ClockRate: 90000,
},
PayloadType: 96,
}},
"vp9": {
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP9,
ClockRate: 90000,
SDPFmtpLine: "profile-id=0",
},
PayloadType: 96,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP9,
ClockRate: 90000,
SDPFmtpLine: "profile-id=1",
},
PayloadType: 96,
},
},
"vp8": {{
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP9,
ClockRate: 90000,
SDPFmtpLine: "profile-id=0",
},
PayloadType: 97,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP9,
ClockRate: 90000,
SDPFmtpLine: "profile-id=1",
},
PayloadType: 98,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
},
PayloadType: 96,
}},
"h264": {{
PayloadType: 99,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264,
ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
},
PayloadType: 96,
}},
PayloadType: 100,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264,
ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f",
},
PayloadType: 101,
},
}
var audioCodecs = map[string][]webrtc.RTPCodecParameters{
"opus": {{
var audioCodecs = []webrtc.RTPCodecParameters{
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeOpus,
ClockRate: 48000,
@ -63,28 +69,28 @@ var audioCodecs = map[string][]webrtc.RTPCodecParameters{
SDPFmtpLine: "minptime=10;useinbandfec=1",
},
PayloadType: 111,
}},
"g722": {{
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeG722,
ClockRate: 8000,
},
PayloadType: 9,
}},
"pcmu": {{
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMU,
ClockRate: 8000,
},
PayloadType: 0,
}},
"pcma": {{
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypePCMA,
ClockRate: 8000,
},
PayloadType: 8,
}},
},
}
type peerConnection struct {
@ -98,8 +104,6 @@ type peerConnection struct {
}
func newPeerConnection(
videoCodec string,
audioCodec string,
iceServers []webrtc.ICEServer,
iceHostNAT1To1IPs []string,
iceUDPMux ice.UDPMux,
@ -124,43 +128,17 @@ func newPeerConnection(
mediaEngine := &webrtc.MediaEngine{}
if videoCodec != "" || audioCodec != "" {
codec, ok := videoCodecs[videoCodec]
if ok {
for _, params := range codec {
err := mediaEngine.RegisterCodec(params, webrtc.RTPCodecTypeVideo)
if err != nil {
return nil, err
}
}
for _, codec := range videoCodecs {
err := mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeVideo)
if err != nil {
return nil, err
}
}
codec, ok = audioCodecs[audioCodec]
if ok {
for _, params := range codec {
err := mediaEngine.RegisterCodec(params, webrtc.RTPCodecTypeAudio)
if err != nil {
return nil, err
}
}
}
} else { // register all codecs
for _, codec := range videoCodecs {
for _, params := range codec {
err := mediaEngine.RegisterCodec(params, webrtc.RTPCodecTypeVideo)
if err != nil {
return nil, err
}
}
}
for _, codec := range audioCodecs {
for _, params := range codec {
err := mediaEngine.RegisterCodec(params, webrtc.RTPCodecTypeAudio)
if err != nil {
return nil, err
}
}
for _, codec := range audioCodecs {
err := mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeAudio)
if err != nil {
return nil, err
}
}

View File

@ -183,7 +183,100 @@ const generateSdpFragment = (offerData, candidates) => {
}
return frag;
}
};
const setCodec = (section, codec) => {
const lines = section.split('\r\n');
const lines2 = [];
const payloadFormats = [];
for (const line of lines) {
if (!line.startsWith('a=rtpmap:')) {
lines2.push(line);
} else {
if (line.toLowerCase().includes(codec)) {
payloadFormats.push(line.slice('a=rtpmap:'.length).split(' ')[0]);
lines2.push(line);
}
}
}
const lines3 = [];
for (const line of lines2) {
if (line.startsWith('a=fmtp:')) {
if (payloadFormats.includes(line.slice('a=fmtp:'.length).split(' ')[0])) {
lines3.push(line);
}
} else if (line.startsWith('a=rtcp-fb:')) {
if (payloadFormats.includes(line.slice('a=rtcp-fb:'.length).split(' ')[0])) {
lines3.push(line);
}
} else {
lines3.push(line);
}
}
return lines3.join('\r\n');
};
const setVideoBitrate = (section, bitrate) => {
let lines = section.split('\r\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('c=')) {
lines = [...lines.slice(0, i+1), 'b=TIAS:' + (parseInt(bitrate) * 1024).toString(), ...lines.slice(i+1)];
break
}
}
return lines.join('\r\n');
};
const setAudioBitrate = (section, bitrate, voice) => {
let opusPayloadFormat = '';
let lines = section.split('\r\n');
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('a=rtpmap:') && lines[i].toLowerCase().includes('opus/')) {
opusPayloadFormat = lines[i].slice('a=rtpmap:'.length).split(' ')[0];
break;
}
}
if (opusPayloadFormat === '') {
return section;
}
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('a=fmtp:' + opusPayloadFormat + ' ')) {
if (voice) {
lines[i] = 'a=fmtp:' + opusPayloadFormat + ' minptime=10;useinbandfec=1;maxaveragebitrate='
+ (parseInt(bitrate) * 1024).toString();
} else {
lines[i] = 'a=fmtp:' + opusPayloadFormat + ' maxplaybackrate=48000;stereo=1;sprop-stereo=1;maxaveragebitrate'
+ (parseInt(bitrate) * 1024).toString();
}
}
}
return lines.join('\r\n');
};
const editAnswer = (answer, videoCodec, audioCodec, videoBitrate, audioBitrate, audioVoice) => {
const sections = answer.split('m=');
for (let i = 0; i < sections.length; i++) {
const section = sections[i];
if (section.startsWith('video')) {
sections[i] = setVideoBitrate(setCodec(section, videoCodec), videoBitrate);
} else if (section.startsWith('audio')) {
sections[i] = setAudioBitrate(setCodec(section, audioCodec), audioBitrate, audioVoice);
}
}
return sections.join('m=');
};
class Transmitter {
constructor(stream) {
@ -221,47 +314,36 @@ class Transmitter {
});
this.pc.createOffer()
.then((desc) => {
this.offerData = parseOffer(desc.sdp);
this.pc.setLocalDescription(desc);
.then((offer) => this.onLocalOffer(offer));
}
console.log("sending offer");
onLocalOffer(offer) {
this.offerData = parseOffer(offer.sdp);
this.pc.setLocalDescription(offer);
const videoCodec = document.getElementById('video_codec').value;
const audioCodec = document.getElementById('audio_codec').value;
const videoBitrate = document.getElementById('video_bitrate').value;
const audioBitrate = document.getElementById('audio_bitrate').value;
const audioVoice = document.getElementById('audio_voice').checked;
console.log("sending offer");
const p = new URLSearchParams(window.location.search);
p.set('video_codec', videoCodec);
p.set('audio_codec', audioCodec);
p.set('video_bitrate', videoBitrate);
p.set('audio_bitrate', audioBitrate);
p.set('audio_voice', audioVoice ? 'true' : 'false');
fetch(new URL('whip', window.location.href) + '?' + p.toString(), {
method: 'POST',
headers: {
'Content-Type': 'application/sdp',
},
body: desc.sdp,
})
.then((res) => {
if (res.status !== 201) {
throw new Error('bad status code');
}
this.eTag = res.headers.get('E-Tag');
return res.text();
})
.then((sdp) => this.onRemoteDescription(new RTCSessionDescription({
type: 'answer',
sdp,
})))
.catch((err) => {
console.log('error: ' + err);
this.scheduleRestart();
});
fetch(new URL('whip', window.location.href) + window.location.search, {
method: 'POST',
headers: {
'Content-Type': 'application/sdp',
},
body: offer.sdp,
})
.then((res) => {
if (res.status !== 201) {
throw new Error('bad status code');
}
this.eTag = res.headers.get('E-Tag');
return res.text();
})
.then((sdp) => this.onRemoteAnswer(new RTCSessionDescription({
type: 'answer',
sdp,
})))
.catch((err) => {
console.log('error: ' + err);
this.scheduleRestart();
});
}
@ -278,11 +360,23 @@ class Transmitter {
}
}
onRemoteDescription(answer) {
onRemoteAnswer(answer) {
if (this.restartTimeout !== null) {
return;
}
answer = new RTCSessionDescription({
type: 'answer',
sdp: editAnswer(
answer.sdp,
document.getElementById('video_codec').value,
document.getElementById('audio_codec').value,
document.getElementById('video_bitrate').value,
document.getElementById('audio_bitrate').value,
document.getElementById('audio_voice').value,
),
});
this.pc.setRemoteDescription(new RTCSessionDescription(answer));
if (this.queuedCandidates.length !== 0) {
@ -445,7 +539,7 @@ const populateCodecs = () => {
for (const codec of ['av1/90000', 'vp9/90000', 'vp8/90000', 'h264/90000']) {
if (sdp.includes(codec)) {
const opt = document.createElement('option');
opt.value = codec.split('/')[0];
opt.value = codec;
opt.text = codec.split('/')[0].toUpperCase();
document.getElementById('video_codec').appendChild(opt);
}
@ -454,7 +548,7 @@ const populateCodecs = () => {
for (const codec of ['opus/48000', 'g722/8000', 'pcmu/8000', 'pcma/8000']) {
if (sdp.includes(codec)) {
const opt = document.createElement('option');
opt.value = codec.split('/')[0];
opt.value = codec;
opt.text = codec.split('/')[0].toUpperCase();
document.getElementById('audio_codec').appendChild(opt);
}

View File

@ -132,34 +132,36 @@ class WHEPClient {
};
this.pc.createOffer()
.then((desc) => {
this.offerData = parseOffer(desc.sdp);
this.pc.setLocalDescription(desc);
.then((offer) => this.onLocalOffer(offer));
}
console.log("sending offer");
onLocalOffer(offer) {
this.offerData = parseOffer(offer.sdp);
this.pc.setLocalDescription(offer);
fetch(new URL('whep', window.location.href) + window.location.search, {
method: 'POST',
headers: {
'Content-Type': 'application/sdp',
},
body: desc.sdp,
})
.then((res) => {
if (res.status !== 201) {
throw new Error('bad status code');
}
this.eTag = res.headers.get('E-Tag');
return res.text();
})
.then((sdp) => this.onRemoteDescription(new RTCSessionDescription({
type: 'answer',
sdp,
})))
.catch((err) => {
console.log('error: ' + err);
this.scheduleRestart();
});
console.log("sending offer");
fetch(new URL('whep', window.location.href) + window.location.search, {
method: 'POST',
headers: {
'Content-Type': 'application/sdp',
},
body: offer.sdp,
})
.then((res) => {
if (res.status !== 201) {
throw new Error('bad status code');
}
this.eTag = res.headers.get('E-Tag');
return res.text();
})
.then((sdp) => this.onRemoteAnswer(new RTCSessionDescription({
type: 'answer',
sdp,
})))
.catch((err) => {
console.log('error: ' + err);
this.scheduleRestart();
});
}
@ -176,7 +178,7 @@ class WHEPClient {
}
}
onRemoteDescription(answer) {
onRemoteAnswer(answer) {
if (this.restartTimeout !== null) {
return;
}

View File

@ -5,7 +5,6 @@ import (
"encoding/hex"
"fmt"
"net/http"
"strconv"
"strings"
"sync"
"time"
@ -14,7 +13,6 @@ import (
"github.com/bluenviron/gortsplib/v3/pkg/ringbuffer"
"github.com/google/uuid"
"github.com/pion/ice/v2"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v3"
"github.com/bluenviron/mediamtx/internal/logger"
@ -48,91 +46,6 @@ func mediasOfIncomingTracks(tracks []*webRTCIncomingTrack) media.Medias {
return ret
}
func findOpusPayloadFormat(attributes []sdp.Attribute) int {
for _, attr := range attributes {
if attr.Key == "rtpmap" && strings.Contains(attr.Value, "opus/") {
parts := strings.SplitN(attr.Value, " ", 2)
pl, err := strconv.ParseUint(parts[0], 10, 31)
if err == nil {
return int(pl)
}
}
}
return 0
}
func editAnswer(
offer *webrtc.SessionDescription,
videoBitrateStr string,
audioBitrateStr string,
audioVoice bool,
) error {
var sd sdp.SessionDescription
err := sd.Unmarshal([]byte(offer.SDP))
if err != nil {
return err
}
if videoBitrateStr != "" {
videoBitrate, err := strconv.ParseUint(videoBitrateStr, 10, 31)
if err != nil {
return err
}
for _, media := range sd.MediaDescriptions {
if media.MediaName.Media == "video" {
media.Bandwidth = []sdp.Bandwidth{{
Type: "TIAS",
Bandwidth: videoBitrate * 1024,
}}
break
}
}
}
if audioBitrateStr != "" {
audioBitrate, err := strconv.ParseUint(audioBitrateStr, 10, 31)
if err != nil {
return err
}
for _, media := range sd.MediaDescriptions {
if media.MediaName.Media == "audio" {
pl := findOpusPayloadFormat(media.Attributes)
if pl != 0 {
for i, attr := range media.Attributes {
if attr.Key == "fmtp" && strings.HasPrefix(attr.Value, strconv.FormatInt(int64(pl), 10)+" ") {
if audioVoice {
media.Attributes[i] = sdp.Attribute{
Key: "fmtp",
Value: strconv.FormatInt(int64(pl), 10) + " minptime=10;useinbandfec=1;maxaveragebitrate=" +
strconv.FormatUint(audioBitrate*1024, 10),
}
} else {
media.Attributes[i] = sdp.Attribute{
Key: "fmtp",
Value: strconv.FormatInt(int64(pl), 10) + " stereo=1;sprop-stereo=1;maxaveragebitrate=" +
strconv.FormatUint(audioBitrate*1024, 10),
}
}
}
}
}
break
}
}
}
enc, err := sd.Marshal()
if err != nil {
return err
}
offer.SDP = string(enc)
return nil
}
func gatherOutgoingTracks(medias media.Medias) ([]*webRTCOutgoingTrack, error) {
var tracks []*webRTCOutgoingTrack
@ -322,8 +235,6 @@ func (s *webRTCSession) runPublish() (int, error) {
defer res.path.publisherRemove(pathPublisherRemoveReq{author: s})
pc, err := newPeerConnection(
s.req.videoCodec,
s.req.audioCodec,
s.parent.genICEServers(),
s.iceHostNAT1To1IPs,
s.iceUDPMux,
@ -381,11 +292,6 @@ func (s *webRTCSession) runPublish() (int, error) {
tmp := pc.LocalDescription()
answer = *tmp
err = editAnswer(&answer, s.req.videoBitrate, s.req.audioBitrate, s.req.audioVoice)
if err != nil {
return http.StatusBadRequest, err
}
err = s.writeAnswer(&answer)
if err != nil {
return http.StatusBadRequest, err
@ -451,8 +357,6 @@ func (s *webRTCSession) runRead() (int, error) {
}
pc, err := newPeerConnection(
"",
"",
s.parent.genICEServers(),
s.iceHostNAT1To1IPs,
s.iceUDPMux,