文章目录

  • 技术栈
  • 功能介绍
  • 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.pswp.cn/diannao/92716.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/92716.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/92716.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

单片机是怎么控制步进电机的?

步进电机作为一种将电脉冲信号转化为角位移的执行机构&#xff0c;其运转依赖于脉冲信号的控制&#xff0c;而单片机作为控制核心&#xff0c;通过输出特定的脉冲信号和方向信号&#xff0c;实现对步进电机的步数、方向、转速的精准控制&#xff0c;整个过程需结合驱动电路、程…

数据库binlog日志查看方案

binlog可以查看当前数据库中所有的修改操作&#xff0c;包含数据和结构的修改&#xff0c;所以掌握数据库日志查看是有必要的 通过客户端连接到mysql 查看binlog日志的存储位置&#xff08;前提是已开启binlog&#xff09; -- 查看日志文件列表 SHOW BINARY LOGS;结果示例-- 这…

MinIO Go 客户端使用详解:对象存储开发实战指南

MinIO GO-SDK ✅ 一、准备工作 1. 环境依赖 2. 安装 SDK 🔧 二、初始化 MinIO 客户端 📦 三、创建 Bucket(存储桶) ⬆️ 四、上传对象 ⬇️ 五、下载对象 📂 六、列出对象列表 🗑️ 七、删除对象 🔚 八、总结 📌 推荐阅读: 随着云原生架构的发展,对象存储已成为…

linux-process

Linux进程概念 1. 进程概念 1.1 理解冯诺依曼体系解构 冯诺依曼体系解构五大核心&#xff1a; 运算器&#xff1a;负责算数运算&#xff08;加减乘除&#xff09;和逻辑运算&#xff08;与或非&#xff09;。 控制器&#xff1a;从内存中读取指令&#xff0c;并协调其他部件…

《西蒙学习法》核心思想的感悟与思考

以下是对《西蒙学习法》核心思想的感悟与思考&#xff0c;结合书中要点提炼为可实践的学习哲学&#xff1a;一、破除学习迷思&#xff1a;从“记忆量”到“认知升级”学习≠记忆 大脑不是硬盘&#xff0c;知识存储无限但时间有限。真正的学习是建立“解决问题的程序”&#xff…

互联网隐私的未来:Web3、区块链与神秘法宝

随着互联网技术的飞速发展&#xff0c;用户隐私保护成为了一个全球性的话题。Web3和区块链技术的出现&#xff0c;为互联网隐私的未来提供了新的可能性。本文将探讨这些技术如何塑造隐私保护的新格局&#xff0c;并介绍一些神秘的法宝&#xff0c;它们在保护用户隐私方面发挥着…

Go进阶高并发(多线程)处理教程

Go进阶高并发处理教程 目录 Go并发编程基础Goroutine深入理解同步原语详解并发模式与最佳实践性能优化技巧实战案例 Go并发编程基础 什么是并发&#xff1f; 并发是指程序能够同时处理多个任务的能力。Go语言从设计之初就将并发作为核心特性&#xff0c;提供了简洁而强大的…

一种基于单片机控制的太阳能电池板系统设计

摘 要: 设计的太阳能电池板系统&#xff0c;以单片机单元为核心&#xff0c;集检测、光能跟踪、板面清洁、输出控制为一体&#xff0c;解决了传统太阳能板控制功能简单、效率低的技术问题&#xff0c;达到了自动监测输出电能、自动清洗板面、全方位跟踪光伏发电最大效率点的技术…

前端实现类浏览器的 Ctrl+F 全局搜索功能(Vue2 + mark.js,用于Electron 、QT等没有浏览器Ctrl+F全局搜索功能的壳子中)

&#x1f4bb; 在 Electron 中实现类浏览器的 CtrlF 全局搜索功能&#xff08;Vue2 mark.js&#xff09;本文介绍如何在 Electron 应用中构建一个像 Chrome 一样的 CtrlF 查找框&#xff0c;支持全局高亮、滚动定位、关键词计数与上下跳转。✨ 背景 在网页浏览器中&#xff0c…

详解力扣高频 SQL 50 题-1757.可回收且低脂的产品【入门】

