前端必问面试题:视频上传,文件签名类型限制,canvas截取视频封面
1. 视频上传
1.1. 获取选中的文件信息
<template>
<input type="file" @change="change">
</template>
<script setup>
import { reactive } from 'vue'
/* 数据 - start */
let videoObj = reactive({ })
/* 数据 - end */
// 监听用户选择文件的操作
function change(e) {
// 获取用户选中的文件
console.log(e) // 这里面有文件列表的类数组集合(伪数组)
let files = e.target.files || e.dataTransfer.files // e.target.files获取文件信息;e.dataTransfer.files获取拖拽的文件信息
if (!files.length) return // 如果集合的长度为0那么就return掉
videoObj.realVideo = files[0] // 拿到视频对象
}
</script>
![]() |
|---|
| 视频对象信息介绍 |
1.2. 上传视频类型的限制
- JS限制
<!-- App.vue -->
<template>
<input type="file" @change="change">
</template>
<script setup>
import { reactive } from 'vue'
/* 数据 - start */
let videoObj = reactive({ })
/* 数据 - end */
// 监听用户选择文件的操作
function change(e) {
// e.target.files获取文件信息;e.dataTransfer.files获取拖拽的文件信息
let files = e.target.files || e.dataTransfer.files
// 如果没有选择文件终止后续代码的执行
if (!files.length) return
// 把文件对象信息给到videoObj.realVideo
videoObj.realVideo = files[0]
// 拿到了文件对象
console.log(videoObj.realVideo)
// 上传的文件类型限制
if(!/(mp4)/i.test(videoObj.realVideo.type)) {
alert('上传的视频只能是mp4类型的') // 至于alert完毕了还是显示在了页面上其实我们不会使用原生input都会模拟,模拟完毕input是隐藏的因此就不会显示在页面上了或者value = ""
return
}
}
</script>
inputaccept属性配置限制
<!-- App.vue -->
<template>
<input type="file" @change="change" accept=".mp4"> // 多种格式写法.mp4, mov中间加逗号即可
</template>
![]() |
|---|
| 把不允许的类型直接过滤掉了 |
1.3. 限制上传文件大小

1.3.1. 位 bit 比特
位(bit)计算机中最小的计量单位——比特
我们在电脑中所看到的任何东西,文本,网页,图片,音频,视频……它们归根结底存储在计算机的磁盘中都是一串0和1组成的数字。最早的时候,在第一台计算机诞生时,我们用打孔纸带表示01,现在我们在计算机里用电压高地,逻辑门开关表示01。
计算机中,一位数字0,或者一位数字1,即一个二进制位,我们称之为 “位”,英文写作bit,中文写作“比特”(必须是小写的b,大写B和小写b是两个不同的单位),可以简写成 1bit。1bit的数据就是一位只能表示0,或者1
1.3.2. 字节Byte
8个二进制位,即 8 bit 记为1Byte, 1字节
在早期计算机中,计算机科学家们定义了一套编码表,把二进制数字和一系列字母符号对应起来并一直沿用至今称之为“ASCII码表”( American Standard Code for Information Interchange,美国信息交换标准代码),就像电报一样,我们规定三长三短表示SOS,在ASCII码表里,我们规定 1000001 表示大写字母 A。 以此类推,将大小写英文字母以及一些常用符号映射成了二进制。而恰好这个编码表每一个数据的长度都是8个二进制位,即8bit表示一个字母,所以我们把 8bit 记作 1Byte字节。
题外话,综上在某些时候的计算机内一个字母一个字符的长度就是1 Byte,8 bit的长度。而早期汉字编码则是使用两个字节来表示一个汉字,即 2Byte, 16bit
限制上传文件大小:限制文件上传大小30MB(1MB = 1024kb,1kb = 1024B),而size的单位是B字节,因此我们可以这么写:
30 * 1024 * 1024就是最大可以上传30MB了
<!-- App.vue -->
<template>
<input type="file" @change="change">
</template>
<script setup>
import { reactive } from 'vue'
/* 数据 - start */
let videoObj = reactive({ })
/* 数据 - end */
// 监听用户选择文件的操作
function change(e) {
// e.target.files获取文件信息;e.dataTransfer.files获取拖拽的文件信息
let files = e.target.files || e.dataTransfer.files
// 如果没有选择文件终止后续代码的执行
if (!files.length) return
// 把文件对象信息给到videoObj.realVideo
videoObj.realVideo = files[0]
// 拿到了文件对象
console.log(videoObj.realVideo)
// 上传的文件类型限制
if(!/(mp4)/i.test(videoObj.realVideo.type)) {
alert('上传的视频只能是mp4类型的')
return
}
// 限制文件上传大小
if(videoObj.realVideo.size > (2 * 1024 * 1024)) {
alert('上传的视频不能大于2MB')
return
}
}
</script>
1.4. 视频上传
<!-- App.vue -->
<template>
<input type="file" @change="change">
<button @click="upload">点击上传</button>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { upLoadVideoAPI } from '../api/person.js'
/* 数据 - start */
let videoObj = reactive({})
/* 数据 - end */
// 监听用户选择文件的操作
function change(e) {
// e.target.files获取文件信息;e.dataTransfer.files获取拖拽的文件信息
let files = e.target.files || e.dataTransfer.files
// 如果没有选择文件终止后续代码的执行
if (!files.length) return
// 把文件对象信息给到videoObj.realVideo
videoObj.realVideo = files[0]
// 拿到了文件对象
console.log(videoObj.realVideo)
// 上传的文件类型限制
if (!/(mp4)/i.test(videoObj.realVideo.type)) {
alert('上传的视频只能是mp4类型的')
return
}
// 限制文件上传大小
if (videoObj.realVideo.size > (2 * 1024 * 1024)) {
alert('上传的视频不能大于2MB')
return
}
}
// 上传方法
async function upload() {
// 判断是否上传了视频
if (!videoObj.realVideo) {
alert('请先选择要上传的文件')
return false
}
// 把文件传递到服务器
let formData = new FormData()
// 后端需要videoPath参数,videoObj.realVideo是视频文件
formData.append('videoPath', videoObj.realVideo)
// 请求上传接口
const result = await upLoadVideoAPI(formData)
// 返回上传后的结果
console.log(result)
}
</script>

