Hi!
I am trying to create an aiortc-based server to send a video stream to a react-native-webrtc client.
Here is my simple server:
import asyncio
import os
import json
from aiohttp import web
from aiortc import MediaStreamTrack, RTCPeerConnection, RTCSessionDescription
from av import VideoFrame
import cv2
import glob
ROOT = os.path.dirname(__file__)
pcs = set()
image_files = sorted(glob.glob("./sample-video-frames/*.jpg")) # Change to the path to your images
current_image_index = 0
def get_next_image():
global current_image_index
img = cv2.imread(image_files[current_image_index])
current_image_index = (current_image_index + 1) % len(image_files)
return img
class ImageVideoTrack(MediaStreamTrack):
kind = "video"
def __init__(self):
super().__init__()
async def recv(self):
img = get_next_image()
new_frame = VideoFrame.from_ndarray(img, format="bgr24")
new_frame.pts = 0
new_frame.time_base = 0
await asyncio.sleep(1/30) # Set the desired frame rate (e.g., 30 FPS)
return new_frame
async def index(request):
content = open(os.path.join(ROOT, "index.html"), "r").read()
return web.Response(content_type="text/html", text=content)
async def offer(request):
params = await request.json()
offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])
pc = RTCPeerConnection()
pcs.add(pc)
@pc.on("connectionstatechange")
async def on_connectionstatechange():
print("Connection state change:", pc.connectionState)
if pc.connectionState == "failed":
await pc.close()
pcs.discard(pc)
@pc.on("track")
def on_track(track):
print("Track received:", track.kind)
if track.kind == "video":
local_video = ImageVideoTrack()
pc.addTrack(local_video)
async def send_frames():
while pc.connectionState == "connected":
local_video.on_frame()
await asyncio.sleep(0.01) # Set the desired frame rate (e.g., 100 FPS)
asyncio.ensure_future(send_frames())
print("Setting remote description")
await pc.setRemoteDescription(offer)
print("Creating answer")
answer = await pc.createAnswer()
# Modify the SDP to change "a=inactive" to "a=sendonly"
sdp_lines = answer.sdp.split('\n')
modified_sdp_lines = [line if "a=inactive" not in line else "a=sendonly" for line in sdp_lines]
modified_sdp = '\n'.join(modified_sdp_lines)
# Set the local description with the modified SDP
print("Setting local description")
await pc.setLocalDescription(RTCSessionDescription(sdp=modified_sdp, type=answer.type))
print("Answer created and local description set")
return web.Response(
content_type="application/json",
text=json.dumps(
{"sdp": pc.localDescription.sdp, "type": pc.localDescription.type}
),
)
async def on_shutdown(app):
print("Shutting down")
coros = [pc.close() for pc in pcs]
await asyncio.gather(*coros)
pcs.clear()
if __name__ == "__main__":
app = web.Application()
app.on_shutdown.append(on_shutdown)
app.router.add_get("/", index)
app.router.add_post("/offer", offer)
web.run_app(app, access_log=None, host="192.168.122.1", port=8080)
And here the client:
import React, { useEffect, useState } from 'react';
import { View } from 'react-native';
import {
RTCPeerConnection,
RTCSessionDescription,
mediaDevices,
} from 'react-native-webrtc';
import InCallManager from 'react-native-incall-manager';
import { RTCView } from 'react-native-webrtc';
const App = () => {
const [remoteStream, setRemoteStream] = useState(null);
useEffect(() => {
async function startStreaming() {
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});
pc.ontrack = (event) => {
console.log('Received remote stream:', event.streams[0]);
setRemoteStream(event.streams[0]);
};
pc.onicecandidate = async (event) => {
if (event.candidate) {
console.log('Sending ICE candidate:', event.candidate.substr(0, 50));
const response = await fetch('http://192.168.122.1:8080/candidate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
candidate: event.candidate,
}),
});
} else {
console.log('All ICE candidates sent');
}
};
const offer = await pc.createOffer({ offerToReceiveVideo: true });
console.log('Offer created:', offer.sdp.substr(0, 50));
await pc.setLocalDescription(offer);
console.log('Local description set:', pc.localDescription.sdp.substr(0, 50));
const response = await fetch('http://192.168.122.1:8080/offer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
sdp: pc.localDescription.sdp,
type: pc.localDescription.type,
}),
});
const responseData = await response.json();
const answer = new RTCSessionDescription(responseData);
console.log('Answer received:', answer.sdp.substr(0, 50));
await pc.setRemoteDescription(answer);
console.log('Remote description set:', pc.remoteDescription.sdp.substr(0, 50));
InCallManager.start({ media: 'video' });
}
startStreaming();
return () => {
InCallManager.stop();
};
}, []);
return (
<View style={{ flex: 1, backgroundColor: 'black' }}>
{remoteStream && (
<RTCView
objectFit="cover"
style={{ flex: 1 }}
streamURL={remoteStream.toURL()}
/>
)}
</View>
);
};
export default App;
Peer connection is successfully established as per logs:
LOG Running "WebrtcImageApp" with {"rootTag":11}
LOG rn-webrtc:pc:DEBUG 0 ctor +0ms
LOG rn-webrtc:pc:DEBUG 0 createOffer +1ms
LOG rn-webrtc:pc:DEBUG 0 createOffer OK +14ms
LOG Offer created: v=0
o=- 8514285042449230946 2 IN IP4 127.0.0.1
s
LOG rn-webrtc:pc:DEBUG 0 setLocalDescription +2ms
LOG rn-webrtc:pc:DEBUG 0 setLocalDescription OK +17ms
LOG Local description set: v=0
o=- 8514285042449230946 2 IN IP4 127.0.0.1
s
LOG Answer received: v=0
o=- 3890536821 3890536821 IN IP4 0.0.0.0
s=-
LOG rn-webrtc:pc:DEBUG 0 setRemoteDescription +62ms
LOG rn-webrtc:pc:DEBUG 0 ontrack +15ms
LOG All ICE candidates sent
However, I keep having the following error:
WARN Possible Unhandled Promise Rejection (id: 3):
TypeError: Cannot read property 'receiver' of undefined
TypeError: Cannot read property 'receiver' of undefined
It’s likely related to the ontrack
event, which is triggered when a remote stream is added to the peer connection. However, I cannot understand how to handle it.
Does anyone have experience with aiortc and react-native-webrtc connection?