vue.js 视频截取为 gif - 2(将截取到的gif 转换为base64 、file)

demo.vue

javascript 复制代码
<template>
  <div>
    <div>
      <video ref="videoRef" :src="theUrl" controls autoplay muted crossOrigin="anonymous"></video>

      <!-- <div class="controls">
        <button :disabled="isRecording" @click="startCapture">
          开始录制
        </button>
        <button :disabled="!isRecording" @click="stopCapture">
          停止录制
        </button>
      </div> -->

      <!-- <div v-if="gifUrl" class="output">
        <h3>录制的GIF:</h3>
        <img :src="gifUrl" alt="GIF 预览" />
      </div> -->
    </div>
  </div>
</template>

<script setup>
import request from '/@/utils/request';

import GIF from 'gif.js'

const emit = defineEmits(['sendGif']);

const props = defineProps({
  fileUrl: {
    type: String,
    default: ''
  }
})

let theUrl = ref('')
let timerId = ref(null) // 新增定时器ID

// 监听视频URL变化
watch(() => props.fileUrl, (newVal) => {
  theUrl.value = newVal
  nextTick(() => {
    if (videoRef.value) {
      // 清除之前的监听
      videoRef.value.removeEventListener('loadeddata', handleVideoLoaded)
      // 添加新的监听
      videoRef.value.addEventListener('loadeddata', handleVideoLoaded)
    }
  })
})

// 视频加载完成后自动开始录制
const handleVideoLoaded = () => {
  console.log('视频加载完成,开始录制')

  // 视频从 20秒 开始播放,即:从20秒开始截取
  videoRef.value.currentTime = 20;

  startCapture()
  // 移除监听,避免重复调用
  videoRef.value.removeEventListener('loadeddata', handleVideoLoaded)
}

const videoRef = ref(null)
const isRecording = ref(false)
const progress = ref(0)
const gifUrl = ref(null)

let gifRecorder = null
let captureInterval = null

let gifBlob = ref(null);

// 初始化GIF录制器
const initGifRecorder = () => {
  if (gifRecorder) {
    gifRecorder.abort()
  }
  gifRecorder = new GIF({
    workers: 2,
    quality: 10,
    width: videoRef.value?.videoWidth || 320,
    height: videoRef.value?.videoHeight || 240,
    workerScript: '/gif.worker.js'
  })

  gifRecorder.on('progress', p => {
    progress.value = Math.round(p * 100)
  })

  gifRecorder.on('finished', blob => {
    gifUrl.value = URL.createObjectURL(blob)
    isRecording.value = false
    progress.value = 0
    console.log('gifUrl=', gifUrl.value);

    // emit('sendGif', gifUrl.value)

    //#region Blob转Base64
    // const reader = new FileReader();
    // reader.onload = () => {
    //   // const base64Data = reader.result.replace(/^data:image\/\w+;base64,/, '');
    //   // emit('sendGif', base64Data);

    //   const base64Data = reader.result;
    //   // console.log('base64Data=', base64Data);
    //   emit('sendGif', base64Data);
    // };
    // reader.onerror = (e) => {
    //   console.error('Base64转换失败:', e);
    //   emit('sendGif', null); // 发送失败信号
    // };
    // reader.readAsDataURL(blob);
    //#endregion

    //#region Blob转换为file文件
    gifBlob.value = blob;

    let file = new File(
      [gifBlob.value],
      `recording-${Date.now()}.gif`,
      { type: 'image/gif' }
    );
    console.log('生成的file=', file);
    uploadGif(file)
  })
}

// 捕获帧
const captureFrame = () => {
  const canvas = document.createElement('canvas')
  const ctx = canvas.getContext('2d')

  // 跨域安全验证
  if (!videoRef.value || !videoRef.value.crossOrigin) {
    console.error('视频元素未配置跨域访问!')
    return
  }

  canvas.width = videoRef.value.videoWidth
  canvas.height = videoRef.value.videoHeight
  ctx.drawImage(videoRef.value, 0, 0)

  gifRecorder.addFrame(canvas, {
    delay: 100,
    copy: true
  })
}

