他们朝我扔泥巴,我拿泥巴种荷花;他们朝我扔巴巴,我用巴巴敲代码,哦哦哦哦哦...
需求描述
- 有一个MP3音频文件,在播放的时候,需要展示对应的字幕给到用户
- 即为需要做到视频和音频同步的效果
- 如下效果图
- 演示地址:http://ashuai.work:8890/19
字幕文件的种类
常见的字幕文件,有三种
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
);
};
加工完毕以后,能得到这样的字幕数组对象,如下:
- 即为,数组中每一项,都是一条字幕对象
- 字幕对象记录了字幕开始时间,字幕结束时间,以及在开始结束时间之间,需要呈现的字幕文字
- 这样的话,我们就可以在对应时间节点,展示对应字幕即可
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上:https://github.com/shuirongshuifu/vue3-echarts5-example
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。