|
@ -1,9 +1,9 @@ |
|
|
<!doctype html> |
|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<head> |
|
|
<meta charset="utf-8"> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|
|
<title>Video Streaming Demonstration</title> |
|
|
<title>WebRTC working example</title> |
|
|
<!-- Bootstrap --> |
|
|
<!-- Bootstrap --> |
|
|
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet"> |
|
|
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet"> |
|
|
<!-- jQuery and Bootstrap --> |
|
|
<!-- jQuery and Bootstrap --> |
|
@ -22,379 +22,118 @@ |
|
|
|
|
|
|
|
|
<script type="text/javascript"> |
|
|
<script type="text/javascript"> |
|
|
'use strict'; |
|
|
'use strict'; |
|
|
|
|
|
const SIGNALING_SERVER_URL = 'wss://' + document.domain + ':' + location.port + '/webrtc'; |
|
|
|
|
|
const PC_CONFIG = {}; |
|
|
var room = '{{room}}'; |
|
|
var room = '{{room}}'; |
|
|
|
|
|
|
|
|
var isChannelReady = false; |
|
|
var socket = io.connect(SIGNALING_SERVER_URL, { autoConnect: false }); |
|
|
var isInitiator = false; |
|
|
|
|
|
var isStarted = false; |
|
|
|
|
|
var localStream; |
|
|
|
|
|
var pc; |
|
|
|
|
|
var remoteStream; |
|
|
|
|
|
var turnReady; |
|
|
|
|
|
var receiveChannel; |
|
|
|
|
|
var sendChannel; |
|
|
|
|
|
var sendText = document.querySelector("#text"); |
|
|
|
|
|
var chatlog = document.querySelector('#chatlog'); |
|
|
|
|
|
|
|
|
|
|
|
var pcConfig = { |
|
|
socket.on('connect', function(){ |
|
|
'iceServers': [{ |
|
|
console.log("Connected...!", socket.connected) |
|
|
'urls': 'stun:stun.l.google.com:19302' |
|
|
}); |
|
|
}] |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// Set up audio and video regardless of what devices are present. |
|
|
|
|
|
var sdpConstraints = { |
|
|
|
|
|
offerToReceiveAudio: true, |
|
|
|
|
|
offerToReceiveVideo: true |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
///////////////////////////////////////////// |
|
|
|
|
|
|
|
|
|
|
|
//room = 'foo'; |
|
|
const video = document.querySelector("#stream"); |
|
|
// Could prompt for room name: |
|
|
|
|
|
// var room = prompt('Enter room name:'); |
|
|
|
|
|
|
|
|
|
|
|
var socket = io.connect('wss://' + document.domain + ':' + location.port + '/test'); |
|
|
|
|
|
|
|
|
|
|
|
if (room !== '') { |
|
|
|
|
|
socket.emit('create or join', room); |
|
|
|
|
|
console.log('Attempted to create or join room', room); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
socket.on('created', function(room) { |
|
|
|
|
|
console.log('Created room ' + room); |
|
|
|
|
|
isInitiator = true; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
socket.on('full', function(room) { |
|
|
|
|
|
console.log('Room ' + room + ' is full'); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
socket.on('join', function (room){ |
|
|
|
|
|
console.log('Another peer made a request to join room ' + room); |
|
|
|
|
|
console.log('This peer is the initiator of room ' + room + '!'); |
|
|
|
|
|
isChannelReady = true; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
socket.on('joined', function(room) { |
|
|
socket.on('data', (data) => { |
|
|
console.log('joined: ' + room); |
|
|
console.log('Data received: ',data); |
|
|
isChannelReady = true; |
|
|
handleSignalingData(data); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
socket.on('log', function(array) { |
|
|
socket.on('ready', () => { |
|
|
console.log.apply(console, array); |
|
|
console.log('Ready'); |
|
|
|
|
|
createPeerConnection(); |
|
|
|
|
|
sendOffer(); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
//////////////////////////////////////////////// |
|
|
let sendData = (data) => { |
|
|
|
|
|
socket.emit('data', data); |
|
|
function sendMessage(message) { |
|
|
|
|
|
console.log('Client sending message: ', message); |
|
|
|
|
|
socket.emit('message', message); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// This client receives a message |
|
|
|
|
|
socket.on('message', function(message) { |
|
|
|
|
|
console.log('Client received message:', message); |
|
|
|
|
|
if (message === 'got user media') { |
|
|
|
|
|
maybeStart(); |
|
|
|
|
|
} else if (message.type === 'offer') { |
|
|
|
|
|
if (!isInitiator && !isStarted) { |
|
|
|
|
|
maybeStart(); |
|
|
|
|
|
} |
|
|
|
|
|
pc.setRemoteDescription(new RTCSessionDescription(message)); |
|
|
|
|
|
doAnswer(); |
|
|
|
|
|
} else if (message.type === 'answer' && isStarted) { |
|
|
|
|
|
pc.setRemoteDescription(new RTCSessionDescription(message)); |
|
|
|
|
|
} else if (message.type === 'candidate' && isStarted) { |
|
|
|
|
|
var candidate = new RTCIceCandidate({ |
|
|
|
|
|
sdpMLineIndex: message.label, |
|
|
|
|
|
candidate: message.candidate |
|
|
|
|
|
}); |
|
|
|
|
|
pc.addIceCandidate(candidate); |
|
|
|
|
|
} else if (message === 'bye' && isStarted) { |
|
|
|
|
|
handleRemoteHangup(); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////// |
|
|
|
|
|
|
|
|
|
|
|
var localVideo = document.querySelector('#localVideo'); |
|
|
|
|
|
var remoteVideo = document.querySelector('#remoteVideo'); |
|
|
|
|
|
|
|
|
|
|
|
navigator.mediaDevices.getUserMedia({ |
|
|
|
|
|
audio: false, |
|
|
|
|
|
video: true |
|
|
|
|
|
}) |
|
|
|
|
|
.then(gotStream) |
|
|
|
|
|
.catch(function(e) { |
|
|
|
|
|
alert('getUserMedia() error: ' + e.name); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
function gotStream(stream) { |
|
|
|
|
|
console.log('Adding local stream.'); |
|
|
|
|
|
localVideo.src = window.URL.createObjectURL(stream); |
|
|
|
|
|
localStream = stream; |
|
|
|
|
|
sendMessage('got user media'); |
|
|
|
|
|
if (isInitiator) { |
|
|
|
|
|
maybeStart(); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var constraints = { |
|
|
|
|
|
video: true |
|
|
|
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
console.log('Getting user media with constraints', constraints); |
|
|
// WebRTC methods |
|
|
|
|
|
let pc; |
|
|
if (false) { |
|
|
let localStream; |
|
|
requestTurn( |
|
|
let remoteStreamElement = document.querySelector('#stream'); |
|
|
'https://computeengineondemand.appspot.com/turn?username=41784574&key=4080218913' |
|
|
|
|
|
); |
|
|
let getLocalStream = () => { |
|
|
|
|
|
navigator.mediaDevices.getUserMedia({ audio: true, video: true }) |
|
|
|
|
|
.then((stream) => { |
|
|
|
|
|
console.log('Stream found'); |
|
|
|
|
|
localStream = stream; |
|
|
|
|
|
// Connect after making sure that local stream is availble |
|
|
|
|
|
socket.connect(); |
|
|
|
|
|
}) |
|
|
|
|
|
.catch(error => { |
|
|
|
|
|
console.error('Stream not found: ', error); |
|
|
|
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function maybeStart() { |
|
|
let createPeerConnection = () => { |
|
|
console.log('>>>>>>> maybeStart() ', isStarted, localStream, isChannelReady); |
|
|
try { |
|
|
if (!isStarted && typeof localStream !== 'undefined' && isChannelReady) { |
|
|
pc = new RTCPeerConnection(PC_CONFIG); |
|
|
console.log('>>>>>> creating peer connection'); |
|
|
pc.onicecandidate = onIceCandidate; |
|
|
createPeerConnection(); |
|
|
pc.onaddstream = onAddStream; |
|
|
pc.addStream(localStream); |
|
|
pc.addStream(localStream); |
|
|
isStarted = true; |
|
|
console.log('PeerConnection created'); |
|
|
console.log('isInitiator', isInitiator); |
|
|
} catch (error) { |
|
|
if (isInitiator) { |
|
|
console.error('PeerConnection failed: ', error); |
|
|
doCall(); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
window.onbeforeunload = function() { |
|
|
|
|
|
socket.emit('disconnect', room) |
|
|
|
|
|
console.log("sending disconnect") |
|
|
|
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////// |
|
|
let sendOffer = () => { |
|
|
|
|
|
console.log('Send offer'); |
|
|
function createPeerConnection() { |
|
|
pc.createOffer().then( |
|
|
try { |
|
|
setAndSendLocalDescription, |
|
|
pc = new RTCPeerConnection(pcConfig); |
|
|
(error) => { console.error('Send offer failed: ', error); } |
|
|
sendChannel = pc.createDataChannel('chat', null); |
|
|
); |
|
|
pc.onicecandidate = handleIceCandidate; |
|
|
}; |
|
|
pc.onaddstream = handleRemoteStreamAdded; |
|
|
|
|
|
pc.onremovestream = handleRemoteStreamRemoved; |
|
|
|
|
|
pc.ondatachannel = handleChannelCallback; |
|
|
|
|
|
|
|
|
|
|
|
console.log('Created RTCPeerConnnection'); |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
console.log('Failed to create PeerConnection, exception: ' + e.message); |
|
|
|
|
|
alert('Cannot create RTCPeerConnection object.'); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function handleChannelCallback(event) { |
|
|
|
|
|
receiveChannel = event.channel; |
|
|
|
|
|
receiveChannel.onmessage = onReceiveMessageCallback; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onReceiveMessageCallback(event) { |
|
|
let sendAnswer = () => { |
|
|
var text = document.createElement("P"); |
|
|
console.log('Send answer'); |
|
|
text.appendChild(document.createTextNode(event.data)) |
|
|
pc.createAnswer().then( |
|
|
|
|
|
setAndSendLocalDescription, |
|
|
|
|
|
(error) => { console.error('Send answer failed: ', error); } |
|
|
|
|
|
); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
chatlog.appendChild(text); |
|
|
let setAndSendLocalDescription = (sessionDescription) => { |
|
|
} |
|
|
pc.setLocalDescription(sessionDescription); |
|
|
|
|
|
console.log('Local description set'); |
|
|
|
|
|
sendData(sessionDescription); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
function handleIceCandidate(event) { |
|
|
let onIceCandidate = (event) => { |
|
|
console.log('icecandidate event: ', event); |
|
|
|
|
|
if (event.candidate) { |
|
|
if (event.candidate) { |
|
|
sendMessage({ |
|
|
console.log('ICE candidate'); |
|
|
|
|
|
sendData({ |
|
|
type: 'candidate', |
|
|
type: 'candidate', |
|
|
label: event.candidate.sdpMLineIndex, |
|
|
candidate: event.candidate |
|
|
id: event.candidate.sdpMid, |
|
|
|
|
|
candidate: event.candidate.candidate |
|
|
|
|
|
}); |
|
|
}); |
|
|
} else { |
|
|
|
|
|
console.log('End of candidates.'); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
function sendData() { |
|
|
|
|
|
var text = document.createElement("P"); |
|
|
|
|
|
text.appendChild(document.createTextNode(sendText.value)); |
|
|
|
|
|
chatlog.appendChild(text); |
|
|
|
|
|
sendChannel.send(sendText.value); |
|
|
|
|
|
sendText.value = ''; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function handleRemoteStreamAdded(event) { |
|
|
|
|
|
console.log('Remote stream added.'); |
|
|
|
|
|
remoteVideo.src = window.URL.createObjectURL(event.stream); |
|
|
|
|
|
remoteStream = event.stream; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function handleCreateOfferError(event) { |
|
|
|
|
|
console.log('createOffer() error: ', event); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function doCall() { |
|
|
|
|
|
console.log('Sending offer to peer'); |
|
|
|
|
|
pc.createOffer(setLocalAndSendMessage, handleCreateOfferError); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function doAnswer() { |
|
|
|
|
|
console.log('Sending answer to peer.'); |
|
|
|
|
|
pc.createAnswer().then( |
|
|
|
|
|
setLocalAndSendMessage, |
|
|
|
|
|
onCreateSessionDescriptionError |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function setLocalAndSendMessage(sessionDescription) { |
|
|
|
|
|
// Set Opus as the preferred codec in SDP if Opus is present. |
|
|
|
|
|
// sessionDescription.sdp = preferOpus(sessionDescription.sdp); |
|
|
|
|
|
pc.setLocalDescription(sessionDescription); |
|
|
|
|
|
console.log('setLocalAndSendMessage sending message', sessionDescription); |
|
|
|
|
|
sendMessage(sessionDescription); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function onCreateSessionDescriptionError(error) { |
|
|
let onAddStream = (event) => { |
|
|
//trace('Failed to create session description: ' + error.toString()); |
|
|
console.log('Add stream'); |
|
|
} |
|
|
remoteStreamElement.srcObject = event.stream; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
function requestTurn(turnURL) { |
|
|
let handleSignalingData = (data) => { |
|
|
var turnExists = false; |
|
|
switch (data.type) { |
|
|
for (var i in pcConfig.iceServers) { |
|
|
case 'offer': |
|
|
if (pcConfig.iceServers[i].urls.substr(0, 5) === 'turn:') { |
|
|
createPeerConnection(); |
|
|
turnExists = true; |
|
|
pc.setRemoteDescription(new RTCSessionDescription(data)); |
|
|
turnReady = true; |
|
|
sendAnswer(); |
|
|
break; |
|
|
break; |
|
|
} |
|
|
case 'answer': |
|
|
} |
|
|
pc.setRemoteDescription(new RTCSessionDescription(data)); |
|
|
if (!turnExists) { |
|
|
|
|
|
console.log('Getting TURN server from ', turnURL); |
|
|
|
|
|
// No TURN server. Get one from computeengineondemand.appspot.com: |
|
|
|
|
|
var xhr = new XMLHttpRequest(); |
|
|
|
|
|
xhr.onreadystatechange = function() { |
|
|
|
|
|
if (xhr.readyState === 4 && xhr.status === 200) { |
|
|
|
|
|
var turnServer = JSON.parse(xhr.responseText); |
|
|
|
|
|
console.log('Got TURN server: ', turnServer); |
|
|
|
|
|
pcConfig.iceServers.push({ |
|
|
|
|
|
'url': 'turn:' + turnServer.username + '@' + turnServer.turn, |
|
|
|
|
|
'credential': turnServer.password |
|
|
|
|
|
}); |
|
|
|
|
|
turnReady = true; |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
xhr.open('GET', turnURL, true); |
|
|
|
|
|
xhr.send(); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function handleRemoteStreamAdded(event) { |
|
|
|
|
|
console.log('Remote stream added.'); |
|
|
|
|
|
remoteVideo.src = window.URL.createObjectURL(event.stream); |
|
|
|
|
|
remoteStream = event.stream; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function handleRemoteStreamRemoved(event) { |
|
|
|
|
|
console.log('Remote stream removed. Event: ', event); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function hangup() { |
|
|
|
|
|
console.log('Hanging up.'); |
|
|
|
|
|
stop(); |
|
|
|
|
|
sendMessage('bye'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function handleRemoteHangup() { |
|
|
|
|
|
console.log('Session terminated.'); |
|
|
|
|
|
stop(); |
|
|
|
|
|
isInitiator = false; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function stop() { |
|
|
|
|
|
isStarted = false; |
|
|
|
|
|
// isAudioMuted = false; |
|
|
|
|
|
// isVideoMuted = false; |
|
|
|
|
|
pc.close(); |
|
|
|
|
|
pc = null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/////////////////////////////////////////// |
|
|
|
|
|
|
|
|
|
|
|
// Set Opus as the default audio codec if it's present. |
|
|
|
|
|
function preferOpus(sdp) { |
|
|
|
|
|
var sdpLines = sdp.split('\r\n'); |
|
|
|
|
|
var mLineIndex; |
|
|
|
|
|
// Search for m line. |
|
|
|
|
|
for (var i = 0; i < sdpLines.length; i++) { |
|
|
|
|
|
if (sdpLines[i].search('m=audio') !== -1) { |
|
|
|
|
|
mLineIndex = i; |
|
|
|
|
|
break; |
|
|
break; |
|
|
} |
|
|
case 'candidate': |
|
|
} |
|
|
pc.addIceCandidate(new RTCIceCandidate(data.candidate)); |
|
|
if (mLineIndex === null) { |
|
|
|
|
|
return sdp; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// If Opus is available, set it as the default in m line. |
|
|
|
|
|
for (i = 0; i < sdpLines.length; i++) { |
|
|
|
|
|
if (sdpLines[i].search('opus/48000') !== -1) { |
|
|
|
|
|
var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i); |
|
|
|
|
|
if (opusPayload) { |
|
|
|
|
|
sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], |
|
|
|
|
|
opusPayload); |
|
|
|
|
|
} |
|
|
|
|
|
break; |
|
|
break; |
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Remove CN in m line and sdp. |
|
|
|
|
|
sdpLines = removeCN(sdpLines, mLineIndex); |
|
|
|
|
|
|
|
|
|
|
|
sdp = sdpLines.join('\r\n'); |
|
|
|
|
|
return sdp; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function extractSdp(sdpLine, pattern) { |
|
|
|
|
|
var result = sdpLine.match(pattern); |
|
|
|
|
|
return result && result.length === 2 ? result[1] : null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Set the selected codec to the first in m line. |
|
|
|
|
|
function setDefaultCodec(mLine, payload) { |
|
|
|
|
|
var elements = mLine.split(' '); |
|
|
|
|
|
var newLine = []; |
|
|
|
|
|
var index = 0; |
|
|
|
|
|
for (var i = 0; i < elements.length; i++) { |
|
|
|
|
|
if (index === 3) { // Format of media starts from the fourth. |
|
|
|
|
|
newLine[index++] = payload; // Put target payload to the first. |
|
|
|
|
|
} |
|
|
|
|
|
if (elements[i] !== payload) { |
|
|
|
|
|
newLine[index++] = elements[i]; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return newLine.join(' '); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Strip CN from sdp before CN constraints is ready. |
|
|
|
|
|
function removeCN(sdpLines, mLineIndex) { |
|
|
|
|
|
var mLineElements = sdpLines[mLineIndex].split(' '); |
|
|
|
|
|
// Scan from end for the convenience of removing an item. |
|
|
|
|
|
for (var i = sdpLines.length - 1; i >= 0; i--) { |
|
|
|
|
|
var payload = extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i); |
|
|
|
|
|
if (payload) { |
|
|
|
|
|
var cnPos = mLineElements.indexOf(payload); |
|
|
|
|
|
if (cnPos !== -1) { |
|
|
|
|
|
// Remove CN payload from m line. |
|
|
|
|
|
mLineElements.splice(cnPos, 1); |
|
|
|
|
|
} |
|
|
|
|
|
// Remove CN line in sdp |
|
|
|
|
|
sdpLines.splice(i, 1); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
sdpLines[mLineIndex] = mLineElements.join(' '); |
|
|
// Start connection |
|
|
return sdpLines; |
|
|
getLocalStream(); |
|
|
} |
|
|
|
|
|
</script> |
|
|
</script> |
|
|
|
|
|
|
|
|
</body> |
|
|
</body> |
|
|