1.5. 上传前的预览
- video标签
<video style="width: 300px; height: 240px; background: blue;" src=""></video>
- 拿到视频地址
<!-- App.vue -->
<template>
<input type="file" @change="change">
<video style="width: 300px; height: 240px; background: blue;" src="" ></video>
<button @click="upload">点击上传</button>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { upLoadVideoAPI } from '../api/person.js'
/* 数据 - start */
let videoObj = reactive({})
/* 数据 - end */
// 监听用户选择文件的操作
function change(e) {
// e.target.files获取文件信息;e.dataTransfer.files获取拖拽的文件信息
let files = e.target.files || e.dataTransfer.files
// 如果没有选择文件终止后续代码的执行
if (!files.length) return
// 把文件对象信息给到videoObj.realVideo
videoObj.realVideo = files[0]
// 拿到了文件对象
console.log(videoObj.realVideo)
// 上传的文件类型限制
if (!/(mp4)/i.test(videoObj.realVideo.type)) {
alert('上传的视频只能是mp4类型的')
return
}
// 限制文件上传大小
if (videoObj.realVideo.size > (2 * 1024 * 1024)) {
alert('上传的视频不能大于2MB')
return
}
// 该方法返回一个blob视频地址
videoObj.videoUrl = getObjectURL(videoObj.realVideo)
}
// 获取视频地址
function getObjectURL(file) {
let url = null
if (window.createObjectURL !== undefined) {
url = window.createObjectURL(file)
} else if (window.URL !== undefined) {
url = window.URL.createObjectURL(file)
} else if (window.webkitURL !== undefined) {
url = window.webkitURL.createObjectURL(file)
}
return url
}
// 上传方法
async function upload() {
// 判断是否上传了视频
if (!videoObj.realVideo) {
alert('请先选择要上传的文件')
return false
}
// 把文件传递到服务器
let formData = new FormData()
// 后端需要videoPath参数,videoObj.realVideo是视频文件
formData.append('videoPath', videoObj.realVideo)
// 请求上传接口
const result = await upLoadVideoAPI(formData)
// 返回上传后的结果
console.log(result)
}
</script>
- 修改video中的src属性
<!-- App.vue -->
<template>
<input type="file" @change="change">
<video style="width: 300px; height: 240px; background: blue;" src="" ref="getVideoDom"></video>
<button @click="upload">点击上传</button>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { upLoadVideoAPI } from '../api/person.js'
/* 数据 - start */
let videoObj = reactive({})
let getVideoDom = ref(null)
/* 数据 - end */
// 监听用户选择文件的操作
function change(e) {
// e.target.files获取文件信息;e.dataTransfer.files获取拖拽的文件信息
let files = e.target.files || e.dataTransfer.files
// 如果没有选择文件终止后续代码的执行
if (!files.length) return
// 把文件对象信息给到videoObj.realVideo
videoObj.realVideo = files[0]
// 拿到了文件对象
console.log(videoObj.realVideo)
// 上传的文件类型限制
if (!/(mp4)/i.test(videoObj.realVideo.type)) {
alert('上传的视频只能是mp4类型的')
return
}
// 限制文件上传大小
if (videoObj.realVideo.size > (2 * 1024 * 1024)) {
alert('上传的视频不能大于2MB')
return
}
// 该方法返回一个blob视频地址
videoObj.videoUrl = getObjectURL(videoObj.realVideo)
// 修改video src地址
setSrcAndCaptureImage()
}
// 获取视频地址
function getObjectURL(file) {
let url = null
if (window.createObjectURL !== undefined) {
url = window.createObjectURL(file)
} else if (window.URL !== undefined) {
url = window.URL.createObjectURL(file)
} else if (window.webkitURL !== undefined) {
url = window.webkitURL.createObjectURL(file)
}
return url
}
// 截图
function setSrcAndCaptureImage() {
// 视频地址赋值给video的src属性
getVideoDom.value.src = videoObj.videoUrl
}
// 上传方法
async function upload() {
// 判断是否上传了视频
if (!videoObj.realVideo) {
alert('请先选择要上传的文件')
return false
}
// 把文件传递到服务器
let formData = new FormData()
// 后端需要videoPath参数,videoObj.realVideo是视频文件
formData.append('videoPath', videoObj.realVideo)
// 请求上传接口
const result = await upLoadVideoAPI(formData)
// 返回上传后的结果
console.log(result)
}
</script>
- 播放视频
<!-- App.vue -->
<template>
<input type="file" @change="change">
<video style="width: 300px; height: 240px; background: blue;" src="" ref="getVideoDom" @click="playHandler"></video>
<button @click="upload">点击上传</button>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { upLoadVideoAPI } from '../api/person.js'
/* 数据 - start */
let videoObj = reactive({})
let getVideoDom = ref(null)
/* 数据 - end */
// 监听用户选择文件的操作
function change(e) {
// e.target.files获取文件信息;e.dataTransfer.files获取拖拽的文件信息
let files = e.target.files || e.dataTransfer.files
// 如果没有选择文件终止后续代码的执行
if (!files.length) return
// 把文件对象信息给到videoObj.realVideo
videoObj.realVideo = files[0]
// 拿到了文件对象
console.log(videoObj.realVideo)
// 上传的文件类型限制
if (!/(mp4)/i.test(videoObj.realVideo.type)) {
alert('上传的视频只能是mp4类型的')
return
}
// 限制文件上传大小
if (videoObj.realVideo.size > (2 * 1024 * 1024)) {
alert('上传的视频不能大于2MB')
return
}
// 该方法返回一个blob视频地址
videoObj.videoUrl = getObjectURL(videoObj.realVideo)
// 修改video src地址
setSrcAndCaptureImage()
}
// 获取视频地址
function getObjectURL(file) {
let url = null
if (window.createObjectURL !== undefined) {
url = window.createObjectURL(file)
} else if (window.URL !== undefined) {
url = window.URL.createObjectURL(file)
} else if (window.webkitURL !== undefined) {
url = window.webkitURL.createObjectURL(file)
}
return url
}
// 截图
function setSrcAndCaptureImage() {
// 视频地址赋值给video的src属性
getVideoDom.value.src = videoObj.videoUrl
}
// 视频播放/暂停
function playHandler() {
getVideoDom.value.paused ? getVideoDom.value.play() : getVideoDom.value.pause()
}
// 上传方法
async function upload() {
// 判断是否上传了视频
if (!videoObj.realVideo) {
alert('请先选择要上传的文件')
return false
}
// 把文件传递到服务器
let formData = new FormData()
// 后端需要videoPath参数,videoObj.realVideo是视频文件
formData.append('videoPath', videoObj.realVideo)
// 请求上传接口
const result = await upLoadVideoAPI(formData)
// 返回上传后的结果
console.log(result)
}
</script>

