400 lines
11 KiB
JavaScript
400 lines
11 KiB
JavaScript
var socket = null;
|
|
var ReconnectDelay = 0;
|
|
var reconnectTimeout;
|
|
var connected;
|
|
|
|
var chatprefix = "";
|
|
var voice = false;
|
|
var ptt = false;
|
|
var printStats = false;
|
|
|
|
var pc;
|
|
var RTCOfferOptions = {
|
|
offerToReceiveAudio: 1,
|
|
offerToReceiveVideo: 0,
|
|
voiceActivityDetection: false
|
|
};
|
|
var RTCICEServers = [{urls: 'stun:stun.l.google.com:19302'}];
|
|
var audioTrack;
|
|
|
|
$(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() != "") {
|
|
Log("<tee> " + $("#chatinput").val());
|
|
}
|
|
|
|
$("#chatinput").val('');
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
$(document).ready(function () {
|
|
$("#voiceButtonJoin").on("click", function () {
|
|
pc = new RTCPeerConnection({
|
|
iceServers: RTCICEServers
|
|
});
|
|
pc.onicecandidate = event => {
|
|
if (event.candidate === null) {
|
|
|
|
}
|
|
};
|
|
navigator.mediaDevices.getUserMedia({audio: true, video: false})
|
|
.then(localStream => {
|
|
audioTracks = localStream.getAudioTracks();
|
|
if (audioTracks.length > 0) {
|
|
console.log(`Using Audio device: ${audioTracks[0].label} - tracks available: ${audioTracks}`);
|
|
}
|
|
|
|
audioTrack = audioTracks[0];
|
|
|
|
pc.addTrack(audioTrack);
|
|
pc.getSenders()[0].replaceTrack(null);
|
|
|
|
pc.ontrack = function (event) {
|
|
console.log(`onTrack ${event.streams.length}`);
|
|
|
|
pc.addTransceiver(event.streams[0].getAudioTracks()[0], {'direction': 'sendrecv'});
|
|
|
|
var el = document.getElementById('audioplayer');
|
|
el.autoplay = true;
|
|
el.srcObject = event.streams[0];
|
|
|
|
voice = true;
|
|
|
|
Log("* tee has joined voice chat");
|
|
Log("* Push to talk is bound to F8");
|
|
$('#voiceinactive').css('display', 'none');
|
|
$('#voiceactive').css('display', 'inline-block');
|
|
$('#voiceactiveside').css('display', 'inline-block');
|
|
$('#voiceButton').html('Quit voice chat');
|
|
|
|
updateVoiceStatus();
|
|
};
|
|
|
|
pc.createOffer(RTCOfferOptions)
|
|
.then(onRTCDescription, onRTCDescriptionError);
|
|
}).catch(Log);
|
|
});
|
|
|
|
$("#pushtotalk").on("mousedown", function (e) {
|
|
StartPTT();
|
|
});
|
|
|
|
$(document).on("mouseup", function (e) {
|
|
StopPTT();
|
|
});
|
|
|
|
$("#voiceButtonQuit").on("click", function () {
|
|
pc.close();
|
|
voice = false;
|
|
|
|
Log("* tee has quit voice chat");
|
|
$('#voiceactive').css('display', 'none');
|
|
$('#voiceactiveside').css('display', 'none');
|
|
$('#voiceinactive').css('display', 'inline-block');
|
|
$('#voiceButton').html('Join voice chat');
|
|
|
|
updateVoiceStatus();
|
|
});
|
|
|
|
Connect();
|
|
|
|
window.setInterval(() => {
|
|
if (!webSocketReady()) {
|
|
return;
|
|
}
|
|
|
|
w(100, "ping");
|
|
}, 15000);
|
|
|
|
if (printStats) {
|
|
window.setInterval(() => {
|
|
if (!pc) {
|
|
return;
|
|
}
|
|
const sender = pc.getSenders()[0];
|
|
if (sender === undefined) {
|
|
return;
|
|
}
|
|
|
|
sender.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>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`;
|
|
}
|
|
});
|
|
});
|
|
|
|
document.querySelector("#stats").innerHTML = statsOutput;
|
|
});
|
|
}, 1000);
|
|
}
|
|
});
|
|
|
|
function onRTCDescriptionError(error) {
|
|
console.log(`Failed to create/set session description: ${error.toString()}`);
|
|
}
|
|
|
|
function onRTCDescription(desc) {
|
|
console.log(`Offer from pc\n${desc.sdp}`);
|
|
pc.setLocalDescription(desc)
|
|
.then(() => {
|
|
desc.sdp = maybePreferCodec(desc.sdp, 'audio', 'send', 'opus');
|
|
desc.sdp = maybePreferCodec(desc.sdp, 'audio', 'receive', 'opus');
|
|
|
|
console.log("onRTCDescription");
|
|
console.log(desc);
|
|
|
|
w(101, desc.sdp);
|
|
}, onRTCDescriptionError);
|
|
}
|
|
|
|
function Connect() {
|
|
reconnectTimeout = null;
|
|
if (webSocketReady() || ReconnectDelay === -1) {
|
|
alert('exit');
|
|
return;
|
|
}
|
|
|
|
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(e);
|
|
console.log(e);
|
|
};
|
|
socket.onopen = function (e) {
|
|
if (reconnectTimeout != null) {
|
|
clearTimeout(reconnectTimeout);
|
|
}
|
|
|
|
Log("* Connected");
|
|
updateVoiceStatus();
|
|
};
|
|
socket.onmessage = function (e) {
|
|
if (ReconnectDelay > 0) {
|
|
ReconnectDelay = 0;
|
|
}
|
|
|
|
try {
|
|
if (typeof e.data === "string") {
|
|
var p = JSON.parse(e.data);
|
|
|
|
if (p.T == 102) {
|
|
pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: atob(p.M)}));
|
|
}
|
|
} else {
|
|
// TODO Binary data
|
|
}
|
|
} catch (e) {
|
|
console.log(e);
|
|
}
|
|
};
|
|
socket.onclose = function (e) {
|
|
connected = false;
|
|
Log("* Disconnected");
|
|
if (ReconnectDelay < 0 || reconnectTimeout != null) {
|
|
return;
|
|
}
|
|
|
|
var waitTime = ReconnectDelay;
|
|
console.log("Reconnecting in " + ReconnectDelay + " seconds...");
|
|
reconnectTimeout = setTimeout(Connect, waitTime * 1000);
|
|
|
|
ReconnectDelay += (ReconnectDelay * 2) + 1;
|
|
if (ReconnectDelay > 10) {
|
|
ReconnectDelay = 10;
|
|
}
|
|
};
|
|
}
|
|
|
|
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) {
|
|
$('#chathistory').append(chatprefix + msg + "\n");
|
|
|
|
if (chatprefix == "") {
|
|
chatprefix = "<br>";
|
|
}
|
|
}
|
|
|
|
function StartPTT() {
|
|
if (ptt) {
|
|
return;
|
|
}
|
|
|
|
ptt = true;
|
|
|
|
var sender = pc.getSenders()[0];
|
|
sender.replaceTrack(audioTrack);
|
|
|
|
updateVoiceStatus();
|
|
}
|
|
|
|
function StopPTT() {
|
|
if (!ptt) {
|
|
return;
|
|
}
|
|
|
|
ptt = false;
|
|
|
|
var sender = pc.getSenders()[0];
|
|
sender.replaceTrack(null);
|
|
|
|
updateVoiceStatus();
|
|
}
|
|
|
|
function updateVoiceStatus() {
|
|
if (ptt) {
|
|
$('#voicestatus').html('<b>PTT Active</b>');
|
|
} else {
|
|
if (voice) {
|
|
$('#voicestatus').html('1 user');
|
|
} else {
|
|
$('#voicestatus').html('0 users');
|
|
}
|
|
}
|
|
}
|
|
|
|
function w(t, m) {
|
|
if (!webSocketReady()) {
|
|
return;
|
|
}
|
|
|
|
socket.send(JSON.stringify({T: t, M: btoa(m)}));
|
|
}
|
|
|
|
// 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(' ');
|
|
}
|