/* * Copyright (c) 2024 lax1dude, ayunami2000. All Rights Reserved. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * */ const EVENT_VOICE_ICE = 0; const EVENT_VOICE_DESC = 1; const EVENT_VOICE_OPEN = 2; const EVENT_VOICE_CLOSE = 3; const platfVoiceClientName = "platformVoiceClient"; function initializePlatfVoiceClient(voiceClientImports) { /** * @return {boolean} */ voiceClientImports["isSupported"] = function() { return typeof navigator.mediaDevices !== "undefined" && typeof navigator.mediaDevices.getUserMedia !== "undefined" && "srcObject" in HTMLAudioElement.prototype; }; /** * @param {string} desc * @suppress {globalThis} */ function setRemoteDescriptionImpl(desc) { try { const remoteDesc = JSON.parse(desc); this["_peerConnection"].setRemoteDescription(remoteDesc).then(() => { if (remoteDesc.hasOwnProperty("type") && "offer" === remoteDesc["type"]) { this["_peerConnection"].createAnswer().then((desc) => { this["_peerConnection"].setLocalDescription(desc).then(() => { pushEvent(EVENT_TYPE_VOICE, EVENT_VOICE_DESC, { "objId": this["objId"], "data": JSON.stringify(desc) }); }).catch((err) => { eagError("Failed to set local description for \"{}\"! {}", this["objId"], err.message); pushEvent(EVENT_TYPE_VOICE, EVENT_VOICE_CLOSE, { "objId": this["objId"] }); }); }).catch((err) => { eagError("Failed to create answer for \"{}\"! {}", this["objId"], err.message); pushEvent(EVENT_TYPE_VOICE, EVENT_VOICE_CLOSE, { "objId": this["objId"] }); }); } }).catch((err) => { eagError("Failed to set remote description for \"{}\"! {}", this["objId"], err.message); pushEvent(EVENT_TYPE_VOICE, EVENT_VOICE_CLOSE, { "objId": this["objId"] }); }); } catch (e) { eagError(e.message); pushEvent(EVENT_TYPE_VOICE, EVENT_VOICE_CLOSE, { "objId": this["objId"] }); } } /** * @param {string} ice * @suppress {globalThis} */ function addRemoteICECandidateImpl(ice) { try { this["_peerConnection"].addIceCandidate(new RTCIceCandidate(/** @type {!RTCIceCandidateInit} */ (JSON.parse(ice)))).catch((err) => { eagError("Failed to parse ice candidate for \"{}\"! {}", this["objId"], err.message); pushEvent(EVENT_TYPE_VOICE, EVENT_VOICE_CLOSE, { "objId": this["objId"] }); }); } catch (e) { eagError(e.message); pushEvent(EVENT_TYPE_VOICE, EVENT_VOICE_CLOSE, { "objId": this["objId"] }); } } /** * @suppress {globalThis} */ function closeImpl() { this["_peerConnection"].close(); } let idCounter = 0; /** * @param {string} iceServers * @param {number} offer * @param {!MediaStream} localStream * @return {Object} */ voiceClientImports["createRTCPeerConnection"] = function(iceServers, offer, localStream) { try { const peerId = idCounter++; var ret; const peerConnection = new RTCPeerConnection(/** @type {!RTCConfiguration} */ ({ "iceServers": JSON.parse(iceServers), "optional": [ { "DtlsSrtpKeyAgreement": true } ] })); peerConnection.addEventListener("icecandidate", /** @type {function(Event)} */ ((/** RTCPeerConnectionIceEvent */ evt) => { if (evt.candidate) { pushEvent(EVENT_TYPE_VOICE, EVENT_VOICE_ICE, { "objId": peerId, "data": JSON.stringify({ "sdpMLineIndex": "" + evt.candidate.sdpMLineIndex, "candidate": evt.candidate.candidate }) }); } })); peerConnection.addEventListener("track", /** @type {function(Event)} */ ((/** RTCTrackEvent */ evt) => { const rawStream = evt.streams[0]; ret["_aud"] = document.createElement("audio"); ret["_aud"]["autoplay"] = true; ret["_aud"]["muted"] = true; ret["_aud"]["srcObject"] = rawStream; pushEvent(EVENT_TYPE_VOICE, EVENT_VOICE_OPEN, { "objId": peerId, "stream": rawStream }); })); localStream.getTracks().forEach(function(track) { peerConnection.addTrack(track, localStream); }); if (offer) { peerConnection.createOffer().then((desc) => { peerConnection.setLocalDescription(desc).then(() => { pushEvent(EVENT_TYPE_VOICE, EVENT_VOICE_DESC, { "objId": peerId, "data": JSON.stringify(desc) }); }).catch((err) => { eagError("Failed to set local description for \"{}\"! {}", peerId, err.message); pushEvent(EVENT_TYPE_VOICE, EVENT_VOICE_CLOSE, { "objId": peerId }); }); }).catch((err) => { eagError("Failed to set create offer for \"{}\"! {}", peerId, err.message); pushEvent(EVENT_TYPE_VOICE, EVENT_VOICE_CLOSE, { "objId": peerId }); }); } peerConnection.addEventListener("connectionstatechange", () => { const cs = peerConnection.connectionState; if ("disconnected" === cs || "failed" === cs) { pushEvent(EVENT_TYPE_VOICE, EVENT_VOICE_CLOSE, { "objId": peerId }); } }); return ret = { "objId": peerId, "_peerConnection": peerConnection, "setRemoteDescription": setRemoteDescriptionImpl, "addRemoteICECandidate": addRemoteICECandidateImpl, "closeHandle": closeImpl }; } catch (e) { eagError(e.message); return null; } }; } function initializeNoPlatfVoiceClient(voiceClientImports) { setUnsupportedFunc(voiceClientImports, platfVoiceClientName, "isSupported"); setUnsupportedFunc(voiceClientImports, platfVoiceClientName, "createRTCPeerConnection"); }