Skip to content

Commit

Permalink
Merge pull request #261 from open-easyrtc/feature/networked-video-source
Browse files Browse the repository at this point in the history
Add Streaming Video support with example
  • Loading branch information
vincentfretin committed Mar 9, 2021
2 parents c0a6951 + 6a291d5 commit 324e9b4
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 43 deletions.
22 changes: 17 additions & 5 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.

101 changes: 101 additions & 0 deletions examples/basic-video.html
@@ -0,0 +1,101 @@
<html>
<head>
<meta charset="utf-8">
<title>Dev 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="/js/spawn-in-circle.component.js"></script>
</head>
<body>
<a-scene networked-scene="
room: basic-video;
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>
<a-plane color="#FFF" width="4" height="3" position="0 .6 0" material="side: back" networked-video-source></a-plane>
</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>
<style>.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>
// On mobile remove elements that are resource heavy
var isMobile = AFRAME.utils.device.isMobile();

if (isMobile) {
var particles = document.getElementById('particles');
particles.parentNode.removeChild(particles);
}
</script>

<script>
// 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());
}
</script>
</body>
</html>
4 changes: 4 additions & 0 deletions examples/index.html
Expand Up @@ -41,6 +41,10 @@ <h1>Networked-Aframe Examples</h1>
<a href="basic-audio.html">Positional Audio</a>
<span>Example of using positional audio with WebRTC</span>
</div>
<div>
<a href="basic-video.html">Video Streaming</a>
<span>Example of using texture video with WebRTC</span>
</div>
<div>
<a href="a-saturday-night/">Dance Club</a>
<span>Night club based on A-Frame's <a href="https://aframe.io/a-saturday-night/">A Saturday Night</a> demo</span>
Expand Down
4 changes: 2 additions & 2 deletions src/NetworkConnection.js
Expand Up @@ -28,7 +28,7 @@ class NetworkConnection {
= this.entities.removeRemoteEntity.bind(this.entities);
}

