You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何对加密视频应用HTTP范围请求?

Handling HTTP Range Requests for Decrypted Video Streams in Node.js

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 RangeTransform handles partial chunks, out-of-range requests, and early termination once the requested bytes are sent.

内容的提问来源于stack exchange,提问作者Tabbyofjudah

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 06:50:29