Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple video streams for the easyrtc adapter #269 #294

Merged
merged 9 commits into from
Oct 31, 2021
8 changes: 4 additions & 4 deletions dist/networked-aframe.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/networked-aframe.min.js

Large diffs are not rendered by default.

121 changes: 121 additions & 0 deletions examples/basic-multi-streams.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<html>
<head>
<meta charset="utf-8">
<title>Multi Streams Example — Networked-Aframe</title>
<meta name="description" content="Dev Example — Networked-Aframe">

<script src="https://aframe.io/releases/1.2.0/aframe.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.slim.js"></script>
<script src="/easyrtc/easyrtc.js"></script>
<script src="/dist/networked-aframe.js"></script>

<script src="https://unpkg.com/aframe-randomizer-components@^3.0.1/dist/aframe-randomizer-components.min.js"></script>
<!--<script src="https://unpkg.com/aframe-particle-system-component@1.0.5/dist/aframe-particle-system-component.min.js"></script>-->
<script src="https://cdn.jsdelivr.net/gh/oneWaveAdrian/aframe-particle-system-component@aframe-1.2.0-upgrade/dist/aframe-particle-system-component.min.js"></script>
<script src="/js/spawn-in-circle.component.js"></script>
</head>
<body>
<a-scene networked-scene="
room: basic-multi-stream;
debug: true;
adapter: easyrtc;
audio: false;
video: true;
">
<a-assets>

<img id="grid" src="https://img.gs/bbdkhfbzkk/stretch/https://i.imgur.com/25P1geh.png" crossorigin="anonymous">
<img id="sky" src="https://i.imgur.com/WqlqEkq.jpg" crossorigin="anonymous" />

<!-- Templates -->

<!-- Avatar -->
<template id="avatar-template">
<a-entity class="avatar">
<a-plane color="#FFF" width="4" height="3" position="0 .6 0" material="side: front" networked-video-source></a-plane>
vincentfretin marked this conversation as resolved.
Show resolved Hide resolved
<a-plane color="#FFF" width="2" height="1" position="0 .7 0" material="side: front" networked-video-source="streamName: screen"></a-plane>
vincentfretin marked this conversation as resolved.
Show resolved Hide resolved
</a-entity>
</template>

<!-- /Templates -->
</a-assets>

<a-entity id="player"
networked="template:#avatar-template;attachTemplateToLocal:false;"
camera
position="0 1.6 0"
spawn-in-circle="radius:3"
wasd-controls look-controls
>
<a-sphere class="head"
visible="false"
random-color
></a-sphere>
</a-entity>

<a-entity position="0 0 0"
geometry="primitive: plane; width: 10000; height: 10000;" rotation="-90 0 0"
material="src: #grid; repeat: 10000 10000; transparent: true; metalness:0.6; roughness: 0.4; sphericalEnvMap: #sky;"></a-entity>

<a-entity light="color: #ccccff; intensity: 1; type: ambient;" visible=""></a-entity>
<a-entity light="color: #ffaaff; intensity: 1.5" position="5 5 5"></a-entity>

<a-sky src="#sky" rotation="0 -90 0"></a-sky>
<a-entity id="particles" particle-system="preset: snow"></a-entity>
</a-scene>

