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> </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
any help appreciated since am completely unaware of how webrtc works.
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.