Skip to content

Instantly share code, notes, and snippets.

@rodolfoap
Last active May 20, 2024 14:22
Show Gist options
  • Save rodolfoap/20411acc86ecca39d18ebfc2060bb8c5 to your computer and use it in GitHub Desktop.
Save rodolfoap/20411acc86ecca39d18ebfc2060bb8c5 to your computer and use it in GitHub Desktop.

AIORTC Memory Leak How to Reproduce

How-to-reproduce code for this issue: aiortc/aiortc#1091

Requirements:

  • Webcam accessible at /dev/video0
  • docker 20.10+
  • python3.11+.
  1. Clone this:
git clone https://gist.github.com/20411acc86ecca39d18ebfc2060bb8c5.git aiortc-memleak
cd aiortc-memleak
  1. Build and run the application using ./start.bash. It will build the application in a container (Ubuntu 24, same happens with Ubuntu 22).
./start.bash

Notice it will use av==11.0.0, but av==12.0.0 can also be used, see the Dockerfile.

See aiortc/aiortc#1088.

Same results with both versions av==11.0.0 and av==12.0.0.

  1. Open a browser window, and navigate to http://localhost:3000/. Close it, open it again,
chromium http://localhost:3000/
# Close the browser window or tab
chromium http://localhost:3000/

(Same results with Firefox)

  1. In a different window, run the memory tracer: ./ram_monitor.py. This is an example output:
