如何对加密视频应用HTTP范围请求?
Your idea of inserting a stream after the decryption step to handle range-based requests is exactly the right approach—but a basic passthrough stream won’t work because we need to dynamically slice the decrypted MP4 data to match the requested start and end offsets (which correspond to the decrypted file, not the encrypted one). Here’s how to make this work:
First, Understand the Core Issue
When you use fs.createReadStream with start/end on the encrypted file, those offsets map to the encrypted bytes, which don’t align with the decrypted MP4’s byte structure (unless you’re using a block cipher with perfectly aligned blocks and no padding, which is rare). We need to decrypt the entire stream, but only pass through the portion of the decrypted data that matches the requested range.
Step 1: Get the Decrypted Video’s Total Size
To properly respond to range requests, you need to know the total size of the decrypted MP4 file. You can:
- Decrypt the file once upfront and store the size in a config/database, or
- Calculate it dynamically if your encryption scheme allows (e.g., subtracting IV/padding sizes from the encrypted file size, but this depends on your cipher setup).
For this example, let’s assume we’ve pre-stored the decrypted size as DECRYPTED_VIDEO_SIZE.
Step 2: Create a Custom Range-Transform Stream
We’ll build a Transform stream that intercepts the decrypted data and only passes through bytes from the requested start to end offset:
const { Transform } = require('stream'); class RangeTransform extends Transform { constructor({ start, end }) { super(); this.startByte = start; this.endByte = end; this.currentByte = 0; this.bytesRemaining = end - start + 1; } _transform(chunk, encoding, callback) { const chunkStart = this.currentByte; const chunkEnd = this.currentByte + chunk.length - 1; // Skip chunks that are entirely before the requested range if (chunkEnd < this.startByte) { this.currentByte += chunk.length; return callback(); } // Pass through chunks that are entirely within the range if (chunkStart >= this.startByte && chunkEnd <= this.endByte) { this.currentByte += chunk.length; this.bytesRemaining -= chunk.length; return callback(null, chunk); } // Slice partial chunks that overlap the range const sliceStart = Math.max(0, this.startByte - chunkStart); const sliceEnd = Math.min(chunk.length - 1, this.endByte - chunkStart); const filteredChunk = chunk.slice(sliceStart, sliceEnd + 1); this.currentByte += filteredChunk.length; this.bytesRemaining -= filteredChunk.length; this.push(filteredChunk); // End the stream early if we've read all requested bytes if (this.bytesRemaining <= 0) { this.end(); } callback(); } }
Step 3: Wire Up the Pipeline with Range Handling
Now, modify your server code to parse the Range header, set the correct response headers, and pipe the streams together:
const http = require('http'); const fs = require('fs'); const crypto = require('crypto'); // Precomputed size of the decrypted MP4 const DECRYPTED_VIDEO_SIZE = 123456789; // Replace with your actual size const ALGORITHM = 'aes-256-cbc'; const SECRET_KEY = 'your-secure-secret-key'; // Use proper key management in production http.createServer((req, res) => { if (req.method !== 'GET' || req.url !== '/video') { res.writeHead(404); return res.end('Not Found'); } let start = 0; let end = DECRYPTED_VIDEO_SIZE - 1; // Parse the Range header if present if (req.headers.range) { const [, rangeStr] = req.headers.range.split('='); const [startStr, endStr] = rangeStr.split('-'); start = parseInt(startStr, 10); end = endStr ? parseInt(endStr, 10) : end; // Ensure we don't exceed the actual file size end = Math.min(end, DECRYPTED_VIDEO_SIZE - 1); } const contentLength = end - start + 1; // Send 206 Partial Content response headers res.writeHead(206, { 'Content-Range': `bytes ${start}-${end}/${DECRYPTED_VIDEO_SIZE}`, 'Accept-Ranges': 'bytes', 'Content-Length': contentLength, 'Content-Type': 'video/mp4', }); // Build the stream pipeline const decipher = crypto.createDecipher(ALGORITHM, SECRET_KEY); const rangeFilter = new RangeTransform({ start, end }); fs.createReadStream('./encrypted-video.dat') .pipe(decipher) .pipe(rangeFilter) .pipe(res) .on('error', (err) => { console.error('Stream error:', err); res.statusCode = 500; res.end('Internal Server Error'); }); }).listen(3000, () => { console.log('Server running on http://localhost:3000'); });
Key Notes
- Performance: This approach streams data incrementally—we never load the entire decrypted file into memory, so it works even for large videos.
- Error Handling: Always add error listeners to your streams to avoid unhandled exceptions crashing the server.
- Key Management: Never hardcode secrets in your code—use environment variables or a secure key vault in production.
- Edge Cases: The
RangeTransformhandles partial chunks, out-of-range requests, and early termination once the requested bytes are sent.
内容的提问来源于stack exchange,提问作者Tabbyofjudah

