I'm trying to change the Bandwidth On The Fly for WebRTC P2P Video Call by using this sample combining to my existing code which are multi-participants video call:
Sample: https://webrtc.github.io/samples/src/content/peerconnection/bandwidth/
When I look into WebRTC Internals via Chrome,
bitsReceivedPerSecond for send (ssrc) (video) has been dropped to the selected bandwidth. However, bitsReceivedPerSecond for recv (ssrc) (video) still remain unchanged. May I know how to make the bandwidth changes applies on both send and receive?
Below are my codes, it would be great if you could help to point out my mistakes, thanks in advance.
Update 14/12/2018: Added 1st option for receiver to the codes
Problem: Uncaught TypeError: receiver.getParameters is not a function
const bandwidthSelector = document.querySelector('select#bandwidth');
bandwidthSelector.disabled = false;
// renegotiate bandwidth on the fly.
bandwidthSelector.onchange = () => {
bandwidthSelector.disabled = true;
const bandwidth = bandwidthSelector.options[bandwidthSelector.selectedIndex].value;
// In Chrome, use RTCRtpSender.setParameters to change bandwidth without
// (local) renegotiation. Note that this will be within the envelope of
// the initial maximum bandwidth negotiated via SDP.
if ((adapter.browserDetails.browser === 'chrome' ||
(adapter.browserDetails.browser === 'firefox' &&
adapter.browserDetails.version >= 64)) &&
'RTCRtpSender' in window &&
'setParameters' in window.RTCRtpSender.prototype) {
$.each(peers, function( index, value ) {
const sender = value.getSenders()[0];
const parameters = sender.getParameters();
if (!parameters.encodings) {
parameters.encodings = [{}];
}
if (bandwidth === 'unlimited') {
delete parameters.encodings[0].maxBitrate;
} else {
parameters.encodings[0].maxBitrate = bandwidth * 1000;
}
sender.setParameters(parameters)
.then(() => {
bandwidthSelector.disabled = false;
})
.catch(e => console.error(e));
/* 1st Option - Start */
const receiver = value.getReceivers()[0];
const recParameters = receiver.getParameters();
if (!recParameters.encodings) {
recParameters.encodings = [{}];
}
if (bandwidth === 'unlimited') {
delete recParameters.encodings[0].maxBitrate;
} else {
recParameters.encodings[0].maxBitrate = bandwidth * 1000;
}
receiver.setParameters(recParameters)
.then(() => {
bandwidthSelector.disabled = false;
})
.catch(e => console.error(e));
/* 1st Option - End */
return;
});
}
// Fallback to the SDP munging with local renegotiation way of limiting
// the bandwidth.
function onSetSessionDescriptionError(error) {
console.log('Failed to set session description: ' + error.toString());
}
};
function updateBandwidthRestriction(sdp, bandwidth) {
let modifier = 'AS';
if (adapter.browserDetails.browser === 'firefox') {
bandwidth = (bandwidth >>> 0) * 1000;
modifier = 'TIAS';
}
if (sdp.indexOf('b=' + modifier + ':') === -1) {
// insert b= after c= line.
sdp = sdp.replace(/c=IN (.*)\r\n/, 'c=IN $1\r\nb=' + modifier + ':' + bandwidth + '\r\n');
} else {
sdp = sdp.replace(new RegExp('b=' + modifier + ':.*\r\n'), 'b=' + modifier + ':' + bandwidth + '\r\n');
}
return sdp;
}
function removeBandwidthRestriction(sdp) {
return sdp.replace(/b=AS:.*\r\n/, '').replace(/b=TIAS:.*\r\n/, '');
}
Update 14/12/2018: 2nd option createOffer
Problem: Failed to set session description: InvalidStateError: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote answer sdp: Called in wrong state: kStable
const bandwidthSelector = document.querySelector('select#bandwidth');
bandwidthSelector.disabled = false;
// renegotiate bandwidth on the fly.
bandwidthSelector.onchange = () => {
bandwidthSelector.disabled = true;
const bandwidth = bandwidthSelector.options[bandwidthSelector.selectedIndex].value;
// In Chrome, use RTCRtpSender.setParameters to change bandwidth without
// (local) renegotiation. Note that this will be within the envelope of
// the initial maximum bandwidth negotiated via SDP.
if ((adapter.browserDetails.browser === 'chrome' ||
(adapter.browserDetails.browser === 'firefox' &&
adapter.browserDetails.version >= 64)) &&
'RTCRtpSender' in window &&
'setParameters' in window.RTCRtpSender.prototype) {
$.each(peers, function( index, value ) {
const sender = value.getSenders()[0];
const parameters = sender.getParameters();
if (!parameters.encodings) {
parameters.encodings = [{}];
}
if (bandwidth === 'unlimited') {
delete parameters.encodings[0].maxBitrate;
} else {
parameters.encodings[0].maxBitrate = bandwidth * 1000;
}
sender.setParameters(parameters)
.then(() => {
bandwidthSelector.disabled = false;
})
.catch(e => console.error(e));
/* 2nd option - Start */
value.createOffer(
function (local_description) {
console.log("Local offer description is: ", local_description);
value.setLocalDescription(local_description,
function () {
signaling_socket.emit('relaySessionDescription', {
'peer_id': index,
'session_description': local_description
});
console.log("Offer setLocalDescription succeeded");
},
function () {
Alert("Offer setLocalDescription failed!");
}
);
},
function (error) {
console.log("Error sending offer: ", error);
}).then(() => {
const desc = {
type: value.remoteDescription.type,
sdp: bandwidth === 'unlimited'
? removeBandwidthRestriction(value.remoteDescription.sdp)
: updateBandwidthRestriction(value.remoteDescription.sdp, bandwidth)
};
console.log('Applying bandwidth restriction to setRemoteDescription:\n' +
desc.sdp);
return value.setRemoteDescription(desc);
})
.then(() => {
bandwidthSelector.disabled = false;
})
.catch(onSetSessionDescriptionError);
/* 2nd option - End */
return;
});
}
// Fallback to the SDP munging with local renegotiation way of limiting
// the bandwidth.
function onSetSessionDescriptionError(error) {
console.log('Failed to set session description: ' + error.toString());
}
};
function updateBandwidthRestriction(sdp, bandwidth) {
let modifier = 'AS';
if (adapter.browserDetails.browser === 'firefox') {
bandwidth = (bandwidth >>> 0) * 1000;
modifier = 'TIAS';
}
if (sdp.indexOf('b=' + modifier + ':') === -1) {
// insert b= after c= line.
sdp = sdp.replace(/c=IN (.*)\r\n/, 'c=IN $1\r\nb=' + modifier + ':' + bandwidth + '\r\n');
} else {
sdp = sdp.replace(new RegExp('b=' + modifier + ':.*\r\n'), 'b=' + modifier + ':' + bandwidth + '\r\n');
}
return sdp;
}
function removeBandwidthRestriction(sdp) {
return sdp.replace(/b=AS:.*\r\n/, '').replace(/b=TIAS:.*\r\n/, '');
}
RTCRtpSender only controls the sending bandwidth. If you want to limit the receiving bandwidth, you need to use either the b=AS / b=TIAS way or make the receiver use setParameters.
Related
I have created a node server that streams mp4 videos from a Mongo database using gridfs. I have no issue when streaming the video to a desktop browser but when I try to stream to any mobile device I only see the video player but it will not play. When I check my server logs I see that the desktop version makes a call for 0-total bytes of the video but on mobile it only makes a call for bytes 0-1 and then stops, no errors given.
These are the two server side functions which work great when streaming to a pc browser.
function StreamGridFile(req, res, GridFile) {
console.log("Request: " + req);
if (req.headers['range']) {
// Range request, partialle stream the file
console.log('Range Request');
var parts = req.headers['range'].replace(/bytes=/, "").split("-");
var partialstart = parts[0];
var partialend = parts[1];
var start = parseInt(partialstart, 10);
var end = partialend ? parseInt(partialend, 10) : GridFile.length - 1;
var chunksize = (end - start) + 1;
console.log('Range ', start, '-', end);
res.writeHead(206, {
'Content-Range': 'bytes ' + start + '-' + end + '/' + GridFile.length,
'Accept-Ranges': 'bytes',
'Content-Length': chunksize,
'Content-Type': GridFile.contentType
});
// Set filepointer
GridFile.seek(start, function() {
// get GridFile stream
var stream = GridFile.stream();
// write to response
stream.on('data', function(buff) {
// count data to abort streaming if range-end is reached
// perhaps theres a better way?
start += buff.length;
if (start >= end) {
// enough data send, abort
GridFile.close();
res.end();
} else {
res.write(buff);
}
});
stream.on("end", function() {
// Record the end was called
GridFile.close()
db.close(function() {
counter--;
console.log("closed db, counter: " + counter);
});
});
});
} else {
// stream back whole file
console.log('No Range Request');
var stream = GridFile.stream();
stream.pipe(res);
stream.on("end", function() {
// Record the end was called
gridfile.close();
db.close(function() {
counter--;
//console.log("closed db, counter: " + counter);
});
});
}
}
and here is the second
exports.streamVideo = function(req, res, id) {
console.log("entered the stream video function");
if (counter == 0) {
console.log("counter = 0");
db.open(function(err, db) {
console.log("db open for video stream");
counter++;
console.log(counter);
if (!err) {
var gs = GridStore(db, ObjectID(id));
gs.open(function(err, GridFile) {
console.log("gridstore open");
if (err) {
res.writeHead(404, {
'Content-Type': 'text/javascript'
});
res.end("Video not found");
gs.close();
db.close();
return;
}
console.log("no error, entering StreamGridFile function");
StreamGridFile(req, res, GridFile);
});
} else {
res.writeHead(404, {
'Content-Type': 'text/javascript'
});
res.end("database error");
gs.close();
db.close();
}
})
}
}
I am pretty sure the error falls into this code because I had initially streamed the video from a static file but later converted to using mongo and that was when it stopped working.
Give this a shot:
function StreamGridFile (req, res, GridFile) {
console.log("Request: " + req);
if (req.headers['range']) {
// Range request, partialle stream the file
console.log('Range Request');
var parts = req.headers['range'].replace(/bytes=/, "").split("-");
var maxIdx = GridFile.length - 1;
var start = parseInt(parts[0], 10);
var end = parseInt(parts[1], 10);
if (isInvalidRange(start, end, maxIdx)) {
return res.writeHead(416, {
'Accept-Ranges': 'bytes',
'Content-Type': GridFile.contentType,
'Content-Range': 'bytes */' + GridFile.length,
}).end();
}
if (end > maxIdx || end !== end) {
end = maxIdx
}
var bytesToSend = end - start + 1;
res.writeHead(206, {
'Accept-Ranges': 'bytes',
'Content-Type': GridFile.contentType,
'Content-Range': 'bytes ' + start + '-' + end + '/' + GridFile.length,
'Content-Length': bytesToSend
});
// Set filepointer
GridFile.seek(start, function() {
// get GridFile stream
var stream = GridFile.stream(true);
// write to response
stream.on('data', function(buff) {
if (buff.length > bytesToSend) {
buff = buff.slice(0, bytesToSend)
}
res.write(buff);
bytesToSend -= buff.length;
if (bytesToSend <= 0) {
// enough data send, abort
GridFile.close();
db.close(function() {
counter--;
console.log("closed db, counter: " + counter);
});
res.end();
}
});
});
} else {
// stream back whole file
console.log('No Range Request');
res.header('Accept-Range', 'bytes');
res.header('Content-Type', GridFile.contentType);
var stream = GridFile.stream(true);
stream.pipe(res);
stream.on('end', function(){
GridFile.close();
db.close(function() {
counter--;
console.log("closed db, counter: " + counter);
});
});
}
}
function isInvalidRange (start, end, maxIdx) {
return start !== start // NaN
|| start < 0
|| end < start
|| start > maxIdx
}
This obviously is not able to handle a multi-byte-range request (where the Range header looks like bytes=2048-4095,4096-6143), but it should be able to handle normal byte-range requests.
I'm trying to implement a mechanism to send textual data (JSON for instance) in from page to page, using javascript at the same machine.
I found some code and wrapped it but it only works at the same page.
At the moment I don't want to use a WwebRTC framework, only adapter.js.
//Must include adapter.js before
var WebRTCManager = (function () {
'use strict';
//Ctor
function WebRTCManagerFn() {
console.log('WebRTCManagerFn ctor reached');
this._events = {};
this._localConnection = null
this._remoteConnection = null;
this._sendChannel = null;
this._receiveChannel = null;
}
WebRTCManagerFn.prototype.addEventListener = function (name, handler) {
if (this._events.hasOwnProperty(name))
this._events[name].push(handler);
else
this._events[name] = [handler];
};
WebRTCManagerFn.prototype._fireEvent = function (name, event) {
if (!this._events.hasOwnProperty(name))
return;
if (!event)
event = {};
var listeners = this._events[name], l = listeners.length;
for (var i = 0; i < l; i++) {
listeners[i].call(null, event);
}
};
WebRTCManagerFn.prototype.createConnection = function () {
var servers = null;
var pcConstraint = null;
var dataConstraint = null;
console.log('Using SCTP based data channels');
// SCTP is supported from Chrome 31 and is supported in FF.
// No need to pass DTLS constraint as it is on by default in Chrome 31.
// For SCTP, reliable and ordered is true by default.
// Add localConnection to global scope to make it visible
// from the browser console.
window.localConnection = this._localConnection =
new RTCPeerConnection(servers, pcConstraint);
console.log('Created local peer connection object localConnection');
this._sendChannel = this._localConnection.createDataChannel('sendDataChannel',
dataConstraint);
console.log('Created send data channel');
this._localConnection.onicecandidate = this._localIceCallback.bind(this);
this._sendChannel.onopen = this._onSendChannelStateChange.bind(this);
this._sendChannel.onclose = this._onSendChannelStateChange.bind(this);
// Add remoteConnection to global scope to make it visible
// from the browser console.
window.remoteConnection = this._remoteConnection =
new RTCPeerConnection(servers, pcConstraint);
console.log('Created remote peer connection object remoteConnection');
this._remoteConnection.onicecandidate = this._remoteIceCallback.bind(this);
this._remoteConnection.ondatachannel = this._receiveChannelCallback.bind(this);
this._localConnection.createOffer(this._gotOfferFromLocalConnection.bind(this), this._onCreateSessionDescriptionError.bind(this));
}
WebRTCManagerFn.prototype._onCreateSessionDescriptionError = function (error) {
console.log('Failed to create session description: ' + error.toString());
}
WebRTCManagerFn.prototype.sendMessage = function (msgText) {
var msg = new Message(msgText);
// Send the msg object as a JSON-formatted string.
var data = JSON.stringify(msg);
this._sendChannel.send(data);
console.log('Sent Data: ' + data);
}
WebRTCManagerFn.prototype.closeDataChannels = function () {
console.log('Closing data channels');
this._sendChannel.close();
console.log('Closed data channel with label: ' + this._sendChannel.label);
this._receiveChannel.close();
console.log('Closed data channel with label: ' + this._receiveChannel.label);
this._localConnection.close();
this._remoteConnection.close();
this._localConnection = null;
this._remoteConnection = null;
console.log('Closed peer connections');
}
WebRTCManagerFn.prototype._gotOfferFromLocalConnection = function (desc) {
console.log('reached _gotOfferFromLocalConnection');
if (this && this._localConnection != 'undefined' && this._remoteConnection != 'undefined') {
this._localConnection.setLocalDescription(desc);
console.log('Offer from localConnection \n' + desc.sdp);
this._remoteConnection.setRemoteDescription(desc);
this._remoteConnection.createAnswer(this._gotAnswerFromRemoteConnection.bind(this),
this._onCreateSessionDescriptionError.bind(this));
}
}
WebRTCManagerFn.prototype._gotAnswerFromRemoteConnection = function (desc) {
console.log('reached _gotAnswerFromRemoteConnection');
if (this && this._localConnection != 'undefined' && this._remoteConnection != 'undefined') {
this._remoteConnection.setLocalDescription(desc);
console.log('Answer from remoteConnection \n' + desc.sdp);
this._localConnection.setRemoteDescription(desc);
}
}
WebRTCManagerFn.prototype._localIceCallback = function (event) {
console.log('local ice callback');
if (event.candidate) {
this._remoteConnection.addIceCandidate(event.candidate,
this._onAddIceCandidateSuccess.bind(this), this._onAddIceCandidateError.bind(this));
console.log('Local ICE candidate: \n' + event.candidate.candidate);
}
}
WebRTCManagerFn.prototype._remoteIceCallback = function (event) {
console.log('remote ice callback');
if (event.candidate) {
this._localConnection.addIceCandidate(event.candidate,
this._onAddIceCandidateSuccess.bind(this), this._onAddIceCandidateError.bind(this));
console.log('Remote ICE candidate: \n ' + event.candidate.candidate);
}
}
WebRTCManagerFn.prototype._onAddIceCandidateSuccess = function (evt) {
debugger;
console.log('AddIceCandidate success. evt: '+ evt);
}
WebRTCManagerFn.prototype._onAddIceCandidateError = function (error) {
console.log('Failed to add Ice Candidate: ' + error.toString());
}
WebRTCManagerFn.prototype._receiveChannelCallback = function (event) {
console.log('Receive Channel Callback');
this._receiveChannel = event.channel;
this._receiveChannel.onmessage = this._onReceiveMessageCallback.bind(this);
this._receiveChannel.onopen = this._onReceiveChannelStateChange.bind(this);
this._receiveChannel.onclose = this._onReceiveChannelStateChange.bind(this);
}
WebRTCManagerFn.prototype._onReceiveMessageCallback = function (event) {
console.log('Received Message: ' + event.data);
console.log('Received Message this is: ' + this);
var msgObj = JSON.parse(event.data);
this._fireEvent("messageRecieved", {
details: {
msg: msgObj
}
});
}
WebRTCManagerFn.prototype._onSendChannelStateChange = function () {
console.log('_onSendChannelStateChange');
var readyState = this._sendChannel.readyState;
console.log('Send channel state is: ' + readyState);
}
WebRTCManagerFn.prototype._onReceiveChannelStateChange = function () {
var readyState = this._receiveChannel.readyState;
console.log('Receive channel state is: ' + readyState);
}
return WebRTCManagerFn;
})();
My question is how to pass data between two pages on the same machine using WebRTC?
This WebRTC tab chat demo works across tabs or windows in the same browser without a server: https://jsfiddle.net/f5y48hcd/ (I gave up making it work in a code snippet due to a SecurityError.)
Open the fiddle in two windows and try it out. For reference, here's the WebRTC code:
var pc = new RTCPeerConnection(), dc, enterPressed = e => e.keyCode == 13;
var connect = () => init(dc = pc.createDataChannel("chat"));
pc.ondatachannel = e => init(dc = e.channel);
var init = dc => {
dc.onopen = e => (dc.send("Hi!"), chat.select());
dc.onclose = e => log("Bye!");
dc.onmessage = e => log(e.data);
};
chat.onkeypress = e => {
if (!enterPressed(e)) return;
dc.send(chat.value);
log("> " + chat.value);
chat.value = "";
};
var sc = new localSocket(), send = obj => sc.send(JSON.stringify(obj));
var incoming = msg => msg.sdp &&
pc.setRemoteDescription(new RTCSessionDescription(msg.sdp))
.then(() => pc.signalingState == "stable" || pc.createAnswer()
.then(answer => pc.setLocalDescription(answer))
.then(() => send({ sdp: pc.localDescription })))
.catch(log) || msg.candidate &&
pc.addIceCandidate(new RTCIceCandidate(msg.candidate)).catch(log);
sc.onmessage = e => incoming(JSON.parse(e.data));
pc.oniceconnectionstatechange = e => log(pc.iceConnectionState);
pc.onicecandidate = e => send({ candidate: e.candidate });
pc.onnegotiationneeded = e => pc.createOffer()
.then(offer => pc.setLocalDescription(offer))
.then(() => send({ sdp: pc.localDescription }))
.catch(log);
var log = msg => div.innerHTML += "<br>" + msg;
I use this for demoing WebRTC data channels. Note that the secret sauce is the localSocket.js that I wrote for this, which looks like this:
function localSocket() {
localStorage.a = localStorage.b = JSON.stringify([]);
this.index = 0;
this.interval = setInterval(() => {
if (!this.in) {
if (!JSON.parse(localStorage.a).length) return;
this.in = "a"; this.out = "b";
}
var arr = JSON.parse(localStorage[this.in]);
if (arr.length <= this.index) return;
if (this.onmessage) this.onmessage({ data: arr[this.index] });
this.index++;
}, 200);
setTimeout(() => this.onopen && this.onopen({}));
}
localSocket.prototype = {
send: function(msg) {
if (!this.out) {
this.out = "a"; this.in = "b";
}
var arr = JSON.parse(localStorage[this.out]);
arr.push(msg);
localStorage[this.out] = JSON.stringify(arr);
},
close: function() {
clearInterval(this.interval);
}
};
It basically uses localStorage to simulate web sockets locally between two tabs. If this is all you want to do, then you don't even need WebRTC data channels.
Disclaimer: It's not very robust, and relies on two pages being ready to communicate, so not production-ready by any means.
I am using usergrid to store data for a customer project. It's got two collections carShowrooms and cars. So far I am good. But I have a scenario where I have refresh the masterdata of the collection cars. Everytime I do this, I have to delete all the existing data in cars and replace it with incoming cars data from the master inventory system.
Now, with the docu in https://www.npmjs.org/package/usergrid, I see that I can only destroy one car at a time.
car.destroy(function(err){
if (err){
//error - car not deleted
//winston log - tbd
} else {
//success - car deleted
}
});
This is ok for smaller showrooms, but bigger multibrand showrooms have variety of cars - sometimes even upto 50 different varieties (8 car brands * approx. 8 different options).
Is there a mass delete option? can someone please point me to a docu if I am missing something here.
P.S. I am new to usergrid, if this is a repeated question, please mark so and point me to the right url
If you're so inclined, I've written a Node.js bulk deleter that runs delete requests in parallel. It takes approximately 3 minutes to delete 1000 entities.
Here's an always up-to-date gist, and a copy for SO:
// Installation
// 1. Install Node.js http://nodejs.org/download/
// 2. In Terminal, cd (navigate) to the directory where you saved this file
// 3. Run 'npm install request async'
// 4. Edit the script config below with your token, org, app, and collection name.
// 5. To run the script, at the Terminal prompt, run 'node api_baas_deleter.js'
// Config
var access_token = "{token}";
var as_basepath = "http://api.usergrid.com/{org}/{app}/"; // You need the trailing slash!
var collection = "{collection_name}";
// End Config
var request = require('request');
var async = require('async');
var authstring = "access_token=" + access_token;
var total = 0;
var startTime = Date.now();
function deleteRecords(callback) {
request.get({
url: as_basepath + collection + "?" + authstring,
json: true
}, function(e, r, body) {
if (body.count === undefined) {
var err = "Error: invalid endpoint. Check your basepath and collection name.";
console.log(err);
if (typeof(callback) === 'function') {
callback(err)
}
} else {
// console.log("Found " + body.count + " entities");
if (body.count > 0) {
var deletes = [];
for (var i = 0; i < body.count; i++) {
deletes.push({
url: as_basepath + collection + "/" + body.entities[i].uuid + "?" + authstring,
json: true
});
console.log("Deleting " + body.entities[i].uuid)
}
async.each(deletes, function(options, callback) {
request.del(options, function(e, r, body) {
if (r.statusCode === 200) {
total++;
}
callback(e);
});
}, function(err) {
setTimeout(function() {
deleteRecords(collection, function(e) {
callback(e);
});
}, 600); // Mandatory, since it seems to not retrieve entities if you make a request in < 600ms
});
} else {
var timeInMinutes = minutesFromMs(Date.now() - startTime);
console.log("Deleted " + total + " entities in " + timeInMinutes + " minute" + ((timeInMinutes > 1 || timeInMinutes < 1) ? "s" : ""));
if (typeof(callback) === 'function') {
callback()
}
}
}
});
}
function minutesFromMs(time) {
return Math.round(((time % 86400000) % 3600000) / 60000).toString();
}
deleteRecords();
There currently isn't a mass delete function in the Usergrid Node SDK, but you can create one. This is how I added a monkey-patched delete-by-query function into the Node SDK:
Usergrid.client.prototype.delete = function(opts, callback) {
if (_.isFunction(opts)) { callback = opts; opts = undefined; }
if (!opts.qs.q) { opts.qs.q = '*'; }
var options = {
method: 'DELETE',
endpoint: opts.type,
qs: opts.qs
};
var self = this;
this.request(options, function (err, data) {
if (err && self.logging) {
console.log('entities could not be deleted');
}
if (typeof(callback) === 'function') {
callback(err, data);
}
});
};
Hope that helps!
Scott
I am trying to create a simple webpage using WebRTC DataChannels that sends pings/pongs between browsers.
When Chrome initiates the connection and then Chrome connects, it works.
When Firefox initiates the connection and then Firefox connects, it works.
When Chrome initiates the connection and then Firefox connects, it works.
But when Firefox initiates the connection and then Chrome connects, it doesn't work. Chrome never receives data sent by Firefox.
I'm using Firefox 26 and Chromium 32, on Archlinux.
Here is my JavaScript code:
<!DOCTYPE html>
<html>
<head>
<title>WebRTC test</title>
<meta charset="utf-8">
</head>
<body>
<button id="create" disabled>Create data channel</button>
<script type="text/javascript">
// DOM
var create = document.getElementById('create');
// Compatibility
window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
// Create a WebRTC object
var rtc = new RTCPeerConnection(null);
// Create a data channel
var sendChannel = rtc.createDataChannel('pingtest', {reliable: false});
var myMsg = 'ping';
function setRecvChannel(recvChannel) {
recvChannel.onmessage = function(event) {
if(event.data.indexOf('\x03\x00\x00\x00\x00\x00\x00\x00\x00') === 0) {
console.log('-> ' + window.btoa(event.data));
return; // Received channel's name, ignore
}
console.log('-> ' + event.data);
window.setTimeout(function() {
console.log('<- ' + myMsg);
sendChannel.send(myMsg);
}, 500);
};
}
// Chrome and Firefox
sendChannel.onopen = function(event) {
setRecvChannel(sendChannel);
if(myMsg === 'ping') {
console.log('<- ' + myMsg);
sendChannel.send(myMsg);
}
};
// Firefox
rtc.ondatachannel = function(event) {
setRecvChannel(event.channel);
};
// ICE
rtc.onicecandidate = function(event) {
if(event.candidate) {
console.log('<- ' + JSON.stringify(event.candidate));
ws.send(JSON.stringify(event.candidate));
}
};
// Signaling channel
var ws = new WebSocket('ws://127.0.0.1:49300/');
ws.onopen = function() {
create.disabled = false;
};
ws.onmessage = function(event) {
console.log('-> ' + event.data);
var data = JSON.parse(event.data);
if(data.sdp) {
rtc.setRemoteDescription(new RTCSessionDescription(data));
if(data.type === 'offer') {
myMsg = 'pong';
rtc.createAnswer(function(anwser) {
rtc.setLocalDescription(anwser, function () {
console.log('<- ' + JSON.stringify(anwser));
ws.send(JSON.stringify(anwser));
});
}, console.error);
}
}
else {
rtc.addIceCandidate(new RTCIceCandidate(data));
}
};
ws.onclose = function() {
create.disabled = true;
};
// Create an offer
create.onclick = function() {
rtc.createOffer(function(offer) {
rtc.setLocalDescription(offer, function () {
offer.sdp = offer.sdp;
console.log(offer.sdp);
console.log('<- ' + JSON.stringify(offer));
ws.send(JSON.stringify(offer));
});
}, console.error);
};
</script>
</body>
</html>
Here is the WebSocket-based signaling server I've created only for test purposes, it simply listens on port 49300 and broadcasts data received from clients to other clients:
#!/usr/bin/python
#-*- encoding: Utf-8 -*-
from socket import socket, AF_INET, SOCK_STREAM, SOL_SOCKET, SO_REUSEADDR
from string import printable
from threading import Thread
from base64 import b64encode
from struct import unpack
from hashlib import sha1
PORT = 49300
activeSocks = []
def SignalingChannel(ip, port, sock):
print 'Connection from %s:%s' % (ip, port)
# Handling the HTTP request
try:
headers = sock.recv(8184)
assert headers.upper().startswith('GET')
assert headers.endswith('\r\n\r\n')
data = headers.strip().replace('\r', '').split('\n')[1:]
headers = {}
for header in data:
name, value = header.split(':', 1)
headers[name.strip().lower()] = value.strip()
assert headers['host']
assert 'upgrade' in headers['connection'].lower()
assert 'websocket' in headers['upgrade'].lower()
assert headers['sec-websocket-version'] == '13'
assert len(headers['sec-websocket-key']) == 24
guid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
accept = b64encode(sha1(headers['sec-websocket-key'] + guid).digest())
sock.send('HTTP/1.1 101 Switching Protocols\r\n' +
'Connection: Upgrade\r\n' +
'Upgrade: websocket\r\n' +
'Sec-WebSocket-Accept: %s\r\n' % accept +
'\r\n')
except:
try:
msg = 'This is a RFC 6455 WebSocket server.\n'
sock.send('HTTP/1.1 400 Bad Request\r\n' +
'Connection: Close\r\n' +
'Content-Length: %d\r\n' % len(msg) +
'Content-Type: text/plain; charset=us-ascii\r\n' +
'Sec-WebSocket-Version: 13\r\n' +
'\r\n' +
msg)
except:
pass
sock.close()
print 'Disconnection from %s:%s' % (ip, port)
return
activeSocks.append(sock)
try:
data = sock.recv(2)
while len(data) == 2:
frame = data[0] + chr(ord(data[1]) & 0b01111111)
opcode = ord(data[0]) & 0b00001111
mask = ord(data[1]) & 0b10000000
paylen = ord(data[1]) & 0b01111111
if paylen == 126:
data = sock.recv(2)
frame += data
paylen = unpack('>H', data)[0]
elif paylen == 127:
data = sock.recv(8)
frame += data
paylen = unpack('>Q', data)[0]
if mask:
mask = sock.recv(4)
data = ''
received = True
while received and len(data) < paylen:
received = sock.recv(paylen - len(data))
data += received
if mask:
unmasked = ''
for i in xrange(len(data)):
unmasked += chr(ord(data[i]) ^ ord(mask[i % 4]))
else:
unmasked = data
frame += unmasked
if opcode != 8:
print '-- From port %d --' % port
if all(ord(c) < 127 and c in printable for c in unmasked):
print unmasked
else:
print repr(unmasked)
for destSock in activeSocks:
if destSock != sock:
destSock.send(frame)
else:
break
data = sock.recv(2)
except:
pass
activeSocks.remove(sock)
sock.close()
print 'Disconnection from %s:%s' % (ip, port)
listenSock = socket(AF_INET, SOCK_STREAM)
listenSock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
listenSock.bind(('0.0.0.0', PORT))
listenSock.listen(20)
print 'Listening on port 49300...'
while True:
clientSock, (ip, port) = listenSock.accept()
Thread(target=SignalingChannel, args=(ip, port, clientSock)).start()
To run the code, launch the signaling server, open the webpage in two browser tabs, click the "Create data channel" button and look at the web console.
Any idea?
Looking through the Chrome/Firefox bug trackers it looks like this issue has been identified and resolved, but only in Chrome Canary 33.0.1715.0 or higher.
If you're unwilling to require the Chrome Canary build mentioned above, you can detect the bad peer combination, and have your 'offer' button signal the other client to make an offer.
Pseudocode:
socket.onMessage(msg) {
if(msg == "request-offer"){
doOffer();
}
...
}
createDataChannelButton.onClick() {
if(!canCreateChannelBasedOnBrowser){
socket.send("request-offer");
}
else {
doOffer();
}
}
With your example code:
<!DOCTYPE html>
<html>
<head>
<title>WebRTC test</title>
<meta charset="utf-8">
</head>
<body>
<button id="create" disabled>Create data channel</button>
<script type="text/javascript">
// DOM
// CHANGE: Add basic browser detection based on google's adapter.js file.
var rtcBrowserVersion = 0;
var rtcCanInitiateDataOffer = false;
if (navigator.mozGetUserMedia) {
rtcBrowserVersion = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
rtcCanInitiateDataOffer = true;
} else if (navigator.webkitGetUserMedia) {
// Chrome Canary reports major version 35 for me. Can't find a reliable resource to confirm
// canary versions.
rtcBrowserVersion = parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10);
rtcCanInitiateDataOffer = rtcBrowserVersion >= 35;
}
var create = document.getElementById('create');
// Compatibility
window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
// Create a WebRTC object
var rtc = new RTCPeerConnection(null);
// Create a data channel
var sendChannel = rtc.createDataChannel('pingtest', {reliable: false});
var myMsg = 'ping';
function setRecvChannel(recvChannel) {
recvChannel.onmessage = function(event) {
if(event.data.indexOf('\x03\x00\x00\x00\x00\x00\x00\x00\x00') === 0) {
console.log('-> ' + window.btoa(event.data));
return; // Received channel's name, ignore
}
console.log('-> ' + event.data);
window.setTimeout(function() {
console.log('<- ' + myMsg);
sendChannel.send(myMsg);
}, 500);
};
}
// Chrome and Firefox
sendChannel.onopen = function(event) {
setRecvChannel(sendChannel);
if(myMsg === 'ping') {
console.log('<- ' + myMsg);
sendChannel.send(myMsg);
}
};
// Firefox
rtc.ondatachannel = function(event) {
setRecvChannel(event.channel);
};
// ICE
rtc.onicecandidate = function(event) {
if(event.candidate) {
console.log('<- ' + JSON.stringify(event.candidate));
ws.send(JSON.stringify(event.candidate));
}
};
// Signaling channel
var ws = new WebSocket('ws://127.0.0.1:49300/');
ws.onopen = function() {
create.disabled = false;
};
ws.onmessage = function(event) {
console.log('-> ' + event.data);
var data = JSON.parse(event.data);
if(data.sdp) {
rtc.setRemoteDescription(new RTCSessionDescription(data));
if(data.type === 'offer') {
myMsg = 'pong';
rtc.createAnswer(function(anwser) {
rtc.setLocalDescription(anwser, function () {
console.log('<- ' + JSON.stringify(anwser));
ws.send(JSON.stringify(anwser));
});
}, console.error);
}
}
// CHANGE: Chrome with offer bug asked to initiate the offer.
else if(data.initiate === true){
doOffer();
}
else {
rtc.addIceCandidate(new RTCIceCandidate(data));
}
};
ws.onclose = function() {
create.disabled = true;
};
// Create an offer
// CHANGE: Create function for offer, so that it may be called from ws.onmessage
function doOffer(){
rtc.createOffer(function(offer) {
rtc.setLocalDescription(offer, function () {
offer.sdp = offer.sdp;
console.log(offer.sdp);
console.log('<- ' + JSON.stringify(offer));
ws.send(JSON.stringify(offer));
});
}, console.error);
}
create.onclick = function() {
// CHANGE: If this client is not able to negotiate a data channel, send a
// message to the peer asking them to offer the channel.
if(rtcCanInitiateDataOffer){
doOffer();
}
else {
ws.send(JSON.stringify({initiate:true}));
}
};
Related Issues:
WebRTC - Test SCTP data channel interop with Firefox Closed as Fixed 11/25/13
Chromium - SCTP Data Channel fails to connect if Chrome offers and FF answers - Closed as Fixed 11/4/13
It is also worth noting that there is currently a max message buffer size of 16KB when sending data to a Chrome peer. If you are sending large data messages, you will need to break them into 16KB chunks before transmitting them over the data channel.
FYI, I revised the HTML further to try and troubleshoot a problem I'm having talking to the C++ libjingle (neither Chrome, nor Firefox) ;)
This version shows on-page status. Not pretty, but useful.
<!DOCTYPE html>
<html>
<head>
<title>WebRTC test</title>
<meta charset="utf-8">
</head>
<body>
<button id="create" disabled>Create data channel</button>
<div id="output1">output1</div>
<div id="output2">output2</div>
<div id="output3">output3</div>
<div id="output4">output4</div>
<div id="output5">output5</div>
<div id="output6">output6</div>
<div id="output7">output7</div>
<div id="output8">output8</div>
<script type="text/javascript">
var myCounts = {
output1: 0,
output2: 0
};
var server1 = 'ws://127.0.0.1:49300/';
// CHANGE: Add basic browser detection based on google's adapter.js file.
var rtcBrowserVersion = 0;
var rtcCanInitiateDataOffer = false;
if (navigator.mozGetUserMedia) {
rtcBrowserVersion = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
rtcCanInitiateDataOffer = true;
} else if (navigator.webkitGetUserMedia) {
// Chrome Canary reports major version 35 for me. Can't find a reliable resource to confirm
// canary versions.
rtcBrowserVersion = parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10);
rtcCanInitiateDataOffer = rtcBrowserVersion >= 35;
}
// DOM
var create = document.getElementById('create');
// Compatibility
window.RTCPeerConnection = window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
window.RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.webkitRTCSessionDescription;
window.RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.webkitRTCIceCandidate;
// Create a WebRTC object
var rtc = new RTCPeerConnection(null);
// Create a data channel
var sendChannel = rtc.createDataChannel('PingTest', {reliable: false});
var myMsg = 'ping';
function setRecvChannel(recvChannel) {
recvChannel.onmessage = function(event) {
myCounts.output1++;
document.getElementById("output1").innerHTML = myCounts.output1 + ": " + event.data;
if(event.data.indexOf('\x03\x00\x00\x00\x00\x00\x00\x00\x00') === 0) {
console.log('-> ' + window.btoa(event.data));
return; // Received channel's name, ignore
};
console.log('-> ' + event.data);
window.setTimeout(function() {
console.log('<- ' + myMsg);
sendChannel.send(myMsg);
}, 500);
};
}
// Chrome and Firefox
sendChannel.onopen = function(event) {
setRecvChannel(sendChannel);
if(myMsg === 'ping') {
console.log('<- ' + myMsg);
sendChannel.send(myMsg);
}
};
// Firefox
rtc.ondatachannel = function(event) {
myCounts.output2++;
document.getElementById("output2").innerHTML = myCounts.output2 + " channel: " + event.channel.label;
setRecvChannel(event.channel);
};
// ICE
rtc.onicecandidate = function(event) {
if(event.candidate) {
console.log('<- ' + JSON.stringify(event.candidate));
ws.send(JSON.stringify(event.candidate));
}
};
// Signaling channel
var ws = new WebSocket(server1);
document.getElementById("output3").innerHTML="created WebSocket (not connected) " + server1;
ws.onopen = function() {
create.disabled = false;
document.getElementById("output3").innerHTML="onOpen WebSocket " + server1;
};
ws.onmessage = function(event) {
document.getElementById("output3").innerHTML="onMessage WebSocket " + event.data;
console.log('-> ' + event.data);
var data = JSON.parse(event.data);
if (data.sdp) {
rtc.setRemoteDescription(new RTCSessionDescription(data));
if (data.type === 'offer') {
document.getElementById("output4").innerHTML="received SessiionDescription offer";
myMsg = 'pong';
rtc.createAnswer(function(anwser) {
rtc.setLocalDescription(anwser, function () {
console.log('<- ' + JSON.stringify(anwser));
ws.send(JSON.stringify(anwser));
});
}, console.error);
}
else if (data.type == 'answer') {
document.getElementById("output6").innerHTML="received SessiionDescription answer";
};
}
// CHANGE: Chrome with offer bug asked to initiate the offer.
else if (data.reverseInitiate === true) {
document.getElementById("output8").innerHTML="was asked to reverseInitiate, I doOffer";
doOffer();
}
else {
rtc.addIceCandidate(new RTCIceCandidate(data));
}
};
ws.onclose = function() {
create.disabled = true;
};
// Create an offer
// CHANGE: Create function for offer, so that it may be called from ws.onmessage
function doOffer(){
rtc.createOffer(function(offer) {
rtc.setLocalDescription(offer, function () {
offer.sdp = offer.sdp;
console.log(offer.sdp);
console.log('<- ' + JSON.stringify(offer));
ws.send(JSON.stringify(offer));
});
}, console.error);
}
create.onclick = function() {
// CHANGE: If this client is not able to negotiate a data channel, send a
// message to the peer asking them to offer the channel.
if (rtcCanInitiateDataOffer){
doOffer();
}
else {
document.getElementById("output7").innerHTML="sending reverseInitiate";
ws.send(JSON.stringify( {reverseInitiate:true} ));
};
};
</script>
</body>
</html>
I am trying to develop a Video Calling/Conferencing application using WebRTC and node.js.
Right now there is no facility to control bandwidth during during video call. Is there any way to control/reduce bandwidth.
(like I want make whole my web application to work on 150 kbps while video conferencing).
Any suggestions are highly appreciated.
Thanks in advance.
Try this demo. You can inject bandwidth attributes (b=AS) in the session descriptions:
audioBandwidth = 50;
videoBandwidth = 256;
function setBandwidth(sdp) {
sdp = sdp.replace(/a=mid:audio\r\n/g, 'a=mid:audio\r\nb=AS:' + audioBandwidth + '\r\n');
sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + videoBandwidth + '\r\n');
return sdp;
}
// ----------------------------------------------------------
peer.createOffer(function (sessionDescription) {
sessionDescription.sdp = setBandwidth(sessionDescription.sdp);
peer.setLocalDescription(sessionDescription);
}, null, constraints);
peer.createAnswer(function (sessionDescription) {
sessionDescription.sdp = setBandwidth(sessionDescription.sdp);
peer.setLocalDescription(sessionDescription);
}, null, constraints);
b=AS is already present in sdp for data m-line; its default value is 50.
Updated at Sept 23, 2015
Here is a library that provides full control over both audio/video tracks' bitrates:
// here is how to use it
var bandwidth = {
screen: 300, // 300kbits minimum
audio: 50, // 50kbits minimum
video: 256 // 256kbits (both min-max)
};
var isScreenSharing = false;
sdp = BandwidthHandler.setApplicationSpecificBandwidth(sdp, bandwidth, isScreenSharing);
sdp = BandwidthHandler.setVideoBitrates(sdp, {
min: bandwidth.video,
max: bandwidth.video
});
sdp = BandwidthHandler.setOpusAttributes(sdp);
Here is the library code. Its quite big but it works!
// BandwidthHandler.js
var BandwidthHandler = (function() {
function setBAS(sdp, bandwidth, isScreen) {
if (!!navigator.mozGetUserMedia || !bandwidth) {
return sdp;
}
if (isScreen) {
if (!bandwidth.screen) {
console.warn('It seems that you are not using bandwidth for screen. Screen sharing is expected to fail.');
} else if (bandwidth.screen < 300) {
console.warn('It seems that you are using wrong bandwidth value for screen. Screen sharing is expected to fail.');
}
}
// if screen; must use at least 300kbs
if (bandwidth.screen && isScreen) {
sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, '');
sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + bandwidth.screen + '\r\n');
}
// remove existing bandwidth lines
if (bandwidth.audio || bandwidth.video || bandwidth.data) {
sdp = sdp.replace(/b=AS([^\r\n]+\r\n)/g, '');
}
if (bandwidth.audio) {
sdp = sdp.replace(/a=mid:audio\r\n/g, 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n');
}
if (bandwidth.video) {
sdp = sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + (isScreen ? bandwidth.screen : bandwidth.video) + '\r\n');
}
return sdp;
}
// Find the line in sdpLines that starts with |prefix|, and, if specified,
// contains |substr| (case-insensitive search).
function findLine(sdpLines, prefix, substr) {
return findLineInRange(sdpLines, 0, -1, prefix, substr);
}
// Find the line in sdpLines[startLine...endLine - 1] that starts with |prefix|
// and, if specified, contains |substr| (case-insensitive search).
function findLineInRange(sdpLines, startLine, endLine, prefix, substr) {
var realEndLine = endLine !== -1 ? endLine : sdpLines.length;
for (var i = startLine; i < realEndLine; ++i) {
if (sdpLines[i].indexOf(prefix) === 0) {
if (!substr ||
sdpLines[i].toLowerCase().indexOf(substr.toLowerCase()) !== -1) {
return i;
}
}
}
return null;
}
// Gets the codec payload type from an a=rtpmap:X line.
function getCodecPayloadType(sdpLine) {
var pattern = new RegExp('a=rtpmap:(\\d+) \\w+\\/\\d+');
var result = sdpLine.match(pattern);
return (result && result.length === 2) ? result[1] : null;
}
function setVideoBitrates(sdp, params) {
params = params || {};
var xgoogle_min_bitrate = params.min;
var xgoogle_max_bitrate = params.max;
var sdpLines = sdp.split('\r\n');
// VP8
var vp8Index = findLine(sdpLines, 'a=rtpmap', 'VP8/90000');
var vp8Payload;
if (vp8Index) {
vp8Payload = getCodecPayloadType(sdpLines[vp8Index]);
}
if (!vp8Payload) {
return sdp;
}
var rtxIndex = findLine(sdpLines, 'a=rtpmap', 'rtx/90000');
var rtxPayload;
if (rtxIndex) {
rtxPayload = getCodecPayloadType(sdpLines[rtxIndex]);
}
if (!rtxIndex) {
return sdp;
}
var rtxFmtpLineIndex = findLine(sdpLines, 'a=fmtp:' + rtxPayload.toString());
if (rtxFmtpLineIndex !== null) {
var appendrtxNext = '\r\n';
appendrtxNext += 'a=fmtp:' + vp8Payload + ' x-google-min-bitrate=' + (xgoogle_min_bitrate || '228') + '; x-google-max-bitrate=' + (xgoogle_max_bitrate || '228');
sdpLines[rtxFmtpLineIndex] = sdpLines[rtxFmtpLineIndex].concat(appendrtxNext);
sdp = sdpLines.join('\r\n');
}
return sdp;
}
function setOpusAttributes(sdp, params) {
params = params || {};
var sdpLines = sdp.split('\r\n');
// Opus
var opusIndex = findLine(sdpLines, 'a=rtpmap', 'opus/48000');
var opusPayload;
if (opusIndex) {
opusPayload = getCodecPayloadType(sdpLines[opusIndex]);
}
if (!opusPayload) {
return sdp;
}
var opusFmtpLineIndex = findLine(sdpLines, 'a=fmtp:' + opusPayload.toString());
if (opusFmtpLineIndex === null) {
return sdp;
}
var appendOpusNext = '';
appendOpusNext += '; stereo=' + (typeof params.stereo != 'undefined' ? params.stereo : '1');
appendOpusNext += '; sprop-stereo=' + (typeof params['sprop-stereo'] != 'undefined' ? params['sprop-stereo'] : '1');
if (typeof params.maxaveragebitrate != 'undefined') {
appendOpusNext += '; maxaveragebitrate=' + (params.maxaveragebitrate || 128 * 1024 * 8);
}
if (typeof params.maxplaybackrate != 'undefined') {
appendOpusNext += '; maxplaybackrate=' + (params.maxplaybackrate || 128 * 1024 * 8);
}
if (typeof params.cbr != 'undefined') {
appendOpusNext += '; cbr=' + (typeof params.cbr != 'undefined' ? params.cbr : '1');
}
if (typeof params.useinbandfec != 'undefined') {
appendOpusNext += '; useinbandfec=' + params.useinbandfec;
}
if (typeof params.usedtx != 'undefined') {
appendOpusNext += '; usedtx=' + params.usedtx;
}
if (typeof params.maxptime != 'undefined') {
appendOpusNext += '\r\na=maxptime:' + params.maxptime;
}
sdpLines[opusFmtpLineIndex] = sdpLines[opusFmtpLineIndex].concat(appendOpusNext);
sdp = sdpLines.join('\r\n');
return sdp;
}
return {
setApplicationSpecificBandwidth: function(sdp, bandwidth, isScreen) {
return setBAS(sdp, bandwidth, isScreen);
},
setVideoBitrates: function(sdp, params) {
return setVideoBitrates(sdp, params);
},
setOpusAttributes: function(sdp, params) {
return setOpusAttributes(sdp, params);
}
};
})();
Here is how to set advance opus bitrate parameters:
sdp = BandwidthHandler.setOpusAttributes(sdp, {
'stereo': 0, // to disable stereo (to force mono audio)
'sprop-stereo': 1,
'maxaveragebitrate': 500 * 1024 * 8, // 500 kbits
'maxplaybackrate': 500 * 1024 * 8, // 500 kbits
'cbr': 0, // disable cbr
'useinbandfec': 1, // use inband fec
'usedtx': 1, // use dtx
'maxptime': 3
});
A more up-to-date answer
const videobitrate = 20000;
var offer = pc.localDescription;
// Set bandwidth for video
offer.sdp = offer.sdp.replace(/(m=video.*\r\n)/g, `$1b=AS:${videobitrate}\r\n`);
pc.setLocalDescription(offer);
Explanation: a=mid:video is not a guaranteed tag. For receive only video, you might not see it or see a=mid:0. Generally it's a better bet to look for the m=video xxxx xxxx (or similar audio) tag and append the bandwidth parameters underneath
Not sure if this helps, but you can limit the video resolution from getUserMedia with constraints: see demo at simpl.info/getusermedia/constraints/.
My answer is not for node.js, but maybe someone stuck with controlling webrtc bandwidth while developing a native phone app (iOS, android).
So, at least in version GoogleWebRTC (1.1.31999) for iOS and org.webrtc:google-webrtc:1.0.22672 for android there is method in PeerConnection instance.
For iOS:
let updateBitrateSuccessful = pc.setBweMinBitrateBps(300000, currentBitrateBps: 1000000, maxBitrateBps: 3000000)
print("Update rtc connection bitrate " + (updateBitrateSuccessful ? "successful" : "failed"))
Respectively, for Android:
boolean updateBitrateSuccessful = pc.setBitrate(300000, 1000000, 3000000);
Log.d("AppLog", "Update rtc connection bitrate " + (updateBitrateSuccessful ? "successful" : "failed"));
It depends on what SFU media server you're using. But in short, your media server needs to tell the client browser what maximum bitrate it should send, by setting the bandwidth attribute in the answer SDP, as well as in the REMB message it periodically sends.
The REMB (receiver estimated maximum bitrate) applies separately to audio and video streams (at least on desktop Chrome and Firefox that I tested). So if REMB is set to 75kps and you have one audio and one video stream, then each will confine to 75kps for a total transport bitrate of 150kps. You should use chrome://webrtc-internals to test and verify this.
If you are using OPUS as the audio codec, you can control the audio bandwidth separately by setting the maxaveragebitrate attribute in the answer SDP. Setting this attribute will override the REMB value (verified on Chrome). So you can set audio bitrate to 16kps and the video bitrate (via REMB) to 134kps for a combined transport bitrate of 150.
Note that the REMB is sent by your server, so your server needs to support this. The other SDP attributes can be manipulated on the client side by modifying the answer SDP that you receive, right before passing it to setRemoteDescription().
This is my limited understanding based on online research. It's not based on deep knowledge of the technology stack, so please take it with a grain of salt.
You should also be able to use bandwidth constraints on the stream (see this demo), but it doesn't appear to be working, even in the latest canary (29.0.1529.3).
There's some discussion of the SDP-based approach on the discuss-webrtc mailing list, which links to WebRTC bug 1846.
I did it Yesterday and it works like a charm! in my case, it was needed to prevent slow and old phones get freeze during a videocall! have a look
function handle_offer_sdp(offer) {
let sdp = offer.sdp.split('\r\n');//convert to an concatenable array
let new_sdp = '';
let position = null;
sdp = sdp.slice(0, -1); //remove the last comma ','
for(let i = 0; i < sdp.length; i++) {//look if exists already a b=AS:XXX line
if(sdp[i].match(/b=AS:/)) {
position = i; //mark the position
}
}
if(position) {
sdp.splice(position, 1);//remove if exists
}
for(let i = 0; i < sdp.length; i++) {
if(sdp[i].match(/m=video/)) {//modify and add the new lines for video
new_sdp += sdp[i] + '\r\n' + 'b=AS:' + '128' + '\r\n';
}
else {
if(sdp[i].match(/m=audio/)) { //modify and add the new lines for audio
new_sdp += sdp[i] + '\r\n' + 'b=AS:' + 64 + '\r\n';
}
else {
new_sdp += sdp[i] + '\r\n';
}
}
}
return new_sdp; //return the new sdp
}
pc.createOffer(function(offer) {
offer.sdp = handle_offer_sdp(offer); //invoke function saving the new sdp
pc.setLocalDescription(offer);
}, function(error) {
console.log('error -> ' + error);
});
I recommend to change value of maxBitrate property as described here https://stackoverflow.com/a/71223675/1199820
Check this, this works for me.
Control your bitrate via getSenders(), after peer is connected then you can set your maximum bitrate.
This method allow you to control bitrate without renegotiation. so,
you can change the streaming quality during a call
//bandwidth => "unlimited", 75 kbps, 250 kbps, 1000 kbps, 2000 kbps
var bandwidth = 75;
const sender = pc1.getSenders()[0];
const parameters = sender.getParameters();
if (!parameters.encodings) {
parameters.encodings = [{}];
}
if (bandwidth === 'unlimited') {
delete parameters.encodings[0].maxBitrate;
} else {
parameters.encodings[0].maxBitrate = bandwidth * 1000;
}
sender.setParameters(parameters)
.then(() => {
// on success
})
.catch(e => console.error(e));
Reference code & demo