// 开始录制
const startCapture = () => {
  if (!videoRef.value || isRecording.value) return

  initGifRecorder()
  isRecording.value = true

  // 每100ms捕获一帧
  captureInterval = setInterval(captureFrame, 100)

  // 设置5秒后自动停止录制
  if (timerId.value) {
    clearTimeout(timerId.value)
  }
  timerId.value = setTimeout(() => {
    stopCapture()
    timerId.value = null
  }, 5000)
}

// 停止录制
const stopCapture = () => {
  if (!isRecording.value) return

  clearInterval(captureInterval)
  gifRecorder.render()
  console.log('录制结束,生成GIF')

  // 清除定时器
  if (timerId.value) {
    clearTimeout(timerId.value)
    timerId.value = null
  }
}

// 清理资源
onUnmounted(() => {
  if (gifRecorder) {
    gifRecorder.abort()
  }
  clearInterval(captureInterval)
  if (timerId.value) {
    clearTimeout(timerId.value)
  }
})

// 上传接口
const uploadGif = async (file) => {
  const { data } = await request({
    url: '/admin/sys-file/upload',
    method: 'post',
    headers: {
      'Content-Type': 'multipart/form-data',
      'Enc-Flag': 'false',
    },
    data: { file },
  });

  console.log('data=', data);
  if (data) {
    emit('sendGif', data.url);
  }
}
</script>

<style scoped>
.controls {
  margin: 1rem 0;
  display: flex;
  gap: 1rem;
}

.progress {
  margin-top: 1rem;
}

.output {
  margin-top: 1rem;
}

.frame img {
  width: 100px;
  height: auto;
}
</style>

父组件使用 demo.vue

javascript 复制代码
这是随便一个上传组件
<upload-file
	@sendFile="getFile"
	v-model="scope.row.itemPath"
	:type="'simple'"
	:fileType="['mp4']"
/>

<demo :fileUrl="theFileUrl" @sendGif="getGifUrl"></demo>

let theFileUrl = ref('')
const getFile = (e) => {
	if(form.itemAddDTOS.length == 1){
		const file = e.raw;
		const blob = new Blob([file], { type: file.type });
		const blobUrl = URL.createObjectURL(blob);
		theFileUrl.value = blobUrl
		subBtnFlag.value = true;
	}
}

const getGifUrl = (e) => {
	form.cover = e
	console.log('e=', e);
	subBtnFlag.value = false;
}
相关推荐
你的电影很有趣6 小时前
lesson73:Vue渐进式框架的进化之路——组合式API、选项式对比与响应式新范式
javascript·vue.js
小张成长计划..6 小时前
VUE工程化开发模式
前端·javascript·vue.js
菜鸟una8 小时前
【微信小程序 + map组件】自定义地图气泡?原生气泡?如何抉择?
前端·vue.js·程序人生·微信小程序·小程序·typescript
岁月宁静15 小时前
深度定制:在 Vue 3.5 应用中集成流式 AI 写作助手的实践
前端·vue.js·人工智能
百锦再16 小时前
Vue Scoped样式混淆问题详解与解决方案
java·前端·javascript·数据库·vue.js·学习·.net
Sheldon一蓑烟雨任平生17 小时前
Vue3 表单输入绑定
vue.js·vue3·v-model·vue3 表单输入绑定·表单输入绑定·input和change区别·vue3 双向数据绑定
YUELEI11819 小时前
Vue 安装依赖的集合和小知识
javascript·vue.js·ecmascript
前端付豪20 小时前
万事从 todolist 开始
前端·vue.js·前端框架
华仔啊21 小时前
别再纠结Pinia和Vuex了!一篇文章彻底搞懂区别与选择
前端·vue.js