I am using RTCMultiConnection v3.4.4
I want to run WebRTC on localhost. I have chosen XHR-Signaling because I want the project to be completely offline. I do not want it to depend on the internet, since everything is on localhost (to be later deployed on LAN)
I have included XHRConnection.js
and set connection.setCustomSocketHandler(XHRConnection)
. I also did the override connection.openSignalingChannel...
However, when I open/start the room, my video shows but the buttons that was disabled by disableInputButtons()
still remains disabled. The chat is not working.
I did a console.log
at override connection.openSignalingChannel...
to confirm if it ever got called, but it does not.
Please help on how to implement XHR-Signaling on localhost.
Thanks.
Code:
File: Audio+Video+TextChat+FileSharing.html
<!-- Demo version: 2017.08.10 -->
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Audio+Video+TextChat+FileSharing using RTCMultiConnection</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<link rel="shortcut icon" href="./logo.png">
<link rel="stylesheet" href="./stylesheet.css">
<script src="./menu.js"></script>
</head>
<body>
<h1>
Audio+Video+TextChat+FileSharing using RTCMultiConnection
<p class="no-mobile">
Multi-user (many-to-many) video streaming + text chat + file sharing using mesh networking model.
</p>
</h1>
<section class="make-center">
<input type="text" id="room-id" value="abcdef" autocorrect=off autocapitalize=off size=20>
<button id="open-room">Open Room</button><button id="join-room">Join Room</button><button id="open-or-join-room">Auto Open Or Join Room</button>
<br><br>
<input type="text" id="input-text-chat" placeholder="Enter Text Chat" disabled>
<button id="share-file" disabled>Share File</button>
<br><br>
<button id="btn-leave-room" disabled>Leave/or close the room</button>
<div id="room-urls" style="text-align: center;display: none;background: #F1EDED;margin: 15px -10px;border: 1px solid rgb(189, 189, 189);border-left: 0;border-right: 0;"></div>
<div id="chat-container">
<div id="file-container"></div>
<div class="chat-output"></div>
</div>
<div id="videos-container"></div>
</section>
<script src="./RTCMultiConnection.min.js"></script>
<script src="./adapter.js"></script>
<script src="./XHRConnection.js"></script>
<!-- custom layout for HTML5 audio/video elements -->
<link rel="stylesheet" href="./getHTMLMediaElement.css">
<script src="./getHTMLMediaElement.js"></script>
<script src="./FileBufferReader.js"></script>
<script>
// ......................................................
// .......................UI Code........................
// ......................................................
document.getElementById('open-room').onclick = function() {
disableInputButtons();
connection.open( document.getElementById('room-id').value , function() {
showRoomURL(connection.sessionid);
xhr
(
'start-broadcast.php' ,
function( responseText ){ console.log( 'Broadcast started [' + document.getElementById('room-id').value + ']' ) },
JSON.stringify( { name: document.getElementById('room-id').value } )
);
});
};
document.getElementById('join-room').onclick = function() {
disableInputButtons();
connection.join(document.getElementById('room-id').value);
};
document.getElementById('open-or-join-room').onclick = function() {
disableInputButtons();
connection.openOrJoin(document.getElementById('room-id').value, function(isRoomExists, roomid) {
if (!isRoomExists) {
showRoomURL(roomid);
}
});
};
document.getElementById('btn-leave-room').onclick = function() {
this.disabled = true;
if (connection.isInitiator) {
// use this method if you did NOT set "autoCloseEntireSession===true"
// for more info: https://github.com/muaz-khan/RTCMultiConnection#closeentiresession
connection.closeEntireSession(function() {
document.querySelector('h1').innerHTML = 'Entire session has been closed.';
});
} else {
connection.leave();
}
};
// ......................................................
// ................FileSharing/TextChat Code.............
// ......................................................
document.getElementById('share-file').onclick = function() {
var fileSelector = new FileSelector();
fileSelector.selectSingleFile(function(file) {
connection.send(file);
});
};
document.getElementById('input-text-chat').onkeyup = function(e) {
if (e.keyCode != 13) return;
// removing trailing/leading whitespace
this.value = this.value.replace(/^\s+|\s+$/g, '');
if (!this.value.length) return;
connection.send(this.value);
appendDIV(this.value);
this.value = '';
};
var chatContainer = document.querySelector('.chat-output');
function appendDIV(event) {
var div = document.createElement('div');
div.innerHTML = event.data || event;
chatContainer.insertBefore(div, chatContainer.firstChild);
div.tabIndex = 0;
div.focus();
document.getElementById('input-text-chat').focus();
}
// ......................................................
// ..................RTCMultiConnection Code.............
// ......................................................
var connection = new RTCMultiConnection();
connection.setCustomSocketHandler(XHRConnection);
connection.direction = 'one-way';
// by default, socket.io server is assumed to be deployed on your own URL
// connection.socketURL = '/';
connection.trickleIce = false;
// comment-out below line if you do not have your own socket.io server
// connection.socketURL = 'https://rtcmulticonnection.herokuapp.com:443/';
//connection.socketMessageEvent = 'audio-video-file-chat-demo';
connection.enableLogs = true;
connection.enableFileSharing = true; // by default, it is "false".
// this object is used to store "onmessage" callbacks from "openSignalingChannel handler
var onMessageCallbacks = {};
// this object is used to make sure identical messages are not used multiple times
var messagesReceived = {};
// overriding "openSignalingChannel handler
connection.openSignalingChannel = function (config) {
console.log( 'called: openSignalingChannel' );
var channel = config.channel || this.channel;
onMessageCallbacks[channel] = config.onmessage;
// let RTCMultiConnection know that server connection is opened!
if (config.onopen) {
console.log( 'Calling the config.open object' );
setTimeout(config.onopen, 1);
}
else console.log( 'No config.open object' );
// returning an object to RTCMultiConnection
// so it can send data using "send" method
return {
send: function (data) {
data = {
channel: channel,
message: data,
sender: connection.userid
};
// posting data to server
// data is also JSON-ified.
xhr('xhr-signalhandler-post.php', null, JSON.stringify(data));
},
channel: channel
};
};
connection.session = {
audio: true,
video: true,
data: true
};
connection.sdpConstraints.mandatory = {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
};
connection.videosContainer = document.getElementById('videos-container');
connection.onstream = function(event) {
var width = parseInt(connection.videosContainer.clientWidth / 2) - 20;
var mediaElement = getHTMLMediaElement(event.mediaElement, {
title: event.userid,
buttons: ['full-screen'],
width: width,
showOnMouseEnter: false
});
connection.videosContainer.appendChild(mediaElement);
setTimeout(function() {
mediaElement.media.play();
}, 5000);
mediaElement.id = event.streamid;
};
connection.onstreamended = function(event) {
var mediaElement = document.getElementById(event.streamid);
if (mediaElement) {
mediaElement.parentNode.removeChild(mediaElement);
}
};
connection.onmessage = appendDIV;
connection.filesContainer = document.getElementById('file-container');
connection.onopen = function() {
console.log( "com. openend" );
document.getElementById('share-file').disabled = false;
document.getElementById('input-text-chat').disabled = false;
document.getElementById('btn-leave-room').disabled = false;
document.querySelector('h1').innerHTML = 'You are connected with: ' + connection.getAllParticipants().join(', ');
};
connection.onclose = function() {
if (connection.getAllParticipants().length) {
document.querySelector('h1').innerHTML = 'You are still connected with: ' + connection.getAllParticipants().join(', ');
} else {
document.querySelector('h1').innerHTML = 'Seems session has been closed or all participants left.';
}
};
connection.onEntireSessionClosed = function(event) {
document.getElementById('share-file').disabled = true;
document.getElementById('input-text-chat').disabled = true;
document.getElementById('btn-leave-room').disabled = true;
document.getElementById('open-or-join-room').disabled = false;
document.getElementById('open-room').disabled = false;
document.getElementById('join-room').disabled = false;
document.getElementById('room-id').disabled = false;
connection.attachStreams.forEach(function(stream) {
stream.stop();
});
// don't display alert for moderator
if (connection.userid === event.userid) return;
document.querySelector('h1').innerHTML = 'Entire session has been closed by the moderator: ' + event.userid;
};
connection.onUserIdAlreadyTaken = function(useridAlreadyTaken, yourNewUserId) {
// seems room is already opened
connection.join(useridAlreadyTaken);
};
function disableInputButtons() {
document.getElementById('open-or-join-room').disabled = true;
document.getElementById('open-room').disabled = true;
document.getElementById('join-room').disabled = true;
document.getElementById('room-id').disabled = true;
}
// ......................................................
// ......................Handling Room-ID................
// ......................................................
function showRoomURL(roomid) {
var roomHashURL = '#' + roomid;
var roomQueryStringURL = '?roomid=' + roomid;
var html = '<h2>Unique URL for your room:</h2><br>';
html += 'Hash URL: <a href="' + roomHashURL + '" target="_blank">' + roomHashURL + '</a>';
html += '<br>';
html += 'QueryString URL: <a href="' + roomQueryStringURL + '" target="_blank">' + roomQueryStringURL + '</a>';
var roomURLsDiv = document.getElementById('room-urls');
roomURLsDiv.innerHTML = html;
roomURLsDiv.style.display = 'block';
}
(function() {
var params = {},
r = /([^&=]+)=?([^&]*)/g;
function d(s) {
return decodeURIComponent(s.replace(/\+/g, ' '));
}
var match, search = window.location.search;
while (match = r.exec(search.substring(1)))
params[d(match[1])] = d(match[2]);
window.params = params;
})();
var roomid = '';
if (localStorage.getItem(connection.socketMessageEvent)) {
roomid = localStorage.getItem(connection.socketMessageEvent);
} else {
roomid = connection.token();
}
document.getElementById('room-id').value = roomid;
document.getElementById('room-id').onkeyup = function() {
localStorage.setItem(connection.socketMessageEvent, this.value);
};
var hashString = location.hash.replace('#', '');
if (hashString.length && hashString.indexOf('comment-') == 0) {
hashString = '';
}
var roomid = params.roomid;
if (!roomid && hashString.length) {
roomid = hashString;
}
if (roomid && roomid.length) {
document.getElementById('room-id').value = roomid;
localStorage.setItem(connection.socketMessageEvent, roomid);
// auto-join-room
(function reCheckRoomPresence() {
connection.checkPresence(roomid, function(isRoomExists) {
if (isRoomExists) {
connection.join(roomid);
return;
}
setTimeout(reCheckRoomPresence, 5000);
});
})();
disableInputButtons();
}
</script>
<footer>
<small id="send-message"></small>
</footer>
<script src="common.js"></script>
</body>
</html>
XHRConnection.js:
function XHRConnection(connection, connectCallback) {
connection.socket = {
send: function(data) {
data = {
message: data,
sender: connection.userid
};
// posting data to server
// data is also JSON-ified.
xhr('xhr-signalhandler-post.php', null, JSON.stringify(data));
}
};
// this object is used to make sure identical messages are not used multiple times
var messagesReceived = {};
function repeatedlyCheck() {
xhr('xhr-signalhandler-get.php', function(data) {
// if server says nothing; wait.
if (data == false) return setTimeout(repeatedlyCheck, 400);
// if already receied same message; skip.
if (messagesReceived[data.ID]) return setTimeout(repeatedlyCheck, 400);
messagesReceived[data.ID] = data.Message;
// "Message" property is JSON-ified in "openSignalingChannel handler
data = JSON.parse(data.Message);
if (data.eventName === connection.socketMessageEvent) {
onMessagesCallback(data.data);
}
if (data.eventName === 'presence') {
data = data.data;
if (data.userid === connection.userid) return;
connection.onUserStatusChanged({
userid: data.userid,
status: data.isOnline === true ? 'online' : 'offline',
extra: connection.peers[data.userid] ? connection.peers[data.userid].extra : {}
});
}
// repeatedly check the database
setTimeout(repeatedlyCheck, 1);
});
}
repeatedlyCheck();
setTimeout
(
function() {
if (connection.enableLogs) {
console.info('XHR connection opened');
}
connection.socket.emit('presence', {
userid: connection.userid,
isOnline: true
});
if( connectCallback ) {
console.log( 'Calling connectCallback...' );
connectCallback(connection.socket);
console.log( 'Done' );
}
},
2000
);
connection.socket.emit = function(eventName, data, callback) {
if (eventName === 'changed-uuid') return;
if (data.message && data.message.shiftedModerationControl) return;
connection.socket.send({
eventName: eventName,
data: data
});
if (callback) {
callback();
}
};
var mPeer = connection.multiPeersHandler;
function onMessagesCallback(message) {
if (message.remoteUserId != connection.userid) return;
if (connection.peers[message.sender] && connection.peers[message.sender].extra != message.extra) {
connection.peers[message.sender].extra = message.extra;
connection.onExtraDataUpdated({
userid: message.sender,
extra: message.extra
});
}
if (message.message.streamSyncNeeded && connection.peers[message.sender]) {
var stream = connection.streamEvents[message.message.streamid];
if (!stream || !stream.stream) {
return;
}
var action = message.message.action;
if (action === 'ended' || action === 'stream-removed') {
connection.onstreamended(stream);
return;
}
var type = message.message.type != 'both' ? message.message.type : null;
stream.stream[action](type);
return;
}
if (message.message === 'connectWithAllParticipants') {
if (connection.broadcasters.indexOf(message.sender) === -1) {
connection.broadcasters.push(message.sender);
}
mPeer.onNegotiationNeeded({
allParticipants: connection.getAllParticipants(message.sender)
}, message.sender);
return;
}
if (message.message === 'removeFromBroadcastersList') {
if (connection.broadcasters.indexOf(message.sender) !== -1) {
delete connection.broadcasters[connection.broadcasters.indexOf(message.sender)];
connection.broadcasters = removeNullEntries(connection.broadcasters);
}
return;
}
if (message.message === 'dropPeerConnection') {
connection.deletePeer(message.sender);
return;
}
if (message.message.allParticipants) {
if (message.message.allParticipants.indexOf(message.sender) === -1) {
message.message.allParticipants.push(message.sender);
}
message.message.allParticipants.forEach(function(participant) {
mPeer[!connection.peers[participant] ? 'createNewPeer' : 'renegotiatePeer'](participant, {
localPeerSdpConstraints: {
OfferToReceiveAudio: connection.sdpConstraints.mandatory.OfferToReceiveAudio,
OfferToReceiveVideo: connection.sdpConstraints.mandatory.OfferToReceiveVideo
},
remotePeerSdpConstraints: {
OfferToReceiveAudio: connection.session.oneway ? !!connection.session.audio : connection.sdpConstraints.mandatory.OfferToReceiveAudio,
OfferToReceiveVideo: connection.session.oneway ? !!connection.session.video || !!connection.session.screen : connection.sdpConstraints.mandatory.OfferToReceiveVideo
},
isOneWay: !!connection.session.oneway || connection.direction === 'one-way',
isDataOnly: isData(connection.session)
});
});
return;
}
if (message.message.newParticipant) {
if (message.message.newParticipant == connection.userid) return;
if (!!connection.peers[message.message.newParticipant]) return;
mPeer.createNewPeer(message.message.newParticipant, message.message.userPreferences || {
localPeerSdpConstraints: {
OfferToReceiveAudio: connection.sdpConstraints.mandatory.OfferToReceiveAudio,
OfferToReceiveVideo: connection.sdpConstraints.mandatory.OfferToReceiveVideo
},
remotePeerSdpConstraints: {
OfferToReceiveAudio: connection.session.oneway ? !!connection.session.audio : connection.sdpConstraints.mandatory.OfferToReceiveAudio,
OfferToReceiveVideo: connection.session.oneway ? !!connection.session.video || !!connection.session.screen : connection.sdpConstraints.mandatory.OfferToReceiveVideo
},
isOneWay: !!connection.session.oneway || connection.direction === 'one-way',
isDataOnly: isData(connection.session)
});
return;
}
if (message.message.readyForOffer || message.message.addMeAsBroadcaster) {
connection.addNewBroadcaster(message.sender);
}
if (message.message.newParticipationRequest && message.sender !== connection.userid) {
if (connection.peers[message.sender]) {
connection.deletePeer(message.sender);
}
var userPreferences = {
extra: message.extra || {},
localPeerSdpConstraints: message.message.remotePeerSdpConstraints || {
OfferToReceiveAudio: connection.sdpConstraints.mandatory.OfferToReceiveAudio,
OfferToReceiveVideo: connection.sdpConstraints.mandatory.OfferToReceiveVideo
},
remotePeerSdpConstraints: message.message.localPeerSdpConstraints || {
OfferToReceiveAudio: connection.session.oneway ? !!connection.session.audio : connection.sdpConstraints.mandatory.OfferToReceiveAudio,
OfferToReceiveVideo: connection.session.oneway ? !!connection.session.video || !!connection.session.screen : connection.sdpConstraints.mandatory.OfferToReceiveVideo
},
isOneWay: typeof message.message.isOneWay !== 'undefined' ? message.message.isOneWay : !!connection.session.oneway || connection.direction === 'one-way',
isDataOnly: typeof message.message.isDataOnly !== 'undefined' ? message.message.isDataOnly : isData(connection.session),
dontGetRemoteStream: typeof message.message.isOneWay !== 'undefined' ? message.message.isOneWay : !!connection.session.oneway || connection.direction === 'one-way',
dontAttachLocalStream: !!message.message.dontGetRemoteStream,
connectionDescription: message,
successCallback: function() {
// if its oneway----- todo: THIS SEEMS NOT IMPORTANT.
if (typeof message.message.isOneWay !== 'undefined' ? message.message.isOneWay : !!connection.session.oneway || connection.direction === 'one-way') {
connection.addNewBroadcaster(message.sender, userPreferences);
}
if (!!connection.session.oneway || connection.direction === 'one-way' || isData(connection.session)) {
connection.addNewBroadcaster(message.sender, userPreferences);
}
}
};
connection.onNewParticipant(message.sender, userPreferences);
return;
}
if (message.message.shiftedModerationControl) {
connection.onShiftedModerationControl(message.sender, message.message.broadcasters);
return;
}
if (message.message.changedUUID) {
if (connection.peers[message.message.oldUUID]) {
connection.peers[message.message.newUUID] = connection.peers[message.message.oldUUID];
delete connection.peers[message.message.oldUUID];
}
}
if (message.message.userLeft) {
mPeer.onUserLeft(message.sender);
if (!!message.message.autoCloseEntireSession) {
connection.leave();
}
return;
}
mPeer.addNegotiatedMessage(message.message, message.sender);
}
window.addEventListener('beforeunload', function() {
connection.socket.emit('presence', {
userid: connection.userid,
isOnline: false
});
}, false);
}
// a simple function to make XMLHttpRequests
function xhr( url, callback, data ) {
// if( data ) console.log('[' + url + '] sending: ' + JSON.stringify( data ) );
if (!window.XMLHttpRequest || !window.JSON){
console.log( 'No JSON and/or XMLHttpRequest support' );
return;
}
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (callback && request.readyState == 4 && request.status == 200) {
// server MUST return JSON text
if( request.responseText != 'false' )
console.log('Logging non-false data [from ' + url + ']: ' + request.responseText + "[...data POST'ed: " + JSON.stringify( data ) + "]" );
callback(JSON.parse(request.responseText));
}
};
request.open( 'POST', url );
var formData = new FormData();
// you're passing "message" parameter
formData.append( 'message', data );
request.send(formData);
}
start-broadcast.php:
<?php
require( "connection.inc.php" );
if( isset( $_POST['message'] ) )
{
$data = json_decode( $_POST['message'] , true );
// Now, if someone initiates WebRTC session; you should make an XHR request to create a record in the room-table; and
// set "Owner-id" equals to that user's "user-id".
//{"message":{"eventName":"presence","data":{"userid":"winey","isOnline":true}},"sender":"winey"}
$query = " INSERT INTO active_broadcasts ( name ) VALUES ( '{$data['name']}' ) ";
if( $mysqli->query( $query ) )
{
$transport = json_encode( false );
exit( $transport );
}
else
exit( $mysqli->error );
}
else
exit( 'No data sent' );
?>
xhr-signalhandler-post.php:
<?php
require( "connection.inc.php" );
$response = array();
//{"message":{"eventName":"presence","data":{"userid":"winey","isOnline":true}},"sender":"winey"}
// var_dump( $_POST );
// exit;
if( isset( $_POST['message'] ) )
{
$query = " INSERT INTO webrtc-messages ( name ) VALUES ( '{$_POST['name']}' ) ";
if( $mysqli->query( $query ) )
{
$transport = json_encode( false );
exit( $transport );
}
else
exit( $mysqli->error );
// Now, if someone else joins the room; you can update above record; and append his "user-id" in the "Participants-id" column.
}
if( @$_POST["message"] = "undefined" )
$response = false;
$transport = json_encode( $response );
exit( $transport );
?>
xhr-signalhandler-get.php:
<?php
require( "connection.inc.php" );
$response = array();
// var_dump( $_POST );
if( isset( $_POST['message'] ) )
{
$query = "SELECT id , message , channel , `sender-id` FROM `webrtc-messages` ";
if( $mysqli->connect_errno )
exit ( "Failed to connect to MySQL: " . $mysqli->connect_error );
if( $res = $mysqli->query( $query ) )
{
if( $res->num_rows > 0 )
{
while( $value = mysqli_fetch_assoc( $res ) )
{
//
}
}
}
else
{
echo "<center class='text-danger'>Server error</center>";
exit( $mysqli->error );
}
}
if( @$_POST["message"] = "undefined" )
$response = false;
$transport = json_encode( $response );
exit( $transport );
?>
https://github.com/muaz-khan/RTCMultiConnection/tree/master/demos/SSEConnection
, you said replace sseDirPath with sseDirPath='php-server.com/SSEConnection'. Does this not violate the requirement that this project is on localhost, because I believe the domain will need to resolve to the internet? – ButtonwoodsseDirPath
is an external domain (i.e. php-server.com/SSEConnection/), it means it requires internet at that point, right? Or I just setsseDirPath = null
because it is on localhost? – ButtonwoodsseDirPath=http://localhost:9001/demos/SSEConection/SSE.php
– Derivation