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

Django流式传输视频至网页仅显示默认图片问题求助

视频帧流式显示问题排查与修复

需求与现状

需要实现网页功能:用户上传视频文件后,点击提交,视频以帧的形式在页面指定div区域显示(用于后续帧处理)。目前视频已成功存入static目录,POST请求返回200状态,但网页仅显示默认图片图标,无法渲染实际帧内容。


原代码片段

Django后端代码

if request.method == 'POST':
        try:
            video_file = request.FILES["video"]
            if video_file:
                video_filename = "temp_video.mp4"
                video_filepath = os.path.join(settings.STATIC_ROOT, video_filename)
                with open(video_filepath, 'wb') as destination:
                    for chunk in video_file.chunks():
                        destination.write(chunk)
                video_cap = cv2.VideoCapture(video_filepath)
                if not video_cap.isOpened():
                    print("Error: Could not open video file")
                    return "Error in processing video."
                while True:
                    ret, frame = video_cap.read()
                    if not ret:
                        break
                    else:
                        #cv2.imshow("output",frame) tried this too but not working.
                        frame_bytes = frame.tobytes()
                        yield (b'--frame\r\n'
                            b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n\r\n')
            else:
                print("no video file")
        except Exception as e:
            print("An error occurred:", e)
            return "An error occurred while processing the video."
def process_video(request):
    return StreamingHttpResponse(gen(request),content_type='multipart/x-mixed-replace; boundary=frame')

前端JavaScript代码

<script src="https://code.jquery.com/jquery-3.5.1.js"
        integrity="sha256-QWo7LDvxbWT2tbbQ97B53yJnYU3WhH/C8ycbRAkjPDc=" crossorigin="anonymous"></script>
    <script>
        document.getElementById('uploadForm').addEventListener('submit', function (event) {
            event.preventDefault(); // Prevent default form submission behavior
            const formData = new FormData(this);
            fetch(this.action, {
                method: this.method,
                body: formData})
                .then(response => {
                    // Handle response
                    // Define a function to handle each frame received
                    function handleFrame(frameUrl) {
                        // Create a new image element
                        var img = $('<img>')
                            .attr('src', frameUrl)
                            .attr('height', '100px')
                            .attr('width', '100px');
                        $('#video-display').empty().append(img);}
                    // Create a function to process the streaming response
                    function processResponse(response) {
                        var contentType = response.headers.get('content-type');                        
                        if (contentType && contentType.indexOf('multipart/x-mixed-replace') !== -1) {const reader = response.body.getReader();
                            async function read() {
                                while (true) {
                                    // Read the next chunk
                                    const { done, value } = await reader.read();
                                    // If the chunk is done, break the loop
                                    if (done) {
                                        break;}
                                    // Convert the chunk to a blob and create a URL for it
                                    const blob = new Blob([value], { type: 'image/jpeg' });                                    const imageUrl = URL.createObjectURL(blob);
                                    // Handle the frame
                                    handleFrame(imageUrl);}}
                            // Start reading chunks
                            read();}}
                    // Process the streaming response
                    processResponse(response);})
                .catch(error => {
                    console.error('Error:', error);});});
    </script>

HTML代码

{% load static %}
<!DOCTYPE html>
<form class="home-form" id="uploadForm" action="{% url 'process_video' %}" method="post"
                        enctype="multipart/form-data">
                        {% csrf_token %}
                        <input type="file" accept="video" id="video-file" name="video"
                            placeholder="Enter your video here" class="home-input input" />
                        <div class="home-container08">
                            <button type="reset" class="home-button button">
                                <span>
                                    <span>Reset</span>
                                    <br />
                                </span>
                            </button>
                            <button type="submit" class="home-button1 button">Submit</button>
                        </div>
                    </form> <div id="video-display" class="home-container16">
                </div>

URL配置

应用urls.py

from django.urls import path
from . import views
urlpatterns= [
    path("",views.home,name="home"),
     path('process-video/', views.process_video, name='process_video'),]

项目urls.py

from django.contrib import admin
from django.urls import path,include
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
urlpatterns = [
    path('admin/', admin.site.urls),
    path("", include("myapp.urls"))
]
# Add staticfiles_urlpatterns to urlpatterns
urlpatterns += staticfiles_urlpatterns()

问题核心原因

  1. 后端帧编码错误:直接输出frame.tobytes()只是OpenCV帧的原始像素数据(无JPEG文件头和编码结构),不是标准JPEG格式,前端无法解析为图片。
  2. 前端流式响应解析错误:multipart/x-mixed-replace格式由带边界的多块数据组成,当前代码直接将每个chunk转为Blob,未分割边界和提取有效JPEG数据,导致Blob无效。
  3. 后端响应格式不严谨:缺少Content-Length头,且未处理帧编码失败的异常,可能导致响应结构损坏。

修复方案

1. 修正Django后端代码

将OpenCV帧编码为标准JPEG格式,完善流式响应结构:

import cv2
import os
from django.http import StreamingHttpResponse
from django.conf import settings

def gen(request):
    if request.method == 'POST':
        try:
            video_file = request.FILES["video"]
            if video_file:
                video_filename = "temp_video.mp4"
                video_filepath = os.path.join(settings.STATIC_ROOT, video_filename)
                with open(video_filepath, 'wb') as destination:
                    for chunk in video_file.chunks():
                        destination.write(chunk)
                video_cap = cv2.VideoCapture(video_filepath)
                if not video_cap.isOpened():
                    print("Error: Could not open video file")
                    yield b'--frame\r\nContent-Type: text/plain\r\n\r\nError opening video\r\n\r\n'
                    return
                while True:
                    ret, frame = video_cap.read()
                    if not ret:
                        break
                    # 将BGR格式帧编码为JPEG字节流
                    ret_jpeg, jpeg_frame = cv2.imencode('.jpg', frame)
                    if not ret_jpeg:
                        print("Error encoding frame to JPEG")
                        continue
                    # 封装为标准multipart块
                    yield (b'--frame\r\n'
                           b'Content-Type: image/jpeg\r\n'
                           b'Content-Length: ' + str(len(jpeg_frame)).encode() + b'\r\n\r\n'
                           + jpeg_frame.tobytes() + b'\r\n\r\n')
                video_cap.release()
                os.remove(video_filepath)  # 清理临时文件
            else:
                yield b'--frame\r\nContent-Type: text/plain\r\n\r\nNo video file uploaded\r\n\r\n'
        except Exception as e:
            print("An error occurred:", e)
            yield b'--frame\r\nContent-Type: text/plain\r\n\r\nError processing video: ' + str(e).encode() + b'\r\n\r\n'

def process_video(request):
    return StreamingHttpResponse(gen(request), content_type='multipart/x-mixed-replace; boundary=frame')

2. 修正前端JavaScript代码

正确解析multipart响应,提取有效JPEG数据:

<script src="https://code.jquery.com/jquery-3.5.1.js"
        integrity="sha256-QWo7LDvxbWT2tbbQ97B53yJnYU3WhH/C8ycbRAkjPDc=" crossorigin="anonymous"></script>
<script>
    document.getElementById('uploadForm').addEventListener('submit', function (event) {
        event.preventDefault();
        const formData = new FormData(this);
        const videoDisplay = $('#video-display');
        videoDisplay.empty(); // 清空历史帧
        
        fetch(this.action, {
            method: this.method,
            body: formData
        }).then(response => {
            const contentType = response.headers.get('content-type');
            if (!contentType || !contentType.includes('multipart/x-mixed-replace')) {
                console.error('Invalid response content type');
                return;
            }
            
            // 提取multipart边界字符串
            const boundary = contentType.split('boundary=')[1];
            const reader = response.body.getReader();
            const decoder = new TextDecoder('utf-8');
            let buffer = new Uint8Array();
            
            async function read() {
                while (true) {
                    const { done, value } = await reader.read();
                    if (done) break;
                    
                    // 拼接新数据到缓冲区
                    const newBuffer = new Uint8Array(buffer.length + value.length);
                    newBuffer.set(buffer);
                    newBuffer.set(value, buffer.length);
                    buffer = newBuffer;
                    
                    // 查找边界位置
                    const boundaryBytes = new TextEncoder().encode(`--${boundary}`);
                    let boundaryIndex = -1;
                    for (let i = 0; i <= buffer.length - boundaryBytes.length; i++) {
                        let match = true;
                        for (let j = 0; j < boundaryBytes.length; j++) {
                            if (buffer[i + j] !== boundaryBytes[j]) {
                                match = false;
                                break;
                            }
                        }
                        if (match) {
                            boundaryIndex = i;
                            break;
                        }
                    }
                    if (boundaryIndex === -1) continue;
                    
                    // 提取当前块并更新缓冲区
                    const chunk = buffer.slice(0, boundaryIndex);
                    buffer = buffer.slice(boundaryIndex + boundaryBytes.length);
                    
                    // 解析块内的HTTP头与内容
                    const chunkStr = decoder.decode(chunk);
                    const headerEndIndex = chunkStr.indexOf('\r\n\r\n');
                    if (headerEndIndex === -1) continue;
                    
                    const headers = chunkStr.slice(0, headerEndIndex);
                    const contentBytes = chunk.slice(headerEndIndex + 4); // 跳过\r\n\r\n分隔符
                    
                    // 渲染JPEG帧
                    if (headers.includes('Content-Type: image/jpeg')) {
                        const blob = new Blob([contentBytes], { type: 'image/jpeg' });
                        const imageUrl = URL.createObjectURL(blob);
                        
                        const img = $('<img>')
                            .attr('src', imageUrl)
                            .attr('height', '100px')
                            .attr('width', '100px')
                            .on('load', () => {
                                URL.revokeObjectURL(imageUrl); // 释放内存
                            });
                        videoDisplay.append(img); // 保留所有帧,替换当前帧则改为.empty().append(img)
                    }
                }
            }
            read();
        }).catch(error => {
            console.error('Error:', error);
        });
    });
</script>

3. 额外注意事项

  • 确保Django环境已安装OpenCV:pip install opencv-python
  • 检查settings.STATIC_ROOT配置是否正确,且Django进程对该目录有写入权限
  • 使用Chrome/Firefox等支持流式响应的浏览器测试

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 23:45:54