<!-- GitHub Corner. -->
<a href="https://github.com/networked-aframe/networked-aframe" class="github-corner">
<svg width="80" height="80" viewBox="0 0 250 250" style="fill:#222; color:#fff; position: absolute; top: 0; border: 0; right: 0;">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path>
</svg>
</a>
<button id="screen-btn" type="button" class="camera-btn">Share screen</button>
<style>.camera-btn{position:absolute;cursor:pointer;bottom:3%;left:3%;background:#fff;height:40px;width:130px;border-radius:30px}.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}
</style>

<script>
// Camera status
let screenEnabled = false;
// Camera button ele
const screenBtnEle = document.getElementById('screen-btn');

// On mobile remove elements that are resource heavy
const isMobile = AFRAME.utils.device.isMobile();

if (isMobile) {
const particles = document.getElementById('particles');
particles.parentNode.removeChild(particles);
}

// Define custom schema for syncing avatar color, set by random-color
NAF.schemas.add({
template: '#avatar-template',
components: [
'position',
'rotation'
]
});

// Called by Networked-Aframe when connected to server
function onConnect () {
console.log("onConnect", new Date());

// Handle screen button click (Off and On)
screenBtnEle.addEventListener('click', function() {
if (screenEnabled) {
NAF.connection.adapter.removeLocalMediaStream("screen");
screenEnabled = false;
screenBtnEle.textContent = 'Share screen';
} else {
navigator.mediaDevices.getDisplayMedia().then((stream) => {
NAF.connection.adapter.addLocalMediaStream(stream, "screen");
screenEnabled = true;
screenBtnEle.textContent = 'Stop Screen';
});
}
});
}
vincentfretin marked this conversation as resolved.
Show resolved Hide resolved
</script>
</body>
</html>
4 changes: 4 additions & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ <h1>Networked-Aframe Examples</h1>
<a href="basic-video.html">Video Streaming</a>
<span>Example of using texture video with WebRTC</span>
</div>
<div>
<a href="basic-multi-streams.html">Multi Streams</a>
<span>Example of using texture video with WebRTC from multiple local streams</span>
</div>
<div>
<a href="shooter-2.html">Shooter</a>
<span>Press spacebar to shoot. Example for spawning networked entities at runtime.</span>
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/NetworkConnection.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class NetworkConnection {
for (var id in oldOccupantList) {
var clientFound = newOccupantList[id];
if (!clientFound) {
NAF.log.write('Closing stream to ', id);
NAF.log.write('Closing stream to', id);
this.adapter.closeStreamConnection(id);
}
}
Expand All @@ -101,7 +101,7 @@ class NetworkConnection {
for (var id in occupantList) {
var startConnection = this.isNewClient(id) && this.adapter.shouldStartConnectionTo(occupantList[id]);
if (startConnection) {
NAF.log.write('Opening datachannel to ', id);
NAF.log.write('Opening datachannel to', id);
this.adapter.startStreamConnection(id);
}
}
Expand Down
148 changes: 108 additions & 40 deletions src/adapters/EasyRtcAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,21 @@ class EasyRtcAdapter extends NoOpAdapter {
this.room = "default";

this.mediaStreams = {};
this.remoteClients = {};
this.pendingMediaRequests = new Map();

this.serverTimeRequests = 0;
this.timeOffsets = [];
this.avgTimeOffset = 0;

this.easyrtc.setPeerOpenListener((clientId) => {
const clientConnection = this.easyrtc.getPeerConnectionByUserId(clientId);
this.remoteClients[clientId] = clientConnection;
});

this.easyrtc.setPeerClosedListener((clientId) => {
delete this.remoteClients[clientId];
});
}

setServerUrl(url) {
Expand Down Expand Up @@ -128,7 +138,7 @@ class EasyRtcAdapter extends NoOpAdapter {
}

closeStreamConnection(clientId) {
// Handled by easyrtc
this.easyrtc.hangup(clientId);
}

sendData(clientId, dataType, data) {
Expand Down Expand Up @@ -173,58 +183,113 @@ class EasyRtcAdapter extends NoOpAdapter {
}
}

getMediaStream(clientId, type = "audio") {
if (this.mediaStreams[clientId]) {
NAF.log.write(`Already had ${type} for ${clientId}`);
return Promise.resolve(this.mediaStreams[clientId][type]);
getMediaStream(clientId, streamName = "audio") {

if (this.mediaStreams[clientId] && this.mediaStreams[clientId][streamName]) {
NAF.log.write(`Already had ${streamName} for ${clientId}`);
return Promise.resolve(this.mediaStreams[clientId][streamName]);
} else {
NAF.log.write(`Waiting on ${type} for ${clientId}`);
NAF.log.write(`Waiting on ${streamName} for ${clientId}`);

// Create initial pendingMediaRequests with audio|video alias
if (!this.pendingMediaRequests.has(clientId)) {
this.pendingMediaRequests.set(clientId, {});
const pendingMediaRequests = {};

const audioPromise = new Promise((resolve, reject) => {
this.pendingMediaRequests.get(clientId).audio = { resolve, reject };
});
pendingMediaRequests.audio = { resolve, reject };
}).catch(e => NAF.log.warn(`${clientId} getMediaStream Audio Error`, e));
pendingMediaRequests.audio.promise = audioPromise;

const videoPromise = new Promise((resolve, reject) => {
this.pendingMediaRequests.get(clientId).video = { resolve, reject };
});
pendingMediaRequests.video = { resolve, reject };
}).catch(e => NAF.log.warn(`${clientId} getMediaStream Video Error`, e));
pendingMediaRequests.video.promise = videoPromise;

this.pendingMediaRequests.get(clientId).audio.promise = audioPromise;
this.pendingMediaRequests.get(clientId).video.promise = videoPromise;
this.pendingMediaRequests.set(clientId, pendingMediaRequests);
}

audioPromise.catch(e => NAF.log.warn(`${clientId} getMediaStream Audio Error`, e));
videoPromise.catch(e => NAF.log.warn(`${clientId} getMediaStream Video Error`, e));
const pendingMediaRequests = this.pendingMediaRequests.get(clientId);

// Create initial pendingMediaRequests with streamName
if (!pendingMediaRequests[streamName]) {
const streamPromise = new Promise((resolve, reject) => {
pendingMediaRequests[streamName] = { resolve, reject };
}).catch(e => NAF.log.warn(`${clientId} getMediaStream "${streamName}"" Error`, e))
pendingMediaRequests[streamName].promise = streamPromise;
}
return this.pendingMediaRequests.get(clientId)[type].promise;

return this.pendingMediaRequests.get(clientId)[streamName].promise;
}
}

setMediaStream(clientId, stream) {
// Safari doesn't like it when you use single a mixed media stream where one of the tracks is inactive, so we
// split the tracks into two streams.
const audioStream = new MediaStream();
try {
stream.getAudioTracks().forEach(track => audioStream.addTrack(track));
} catch(e) {
NAF.log.warn(`${clientId} setMediaStream Audio Error`, e);
}
setMediaStream(clientId, stream, streamName) {

const videoStream = new MediaStream();
try {
stream.getVideoTracks().forEach(track => videoStream.addTrack(track));
} catch (e) {
NAF.log.warn(`${clientId} setMediaStream Video Error`, e);
}
const clientMediaStreams = this.mediaStreams[clientId] = this.mediaStreams[clientId] || new Map();
clientMediaStreams.set(streamName, stream);

this.mediaStreams[clientId] = { audio: audioStream, video: videoStream };

// Resolve the promise for the user's media stream if it exists.
if (this.pendingMediaRequests.has(clientId)) {
this.pendingMediaRequests.get(clientId).audio.resolve(audioStream);
this.pendingMediaRequests.get(clientId).video.resolve(videoStream);
const pendingMediaRequests = this.pendingMediaRequests.get(clientId);

// Add mediaStreams audio streamName alias if does not exist yet
const audioTracks = stream.getAudioTracks();
if (!clientMediaStreams.has('audio') && audioTracks.length > 0) {
// Safari doesn't like it when you use single a mixed media stream where one of the tracks is inactive, so we
// split the tracks into two streams.
const audioStream = new MediaStream();
try {
audioTracks.forEach(track => audioStream.addTrack(track));
clientMediaStreams.set('audio', audioStream)
} catch(e) {
vincentfretin marked this conversation as resolved.
Show resolved Hide resolved
NAF.log.warn(`${clientId} setMediaStream "audio" alias Error`, e);
}

// Resolve the promise for the user's media stream audio alias if it exists.
pendingMediaRequests.audio.resolve(audioStream);
}

// Add mediaStreams video streamName alias if does not exist yet
const videoTracks = stream.getVideoTracks();
if (!clientMediaStreams.has('video') && videoTracks.length > 0) {
// Safari doesn't like it when you use single a mixed media stream where one of the tracks is inactive, so we
// split the tracks into two streams.
const videoStream = new MediaStream();
try {
videoTracks.forEach(track => videoStream.addTrack(track));
clientMediaStreams.set('video', videoStream)
} catch(e) {
NAF.log.warn(`${clientId} setMediaStream "video" alias Error`, e);
}

// Resolve the promise for the user's media stream video alias if it exists.
pendingMediaRequests.video.resolve(videoStream);
}

// Resolve the promise for the user's media stream by StreamName if it exists.
if (pendingMediaRequests[streamName]) {
pendingMediaRequests[streamName].resolve(stream);
}
}
}

addLocalMediaStream(stream, streamName) {
const easyrtc = this.easyrtc;
streamName = streamName || stream.id;
this.setMediaStream("local", stream, streamName);
easyrtc.register3rdPartyLocalMediaStream(stream, streamName);

// Add local stream to existing connections
Object.keys(this.remoteClients).forEach((clientId) => {
if (easyrtc.getConnectStatus(clientId) !== easyrtc.NOT_CONNECTED) {
easyrtc.addStreamToCall(clientId, streamName);
}
});
}

removeLocalMediaStream(streamName) {
this.easyrtc.closeLocalMediaStream(streamName);
delete this.mediaStreams["local"][streamName];
}

enableMicrophone(enabled) {
this.easyrtc.enableMicrophone(enabled);
}
Expand All @@ -246,14 +311,17 @@ class EasyRtcAdapter extends NoOpAdapter {

this.easyrtc.setStreamAcceptor(this.setMediaStream.bind(this));

this.easyrtc.setOnStreamClosed(function(easyrtcid) {
delete that.mediaStreams[easyrtcid];
this.easyrtc.setOnStreamClosed(function(clientId, stream, streamName) {
delete this.mediaStreams[clientId][streamName];
});

if (that.easyrtc.audioEnabled || that.easyrtc.videoEnabled) {
this.easyrtc.initMediaSource(
navigator.mediaDevices.getUserMedia({
video: that.easyrtc.videoEnabled,
audio: that.easyrtc.audioEnabled
}).then(
vincentfretin marked this conversation as resolved.
Show resolved Hide resolved
function(stream) {
that.setMediaStream(that.easyrtc.myEasyrtcid, stream);
that.addLocalMediaStream(stream, "default");
that.easyrtc.connect(that.app, connectSuccess, connectFailure);
},
function(errorCode, errmesg) {
Expand Down
3 changes: 2 additions & 1 deletion src/components/networked-audio-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ var naf = require('../NafIndex');

AFRAME.registerComponent('networked-audio-source', {
schema: {
streamName: { default: 'audio' },
positional: { default: true },
distanceModel: {
default: "inverse",
Expand All @@ -23,7 +24,7 @@ AFRAME.registerComponent('networked-audio-source', {
const ownerId = networkedEl.components.networked.data.owner;

if (ownerId) {
NAF.connection.adapter.getMediaStream(ownerId)
NAF.connection.adapter.getMediaStream(ownerId, this.data.streamName)
.then(this._setMediaStream)
.catch((e) => naf.log.error(`Error getting media stream for ${ownerId}`, e));
} else {
Expand Down
3 changes: 2 additions & 1 deletion src/components/networked-video-source.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var naf = require('../NafIndex');
AFRAME.registerComponent('networked-video-source', {

schema: {
streamName: { default: 'video' },
},

dependencies: ['material'],
Expand All @@ -19,7 +20,7 @@ AFRAME.registerComponent('networked-video-source', {
const ownerId = networkedEl.components.networked.data.owner;

if (ownerId) {
NAF.connection.adapter.getMediaStream(ownerId, "video")
NAF.connection.adapter.getMediaStream(ownerId, this.data.streamName)
.then(this._setMediaStream)
.catch((e) => naf.log.error(`Error getting media stream for ${ownerId}`, e));
} else {
Expand Down