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]
}