卡卷网
当前位置:卡卷网 / 每日看点 / 正文

各位大神,vue怎么加流动字幕?

作者:卡卷网发布时间:2025-05-07 22:53浏览数量:16次评论数量:0次

他们朝我扔泥巴,我拿泥巴种荷花;他们朝我扔巴巴,我用巴巴敲代码,哦哦哦哦哦...

需求描述

  • 有一个MP3音频文件,在播放的时候,需要展示对应的字幕给到用户
  • 即为需要做到视频和音频同步的效果
  • 如下效果图
  • 演示地址:
ashuai.work:8890/19

各位大神,vue怎么加流动字幕?  第1张

字幕文件的种类

常见的字幕文件,有三种

1. SRT格式(SubRip Subtitle)

最常见的字幕格式,包含了字幕文本、显示时间(开始和结束时间),文件结构简单、易于创建,如下简单示例:

1 00:00:01,000 --> 00:00:04,000 你好这个世界 2 00:00:05,000 --> 00:00:08,000 这个世界你好

2. VVT格式(WebVTT,Web Video Text Tracks)

HTML网页专属,前端最常用,支持HTML5视频元素。与SRT类似,但具有更多的功能,如HTML标签、文本样式和位置。如下简单示例:

WEBVTT 00:00:00.100 --> 00:00:02.175 不必说碧绿的菜畦 00:00:02.125 --> 00:00:03.850 光滑的石井栏

3. ASS格式(Advanced SubStation Alpha)

比较复杂的字幕文件,支持更多的样式和特效,如字体、颜色、位置等,常用于高质量的视频或动画字幕。用的少,如下示例:

[Script Info] Title: Example Subtitle Original Script: John Doe ScriptType: v4.00+ [Events] Dialogue: 0,0:00:01.00,0:00:05.00,Default,,0,0,0,,Hello, how are you?

就前端而言,VVT用的最多,因此本篇文章,我们以VVT来讲解

首先来一份字幕文件

字幕文件如何获取

  • 这里笔者推荐一些在线网站
  • 可以直接把纯人声音频或者视频转出一个字幕文件
  • 比如这个熊猫字幕:在线字幕自动生成工具_字幕制作_语音转字幕-熊猫字幕 (pdsub.com)
另外,可能部分道友会遇到想要把文本转语音的同时,再生成对应的字幕,后续笔者也会出一篇TTS文章,敬请期待...

示例VVT字幕

WEBVTT 00:00:00.100 --> 00:00:02.175 不必说碧绿的菜畦 00:00:02.125 --> 00:00:03.850 光滑的石井栏 00:00:03.850 --> 00:00:05.713 高大的皂荚树 00:00:05.713 --> 00:00:07.287 紫红的桑葚 00:00:07.287 --> 00:00:10.350 也不必说鸣蝉在树叶里长吟 00:00:10.350 --> 00:00:13.062 肥胖的黄蜂伏在菜花上 00:00:13.062 --> 00:00:18.488 轻捷的叫天子云雀忽然从草间直窜向云霄里去了 00:00:18.488 --> 00:00:21.000 单是周围的短短的泥墙根一带 00:00:21.000 --> 00:00:22.738 就有无限趣味 00:00:22.738 --> 00:00:24.438 油蛉在这里低唱 00:00:24.438 --> 00:00:26.613 蟋蟀们在这里弹琴 00:00:26.613 --> 00:00:28.337 翻开断砖来 00:00:28.337 --> 00:00:30.113 有时会遇见蜈蚣 00:00:30.113 --> 00:00:31.488 还有斑蝥 00:00:31.488 --> 00:00:33.950 倘若用手指按住它的脊梁 00:00:33.950 --> 00:00:35.625 便会的一声 00:00:35.625 --> 00:00:38.175 从后窍喷出一阵烟雾

一、audio标签形式之读取并加工展示字幕

1. 读取字幕

  • 这里把字幕文件,放在public文件夹下
  • 再使用fetch去得到对应字幕文件内容

