Vue 视频上传实战:视频预览、MediaRecorder 压缩与自定义上传

最近在项目里封装了一个视频上传组件,核心功能有 3 个:

  1. 支持常见视频格式上传
  2. 文件过大时,前端先压缩再上传
  3. 上传后支持视频预览、删除、回显

这篇文章就结合一个实际 Vue 组件,讲清楚 Vue 视频上传、MediaRecorder 压缩、预览弹窗、自定义上传 的完整实现思路。


一、效果说明

这个组件主要实现了下面几个能力:

  • 基于 el-upload 实现视频上传
  • 支持上传数量限制、格式限制、大小限制
  • 超过大小限制时,如果系统配置允许,就走前端压缩
  • 压缩时展示进度条、预计剩余时间,并支持取消上传
  • 上传完成后展示视频封面
  • 点击可直接预览视频
  • 支持回显服务端已有视频列表

二、上传区域怎么做

组件外层直接使用 Element UI 的 el-upload,但这里没有走默认上传,而是通过 http-request 接管上传请求:

vue 复制代码
<el-upload
  ref="uploadRef"
  :action="uploadVideoUrl"
  :http-request="submitUploadRequest"
  list-type="picture-card"
  :on-success="handleUploadSuccess"
  :before-upload="handleBeforeUpload"
  :limit="limit"
  :on-error="handleUploadError"
  :on-exceed="handleExceed"
  name="file"
  :accept="fileType.join(',')"
  :show-file-list="true"
  :headers="headers"
  :file-list="fileList"
  :disabled="disabled"
  :on-remove="handleRemove"
>
</el-upload>

这里最关键的是两个钩子:

  • before-upload:上传前校验格式、大小,必要时执行压缩
  • http-request:自己用 axios 上传压缩后的文件

这种方式比默认上传更灵活,因为我们可以在真正请求接口之前,对文件做加工。


三、先做格式和大小校验

上传前先进入 handleBeforeUpload,这里主要做了两件事:

1. 校验文件类型

组件通过 file.type 和文件后缀双重判断,避免某些浏览器拿不到标准 MIME 类型时误判。

js 复制代码
if (!isVideo) {
  return this.blockUpload(`文件格式不正确, 请上传${this.fileType.join("/")}视频格式文件!`);
}

2. 校验文件大小

如果视频没有超过限制,直接上传;如果超出限制,就看是否允许压缩:

js 复制代码
const isLt = file.size / 1024 / 1024 < this.fileSize;
if (!isLt) {
  if (!this.configNeedCompress) {
    return this.blockUpload(`上传视频大小不能超过 ${this.fileSize} MB!`);
  }
}

这里的设计很实用:

  • 小文件直接传,节省前端算力
  • 大文件才压缩,避免不必要的等待
  • 是否开启压缩由配置控制,方便线上开关

四、MediaRecorder 为什么能做视频压缩

很多人提到前端压缩视频,第一反应是 ffmpeg.wasm。但这个组件走的是另一条路线:MediaRecorder + canvas.captureStream()

实现思路是:

  1. 先把原视频加载到隐藏的 video 元素中
  2. canvas 按目标尺寸不断绘制视频帧
  3. 通过 canvas.captureStream(30) 生成新的视频流
  4. 再配合 MediaRecorder 按目标码率重新编码输出
  5. 最后生成新的 File 对象再上传

这部分非常关键。


五、压缩前先拿到视频元信息

组件先通过 video 标签读取原视频的时长、宽高、码率,用于判断是否需要压缩:

js 复制代码
getVideoMeta(file) {
  return new Promise((resolve, reject) => {
    const video = document.createElement("video");
    const objectUrl = URL.createObjectURL(file);
    video.preload = "metadata";
    video.onloadedmetadata = () => {
      const duration = Number(video.duration) || 0;
      const width = Number(video.videoWidth) || 0;
      const height = Number(video.videoHeight) || 0;
      URL.revokeObjectURL(objectUrl);
      resolve({
        duration,
        width,
        height,
        bitrateMbps: duration > 0 ? (file.size * 8) / duration / 1024 / 1024 : 0,
      });
    };
    video.onerror = () => {
      URL.revokeObjectURL(objectUrl);
      reject(new Error("failed to load video metadata"));
    };
    video.src = objectUrl;
  });
}

当前组件的压缩判断逻辑是:

  • 分辨率超过 1080P
  • 或码率超过 4Mbps