1.6. 截取封面图
1.6.1. 点击截图
<!-- App.vue -->
<template>
<input type="file" @change="change">
<video style="width: 300px; height: 240px; background: blue;" ref="getDomVideo" src="" autoplay></video>
<button @click="upload">点击上传</button>
<button @click="capture">点击截图</button>
<!-- 截图完毕在这里显示 -->
<img ref="image" style=" margin: 23px 10px 0 10px;"/>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { upLoadVideoAPI } from '../api/person.js'
/* 数据 - start */
let videoObj = reactive({})
// video元素获取
let getDomVideo = ref(null)
let image = ref(null)
/* 数据 - start */
// 监听用户选中文件的操作
function change(ev) {
console.log(ev)
// ev.target.files点击上传的;ev.dataTransfer.files拖拽上传
let files = ev.target.files || ev.dataTransfer.files
console.log(files)
// 如果集合的长度是0那么就return掉不执行了
if (!files.length) return
// 如果有长度了
videoObj.realVideo = files[0]
// 限制上传文件的类型
if (!/(mp4)/i.test(videoObj.realVideo.type)) {
alert('上传的人视频只能是mp4的格式')
return
}
// 限制上传文件的大小
if (videoObj.realVideo.size > 2 * 1024 * 1024) {
alert('上传的人视频不能大于2MB')
return
}
// 这里就可以拿到你上传的文件基本信息了
videoObj.videoUrl = getObjectURL(videoObj.realVideo)
console.log(videoObj)
// 调用修改视频src的方法
setSrc()
}
// 拿到当前上传的视频地址址 file参数值的就是我们的要被预览的文件(要上传的文件)
function getObjectURL(file) {
let url = null
if (window.createObjectURL !== undefined) {
url = window.createObjectURL(file)
} else if (window.URL !== undefined) {
url = window.URL.createObjectURL(file)
} else if (window.webkitURL !== undefined) {
url = window.webkitURL.createObjectURL(file)
}
return url
}
// 修改video的src属性
function setSrc() {
// 修改video的src属性为上传的视频的blob地址
getDomVideo.value.src = videoObj.videoUrl
}
async function upload() {
// 判断有没有选中文件
if (!videoObj.realVideo) {
alert('请先选择要上传的文件')
return
}
// 把文件上传到服务器
var formData = new FormData(); // 当前为空
// 后端需要videoPath这个参数我们通过append方法添加格式就是key-value形式
formData.append('videoPath', videoObj.realVideo);
// 请求上传接口
const result = await upLoadVideoAPI(formData)
console.log(result)
}
function capture() {
let canvas = document.createElement('canvas')
canvas.width = getDomVideo.value.videoWidth
canvas.height = getDomVideo.value.videoHeight
// 从图像的0,0坐标截取宽度为canvas.width和canvas.height
canvas.getContext('2d').drawImage(getDomVideo.value, 0, 0, canvas.width, canvas.height)
image.value.src = canvas.toDataURL("image/jpg")
}
</script>
1.6.2. 自动截图&手动截图
<!-- App.vue -->
<template>
<input type="file" @change="change">
<video style="width: 300px; height: 240px; background: blue;" ref="getDomVideo" src="" autoplay></video>
<button @click="upload">点击上传</button>
<button @click="capture">点击截图</button>
<img ref="image" style=" margin: 23px 10px 0 10px;" />
</template>
<script setup>
import { reactive, ref } from 'vue'
import { upLoadVideoAPI } from '../api/person.js'
/* 数据 - start */
let videoObj = reactive({})
// video元素获取
let getDomVideo = ref(null)
let image = ref(null)
/* 数据 - start */
// 监听用户选中文件的操作
function change(ev) {
console.log(ev)
// ev.target.files点击上传的;ev.dataTransfer.files拖拽上传
let files = ev.target.files || ev.dataTransfer.files
console.log(files)
// 如果集合的长度是0那么就return掉不执行了
if (!files.length) return
// 如果有长度了
videoObj.realVideo = files[0]
// 限制上传文件的类型
if (!/(mp4)/i.test(videoObj.realVideo.type)) {
alert('上传的人视频只能是mp4的格式')
return
}
// 限制上传文件的大小
if (videoObj.realVideo.size > 2 * 1024 * 1024) {
alert('上传的人视频不能大于2MB')
return
}
// 这里就可以拿到你上传的文件基本信息了
videoObj.videoUrl = getObjectURL(videoObj.realVideo)
console.log(videoObj)
// 调用修改视频src的方法
setSrc()
}
// 拿到当前上传的视频地址址 file参数值的就是我们的要被预览的文件(要上传的文件)
function getObjectURL(file) {
let url = null
if (window.createObjectURL !== undefined) {
url = window.createObjectURL(file)
} else if (window.URL !== undefined) {
url = window.URL.createObjectURL(file)
} else if (window.webkitURL !== undefined) {
url = window.webkitURL.createObjectURL(file)
}
return url
}
// 修改video的src属性
function setSrc() {
// 修改video的src属性为上传的视频的blob地址
getDomVideo.value.src = videoObj.videoUrl
// loadeddata当前帧的数据是可用的
getDomVideo.value.addEventListener('loadeddata', capture)
}
// 截图方法
function capture() {
let canvas = document.createElement('canvas')
canvas.width = getDomVideo.value.videoWidth
canvas.height = getDomVideo.value.videoHeight
// 从图像的0,0坐标截取宽度为canvas.width和canvas.height
canvas.getContext('2d').drawImage(getDomVideo.value, 0, 0, canvas.width, canvas.height)
// 仅仅为了展示和所有功能均无关系
image.value.src = canvas.toDataURL("image/jpg")
// 返回一个包含图片展示的 data URI可以理解为是base64编码的图片并且存储到videoObj对象中上传封面的时候会用到该数据
videoObj.coverUrl = canvas.toDataURL('image/png')
console.log(videoObj)
// 移除事件(性能优化)
getVideoDom.value.removeEventListener('loadeddata', captureImage)
}
async function upload() {
// 判断有没有选中文件
if (!videoObj.realVideo) {
alert('请先选择要上传的文件')
return
}
// 把文件上传到服务器
var formData = new FormData(); // 当前为空
// 后端需要videoPath这个参数我们通过append方法添加格式就是key-value形式
formData.append('videoPath', videoObj.realVideo);
// 请求上传接口
const result = await upLoadVideoAPI(formData)
console.log(result)
}
</script>
如果截取的第一帧图片是透明的也就是截图有问题,解决方案:
在创建<video>元素时添加preload="auto",强制浏览器提前加载元数据和部分视频内容,避免因缓冲不足导致截取失败
1.7. 调用上传封面图接口
<!-- App.vue -->
<template>
<input type="file" @change="change">
<video style="width: 300px; height: 240px; background: blue;" src="" ref="getVideoDom" @click="playHandler"></video>
<button @click="upload">点击上传</button>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { upLoadVideoAPI, upLoadCoverVideoAPI } from '../api/person.js'
/* 数据 - start */
let videoObj = reactive({})
let getVideoDom = ref(null)
/* 数据 - end */
// 监听用户选择文件的操作
function change(e) {
// e.target.files获取文件信息;e.dataTransfer.files获取拖拽的文件信息
let files = e.target.files || e.dataTransfer.files
// 如果没有选择文件终止后续代码的执行
if (!files.length) return
// 把文件对象信息给到videoObj.realVideo
videoObj.realVideo = files[0]
// 拿到了文件对象
console.log(videoObj.realVideo)
// 上传的文件类型限制
if (!/(mp4)/i.test(videoObj.realVideo.type)) {
alert('上传的视频只能是mp4类型的')
return
}
// 限制文件上传大小
if (videoObj.realVideo.size > (2 * 1024 * 1024)) {
alert('上传的视频不能大于2MB')
return
}
// 该方法返回一个blob视频地址
videoObj.videoUrl = getObjectURL(videoObj.realVideo)
// 修改video src地址
setSrcAndCaptureImage()
}
// 获取视频地址
function getObjectURL(file) {
let url = null
if (window.createObjectURL !== undefined) {
url = window.createObjectURL(file)
} else if (window.URL !== undefined) {
url = window.URL.createObjectURL(file)
} else if (window.webkitURL !== undefined) {
url = window.webkitURL.createObjectURL(file)
}
return url
}
// 设置url和截图
function setSrcAndCaptureImage() {
// 视频地址赋值给video的src属性
getVideoDom.value.src = videoObj.videoUrl
// loadeddata当前帧的数据是可用的
getVideoDom.value.addEventListener('loadeddata', captureImage)
// 截图视频的第一帧作为封面图
function captureImage() {
// 自动播放
getVideoDom.value.play()
let scale = 1
// 创建canvas画布
let canvas = document.createElement('canvas')
// canvas画布大小宽度设置
canvas.width = getVideoDom.value.videoWidth * scale
// canvas画布大小宽度设置
canvas.height = getVideoDom.value.videoHeight * scale
// 开启2D绘图空间 drawImage()方法绘制一张图像
canvas.getContext('2d').drawImage(getVideoDom.value, 0, 0, canvas.width, canvas.height)
// 返回一个包含图片展示的 data URI可以理解为是base64编码的图片
videoObj.coverUrl = canvas.toDataURL('image/png')
console.log(videoObj)
// 移除事件
getVideoDom.value.removeEventListener('loadeddata', captureImage)
}
}
// 视频播放/暂停
function playHandler() {
getVideoDom.value.paused ? getVideoDom.value.play() : getVideoDom.value.pause()
}
// 图片转成Buffer(用来处理二进制的一个接口,后端通过fs模块,读取一张图片获取到图片的文件流)google(上传图片buffer)
function dataURItoBlob(dataURI) {
var byteString = atob(dataURI.split(',')[1])
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]
var ab = new ArrayBuffer(byteString.length)
var ia = new Uint8Array(ab)
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i)
}
return new Blob([ab], { type: mimeString })
}
function dataURItoBlob(dataURI) {
var byteString = atob(dataURI.split(',')[1])
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]
var ab = new ArrayBuffer(byteString.length)
var ia = new Uint8Array(ab)
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i)
}
return new Blob([ab], { type: mimeString })
}
// 上传方法
async function upload() {
// 判断是否上传了视频
if (!videoObj.realVideo) {
alert('请先选择要上传的文件')
return false
}
// 把文件传递到服务器
let formData = new FormData()
// 后端需要videoPath参数,videoObj.realVideo是视频文件
formData.append('videoPath', videoObj.realVideo)
// 请求上传接口
const result = await upLoadVideoAPI(formData)
// 返回上传后的结果
console.log(result)
if (result.status === 200) {
// 视频文件名
let filename = result.data.filename // 4hj25hi32h5i23h6i34hi64h3i6.mp4
// 视频的id截取(要.前面的作为视频的id)
const videoId = filename.substr(0, filename.indexOf('.'))
// base64转buffer
let blob = dataURItoBlob(videoObj.coverUrl)
// 上传数据组合
let coverPic = new FormData()
coverPic.append('videoId', videoId)
coverPic.append('videoCover', blob)
// 请求接口
const resultCover = await upLoadCoverVideoAPI(coverPic)
console.log(resultCover)
}
}
</script>
![]() |
|---|
| uploadCover封面接口调用完成 |
1.8. 返回完整的视频数据
<!-- App.vue -->
<template>
<input type="file" @change="change">
<video style="width: 300px; height: 240px; background: blue;" src="" ref="getVideoDom" @click="playHandler"></video>
<textarea name="" v-model="videoObj.videoDesc" id="" cols="30" rows="10"></textarea>
<button @click="upload">点击上传</button>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { upLoadVideoAPI, upLoadCoverVideoAPI, publishVideoAPI } from '../api/person.js'
/* 数据 - start */
let videoObj = reactive({
videoDesc: ''
})
let getVideoDom = ref(null)
/* 数据 - end */
// 监听用户选择文件的操作
function change(e) {
// e.target.files获取文件信息;e.dataTransfer.files获取拖拽的文件信息
let files = e.target.files || e.dataTransfer.files
// 如果没有选择文件终止后续代码的执行
if (!files.length) return
// 把文件对象信息给到videoObj.realVideo
videoObj.realVideo = files[0]
// 拿到了文件对象
console.log(videoObj.realVideo)
// 上传的文件类型限制
if (!/(mp4)/i.test(videoObj.realVideo.type)) {
alert('上传的视频只能是mp4类型的')
return
}
// 限制文件上传大小
if (videoObj.realVideo.size > (2 * 1024 * 1024)) {
alert('上传的视频不能大于2MB')
return
}
// 该方法返回一个blob视频地址
videoObj.videoUrl = getObjectURL(videoObj.realVideo)
// 修改video src地址
setSrcAndCaptureImage()
}
// 获取视频地址
function getObjectURL(file) {
let url = null
if (window.createObjectURL !== undefined) {
url = window.createObjectURL(file)
} else if (window.URL !== undefined) {
url = window.URL.createObjectURL(file)
} else if (window.webkitURL !== undefined) {
url = window.webkitURL.createObjectURL(file)
}
return url
}
// 设置url和截图
function setSrcAndCaptureImage() {
// 视频地址赋值给video的src属性
getVideoDom.value.src = videoObj.videoUrl
// loadeddata当前帧的数据是可用的
getVideoDom.value.addEventListener('loadeddata', captureImage)
// 截图视频的第一帧作为封面图
function captureImage() {
// 自动播放
getVideoDom.value.play()
let scale = 1
// 创建canvas画布
let canvas = document.createElement('canvas')
// canvas画布大小宽度设置
canvas.width = getVideoDom.value.videoWidth * scale
// canvas画布大小宽度设置
canvas.height = getVideoDom.value.videoHeight * scale
// 开启2D绘图空间 drawImage()方法绘制一张图像
canvas.getContext('2d').drawImage(getVideoDom.value, 0, 0, canvas.width, canvas.height)
// 返回一个包含图片展示的 data URI可以理解为是base64编码的图片
videoObj.coverUrl = canvas.toDataURL('image/png')
console.log(videoObj)
// 移除事件
getVideoDom.value.removeEventListener('loadeddata', captureImage)
}
}
// 视频播放/暂停
function playHandler() {
getVideoDom.value.paused ? getVideoDom.value.play() : getVideoDom.value.pause()
}
// 图片转成Buffer(用来处理二进制的一个接口,后端通过fs模块,读取一张图片获取到图片的文件流)google(上传图片buffer)
function dataURItoBlob(dataURI) {
var byteString = atob(dataURI.split(',')[1])
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]
var ab = new ArrayBuffer(byteString.length)
var ia = new Uint8Array(ab)
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i)
}
return new Blob([ab], { type: mimeString })
}
// 上传方法
async function upload() {
// 判断是否上传了视频
if (!videoObj.realVideo) {
alert('请先选择要上传的文件')
return false
}
// 把文件传递到服务器
let formData = new FormData()
// 后端需要videoPath参数,videoObj.realVideo是视频文件
formData.append('videoPath', videoObj.realVideo)
// 请求上传接口
const result = await upLoadVideoAPI(formData)
// 返回上传后的结果
console.log(result)
if (result.status === 200) {
// 视频文件名
let filename = result.data.filename
// 视频的id截取(要.前面的作为视频的id)
const videoId = filename.substr(0, filename.indexOf('.'))
let blob = dataURItoBlob(videoObj.coverUrl)
let coverPic = new FormData()
coverPic.append('videoId', videoId)
coverPic.append('videoCover', blob)
const resultCover = await upLoadCoverVideoAPI(coverPic)
console.log(resultCover)
console.log(filename.substr(filename.indexOf('.') + 1)) // mp4(其实就是后缀名)
if (resultCover.status === 200) {
const publishResult = await publishVideoAPI({
videoId,
videoCover: `http://43.138.15.137:3000/assets/videoCover/${videoId}.jpg`,
videoPath: `http://43.138.15.137:3000/assets/videoPath/${videoId}.${filename.substr(filename.indexOf('.') + 1)}`,
// videoId代表视频名字;
// videoId后面的.点,就是一个字符串点
// filename.substr(filename.indexOf('.') + 1)运行结果是后缀名(mp4等等)
videoDesc: videoObj.videoDesc
})
console.log(publishResult)
}
}
}
</script>

