Skip to content

Commit

Permalink
Add python rtc server
Browse files Browse the repository at this point in the history
  • Loading branch information
apssouza22 committed Oct 22, 2024
1 parent a8f1769 commit e27a91a
Show file tree
Hide file tree
Showing 7 changed files with 758 additions and 0 deletions.
52 changes: 52 additions & 0 deletions python/README.rst
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
319 changes: 319 additions & 0 deletions python/client.js
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 added python/demo-instruct.wav
Binary file not shown.
Loading

0 comments on commit e27a91a

Please sign in to comment.