connect(serverUrl, appName, roomName, enableAudio = false) {
connect(serverUrl, appName, roomName, enableAudio = false, enableVideo = false) {
NAF.app = appName;
NAF.room = roomName;

Expand All @@ -38,7 +38,7 @@ class NetworkConnection {

var webrtcOptions = {
audio: enableAudio,
video: false,
video: enableVideo,
datachannel: true
};
this.adapter.setWebRtcOptions(webrtcOptions);
Expand Down
105 changes: 71 additions & 34 deletions src/adapters/EasyRtcAdapter.js
Expand Up @@ -10,8 +10,8 @@ class EasyRtcAdapter extends NoOpAdapter {
this.app = "default";
this.room = "default";

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

this.serverTimeRequests = 0;
this.timeOffsets = [];
Expand All @@ -31,15 +31,16 @@ class EasyRtcAdapter extends NoOpAdapter {
this.easyrtc.joinRoom(roomName, null);
}

// options: { datachannel: bool, audio: bool }
// options: { datachannel: bool, audio: bool, video: bool }
setWebRtcOptions(options) {
// this.easyrtc.enableDebug(true);
this.easyrtc.enableDataChannels(options.datachannel);

this.easyrtc.enableVideo(false);
this.easyrtc.enableVideo(options.video);
this.easyrtc.enableAudio(options.audio);

this.easyrtc.enableVideoReceive(false);
// TODO receive(audio|video) options ?
this.easyrtc.enableVideoReceive(true);
this.easyrtc.enableAudioReceive(true);
}

Expand Down Expand Up @@ -97,14 +98,9 @@ class EasyRtcAdapter extends NoOpAdapter {
Promise.all([
this.updateTimeOffset(),
new Promise((resolve, reject) => {
this._connect(this.easyrtc.audioEnabled, resolve, reject);
this._connect(resolve, reject);
})
]).then(([_, clientId]) => {
this._storeAudioStream(
this.easyrtc.myEasyrtcid,
this.easyrtc.getLocalStream()
);

this._myRoomJoinTime = this._getRoomJoinTime(clientId);
this.connectSuccess(clientId);
}).catch(this.connectFailure);
Expand Down Expand Up @@ -177,17 +173,66 @@ class EasyRtcAdapter extends NoOpAdapter {
}
}

getMediaStream(clientId) {
var that = this;
if (this.audioStreams[clientId]) {
NAF.log.write("Already had audio for " + clientId);
return Promise.resolve(this.audioStreams[clientId]);
getMediaStream(clientId, type = "audio") {
if (this.mediaStreams[clientId]) {
NAF.log.write(`Already had ${type} for ${clientId}`);
return Promise.resolve(this.mediaStreams[clientId][type]);
} else {
NAF.log.write("Waiting on audio for " + clientId);
return new Promise(function(resolve) {
that.pendingAudioRequest[clientId] = resolve;
});
NAF.log.write(`Waiting on ${type} for ${clientId}`);
if (!this.pendingMediaRequests.has(clientId)) {
this.pendingMediaRequests.set(clientId, {});

const audioPromise = new Promise((resolve, reject) => {
this.pendingMediaRequests.get(clientId).audio = { resolve, reject };
});
const videoPromise = new Promise((resolve, reject) => {
this.pendingMediaRequests.get(clientId).video = { resolve, reject };
});

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

audioPromise.catch(e => NAF.log.warn(`${clientId} getMediaStream Audio Error`, e));
videoPromise.catch(e => NAF.log.warn(`${clientId} getMediaStream Video Error`, e));
}
return this.pendingMediaRequests.get(clientId)[type].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);
}

const videoStream = new MediaStream();
try {
stream.getVideoTracks().forEach(track => videoStream.addTrack(track));
} catch (e) {
NAF.log.warn(`${clientId} setMediaStream Video Error`, e);
}

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);
}
}

enableMicrophone(enabled) {
this.easyrtc.enableAudio(enabled);
this.easyrtc.enableMicrophone(enabled);
}

enableCamera(enabled) {
this.easyrtc.enableVideo(enabled);
this.easyrtc.enableCamera(enabled);
}

disconnect() {
Expand All @@ -198,27 +243,19 @@ class EasyRtcAdapter extends NoOpAdapter {
* Privates
*/

_storeAudioStream(easyrtcid, stream) {
this.audioStreams[easyrtcid] = stream;
if (this.pendingAudioRequest[easyrtcid]) {
NAF.log.write("got pending audio for " + easyrtcid);
this.pendingAudioRequest[easyrtcid](stream);
delete this.pendingAudioRequest[easyrtcid](stream);
}
}

_connect(audioEnabled, connectSuccess, connectFailure) {
_connect(connectSuccess, connectFailure) {
var that = this;

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

this.easyrtc.setOnStreamClosed(function(easyrtcid) {
delete that.audioStreams[easyrtcid];
delete that.mediaStreams[easyrtcid];
});

if (audioEnabled) {
if (that.easyrtc.audioEnabled || that.easyrtc.videoEnabled) {
this.easyrtc.initMediaSource(
function() {
function(stream) {
that.setMediaStream(that.easyrtc.myEasyrtcid, stream);
that.easyrtc.connect(that.app, connectSuccess, connectFailure);
},
function(errorCode, errmesg) {
Expand Down
3 changes: 2 additions & 1 deletion src/components/networked-scene.js
Expand Up @@ -9,6 +9,7 @@ AFRAME.registerComponent('networked-scene', {
onConnect: {default: 'onConnect'},
adapter: {default: 'wseasyrtc'}, // See https://github.com/networked-aframe/networked-aframe#adapters for list of adapters
audio: {default: false}, // Only if adapter supports audio
video: {default: false}, // Only if adapter supports video
debug: {default: false},
},

Expand All @@ -34,7 +35,7 @@ AFRAME.registerComponent('networked-scene', {
if (this.hasOnConnectFunction()) {
this.callOnConnect();
}
return NAF.connection.connect(this.data.serverURL, this.data.app, this.data.room, this.data.audio);
return NAF.connection.connect(this.data.serverURL, this.data.app, this.data.room, this.data.audio, this.data.video);
},

checkDeprecatedProperties: function() {
Expand Down

0 comments on commit 324e9b4

Please sign in to comment.