只要满足其中一个条件,就进入压缩流程。


六、如何生成压缩方案

拿到元信息后,组件会构建压缩计划:

js 复制代码
buildCompressionPlan(file, meta) {
  const over1080P = this.isOver1080P(meta);
  const overBitrate = meta.bitrateMbps > BITRATE_LIMIT_MBPS;
  if (!over1080P && !overBitrate) {
    return null;
  }
  return {
    outputBaseName: `${Date.now()}_${file.name.replace(/\.[^.]+$/, "") || "video"}_compressed`,
    targetVideoBitrate: over1080P ? TARGET_HD_VIDEO_BITRATE : TARGET_SD_VIDEO_BITRATE,
    shouldScaleTo1080: over1080P,
  };
}

这里的策略比较清晰:

  • 超过 1080P,就缩到 1080 高度以内
  • 视频码率统一控制到目标值
  • 输出一个新的压缩文件名

这个设计适合后台管理系统,优先保证"能传、够清晰、速度可控"。


七、MediaRecorder 压缩核心实现

真正的压缩逻辑在 compressVideoByMediaRecorder 里。

1. 先判断浏览器是否支持

js 复制代码
isMediaRecorderCompressionSupported() {
  return typeof window !== "undefined"
    && typeof window.MediaRecorder !== "undefined"
    && typeof document !== "undefined";
}

然后再通过 MediaRecorder.isTypeSupported() 选择当前浏览器可用的编码格式,比如:

  • video/mp4
  • video/webm
  • video/ogg

这样能尽可能兼容不同浏览器环境。

2. 通过 canvas 重绘视频帧

js 复制代码
const drawFrame = () => {
  if (video.ended || stoppedByCancel) {
    return;
  }
  canvasContext.drawImage(video, 0, 0, width, height);
  animationFrameId = window.requestAnimationFrame(drawFrame);
};

这一步的作用是:

  • 可以顺便做分辨率缩放
  • 让新输出的视频使用新的画面尺寸

3. 把音频也混进新流里

很多前端压缩方案只处理画面,结果上传后视频没声音。这个组件用 AudioContext 把原视频音轨重新混入录制流:

js 复制代码
audioContext = new AudioContextClass();
audioSourceNode = audioContext.createMediaElementSource(video);
audioDestination = audioContext.createMediaStreamDestination();
audioSourceNode.connect(audioDestination);
audioDestination.stream.getAudioTracks().forEach((track) => {
  mixedStream.addTrack(track);
});

这个细节非常重要,不然你得到的往往是一个"静音压缩版视频"。

4. 使用目标码率重新录制

js 复制代码
const recorder = new MediaRecorder(mixedStream, {
  mimeType,
  videoBitsPerSecond: this.parseBitrate(plan.targetVideoBitrate),
  audioBitsPerSecond: this.parseBitrate(TARGET_AUDIO_BITRATE),
});

这其实就是压缩的关键点:

  • 分辨率降了
  • 码率降了
  • 最终文件体积自然就降下来了

八、压缩进度条怎么做

MediaRecorder 本身不会直接给你压缩百分比,所以组件采用了一个比较实用的思路:

  • video.currentTime / video.duration 估算进度
  • 用已耗时反推剩余时间
js 复制代码
progressTimer = window.setInterval(() => {
  const duration = Number(video.duration) || meta.duration || 0;
  const current = Math.min(Number(video.currentTime) || 0, duration);
  const ratio = duration > 0 ? current / duration : 0;
  this.compressProgress = Math.max(1, Math.min(99, Math.round(ratio * 100)));
  if (ratio > 0 && ratio < 1) {
    const elapsed = (Date.now() - startTime) / 1000;
    this.compressEtaText = this.formatSeconds((elapsed * (1 - ratio)) / ratio);
  }
}, 200);

虽然这不是"真实编码进度",但对于用户体验已经足够友好了。


九、如何支持取消压缩和取消上传

组件还做了一个很实用的功能:用户可以取消上传

实现思路是自己维护一个 compressAbortController,本质上是手动封装的中断器:

js 复制代码
this.compressAbortController = {
  abort: () => {
    stoppedByCancel = true;
    if (recorder.state !== "inactive") {
      recorder.stop();
    }
  },
};

点击取消按钮后:

  • 标记当前任务已取消
  • 移除待上传文件
  • 停止录制
  • 清理上传列表里的临时项

