当前位置: 首页 > news >正文

使用 Vue 实现移动端视频录制与自动截图功能

文章目录

  • 技术栈
  • 功能介绍
  • video标签属性
  • 完整代码
  • js 前端实现将视频Blob转Base64
  • java 后端实现将视频Base64转mp4文件

在移动端网页开发中,使用摄像头录制视频并自动生成截图是一个常见的需求,比如身份认证、人脸识别或互动问卷等场景。本文将介绍如何使用 Vue 实现一个简洁的前端视频录制组件,并在录制结束后自动截图。

在这里插入图片描述

技术栈

  • Vue 2.x
  • JavaScript(原生 API)
  • WebRTC(MediaDevices、MediaRecorder)
  • HTML5 元素
  • Canvas 截图

功能介绍

  • 支持前/后摄像头切换;
  • 录制指定时长(默认 5 秒)的视频;
  • 录制完成后自动截图视频中间帧;
  • 视频播放支持 controls 控件;
  • 截图以 Base64 显示;
  • 提供 @change 和 @screenshot 事件给父组件处理。

video标签属性

  • autoplay:页面加载完成后自动播放视频。注意浏览器通常要求视频是静音的才能自动播放。
  • playsinline:允许视频在网页内联播放,阻止在 iOS 上自动全屏。是 HTML 标准属性。
  • controls:是否显示视频播放控件,布尔值控制。
  • muted:是否静音播放。如果不显示控件就静音,满足自动播放要求。
  • webkit-playsinline:iOS Safari 专用,允许内联播放,防止自动全屏。
  • x5-playsinline:腾讯 X5 内核浏览器专用(如微信浏览器),允许内联播放。
  • x5-video-player-type=“h5”:强制使用 HTML5 播放器而不是系统播放器。适用于 X5 内核浏览器。
  • x5-video-player-fullscreen=“false”:禁止自动全屏(X5 内核浏览器),和 x5-playsinline 配合使用。

完整代码

  • index.vue
<template><div class="container"><video ref="video" class="video-container"autoPlayplaysinlinewebkit-playsinlinex5-playsinlinex5-video-player-type="h5"x5-video-player-fullscreen="false":controls="showVideoControls":muted="!showVideoControls"></video><div class="btn-group"><van-button icon="revoke" @click="toggleCamera" :disabled="recordStatus === 1">切换{{ cameraFacing === 'user' ? '后置' : '前置' }}</van-button><van-button type="primary" icon="play-circle" @click="startRecord" :disabled="recordStatus === 1">{{ `${recordStatus === 1 ? countDown : ''} ${recordStatusText[recordStatus]}` }}</van-button></div><img v-if="isScreenshot && videoScreenshotUrl" class="screenshot-container" :src="videoScreenshotUrl"></div>
</template><script src="./index.js">
</script><style scoped>
@import "./index.css";
</style>
  • index.js
