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 console.log('[URL] ASSET_ID', ASSET_ID, '| ACCOUNT_ID', ACCOUNT_ID, '| callId', callId, ' | IS_PRODUCER', IS_PRODUCER) console.log('🟩 config', config) let socket, hub let device let rtpCapabilities let producerTransport let consumerTransport let producerVideo let producerAudio let consumer let originAssetId // 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() // document.getElementById('btnRecvSendTransport').click(); } if (IS_PRODUCER && urlParams.get('testing') === 'true') { getLocalStream() } }) } 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: true, 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, callId }, (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 producerVideo = await producerTransport.produce(videoParams) console.log('videoParams', videoParams); console.log('producerVideo', producerVideo); producerVideo.on('trackended', () => { console.log('track ended') // close video track }) producerVideo.on('transportclose', () => { console.log('transport ended') // close video track }) // Produce audio producerAudio = await producerTransport.produce(audioParams) console.log('audioParams', audioParams); console.log('producerAudio', producerAudio); producerAudio.on('trackended', () => { console.log('track ended') // close audio track }) producerAudio.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'); } 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) } }) 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 ({ params }) => { if (params.error) { console.log('Cannot Consume') return } // Then consume with the local consumer transport // which creates a consumer consumer = await consumerTransport.consume({ id: params.id, producerId: params.producerId, kind: params.kind, rtpParameters: params.rtpParameters }) // destructure and retrieve the video track from the producer const { track } = consumer let stream = new MediaStream() stream.addTrack(track) // stream.removeTrack(track) remoteVideo.srcObject = stream socket.emit('consumer-resume') console.log('consumer', consumer); }) } 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() } btnLocalVideo.addEventListener('click', getLocalStream) btnRecvSendTransport.addEventListener('click', goConnect) btnCloseCall.addEventListener('click', closeCall)