实时屏幕录制并存储至S3等云存储的实现方案咨询
Great question—real-time screen recording with incremental cloud storage uploads solves the big-file bottleneck perfectly, and both Node.js and Go have robust tooling to make this work. Let’s walk through practical implementations, covering media streaming, chunk handling, and S3 multipart uploads.
Core Architecture Overview
Here’s the high-level flow to avoid full-file uploads:
- Frontend: Use the browser’s
MediaRecorderAPI to capture screen/audio, generate small media chunks (1-5 seconds each), and send them to your backend in real time. - Backend: Receive chunks, use S3’s Multipart Upload API to push each chunk as a "part" to cloud storage, then finalize the upload once recording stops.
- Optional: Use WebRTC instead of WebSockets for ultra-low-latency streaming, though WebSockets are simpler for most use cases.
Node.js Implementation
1. Frontend: Capture & Stream Screen Chunks
Use MediaRecorder to split the screen stream into chunks, then send them via WebSocket to your backend:
async function startRecording() { // Request screen + audio access const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: { echoCancellation: true } }); // Configure recorder for WebM (widely supported) const recorder = new MediaRecorder(stream, { mimeType: 'video/webm;codecs=vp9', bitsPerSecond: 2_000_000 // Adjust based on quality needs }); // Connect to backend WebSocket const socket = new WebSocket('ws://your-backend-domain/ws'); // Send each chunk as it's generated recorder.ondataavailable = (e) => { if (e.data.size > 0) { socket.send(e.data); } }; // Generate a chunk every 3 seconds recorder.start(3000); }
2. Backend: Handle WebSocket & S3 Multipart Upload
Use the AWS SDK v3 and ws library to manage WebSocket connections and incremental S3 uploads:
const WebSocket = require('ws'); const { S3Client, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand } = require('@aws-sdk/client-s3'); const s3Client = new S3Client({ region: 'your-aws-region' }); const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', async (ws) => { console.log('New recording client connected'); let uploadId; const bucket = 'your-s3-bucket-name'; const recordingKey = `screen-recordings/${Date.now()}.webm`; // Initialize S3 multipart upload try { const createUpload = new CreateMultipartUploadCommand({ Bucket: bucket, Key: recordingKey, ContentType: 'video/webm' }); const uploadInit = await s3Client.send(createUpload); uploadId = uploadInit.UploadId; } catch (err) { console.error('Failed to start S3 upload:', err); ws.close(); return; } const uploadedParts = []; let partNumber = 1; // Process incoming chunks ws.on('message', async (chunk) => { try { // Upload chunk as an S3 multipart part const uploadPart = new UploadPartCommand({ Bucket: bucket, Key: recordingKey, UploadId: uploadId, PartNumber: partNumber, Body: chunk }); const partResult = await s3Client.send(uploadPart); uploadedParts.push({ PartNumber: partNumber, ETag: partResult.ETag }); partNumber++; } catch (err) { console.error('Failed to upload chunk:', err); ws.send(JSON.stringify({ error: 'Chunk upload failed' })); } }); // Clean up on client disconnect ws.on('close', async () => { console.log('Recording client disconnected'); if (!uploadId) return; if (uploadedParts.length > 0) { // Finalize the S3 upload const completeUpload = new CompleteMultipartUploadCommand({ Bucket: bucket, Key: recordingKey, UploadId: uploadId, MultipartUpload: { Parts: uploadedParts } }); await s3Client.send(completeUpload); console.log(`Recording saved to S3: ${recordingKey}`); } else { // Abort empty upload to avoid S3 clutter const abortUpload = new AbortMultipartUploadCommand({ Bucket: bucket, Key: recordingKey, UploadId: uploadId }); await s3Client.send(abortUpload); } }); });
Go Implementation
1. Frontend: Same as Node.js
Reuse the frontend code above—stream chunks via WebSocket to your Go backend.
2. Backend: WebSocket & S3 Multipart Handling
Use gorilla/websocket for WebSocket management and AWS SDK v2 for S3 operations:
package main import ( "bytes" "context" "fmt" "log" "net/http" "time" "github.com/gorilla/websocket" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" ) var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true // Add origin validation for production }, } func main() { http.HandleFunc("/ws", handleRecordingConnection) log.Fatal(http.ListenAndServe(":8080", nil)) } func handleRecordingConnection(w http.ResponseWriter, r *http.Request) { conn, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Println("Failed to upgrade connection:", err) return } defer conn.Close() ctx := context.Background() cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("your-aws-region")) if err != nil { log.Println("Failed to load AWS config:", err) return } s3Client := s3.NewFromConfig(cfg) bucket := "your-s3-bucket-name" recordingKey := fmt.Sprintf("screen-recordings/%d.webm", time.Now().Unix()) // Initialize S3 multipart upload createResp, err := s3Client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{ Bucket: aws.String(bucket), Key: aws.String(recordingKey), ContentType: aws.String("video/webm"), }) if err != nil { log.Println("Failed to start S3 upload:", err) return } uploadID := createResp.UploadId // Cleanup on connection close defer func() { parts, err := s3Client.ListParts(ctx, &s3.ListPartsInput{ Bucket: aws.String(bucket), Key: aws.String(recordingKey), UploadId: uploadID, }) if err != nil { log.Println("Failed to list uploaded parts:", err) return } if len(parts.Parts) > 0 { // Finalize upload _, err := s3Client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{ Bucket: aws.String(bucket), Key: aws.String(recordingKey), UploadId: uploadID, MultipartUpload: &types.CompletedMultipartUpload{ Parts: convertToCompletedParts(parts.Parts), }, }) if err != nil { log.Println("Failed to complete upload:", err) return } log.Println("Recording saved to S3:", recordingKey) } else { // Abort empty upload _, err := s3Client.AbortMultipartUpload(ctx, &s3.AbortMultipartUploadInput{ Bucket: aws.String(bucket), Key: aws.String(recordingKey), UploadId: uploadID, }) if err != nil { log.Println("Failed to abort empty upload:", err) } } }() partNumber := int32(1) for { _, chunk, err := conn.ReadMessage() if err != nil { log.Println("Failed to read chunk:", err) break } // Upload chunk to S3 _, err = s3Client.UploadPart(ctx, &s3.UploadPartInput{ Bucket: aws.String(bucket), Key: aws.String(recordingKey), UploadId: uploadID, PartNumber: partNumber, Body: bytes.NewReader(chunk), }) if err != nil { log.Println("Failed to upload part:", err) conn.WriteMessage(websocket.TextMessage, []byte("Chunk upload failed")) continue } partNumber++ } } func convertToCompletedParts(parts []types.Part) []types.CompletedPart { var completedParts []types.CompletedPart for _, p := range parts { completedParts = append(completedParts, types.CompletedPart{ ETag: p.ETag, PartNumber: p.PartNumber, }) } return completedParts }
Key Notes for Production
- Chunk Size: S3 requires multipart parts to be at least 5MB (except the final part). Adjust your
MediaRecorder.start()interval or add backend buffering to meet this requirement. - Error Handling: Add retry logic for failed chunk uploads, and track uploaded parts to resume interrupted recordings.
- WebRTC Option: For ultra-low latency, use libraries like
pion/webrtc(Go) ormediasoup(Node.js) to receive WebRTC streams directly, then transcode chunks for S3 upload. - Cost Optimization: Set lifecycle rules in S3 to auto-delete incomplete multipart uploads after a set period.
内容的提问来源于stack exchange,提问作者Deepak Poojari