2. 安全隐患
我们现在可以通过多种方式限制了文件上传的类型
2.1. 使用html的属性
<template>
<input type="file" @change="change" accept=".mp4"> // 多种格式写法.mp4, mov中间加逗号即可
</template>
缺点:可通过浏览器控制台删除 accept 属性,绕过限制
2.2. 使用MIME类型
// 上传的文件类型限制
if(!/(mp4)/i.test(videoObj.realVideo.type)) {
alert('上传的视频只能是mp4类型的')
return
}
缺点:可通过本地修改文件扩展名,绕过限制
如果把任意的垃圾文件后缀名修改成允许上传的类型,这种恶意上传的操作,不仅带来安全隐患,还会造成不必要的性能开销
2.3. 安全性类型校验
在文件二进制数据中,包含着文件签名,是用于标识或验证文件内容的数据,可以通过文件签名来判断文件的类型。这样的签名也被称为魔术数字
文件在内存中的都是以二进制数据的形式保存,包含着文件签名适用于标识或验证文件内容的数据的,可以通过文件签名来判断文件的类型。这样的签名也被称为魔术数字(Magic Numbers)或魔术字节(Magic Bytes)我们一般称为签名或魔数,以 png 文件为例,其 16 进制签名字串为89 50 4E 47 0d 0A 1A 0A,MIME 类型为image/png。我们可以根据这个去判断文件的真实类型
所有文件在计算机中都是以二进制形式进行存储的,但二进制数据是不方便做判断的,我们可以利用 vscode 插件Hex Editor以十六进制的形式查看二进制文件
我们分别查看一下jpg png的十六进制头信息


