视频切片上传

js 复制代码
<ElUpload
          :show-file-list="false"
          :auto-upload="true"
          :multiple="true"
          accept="video/mp4,video/mpeg,video/avi,video/flv,video/wmv,video/mov"
          :http-request="handleVideoUpload">
js 复制代码
// 切片上传相关配置
const chunkSize = ref(2 * 1024 * 1024) // 切片大小 2MB
const uploadProgress = ref<Record<string, number>>({}) // 上传进度
const uploadAbort = ref<Record<string, AbortController>>({}) // 取消控制器

// 生成文件唯一标识
const generateFileId = (file: File): string => {
  return `${file.name}-${file.size}-${file.lastModified}`
}

// 计算文件哈希(简化版,使用文件名+大小+时间戳)
const getFileHash = (file: File): string => {
  return `${file.name}-${file.size}-${file.lastModified}`
}

// 切片上传主函数
/**
 * 视频切片上传主函数
 * @param file - 要上传的视频文件对象
 */
const uploadVideoChunk = async (file: File) => {
  // 文件唯一标识(用于追踪上传进度)
  const fileId = generateFileId(file)
  // 文件哈希值(用于断点续传,判断哪些切片已上传)
  const fileHash = getFileHash(file)
  // 总切片数 = 向上取整(文件大小 / 切片大小)
  const totalChunks = Math.ceil(file.size / chunkSize.value)
  // 存储所有切片上传的Promise,用于并行上传
  const chunkPromises: Promise<void>[] = []

  // 初始化上传进度为0%
  uploadProgress.value[fileId] = 0

  // 获取视频时长(秒)
  const duration = (await getVideoLengthAcsyn(file)) as number
  // 格式化时长为 HH:MM:SS 格式
  const durationStr = formatDuration(duration)

  // 预检查阶段:询问后端哪些切片已上传(用于断点续传)
  try {
    // 调用预检查接口
    const checkRes = await fetch(`${PATH_URL}/admin/file/chunk/check`, {
      method: 'POST',
      headers: {
        Authorization: 'Bearer ' + yourToken, // 身份认证token
        'Content-Type': 'application/json' // 请求体格式
      },
      body: JSON.stringify({
        fileHash, // 文件哈希(用于标识文件)
        fileName: file.name, // 原始文件名
        totalChunks, // 总切片数
        group_id: fileGroup.value.id, // 文件分组ID
        module: 'video', // 文件类型标记
        length: duration, // 视频时长(秒)
        duration: durationStr // 视频时长(时分秒格式)
      })
    })
    // 解析预检查响应
    const checkData = await checkRes.json()
    // 已上传的切片索引数组(后端返回),默认为空数组
    const uploadedChunks = checkData.data?.uploadedChunks || []

    // 遍历所有切片,只上传未完成的切片
    for (let i = 0; i < totalChunks; i++) {
      // 如果该切片已上传,则跳过
      if (uploadedChunks.includes(i)) continue

      // 计算当前切片的起始位置
      const start = i * chunkSize.value
      // 计算当前切片的结束位置(最后一个切片可能小于chunkSize)
      const end = Math.min(start + chunkSize.value, file.size)
      // 从文件中切出当前切片
      const chunk = file.slice(start, end)

      // 添加到并行上传队列
      chunkPromises.push(uploadChunk(chunk, i, totalChunks, fileHash, file.name, fileId))
    }

    // 等待所有切片上传完成
    await Promise.all(chunkPromises)

    // 切片上传完成后,调用合并接口
    await mergeChunks(fileHash, file.name, totalChunks, duration, durationStr)

    // 清理上传进度记录
    delete uploadProgress.value[fileId]
    // 提示上传成功
    ElMessage.success(`${file.name} 上传成功`)
    // 刷新文件列表
    fileMangeRightListData(fileGroup.value.id)
  } catch (error) {
    // 上传失败时清理进度记录
    delete uploadProgress.value[fileId]
    // 提示上传失败
    ElMessage.error(`${file.name} 上传失败: ${(error as Error).message}`)
  }
}

