harmony/pkg/web/public/assets/js/harmony.js

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(' ');
}