当前位置:首页 > 每日看点

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

卡卷网1年前 (2025-05-07)每日看点344
他们朝我扔泥巴,我拿泥巴种荷花;他们朝我扔巴巴,我用巴巴敲代码,哦哦哦哦哦...

需求描述

  • 有一个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

扫描二维码推送至手机访问。

版权声明:本文由卡卷网发布,如需转载请注明出处。

本文链接:https://www.kajuan.net/ttnews/2025/05/12926.html

分享给朋友:

相关文章

有了Istio,开发还需要微服务架构吗?

有了Istio,开发还需要微服务架构吗?

Istio 是一个开源的服务网格(Service Mesh),通过它可以实现对服务间通信的管理和监控。对于那些本身没有设计为具备安全功能的传统应用程序,Istio 可以提供一个“透明”的安全保护层,而不需要对应用本身进行任何代码修改。…

大量刷短视频,会让大脑变笨拙吗?

会。 我曾经是一名高三学生,亲身实践过。 当时集中突破语文,每天都在刷语文卷。然后有一天想躺一下刷手机,结果短视频刷完了,我再去看哪些文章,只觉头晕眼花,难以理解文字。不过好在这种情况是短时间的,过了一天我的能力又恢复了。 在我看来,长期刷…

只有我一个人后悔升级鸿蒙next吗?

只有我一个人后悔升级鸿蒙next吗?

我有一台mate 60 pro,第一时间升级了“纯血鸿蒙”。 虽然功能并不完善,甚至有些简陋,但是我非常~非常不后悔升级鸿蒙next系统。 因为.... 这就是一款“大型养成系游戏“,给我平淡的生活提供了源源不断的情绪价值。 我每天特别…

常微分方程的理论对偏微分方程的研究有没有帮助?

常微分方程的理论对偏微分方程的研究有没有帮助?

之前看到过一句话,好像是 Bourgain 说的,大意是做 ode 的做到深处就到了 pde,做 pde 的做到深处就到了 ode,ode 和 pde 最后是相通的。 以我目前有限的知识,只知道下面两个方向: 1. Hamilton 系统在…

在 Kubernetes 中,Service 的实现原理是什么,它是如何实现服务发现的?

行,问的这个问题挺有意思,Kubernetes 里 Service 是怎么实现的,服务发现是怎么回事,咱就直说了。这事儿看起来挺玄乎,但掰开揉碎了讲,也就那么回事。你得把这事儿想得简单点,别一上来就被啥术语吓住了——其实全是些搬砖的套路。…

你见过哪些有趣的偏微分方程组?

你见过哪些有趣的偏微分方程组?

说说我一直在做的一个偏微分方程系统吧,该模型为生物领域的趋化性(chemotaxis)模型,也叫Keller-Segel模型,由该二人于1971年左右提出,用于刻画细胞或者细菌的趋化机制。解释一下趋化性(chemotaxis):趋化性是指单…

发表评论

访客

看不清,换一张

◎欢迎参与讨论,请在这里发表您的看法和观点。