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

Add Streaming Video support with example #261

Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
hthetiot marked this conversation as resolved.
Show resolved Hide resolved
">
<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>
hthetiot 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>
<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: 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
112 changes: 77 additions & 35 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,16 +31,17 @@ 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);
hthetiot marked this conversation as resolved.
Show resolved Hide resolved

this.easyrtc.enableVideoReceive(false);
this.easyrtc.enableAudioReceive(true);
// TODO receive(audio|video) options ?
this.easyrtc.enableVideoReceive(options.video);
this.easyrtc.enableAudioReceive(options.audio);
hthetiot marked this conversation as resolved.
Show resolved Hide resolved
}

setServerConnectListeners(successListener, failureListener) {
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,19 +173,73 @@ 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;
hthetiot marked this conversation as resolved.
Show resolved Hide resolved
}
}

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

setLocalMediaStream(stream) {
this.setMediaStream(
this.easyrtc.myEasyrtcid,
stream
);
}

enableMicrophone(enabled) {
hthetiot marked this conversation as resolved.
Show resolved Hide resolved
this.easyrtc.enableMicrophone(enabled);
}

enableCamera(enabled) {
this.easyrtc.enableCamera(enabled);
hthetiot marked this conversation as resolved.
Show resolved Hide resolved
}

disconnect() {
this.easyrtc.disconnect();
}
Expand All @@ -198,27 +248,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
87 changes: 87 additions & 0 deletions src/components/networked-video-source.js
@@ -0,0 +1,87 @@
/* global AFRAME, NAF, THREE */
var naf = require('../NafIndex');

AFRAME.registerComponent('networked-video-source', {

schema: {
},

dependencies: ['material'],

init: function () {
this.videoTexture = null;
this.video = null;
this.stream = null;

this._setMediaStream = this._setMediaStream.bind(this);

NAF.utils.getNetworkedEntity(this.el).then((networkedEl) => {
const ownerId = networkedEl.components.networked.data.owner;

if (ownerId) {
NAF.connection.adapter.getMediaStream(ownerId, "video")
.then(this._setMediaStream)
.catch((e) => naf.log.error(`Error getting media stream for ${ownerId}`, e));
} else {
// Correctly configured local entity, perhaps do something here for enabling debug audio loopback
}
});
},

_setMediaStream(newStream) {

if(!this.video) {
this.setupVideo();
}

if(newStream != this.stream) {
if (this.stream) {
this._clearMediaStream();
}

if (newStream) {
this.video.srcObject = newStream;
hthetiot marked this conversation as resolved.
Show resolved Hide resolved

var playResult = this.video.play();
hthetiot marked this conversation as resolved.
Show resolved Hide resolved
if (playResult instanceof Promise) {
playResult.catch((e) => naf.log.error(`Error play video stream`, e));
}

const mesh = this.el.getObject3D('mesh');
mesh.material.map = new THREE.VideoTexture(this.video);
hthetiot marked this conversation as resolved.
Show resolved Hide resolved
mesh.material.needsUpdate = true;
}

this.stream = newStream;
}
},

_clearMediaStream() {
if (this.video) {
this.video.srcObject = null;
this.video = null;
this.stream = null;
}
},

remove: function() {
if (!this.videoTexture) return;
hthetiot marked this conversation as resolved.
Show resolved Hide resolved

if (this.stream) {
this._clearMediaStream();
}
},

setupVideo: function() {
var el = this.el;

if (!this.video) {
var video = document.createElement('video');
video.setAttribute('autoplay', true);
video.setAttribute('playsinline', true);
video.setAttribute('muted', true);
}

this.video = video;
}
});
1 change: 1 addition & 0 deletions src/index.js
Expand Up @@ -5,3 +5,4 @@ require('./NafIndex.js');
require('./components/networked-scene');
require('./components/networked');
require('./components/networked-audio-source');
require('./components/networked-video-source');