-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a8f1769
commit e27a91a
Showing
7 changed files
with
758 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
Audio, video and data channel server | ||
==================================== | ||
|
||
This example illustrates establishing audio, video and a data channel with a | ||
browser. It also performs some image processing on the video frames using | ||
OpenCV. | ||
|
||
Running | ||
------- | ||
|
||
First install the required packages: | ||
|
||
.. code-block:: console | ||
$ pip install aiohttp aiortc opencv-python | ||
When you start the example, it will create an HTTP server which you | ||
can connect to from your browser: | ||
|
||
.. code-block:: console | ||
$ python server.py | ||
You can then browse to the following page with your browser: | ||
|
||
http://127.0.0.1:8080 | ||
|
||
Once you click `Start` the browser will send the audio and video from its | ||
webcam to the server. | ||
|
||
The server will play a pre-recorded audio clip and send the received video back | ||
to the browser, optionally applying a transform to it. | ||
|
||
In parallel to media streams, the browser sends a 'ping' message over the data | ||
channel, and the server replies with 'pong'. | ||
|
||
Additional options | ||
------------------ | ||
|
||
If you want to enable verbose logging, run: | ||
|
||
.. code-block:: console | ||
$ python server.py -v | ||
Credits | ||
------- | ||
|
||
The audio file "demo-instruct.wav" was borrowed from the Asterisk | ||
project. It is licensed as Creative Commons Attribution-Share Alike 3.0: | ||
|
||
https://wiki.asterisk.org/wiki/display/AST/Voice+Prompts+and+Music+on+Hold+License |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,319 @@ | ||
// get DOM elements | ||
var dataChannelLog = document.getElementById('data-channel'), | ||
iceConnectionLog = document.getElementById('ice-connection-state'), | ||
iceGatheringLog = document.getElementById('ice-gathering-state'), | ||
signalingLog = document.getElementById('signaling-state'); | ||
|
||
// peer connection | ||
var pc = null; | ||
|
||
// data channel | ||
var dc = null, dcInterval = null; | ||
|
||
function createPeerConnection() { | ||
var config = { | ||
sdpSemantics: 'unified-plan' | ||
}; | ||
|
||
if (document.getElementById('use-stun').checked) { | ||
config.iceServers = [{ urls: ['stun:stun.l.google.com:19302'] }]; | ||
} | ||
|
||
pc = new RTCPeerConnection(config); | ||
|
||
// register some listeners to help debugging | ||
pc.addEventListener('icegatheringstatechange', () => { | ||
iceGatheringLog.textContent += ' -> ' + pc.iceGatheringState; | ||
}, false); | ||
iceGatheringLog.textContent = pc.iceGatheringState; | ||
|
||
pc.addEventListener('iceconnectionstatechange', () => { | ||
iceConnectionLog.textContent += ' -> ' + pc.iceConnectionState; | ||
}, false); | ||
iceConnectionLog.textContent = pc.iceConnectionState; | ||
|
||
pc.addEventListener('signalingstatechange', () => { | ||
signalingLog.textContent += ' -> ' + pc.signalingState; | ||
}, false); | ||
signalingLog.textContent = pc.signalingState; | ||
|
||
// connect audio / video | ||
pc.addEventListener('track', (evt) => { | ||
if (evt.track.kind == 'video') | ||
document.getElementById('video').srcObject = evt.streams[0]; | ||
else | ||
document.getElementById('audio').srcObject = evt.streams[0]; | ||
}); | ||
|
||
return pc; | ||
} | ||
|
||
function enumerateInputDevices() { | ||
const populateSelect = (select, devices) => { | ||
let counter = 1; | ||
devices.forEach((device) => { | ||
const option = document.createElement('option'); | ||
option.value = device.deviceId; | ||
option.text = device.label || ('Device #' + counter); | ||
select.appendChild(option); | ||
counter += 1; | ||
}); | ||
}; | ||
|
||
navigator.mediaDevices.enumerateDevices().then((devices) => { | ||
populateSelect( | ||
document.getElementById('audio-input'), | ||
devices.filter((device) => device.kind == 'audioinput') | ||
); | ||
populateSelect( | ||
document.getElementById('video-input'), | ||
devices.filter((device) => device.kind == 'videoinput') | ||
); | ||
}).catch((e) => { | ||
alert(e); | ||
}); | ||
} | ||
|
||
function negotiate() { | ||
return pc.createOffer().then((offer) => { | ||
return pc.setLocalDescription(offer); | ||
}).then(() => { | ||
// wait for ICE gathering to complete | ||
return new Promise((resolve) => { | ||
if (pc.iceGatheringState === 'complete') { | ||
resolve(); | ||
} else { | ||
function checkState() { | ||
if (pc.iceGatheringState === 'complete') { | ||
pc.removeEventListener('icegatheringstatechange', checkState); | ||
resolve(); | ||
} | ||
} | ||
pc.addEventListener('icegatheringstatechange', checkState); | ||
} | ||
}); | ||
}).then(() => { | ||
var offer = pc.localDescription; | ||
var codec; | ||
|
||
codec = document.getElementById('audio-codec').value; | ||
if (codec !== 'default') { | ||
offer.sdp = sdpFilterCodec('audio', codec, offer.sdp); | ||
} | ||
|
||
codec = document.getElementById('video-codec').value; | ||
if (codec !== 'default') { | ||
offer.sdp = sdpFilterCodec('video', codec, offer.sdp); | ||
} | ||
|
||
document.getElementById('offer-sdp').textContent = offer.sdp; | ||
return fetch('/offer', { | ||
body: JSON.stringify({ | ||
sdp: offer.sdp, | ||
type: offer.type, | ||
video_transform: document.getElementById('video-transform').value | ||
}), | ||
headers: { | ||
'Content-Type': 'application/json' | ||
}, | ||
method: 'POST' | ||
}); | ||
}).then((response) => { | ||
return response.json(); | ||
}).then((answer) => { | ||
document.getElementById('answer-sdp').textContent = answer.sdp; | ||
return pc.setRemoteDescription(answer); | ||
}).catch((e) => { | ||
alert(e); | ||
}); | ||
} | ||
|
||
function start() { | ||
document.getElementById('start').style.display = 'none'; | ||
|
||
pc = createPeerConnection(); | ||
|
||
var time_start = null; | ||
|
||
const current_stamp = () => { | ||
if (time_start === null) { | ||
time_start = new Date().getTime(); | ||
return 0; | ||
} else { | ||
return new Date().getTime() - time_start; | ||
} | ||
}; | ||
|
||
if (document.getElementById('use-datachannel').checked) { | ||
var parameters = JSON.parse(document.getElementById('datachannel-parameters').value); | ||
|
||
dc = pc.createDataChannel('chat', parameters); | ||
dc.addEventListener('close', () => { | ||
clearInterval(dcInterval); | ||
dataChannelLog.textContent += '- close\n'; | ||
}); | ||
dc.addEventListener('open', () => { | ||
dataChannelLog.textContent += '- open\n'; | ||
dcInterval = setInterval(() => { | ||
var message = 'ping ' + current_stamp(); | ||
dataChannelLog.textContent += '> ' + message + '\n'; | ||
dc.send(message); | ||
}, 1000); | ||
}); | ||
dc.addEventListener('message', (evt) => { | ||
dataChannelLog.textContent += '< ' + evt.data + '\n'; | ||
|
||
if (evt.data.substring(0, 4) === 'pong') { | ||
var elapsed_ms = current_stamp() - parseInt(evt.data.substring(5), 10); | ||
dataChannelLog.textContent += ' RTT ' + elapsed_ms + ' ms\n'; | ||
} | ||
}); | ||
} | ||
|
||
// Build media constraints. | ||
|
||
const constraints = { | ||
audio: false, | ||
video: false | ||
}; | ||
|
||
if (document.getElementById('use-audio').checked) { | ||
const audioConstraints = {}; | ||
|
||
const device = document.getElementById('audio-input').value; | ||
if (device) { | ||
audioConstraints.deviceId = { exact: device }; | ||
} | ||
|
||
constraints.audio = Object.keys(audioConstraints).length ? audioConstraints : true; | ||
} | ||
|
||
if (document.getElementById('use-video').checked) { | ||
const videoConstraints = {}; | ||
|
||
const device = document.getElementById('video-input').value; | ||
if (device) { | ||
videoConstraints.deviceId = { exact: device }; | ||
} | ||
|
||
const resolution = document.getElementById('video-resolution').value; | ||
if (resolution) { | ||
const dimensions = resolution.split('x'); | ||
videoConstraints.width = parseInt(dimensions[0], 0); | ||
videoConstraints.height = parseInt(dimensions[1], 0); | ||
} | ||
|
||
constraints.video = Object.keys(videoConstraints).length ? videoConstraints : true; | ||
} | ||
|
||
// Acquire media and start negociation. | ||
|
||
if (constraints.audio || constraints.video) { | ||
if (constraints.video) { | ||
document.getElementById('media').style.display = 'block'; | ||
} | ||
navigator.mediaDevices.getUserMedia(constraints).then((stream) => { | ||
stream.getTracks().forEach((track) => { | ||
pc.addTrack(track, stream); | ||
}); | ||
return negotiate(); | ||
}, (err) => { | ||
alert('Could not acquire media: ' + err); | ||
}); | ||
} else { | ||
negotiate(); | ||
} | ||
|
||
document.getElementById('stop').style.display = 'inline-block'; | ||
} | ||
|
||
function stop() { | ||
document.getElementById('stop').style.display = 'none'; | ||
|
||
// close data channel | ||
if (dc) { | ||
dc.close(); | ||
} | ||
|
||
// close transceivers | ||
if (pc.getTransceivers) { | ||
pc.getTransceivers().forEach((transceiver) => { | ||
if (transceiver.stop) { | ||
transceiver.stop(); | ||
} | ||
}); | ||
} | ||
|
||
// close local audio / video | ||
pc.getSenders().forEach((sender) => { | ||
sender.track.stop(); | ||
}); | ||
|
||
// close peer connection | ||
setTimeout(() => { | ||
pc.close(); | ||
}, 500); | ||
} | ||
|
||
function sdpFilterCodec(kind, codec, realSdp) { | ||
var allowed = [] | ||
var rtxRegex = new RegExp('a=fmtp:(\\d+) apt=(\\d+)\r$'); | ||
var codecRegex = new RegExp('a=rtpmap:([0-9]+) ' + escapeRegExp(codec)) | ||
var videoRegex = new RegExp('(m=' + kind + ' .*?)( ([0-9]+))*\\s*$') | ||
|
||
var lines = realSdp.split('\n'); | ||
|
||
var isKind = false; | ||
for (var i = 0; i < lines.length; i++) { | ||
if (lines[i].startsWith('m=' + kind + ' ')) { | ||
isKind = true; | ||
} else if (lines[i].startsWith('m=')) { | ||
isKind = false; | ||
} | ||
|
||
if (isKind) { | ||
var match = lines[i].match(codecRegex); | ||
if (match) { | ||
allowed.push(parseInt(match[1])); | ||
} | ||
|
||
match = lines[i].match(rtxRegex); | ||
if (match && allowed.includes(parseInt(match[2]))) { | ||
allowed.push(parseInt(match[1])); | ||
} | ||
} | ||
} | ||
|
||
var skipRegex = 'a=(fmtp|rtcp-fb|rtpmap):([0-9]+)'; | ||
var sdp = ''; | ||
|
||
isKind = false; | ||
for (var i = 0; i < lines.length; i++) { | ||
if (lines[i].startsWith('m=' + kind + ' ')) { | ||
isKind = true; | ||
} else if (lines[i].startsWith('m=')) { | ||
isKind = false; | ||
} | ||
|
||
if (isKind) { | ||
var skipMatch = lines[i].match(skipRegex); | ||
if (skipMatch && !allowed.includes(parseInt(skipMatch[2]))) { | ||
continue; | ||
} else if (lines[i].match(videoRegex)) { | ||
sdp += lines[i].replace(videoRegex, '$1 ' + allowed.join(' ')) + '\n'; | ||
} else { | ||
sdp += lines[i] + '\n'; | ||
} | ||
} else { | ||
sdp += lines[i] + '\n'; | ||
} | ||
} | ||
|
||
return sdp; | ||
} | ||
|
||
function escapeRegExp(string) { | ||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string | ||
} | ||
|
||
enumerateInputDevices(); |
Binary file not shown.
Oops, something went wrong.