jasonr
Fri Jun 27 2025 03:59:55 PM PDT
Streaming MJPEG from Raspberry Pi to Windows via UDP
This guide explains how to stream MJPEG video from a Raspberry Pi using `libcamera-jpeg` to a Windows machine using UDP sockets, and display it in real-time using FastAPI and HTML.
- Raspberry Pi with camera module and libcamera installed
- Windows machine with Python 3.12 and FastAPI installed
- Local network connection between the two devices
On the Raspberry Pi, create and run this script:
#!/usr/bin/env python3
import socket
import time
UDP_IP = "192.168.0.x" # Windows machine IP
UDP_PORT = 5005
CHUNK_SIZE = 4096
PIPE_PATH = "/tmp/stream.mjpeg"
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
def find_frame_end(buffer):
try:
return buffer.index(b'\xff\xd9') + 2
except ValueError:
return None
buffer = bytearray()
with open(PIPE_PATH, 'rb') as f:
while True:
chunk = f.read(CHUNK_SIZE)
if not chunk:
continue
buffer.extend(chunk)
eof = find_frame_end(buffer)
if eof:
frame = buffer[:eof]
buffer = buffer[eof:]
for i in range(0, len(frame), CHUNK_SIZE):
sock.sendto(frame[i:i+CHUNK_SIZE], (UDP_IP, UDP_PORT))
time.sleep(0.01)
Run this command to start the MJPEG stream:
libcamera-jpeg -t 0 --width 320 --height 240 --quality 50 -o /tmp/stream.mjpeg
Save and run this FastAPI script on your Windows machine:
import socket
import asyncio
import threading
import base64
import queue
import numpy as np
import cv2
from fastapi import FastAPI, WebSocket
from fastapi.responses import HTMLResponse
import uvicorn
UDP_IP = "0.0.0.0"
UDP_PORT = 5005
BUFFER_SIZE = 65536
frame_queue = queue.Queue(maxsize=10)
def udp_receiver():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))
buffer = bytearray()
def find_jpeg_end(buf):
try:
return buf.index(b'\xff\xd9') + 2
except ValueError:
return None
while True:
data, _ = sock.recvfrom(BUFFER_SIZE)
buffer.extend(data)
while True:
eof = find_jpeg_end(buffer)
if eof is None:
break
jpeg = buffer[:eof]
buffer = buffer[eof:]
frame = cv2.imdecode(np.frombuffer(jpeg, dtype=np.uint8), 1)
if frame is not None:
ret, encoded = cv2.imencode('.jpg', frame)
if ret:
if frame_queue.qsize() > 5:
frame_queue.get()
frame_queue.put(encoded.tobytes())
app = FastAPI()
@app.get("/")
async def get_page():
return HTMLResponse(""" <!DOCTYPE html>
<html>
<head><title>UDP Stream Viewer</title></head>
<body style="background:black;text-align:center;">
<h2 style="color:white;">Live Stream</h2>
<img id="stream" width="640"/>
<p style="color:lime;">Bytes: <span id="info">0</span></p>
<script>
const img = document.getElementById('stream');
const info = document.getElementById('info');
const ws = new WebSocket(`ws://${location.host}/ws`);
ws.onmessage = event => {
img.src = 'data:image/jpeg;base64,' + event.data;
info.textContent = event.data.length;
};
</script>
</body>
</html>
""")
@app.websocket("/ws")
async def ws(websocket: WebSocket):
await websocket.accept()
while True:
if not frame_queue.empty():
frame = frame_queue.get()
b64 = base64.b64encode(frame).decode()
await websocket.send_text(b64)
await asyncio.sleep(0.01)
if __name__ == "__main__":
threading.Thread(target=udp_receiver, daemon=True).start()
uvicorn.run(app, host="0.0.0.0", port=8080)
Once everything is set up and running, you can access the live stream by opening:
http://192.168.0.x:8080