webcom-reach
Version:
699 lines (676 loc) • 24.4 kB
HTML
<html>
<head>
<meta charset="UTF-8">
<script type="text/javascript" src="https://unpkg.com/jquery@2.1.1/dist/jquery.min.js"></script>
<!-- Materialize -->
<link rel="stylesheet" href="https://unpkg.com/materialize-css@0.97.6/dist/css/materialize.min.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<script src="https://unpkg.com/materialize-css@0.97.6/dist/js/materialize.min.js"></script>
<!-- Flexible Datasync -->
<script src="https://unpkg.com/webcom@1.2.3/webcom.js"></script>
<!-- Reach -->
<script src="../dist/reach-debug.js"></script>
<!-- Tester -->
<script type="text/javascript">
var format = function (tpl) {
var values = [].slice.call(arguments, 1);
return tpl.replace(
new RegExp('{([0-' + (values.length - 1 ) + '])}', 'g'),
function (match, index) {
return values[index];
}
);
};
// Use the same user
var localEmail = localStorage.getItem('reachtest:mail');
if (!localEmail && Reach.browser.compatible) {
var os;
if (/android/i.test(navigator.userAgent)) {
os = /android\s[0-9\.]+/i.exec(navigator.userAgent)[0].toLowerCase().replace(/[\s\.]/g, '_');
} else if (/iphone/i.test(navigator.userAgent)) {
os = 'ios'
} else if (/mac\sos\sx/i.test(navigator.userAgent)) {
os = 'osx';
} else if (/linux/i.test(navigator.userAgent)) {
os = 'linux';
} else {
os = 'win';
}
localEmail = format('{0}.{1}.{2}.{3}.@reach.io', Reach.browser.browser, Reach.browser.version, os, Date.now());
localStorage.setItem('reachtest:mail', localEmail);
}
var modalError = function (title, message) {
var dialog = $('#errorDialog');
dialog.find('h4').text(title);
dialog.find('p').text(message);
dialog.openModal();
};
$(document).ready(function () {
if (!Reach.browser.compatible) {
modalError('Incompatible browser', JSON.stringify(Reach.browser));
} else {
$('#current').show();
window.reachtest = new Reach('https://io.datasync.orange.com/base/reachtest', {
preferredAudioCodec: Reach.codecs.audio.OPUS,
preferredVideoCodec: Reach.codecs.video.VP9,
remoteStreamContainer: $('#remoteVideo')[0],
localStreamContainer: $('#localVideo')[0],
logLevel: 'DEBUG',
constraints: Reach.media.constraints()
});
var _radio = function (node, name, idx, label, value) {
$(node).append(
format(
'<p><input name="{0}" type="radio" id="{0}_{1}" value="{3}" {4}/><label for="{0}_{1}">{2}</label></p>',
name, idx, label, value, (idx === 0 ? 'checked' : '')
)
)
};
var _radios = function (node, name, values) {
values.forEach(function (item, i) {
_radio(node, name, i, item.label, item.value);
})
};
Reach.media.devices()
.then(function (devices) {
var map_devices = function (type) {
return devices[type].map(function (d) {
return {label: d.label || d.deviceId, value: d.deviceId}
})
};
var facingModes = Object.keys(Reach.media.facingMode).map(function (m) {
var v = Reach.media.facingMode[m];
return {value: v, label: v}
});
_radios('#vdevices', 'vdevice', map_devices('videoinput').concat(facingModes));
_radios('#adevices', 'adevice', map_devices('audioinput'));
});
var map_values = function (arr) {
return arr.map(function (m) {
return {value: m, label: m}
});
};
var map_codecs = function (type) {
return map_values(Object.keys(Reach.codecs[type]));
};
_radios('#vcodecs', 'vcodec', map_codecs('video'));
_radios('#acodecs', 'acodec', map_codecs('audio'));
_radios('#vpresets', 'vpreset', map_values(['HD', 'SD', 'FHD', 'UHD', 'SVGA', 'VGA']));
}
});
var user = {
add: function (u) {
if (u.uid != reachtest.current.uid && /reach.io$/.test(u.name) && !$(format('tr[data-userId={0}]', u.uid)).length) {
$('#users_list').append(
format(
'<tr data-userId="{0}"><td>{1}</td><td>{2}</td><td>{3}</td><td><a class="waves-effect waves-light btn">invite</a></td></tr>',
u.uid, u.name, u.status, new Date(u.lastSeen)
)
);
var bt = $('#users_list').find(format('tr[data-userId={0}] a', u.uid));
bt.toggleClass('disabled', /^DISCONNECTED$/.test(u.status) || /^NOT_CONNECTED$/.test(u.status));
bt.click(function (e) {
if (!$(e.target).hasClass('disabled') && !window._room) {
u.invite()
.then(function (d) {
window._room = d.room;
room.join(true);
});
}
});
}
},
update: function (u) {
if (u.uid && $('#users_list').find(format('tr[data-userId={0}]', u.uid)).length === 1) {
var tds = $('#users_list').find(format('tr[data-userId={0}] td', u.uid));
$(tds[0]).text(u.name);
$(tds[1]).text(u.status);
$('#users_list').find(format('tr[data-userId={0}] a', u.uid)).toggleClass('disabled', /^DISCONNECTED$/.test(u.status) || /^NOT_CONNECTED$/.test(u.status))
}
},
remove: function (u) {
u.uid && $('#users_list').find(format('tr[data-userId={0}]', u.uid)).remove();
}
};
var invite = {
add: function (i) {
if (i.isOnGoing && !window._room) {
i.accept()
.then(function (d) {
Materialize.toast(format('Invite from {0} to room {1} accepted', i.from, i.room), 3000);
window._room = d.room;
return room.join();
})
.catch(function (e) {
console.error(e);
window._room = null;
});
} else if (i.isOnGoing) {
i.reject()
.then(function () {
Materialize.toast(format('Invite from {0} to room {1} has been rejected', i.from, i.room), 3000);
})
} else {
// Old invite
reachtest.base.child(format('_/invites/{0}/{1}', reachtest.current.uid, +i.uid)).remove();
}
}
};
var constraints = {
show: function () {
$('#aconstraints').show();
$('#vconstraints').show();
},
hide: function () {
$('#aconstraints').hide();
$('#vconstraints').hide();
},
update: function () {
// Update constraints
var val = function (name) {
return $('[name=' + name + ']:checked').val();
};
window.reachtest.config.constraints = Reach.media.constraints(
val('vpreset'),
true,
'exact',
val('vdevice'),
val('adevice')
);
window.reachtest.config.preferredAudioCodec = Reach.codecs.audio[val('acodec')];
window.reachtest.config.preferredVideoCodec = Reach.codecs.video[val('vcodec')];
console.group('NEW CONSTRAINTS');
console.warn('Preset', val('vpreset'));
console.warn('Video device', val('vdevice'));
console.warn('Audio device', val('adevice'));
console.warn('Video codec', val('vcodec'), window.reachtest.config.preferredVideoCodec);
console.warn('Audio codec', val('acodec'), window.reachtest.config.preferredAudioCodec);
console.warn('Constraints', window.reachtest.config.constraints);
console.groupEnd();
}
};
var room = {
join: function (joined) {
(joined ? Promise.resolve(window._room) : window._room.join())
.then(function (_room) {
$('#users').hide();
constraints.show();
var roomNode = $('#room');
roomNode.find('table tbody').empty();
Object.keys(_room)
.filter(function (key) {
return !/^extra$/.test(key) && !/^_callbacks/.test(key);
})
.forEach(function (key) {
$("#roomMeta").append(format('<tr><td>{0}</td><td>{1}</td></tr>', key, _room[key]));
});
roomNode.show();
_room.on(Reach.events.room.STREAM_PUBLISHED, function (remoteStream) {
if (remoteStream.from !== reachtest.current.uid) {
window._remoteStream = remoteStream;
stream.subscribe();
}
});
_room.on(Reach.events.room.STREAM_UNPUBLISHED, function (stream) {
if (stream.uid === window._remoteStream.uid) {
$('#remote').hide();
window._remoteStream = null;
}
});
_room.on(Reach.events.room.PARTICIPANT_ADDED, function (participant) {
console.warn(Reach.events.room.PARTICIPANT_ADDED, participant.uid, participant.status);
var userName = participant.uid === reachtest.current.uid ? localEmail : $('#users_list').find(format('tr[data-userId={0}] td:first-child', participant.uid)).text();
$("#participants").append(
format(
'<tr data-userId="{0}"><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td></tr>',
participant.uid, userName, participant.role || 'NONE', new Date(participant._joined), participant.status
)
);
});
_room.on(Reach.events.room.PARTICIPANT_CHANGED, function (participant) {
console.warn(Reach.events.room.PARTICIPANT_CHANGED, participant.uid, participant.status);
var userName = $('#users_list').find(format('tr[data-userId={0}] td:first-child', participant.uid)).text();
$("#participants")
.find(format('tr[data-userId={0}]', participant.uid))
.replaceWith(
format(
'<tr data-userId="{0}"><td>{1}</td><td>{2}</td><td>{3}</td><td>{4}</td></tr>',
participant.uid, userName, participant.role || 'NONE', new Date(participant._joined), participant.status
)
);
});
_room.on(Reach.events.room.PARTICIPANT_REMOVED, function (participant) {
console.warn(Reach.events.room.PARTICIPANT_REMOVED, participant.uid, participant.status);
$("#participants").find(format('tr[data-userId={0}]', participant.uid)).remove();
room._leave();
});
});
},
_leave: function () {
window._room && window._room.leave().then(function () {
$('#users').show();
constraints.hide();
$('#local').hide();
$('#remote').hide();
$('#room').hide();
$('#share_bts').find('').toggleClass('disabled', false);
window._room = null;
});
},
leave: function (btn) {
if (!$(btn).hasClass('disabled')) {
room._leave();
}
},
publish: function (btn, type) {
if (!$(btn).hasClass('disabled')) {
constraints.update();
window._room.share(type)
.then(function (localStream) {
window._localStream = localStream;
$('#local').show();
constraints.hide();
$('#share_bts').find('a').toggleClass('disabled', true);
$('#audio_bt').toggle(/audio/i.test(type));
$('#video_bt').toggle(/video/i.test(type));
stream.muteLocalStatus(localStream.muted);
Reach.media.devices()
.then(function (devices) {
Object.keys(devices).forEach(function (kind) {
if (/input$/.test(kind)) {
var node = $('#' + kind);
node.empty();
var inputDevices = devices[kind];
if (kind === 'videoinput' && inputDevices.length > 1) {
inputDevices = inputDevices.concat([{deviceId: 'user'}, {deviceId: 'environment'}])
}
inputDevices.forEach(function (device, i) {
node.append(format('<li><input name="{0}" type="radio" id="{0}_{1}" value="{3}" {4}/><label for="{0}_{1}">{2}</label></li>',
kind, i, device.label || device.deviceId, device.deviceId, localStream._inputs[kind.replace(/input$/, '')] === device.deviceId ? 'checked' : ''
));
});
$('input[type=radio][name=' + kind + ']').change(function () {
window._localStream && window._localStream[kind === 'audioinput' ? 'switchMicrophone' : 'switchCamera'](this.value);
});
}
});
});
})
.catch(function (e) {
constraints.show();
modalError('Publish fail', e.message);
console.error('Publish fail', e);
});
}
}
};
var stream = {
switchCamera: function (btn, id) {
!$(btn).hasClass('disabled') && window._localStream && window._localStream.switchCamera(id).then(function () {
$('input[name=videoinput][value=' + (id || window._localStream._inputs.video) + ']').prop('checked', true);
});
},
switchMic: function (btn, id) {
!$(btn).hasClass('disabled') && window._localStream && window._localStream.switchMicrophone(id).then(function () {
$('input[name=audioinput][value=' + (id || window._localStream._inputs.audio) + ']').prop('checked', true);
});
},
close: function () {
window._localStream && window._localStream.close();
constraints.show();
$('#local').hide();
$('#share_bts').find('a').toggleClass('disabled', false);
},
muteStatus: function (status) {
$('#video_mute').toggle(status.video);
$('#audio_mute').toggle(status.audio);
},
muteLocalStatus: function (status) {
$('#mute_video').toggle(!status.video);
$('#unmute_video').toggle(status.video);
$('#mute_audio').toggle(!status.audio);
$('#unmute_audio').toggle(status.audio);
},
remoteBtns: function (subscribed) {
var remote = $('#remote');
remote.find('.col:first-child a').toggleClass('disabled', !subscribed);
remote.find('.col.s4 a:first-child').toggleClass('disabled', !subscribed);
remote.find('.col.s4 a:last-child').toggleClass('disabled', subscribed)
},
subscribe: function (btn) {
if (!$(btn).hasClass('disabled') && window._remoteStream) {
window._remoteStream.on(Reach.events.stream.MUTE, stream.muteStatus);
window._remoteStream.subscribe()
.then(function () {
$('#remote').show();
stream.muteStatus(window._remoteStream.muted);
stream.remoteBtns(true);
});
}
},
unSubscribe: function (btn) {
if (!$(btn).hasClass('disabled') && window._remoteStream) {
// window._remoteStream.off();
window._remoteStream.unSubscribe();
stream.remoteBtns(false);
}
},
mute: function (btn, type, value) {
if (!$(btn).hasClass('disabled') && window._localStream) {
window._localStream.mute(type, value);
stream.muteLocalStatus(window._localStream.muted);
}
},
localStats: function (btn) {
if (!$(btn).hasClass('disabled') && window._localStream && window._localStream.peerConnections.length) {
stream.stats('Local stats', window._localStream.peerConnections[0].pc)
}
},
remoteStats: function (btn) {
if (!$(btn).hasClass('disabled') && window._remoteStream && window._remoteStream.peerConnection) {
stream.stats('Remote stats', window._remoteStream.peerConnection.pc)
}
},
stats: function (title, peerCo) {
peerCo.getStats()
.then(function (stats) {
var statsDialog = $('#statsDialog');
statsDialog.find('h4').text(title);
statsDialog.find('ul').empty();
var values = stats.values(), next = values.next();
while (!next.done) {
var li = document.createElement('li');
$(li).append(format('<div class="collapsible-header truncate">{0}</div>', next.value.id));
$(li).append('<div class="collapsible-body"><table class="striped"><tbody></tbody></table></div>');
Object.keys(next.value).forEach(function (key) {
$(li).find('tbody').append(format('<tr><td>{0}</td><td class="truncate">{1}</td></tr>', key, next.value[key]));
});
statsDialog.find('ul').append(li);
next = values.next();
}
statsDialog.openModal();
})
}
};
var login = function (btn) {
if (!$(btn).hasClass('disabled')) {
var previousId = localStorage.getItem('reachtest:uid');
reachtest.login(localEmail, 'Passwd123', localEmail)
.catch(
function (e) {
if (/INVALID_USER/i.test(e.code)) {
return reachtest.register(localEmail, 'Passwd123');
}
return Promise.reject(e);
}
)
.then(function () {
$('#current').find('a').toggleClass('disabled');
$('#users').show();
constraints.hide();
room.leave();
reachtest.on(Reach.events.reach.INVITE_ADDED, invite.add);
reachtest.on(Reach.events.reach.USER_ADDED, user.add);
reachtest.on(Reach.events.reach.USER_CHANGED, user.update);
reachtest.on(Reach.events.reach.USER_REMOVED, user.remove);
if (previousId !== reachtest.current.uid) {
localStorage.setItem('reachtest:uid', reachtest.current.uid);
reachtest.base.child('/users/' + previousId).remove();
reachtest.base.child('/_/invite/' + previousId).remove();
reachtest.base.child('/_/devices/' + previousId).remove();
Webcom.INTERNAL.PersistentStorage.remove(previousId);
}
});
}
};
var logout = function (btn) {
if (!$(btn).hasClass('disabled')) {
reachtest.logout()
.then(function () {
$('#users').hide();
constraints.hide();
$('#users_list').empty();
$('#current').find('a').toggleClass('disabled');
$('#local').hide();
$('#room').hide();
$('#remote').hide()
});
}
}
</script>
<style>
video {
position: relative;
max-width: 100%;
}
</style>
</head>
<body>
<div id="errorDialog" class="modal">
<div class="modal-content">
<h4>Error</h4>
<p></p>
</div>
<div class="modal-footer">
<a href="#!" class=" modal-action modal-close waves-effect waves-green btn-flat">ok</a>
</div>
</div>
<div id="statsDialog" class="modal modal-fixed-footer">
<div class="modal-content">
<h4>Stats</h4>
<ul class="collapsible"></ul>
</div>
<div class="modal-footer">
<a href="#!" class=" modal-action modal-close waves-effect waves-green btn-flat">close</a>
</div>
</div>
<div class="row">
<div class="col s12" id="current" style="display:none">
<div class="card">
<div class="card-content">
<span class="card-title">Local User: <script>document.write(localEmail);</script></span>
</div>
<div class="card-action">
<a class="waves-effect waves-light btn" onclick="window.login(this)">login</a>
<a class="waves-effect waves-light btn disabled" onclick="window.logout(this)">logout</a>
</div>
</div>
</div>
<div class="col s12" id="users" style="display:none">
<div class="card">
<div class="card-content">
<span class="card-title">Connected Users</span>
<table class="striped">
<thead>
<th data-field="name">Name</th>
<th data-field="status">Status</th>
<th data-field="lastSeen">Last Seen</th>
</thead>
<tbody id="users_list"></tbody>
</table>
</div>
</div>
</div>
<div class="col s6" id="vconstraints" style="display:none">
<div class="card">
<div class="card-content">
<span class="card-title">Default Video Constraints</span>
<form action="#">
<h5>Device</h5>
<div id="vdevices"></div>
<h5>Preferred Codec</h5>
<div id="vcodecs"></div>
<h5>Preset</h5>
<div id="vpresets"></div>
</form>
</div>
</div>
</div>
<div class="col s6" id="aconstraints" style="display:none">
<div class="card">
<div class="card-content">
<span class="card-title">Default Audio Constraints</span>
<form action="#">
<h5>Device</h5>
<div id="adevices"></div>
<h5>Preferred Codec</h5>
<div id="acodecs"></div>
</form>
</div>
</div>
</div>
<div class="col s12" id="room" style="display:none">
<div class="card">
<div class="card-content">
<div class="row">
<div class="col s12 m12 l6">
<span class="card-title">Room</span>
<table class="striped">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody id="roomMeta"></tbody>
</table>
</div>
<div class="col s12 m12 l6">
<span class="card-title">Participants</span>
<table class="striped">
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th>Last Seen</th>
<th>Status</th>
</tr>
</thead>
<tbody id="participants"></tbody>
</table>
</div>
</div>
</div>
<div class="card-action">
<div class="row">
<div class="col s2">
<a class="waves-effect waves-light btn-floating blue" onclick="room.leave(this)"
title="Leave room"><i class="material-icons">power_settings_new</i></a>
</div>
<div class="col s4" id="share_bts">
<a class="waves-effect waves-light btn-floating cyan"
onclick="room.publish(this, Reach.types.AUDIO_VIDEO)" title="Share audio & video"><i
class="material-icons">perm_camera_mic</i></a>
<a class="waves-effect waves-light btn-floating blue-grey"
onclick="room.publish(this, Reach.types.AUDIO)" title="Share audio"><i
class="material-icons">mic</i></a>
<a class="waves-effect waves-light btn-floating green"
onclick="room.publish(this, Reach.types.VIDEO)" title="Share video"><i
class="material-icons">videocam</i></a>
</div>
<div class="col s6"></div>
</div>
</div>
</div>
</div>
<div class="col s12 m12 l6" id="local" style="display:none">
<div class="card">
<div class="card-content">
<span class="card-title">Local Stream</span>
<div id="localVideo"></div>
</div>
<div class="card-action">
<div class="row">
<div class="col s2">
<a class="waves-effect waves-light btn-floating blue" onclick="stream.localStats(this)"
title="stats"><i class="material-icons">trending_up</i></a>
</div>
<div class="col s2">
<a class="waves-effect waves-light btn-floating red" onclick="stream.close(this)" title="close"><i
class="material-icons">call_end</i></a>
</div>
<div class="col s4" id="video_bt">
<a
class="waves-effect waves-light btn-floating green"
id="mute_video"
onclick="stream.mute(this, 'video')"
>
<i class="material-icons">videocam_off</i>
</a>
<a
class="waves-effect waves-light btn-floating green"
id="unmute_video"
onclick="stream.mute(this, 'video', false)"
>
<i class="material-icons">videocam</i>
</a>
<a
class="waves-effect waves-light btn-floating green"
onclick="stream.switchCamera(this)"
title="Switch camera"
>
<i class="material-icons left">loop</i>
</a>
<ul id='videoinput'></ul>
</div>
<div class="col s4" id="audio_bt">
<a
class="waves-effect waves-light btn-floating blue-grey"
id="mute_audio"
onclick="stream.mute(this, 'audio')"
>
<i class="material-icons">mic_off</i>
</a>
<a
class="waves-effect waves-light btn-floating blue-grey"
id="unmute_audio"
onclick="stream.mute(this, 'audio', false)"
>
<i class="material-icons">mic</i>
</a>
<a
class="waves-effect waves-light btn-floating blue-grey"
onclick="stream.switchMic(this)"
title="Switch Microphone"
>
<i class="material-icons left">loop</i>
</a>
<ul id='audioinput' class='dropdown-content'></ul>
</div>
</div>
</div>
</div>
</div>
<div class="col s12 m12 l6" id="remote" style="display:none">
<div class="card">
<div class="card-content">
<span class="card-title">Remote Stream</span>
<div id="remoteVideo"></div>
</div>
<div class="card-action">
<div class="row">
<div class="col s2">
<a class="waves-effect waves-light btn-floating blue" onclick="stream.remoteStats(this)"
title="stats"><i class="material-icons">trending_up</i></a>
</div>
<div class="col s4">
<a class="waves-effect waves-light btn-floating red" onclick="stream.unSubscribe(this)"
title="unsubscribe"><i class="material-icons">call_end</i></a>
<a class="waves-effect waves-light btn-floating green" onclick="stream.subscribe(this)"
title="re-subscribe"><i class="material-icons">call</i></a>
</div>
<div class="col s2" id="video_status">
<a class="btn-floating disabled" id="video_mute" style="display:none"><i class="material-icons">videocam_off</i></a>
</div>
<div class="col s2" id="audio_status">
<a class="btn-floating disabled" id="audio_mute" style="display:none"><i class="material-icons">mic_off</i></a>
</div>
<div class="col s2"></div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>