export default {name: 'video-record',data() {return {videoWidth: 320,videoHeight: 240,videoType: 'video/mp4',imageType: 'image/png',stream: null,mediaRecorder: null,recordedChunks: [],videoBlob: null,showVideoControls: false,// 前置摄像头cameraFacing: 'user',// 0-未开始 1-录制中 2-录制完成recordStatus: 0,recordStatusText: ['开始录制', '秒后停止', '重新录制'],// 录制时长countDown: 5,timer: null,// 视频截图Base64数据videoScreenshotUrl: null,// 是否展示截图isScreenshot: true}},methods: {toggleCamera() {this.cameraFacing = this.cameraFacing === 'user' ? 'environment' : 'user';},async startRecord() {this.showVideoControls = falsethis.countDown = 5try {this.stream = await navigator.mediaDevices.getUserMedia({video: {facingMode: this.cameraFacing,width: {ideal: this.videoWidth},height: {ideal: this.videoHeight},frameRate: {ideal: 15}},audio: true,});this.$refs.video.srcObject = this.stream;this.mediaRecorder = new MediaRecorder(this.stream);this.mediaRecorder.ondataavailable = e => {if (e.data.size > 0) this.recordedChunks.push(e.data);};// 录制结束回调this.mediaRecorder.onstop = this.onRecordStop;// 开始录制this.mediaRecorder.start();this.recordStatus = 1;// 开始倒计时this.timer = setInterval(() => {this.countDown--;this.elapsedSeconds++;if (this.countDown <= 0) {this.stopRecord();}}, 1000);} catch (err) {console.error('获取摄像头失败', err);this.recordStatus = 0}},stopRecord() {if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {this.mediaRecorder.stop();}if (this.stream) {this.stream.getTracks().forEach(track => track.stop());this.$refs.video.srcObject = null;}this.recordStatus = 2;clearInterval(this.timer);this.timer = null;},onRecordStop() {this.videoBlob = new Blob(this.recordedChunks, {type: this.videoType});const videoUrl = URL.createObjectURL(this.videoBlob);this.$emit('change', {videoBlob: this.videoBlob});const video = this.$refs.video;video.src = videoUrl;video.onloadedmetadata = () => {this.showVideoControls = true;video.currentTime = 0;video.play();// 播放到视频中间段自动执行截图const duration = (isFinite(video.duration) && !isNaN(video.duration)) ? video.duration : 5.0;const targetTime = duration / 2;const onTimeUpdate = () => {if (video.currentTime >= targetTime) {// 移除监听器,防止多次触发截图操作。video.removeEventListener('timeupdate', onTimeUpdate)// 在浏览器下一帧进行截图,确保渲染完成后再执行requestAnimationFrame(() => {// console.log('执行截图操作')this.captureFrame()})}}// 注册事件监听器:只要视频播放,onTimeUpdate 会不断被触发(每约250ms,甚至更频繁),直到满足条件。video.addEventListener('timeupdate', onTimeUpdate);}},// 截图操作captureFrame() {const video = this.$refs.videoif (!video) {console.warn('未找到 video 元素,跳过截图');return}const canvas = document.createElement('canvas');const ctx = canvas.getContext('2d');canvas.width = video.videoWidth || 320;canvas.height = video.videoHeight || 240;ctx.drawImage(video, 0, 0, canvas.width, canvas.height);// 图片Base64数据this.videoScreenshotUrl = canvas.toDataURL(this.imageType);this.$emit('screenshot', {videoScreenshot: this.videoScreenshotUrl});}}
}
  • index.css
.container {display: flex;flex-direction: column;align-items: center;width: 100vw;
}.video-container {margin-top: 24px;margin-bottom: 24px;width: 320px;height: 240px;background: #000000;
}.btn-group {display: flex;flex-direction: row;justify-content: space-between;align-items: center;width: 320px;margin-bottom: 24px;
}.screenshot-container {width: 320px;height: 240px;
}

js 前端实现将视频Blob转Base64

function blobToBase64(blob) {return new Promise((resolve, reject) => {const reader = new FileReader();reader.onloadend = () => resolve(reader.result); // 结果是 data:video/mp4;base64,...reader.onerror = reject;reader.readAsDataURL(blob);});
}

java 后端实现将视频Base64转mp4文件

import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;private void base64ToFile(String base64Str, Path filePath) throws IOException {// 如果 base64Str 含有 "data:video/mp4;base64," 头部,需要去除if (base64Str.contains(",")) {base64Str = base64Str.substring(base64Str.indexOf(",") + 1);}// Base64 解码byte[] data = Base64.getDecoder().decode(base64Str);// 写入文件try (OutputStream stream = Files.newOutputStream(filePath)) {stream.write(data);}
}
Path videoFile = Files.createTempFile("filename", ".mp4");
base64ToFile(videoBase64, videoFile);
http://www.lryc.cn/news/598468.html

相关文章:

  • 每日算法刷题Day52:7.24:leetcode 栈5道题,用时1h35min
  • linux权限续
  • 【从0开始学习Java | 第3篇】阶段综合练习 - 五子棋制作
  • 奇异值分解(Singular Value Decomposition, SVD)
  • 光通信从入门到精通:PDH→DWDM→OTN 的超详细演进笔记
  • day62-可观测性建设-全链路监控zabbix+grafana
  • 深度分析Java内存结构
  • 排序查找算法,Map集合,集合的嵌套,Collections工具类
  • SSM之表现层数据封装-统一响应格式全局异常处理
  • Spring AI 系列之二十四 - ModerationModel
  • 从0到1学习c++ 命名空间
  • 【Linux】linux基础开发工具(一) 软件包管理器yum、编辑器vim使用与相关命令
  • 【YOLOv8改进 - 特征融合】FCM:特征互补映射模块 ,通过融合丰富语义信息与精确空间位置信息,增强深度网络中小目标特征匹配能力
  • Springboot儿童医院问诊导诊系统aqy75(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
  • 免费生成文献综述的网站推荐,助力高效学术写作
  • 408——数据结构(第二章 线性表)
  • 线段树学习笔记 - 练习题(2)
  • Flowable + Spring Boot 自定义审批流实战教程
  • 「iOS」黑魔法——方法交换
  • 词嵌入维度与多头注意力关系解析
  • 51c视觉~3D~合集4
  • 【C语言进阶】柔性数组
  • 11款Scrum看板软件评测:功能、价格、优缺点
  • C++标准库算法实战指南
  • Java基础day16-Vector类-Stack类-Collection子接口Set接口
  • 基础NLP | 02 深度学习基本原理
  • EasyExcel 模板导出数据 + 自定义策略(合并单元格)
  • 亚马逊云科技 EC2 部署 Dify,集成 Amazon Bedrock 构建生成式 AI 应用
  • 货车手机远程启动的扩展功能有哪些
  • QML 模型