789 lines
21 KiB
JavaScript
789 lines
21 KiB
JavaScript
var displayStats = false;
|
|
|
|
var socket = null;
|
|
var ReconnectDelay = 0;
|
|
var reconnectTimeout;
|
|
var connected;
|
|
|
|
var chatprefix = "";
|
|
var voice = false;
|
|
var ptt = false;
|
|
var nickname = "Anonymous";
|
|
|
|
var numConnections = 3;
|
|
var peerConnections = [];
|
|
var RTCConstraints = {
|
|
audio: {
|
|
channelCount: {exact: 2},
|
|
autoGainControl: false,
|
|
echoCancellation: false,
|
|
noiseSuppression: false,
|
|
sampleRate: 48000,
|
|
sampleSize: 16,
|
|
volume: 1.0
|
|
},
|
|
video: false
|
|
};
|
|
var RTCOfferOptions = {
|
|
offerToReceiveAudio: 1,
|
|
offerToReceiveVideo: 0,
|
|
voiceActivityDetection: false
|
|
};
|
|
var RTCICEServers = [{urls: 'stun:stun.l.google.com:19302'}];
|
|
var audioTrack;
|
|
|
|
var shownPTTHelp = false;
|
|
var muteOnMouseUp = true;
|
|
|
|
var lastPing = 0;
|
|
var userPing = 0;
|
|
|
|
var allChannels;
|
|
var voiceChannel = 0;
|
|
var disableVoice = false;
|
|
var voiceCompatibilityNotice = "Sorry, your browser does not support WebRTC. This is required join voice channels. Firefox (recommended) and Chrome support WebRTC.";
|
|
|
|
var channelList = '';
|
|
var userList = '';
|
|
var userListStatus = 'Loading...';
|
|
|
|
var MessageBinary = 2;
|
|
var MessagePing = 101;
|
|
var MessagePong = 102;
|
|
var MessageCall = 103;
|
|
var MessageAnswer = 104;
|
|
var MessageConnect = 110;
|
|
var MessageJoin = 111;
|
|
var MessageQuit = 112;
|
|
var MessageNick = 113;
|
|
var MessageTopic = 114;
|
|
var MessageAction = 115;
|
|
var MessageDisconnect = 119;
|
|
var MessageChat = 120;
|
|
var MessageTypingStart = 121;
|
|
var MessageTypingStop = 122;
|
|
var MessageTransmitStart = 123;
|
|
var MessageTransmitStop = 124;
|
|
var MessageServers = 130;
|
|
var MessageChannels = 131;
|
|
var MessageUsers = 132;
|
|
|
|
var ChannelUnknown = 0;
|
|
var ChannelAll = 1;
|
|
var ChannelText = 2;
|
|
var ChannelVoice = 3;
|
|
|
|
var tagsToReplace = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>'
|
|
};
|
|
|
|
$(document).keydown(HandleInput);
|
|
$(document).keyup(HandleInput);
|
|
|
|
function HandleInput(e) {
|
|
if (e.which == 119) {
|
|
if (e.type == "keydown") {
|
|
StartPTT();
|
|
} else if (e.type == "keyup") {
|
|
StopPTT();
|
|
}
|
|
|
|
e.preventDefault();
|
|
} else if ((e.which == 13 || e.which == 176) && e.type == "keydown" && !e.shiftKey) {
|
|
if (!$("#chatinput").is(":focus")) {
|
|
return;
|
|
}
|
|
|
|
if ($("#chatinput").val() != "") {
|
|
if ($("#chatinput").val() == "/debugstats") {
|
|
enableStats();
|
|
} else {
|
|
w(MessageChat, $("#chatinput").val());
|
|
}
|
|
}
|
|
|
|
$("#chatinput").val('');
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
$(document).ready(function () {
|
|
// Confirm voice chat support
|
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia || !window.RTCPeerConnection) {
|
|
alert(voiceCompatibilityNotice);
|
|
Log(voiceCompatibilityNotice);
|
|
|
|
disableVoice = true;
|
|
}
|
|
|
|
$("#voiceptt").on("touchstart", function (e) {
|
|
muteOnMouseUp = false;
|
|
|
|
StartPTT();
|
|
|
|
$("#chatinput").focus();
|
|
|
|
return false;
|
|
});
|
|
|
|
$("#voiceptt").on("mousedown", function (e) {
|
|
muteOnMouseUp = true;
|
|
|
|
StartPTT();
|
|
|
|
$("#chatinput").focus();
|
|
|
|
return false;
|
|
});
|
|
|
|
$(document).on("mouseup", function (e) {
|
|
if (!muteOnMouseUp) {
|
|
return;
|
|
}
|
|
|
|
StopPTT();
|
|
|
|
return false;
|
|
});
|
|
|
|
$(document).on("touchend", function (e) {
|
|
StopPTT();
|
|
|
|
return false;
|
|
});
|
|
|
|
$("#chatinputcontainer,#voicepttcontainer").on("click touchstart", function (e) {
|
|
$("#chatinput").focus();
|
|
|
|
return false;
|
|
});
|
|
|
|
$("#chatinput").on("touchstart", function (e) {
|
|
$("#chatinput").focus();
|
|
|
|
return false;
|
|
});
|
|
|
|
window.setInterval(() => {
|
|
if (!webSocketReady()) {
|
|
return;
|
|
}
|
|
|
|
pingServer();
|
|
}, 15000);
|
|
|
|
nickname = prompt("What is your name?", nickname);
|
|
Connect();
|
|
});
|
|
|
|
function createPeerConnection(id) {
|
|
var pc = new RTCPeerConnection({
|
|
iceServers: RTCICEServers
|
|
});
|
|
pc.onicecandidate = event => {
|
|
if (event.candidate === null) {
|
|
|
|
}
|
|
};
|
|
|
|
if (id > 0) {
|
|
pc.addTransceiver('audio', {'direction': 'recvonly'});
|
|
}
|
|
|
|
pc.ontrack = function (event) {
|
|
console.log(`PC ${id} onTrack ${event.streams.length} ${event.streams[0].id} ${event.streams[0].getAudioTracks().length}`);
|
|
|
|
if (id > 0) {
|
|
pc.addTransceiver(event.streams[0].getAudioTracks()[0], {'direction': 'sendrecv'});
|
|
}
|
|
|
|
$("#hidden").append('<audio id="audiostream' + id + '"></audio>');
|
|
|
|
var el = document.getElementById('audiostream' + id);
|
|
el.autoplay = true;
|
|
el.srcObject = event.streams[0];
|
|
|
|
if (id == 0) {
|
|
if (!shownPTTHelp) {
|
|
shownPTTHelp = true;
|
|
|
|
Log("Note: Push-to-talk is bound to <F8>");
|
|
}
|
|
|
|
$('#voicepttcontainer').css('display', 'block');
|
|
updateStatus();
|
|
}
|
|
};
|
|
|
|
if (id == 0) {
|
|
navigator.mediaDevices.getUserMedia(RTCConstraints).then(localStream => {
|
|
audioTracks = localStream.getAudioTracks();
|
|
if (audioTracks.length > 0) {
|
|
console.log(`PC ${id} Using Audio device: ${audioTracks[0].label} - tracks available: ${audioTracks}`);
|
|
}
|
|
|
|
audioTrack = audioTracks[0];
|
|
|
|
pc.addTrack(audioTrack);
|
|
pc.getSenders()[0].replaceTrack(null);
|
|
|
|
pc.createOffer(RTCOfferOptions)
|
|
.then(onRTCDescription(id), onRTCDescriptionError(id));
|
|
}).catch(Log);
|
|
} else {
|
|
pc.createOffer(RTCOfferOptions)
|
|
.then(onRTCDescription(id), onRTCDescriptionError(id));
|
|
}
|
|
|
|
return pc;
|
|
}
|
|
|
|
function onRTCDescriptionError(id) {
|
|
return function (error) {
|
|
console.log(`Failed to create/set session description: ${error.toString()}`);
|
|
}
|
|
}
|
|
|
|
function onRTCDescription(id) {
|
|
return function (desc) {
|
|
console.log(`PC ${id} Offer received \n${desc.sdp}`);
|
|
|
|
if (peerConnections.length < id) {
|
|
return;
|
|
}
|
|
|
|
peerConnections[id].setLocalDescription(desc)
|
|
.then(() => {
|
|
desc.sdp = maybePreferCodec(desc.sdp, 'audio', 'send', 'opus/48000');
|
|
desc.sdp = maybePreferCodec(desc.sdp, 'audio', 'receive', 'opus/48000');
|
|
|
|
console.log(`PC ${id} onRTCDescription`);
|
|
|
|
wpc(id, MessageCall, desc.sdp);
|
|
}, onRTCDescriptionError);
|
|
}
|
|
}
|
|
|
|
function Connect() {
|
|
reconnectTimeout = null;
|
|
if (webSocketReady() || ReconnectDelay === -1) {
|
|
return;
|
|
}
|
|
|
|
userPing = 0;
|
|
userListStatus = 'Connecting...';
|
|
updateStatus();
|
|
|
|
Log("Connecting...");
|
|
|
|
var loc = window.location, wsurl, pathname;
|
|
if (loc.protocol === "https:") {
|
|
wsurl = "wss:";
|
|
} else {
|
|
wsurl = "ws:";
|
|
}
|
|
if (loc.pathname && loc.pathname !== "") {
|
|
pathname = loc.pathname;
|
|
} else {
|
|
pathname = "/";
|
|
}
|
|
wsurl += "//" + loc.host + pathname + "w";
|
|
|
|
socket = new WebSocket(wsurl);
|
|
socket.onerror = function (e) {
|
|
Log("Connection error");
|
|
console.log(e);
|
|
};
|
|
socket.onopen = function (e) {
|
|
if (reconnectTimeout != null) {
|
|
clearTimeout(reconnectTimeout);
|
|
}
|
|
|
|
userListStatus = "";
|
|
updateStatus();
|
|
|
|
w(MessageNick, nickname);
|
|
|
|
pingServer();
|
|
};
|
|
socket.onmessage = function (e) {
|
|
if (ReconnectDelay > 0) {
|
|
ReconnectDelay = 0;
|
|
}
|
|
|
|
try {
|
|
if (typeof e.data === "string") {
|
|
var p = JSON.parse(e.data);
|
|
if (p.M !== undefined) {
|
|
p.M = atob(p.M);
|
|
}
|
|
|
|
if (p.T == MessagePong) {
|
|
if (parseInt(p.M, 10) == lastPing) {
|
|
userPing = Date.now() - lastPing;
|
|
|
|
updateStatus();
|
|
}
|
|
} else if (p.T == MessageAnswer) {
|
|
if (p.PC === undefined || p.PC > peerConnections.length) {
|
|
return;
|
|
}
|
|
|
|
var pc = peerConnections[p.PC];
|
|
pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: p.M}));
|
|
} else if (p.T == MessageConnect) {
|
|
if (p.N === undefined) {
|
|
return;
|
|
}
|
|
|
|
Log(escapeEntities(p.N) + " connected");
|
|
} else if (p.T == MessageJoin) {
|
|
if (p.C === undefined || p.N === undefined) {
|
|
return;
|
|
}
|
|
|
|
Log(escapeEntities(p.N) + " joined " + allChannels[p.C].Name);
|
|
} else if (p.T == MessageQuit) {
|
|
if (p.C === undefined || p.N === undefined) {
|
|
return;
|
|
}
|
|
|
|
Log(escapeEntities(p.N) + " quit " + allChannels[p.C].Name);
|
|
} else if (p.T == MessageDisconnect) {
|
|
if (p.N === undefined) {
|
|
return;
|
|
}
|
|
|
|
Log(escapeEntities(p.N) + " disconnected");
|
|
} else if (p.T == MessageNick) {
|
|
if (p.N === undefined) {
|
|
return;
|
|
}
|
|
|
|
Log(escapeEntities(p.N) + " is now known as " + escapeEntities(p.M));
|
|
} else if (p.T == MessageChat) {
|
|
if (p.N === undefined) {
|
|
return;
|
|
}
|
|
|
|
Log("<" + escapeEntities(p.N) + "> " + p.M);
|
|
} else if (p.T == MessageChannels) {
|
|
allChannels = JSON.parse(p.M);
|
|
|
|
updateChannelList();
|
|
} else if (p.T == MessageUsers) {
|
|
var userListNew = '<ul>';
|
|
|
|
var u = JSON.parse(p.M);
|
|
userListNew += '<li><b>' + u.length + ' user' + (u.length != 1 ? 's' : '') + '</b></li>';
|
|
for (let i = 0; i < u.length; i++) {
|
|
userListNew += '<li>' + u[i].N + '</li>';
|
|
}
|
|
|
|
userListNew += '</ul>';
|
|
|
|
userList = userListNew;
|
|
|
|
var userListVoiceNew = '';
|
|
|
|
var u = JSON.parse(p.M);
|
|
for (let i in allChannels) {
|
|
$("#userscontainer" + i).remove();
|
|
}
|
|
|
|
for (let i = 0; i < u.length; i++) {
|
|
if (u[i].C == 0) {
|
|
continue;
|
|
}
|
|
|
|
if ($("#userscontainer" + u[i].C).length == 0) {
|
|
$('<li/>')
|
|
.attr('id', 'userscontainer' + u[i].C)
|
|
.insertAfter($("#channelvoice" + u[i].C));
|
|
|
|
$('<ul/>')
|
|
.attr('id', 'users' + u[i].C)
|
|
.appendTo($("#userscontainer" + u[i].C));
|
|
}
|
|
|
|
userListVoiceNew = '<li>';
|
|
if (voice && u[i].C == voiceChannel) {
|
|
userListVoiceNew += '<span id="voiceindicator' + u[i].ID + '"><div class="voiceinactive">🔈</div></span> ';
|
|
}
|
|
userListVoiceNew += u[i].N + '</li>';
|
|
|
|
$("#users" + u[i].C).append(userListVoiceNew);
|
|
}
|
|
|
|
updateUserList();
|
|
} else if (p.T == MessageTransmitStart || p.T == MessageTransmitStop) {
|
|
if (!voice || p.S === undefined) {
|
|
return;
|
|
}
|
|
|
|
$("#voiceindicator" + p.S).html(p.T == MessageTransmitStart ? '<div class="voiceactive">🔊</div>' : '<div class="voiceinactive">🔈</div>');
|
|
}
|
|
} else {
|
|
// TODO Binary data
|
|
}
|
|
} catch (e) {
|
|
console.log(e);
|
|
}
|
|
};
|
|
socket.onclose = function (e) {
|
|
connected = false;
|
|
Log("Disconnected");
|
|
if (ReconnectDelay < 0 || reconnectTimeout != null) {
|
|
return;
|
|
}
|
|
|
|
ReconnectDelay += (ReconnectDelay * 2) + 1;
|
|
|
|
var waitTime = ReconnectDelay;
|
|
console.log("Reconnecting in " + ReconnectDelay + " seconds...");
|
|
reconnectTimeout = setTimeout(Connect, waitTime * 1000);
|
|
|
|
if (ReconnectDelay > 10) {
|
|
ReconnectDelay = 10;
|
|
}
|
|
};
|
|
}
|
|
|
|
function pingServer() {
|
|
lastPing = Date.now();
|
|
w(MessagePing, lastPing);
|
|
}
|
|
|
|
function webSocketReady() {
|
|
return (socket !== null && socket.readyState === 1);
|
|
}
|
|
|
|
function waitForSocketConnection(socket, callback) {
|
|
setTimeout(function () {
|
|
if (webSocketReady()) {
|
|
if (callback != null) {
|
|
callback();
|
|
}
|
|
} else {
|
|
waitForSocketConnection(socket, callback);
|
|
}
|
|
}, 250);
|
|
}
|
|
|
|
function Log(msg) {
|
|
if (msg.charAt(0) != '<' && msg.charAt(0) != '&' && msg.charAt(1) != '&' && msg.charAt(2) != 't' && msg.charAt(3) != ';') {
|
|
msg = '* ' + msg;
|
|
}
|
|
|
|
var date = new Date();
|
|
$('#chathistory').append(chatprefix + date.getHours() + ":" + (date.getMinutes() < 10 ? "0" : "") + date.getMinutes() + " " + msg + "\n")
|
|
$('#chathistory').scrollTop($('#chathistory').prop("scrollHeight"));
|
|
|
|
if (chatprefix == "") {
|
|
chatprefix = "<br>";
|
|
}
|
|
}
|
|
|
|
function StartPTT() {
|
|
if (ptt || !voice || peerConnections.length == 0) {
|
|
return;
|
|
}
|
|
|
|
ptt = true;
|
|
|
|
$("#voiceptt").html('Transmitting...');
|
|
|
|
var sender = peerConnections[0].getSenders()[0];
|
|
sender.replaceTrack(audioTrack);
|
|
|
|
updateStatus();
|
|
}
|
|
|
|
function StopPTT() {
|
|
if (!ptt || !voice || peerConnections.length == 0) {
|
|
return;
|
|
}
|
|
|
|
ptt = false;
|
|
|
|
$("#voiceptt").html('Push-To-Talk');
|
|
|
|
var sender = peerConnections[0].getSenders()[0];
|
|
sender.replaceTrack(null);
|
|
|
|
updateStatus();
|
|
}
|
|
|
|
function JoinVoice(channelID) {
|
|
if (!webSocketReady()) {
|
|
return;
|
|
}
|
|
|
|
voiceChannel = parseInt(channelID);
|
|
for (let i in allChannels) {
|
|
$("#joinvoice" + i).html(i != voiceChannel ? 'Join' : 'Quit');
|
|
}
|
|
|
|
voice = true;
|
|
|
|
if (peerConnections.length == 0) {
|
|
var i;
|
|
for (i = 0; i < numConnections; i++) {
|
|
peerConnections.push(createPeerConnection(i));
|
|
}
|
|
}
|
|
|
|
socket.send(JSON.stringify({T: MessageJoin, C: parseInt(channelID)}));
|
|
}
|
|
|
|
function QuitVoice() {
|
|
for (let i in allChannels) {
|
|
$("#joinvoice" + i).html('Join');
|
|
}
|
|
|
|
voice = false;
|
|
voiceChannel = 0;
|
|
|
|
w(MessageQuit, "");
|
|
|
|
peerConnections.forEach(function (pc) {
|
|
pc.close();
|
|
});
|
|
peerConnections = [];
|
|
|
|
$('#voicepttcontainer').css('display', 'none');
|
|
|
|
updateStatus();
|
|
}
|
|
|
|
function updateStatus() {
|
|
var out = '';
|
|
if (userPing > 0) {
|
|
out += userPing + 'ms ping';
|
|
}
|
|
|
|
if (userListStatus != '') {
|
|
if (out != '') {
|
|
out += '<br>';
|
|
}
|
|
|
|
out += userListStatus;
|
|
}
|
|
|
|
$('#userstatus').html(out);
|
|
|
|
$('#togglevoice').html(peerConnections.length > 0 ? 'Quit' : 'Join');
|
|
}
|
|
|
|
function updateChannelList() {
|
|
var c;
|
|
var channelListNew = '<ul>';
|
|
|
|
channelListNew += '<li style="margin-bottom: 10px;"><div class="headericon">📰</div> Text Channels</li>';
|
|
for (let i in allChannels) {
|
|
c = allChannels[i];
|
|
|
|
if (c.Type != ChannelAll && c.Type != ChannelText) {
|
|
continue;
|
|
}
|
|
|
|
channelListNew += '<li id="channeltext' + c.ID + '">' + c.Name + '</li>';
|
|
}
|
|
|
|
channelListNew += '<li style="margin-bottom: 10px;"> </li><li style="margin-bottom: 10px;"><div class="headericon">🔊</div> Voice Channels</li>';
|
|
for (let i in allChannels) {
|
|
c = allChannels[i];
|
|
|
|
if (c.Type != ChannelAll && c.Type != ChannelVoice) {
|
|
continue;
|
|
}
|
|
|
|
channelListNew += '<li id="channelvoice' + c.ID + '">' + c.Name + ' <a href="#" id="joinvoice' + c.ID + '">' + (parseInt(c.ID) != voiceChannel ? 'Join' : 'Quit') + '</a></li>';
|
|
}
|
|
|
|
channelListNew += '</ul>';
|
|
|
|
channelList = channelListNew;
|
|
|
|
$("#sideleft").html(channelList);
|
|
|
|
for (let i in allChannels) {
|
|
c = allChannels[i];
|
|
|
|
if (c.Type != ChannelAll && c.Type != ChannelVoice) {
|
|
continue;
|
|
}
|
|
|
|
$("#joinvoice" + c.ID).click(function (e) {
|
|
if (voiceChannel == parseInt($(this).attr('id').substring(9))) {
|
|
QuitVoice();
|
|
return false;
|
|
}
|
|
|
|
if (disableVoice) {
|
|
alert(voiceCompatibilityNotice);
|
|
return false;
|
|
}
|
|
|
|
voiceChannel = parseInt($(this).attr('id').substring(9));
|
|
JoinVoice(voiceChannel);
|
|
return false;
|
|
});
|
|
}
|
|
}
|
|
|
|
function updateUserList() {
|
|
$('#sideright').html(userList);
|
|
}
|
|
|
|
function w(t, m) {
|
|
if (!webSocketReady()) {
|
|
return;
|
|
}
|
|
|
|
socket.send(JSON.stringify({T: t, M: btoa(m)}));
|
|
}
|
|
|
|
function wpc(pc, t, m) {
|
|
if (!webSocketReady()) {
|
|
return;
|
|
}
|
|
|
|
socket.send(JSON.stringify({PC: parseInt(pc), T: t, M: btoa(m)}));
|
|
}
|
|
|
|
function escapeEntitiesCallback(tag) {
|
|
return tagsToReplace[tag] || tag;
|
|
}
|
|
|
|
function escapeEntities(str) {
|
|
return str.replace(/[&<>]/g, escapeEntitiesCallback);
|
|
}
|
|
|
|
function enableStats() {
|
|
if (displayStats) {
|
|
return;
|
|
}
|
|
|
|
displayStats = true;
|
|
|
|
window.setInterval(() => {
|
|
var i;
|
|
for (i = 0; i < peerConnections.length; i++) {
|
|
var senders = peerConnections[i].getSenders();
|
|
|
|
var j;
|
|
for (j = 0; j < senders.length; j++) {
|
|
senders[j].getStats().then(stats => {
|
|
let statsOutput = "";
|
|
|
|
stats.forEach(report => {
|
|
if (report.type == "local-candidate" || report.type == "remote-candidate") {
|
|
return;
|
|
} else if (report.type == "candidate-pair" && (report.bytesSent == 0 && report.bytesReceived == 0)) {
|
|
return;
|
|
}
|
|
|
|
statsOutput += `<b>PC${i}S${j} Report: ${report.type}</b>\n<strong>ID:</strong> ${report.id}<br>\n` +
|
|
`<strong>Timestamp:</strong> ${report.timestamp}<br>\n`;
|
|
|
|
Object.keys(report).forEach(statName => {
|
|
if (statName !== "id" && statName !== "timestamp" && statName !== "type") {
|
|
statsOutput += `<strong>${statName}:</strong> ${report[statName]}<br>\n`;
|
|
}
|
|
});
|
|
});
|
|
|
|
if (statsOutput != "") {
|
|
Log(statsOutput);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}, 1000);
|
|
|
|
Log("Debug stats enabled");
|
|
}
|
|
|
|
// Copied from AppRTC's sdputils.js:
|
|
|
|
// Sets |codec| as the default |type| codec if it's present.
|
|
// The format of |codec| is 'NAME/RATE', e.g. 'opus/48000'.
|
|
function maybePreferCodec(sdp, type, dir, codec) {
|
|
const str = `${type} ${dir} codec`;
|
|
if (codec === '') {
|
|
console.log(`No preference on ${str}.`);
|
|
return sdp;
|
|
}
|
|
|
|
console.log(`Prefer ${str}: ${codec}`);
|
|
|
|
const sdpLines = sdp.split('\r\n');
|
|
|
|
// Search for m line.
|
|
const mLineIndex = findLine(sdpLines, 'm=', type);
|
|
if (mLineIndex === null) {
|
|
return sdp;
|
|
}
|
|
|
|
// If the codec is available, set it as the default in m line.
|
|
const codecIndex = findLine(sdpLines, 'a=rtpmap', codec);
|
|
console.log('codecIndex', codecIndex);
|
|
if (codecIndex) {
|
|
const payload = getCodecPayloadType(sdpLines[codecIndex]);
|
|
if (payload) {
|
|
sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], payload);
|
|
}
|
|
}
|
|
|
|
sdp = sdpLines.join('\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) {
|
|
const realEndLine = endLine !== -1 ? endLine : sdpLines.length;
|
|
for (let 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) {
|
|
const pattern = new RegExp('a=rtpmap:(\\d+) \\w+\\/\\d+');
|
|
const result = sdpLine.match(pattern);
|
|
return (result && result.length === 2) ? result[1] : null;
|
|
}
|
|
|
|
// Returns a new m= line with the specified codec as the first one.
|
|
function setDefaultCodec(mLine, payload) {
|
|
const elements = mLine.split(' ');
|
|
|
|
// Just copy the first three parameters; codec order starts on fourth.
|
|
const newLine = elements.slice(0, 3);
|
|
|
|
// Put target payload first and copy in the rest.
|
|
newLine.push(payload);
|
|
for (let i = 3; i < elements.length; i++) {
|
|
if (elements[i] !== payload) {
|
|
newLine.push(elements[i]);
|
|
}
|
|
}
|
|
return newLine.join(' ');
|
|
}
|