这部分把边界处理得比较完整,用户体验会比"只能等压缩结束"好很多。


十、为什么还要自定义上传

压缩后的文件和原始文件不是同一个对象,所以默认上传已经不够用了。

组件里通过 pendingUploadFiles 暂存真正要上传的文件:

js 复制代码
this.pendingUploadFiles[file.uid] = uploadFile;

然后在 submitUploadRequest 中取出压缩后的文件,通过 axios 手动上传:

js 复制代码
const uploadFile = this.pendingUploadFiles[option.file.uid] || option.file;
const formData = new FormData();
formData.append("file", uploadFile, uploadFile.name);

axios({
  method: "post",
  url: this.uploadVideoUrl,
  data: formData,
  headers: {
    ...this.headers,
    "Content-Type": "multipart/form-data",
  },
  onUploadProgress: (event) => {
    const percent = event.total ? (event.loaded / event.total) * 100 : 0;
    option.onProgress({ percent });
  },
});

这样就把"压缩"和"上传"完整串起来了。


十一、视频预览怎么做

视频预览实现就比较直接了:

js 复制代码
handlePreviewVideo(file) {
  this.previewVideoUrl = file.url;
  this.previewVideoDialog = true;
}

弹窗中放一个 video 标签:

vue 复制代码
<el-dialog title="预览视频" :visible.sync="previewVideoDialog" width="900px">
  <video id="video" height="520" style="width: 100%;" :src="previewVideoUrl" controls></video>
</el-dialog>

另外在关闭弹窗前调用 pause(),避免关闭后视频还在后台继续播放:

js 复制代码
beforePreviewVideoClose(done) {
  let videoDom = document.getElementById('video');
  if (videoDom) {
    videoDom.pause();
  }
  done();
}

十二、已上传视频如何回显

组件支持把接口返回的字符串或数组,转换成上传列表需要的数据结构:

js 复制代码
const list = Array.isArray(val) ? val : val.split(",");
this.fileList = list.map((item) => {
  if (typeof item === "string") {
    let url;
    let coverPicture;
    if(item?.startsWith('http://') || item?.startsWith('https://')){
      url = item;
      coverPicture = item.replace('.mp4','.png');
    } else {
      url = this.imgUrl + item;
      coverPicture = this.imgUrl + item.replace('.mp4','.png');
    }
    item = { name: item, url, serverPath: item, coverPicture };
  }
  return item;
});

这样编辑页面打开时,就能自动显示历史视频和封面图。


十三、总结

这套 Vue 视频上传组件的优点:

  • 基于 el-upload,接入成本低
  • 前端压缩只在超限时触发,比较省资源
  • MediaRecorder + canvas 不依赖重型 wasm 库,方案相对轻量
  • 上传、预览、删除、回显都做全了
  • 还有取消压缩、进度展示这类体验优化

当然,它也有几个注意点:

  • MediaRecorder 编码能力受浏览器支持影响较大
  • mp4 在不同浏览器里的兼容性要重点测试
  • 超大视频前端压缩依然会比较耗时
  • 管理后台适合这套方案,超专业视频处理场景更推荐服务端转码
相关推荐
Hilaku2 小时前
AI 生成的代码都是一坨屎?聊聊怎么给 Agent 制定工程约束
前端·javascript·ai编程
吴声子夜歌2 小时前
Vue3——使用Vue Router实现路由
前端·javascript·vue.js·vue-router
CDwenhuohuo2 小时前
小程序全局使用api
javascript·vue.js·小程序
whinc2 小时前
Node.js技术周刊 2026年第16周
前端·javascript
DyLatte2 小时前
我做了个AI项目后才发现:会做事的人,正在输给会讲故事的人
前端·后端·程序员
深海鱼在掘金2 小时前
从Claude Code泄露源码看工程架构:第三章 — CLI 启动链路的分流策略与按需加载机制
前端·人工智能·设计模式
踩着两条虫2 小时前
VTJ:低代码平台原理
前端·低代码·ai编程
颜酱2 小时前
提示词强化1:三个让大模型更「听话」的习惯
前端·javascript·人工智能
破茧成蝶8102 小时前
修复播报缺失文字的bug,改为“播放单个 -> 等待结束 -> 延迟 10ms秒 -> 播放下一个”的递归/循环模式
前端