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. 上传视频类型的限制

  1. 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>
  1. input accept属性配置限制
<!-- 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. 上传前的预览

  1. video标签
<video style="width: 300px; height: 240px; background: blue;" src=""></video>
  1. 拿到视频地址
<!-- 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>
  1. 修改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>
  1. 播放视频
<!-- 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封面接口调用完成
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. 常见签名列表

完整签名可参考wikipedia和whatwg

文件十六进制签名offsetMIME 类型文件后缀
25 50 44 46 2D0application/pdfpdf
FF D8 FF0image/jpegjpg,jpeg
89 50 4E 47 0D 0A 1A 0A0image/pngpng
66 74 79 70 69 73 6F 6D4video/mp4mp4
47 49 46 38 37 610image/gifgif
49 44 330audio/mpegmp3
46 4C 560video/x-flvflv
4F 67 67 530application/oggogg,oga,ogv
52 61 72 21 1A 07 000application/x-rar-compressedrar

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来进行封装的,其原理和上面的方式一样,它支持的文件类型有几十种,几乎能满足我们的日常开发需求

file-type

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