webrtcably-realtime

ably webrtc group call Not working in Javascript and simple peer


we are working on making a simple group call web app using Ably and simple peer. By following the step by step tutorial, the 1-to-1 call works (that too has issues when using on public ip but that is another day's topic)

However when same code has been modified to work for group calls, it doesnt seems to work. The members are listed, user is able to join the Group (channel) and the console even shows that signal has been sent/received. but the audio video stream is not working. following is some piece of the js code (modified from ably example)

var membersList = []
var connections = {}
var currentCall
var localStream
var CurrentGroup = ""
var constraints = { video: true, audio: true }
var apiKey = 'oZ_NdA.xyzabc' // a new app key for Group Call
    //var clientId = GetUserName() +'_'+ Math.random().toString(36).substr(2, 6) || 'client-' + Math.random().toString(36).substr(2, 16)
var clientId = GetUserName() || 'client-' + Math.random().toString(36).substr(2, 16)
let GroupMembers = null

let RealtimeApi = new Ably.Realtime({ key: apiKey, clientId: clientId })
var RealtimeChannel;


function JoinGroup(groupName) {

    if (CurrentGroup !== "") {
        LeaveCurrentGroup()
    }
    if (CurrentGroup == groupName) {
        // user is already in this group
        return
    }

    CurrentGroup = groupName

    InitializeRealtimeChannel()
    RealtimeChannel.presence.enter()

    StartLocalCamera()
        //RegisterSignalReceivingEvent()



    RealtimeChannel.presence.subscribe('enter', function(member) {

        RealtimeChannel.presence.get((err, members) => {
            GroupMembers = members

            RenderGroupMemberList(groupName, GroupMembers)
            GenerateGroupMembersVideoTags(GroupMembers)
        })

        if (member.clientId === clientId) {
            document.querySelector("#GroupStatus").innerHTML = "You have Joined the Group"

            InitiateConnectionsWithGroupMembers(groupName)

            RealtimeChannel.subscribe(`rtc-signal/${clientId}`, msg => {
                if (localStream === undefined) {
                    navigator.mediaDevices.getUserMedia(constraints)
                        .then(function(stream) {
                            /* use the stream */
                            localStream = stream
                            var video = document.getElementById('local')
                                //video.src = window.URL.createObjectURL(stream)
                            video.srcObject = stream
                            video.play()
                            connect(msg.data, stream)
                        })
                        .catch(function(err) {
                            alert('error occurred while trying to get stream')
                        })
                } else {
                    connect(msg.data, localStream)
                }
            })

        } else {
            document.querySelector("#GroupStatus").innerHTML = member.clientId + " has Joined"
        }


    })

    RealtimeChannel.presence.subscribe('leave', member => {

        if (member.clientId === clientId) {
            document.querySelector("#GroupStatus").innerHTML = "You have Left the Group"
            document.querySelector("#GroupMemberVideos").innerHTML = ""
            connections = []

        } else {
            document.querySelector("#GroupStatus").innerHTML = member.clientId + " has Left"
            ReleaseLeavingMemberResources(member.clientId)
        }

        RealtimeChannel.presence.get((err, members) => {
            GroupMembers = members
            RenderGroupMemberList(groupName, GroupMembers)
        })

    })
}

function LeaveCurrentGroup() {
    if (CurrentGroup !== "") {
        InitializeRealtimeChannel()

        RealtimeChannel.presence.leave()
        CurrentGroup = ""
    }
}

function GetAvailableMembersInGroup(groupName) {

    let Channel = RealtimeApi.channels.get(groupName)

    let GroupMembers = new Array()


    Channel.presence.get((err, members) => {
        GroupMembers = members

        RenderGroupMemberList(groupName, GroupMembers)

        // if (CurrentGroup == groupName) {
        //     GenerateGroupMembersVideoTags(members)
        // }

    })

}

function RenderGroupMemberList(groupName, memberNames) {

    let Element = document.querySelector("#" + groupName + " .people-list")
    let html = "<ul class='MemberList'>";

    if (!memberNames || memberNames.length < 1) {
        html += "<li style='font-weight: bold;'>No Member Yet..</li>"
    }

    if (memberNames) {


        for (var i = 0; i < memberNames.length; i++) {

            if (memberNames[i].clientId !== clientId) {
                html += "<li>" + memberNames[i].clientId + "</li>"
            } else {
                html += "<li style='font-weight: bold;'>" + memberNames[i].clientId + " (You)</li>"
            }


        }
    }


    html += "</ul>"

    Element.innerHTML = html;
}

function GenerateGroupMembersVideoTags(memberNames) {

    let VideoHtml = "";

    if (memberNames) {

        for (var i = 0; i < memberNames.length; i++) {

            if (memberNames[i].clientId === clientId || document.querySelector("#VideoContainer_" + memberNames[i].clientId) != null) {
                // do nothing 
            } else {

                let VideoTag = '<div class = "col-lg-3 col-sm-6 col-xs-12" id="VideoContainer_' + memberNames[i].clientId + '" >\n';
                VideoTag += '<video controls style = "width: 100%; height: 100%; min-height: 200px;" id="Video_' + memberNames[i].clientId + '" ></video> \n';
                VideoTag += '</div >';

                VideoHtml += VideoTag;
            }
        }
    }


    // document.querySelector("#GroupMemberVideos").innerHTML += VideoHtml;
    document.querySelector("#GroupMemberVideos").insertAdjacentHTML("beforeend", VideoHtml)

}

function ReleaseLeavingMemberResources(client_Id) {
    if (document.querySelector("#VideoContainer_" + client_Id)) {
        document.querySelector("#VideoContainer_" + client_Id).remove();
    }

    if (connections[client_Id]) {
        connections[client_Id] = null
        delete connections[client_Id]
    }
}


function InitiateConnectionsWithGroupMembers(groupName) {


    if (GroupMembers) {
        for (let index = 0; index < GroupMembers.length; index++) {
            if (GroupMembers[index].clientId != clientId && connections[GroupMembers[index].clientId] == null)
                initiateCall(GroupMembers[index].clientId)
        }
    }


}

function StartLocalCamera() {
    navigator.mediaDevices.getUserMedia(constraints)
        .then(function(stream) {
            /* use the stream */
            localStream = stream
            var video = document.getElementById('local')

            if (video.srcObject == null) {
                video.srcObject = stream
                video.play()
            }

            //video.src = window.URL.createObjectURL(stream)
        })
        .catch(function(err) {
            console.error(err)
            alert('Could not get video stream from source')
        })
}

function InitializeRealtimeChannel() {

    if (!RealtimeChannel) {
        RealtimeChannel = RealtimeApi.channels.get(CurrentGroup)
    }

}

function initiateCall(client_id) {

    console.info(`Initialting call with ${client_id}`)

    // Create a new connection
    currentCall = client_id
    if (!connections[client_id]) {
        connections[client_id] = new Connection(client_id, RealtimeChannel, true, localStream)
    }
}


function connect(data, stream) {
    if (!connections[data.user]) {
        connections[data.user] = new Connection(data.user, RealtimeChannel, false, stream)
    }
    if (!connections[data.user].isConnected) {
        connections[data.user].handleSignal(data.signal)
    }


}

function receiveStream(client_id, stream) {
    let video = document.getElementById('Video_' + client_id)
        //video.src = window.URL.createObjectURL(stream)
    video.srcObject = stream
    video.play()
}

and the simple peer helper us as under:

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) {
        InitializeRealtimeChannel()

        try {
            console.info("Sending Signal to :" + `${this._remoteClient}`)

            RealtimeChannel.publish(`rtc-signal/${this._remoteClient}`, {
                user: clientId,
                signal: signal
            })
        } catch (error) {
            console.error("Signal error: " + error)
        }
    }
    _onConnect() {
        this.isConnected = true
        console.log('connected to ' + this._remoteClient)
    }
    _onClose() {
        console.log(`connection to ${this._remoteClient} closed`)

    }
    _onStream(data) {
        console.info("Attempting to receive stream from " + this._remoteClient)
        receiveStream(this._remoteClient, data)
    }
    _onError(error) {
        console.log(`an error occurred ${error.toString()}`)
    }
}