多打开几个文件试试,你会发现同一种类型的文件,他们的十六制头信息是完全相同的。通过vscode的插件可以查看到该文件头部信息前8个字节为:89 50 4E 47 0D 0A 1A 0A。这其实是 png 图片的头部信息,前8个字节属于 png 图片的头标识,后4个字节为数据域长度,最后4个字节为 png 的 IHDR 标识,是图片宽高等数据流前的第一个数据块。通过前8个字节证明了即使图片后缀为jpg,但文件类型仍然为png,这是无法伪造的。
接下来,我们就可以根据头信息来判断文件类型了
2.4. 常见签名列表
| 文件十六进制签名 | offset | MIME 类型 | 文件后缀 |
|---|---|---|---|
| 25 50 44 46 2D | 0 | application/pdf | |
| FF D8 FF | 0 | image/jpeg | jpg,jpeg |
| 89 50 4E 47 0D 0A 1A 0A | 0 | image/png | png |
| 66 74 79 70 69 73 6F 6D | 4 | video/mp4 | mp4 |
| 47 49 46 38 37 61 | 0 | image/gif | gif |
| 49 44 33 | 0 | audio/mpeg | mp3 |
| 46 4C 56 | 0 | video/x-flv | flv |
| 4F 67 67 53 | 0 | application/ogg | ogg,oga,ogv |
| 52 61 72 21 1A 07 00 | 0 | application/x-rar-compressed | rar |
2.5. FileReader()
通过JS的FileReader方法获取上传文件的头信息,从而进行限制
在JS中通过读取文件的十六进制的头信息,来进行限制,因为相同类型文件的文件头信息是相同的,即使用户修改了文件的后缀名(扩展名),但文件的头信息是不会改变的
FileReader对象允许 Web 应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。 其中File对象可以是来自用户在一个 < input > 元素上选择文件后返回的 FileList 对象,也可以来自拖放操作生成的 DataTransfer 对象,还可以是来自在一个 HTMLCanvasElement 上执行 mozGetAsFile() 方法后返回结果
重要提示: FileReader仅用于以安全的方式从用户(远程)系统读取文件内容,不能用于从文件系统中按路径名简单地读取文件
| FileReader.readAsArrayBuffer() | 开始读取指定的 Blob或File 中的内容。 一旦完成,result 属性中保存的将是被读取文件的 ArrayBuffer数据对象 |
|---|
2.6. 前置知识
2.6.1. 2进制
二进制数是以 0b或者 0B开头,并且所有字符只能包含 0和 1
比如 0101 是二进制数
2.6.2. 8进制
八进制数以 0O或 0开头,并由0~7组成
比如 0266(八进制数)
2.6.3. 10进制
十进制数第一位不是0,除表示正负符号外,要以1-9开头,由0-9组成
比如 -123(十进制数)123(十进制数)
2.6.4. 16进制
十六进制数以0X或0x开头,由0-9,A-F或a-f组成
比如 0X16 、0Xf1(均为16进制数)
2.6.5. 在线转换工具
2.7. 代码实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<input type="file" id="fileInput" onchange="checkFileType()" />
<script>
function checkFileType() {
// 获取文件输入框元素
var fileInput = document.getElementById("fileInput")
// 获取文件对象
var file = fileInput.files[0]
// 创建 FileReader 对象
var reader = new FileReader()
// 读取文件
reader.readAsArrayBuffer(file);
// 文件读取完成时触发
reader.onloadend = function () {
console.log("读取完成", reader.result); // ArrayBuffer,表示一段原始的二进制数据缓冲区,不能直接读写,必须通过视图
//具体操作二进制数据的类型化数组,可通过索引读写
console.log(new Uint8Array(reader.result));
// Uint8Array:8位无符号整数,长度1个字节。(0~255)
// Int8Array:8位有符号整数,长度1个字节。(-128~127)
// [255, 216, 255, 225, 0, 24, 69, 120, 105, 102, 0, 0, 73, 73, 42, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 236, 0, 17, 68, 117, 99, 107, 121, 0, 1, 0, 4, 0, 0, 0, 100, 0, 0, 255, 225, 3, 45, 104, 116, 116, 112, 58, 47, 47, 110, 115, 46, 97, 100, 111, 98, 101, 46, 99, 111, 109, 47, 120, 97, 112, 47, 49, 46, 48, 47, 0, 60, 63, 120, 112, 97, 99, 107, 101, 116, 32, 98, 101, 103, 105, 110, 61, 34, 239, 187, 191, …]
// 获取文件的类型
var type = getFileType(new Uint8Array(reader.result)); //
// 判断文件类型
if (type === "pdf") {
console.log("文件类型为 PDF");
} else if (type === "jpg" || type === "jpeg") {
console.log("文件类型为 JPG/JPEG");
} else if (type === "png") {
console.log("文件类型为 PNG");
} else if (type === "mp4") {
console.log("文件类型为 mp4");
} else {
console.log("文件类型不支持");
}
};
}
function getFileType(bytes) {
// 根据文件头判断文件类型
if (
bytes[0] === 0x25 &&
bytes[1] === 0x50 &&
bytes[2] === 0x44 &&
bytes[3] === 0x46
) {
return "pdf";
} else if (bytes[0] === 0xff && bytes[1] === 0xd8) {
// 16进制的0xff和10进制的255相等,16进制的0xd8和10进制的216相等
return "jpg";
} else if (
bytes[0] === 0x00 &&
bytes[1] === 0x00 &&
bytes[2] === 0x00 &&
bytes[3] === 0x20 &&
bytes[4] === 0x66
) {
return "mp4"
} else if (
bytes[0] === 0x89 &&
bytes[1] === 0x50 &&
bytes[2] === 0x4e &&
bytes[3] === 0x47
) {
return "png";
} else {
return "unknown";
}
}
</script>
</body>
</html>

