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

Django Rest上传视频时实时转码为H264格式实现方案咨询

实现实时流式转码上传的方案

1. 绕过Django Rest默认的文件接收机制

Django Rest默认的MultipartParser会把上传文件完整写入临时文件后再交给视图处理,这不符合实时转码需求。需要自定义流式解析器,直接从请求数据流中读取视频片段:

from rest_framework.parsers import MultipartParser
from rest_framework.exceptions import ParseError
import io
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.conf import settings

class StreamingMultipartParser(MultipartParser):
    def parse(self, stream, media_type=None, parser_context=None):
        parser_context = parser_context or {}
        request = parser_context.get('request')
        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
        
        boundary = self.get_boundary(media_type, request)
        if not boundary:
            raise ParseError("Missing boundary in multipart request")
        
        # 自定义上传处理器,仅流式读取数据不生成临时文件
        class StreamingUploadHandler:
            def __init__(self):
                self.stream = io.BytesIO()
                self.file_name = None
                self.content_type = None
            
            def receive_data_chunk(self, raw_data, start):
                self.stream.write(raw_data)
            
            def file_complete(self, file_size):
                self.stream.seek(0)
                return self
        
        request.upload_handlers = [StreamingUploadHandler()]
        try:
            parser = DjangoMultiPartParser(request.META, stream, boundary, encoding)
            data, files = parser.parse()
        except Exception as e:
            raise ParseError(f"Failed to parse multipart request: {str(e)}")
        
        video_file = next(iter(files.values()), None)
        if not video_file:
            raise ParseError("No video file found in request")
        
        return {'data': data, 'video_stream': video_file.stream, 'file_name': video_file.name}

2. 用FFmpeg管道实现实时转码

在视图中启动FFmpeg子进程,将流式读取的视频数据直接传入FFmpeg标准输入,同时将转码后的输出写入目标文件:

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
import subprocess
import os
from django.conf import settings

class VideoUploadView(APIView):
    parser_classes = [StreamingMultipartParser]
    
    def post(self, request, *args, **kwargs):
        video_stream = request.data.get('video_stream')
        original_name = request.data.get('file_name')
        if not video_stream:
            return Response({"error": "No video stream provided"}, status=status.HTTP_400_BAD_REQUEST)
        
        # 生成H264编码的输出文件名
        output_filename = f"{os.path.splitext(original_name)[0]}_h264.mp4"
        output_path = os.path.join(settings.MEDIA_ROOT, output_filename)
        
        # FFmpeg核心命令:从标准输入读取,转码为H264格式MP4
        ffmpeg_cmd = [
            'ffmpeg',
            '-i', 'pipe:0',          # 从标准输入读取原始视频流
            '-c:v', 'libx264',       # 指定视频编码器为H264
            '-crf', '23',            # 质量控制参数(值越小质量越高,18-28为常用范围)
            '-preset', 'medium',     # 编码速度与质量平衡(fast/medium/slow)
            '-f', 'mp4',             # 输出格式为MP4
            '-movflags', '+faststart', # 优化MP4的Web播放兼容性
            '-y',                    # 覆盖已存在的输出文件
            output_path
        ]
        
        try:
            # 启动FFmpeg子进程,绑定标准输入/错误流
            process = subprocess.Popen(
                ffmpeg_cmd,
                stdin=subprocess.PIPE,
                stderr=subprocess.PIPE,
                stdout=subprocess.DEVNULL
            )
            
            # 分块读取上传流并写入FFmpeg进程
            chunk_size = 1024 * 1024  # 按1MB块读取,可根据服务器性能调整
            while True:
                chunk = video_stream.read(chunk_size)
                if not chunk:
                    break
                process.stdin.write(chunk)
                process.stdin.flush()
            
            # 关闭输入流,等待转码完成
            process.stdin.close()
            process.wait()
            
            # 检查转码是否成功
            if process.returncode != 0:
                stderr = process.stderr.read().decode('utf-8')
                if os.path.exists(output_path):
                    os.remove(output_path)
                return Response({"error": f"Transcoding failed: {stderr}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
            
            return Response({
                "status": "success",
                "output_file": output_filename,
                "file_path": output_path
            }, status=status.HTTP_201_CREATED)
        
        except Exception as e:
            # 异常时清理进程与未完成文件
            if 'process' in locals() and process.poll() is None:
                process.kill()
            if os.path.exists(output_path):
                os.remove(output_path)
            return Response({"error": f"Upload failed: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

3. 关键注意事项

  • 资源占用优化:转码是CPU密集型操作,高并发场景下建议将转码逻辑放到Celery异步队列中,避免阻塞Web服务器。
  • 错误处理:需处理上传中断、FFmpeg崩溃等异常,确保及时清理进程和未完成的文件。
  • 参数调整:根据业务需求调整-crf(质量)、-preset(速度)参数,若需要保留音频,添加-c:a aac参数指定音频编码器。
  • 格式校验:可在读取初始流片段后,用FFmpeg的-probesize参数检测输入格式,避免无效文件进入转码流程。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 12:06:11