the HTML is as under

<!--https://www.ably.io/tutorials/web-rtc-video-calling#testing-our-app-->

<!DOCTYPE html>
<html>

<head>
    <title>Group Calls</title>
    <!-- Latest compiled and minified CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <!-- Optional theme -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
    <link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN" crossorigin="anonymous">

</head>

<body class="bodybg">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/webrtc-adapter/6.2.1/adapter.min.js"></script>
    <script src="https://cdn.ably.io/lib/ably.min-1.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/simple-peer/9.1.2/simplepeer.min.js"></script>
    <!-- <script src="js/simple-peer/simplepeer.min.js" type="javascript"></script> -->
    <script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
    <!-- Latest compiled and minified JavaScript -->
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

    <!-- <script src="js/simple-peer/index.js"></script> -->
    <script src="js/helper.js"></script>
    <script src="js/group-call.js"></script>
    <script src="js/ably-groupcall.js"></script>
    <!-- <script src="js/connection-helper.js"></script> -->
    <script src="js/group-connectionhelper.js"></script>







    <form action="groups.html" method="GET" id="DisplayNameForm">
        <div class="row" style="background-color:  rgba(136, 26, 32, 0.53);">

            <div class="col-sm-4">
                <div class="form-group">
                    <label style="color: #fff;">Display Name</label>
                    <input type="text" name="Name" class="form-control" />
                </div>
            </div>

            <div class="col-sm-2">
                <div class="form-group">
                    <label>&nbsp;</label>
                    <button type="submit" class="btn btn-primary btn-block">Update</button>
                </div>

            </div>

        </div>
    </form>

    <div class="conatainer-fluid" style="margin-top: 3em;">

        <div class="row">

            <!-- Group-1 -->

            <div class="col-sm-3 col-xs-6">
                <div class="panel panel-primary" id="Group_1">
                    <div class="panel-heading" id="Group_1_Heading">
                        Group-1
                    </div>
                    <div class="panel-body">
                        <h4>Members:</h4>
                        <div class="people-list"></div>
                    </div>
                    <div class="panel-footer">
                        <button onclick="JoinGroup('Group_1')" class="btn btn-info btn-block">Join</button>
                    </div>
                </div>
            </div>

            <!-- Group-2 -->

            <div class="col-sm-3 col-xs-6">
                <div class="panel panel-primary" id="Group_2">
                    <div class="panel-heading" id="Group_2_Heading">
                        Group-2
                    </div>
                    <div class="panel-body">
                        <h4>Members:</h4>
                        <div class="people-list"></div>
                    </div>
                    <div class="panel-footer">
                        <button onclick="JoinGroup('Group_2')" class="btn btn-info btn-block">Join</button>
                    </div>
                </div>
            </div>

            <!-- Group-3 -->

            <div class="col-sm-3 col-xs-6">
                <div class="panel panel-primary" id="Group_3">
                    <div class="panel-heading" id="Group_3_Heading">
                        Group-3
                    </div>
                    <div class="panel-body">
                        <h4>Members:</h4>
                        <div class="people-list"></div>
                    </div>
                    <div class="panel-footer">
                        <button onclick="JoinGroup('Group_3')" class="btn btn-info btn-block">Join</button>
                    </div>
                </div>
            </div>

            <!-- Group-4 -->

            <div class="col-sm-3 col-xs-6">
                <div class="panel panel-primary" id="Group_4">
                    <div class="panel-heading" id="Group_4_Heading">
                        Group-4
                    </div>
                    <div class="panel-body">
                        <h4>Members:</h4>
                        <div class="people-list"></div>
                    </div>
                    <div class="panel-footer">
                        <button onclick="JoinGroup('Group_4')" class="btn btn-info btn-block">Join</button>
                    </div>
                </div>
            </div>


        </div>


        <div class="row">
            <div class="col-sm-3 col-xs-12">
                <div class="panel panel-default">
                    <div class="panel-body">
                        <video id="local" muted style="z-index: 2; width: 100%; min-height: 200px;"></video>

                        <div class="row">
                            <div class="col-sm-12">
                                <div class="col-xs-6">
                                    <h3>Duration:</h3>
                                    <label id="minutes"></label> : <label id="seconds"></label>
                                </div>

                                <div class="col-xs-6">
                                    <button class="btn  btn-default" onclick="LeaveCurrentGroup()" title="Leave Group">
                                        <img src="asset/hang-up.svg" style="width: 50px; height: 50px; color: #fff;">
                                    </button>
                                </div>
                            </div>
                            <div class="col-sm-12">
                                <h3 id="CallStatus"></h3>
                            </div>

                            <div class="col-sm-12">
                                <h3 id="GroupStatus"></h3>
                            </div>

                        </div>

                    </div>
                </div>
                Icons made by <a href="https://www.flaticon.com/authors/dmitri13" title="dmitri13">dmitri13</a> from <a href="https://www.flaticon.com/" title="Flaticon"> www.flaticon.com</a>
            </div>


            <div class="col-sm-9 col-xs-12" style="margin-bottom: 50px;">

                <div id="call" style="position: relative;">
                    <div class="row" id="GroupMemberVideos">


                    </div>


                </div>

            </div>

        </div>


    </div>


</body>
<style>
    .MemberList {
        list-style: decimal;
    }
</style>

</html>

<script type="text/javascript">
    window.onload = function() {

        var Name = GetUrlParameter('Name');


        if (Name != null && Name != '') {
            document.getElementById('DisplayNameForm').style.display = 'none'
        }

        setTimeout(() => {
            UpdateGroupMemeberLists()
        }, 5000);

    }

    function UpdateGroupMemeberLists() {

        for (let index = 1; index <= 4; index++) {
            GetAvailableMembersInGroup("Group_" + index)
        }

    }
</script>

by doing some basic debugging , it shows that connection is established but there is no stream

enter image description here any help appreciated since am completely unaware of how webrtc works.


Solution

  • Found the problem. :)

    was initializing the local camera stream inside the Join group function. The problem was resolved by initializing the local video stream at document loading.

    hope this helps others.