onMounted(() => { getVvtData(); }); const getVvtData = async () => { // 获取当前字幕文件的路径 const vvtUrl = new URL("/subtitles/1.vvt", import.meta.url).href; // 使用fetch请求,此路径下的字幕文件 const response = await fetch(vvtUrl); // 状态判断 if (!response.ok) throw new Error("网络错误或文件不存在"); // 拿到字幕数据转成的文本 const vvtData = await response.text(); // 使用正则将字幕文件加工成JSON格式 subtitles.value = parseVvtData(vvtData); };

字幕文本不能直接使用,所以我们需要将其转成对象形式,才方便使用

2. 解析并加工成对象形式

// 解析字幕文件并将其转换为 JSON const parseVvtData = (data) => { const subtitlePattern = /(\d{2}:\d{2}:\d{2}\.\d{3}) --> (\d{2}:\d{2}:\d{2}\.\d{3})\s*([\s\S]+?)(?=\n\d{2}:\d{2}:\d{2}\.\d{3}|$)/g; let matches; const parsedSubtitles = []; while ((matches = subtitlePattern.exec(data)) !== null) { const start = convertTimeToSeconds(matches[1]); const end = convertTimeToSeconds(matches[2]); const text = matches[3].trim(); parsedSubtitles.push({ start, end, text, }); } return parsedSubtitles; }; // 将字幕时间从字符串"00:00:05.000" 格式转换为秒数数字 const convertTimeToSeconds = (timeStr) => { const [hours, minutes, seconds] = timeStr.split(":"); const [sec, ms] = seconds.split("."); return ( parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(sec) + parseInt(ms) / 1000 ); };

加工完毕以后,能得到这样的字幕数组对象,如下:

各位大神,vue怎么加流动字幕?  第2张

  • 即为,数组中每一项,都是一条字幕对象
  • 字幕对象记录了字幕开始时间,字幕结束时间,以及在开始结束时间之间,需要呈现的字幕文字
  • 这样的话,我们就可以在对应时间节点,展示对应字幕即可

3. 音频播放的时候,根据时间,找到对应的字幕展示即可

当音频播放的时候,audio标签,自带的timeupdate事件,可以拿到当前播放的时间是什么时间节点

<audio ref="myAudioRef" @timeupdate="timeupdate" controls :src="mp3"></audio> // 展示字幕的div <div v-if="currentSubtitle">{{ currentSubtitle.text }}</div> const timeupdate = (e) => { // 当前音频播放的时间 currentTime.value = e.target.currentTime; updateSubtitle(currentTime.value); }; // 根据当前时间戳更新显示的字幕 const updateSubtitle = (curTime) => { // 根据播放的时间,找到当前播放的是哪一项 const subtitle = subtitles.value.find( // 当前时间,大于字幕开始,小于字幕结束 (sub) => curTime >= sub.start && curTime <= sub.end ); // 找到对应字幕项 currentSubtitle.value = subtitle || null; };

4. 完整代码(单行字幕播放)

至于多行字幕,就是循环不断往后拼接即可,这里不赘述

<template> <div class="boxA"> <h3>音频播放字幕同步出现——只显示单条</h3> <audio ref="myAudioRef" @timeupdate="timeupdate" controls :src="mp3"></audio> <div v-if="currentSubtitle">{{ currentSubtitle.text }}</div> </div> </template> <script setup> import { ref, onMounted } from "vue"; import mp3 from "./1.mp3"; const myAudioRef = ref(); const currentTime = ref(); const subtitles = ref(); // 所有字幕数据 const currentSubtitle = ref(); // 当前显示的字幕项 onMounted(() => { getVvtData(); }); const getVvtData = async () => { // 获取当前字幕文件的路径 const vvtUrl = new URL("/subtitles/1.vvt", import.meta.url).href; // 使用fetch请求,此路径下的字幕文件 const response = await fetch(vvtUrl); // 状态判断 if (!response.ok) throw new Error("网络错误或文件不存在"); // 拿到字幕数据转成的文本 const vvtData = await response.text(); // 使用正则将字幕文件加工成JSON格式 subtitles.value = parseVvtData(vvtData); }; // 解析字幕文件并将其转换为 JSON const parseVvtData = (data) => { const subtitlePattern = /(\d{2}:\d{2}:\d{2}\.\d{3}) --> (\d{2}:\d{2}:\d{2}\.\d{3})\s*([\s\S]+?)(?=\n\d{2}:\d{2}:\d{2}\.\d{3}|$)/g; let matches; const parsedSubtitles = []; while ((matches = subtitlePattern.exec(data)) !== null) { const start = convertTimeToSeconds(matches[1]); const end = convertTimeToSeconds(matches[2]); const text = matches[3].trim(); parsedSubtitles.push({ start, end, text, }); } return parsedSubtitles; }; // 将字幕时间从字符串"00:00:05.000" 格式转换为秒数数字 const convertTimeToSeconds = (timeStr) => { const [hours, minutes, seconds] = timeStr.split(":"); const [sec, ms] = seconds.split("."); return ( parseInt(hours) * 3600 + parseInt(minutes) * 60 + parseInt(sec) + parseInt(ms) / 1000 ); }; const timeupdate = (e) => { currentTime.value = e.target.currentTime; updateSubtitle(currentTime.value); }; // 根据当前时间戳更新显示的字幕 const updateSubtitle = (curTime) => { // 根据播放的时间,找到当前播放的是哪一项 const subtitle = subtitles.value.find( // 当前时间,大于字幕开始,小于字幕结束 (sub) => curTime >= sub.start && curTime <= sub.end ); currentSubtitle.value = subtitle || null; }; </script> <style lang="less" scoped> .boxA { height: 160px; } </style>

某些情况下,我们不能使用audio标签来播放音频,这个时候,就需要使用另外一种方式:window.AudioContext 去实例化一个音频播放器,q去对应播放音频,如下

二、AudioContext之读取并加工展示字幕

1. 读取字幕并加工字幕

  • 原理很简单,和上述的读取字幕一样,这里不赘述
  • 也是把public文件夹中字幕文件读取并解析
  • 最后得到字幕数组对象
  • 在AudioContext播放音频的时候,使用一个定时器,或者requestAnimationFrame之类的
  • 不断查找当前时间对应的字幕数据,直接展示到页面上

2. 当点击按钮时,播放音频且用定时器,查找字幕数组中的对应文件

如下html结构

<template> <div class="boxA"> <button @click="play">播放音频</button> <!-- 循环出字幕内容 --> <div v-if="displayedSubtitles.length"> <p v-for="(subtitle, index) in displayedSubtitles" :key="index">{{ subtitle }}</p> </div> </div> </template>

注意,play方法的音频和字幕文件的处理使用

const subtitles = ref(); // 所有字幕数据 const displayedSubtitles = ref([]); // 当前显示的所有字幕项 // 创建 AudioContext 实例 const audioContext = new (window.AudioContext || window.webkitAudioContext)(); // 用于播放音频 const currentTime = ref(0); // 当前播放时间 const play = async () => { try { // 获取音频文件并转换为 ArrayBuffer const response = await fetch(mp3); const arrayBuffer = await response.arrayBuffer(); // 解码音频数据 const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); // 创建音频源 const audioSource = audioContext.createBufferSource(); audioSource.buffer = audioBuffer; // 连接音频源到输出(扬声器) audioSource.connect(audioContext.destination); // 播放音频 audioSource.start(); // 设置一个定时器,模拟 timeupdate 事件 const intervalId = setInterval(() => { if (audioContext.state === "running") { currentTime.value = audioContext.currentTime; console.log("currentTime.value", currentTime.value.toFixed(3)); updateSubtitle(currentTime.value); } // 停止定时器,当音频播放结束时 if (audioContext.currentTime >= audioBuffer.duration) { clearInterval(intervalId); } }, 100); // 每100ms更新一次 } catch (error) { console.error("音频播放失败:", error); } }; // 根据当前时间戳更新显示的字幕 const updateSubtitle = (curTime) => { // 找到当前时间点应该显示的字幕 const newSubtitles = subtitles.value.filter( (sub) => curTime >= sub.start && curTime <= sub.end ); // 找到了,就将其添加到displayedSubtitles数组中 if (newSubtitles.length > 0) { // 但是因为timeupdate触发频繁,所以追加前,要看看这条字幕是否存在过 newSubtitles.forEach((subtitle) => { // 不存在,才去往里面追加 if (!displayedSubtitles.value.includes(subtitle.text)) { displayedSubtitles.value.push(subtitle.text); } }); } };

3. 完整代码

在笔者的github上:

github.com/shuirongshui

END

免责声明:本文由卡卷网编辑并发布,但不代表本站的观点和立场,只提供分享给大家。

卡卷网

卡卷网 主页 联系他吧

请记住:卡卷网 Www.Kajuan.Net

欢迎 发表评论:

请填写验证码