const io = require('socket.io-client') const mediasoupClient = require('mediasoup-client') const urlParams = new URLSearchParams(location.search); const config = require('./config') console.log('[CONFIG]', config); const ASSET_ID = parseInt(urlParams.get('assetId')) || null; const ACCOUNT_ID = parseInt(urlParams.get('accountId')) || null; const ASSET_NAME = urlParams.get('assetName') || null; const ASSET_TYPE = urlParams.get('assetType') || null; let callId = parseInt(urlParams.get('callId')) || null; const IS_PRODUCER = urlParams.get('producer') === 'true' ? true : false let remoteVideo = document.getElementById('remoteVideo') remoteVideo.defaultMuted = true let produceAudio = false console.log('[URL] ASSET_ID', ASSET_ID, '| ACCOUNT_ID', ACCOUNT_ID, '| callId', callId, ' | IS_PRODUCER', IS_PRODUCER) console.log('🟩 config', config) produceAudioSelector = document.getElementById('produceAudio'); produceAudioSelector.addEventListener('change', e => { if(e.target.checked) { produceAudio = true console.log('produce audio'); } else { produceAudio = false } }); let socket, hub let device let rtpCapabilities let producerTransport let consumerTransport let producerVideo let producerAudio let consumer let originAssetId let consumerVideo // local consumer video(consumer not transport) let consumerAudio // local consumer audio(consumer not transport) const remoteSoundControl = document.getElementById('remoteSoundControl'); remoteSoundControl.addEventListener('click', function handleClick() { console.log('remoteSoundControl.textContent', remoteSoundControl.textContent); if (remoteSoundControl.textContent === 'Unmute') { remoteVideo.muted = false remoteSoundControl.textContent = 'Mute'; } else { remoteVideo.muted = true remoteSoundControl.textContent = 'Unmute'; } }); // https://mediasoup.org/documentation/v3/mediasoup-client/api/#ProducerOptions // https://mediasoup.org/documentation/v3/mediasoup-client/api/#transport-produce let videoParams = { encodings: [ { scaleResolutionDownBy: 4, maxBitrate: 500000 }, { scaleResolutionDownBy: 2, maxBitrate: 1000000 }, { scaleResolutionDownBy: 1, maxBitrate: 5000000 }, { scalabilityMode: 'S3T3_KEY' } ], codecOptions: { videoGoogleStartBitrate: 1000 } } let audioParams = { codecOptions : { opusStereo : true, opusDtx : true } } setTimeout(() => { hub = io(config.hubAddress) const connectToMediasoup = () => { socket = io(config.mediasoupAddress, { reconnection: true, reconnectionDelay: 1000, reconnectionDelayMax : 5000, reconnectionAttempts: Infinity }) socket.on('connection-success', ({ _socketId, existsProducer }) => { console.log(`[MEDIA] ${config.mediasoupAddress} | connected: ${socket.connected} | existsProducer: ${existsProducer}`) if (!IS_PRODUCER && existsProducer && consumer === undefined) { goConnect() } if (IS_PRODUCER && urlParams.get('testing') === 'true') { getLocalStream() } }) socket.on('new-producer', ({ callId, kind }) => { console.log(`🟢 new-producer | callId: ${callId} | kind: ${kind} | Ready to consume`); connectRecvTransport(); }) socket.on('close-producer', ({ callId, kind }) => { console.log(`🔴 close-producer | callId: ${callId} | kind: ${kind}`); if (kind === 'video') { consumerVideo.close() remoteVideo.srcObject = null } else if (kind === 'audio') consumerAudio.close() }) } if (IS_PRODUCER === true) { hub.on('connect', async () => { console.log(`[HUB]! ${config.hubAddress} | connected: ${hub.connected}`) connectToMediasoup() hub.emit( 'ars', JSON.stringify({ ars: true, asset_id: ASSET_ID, account_id: ACCOUNT_ID, }) ) hub.on('video', (data) => { const parsedData = JSON.parse(data); if (parsedData.type === 'notify-request') { console.log('video', parsedData) originAssetId = parsedData.origin_asset_id; // originAssetName = parsedData.origin_asset_name; // originAssetTypeName = parsedData.origin_asset_type_name; callId = parsedData.video_call_id; console.log('[VIDEO] notify-request | IS_PRODUCER', IS_PRODUCER, 'callId', callId); getLocalStream() } if (parsedData.type === 'notify-end') { console.log('[VIDEO] notify-end | IS_PRODUCER', IS_PRODUCER, 'callId', callId); resetCallSettings() } }) }) hub.on('connect_error', (error) => { console.log('connect_error', error); }); hub.on('connection', () => { console.log('connection') }) hub.on('disconnect', () => { console.log('disconnect') }) } else { connectToMediasoup() } }, 1600); const streamSuccess = (stream) => { console.log('[streamSuccess] device', device); localVideo.srcObject = stream console.log('stream', stream); const videoTrack = stream.getVideoTracks()[0] const audioTrack = stream.getAudioTracks()[0] videoParams = { track: videoTrack, ...videoParams } audioParams = { track: audioTrack, ...audioParams } console.log('[streamSuccess] videoParams', videoParams, ' | audioParams', audioParams); goConnect() } const getLocalStream = () => { console.log('[getLocalStream]'); navigator.mediaDevices.getUserMedia({ audio: produceAudio ? true : false, video: { qvga : { width: { ideal: 320 }, height: { ideal: 240 } }, vga : { width: { ideal: 640 }, height: { ideal: 480 } }, hd : { width: { ideal: 1280 }, height: { ideal: 720 } } } }) .then(streamSuccess) .catch(error => { console.log(error.message) }) navigator.permissions.query( { name: 'microphone' } ).then((permissionStatus) =>{ console.log('🟨 [PERMISSION] permissionStatus', permissionStatus); // granted, denied, prompt // It will block the code from execution and display "Permission denied" if we don't have microphone permissions }) } const goConnect = () => { console.log('[goConnect] device:', device); device === undefined ? getRtpCapabilities() : goCreateTransport() } const goCreateTransport = () => { console.log('[goCreateTransport] IS_PRODUCER:', IS_PRODUCER); IS_PRODUCER ? createSendTransport() : createRecvTransport() } // A device is an endpoint connecting to a Router on the // server side to send/recive media const createDevice = async () => { try { device = new mediasoupClient.Device() // https://mediasoup.org/documentation/v3/mediasoup-client/api/#device-load // Loads the device with RTP capabilities of the Router (server side) await device.load({ // see getRtpCapabilities() below routerRtpCapabilities: rtpCapabilities }) console.log('Device RTP Capabilities', device.rtpCapabilities) console.log('[createDevice] device', device); // once the device loads, create transport goCreateTransport() } catch (error) { console.log(error) if (error.name === 'UnsupportedError') console.warn('browser not supported') } } const getRtpCapabilities = () => { console.log('[getRtpCapabilities]'); // make a request to the server for Router RTP Capabilities // see server's socket.on('getRtpCapabilities', ...) // the server sends back data object which contains rtpCapabilities socket.emit('createRoom', { callId }, (data) => { console.log(`Router RTP Capabilities... ${data.rtpCapabilities}`) // we assign to local variable and will be used when // loading the client Device (see createDevice above) rtpCapabilities = data.rtpCapabilities // once we have rtpCapabilities from the Router, create Device createDevice() }) } const createSendTransport = () => { console.log('[createSendTransport'); // see server's socket.on('createWebRtcTransport', sender?, ...) // this is a call from Producer, so sender = true socket.emit('createWebRtcTransport', { sender: true }, (value) => { console.log(`[createWebRtcTransport] value: ${JSON.stringify(value)}`); const params = value.params; // The server sends back params needed // to create Send Transport on the client side if (params.error) { console.log(params.error) return } // creates a new WebRTC Transport to send media // based on the server's producer transport params // https://mediasoup.org/documentation/v3/mediasoup-client/api/#TransportOptions producerTransport = device.createSendTransport(params) // https://mediasoup.org/documentation/v3/communication-between-client-and-server/#producing-media // this event is raised when a first call to transport.produce() is made // see connectSendTransport() below producerTransport.on('connect', async ({ dtlsParameters }, callback, errback) => { try { // Signal local DTLS parameters to the server side transport // see server's socket.on('transport-connect', ...) await socket.emit('transport-connect', { dtlsParameters, }) // Tell the transport that parameters were transmitted. callback() } catch (error) { errback(error) } }) producerTransport.on('produce', async (parameters, callback, errback) => { console.log('[produce] parameters', parameters) try { // Tell the server to create a Producer // with the following parameters and produce // and expect back a server side producer id // see server's socket.on('transport-produce', ...) await socket.emit('transport-produce', { kind: parameters.kind, rtpParameters: parameters.rtpParameters, appData: parameters.appData, }, ({ id }) => { // Tell the transport that parameters were transmitted and provide it with the // server side producer's id. callback({ id }) }) } catch (error) { errback(error) } }) connectSendTransport() }) } const connectSendTransport = async () => { console.log('[connectSendTransport] producerTransport'); // We now call produce() to instruct the producer transport // to send media to the Router // https://mediasoup.org/documentation/v3/mediasoup-client/api/#transport-produce // this action will trigger the 'connect' and 'produce' events above // Produce video let producerVideoHandler = await producerTransport.produce(videoParams) console.log('videoParams', videoParams); console.log('producerVideo', producerVideo); producerVideoHandler.on('trackended', () => { console.log('track ended') // close video track }) producerVideoHandler.on('transportclose', () => { console.log('transport ended') // close video track }) // Produce audio if (produceAudio) { let producerAudioHandler = await producerTransport.produce(audioParams) console.log('audioParams', audioParams); console.log('producerAudio', producerAudio); producerAudioHandler.on('trackended', () => { console.log('track ended') // close audio track }) producerAudioHandler.on('transportclose', () => { console.log('transport ended') // close audio track }) } const answer = { origin_asset_id: ASSET_ID, dest_asset_id: originAssetId || parseInt(urlParams.get('dest_asset_id')), type: 'notify-answer', origin_asset_priority: 1, origin_asset_type_name: ASSET_TYPE, origin_asset_name: ASSET_NAME, video_call_id: callId, answer: 'accepted', // answer: accepted/rejected }; console.log('SEND answer', answer); hub.emit( 'video', JSON.stringify(answer) ); // Enable Close call button const closeCallBtn = document.getElementById('btnCloseCall'); closeCallBtn.removeAttribute('disabled'); createRecvTransport(); } const createRecvTransport = async () => { console.log('createRecvTransport'); // See server's socket.on('consume', sender?, ...) // this is a call from Consumer, so sender = false await socket.emit('createWebRtcTransport', { sender: false, callId }, ({ params }) => { // The server sends back params needed // to create Send Transport on the client side if (params.error) { console.log(params.error) return } console.log('[createRecvTransport] params', params) // Creates a new WebRTC Transport to receive media // based on server's consumer transport params // https://mediasoup.org/documentation/v3/mediasoup-client/api/#device-createRecvTransport consumerTransport = device.createRecvTransport(params) // https://mediasoup.org/documentation/v3/communication-between-client-and-server/#producing-media // This event is raised when a first call to transport.produce() is made // see connectRecvTransport() below consumerTransport.on('connect', async ({ dtlsParameters }, callback, errback) => { try { // Signal local DTLS parameters to the server side transport // see server's socket.on('transport-recv-connect', ...) await socket.emit('transport-recv-connect', { dtlsParameters, }) // Tell the transport that parameters were transmitted. callback() } catch (error) { // Tell the transport that something was wrong errback(error) } }) // We call it in new-rpoducer, we don't need it here anymore // connectRecvTransport() }) } const resetCallSettings = () => { localVideo.srcObject = null remoteVideo.srcObject = null consumer = null producerVideo = null producerAudio = null producerTransport = null consumerTransport = null device = undefined } const connectRecvTransport = async () => { console.log('connectRecvTransport'); // For consumer, we need to tell the server first // to create a consumer based on the rtpCapabilities and consume // if the router can consume, it will send back a set of params as below await socket.emit('consume', { rtpCapabilities: device.rtpCapabilities, callId }, async ({videoParams, audioParams}) => { console.log(`[consume] 🟩 videoParams`, videoParams) console.log(`[consume] 🟩 audioParams`, audioParams) console.log('[consume] 🟩 consumerTransport', consumerTransport) let stream = new MediaStream() // Maybe the unit does not produce video or audio, so we must only consume what is produced if (videoParams) { console.log('❗ Have VIDEO stream to consume'); stream.addTrack(await getVideoTrask(videoParams)) } else { console.log('❗ Don\'t have VIDEO stream to consume'); } if (audioParams) { console.log('❗ Have AUDIO stream to consume'); let audioTrack = await getAudioTrask(audioParams) stream.addTrack(audioTrack) } else { console.log('❗ Don\'t have AUDIO stream to consume'); } socket.emit('consumer-resume') remoteVideo.srcObject = stream remoteVideo.setAttribute('autoplay', true) remoteVideo.play() .then(() => { console.log('remoteVideo PLAY') }) .catch((error) => { console.error(`remoteVideo PLAY ERROR | ${error.message}`) }) }) } const getVideoTrask = async (videoParams) => { consumerVideo = await consumerTransport.consume({ id: videoParams.id, producerId: videoParams.producerId, kind: videoParams.kind, rtpParameters: videoParams.rtpParameters }) return consumerVideo.track } const getAudioTrask = async (audioParams) => { consumerAudio = await consumerTransport.consume({ id: audioParams.id, producerId: audioParams.producerId, kind: audioParams.kind, rtpParameters: audioParams.rtpParameters }) consumerAudio.on('transportclose', () => { console.log('transport closed so consumer closed') }) const audioTrack = consumerAudio.track audioTrack.applyConstraints({ audio: { advanced: [ { echoCancellation: {exact: true} }, { autoGainControl: {exact: true} }, { noiseSuppression: {exact: true} }, { highpassFilter: {exact: true} } ] } }) return audioTrack } const closeCall = () => { console.log('closeCall'); // Emit 'notify-end' to Hub so the consumer will know to close the video const notifyEnd = { origin_asset_id: ASSET_ID, dest_asset_id: originAssetId || parseInt(urlParams.get('dest_asset_id')), type: 'notify-end', video_call_id: callId } console.log('notifyEnd', notifyEnd) hub.emit('video', JSON.stringify(notifyEnd)) // Disable Close call button const closeCallBtn = document.getElementById('btnCloseCall') closeCallBtn.setAttribute('disabled', '') // Reset settings resetCallSettings() } // const consume = async (kind) => { // console.log(`[consume] kind: ${kind}`) // console.log('createRecvTransport Consumer') // await socket.emit('createWebRtcTransport', { sender: false, callId, dispatcher: true }, ({ params }) => { // if (params.error) { // console.log('createRecvTransport | createWebRtcTransport | Error', params.error) // return // } // consumerTransport = device.createRecvTransport(params) // consumerTransport.on('connect', async ({ dtlsParameters }, callback, errback) => { // try { // await socket.emit('transport-recv-connect', { // dtlsParameters, // }) // callback() // } catch (error) { // errback(error) // } // }) // connectRecvTransport() // }) // } btnLocalVideo.addEventListener('click', getLocalStream) // btnRecvSendTransport.addEventListener('click', consume) btnCloseCall.addEventListener('click', closeCall)