// 上传单个切片
const uploadChunk = async (chunk: Blob, chunkIndex: number, totalChunks: number, fileHash: string, fileName: string, fileId: string) => {
  const formData = new FormData()
  formData.append('chunk', chunk)
  formData.append('chunkIndex', chunkIndex.toString())
  formData.append('totalChunks', totalChunks.toString())
  formData.append('fileHash', fileHash)
  formData.append('fileName', fileName)

  const controller = new AbortController()
  uploadAbort.value[fileId] = controller

  const res = await fetch(`${PATH_URL}/admin/file/chunk/upload`, {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + yourToken
    },
    body: formData,
    signal: controller.signal
  })

  const result = await res.json()
  if (result.code !== 0) {
    throw new Error(result.msg || '切片上传失败')
  }

  // 更新进度
  uploadProgress.value[fileId] = ((chunkIndex + 1) / totalChunks) * 100
}

// 合并切片
const mergeChunks = async (fileHash: string, fileName: string, totalChunks: number, duration: number, durationStr: string) => {
  const res = await fetch(`${PATH_URL}/admin/file/chunk/merge`, {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + yourToken,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      fileHash,
      fileName,
      totalChunks,
      group_id: fileGroup.value.id,
      module: 'video',
      length: duration,
      duration: durationStr
    })
  })

  const result = await res.json()
  if (result.code !== 0) {
    throw new Error(result.msg || '切片合并失败')
  }
}

// 取消上传
const cancelUpload = (fileId: string) => {
  uploadAbort.value[fileId]?.abort()
  delete uploadProgress.value[fileId]
  delete uploadAbort.value[fileId]
}

// 自定义上传请求(视频切片上传)
const handleVideoUpload = async (options: { file: File }) => {
  const { file } = options

  // 验证文件类型
  const videoTypes = ['video/mp4', 'video/mpeg', 'video/avi', 'video/flv', 'video/wmv', 'video/mov']
  if (!videoTypes.includes(file.type)) {
    ElMessage.error('只支持 mp4、mpeg、avi、flv、wmv、mov 格式的视频')
    return
  }

  // 验证文件大小(可选,根据需求添加)
  const maxSize = 100 * 1024 * 1024 // 100MB
  if (file.size > maxSize) {
    ElMessage.error('视频文件大小不能超过 100MB')
    return
  }

  loading.value = true
  await uploadVideoChunk(file)
  loading.value = false
}
相关推荐
山甫aa9 分钟前
【从零开始的 Web 后端学习】文件上传一篇搞定(MultipartFile + 本地存储 + 阿里云 OSS 保姆级教程)
前端·学习·阿里云
风骏时光牛马13 分钟前
AIRAGDebug智能检索链路异常定位与调试分析
前端
ljt27249606611 小时前
Flutter笔记--envied
前端·笔记·flutter
Flynt1 小时前
React Router v8 升了之后,我花了两天才把项目跑起来
前端·typescript·preact
泯泷8 小时前
手搓JSVMM第 8 篇:函数调用:参数、返回值与调用帧
前端·javascript·前端框架
Sherotree8 小时前
Google Antigravity——Agent 被提到主界面
前端·ide·后端·edge浏览器
里欧跑得慢9 小时前
Flutter 像素级还原:设计稿到代码的视觉无损转换技巧
前端·css·flutter·web
里欧跑得慢9 小时前
Flutter 三方库 function_tree — 鸿蒙应用开发中的动态数学公式解析与计算神器,实现鸿蒙深度适配下的复杂函数逻辑运行时求值实战(适配鸿蒙 HarmonyOS Next ohos)
android·前端·安全·flutter·华为·harmonyos
苏灿烤鱼10 小时前
没有机密文件,也没有付费数据,在浏览器里造一颗"间谍卫星"
前端·javascript·github
泯泷10 小时前
手搓JSVM第 5 篇:写第一个编译器:从 AST 生成 IR
前端·javascript·算法