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

基于JS Ajax与Django的2GB大视频分片上传及合并问题求助

问题根因分析

1. 分片无法拼接为完整文件的原因

  • 前端使用readAsDataURL将分片转成了Base64编码的DataURL字符串,后端直接通过bytes(chunk_data, 'ascii')转换写入,内容已经被篡改,不是原始二进制视频数据
  • 后端每次打开文件使用wb+模式,会覆盖已有内容,只有最后一次上传的分片会被保留,自然无法拼接成完整文件
  • 缺少分片序号、总分片数的参数传递,后端无法判断分片顺序和是否全部上传完成

2. 上传耗时过长的原因

  • 前端代码中存在videoform.append("video_file", file)的逻辑,每次上传分片都会把整个2G的完整文件提交到后端,相当于2G的文件被重复上传了几十到上百次,带宽被完全浪费
  • 使用Base64编码传输分片会让数据体积膨胀33%左右,额外增加了传输量
  • 递归串行上传分片,没有利用多请求并发能力

修正后的实现代码

前端JS代码

(function($) {
      var file = {};
      // 分片大小调整为20M,可根据网络情况调整为10M-50M
      var slice_size = 20 * 1024 * 1024;
      // 并发上传的请求数,建议设为3-5
      var concurrent_num = 3;
      
      $(document).on('click', '.video_form_submit', function(event){
            event.preventDefault();
            const form_id = $(this).data("form-id")
            const course_id = $(this).data("course-id") 
            const video_title = $("#video_title_"+form_id).val()
            file = $("#video_id_"+form_id)[0].files[0]
            const csrf_token = $(this).data("csrf-token")
            $("#progress_process_"+form_id).css('display', 'block');
            $("#progress-bar1_"+form_id).css("width", "0%");
            $("#progress-text1_"+form_id).html("0 %");
            
            // 计算总分片数
            const total_chunks = Math.ceil(file.size / slice_size);
            start_upload(file, csrf_token, course_id, video_title, form_id, total_chunks);
      });

      async function start_upload(file, csrf_token, course_id, video_title, form_id, total_chunks) {
            let uploaded_count = 0;
            const queue = [];
            for (let chunk_index = 0; chunk_index < total_chunks; chunk_index++) {
                  const start = chunk_index * slice_size;
                  const end = Math.min(start + slice_size, file.size);
                  const blob = file.slice(start, end);
                  
                  // 控制并发数
                  if (queue.length >= concurrent_num) {
                        await Promise.race(queue);
                  }
                  
                  const upload_promise = upload_chunk(blob, chunk_index, total_chunks, csrf_token, course_id, video_title, form_id);
                  upload_promise.then(() => {
                        uploaded_count++;
                        const percent_done = Math.floor((uploaded_count / total_chunks) * 100);
                        $("#progress-text1_"+form_id).text(percent_done + '%');
                        $("#progress-bar1_"+form_id).css('width', percent_done + '%');
                        // 移除已完成的请求
                        const idx = queue.indexOf(upload_promise);
                        if (idx > -1) queue.splice(idx, 1);
                  });
                  queue.push(upload_promise);
            }
            // 等待所有分片上传完成
            await Promise.all(queue);
            $("#dbi-upload-progress_"+form_id).html('上传完成!');
      }

      function upload_chunk(blob, chunk_index, total_chunks, csrf_token, course_id, video_title, form_id) {
            return new Promise((resolve, reject) => {
                  const videoform = new FormData();
                  // 只传当前分片,不要传完整文件!
                  videoform.append("chunk_file", blob);
                  videoform.append("course_id", course_id);
                  videoform.append("video_title", video_title);
                  videoform.append("video_file_name", file.name);
                  videoform.append("csrfmiddlewaretoken", csrf_token);
                  videoform.append("chunk_index", chunk_index);
                  videoform.append("total_chunks", total_chunks);
                  
                  $.ajax({
                        url: "/video_upload/",
                        type: 'POST',
                        data: videoform,
                        cache: false,
                        contentType: false,
                        processData: false,
                        error: function(jqXHR, textStatus, errorThrown) {
                              console.log(jqXHR, textStatus, errorThrown);
                              // 上传失败重试,可根据需求调整重试次数
                              upload_chunk(blob, chunk_index, total_chunks, csrf_token, course_id, video_title, form_id).then(resolve);
                        },
                        success: function(data) {
                              resolve(data);
                        }
                  });
            });
      }
})(jQuery);

后端views.py代码

import os
import time
from django.conf import settings
from django.http import JsonResponse
from .forms import VideosForm

def video_upload(request):
    video_upload_response = {"status": "success"}
    if request.method == 'POST':
        form = VideosForm(request.POST, request.FILES) 
        if form.is_valid():
            course_id = request.POST.get("course_id")
            video_title = request.POST.get("video_title")
            video_file_name = request.POST.get("video_file_name") 
            chunk_index = int(request.POST.get("chunk_index"))
            total_chunks = int(request.POST.get("total_chunks"))
            chunk_file = request.FILES['chunk_file']
            
            # 临时文件存储路径,按文件名区分,避免重名冲突,生产环境建议用文件MD5作为唯一标识
            temp_dir = os.path.join(settings.MEDIA_ROOT, "temp_chunks", video_file_name.rsplit('.',1)[0])
            os.makedirs(temp_dir, exist_ok=True)
            temp_chunk_path = os.path.join(temp_dir, f"{chunk_index}.part")
            
            # 写入当前分片
            with open(temp_chunk_path, 'wb+') as f:
                for chunk in chunk_file.chunks():
                    f.write(chunk)
            
            # 判断是否所有分片都上传完成
            uploaded_chunks = len(os.listdir(temp_dir))
            if uploaded_chunks == total_chunks:
                # 拼接所有分片
                final_file_path = os.path.join(settings.MEDIA_ROOT, "videos", video_file_name)
                os.makedirs(os.path.dirname(final_file_path), exist_ok=True)
                with open(final_file_path, 'wb+') as final_f:
                    for i in range(total_chunks):
                        part_path = os.path.join(temp_dir, f"{i}.part")
                        with open(part_path, 'rb') as part_f:
                            final_f.write(part_f.read())
                        # 拼接完成后删除分片
                        os.remove(part_path)
                # 删除临时目录
                os.rmdir(temp_dir)
                # 这里可以添加保存视频信息到数据库的逻辑
                video_upload_response["final_path"] = final_file_path

    return JsonResponse(video_upload_response, safe=False)

额外优化建议
  • 生产环境建议前端先计算文件的MD5值作为唯一标识,避免不同用户上传同名文件导致的冲突,同时可以实现秒传功能
  • 可以添加断点续传逻辑,上传前先向后端查询已上传的分片,跳过已经上传完成的分片
  • 大文件上传建议配置Nginx等反向代理的客户端最大上传大小限制,避免请求被拦截

内容的提问来源于stack exchange,提问作者Subhra Kanti Ray

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 20:24:03