他們朝我扔泥巴,我拿泥巴種荷花;他們朝我扔巴巴,我用巴巴敲代碼,哦哦哦哦哦...
需求描述
- 有一個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