1
Fork 0
mirror of https://github.com/jech/galene.git synced 2024-11-22 08:35:57 +01:00

Simulcast.

This commit is contained in:
Juliusz Chroboczek 2021-04-29 17:03:25 +02:00
parent f1a15f07db
commit 795a40ceaf
11 changed files with 218 additions and 112 deletions

View file

@ -135,8 +135,17 @@ A peer must explicitly request the streams that it wants to receive.
``` ```
The field `request` is a dictionary that maps the labels of requested The field `request` is a dictionary that maps the labels of requested
streams to a list containing either 'audio', 'video' or both. An entry streams to a list containing either 'audio', or one of 'video' or
with an empty key `''` serves as default. 'video-low'. The empty key `''` serves as default. For example:
```javascript
{
type: 'request',
request: {
camera: ['audio', 'video-low'],
'': ['audio', 'video']
}
}
## Pushing streams ## Pushing streams
@ -157,16 +166,22 @@ A stream is created by the sender with the `offer` message:
If a stream with the same id exists, then this is a renegotation; If a stream with the same id exists, then this is a renegotation;
otherwise this message creates a new stream. If the field `replace` is otherwise this message creates a new stream. If the field `replace` is
not empty, then this request additionally requests that an existing stream not empty, then this request additionally requests that an existing stream
with the given id should be closed, and the new stream should replace it. with the given id should be closed, and the new stream should replace it;
this is used most notably when changing the simulcast envelope.
The field `label` is one of `camera`, `screenshare` or `video`, as in the The field `label` is one of `camera`, `screenshare` or `video`, and will
`request` message. be matched against the keys sent by the receiver in their `request` message.
The field `sdp` contains the raw SDP string (i.e. the `sdp` field of The field `sdp` contains the raw SDP string (i.e. the `sdp` field of
a JSEP session description). Galène will interpret the `nack`, a JSEP session description). Galène will interpret the `nack`,
`nack pli`, `ccm fir` and `goog-remb` RTCP feedback types, and act `nack pli`, `ccm fir` and `goog-remb` RTCP feedback types, and act
accordingly. accordingly.
The sender may either send a single stream per media section in the SDP,
or use rid-based simulcasting. In the latter case, it should send two
video streams, one with rid 'h' and high throughput, and one with rid 'l'
and throughput limited to roughly 100kbit/s.
The receiver may either abort the stream immediately (see below), or send The receiver may either abort the stream immediately (see below), or send
an answer. an answer.

View file

@ -25,6 +25,7 @@ type UpTrack interface {
AddLocal(DownTrack) error AddLocal(DownTrack) error
DelLocal(DownTrack) bool DelLocal(DownTrack) bool
Kind() webrtc.RTPCodecType Kind() webrtc.RTPCodecType
Label() string
Codec() webrtc.RTPCodecCapability Codec() webrtc.RTPCodecCapability
// get a recent packet. Returns 0 if the packet is not in cache. // get a recent packet. Returns 0 if the packet is not in cache.
GetRTP(seqno uint16, result []byte) uint16 GetRTP(seqno uint16, result []byte) uint16
@ -33,7 +34,6 @@ type UpTrack interface {
// Type Down represents a connection in the server to client direction. // Type Down represents a connection in the server to client direction.
type Down interface { type Down interface {
GetMaxBitrate(now uint64) uint64
} }
// Type DownTrack represents a track in the server to client direction. // Type DownTrack represents a track in the server to client direction.
@ -42,4 +42,5 @@ type DownTrack interface {
Accumulate(bytes uint32) Accumulate(bytes uint32)
SetTimeOffset(ntp uint64, rtp uint32) SetTimeOffset(ntp uint64, rtp uint32)
SetCname(string) SetCname(string)
GetMaxBitrate() uint64
} }

View file

@ -600,7 +600,7 @@ func (conn *diskConn) initWriter(width, height uint32) error {
return nil return nil
} }
func (down *diskConn) GetMaxBitrate(now uint64) uint64 { func (t *diskTrack) GetMaxBitrate() uint64 {
return ^uint64(0) return ^uint64(0)
} }

View file

@ -13,6 +13,7 @@ import (
"time" "time"
"github.com/pion/ice/v2" "github.com/pion/ice/v2"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3"
) )
@ -60,7 +61,8 @@ type ChatHistoryEntry struct {
} }
const ( const (
MinBitrate = 200000 LowBitrate = 100000
MinBitrate = 2 * LowBitrate
) )
type Group struct { type Group struct {
@ -252,6 +254,12 @@ func APIFromCodecs(codecs []webrtc.RTPCodecCapability) (*webrtc.API, error) {
if UDPMin > 0 && UDPMax > 0 { if UDPMin > 0 && UDPMax > 0 {
s.SetEphemeralUDPPortRange(UDPMin, UDPMax) s.SetEphemeralUDPPortRange(UDPMin, UDPMax)
} }
m.RegisterHeaderExtension(
webrtc.RTPHeaderExtensionCapability{sdp.SDESMidURI},
webrtc.RTPCodecTypeVideo)
m.RegisterHeaderExtension(
webrtc.RTPHeaderExtensionCapability{sdp.SDESRTPStreamIDURI},
webrtc.RTPCodecTypeVideo)
return webrtc.NewAPI( return webrtc.NewAPI(
webrtc.WithSettingEngine(s), webrtc.WithSettingEngine(s),

View file

@ -82,6 +82,7 @@ type rtpDownTrack struct {
remote conn.UpTrack remote conn.UpTrack
ssrc webrtc.SSRC ssrc webrtc.SSRC
maxBitrate *bitrate maxBitrate *bitrate
maxREMBBitrate *bitrate
rate *estimator.Estimator rate *estimator.Estimator
stats *receiverStats stats *receiverStats
atomics *downTrackAtomics atomics *downTrackAtomics
@ -140,7 +141,6 @@ type rtpDownConnection struct {
id string id string
pc *webrtc.PeerConnection pc *webrtc.PeerConnection
remote conn.Up remote conn.Up
maxREMBBitrate *bitrate
iceCandidates []*webrtc.ICECandidateInit iceCandidates []*webrtc.ICECandidateInit
negotiationNeeded int negotiationNeeded int
@ -174,31 +174,22 @@ func newDownConn(c group.Client, id string, remote conn.Up) (*rtpDownConnection,
id: id, id: id,
pc: pc, pc: pc,
remote: remote, remote: remote,
maxREMBBitrate: new(bitrate),
} }
return conn, nil return conn, nil
} }
func (down *rtpDownConnection) GetMaxBitrate(now uint64) uint64 { func (t *rtpDownTrack) GetMaxBitrate() uint64 {
rate := down.maxREMBBitrate.Get(now) now := rtptime.Jiffies()
var trackRate uint64
tracks := down.getTracks()
for _, t := range tracks {
r := t.maxBitrate.Get(now) r := t.maxBitrate.Get(now)
if r == ^uint64(0) { if r == ^uint64(0) {
if t.track.Kind() == webrtc.RTPCodecTypeAudio {
r = 128 * 1024
} else {
r = 512 * 1024 r = 512 * 1024
} }
rr := t.maxREMBBitrate.Get(now)
if rr == 0 || r < rr {
return r
} }
trackRate += r return rr
}
if trackRate < rate {
return trackRate
}
return rate
} }
func (down *rtpDownConnection) addICECandidate(candidate *webrtc.ICECandidateInit) error { func (down *rtpDownConnection) addICECandidate(candidate *webrtc.ICECandidateInit) error {
@ -311,6 +302,10 @@ func (up *rtpUpTrack) GetRTP(seqno uint16, result []byte) uint16 {
return up.cache.Get(seqno, result) return up.cache.Get(seqno, result)
} }
func (up *rtpUpTrack) Label() string {
return up.track.RID()
}
func (up *rtpUpTrack) Kind() webrtc.RTPCodecType { func (up *rtpUpTrack) Kind() webrtc.RTPCodecType {
return up.track.Kind() return up.track.Kind()
} }
@ -687,7 +682,7 @@ func rtcpUpListener(conn *rtpUpConnection, track *rtpUpTrack, r *webrtc.RTPRecei
for { for {
firstSR := false firstSR := false
n, _, err := r.Read(buf) n, _, err := r.ReadSimulcast(buf, track.track.RID())
if err != nil { if err != nil {
if err != io.EOF && err != io.ErrClosedPipe { if err != io.EOF && err != io.ErrClosedPipe {
log.Printf("Read RTCP: %v", err) log.Printf("Read RTCP: %v", err)
@ -752,11 +747,11 @@ func rtcpUpListener(conn *rtpUpConnection, track *rtpUpTrack, r *webrtc.RTPRecei
} }
} }
func sendUpRTCP(conn *rtpUpConnection) error { func sendUpRTCP(up *rtpUpConnection) error {
tracks := conn.getTracks() tracks := up.getTracks()
if len(conn.tracks) == 0 { if len(up.tracks) == 0 {
state := conn.pc.ConnectionState() state := up.pc.ConnectionState()
if state == webrtc.PeerConnectionStateClosed { if state == webrtc.PeerConnectionStateClosed {
return io.ErrClosedPipe return io.ErrClosedPipe
} }
@ -765,7 +760,7 @@ func sendUpRTCP(conn *rtpUpConnection) error {
now := rtptime.Jiffies() now := rtptime.Jiffies()
reports := make([]rtcp.ReceptionReport, 0, len(conn.tracks)) reports := make([]rtcp.ReceptionReport, 0, len(up.tracks))
for _, t := range tracks { for _, t := range tracks {
updateUpTrack(t) updateUpTrack(t)
stats := t.cache.GetStats(true) stats := t.cache.GetStats(true)
@ -810,29 +805,38 @@ func sendUpRTCP(conn *rtpUpConnection) error {
}, },
} }
rate := ^uint64(0)
local := conn.getLocal()
for _, l := range local {
r := l.GetMaxBitrate(now)
if r < rate {
rate = r
}
}
if rate < group.MinBitrate {
rate = group.MinBitrate
}
var ssrcs []uint32 var ssrcs []uint32
var rate uint64
for _, t := range tracks { for _, t := range tracks {
if !t.hasRtcpFb("goog-remb", "") { if !t.hasRtcpFb("goog-remb", "") {
continue continue
} }
ssrcs = append(ssrcs, uint32(t.track.SSRC())) ssrcs = append(ssrcs, uint32(t.track.SSRC()))
var r uint64
if t.Kind() == webrtc.RTPCodecTypeAudio {
r = 100 * 1024
} else if t.Label() == "l" {
r = group.LowBitrate
} else {
local := t.getLocal()
r = ^uint64(0)
for _, down := range local {
rr := down.GetMaxBitrate()
if rr < group.MinBitrate {
rr = group.MinBitrate
}
if r > rr {
r = rr
}
}
if r == ^uint64(0) {
r = 512 * 1024
}
}
rate += r
} }
if len(ssrcs) > 0 { if rate < ^uint64(0) && len(ssrcs) > 0 {
packets = append(packets, packets = append(packets,
&rtcp.ReceiverEstimatedMaximumBitrate{ &rtcp.ReceiverEstimatedMaximumBitrate{
Bitrate: rate, Bitrate: rate,
@ -840,7 +844,7 @@ func sendUpRTCP(conn *rtpUpConnection) error {
}, },
) )
} }
return conn.pc.WriteRTCP(packets) return up.pc.WriteRTCP(packets)
} }
func rtcpUpSender(conn *rtpUpConnection) { func rtcpUpSender(conn *rtpUpConnection) {
@ -1049,7 +1053,7 @@ func rtcpDownListener(conn *rtpDownConnection, track *rtpDownTrack, s *webrtc.RT
log.Printf("sendFIR: %v", err) log.Printf("sendFIR: %v", err)
} }
case *rtcp.ReceiverEstimatedMaximumBitrate: case *rtcp.ReceiverEstimatedMaximumBitrate:
conn.maxREMBBitrate.Set(p.Bitrate, jiffies) track.maxREMBBitrate.Set(p.Bitrate, jiffies)
case *rtcp.ReceiverReport: case *rtcp.ReceiverReport:
for _, r := range p.Reports { for _, r := range p.Reports {
if r.SSRC == uint32(track.ssrc) { if r.SSRC == uint32(track.ssrc) {

View file

@ -149,6 +149,16 @@ func readLoop(conn *rtpUpConnection, track *rtpUpTrack) {
kf, _ := isKeyframe(codec.MimeType, &packet) kf, _ := isKeyframe(codec.MimeType, &packet)
if packet.Extension {
packet.Extension = false
packet.Extensions = nil
bytes, err = packet.MarshalTo(buf)
if err != nil {
log.Printf("%v", err)
continue
}
}
first, index := track.cache.Store( first, index := track.cache.Store(
packet.SequenceNumber, packet.Timestamp, packet.SequenceNumber, packet.Timestamp,
kf, packet.Marker, buf[:bytes], kf, packet.Marker, buf[:bytes],

View file

@ -47,7 +47,6 @@ func (c *webClient) GetStats() *stats.Client {
for _, down := range c.down { for _, down := range c.down {
conns := stats.Conn{ conns := stats.Conn{
Id: down.id, Id: down.id,
MaxBitrate: down.GetMaxBitrate(jiffies),
} }
for _, t := range down.tracks { for _, t := range down.tracks {
rate, _ := t.rate.Estimate() rate, _ := t.rate.Estimate()

View file

@ -385,6 +385,7 @@ func addDownTrackUnlocked(conn *rtpDownConnection, remoteTrack *rtpUpTrack, remo
ssrc: parms.Encodings[0].SSRC, ssrc: parms.Encodings[0].SSRC,
remote: remoteTrack, remote: remoteTrack,
maxBitrate: new(bitrate), maxBitrate: new(bitrate),
maxREMBBitrate: new(bitrate),
stats: new(receiverStats), stats: new(receiverStats),
rate: estimator.New(time.Second), rate: estimator.New(time.Second),
atomics: &downTrackAtomics{}, atomics: &downTrackAtomics{},
@ -646,33 +647,60 @@ func requestedTracks(c *webClient, up conn.Up, tracks []conn.UpTrack) []conn.UpT
return nil return nil
} }
var audio, video bool var audio, video, videoLow bool
for _, s := range r { for _, s := range r {
switch s { switch s {
case "audio": case "audio":
audio = true audio = true
case "video": case "video":
video = true video = true
case "video-low":
videoLow = true
default: default:
log.Printf("client requested unknown value %v", s) log.Printf("client requested unknown value %v", s)
} }
} }
find := func(kind webrtc.RTPCodecType, labels ...string) conn.UpTrack {
for _, l := range labels {
for _, t := range tracks {
if t.Kind() != kind {
continue
}
if t.Label() == l {
return t
}
}
}
for _, t := range tracks {
if t.Kind() != kind {
continue
}
return t
}
return nil
}
var ts []conn.UpTrack var ts []conn.UpTrack
if audio { if audio {
for _, t := range tracks { t := find(webrtc.RTPCodecTypeAudio)
if t.Kind() == webrtc.RTPCodecTypeAudio { if t != nil {
ts = append(ts, t) ts = append(ts, t)
break
}
} }
} }
if video { if video {
for _, t := range tracks { t := find(
if t.Kind() == webrtc.RTPCodecTypeVideo { webrtc.RTPCodecTypeVideo, "h", "m", "video",
)
if t != nil {
ts = append(ts, t) ts = append(ts, t)
break
} }
} else if videoLow {
t := find(
webrtc.RTPCodecTypeVideo, "l", "m", "video",
)
if t != nil {
ts = append(ts, t)
} }
} }

View file

@ -213,7 +213,9 @@
<select id="requestselect" class="select select-inline"> <select id="requestselect" class="select select-inline">
<option value="">nothing</option> <option value="">nothing</option>
<option value="audio">audio only</option> <option value="audio">audio only</option>
<option value="screenshare-low">screen share (low)</option>
<option value="screenshare">screen share</option> <option value="screenshare">screen share</option>
<option value="everything-low">everything (low)</option>
<option value="everything" selected>everything</option> <option value="everything" selected>everything</option>
</select> </select>
</form> </form>

View file

@ -78,6 +78,7 @@ function getUserPass() {
* @property {boolean} [localMute] * @property {boolean} [localMute]
* @property {string} [video] * @property {string} [video]
* @property {string} [audio] * @property {string} [audio]
* @property {boolean} [simulcast]
* @property {string} [send] * @property {string} [send]
* @property {string} [request] * @property {string} [request]
* @property {boolean} [activityDetection] * @property {boolean} [activityDetection]
@ -550,9 +551,15 @@ function mapRequest(what) {
case 'audio': case 'audio':
return {'': ['audio']}; return {'': ['audio']};
break; break;
case 'screenshare-low':
return {screenshare: ['audio','video-low'], '': ['audio']};
break;
case 'screenshare': case 'screenshare':
return {screenshare: ['audio','video'], '': ['audio']}; return {screenshare: ['audio','video'], '': ['audio']};
break; break;
case 'everything-low':
return {'': ['audio','video-low']};
break;
case 'everything': case 'everything':
return {'': ['audio','video']} return {'': ['audio','video']}
break; break;
@ -611,20 +618,25 @@ getInputElement('fileinput').onchange = function(e) {
function gotUpStats(stats) { function gotUpStats(stats) {
let c = this; let c = this;
let text = ''; let values = [];
c.pc.getSenders().forEach(s => { for(let id in stats) {
let tid = s.track && s.track.id; if(stats[id] && stats[id]['outbound-rtp']) {
let stats = tid && c.stats[tid]; let rate = stats[id]['outbound-rtp'].rate;
let rate = stats && stats['outbound-rtp'] && stats['outbound-rtp'].rate;
if(typeof rate === 'number') { if(typeof rate === 'number') {
if(text) values.push(rate);
text = text + ' + '; }
text = text + Math.round(rate / 1000) + 'kbps'; }
} }
});
setLabel(c, text); if(values.length === 0) {
setLabel(c, '');
} else {
values.sort((x,y) => x - y);
setLabel(c, values
.map(x => Math.round(x / 1000).toString())
.reduce((x, y) => x + '+' + y));
}
} }
/** /**
@ -800,6 +812,7 @@ function newUpStream(localId) {
* @param {number} [bps] * @param {number} [bps]
*/ */
async function setMaxVideoThroughput(c, bps) { async function setMaxVideoThroughput(c, bps) {
let simulcast = doSimulcast();
let senders = c.pc.getSenders(); let senders = c.pc.getSenders();
for(let i = 0; i < senders.length; i++) { for(let i = 0; i < senders.length; i++) {
let s = senders[i]; let s = senders[i];
@ -808,17 +821,17 @@ async function setMaxVideoThroughput(c, bps) {
let p = s.getParameters(); let p = s.getParameters();
if(!p.encodings) if(!p.encodings)
p.encodings = [{}]; p.encodings = [{}];
p.encodings.forEach(e => { if((!simulcast && p.encodings.length != 1) ||
if(bps > 0) (simulcast && p.encodings.length != 2)) {
e.maxBitrate = bps; // change the simulcast envelope
else await replaceUpStream(c);
delete e.maxBitrate; return;
});
try {
await s.setParameters(p);
} catch(e) {
console.error(e);
} }
p.encodings.forEach(e => {
if(!e.rid || e.rid === 'h')
e.maxBitrate = bps || unlimitedRate;
});
await s.setParameters(p);
} }
} }
@ -1022,6 +1035,19 @@ function isSafari() {
return ua.indexOf('safari') >= 0 && ua.indexOf('chrome') < 0; return ua.indexOf('safari') >= 0 && ua.indexOf('chrome') < 0;
} }
const unlimitedRate = 1000000000;
const simulcastRate = 100000;
/**
* @returns {boolean}
*/
function doSimulcast() {
if(!getSettings().simulcast)
return false;
let bps = getMaxVideoThroughput();
return bps <= 0 || bps >= 2 * simulcastRate;
}
/** /**
* Sets up c to send the given stream. Some extra parameters are stored * Sets up c to send the given stream. Some extra parameters are stored
* in c.userdata. * in c.userdata.
@ -1029,6 +1055,7 @@ function isSafari() {
* @param {Stream} c * @param {Stream} c
* @param {MediaStream} stream * @param {MediaStream} stream
*/ */
function setUpStream(c, stream) { function setUpStream(c, stream) {
if(c.stream != null) if(c.stream != null)
throw new Error("Setting nonempty stream"); throw new Error("Setting nonempty stream");
@ -1073,11 +1100,20 @@ function setUpStream(c, stream) {
c.close(); c.close();
}; };
let encodings = [{}]; let encodings = [];
if(t.kind === 'video') { if(t.kind === 'video') {
let simulcast = doSimulcast();
let bps = getMaxVideoThroughput(); let bps = getMaxVideoThroughput();
if(bps > 0) encodings.push({
encodings[0].maxBitrate = bps; rid: 'h',
maxBitrate: bps || unlimitedRate,
});
if(simulcast)
encodings.push({
rid: 'l',
scaleResolutionDownBy: 2,
maxBitrate: simulcastRate,
});
} }
c.pc.addTransceiver(t, { c.pc.addTransceiver(t, {
direction: 'sendonly', direction: 'sendonly',

View file

@ -1246,17 +1246,20 @@ Stream.prototype.updateStats = async function() {
if(report) { if(report) {
for(let r of report.values()) { for(let r of report.values()) {
if(stid && r.type === 'outbound-rtp') { if(stid && r.type === 'outbound-rtp') {
let id = stid;
if(r.rid)
id = id + '-' + r.rid
if(!('bytesSent' in r)) if(!('bytesSent' in r))
continue; continue;
if(!stats[stid]) if(!stats[id])
stats[stid] = {}; stats[id] = {};
stats[stid][r.type] = {}; stats[id][r.type] = {};
stats[stid][r.type].timestamp = r.timestamp; stats[id][r.type].timestamp = r.timestamp;
stats[stid][r.type].bytesSent = r.bytesSent; stats[id][r.type].bytesSent = r.bytesSent;
if(old[stid] && old[stid][r.type]) if(old[id] && old[id][r.type])
stats[stid][r.type].rate = stats[id][r.type].rate =
((r.bytesSent - old[stid][r.type].bytesSent) * 1000 / ((r.bytesSent - old[id][r.type].bytesSent) * 1000 /
(r.timestamp - old[stid][r.type].timestamp)) * 8; (r.timestamp - old[id][r.type].timestamp)) * 8;
} }
} }
} }