$ ./ram_monitor.py
aiortc: 63.36; max:63.36, last_delta:63.36 Mb <- Server started
aiortc: 63.36; max:63.36, last_delta:63.36 Mb
aiortc: 63.36; max:63.36, last_delta:63.36 Mb
aiortc: 63.36; max:63.36, last_delta:63.36 Mb
aiortc: 101.56; max:101.56, last_delta:38.20 Mb <- Opened one browser window (1 browser window opened)
aiortc: 98.66; max:101.56, last_delta:38.20 Mb
aiortc: 94.23; max:101.56, last_delta:38.20 Mb
aiortc: 97.11; max:101.56, last_delta:38.20 Mb
aiortc: 94.46; max:101.56, last_delta:38.20 Mb
aiortc: 93.96; max:101.56, last_delta:38.20 Mb <- Closed browser window, this seems to be important
aiortc: 94.72; max:101.56, last_delta:38.20 Mb (0 browser windows opened)
aiortc: 96.80; max:101.56, last_delta:38.20 Mb
aiortc: 124.29; max:124.29, last_delta:22.72 Mb <- Opened one browser window (1 browser window opened)
aiortc: 124.15; max:124.29, last_delta:22.72 Mb <- No mouse/keyboard actions from here on
aiortc: 124.36; max:124.36, last_delta:0.07 Mb
aiortc: 126.22; max:126.22, last_delta:1.87 Mb
aiortc: 126.05; max:126.22, last_delta:1.87 Mb
aiortc: 124.65; max:126.22, last_delta:1.87 Mb
aiortc: 126.37; max:126.37, last_delta:0.14 Mb
aiortc: 125.99; max:126.37, last_delta:0.14 Mb
aiortc: 126.39; max:126.39, last_delta:0.02 Mb
aiortc: 123.27; max:126.39, last_delta:0.02 Mb
aiortc: 119.70; max:126.39, last_delta:0.02 Mb
aiortc: 119.83; max:126.39, last_delta:0.02 Mb
aiortc: 124.59; max:126.39, last_delta:0.02 Mb
aiortc: 126.18; max:126.39, last_delta:0.02 Mb
aiortc: 126.23; max:126.39, last_delta:0.02 Mb
aiortc: 126.36; max:126.39, last_delta:0.02 Mb
aiortc: 124.78; max:126.39, last_delta:0.02 Mb
aiortc: 127.43; max:127.43, last_delta:1.04 Mb
aiortc: 125.82; max:127.43, last_delta:1.04 Mb
aiortc: 120.86; max:127.43, last_delta:1.04 Mb
aiortc: 119.93; max:127.43, last_delta:1.04 Mb
aiortc: 119.82; max:127.43, last_delta:1.04 Mb
aiortc: 125.95; max:127.43, last_delta:1.04 Mb
aiortc: 120.29; max:127.43, last_delta:1.04 Mb
aiortc: 126.14; max:127.43, last_delta:1.04 Mb
aiortc: 126.02; max:127.43, last_delta:1.04 Mb
aiortc: 125.74; max:127.43, last_delta:1.04 Mb
aiortc: 126.10; max:127.43, last_delta:1.04 Mb
aiortc: 125.96; max:127.43, last_delta:1.04 Mb
aiortc: 126.09; max:127.43, last_delta:1.04 Mb
aiortc: 115.25; max:127.43, last_delta:1.04 Mb
aiortc: 127.70; max:127.70, last_delta:0.27 Mb
aiortc: 150.23; max:150.23, last_delta:22.54 Mb <- Memory leak starts with no reason. Fan blows a bit
aiortc: 173.70; max:173.70, last_delta:23.46 Mb    browser window response is fast, consider there is ONLY
aiortc: 195.98; max:195.98, last_delta:22.29 Mb    one browser window being displayed
aiortc: 218.69; max:218.69, last_delta:22.70 Mb
aiortc: 241.56; max:241.56, last_delta:22.88 Mb
aiortc: 264.44; max:264.44, last_delta:22.88 Mb
aiortc: 286.98; max:286.98, last_delta:22.54 Mb
aiortc: 310.10; max:310.10, last_delta:23.12 Mb
aiortc: 332.96; max:332.96, last_delta:22.87 Mb
aiortc: 355.93; max:355.93, last_delta:22.97 Mb
aiortc: 378.55; max:378.55, last_delta:22.62 Mb
aiortc: 401.27; max:401.27, last_delta:22.72 Mb
aiortc: 423.98; max:423.98, last_delta:22.71 Mb
aiortc: 447.43; max:447.43, last_delta:23.45 Mb
aiortc: 469.98; max:469.98, last_delta:22.55 Mb
aiortc: 492.85; max:492.85, last_delta:22.87 Mb
aiortc: 515.55; max:515.55, last_delta:22.71 Mb
aiortc: 538.34; max:538.34, last_delta:22.79 Mb
aiortc: 561.22; max:561.22, last_delta:22.88 Mb
aiortc: 584.44; max:584.44, last_delta:23.21 Mb
aiortc: 607.14; max:607.14, last_delta:22.70 Mb
aiortc: 629.81; max:629.81, last_delta:22.67 Mb
aiortc: 652.85; max:652.85, last_delta:23.04 Mb
aiortc: 675.30; max:675.30, last_delta:22.45 Mb
aiortc: 698.05; max:698.05, last_delta:22.75 Mb
aiortc: 721.34; max:721.34, last_delta:23.29 Mb
aiortc: 744.23; max:744.23, last_delta:22.89 Mb
... (grows until hanging up the PC)
var pc = null;
function negotiate() {
pc.addTransceiver('video', { direction: 'recvonly' });
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 {
const checkState = () => {
if (pc.iceGatheringState === 'complete') {
pc.removeEventListener('icegatheringstatechange', checkState);
resolve();
}
};
pc.addEventListener('icegatheringstatechange', checkState);
}
});
}).then(() => {
var offer = pc.localDescription;
return fetch('/offer', {
body: JSON.stringify({
sdp: offer.sdp,
type: offer.type,
}),
headers: {
'Content-Type': 'application/json'
},
method: 'POST'
});
}).then((response) => {
return response.json();
}).then((answer) => {
return pc.setRemoteDescription(answer);
}).catch((e) => {
alert(e);
});
}
function start() {
var config = {
sdpSemantics: 'unified-plan'
};
pc = new RTCPeerConnection(config);
// connect video
pc.addEventListener('track', (evt) => {
document.getElementById('video').srcObject = evt.streams[0];
});
negotiate();
}
function stop() {
setTimeout(() => { pc.close(); }, 500);
}
FROM ubuntu:noble
WORKDIR /app
ENV DEBIAN_FRONTEND=noninteractive
RUN apt update && apt install -y python3 python3-pip python3-venv python3-dev libavdevice-dev libavfilter-dev libopus-dev libvpx-dev pkg-config libopencv-dev
RUN python3 -m venv venv
ENV PATH="/app/venv/bin:$PATH"
RUN /app/venv/bin/pip install aiohttp==3.9.5 aioice==0.9.0 aiortc==1.8.0 aiosignal==1.3.1 opencv-python==4.9.0.80
# Uncomment this to use av==12.0.0
# See https://github.com/aiortc/aiortc/discussions/1088#discussioncomment-9287230
# RUN ./venv/bin/pip install av==12.0.0
EXPOSE 3000
COPY server.py /app/
COPY client.js /app/
COPY index.html /app/
COPY myrtc.py /app/
CMD /app/venv/bin/python3 server.py
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebRTC</title>
<script src="client.js"></script>
</head>
<body onload=start()>
<video id="video" autoplay controls muted></video>
<script> window.onbeforeunload = function(){ stop(); } </script>
</body>
</html>
import cv2, datetime
from av import VideoFrame
from aiortc.contrib.media import MediaStreamTrack
class VideoResizeTrack(MediaStreamTrack):
kind = "video"
def __init__(self, track, width=640):
super().__init__()
self.track=track
self.width=width
async def recv(self):
# Call the original recv()
frame = await self.track.recv()
# Transform video
image = frame.to_ndarray(format="bgr24")
height, width, layers = image.shape
ratio=width/self.width
image_resized = cv2.resize(image, (self.width, int(height/ratio)), interpolation = cv2.INTER_AREA)
frame_resized = VideoFrame.from_ndarray(image_resized, format="bgr24")
frame_resized.pts = frame.pts
frame_resized.time_base = frame.time_base
return frame_resized
#!/bin/python3
import docker, time, json
client = docker.from_env()
container = client.containers.get('aiortc')
stream = container.stats(stream=True)
max_ram_occupation, delta = 0, 0
for frame in stream:
stats=json.loads(frame.decode('utf-8'))
ram_occupation=stats['memory_stats']['usage']/1024/1024
if ram_occupation>max_ram_occupation:
delta=ram_occupation-max_ram_occupation
max_ram_occupation=ram_occupation
print(f"aiortc: {ram_occupation:.2f}; max:{max_ram_occupation:.2f}, last_delta:{delta:.2f} Mb")
import asyncio, json, os, logging
from aiohttp import web
from aiortc import RTCPeerConnection, RTCSessionDescription
from aiortc.contrib.media import MediaPlayer, MediaRelay
from myrtc import VideoResizeTrack
ROOT = os.path.dirname(__file__)
peer_connections = set()
player = MediaPlayer("/dev/video0")
relay = MediaRelay()
async def offer(request):
params = await request.json()
offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])
peer_connection = RTCPeerConnection()
peer_connections.add(peer_connection)
@peer_connection.on("connectionstatechange")
async def on_connectionstatechange():
print("Connection state is %s" % peer_connection.connectionState)
if peer_connection.connectionState in ["failed", "closed"]:
await peer_connection.close()
peer_connections.discard(peer_connection)
# I've resized the video, so the problem is easy to visualize.
video = VideoResizeTrack(relay.subscribe(player.video), width=800)
track = peer_connection.addTrack(video)
await peer_connection.setRemoteDescription(offer)
answer = await peer_connection.createAnswer()
await peer_connection.setLocalDescription(answer)
return web.Response(
content_type="application/json",
text=json.dumps(
{"sdp": peer_connection.localDescription.sdp, "type": peer_connection.localDescription.type}
),
)
async def on_shutdown(app):
coroutines = [peer_connection.close() for peer_connection in peer_connections]
await asyncio.gather(*coroutines)
peer_connections.clear()
logging.basicConfig(level=logging.INFO)
app = web.Application()
app.router.add_get("/", lambda request: web.Response(
content_type="text/html", text=open(os.path.join(ROOT, "index.html"), "r").read()
))
app.router.add_get("/client.js", lambda request: web.Response(
content_type="application/javascript", text=open(os.path.join(ROOT, "client.js"), "r").read()
))
app.router.add_post("/offer", offer)
app.on_shutdown.append(on_shutdown)
web.run_app(app, host='0.0.0.0', port=3000, ssl_context=None)
#!/bin/bash
# Build the image
[ $(docker image ls -q -f reference=aiortc|wc -l) == 1 ] || docker build -t aiortc .
# Create a docker network:
[ $(docker network ls -f name=net_oia -q|wc -l) == '1' ] || docker network create net_oia;
# Launch the server
docker run -ti --rm --name aiortc --network net_oia \
--device=/dev/video0:/dev/video0 \
-p 3000:3000 \
aiortc;
# Now, open your browser, press F12 to open the developer tools, select the "CONSOLE" tab and browse to http://localhost:3000
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment