demo.vue
1<template> 2 <div> 3 <div> 4 <video ref="videoRef" :src="theUrl" controls autoplay muted crossOrigin="anonymous"></video> 5 6 <!-- <div class="controls"> 7 <button :disabled="isRecording" @click="startCapture"> 8 开始录制 9 </button> 10 <button :disabled="!isRecording" @click="stopCapture"> 11 停止录制 12 </button> 13 </div> --> 14 15 <!-- <div v-if="gifUrl" class="output"> 16 <h3>录制的GIF:</h3> 17 <img :src="gifUrl" alt="GIF 预览" /> 18 </div> --> 19 </div> 20 </div> 21</template> 22 23<script setup> 24import request from '/@/utils/request'; 25 26import GIF from 'gif.js' 27 28const emit = defineEmits(['sendGif']); 29 30const props = defineProps({ 31 fileUrl: { 32 type: String, 33 default: '' 34 } 35}) 36 37let theUrl = ref('') 38let timerId = ref(null) // 新增定时器ID 39 40// 监听视频URL变化 41watch(() => props.fileUrl, (newVal) => { 42 theUrl.value = newVal 43 nextTick(() => { 44 if (videoRef.value) { 45 // 清除之前的监听 46 videoRef.value.removeEventListener('loadeddata', handleVideoLoaded) 47 // 添加新的监听 48 videoRef.value.addEventListener('loadeddata', handleVideoLoaded) 49 } 50 }) 51}) 52 53// 视频加载完成后自动开始录制 54const handleVideoLoaded = () => { 55 console.log('视频加载完成,开始录制') 56 57 // 视频从 20秒 开始播放,即:从20秒开始截取 58 videoRef.value.currentTime = 20; 59 60 startCapture() 61 // 移除监听,避免重复调用 62 videoRef.value.removeEventListener('loadeddata', handleVideoLoaded) 63} 64 65const videoRef = ref(null) 66const isRecording = ref(false) 67const progress = ref(0) 68const gifUrl = ref(null) 69 70let gifRecorder = null 71let captureInterval = null 72 73let gifBlob = ref(null); 74 75// 初始化GIF录制器 76const initGifRecorder = () => { 77 if (gifRecorder) { 78 gifRecorder.abort() 79 } 80 gifRecorder = new GIF({ 81 workers: 2, 82 quality: 10, 83 width: videoRef.value?.videoWidth || 320, 84 height: videoRef.value?.videoHeight || 240, 85 workerScript: '/gif.worker.js' 86 }) 87 88 gifRecorder.on('progress', p => { 89 progress.value = Math.round(p * 100) 90 }) 91 92 gifRecorder.on('finished', blob => { 93 gifUrl.value = URL.createObjectURL(blob) 94 isRecording.value = false 95 progress.value = 0 96 console.log('gifUrl=', gifUrl.value); 97 98 // emit('sendGif', gifUrl.value) 99 100 //#region Blob转Base64 101 // const reader = new FileReader(); 102 // reader.onload = () => { 103 // // const base64Data = reader.result.replace(/^data:image\/\w+;base64,/, ''); 104 // // emit('sendGif', base64Data); 105 106 // const base64Data = reader.result; 107 // // console.log('base64Data=', base64Data); 108 // emit('sendGif', base64Data); 109 // }; 110 // reader.onerror = (e) => { 111 // console.error('Base64转换失败:', e); 112 // emit('sendGif', null); // 发送失败信号 113 // }; 114 // reader.readAsDataURL(blob); 115 //#endregion 116 117 //#region Blob转换为file文件 118 gifBlob.value = blob; 119 120 let file = new File( 121 [gifBlob.value], 122 `recording-${Date.now()}.gif`, 123 { type: 'image/gif' } 124 ); 125 console.log('生成的file=', file); 126 uploadGif(file) 127 }) 128} 129 130// 捕获帧 131const captureFrame = () => { 132 const canvas = document.createElement('canvas') 133 const ctx = canvas.getContext('2d') 134 135 // 跨域安全验证 136 if (!videoRef.value || !videoRef.value.crossOrigin) { 137 console.error('视频元素未配置跨域访问!') 138 return 139 } 140 141 canvas.width = videoRef.value.videoWidth 142 canvas.height = videoRef.value.videoHeight 143 ctx.drawImage(videoRef.value, 0, 0) 144 145 gifRecorder.addFrame(canvas, { 146 delay: 100, 147 copy: true 148 }) 149} 150 151// 开始录制 152const startCapture = () => { 153 if (!videoRef.value || isRecording.value) return 154 155 initGifRecorder() 156 isRecording.value = true 157 158 // 每100ms捕获一帧 159 captureInterval = setInterval(captureFrame, 100) 160 161 // 设置5秒后自动停止录制 162 if (timerId.value) { 163 clearTimeout(timerId.value) 164 } 165 timerId.value = setTimeout(() => { 166 stopCapture() 167 timerId.value = null 168 }, 5000) 169} 170 171// 停止录制 172const stopCapture = () => { 173 if (!isRecording.value) return 174 175 clearInterval(captureInterval) 176 gifRecorder.render() 177 console.log('录制结束,生成GIF') 178 179 // 清除定时器 180 if (timerId.value) { 181 clearTimeout(timerId.value) 182 timerId.value = null 183 } 184} 185 186// 清理资源 187onUnmounted(() => { 188 if (gifRecorder) { 189 gifRecorder.abort() 190 } 191 clearInterval(captureInterval) 192 if (timerId.value) { 193 clearTimeout(timerId.value) 194 } 195}) 196 197// 上传接口 198const uploadGif = async (file) => { 199 const { data } = await request({ 200 url: '/admin/sys-file/upload', 201 method: 'post', 202 headers: { 203 'Content-Type': 'multipart/form-data', 204 'Enc-Flag': 'false', 205 }, 206 data: { file }, 207 }); 208 209 console.log('data=', data); 210 if (data) { 211 emit('sendGif', data.url); 212 } 213} 214</script> 215 216<style scoped> 217.controls { 218 margin: 1rem 0; 219 display: flex; 220 gap: 1rem; 221} 222 223.progress { 224 margin-top: 1rem; 225} 226 227.output { 228 margin-top: 1rem; 229} 230 231.frame img { 232 width: 100px; 233 height: auto; 234} 235</style> 236 237
父组件使用 demo.vue
1这是随便一个上传组件 2<upload-file 3 @sendFile="getFile" 4 v-model="scope.row.itemPath" 5 :type="'simple'" 6 :fileType="['mp4']" 7/> 8 9<demo :fileUrl="theFileUrl" @sendGif="getGifUrl"></demo> 10 11let theFileUrl = ref('') 12const getFile = (e) => { 13 if(form.itemAddDTOS.length == 1){ 14 const file = e.raw; 15 const blob = new Blob([file], { type: file.type }); 16 const blobUrl = URL.createObjectURL(blob); 17 theFileUrl.value = blobUrl 18 subBtnFlag.value = true; 19 } 20} 21 22const getGifUrl = (e) => { 23 form.cover = e 24 console.log('e=', e); 25 subBtnFlag.value = false; 26} 27
《vue.js 视频截取为 gif - 2(将截取到的gif 转换为base64 、file)》 是转载文章,点击查看原文。