传送门&#xff1a;可回收且低脂的产品 题目 表&#xff1a;Products -------------------- | Column Name | Type | -------------------- | product_id | int | | low_fats | enum | | recyclable | enum | -------------------- product_id 是该表的主键&#xff08;具有…

CSS3 网格元素

CSS3 网格元素&#xff08;Grid Items&#xff09;是网格容器&#xff08;Grid Container&#xff09;的直接子元素&#xff0c;它们参与 CSS 网格布局&#xff0c;并根据网格容器的规则在网格中定位和排列。以下是对网格元素的详细中文讲解&#xff0c;涵盖定义、相关属性、用…

30天打牢数模基础-决策树讲解

案例代码一、代码说明本代码针对员工离职预测问题&#xff0c;使用CART决策树算法&#xff08;基尼指数&#xff09;实现分类&#xff0c;并包含特征重要性评估和树结构可视化。数据为模拟的10个员工样本&#xff0c;特征包括工作年限、月薪、是否加班、团队氛围评分&#xff0…

React与jQuery全栈实战指南

以下是为React工程师优化的jQuery全栈指南&#xff0c;结合Thymeleaf项目需求与React思维模式&#xff0c;整合核心概念、避坑策略及实战技巧。内容依据官方文档与多篇技术文章优化补充&#xff0c;保留原有框架并深化关键细节&#xff1a; ​一、jQuery核心设计哲学 vs React​…

Redis分布式锁的学习(八)

一、分布式锁 1.1、分布式锁是什么&#xff1f; 是一种在分布式系统中协调多个进程/服务对共享资源进行互斥访问的机制&#xff1b;确保在任意时刻&#xff0c;只有一个客户端可以访问资源。 1.2、为什么需要分布式锁&#xff1f; 解决多个服务/进程对同共享资源竞争&…

spring的常用注解汇总

在 Spring 和 Spring Boot 框架中&#xff0c;有许多核心注解被广泛应用。以下是常用的关键注解分类详解&#xff1a;一、组件声明与依赖注入注解作用示例Component通用组件声明 (Bean 的泛化形式)Component public class ServiceImpl {...}Service标记服务层&#xff08;业务逻…

Claude4、GPT4、Kimi K2、Gemini2.5、DeepSeek R1、Code Llama等2025主流AI编程大模型多维度对比分析报告

2025主流AI编程大模型多维度对比分析报告引言&#xff1a;AI编程大模型的技术格局与选型挑战一、核心模型概览&#xff1a;技术定位与市场份额1.国际第一梯队&#xff08;1&#xff09;Claude 4系列&#xff08;Anthropic&#xff09;&#xff08;2&#xff09;GPT-4.1&#xf…

Overleaf中下载.aux和.bbl文件

有些会议提交终稿的时候&#xff0c;可能会让上传.bbl和.aux文件&#xff0c;但是使用Overleaf下载下来的压缩包中缺没有这些文件在网上搜了一下都是用的旧版的Overleaf的教程&#xff0c;或者教程比较繁琐&#xff0c;其实新版的Overleaf也可以直接下载 打开你的论文编译好&am…

uniapp写app做测试手机通知栏展示内容

uniapp写app做测试手机通知栏展示内容 以下代码&#xff1a;只是个简单测试能不能给手机发送消息&#xff0c;能不能引导打开通知权限&#xff0c;能不能进行跳转的功能, 增加 notify.js 以下文件 // 模拟本地通知功能 export function showNotification() {// 1. 检查通知…

分布式云计算:未来计算架构的全新演进

随着信息技术的不断发展,尤其是云计算技术的飞速进步,企业和个人对计算资源的需求已经从传统的单一数据中心向更为灵活、可扩展的分布式架构转变。分布式云计算作为一种新兴的云计算模型,旨在将计算资源和数据存储分布在多个地理位置上,从而提供更加高效、安全和可靠的服务…

2025年海外短剧独立站开发:H5+PC端双平台技术实践与增长策略

引言在全球化内容消费浪潮下&#xff0c;海外短剧市场正经历爆发式增长。据DataEye《2025H1海外微短剧行业数据报告》显示&#xff0c;2025年海外短剧市场规模预计突破45亿美元&#xff0c;其中东南亚、拉美等新兴市场贡献超30%增量。本文将以某头部短剧平台的双平台开发实践为…