I am building a large-scale video monitoring application that needs to record a user’s webcam and screen for up to 3 hours and upload the streams in real-time to Azure Blob Storage. The target is to handle up to 10,000 concurrent users.
My current architecture is:
Frontend: Uses the MediaRecorder API in JavaScript to capture webcam and screen streams. It sends video chunks every second over two separate WebSockets.
Backend: A FastAPI server receives the binary data from the WebSockets and appends it directly to an Azure Append Blob.
Playback: A separate FastAPI endpoint (/video/{session_id}) streams the final .webm file from Azure Blob Storage for playback.
Despite the basic functionality being in place, I’m encountering critical issues with reliability and playback.
The Problems
**Incomplete Recordings: **For a 3-hour session, the final video file in Azure is often incomplete. The duration might be much shorter, or the video just stops abruptly, indicating significant data loss.
Unseekable Video Files: When playing the saved .webm video from Azure, the video player’s seek bar does not work. The video plays from the beginning, but you cannot skip to different timestamps.
Inconsistent File Sizes: For recordings of the same duration, the final file sizes vary dramatically between sessions, which I suspect is another symptom of the data loss problem.
Frontend (JavaScript)
let websocketWebcam = null;
let websocketScreen = null;
let webcamRecorder = null;
let screenRecorder = null;
let isRecording = false;
// Connects to a WebSocket endpoint
function connectWebSocket(streamType, sessionId) {
// Replaced 'your-backend-domain.com' with your actual domain
const wsUrl = `wss://your-backend-domain.com/upload_video/${sessionId}_${streamType}`;
const ws = new WebSocket(wsUrl);
ws.onopen = () => console.log(`WebSocket connected: ${streamType}`);
ws.onclose = () => console.log(`WebSocket closed: ${streamType}`);
ws.onerror = error => console.error(`WebSocket error (${streamType}):`, error);
return ws;
}
// Periodically reconnects WebSockets to keep the connection alive
function reconnectWebSockets(sessionId) {
console.log("Attempting to reconnect WebSockets...");
if (websocketWebcam) websocketWebcam.close();
if (websocketScreen) websocketScreen.close();
websocketWebcam = connectWebSocket("webcam", sessionId);
websocketScreen = connectWebSocket("screen", sessionId);
console.log("WebSockets reconnected.");
}
// Starts the recording process
async function startRecording(sessionId) {
try {
isRecording = true;
// Assume webcamStream and screenStream are already acquired from navigator.mediaDevices
websocketWebcam = connectWebSocket("webcam", sessionId);
websocketScreen = connectWebSocket("screen", sessionId);
// Recorder for webcam stream
webcamRecorder = new MediaRecorder(webcamStream, { mimeType: "video/webm; codecs=vp9" });
webcamRecorder.ondataavailable = event => {
if (event.data.size > 0) sendToWebSocket(event.data, websocketWebcam);
};
webcamRecorder.start(1000); // Send data every 1 second
// Recorder for screen stream
screenRecorder = new MediaRecorder(screenStream, { mimeType: "video/webm; codecs=vp9" });
screenRecorder.ondataavailable = event => {
if (event.data.size > 0) sendToWebSocket(event.data, websocketScreen);
};
screenRecorder.start(1000); // Send data every 1 second
console.log("Recording started for session:", sessionId);
// Set interval to reconnect WebSockets every 45 minutes
setInterval(() => reconnectWebSockets(sessionId), 2700000); // 45 minutes
} catch (error) {
console.error("Error starting recording:", error);
}
}
// Sends a blob of data to the WebSocket
function sendToWebSocket(blob, ws) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(blob);
} else {
console.warn("WebSocket not open. Data chunk might be lost.");
// Simple retry logic, which might not be robust enough
setTimeout(() => sendToWebSocket(blob, ws), 1000);
}
}
import asyncio
from fastapi import FastAPI, WebSocket, HTTPException, Request
from fastapi.responses import StreamingResponse, Response
from azure.storage.blob import BlobServiceClient, BlobClient, AppendBlobService
import re
# Assume blob_service_client and container_name are configured
# blob_service_client = BlobServiceClient.from_connection_string(...)
# container_name = "videos"
app = FastAPI()
@app.websocket("/upload_video/{session_id}")
async def upload_video(websocket: WebSocket, session_id: str):
""" WebSocket endpoint to receive video chunks and append to an Append Blob """
await websocket.accept()
blob_name = f"{session_id}.webm"
blob_client = blob_service_client.get_blob_client(container=container_name, blob=blob_name)
try:
# Create Append Blob if it doesn't exist
if not blob_client.exists():
blob_client.create_append_blob()
except Exception as e:
print(f"Error initializing blob: {e}")
await websocket.close()
return
try:
while True:
data = await websocket.receive_bytes()
blob_client.append_block(data)
except Exception as e:
print(f"Error during video upload for session {session_id}: {e}")
finally:
print(f"Upload completed for session {session_id}")
await websocket.close()
# Video streaming endpoint (simplified for brevity)
@app.get("/video/{session_id}")
async def get_video(session_id: str, request: Request):
""" Video Streaming API with Range Support """
blob_client = blob_service_client.get_blob_client(container=container_name, blob=f"{session_id}.webm")
try:
blob_properties = blob_client.get_blob_properties()
file_size = blob_properties.size
range_header = request.headers.get("Range")
start, end = 0, file_size - 1
status_code = 200 # Full content
if range_header:
match = re.search(r"bytes=(d+)-(d*)", range_header)
if match:
start = int(match.group(1))
end = int(match.group(2)) if match.group(2) else file_size - 1
status_code = 206 # Partial content
length = end - start + 1
headers = {
"Content-Range": f"bytes {start}-{end}/{file_size}",
"Content-Length": str(length),
"Accept-Ranges": "bytes",
"Content-Type": "video/webm",
}
def stream_video():
stream = blob_client.download_blob(offset=start, length=length)
yield from stream.chunks()
return StreamingResponse(stream_video(), headers=headers, status_code=status_code)
except Exception as e:
raise HTTPException(status_code=404, detail="Video not found or error in streaming")
My Questions
How to Create a Seekable WEBM file? My current process of appending raw MediaRecorder chunks results in a webm file without the proper metadata (like a Cues element) needed for seeking. How can I fix this? Should I be post-processing the file on the server (e.g., with FFmpeg) after the stream ends to inject the right metadata? Is there a way to generate this metadata on the client?
How to Prevent Data Loss? My strategy of reconnecting the WebSocket every 45 minutes feels wrong and is likely a major source of data loss. What is a more robust method for maintaining a long-running, stable connection for 3+ hours? Should I implement a heartbeat (ping/pong) mechanism instead?
**Is Append Blob the Right Architecture? **Is streaming to a single, large Append Blob for 3 hours a sound strategy? Or would it be more reliable to create smaller, timed video chunks (e.g., a new blob every 5 minutes) and then create a manifest file or concatenate them later?