Changing 1 on 1 webRTC Ably API code to handle group voice call

60 views Asked by At

Right now I have a fully working 1 on 1 voice call using webRTC and Ably API. I want to modify my code to somehow create one room (I won't need more than one channel) so when people click to join it they will be able to talk to each other together. Any ideas how to do it?

my ably-videocall.js:

var membersList = []
var connections = {}
var currentCall
var localStream
var constraints = {video: false, audio: { echoCancellation: true}}
var apiKey = '0uLlaA.7H2Oow:P4nF0mGqCpOOmFtxNGPsctl5PGh8uTuCz1HPxf_yIfI'
var clientId = 'client-' + Math.random().toString(36).substr(2, 16)
var realtime = new Ably.Realtime({ key: apiKey, clientId: clientId })
var AblyRealtime = realtime.channels.get('ChatChannel')

AblyRealtime.presence.subscribe('enter', function(member) {
    AblyRealtime.presence.get((err, members) => {
        membersList = members
        renderMembers()
    })
})
AblyRealtime.presence.subscribe('leave', member => {
    AblyRealtime.presence.get((err, members) => {
        membersList = members
        renderMembers()
    })
})
AblyRealtime.presence.enter()

function renderMembers() {
    var list = document.getElementById('memberList')
    var online = document.getElementById('online')
    online.innerHTML = 'Users online (' + (membersList.length === 0 ? 0 : membersList.length - 1) + ')'
    var html = ''
    if (membersList.length === 1) {
        html += '<li> No member online </li>'
        list.innerHTML = html
        return
    }
    for (var index = 0; index < membersList.length; index++) {
        var element = membersList[index]
        if (element.clientId !== clientId) {
            html += '<li><small>' + element.clientId + ' <button class="btn btn-xs btn-success" onclick=call("' + element.clientId + '")>call now</button> </small></li>'
        }
    }
    list.innerHTML = html
}
function call(client_id) {
    if (client_id === clientId) return
    alert(`attempting to call ${client_id}`)
    AblyRealtime.publish(`incoming-call/${client_id}`, {
            user: clientId
        })
}
AblyRealtime.subscribe(`incoming-call/${clientId}`, call => {
    if (currentCall != undefined) {
        // user is on another call
        AblyRealtime.publish(`call-details/${call.data.user}`, {
            user: clientId,
            msg: 'User is on another call'
        })
        return
    }
    var isAccepted = confirm(`You have a call from ${call.data.user}, do you want to accept?`)
    if (!isAccepted) {
        // user rejected the call
        AblyRealtime.publish(`call-details/${call.data.user}`, {
            user: clientId,
            msg: 'User declined the call'
        })
        return
    }
    currentCall = call.data.user
    AblyRealtime.publish(`call-details/${call.data.user}`, {
        user: clientId,
        accepted: true
    })
})
AblyRealtime.subscribe(`call-details/${clientId}`, call => {
    if (call.data.accepted) {
        initiateCall(call.data.user)
    } else {
        alert(call.data.msg)
    }
})
function initiateCall(client_id) {
    navigator.mediaDevices.getUserMedia(constraints)
        .then(function(stream) {
            /* use the stream */
            localStream = stream
            localStream.getAudioTracks().forEach(track => {
                track.enabled = true; // Ensure the track is enabled
                track.volume = 0; // Set volume to zero
            });
                // Create a new connection
            currentCall = client_id
            if (!connections[client_id]) {
                connections[client_id] = new Connection(client_id, AblyRealtime, true, stream)
            }
            document.getElementById('call').style.display = 'block'
        })
        .catch(function(err) {
            /* handle the error */
            alert('Could not get video stream from source')
        })
}
AblyRealtime.subscribe(`rtc-signal/${clientId}`, msg => {
    if (localStream === undefined) {
        navigator.mediaDevices.getUserMedia(constraints)
            .then(function(stream) {
                /* use the stream */
                localStream = stream
                localStream.getAudioTracks().forEach(track => {
                track.enabled = true; // Ensure the track is enabled
                track.volume = 0; // Set volume to zero
            });
                connect(msg.data, stream)
            })
            .catch(function(err) {
                alert('error occurred while trying to get stream')
            })
    } else {
        connect(msg.data, localStream)
    }
})
function connect(data, stream) {
    if (!connections[data.user]) {
        connections[data.user] = new Connection(data.user, AblyRealtime, false, stream)
    }
    connections[data.user].handleSignal(data.signal)
    document.getElementById('call').style.display = 'block'
}
function receiveStream(client_id, stream) {
    var audio = new Audio();
            audio.srcObject = stream;
            audio.play();
    renderMembers()
}
function handleEndCall(client_id = null) {
    if (client_id && client_id != currentCall) {
        return
    }
    client_id = currentCall;
    alert('call ended')
    currentCall = undefined
    connections[client_id].destroy()
    delete connections[client_id]
    for (var track of localStream.getTracks()) {
        track.stop()
    }
    localStream = undefined
    document.getElementById('call').style.display = 'none'
}

my Connection class:

class Connection {
    constructor(remoteClient, AblyRealtime, initiator, stream) {
        console.log(`Opening connection to ${remoteClient}`)
        this._remoteClient = remoteClient
        this.isConnected = false
        this._p2pConnection = new SimplePeer({
            initiator: initiator,
            stream: stream
        })
        this._p2pConnection.on('signal', this._onSignal.bind(this))
        this._p2pConnection.on('error', this._onError.bind(this))
        this._p2pConnection.on('connect', this._onConnect.bind(this))
        this._p2pConnection.on('close', this._onClose.bind(this))
        this._p2pConnection.on('stream', this._onStream.bind(this))
    }
    handleSignal(signal) {
        this._p2pConnection.signal(signal)
    }
    send(msg) {
        this._p2pConnection.send(msg)
    }
    destroy() {
        this._p2pConnection.destroy()
    }
    _onSignal(signal) {
        AblyRealtime.publish(`rtc-signal/${this._remoteClient}`, {
            user: clientId,
            signal: signal
        })
    }
    _onConnect() {
        this.isConnected = true
        console.log('connected to ' + this._remoteClient)
    }
    _onClose() {
        console.log(`connection to ${this._remoteClient} closed`)
        handleEndCall(this._remoteClient)
    }
    _onStream(data) {
        receiveStream(this._remoteClient, data)
    }
    _onError(error) {
        console.log(`an error occurred ${error.toString()}`)
    }
}
1

There are 1 answers

0
DMakeev On

The main question is: do you want to implement it fast or properly?

  • fast/wrong solution: each joining user makes a call to each other person in the room. Complicated code & excess load at client side are inevitable;
  • correct solution: use any kind of media server (Janus, Jitsi, MediaSoup, ...) in SFU or MCU mode. Or use any WebRTC Saas platform, supporting Audio MCU or SFU. In this case each user will send only one audio (and video, if needed) to the server and receive only one mixed (MCU) or one per user (SFU) stream.

Key point is CPU load at client side - WebRTC encodes each outgoing media stream independently, so it's CPU expensive to send a lot of media streams for other peers. That's why media servers are required for group calls with more than 4-5 video or 6-8 audio-only users in the room.