Implemented WebRTC peer-to-peer screen-roadcasting
This commit is contained in:
parent
62fc69489d
commit
96cdcfc840
5 changed files with 120 additions and 29 deletions
|
|
@ -36,7 +36,7 @@
|
||||||
},
|
},
|
||||||
"iceServers":[
|
"iceServers":[
|
||||||
{
|
{
|
||||||
"url":"stun:stun.moonlight-stream.org:3478"
|
"urls":["stun:stun.moonlight-stream.org:3478"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links":{
|
"links":{
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
//Privdes STUN/TURN servers for WebRTC functionality
|
//Privdes STUN/TURN servers for WebRTC functionality
|
||||||
"iceServers":[
|
"iceServers":[
|
||||||
{
|
{
|
||||||
"url":"stun:stun.moonlight-stream.org:3478"
|
"urls":["stun:stun.moonlight-stream.org:3478"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
//Provides customizable links for navbar
|
//Provides customizable links for navbar
|
||||||
|
|
|
||||||
|
|
@ -944,6 +944,7 @@ class WebRTCLivestreamHandler extends rawFileBase{
|
||||||
//Call derived constructor
|
//Call derived constructor
|
||||||
super(client, player, media, "webrtc");
|
super(client, player, media, "webrtc");
|
||||||
|
|
||||||
|
//Define RTCHandler related listeners
|
||||||
this.defineListeners();
|
this.defineListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -968,18 +969,22 @@ class WebRTCLivestreamHandler extends rawFileBase{
|
||||||
};
|
};
|
||||||
|
|
||||||
//If we're not streaming
|
//If we're not streaming
|
||||||
if(!this.client.webRTCHandler.isStreaming){
|
if(!this.client.webRTCHandler.isBroadcasting){
|
||||||
//Send stream request
|
//Send stream request
|
||||||
await this.client.webRTCHandler.requestStream();
|
await this.client.webRTCHandler.requestStream();
|
||||||
|
//otherwise
|
||||||
|
}else{
|
||||||
|
//Load broadcasting stream into the player for local playback
|
||||||
|
this.startStream(this.client.webRTCHandler.broadcastingStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start(){
|
startStream(stream){
|
||||||
//call derived start function
|
//Set video source object to stream
|
||||||
super.start();
|
this.video.srcObject = stream;
|
||||||
|
|
||||||
//Lock the player
|
//Autoplay Stream
|
||||||
this.setPlayerLock(true);
|
this.video.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
setVideoTitle(title){
|
setVideoTitle(title){
|
||||||
|
|
@ -996,4 +1001,12 @@ class WebRTCLivestreamHandler extends rawFileBase{
|
||||||
//Inject glowspan into title in a way that allows it to be easily replaced
|
//Inject glowspan into title in a way that allows it to be easily replaced
|
||||||
this.player.title.prepend(glowSpan);
|
this.player.title.prepend(glowSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
end(){
|
||||||
|
//Let the webRTC handler know the stream ended
|
||||||
|
this.client.webRTCHandler.end();
|
||||||
|
|
||||||
|
//Call derived method
|
||||||
|
super.end();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -309,9 +309,11 @@ class queuePanel extends panelObj{
|
||||||
async goLive(event){
|
async goLive(event){
|
||||||
//If we're broadcastinv via webrtc
|
//If we're broadcastinv via webrtc
|
||||||
if(this.client.streamType == "webrtc"){
|
if(this.client.streamType == "webrtc"){
|
||||||
this.client.webRTCHandler.isBroadcasting = true;
|
await this.client.webRTCHandler.startStream();
|
||||||
|
|
||||||
this.client.webRTCHandler.broadcastingStream = await navigator.mediaDevices.getDisplayMedia();
|
//this.client.webRTCHandler.isBroadcasting = true;
|
||||||
|
|
||||||
|
//this.client.webRTCHandler.broadcastingStream = await navigator.mediaDevices.getDisplayMedia();
|
||||||
}
|
}
|
||||||
|
|
||||||
//Start a livestream
|
//Start a livestream
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,22 @@ class webRTCHandler{
|
||||||
* Parent Client Management object
|
* Parent Client Management object
|
||||||
*/
|
*/
|
||||||
this.client = client
|
this.client = client
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flag thrown when this device is broadcasting via webRTC
|
* Flag thrown when this device is broadcasting via webRTC
|
||||||
*/
|
*/
|
||||||
this.isBroadcasting = false;
|
this.isBroadcasting = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag thrown when a request to view a stream is answered
|
||||||
|
*/
|
||||||
|
this.answered = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create variable to hold Internet Connectivity Establishment configuration
|
||||||
|
*/
|
||||||
|
this.iceConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create variable to hold the peer connection object for the currently playing remote stream
|
* Create variable to hold the peer connection object for the currently playing remote stream
|
||||||
*/
|
*/
|
||||||
|
|
@ -49,9 +59,9 @@ class webRTCHandler{
|
||||||
this.currentViewers = new Map();
|
this.currentViewers = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create variable to hold Internet Connectivity Establishment configuration
|
* Create array to hold candidates cached before
|
||||||
*/
|
*/
|
||||||
this.iceConfig;
|
this.cachedCandidates = [];
|
||||||
|
|
||||||
//Call setup functions
|
//Call setup functions
|
||||||
this.setupInput();
|
this.setupInput();
|
||||||
|
|
@ -71,6 +81,29 @@ class webRTCHandler{
|
||||||
this.client.socket.on("RTCData", this.handleRTCData.bind(this));
|
this.client.socket.on("RTCData", this.handleRTCData.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async startStream(){
|
||||||
|
//Throw broadcasting flag
|
||||||
|
this.isBroadcasting = true;
|
||||||
|
|
||||||
|
//Get media stream from browser input
|
||||||
|
this.broadcastingStream = await navigator.mediaDevices.getDisplayMedia({
|
||||||
|
video: true,
|
||||||
|
audio: true
|
||||||
|
});
|
||||||
|
|
||||||
|
//If we didn't get any audio tracks from the DisplayMedia portion of the API
|
||||||
|
if(this.broadcastingStream == null || this.broadcastingStream.getAudioTracks().length == 0){
|
||||||
|
//Bug user for an audio device
|
||||||
|
const tempStream = await navigator.mediaDevices.getUserMedia({video: false, audio: true});
|
||||||
|
|
||||||
|
//For each track received from audio device
|
||||||
|
for(const track of tempStream.getAudioTracks()){
|
||||||
|
//Add to existing broadcast stream
|
||||||
|
this.broadcastingStream.addTrack(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles and routes incoming RTC Messages
|
* Handles and routes incoming RTC Messages
|
||||||
* Normally I'd use dedicated socket.io message types, however throwing user input seems like bad security practice
|
* Normally I'd use dedicated socket.io message types, however throwing user input seems like bad security practice
|
||||||
|
|
@ -113,14 +146,15 @@ class webRTCHandler{
|
||||||
//Create new peer connection w/ provided ice config
|
//Create new peer connection w/ provided ice config
|
||||||
this.currentStream = new RTCPeerConnection(this.iceConfig)
|
this.currentStream = new RTCPeerConnection(this.iceConfig)
|
||||||
|
|
||||||
//Not really used atm but it's necissary for most browsers to need either a data channel or media track
|
//Setup peer event handlers
|
||||||
//addeed to the requesting peer before they start pulling ICE candidates...
|
|
||||||
this.currentStream.createDataChannel("aux");
|
|
||||||
|
|
||||||
this.currentStream.addEventListener('icecandidate', this.sendIceCandidate.bind(this));
|
this.currentStream.addEventListener('icecandidate', this.sendIceCandidate.bind(this));
|
||||||
|
this.currentStream.addEventListener('track', this.handleVideoStream.bind(this));
|
||||||
|
|
||||||
//Create offer from peerConnection
|
//Create offer from peerConnection
|
||||||
const offer = await this.currentStream.createOffer();
|
const offer = await this.currentStream.createOffer({
|
||||||
|
offerToReceiveAudio: true,
|
||||||
|
offerToReceiveVideo: true
|
||||||
|
});
|
||||||
|
|
||||||
//Set offer as peerConnections local description
|
//Set offer as peerConnections local description
|
||||||
await this.currentStream.setLocalDescription(offer);
|
await this.currentStream.setLocalDescription(offer);
|
||||||
|
|
@ -161,18 +195,35 @@ class webRTCHandler{
|
||||||
//Get answer from received data
|
//Get answer from received data
|
||||||
const answer = new RTCSessionDescription(data.body);
|
const answer = new RTCSessionDescription(data.body);
|
||||||
|
|
||||||
|
//Throw answered flag
|
||||||
|
this.answered = true;
|
||||||
|
|
||||||
|
//for each cached ICE candidate
|
||||||
|
for(const candidate of this.cachedCandidates){
|
||||||
|
//Send cached candidate, handing candidate to method packed in a phoney event object
|
||||||
|
this.sendIceCandidate({candidate});
|
||||||
|
}
|
||||||
|
|
||||||
//Set remote desription
|
//Set remote desription
|
||||||
this.currentStream.setRemoteDescription(answer);
|
this.currentStream.setRemoteDescription(answer);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sendIceCandidate(event){
|
sendIceCandidate(event){
|
||||||
//If we have a candidate
|
//If we have a candidate
|
||||||
if(event.candidate != null){
|
if(event.candidate != null){
|
||||||
//Send that shit off!
|
//If our stream request was answered
|
||||||
this.client.socket.emit('relayRTCData',{
|
if(this.answered){
|
||||||
type: 'candidate',
|
//Send that shit off!
|
||||||
body: event.candidate
|
this.client.socket.emit('relayRTCData',{
|
||||||
});
|
type: 'candidate',
|
||||||
|
body: event.candidate
|
||||||
|
});
|
||||||
|
//Otherwise
|
||||||
|
}else{
|
||||||
|
//Cache candidate
|
||||||
|
this.cachedCandidates.push(event.candidate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,10 +237,34 @@ class webRTCHandler{
|
||||||
this.currentStream.addIceCandidate(data.body);
|
this.currentStream.addIceCandidate(data.body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleVideoStream(event){
|
||||||
|
//If we where given a media stream
|
||||||
|
if(event.streams != null && event.streams[0] != null){
|
||||||
|
//hand it over to our player
|
||||||
|
this.client.player.mediaHandler.startStream(event.streams[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
end(){
|
||||||
|
//Drop flags
|
||||||
|
this.isBroadcasting = false;
|
||||||
|
this.answered = false;
|
||||||
|
|
||||||
|
//clear peer/media stream objects
|
||||||
|
this.currentStream = null;
|
||||||
|
this.broadcastingStream = null;
|
||||||
|
|
||||||
|
//clear currentViewers map
|
||||||
|
this.currentViewers.clear();
|
||||||
|
|
||||||
|
//Clear candidate cache
|
||||||
|
this.cachedCandidates = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class representing a single connected viewer
|
* Class representing a single connected remote viewer
|
||||||
*/
|
*/
|
||||||
class connectedViewer{
|
class connectedViewer{
|
||||||
constructor(user, socket, offer, broadcastingStream, iceConfig, client){
|
constructor(user, socket, offer, broadcastingStream, iceConfig, client){
|
||||||
|
|
@ -214,7 +289,7 @@ class connectedViewer{
|
||||||
this.iceConfig = iceConfig;
|
this.iceConfig = iceConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client channe object
|
* Client channel object
|
||||||
*/
|
*/
|
||||||
this.client = client;
|
this.client = client;
|
||||||
|
|
||||||
|
|
@ -226,12 +301,12 @@ class connectedViewer{
|
||||||
//Create peer from iceConfig
|
//Create peer from iceConfig
|
||||||
const peer = new RTCPeerConnection(this.iceConfig);
|
const peer = new RTCPeerConnection(this.iceConfig);
|
||||||
|
|
||||||
|
//For every track from the local stream
|
||||||
for(const track of this.broadcastingStream.getTracks()){
|
for(const track of this.broadcastingStream.getTracks()){
|
||||||
|
//Add it to the list of available tracks for our peer
|
||||||
peer.addTrack(track, this.broadcastingStream);
|
peer.addTrack(track, this.broadcastingStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
peer.addEventListener('icecandidate', this.sendIceCandidate.bind(this));
|
|
||||||
|
|
||||||
//Set peer remote description to connection offer from viewer
|
//Set peer remote description to connection offer from viewer
|
||||||
peer.setRemoteDescription(new RTCSessionDescription(offer));
|
peer.setRemoteDescription(new RTCSessionDescription(offer));
|
||||||
|
|
||||||
|
|
@ -241,6 +316,9 @@ class connectedViewer{
|
||||||
//Set answer as local description
|
//Set answer as local description
|
||||||
await peer.setLocalDescription(answer);
|
await peer.setLocalDescription(answer);
|
||||||
|
|
||||||
|
//Setup peer ICE Candidate event listner
|
||||||
|
peer.addEventListener('icecandidate', this.sendIceCandidate.bind(this));
|
||||||
|
|
||||||
//Save peer by socket identifier
|
//Save peer by socket identifier
|
||||||
this.peers.set(socket, peer);
|
this.peers.set(socket, peer);
|
||||||
|
|
||||||
|
|
@ -265,9 +343,7 @@ class connectedViewer{
|
||||||
}
|
}
|
||||||
|
|
||||||
handleIceCandidate(data){
|
handleIceCandidate(data){
|
||||||
console.log(this);
|
|
||||||
const peer = this.peers.get(data.source.socket);
|
const peer = this.peers.get(data.source.socket);
|
||||||
console.log(peer);
|
|
||||||
peer.addIceCandidate(data.body);
|
peer.addIceCandidate(data.body);
|
||||||
|
|
||||||
//Save peer by socket identifier
|
//Save peer by socket identifier
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue