视频保存在本地时的前端优化方案

89 阅读3分钟

开发中若是碰到比较大的视频因为一些特殊原因无法放在服务器上,而不得不保存在前端本地时,可以通过ffmpeg压缩后再根据video标签的一些原生属性来优化用户体验。

安装ffmpeg

windows系统安装地址:www.gyan.dev/ffmpeg/buil…
其他系统可自行在官网查阅:ffmpeg.org/download.ht…
安装完成后在系统环境变量中配置解压后的bin文件夹的路径,配置完成后输入”ffmpeg -version"来校验是否安装成功。

使用ffmpeg

在powershell中将视频转换为WebM格式(VP9编码,兼容现代浏览器)

ffmpeg -i "desktop\input.mp4" -c:v libvpx-vp9 -crf 30 -b:v 0 -c:a libopus "desktop\output.webm"

此处我是将视频保存在了桌面,且输出后的视频也是直接保存在了桌面。

ffmpeg的一些其他实用方法:

降低视频分辨率:
ffmpeg -i "D:\input.mp4" -vf "scale=-1:720" -c:v libx264 -crf 23 "D:\output_720p.mp4"

裁剪视频时长:
ffmpeg -i "D:\input.mp4" -ss 00:00:00 -to 00:05:00 -c copy "D:\output_cut.mp4"

Vue项目中引入视频

<div class="video">
    <div class="loading" v-if="loading">
      <span class="text" style="--i: 1"></span>
      <span class="text" style="--i: 2"></span>
      <span class="text" style="--i: 3"></span>
      <span class="text" style="--i: 4"></span>
      <span class="text" style="--i: 5"></span>
      <span class="text" style="--i: 6">·</span>
      <span class="text" style="--i: 7">·</span>
      <span class="text" style="--i: 8">·</span>
      <span>{{ pecent }}</span>
    </div>
    <video
      ref="videoRef"
      src="./output.webm"
      muted
      autoplay
      loop
      @playing="onPlaying"
      @waiting="onWaiting"
    ></video>
  </div>
  
  <script setup>
import { onBeforeMount, onMounted, ref } from "vue";

const loading = ref(true);
const videoRef = ref(null);

function onPlaying() {
  loading.value = false;
  startBufferCheck();
}

function onWaiting() {
  loading.value = true;
}

let lastSpeedUpdate = ref(Date.now());
let lastBuffered = ref(0);
let speedKbps = ref(10);

// 动态计算阈值
function calculateNetworkSpeed() {
  const now = Date.now();
  if (!videoRef.value?.buffered.length) return;

  const bufferedNow = videoRef.value.buffered.end(0);
  const timeDelta = (now - lastSpeedUpdate.value) / 1000; // 转换为秒
  const bytesDelta = bufferedNow - lastBuffered.value;

  // 每秒可以缓冲的视频时长
  speedKbps.value = bytesDelta / timeDelta;
  if (speedKbps.value < 1) {
    speedKbps.value = 1;
  }
  lastSpeedUpdate.value = now;
  lastBuffered.value = bufferedNow;
}

let checkInterval = ref(null);
function startBufferCheck() {
  if (checkInterval.value) clearInterval(checkInterval.value);
  checkInterval.value = setInterval(() => {
    if (!videoRef.value.buffered.length || videoRef.value.paused) return;
    // 获取当前缓冲区的末尾时间
    const bufferedEnd = videoRef.value.buffered.end(0);
    // 此时视频已经完全加载完毕
    if (bufferedEnd === videoRef.value.duration) {
      clearInterval(checkInterval.value);
      loading.value = false;
      return;
    }
    const currentTime = videoRef.value.currentTime;
    // 如果播放位置接近缓冲区末尾,暂停并等待加载
    if (bufferedEnd - currentTime < speedKbps.value) {
      loading.value = true;
      videoRef.value.pause();
      console.log("缓冲区不足,暂停播放");
      calculateNetworkSpeed();
      
      function checkResume() {
        if (videoRef.value.buffered.end(0) - currentTime >= speedKbps.value) {
          // 如果缓冲区已经超过缓冲区阈值,继续播放
          videoRef.value.play();
          loading.value = false;
        } else {
          // 继续等待加载
          requestAnimationFrame(checkResume);
        }
      }

      checkResume();
    } else {
      loading.value = false;
    }
  }, 1000); // 每秒检查一次
}

let pecent = ref(null);
onMounted(() => {
  videoRef.value.addEventListener("progress", () => {
    // 视频已缓冲时间范围
    if (videoRef.value.buffered.length > 0) {
      const bufferedEnd = videoRef.value.buffered.end(
        videoRef.value.buffered.length - 1
      );
      // 返回当前视频的长度,以秒为单位
      const duration = videoRef.value.duration;
      pecent.value = ((bufferedEnd / duration) * 100).toFixed(2) + "%";
    }
  });
});

onBeforeMount(() => {
  clearInterval(checkInterval.value);
});
</script>
  
  <style scoped lang="less">
  @keyframes upDown {
   0% {
     transform: translateY(0);
   }

   20% {
     transform: translateY(-12px);
   }

   40%,
   100% {
     transform: translateY(0);
   }
 }

 .video {
   width: 100%;
   height: 100%;
   position: relative;

   .loading {
     position: absolute;
     top: 50%;
     left: 50%;
     transform: translate(-50%, -50%);
     font-size: 20px;
     letter-spacing: 5px;
     color: #fff;

     .text {
       display: inline-block;
       animation: upDown 1.5s ease-in-out infinite;
       animation-delay: calc(0.1s * var(--i));
     }
   }
 }
</style>

video标签:
playing事件:在视频因缓冲而暂停或就绪后触发;
waiting事件:在视频需要缓冲下一帧而停止时触发;
duration:当前视频的长度,以秒为单位;
buffered:视频的缓冲范围;

代码解析:
视频加载每次至少提前缓冲10秒的内容,若已缓冲视频时长 - 当前视频正在播放的秒数小于缓冲阈值时,唤起loading效果并暂停视频,否则的话则继续播放视频。当视频缓冲时长等于视频总时长时,移除loading效果。不过这样做有一个缺陷就是当缓冲视频的速度小于定时器速度时,容易频繁唤起loading效果。因此动态处理下阈值,根据缓冲的速度来给阈值赋值。

当然,最好的办法还是直接将视频存到服务器上😅