2.8. 添加了真正类型限制的代码
<!-- App.vue -->
<template>
<input type="file" @change="change" />
<video
style="width: 300px; height: 240px; background: blue"
src=""
ref="getVideoDom"
@click="playHandler"
></video>
<textarea
name=""
v-model="videoObj.videoDesc"
id=""
cols="30"
rows="10"
></textarea>
<button @click="upload">点击上传</button>
</template>
<script setup>
import { reactive, ref } from "vue";
import {
upLoadVideoAPI,
upLoadCoverVideoAPI,
publishVideoAPI,
} from "../api/person.js";
/* 数据 - start */
let videoObj = reactive({
videoDesc: "",
});
let getVideoDom = ref(null);
/* 数据 - end */
// 文件类型检测
function checkFileType(file) {
// 创建 FileReader 对象
var reader = new FileReader();
// 读取文件
reader.readAsArrayBuffer(file);
// 文件读取完成时触发
reader.onloadend = function () {
// 获取文件的类型
var type = getFileType(new Uint8Array(reader.result));
// 判断文件类型
if (type !== "mp4") {
alert("文件类型不是mp4格式,别想糊弄我");
}
};
}
// 根据文件头判断文件类型
function getFileType(bytes) {
if (
bytes[0] === 0x00 &&
bytes[1] === 0x00 &&
bytes[2] === 0x00 &&
bytes[3] === 0x20 &&
bytes[4] === 0x66
) {
return "mp4";
} else {
return "unknown";
}
}
// 监听用户选择文件的操作
function change(e) {
// e.target.files获取文件信息;e.dataTransfer.files获取拖拽的文件信息
let files = e.target.files || e.dataTransfer.files;
// 如果没有选择文件终止后续代码的执行
if (!files.length) return;
// 把文件对象信息给到videoObj.realVideo
videoObj.realVideo = files[0];
// 判断选中的文件类型
checkFileType(videoObj.realVideo);
// 限制文件上传大小
if (videoObj.realVideo.size > 10 * 1024 * 1024) {
alert("上传的视频不能大于2MB");
return;
}
// 该方法返回一个blob视频地址
videoObj.videoUrl = getObjectURL(videoObj.realVideo);
// 修改video src地址
setSrcAndCaptureImage();
}
// 获取视频地址
function getObjectURL(file) {
let url = null;
if (window.createObjectURL !== undefined) {
url = window.createObjectURL(file);
} else if (window.URL !== undefined) {
url = window.URL.createObjectURL(file);
} else if (window.webkitURL !== undefined) {
url = window.webkitURL.createObjectURL(file);
}
return url;
}
// 设置url和截图
function setSrcAndCaptureImage() {
// 视频地址赋值给video的src属性
getVideoDom.value.src = videoObj.videoUrl;
// loadeddata当前帧的数据是可用的
getVideoDom.value.addEventListener("loadeddata", captureImage);
// 截图视频的第一帧作为封面图
function captureImage() {
// 自动播放
getVideoDom.value.play();
let scale = 1;
// 创建canvas画布
let canvas = document.createElement("canvas");
// canvas画布大小宽度设置
canvas.width = getVideoDom.value.videoWidth * scale;
// canvas画布大小宽度设置
canvas.height = getVideoDom.value.videoHeight * scale;
// 开启2D绘图空间 drawImage()方法绘制一张图像
canvas
.getContext("2d")
.drawImage(getVideoDom.value, 0, 0, canvas.width, canvas.height);
// 返回一个包含图片展示的 data URI可以理解为是base64编码的图片
videoObj.coverUrl = canvas.toDataURL("image/png");
console.log(videoObj);
// 移除事件
getVideoDom.value.removeEventListener("loadeddata", captureImage);
}
}
// 视频播放/暂停
function playHandler() {
getVideoDom.value.paused
? getVideoDom.value.play()
: getVideoDom.value.pause();
}
// 图片转成Buffer(用来处理二进制的一个接口,后端通过fs模块,读取一张图片获取到图片的文件流)google(上传图片buffer)
function dataURItoBlob(dataURI) {
var byteString = atob(dataURI.split(",")[1]);
var mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0];
var ab = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(ab);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ab], { type: mimeString });
}
// 上传方法
async function upload() {
// 判断是否上传了视频
if (!videoObj.realVideo) {
alert("请先选择要上传的文件");
return false;
}
// 把文件传递到服务器
let formData = new FormData();
// 后端需要videoPath参数,videoObj.realVideo是视频文件
formData.append("videoPath", videoObj.realVideo);
// 请求上传接口
const result = await upLoadVideoAPI(formData);
// 返回上传后的结果
console.log(result);
if (result.status === 200) {
// 视频文件名
let filename = result.data.filename;
// 视频的id截取(要.前面的作为视频的id)
const videoId = filename.substr(0, filename.indexOf("."));
let blob = dataURItoBlob(videoObj.coverUrl);
let coverPic = new FormData();
coverPic.append("videoId", videoId);
coverPic.append("videoCover", blob);
const resultCover = await upLoadCoverVideoAPI(coverPic);
console.log(resultCover);
console.log(filename.substr(filename.indexOf(".") + 1)); // mp4(其实就是后缀名)
if (resultCover.status === 200) {
const publishResult = await publishVideoAPI({
videoId,
videoCover: `http://43.138.15.137:3000/assets/videoCover/${videoId}.jpg`,
videoPath: `http://43.138.15.137:3000/assets/videoPath/${videoId}.${filename.substr(
filename.indexOf(".") + 1
)}`,
// videoId代表视频名字;
// videoId后面的.点,就是一个字符串点
// filename.substr(filename.indexOf('.') + 1)运行结果是后缀名(mp4等等)
videoDesc: videoObj.videoDesc,
});
console.log(publishResult);
}
}
}
</script>
2.9. file-type包
file-type工具包也是用Js来进行封装的,其原理和上面的方式一样,它支持的文件类型有几十种,几乎能满足我们的日常开发需求
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐



所有评论(0)