Skip to main content

WebRTC

Stream videos to the browser

Backend

Prerequisites

poetry init
poetry add aiortc aiohttp python-socketio

Code

1. Basic app skeleton

backend/main.py
#!/usr/bin/env python3
import aiohttp
import aiortc
import socketio

sio = socketio.AsyncServer(cors_allowed_origins='*')

if __name__ == '__main__':
app = aiohttp.web.Application()
sio.attach(app)
aiohttp.web.run_app(app, port=5000)

2. Routes

backend/main.py
@sio.event
async def connect(sid, environ):
await sio.emit('connection', room=sid)

@sio.on('offer')
async def offer(sid, params):
offer = aiortc.RTCSessionDescription(**params)
connection = aiortc.RTCPeerConnection()

@connection.on("connectionstatechange")
async def on_connectionstatechange():
if connection.connectionState == "failed":
await connection.close()

await connection.setRemoteDescription(offer)
connection.addTrack(create_track())
answer = await connection.createAnswer()
await connection.setLocalDescription(answer)
await sio.emit("answer", {
"sdp": connection.localDescription.sdp,
"type": connection.localDescription.type
}, room=sid)

@sio.event
async def disconnect(sid):
if connection:
await connection.close()

async def on_shutdown(app):
if connection:
await connection.close()

# ...
app.on_shutdown.append(on_shutdown)

3. Create Track

backend/main.py
from aiortc.contrib.media import MediaPlayer, MediaRelay

relay = None
webcam = None

def create_track():
global relay, webcam
if relay is None:
options = {"framerate": "30", "video_size": "640x480"}
webcam = MediaPlayer("/dev/video0", format="v4l2", options=options)
relay = MediaRelay()
return relay.subscribe(webcam.video)

Frontend

Prerequisites

npx create-react-app frontend --template typescript
cd frontend
yarn add socket.io-client

Code

1. Modify App.tsx

frontend/src/App.tsx
import {useWebRTCStream} from './hooks/useWebRTCStream'

function App() {
const [stream] = useWebRTCStream("http://localhost:5000")
return <video
ref={video => {if (video && stream) {video.srcObject = stream}}}
autoPlay
playsInline
/>
}

export default App;

2. Add hooks/useWebRTCStream.ts

frontend/src/hooks/useWebRTCStream.ts
import { useState, useEffect } from 'react';
import { io } from "socket.io-client";

let connection: RTCPeerConnection|undefined;

export const useWebRTCStream = (url:string):[MediaStream|null] => {
const [stream, setStream] = useState<MediaStream|null>(null);

useEffect(()=>{
setStream(null)
const socket = io(url);
const config = {
sdpSemantics: 'unified-plan',
iceServers:[{urls: ['stun:stun.l.google.com:19302']}]
};
socket.on("disconnect", () => {
setStream(null)
connection?.close()
})

socket.on('connection', async () => {
connection = new RTCPeerConnection(config);
connection.ontrack = ({ streams: [stream]}) => {
setStream(stream)
}
connection.addTransceiver('video', {direction: 'recvonly'});
const offer = await connection.createOffer();
await connection.setLocalDescription(offer);
await new Promise<void>((resolve) => {
if(connection?.iceGatheringState === 'complete') {
resolve()
} else {
const checkState = () => {
if (connection?.iceGatheringState === 'complete') {
connection?.removeEventListener('icegatheringstatechange', checkState)
resolve()
}
}
connection?.addEventListener('icegatheringstatechange', checkState)
}
})
await socket.emit('offer', {
sdp: connection.localDescription?.sdp,
type: connection.localDescription?.type
})
})
socket.on('answer', async answer => {
await connection?.setRemoteDescription(answer)
})
return () => {
connection?.close()
socket.close()
}
}, [url])

return [stream]
}

Source