前言
在平时的生活中,我们遇到的流媒体播放很多都是有字幕的,字幕的出现可以大幅度的提升用户的观感。那么在Web
中字幕到底是如何呈现的?今天就让我们一起来一探究竟。
onTimeUpdate
假设我们有一个这样的json
,表示字幕数据:
const jsonContent = [ { start: "00:00:01.000", end: "00:00:02.000", text: "这是第一句字幕", }, { start: "00:00:03.500", end: "00:00:04.000", text: "这是第二句字幕", }, { start: "00:00:05.000", end: "00:00:06.000", text: "这是第三句字幕", }, ];
其中start
表示该条字幕的开始时间,end
表示该条字幕的结束时间,text
表示字幕的内容。在Web
的流媒体中,有一个事件叫做onTimeUpdate
,它会在媒体播放时以固定的间隔触发,我们可以监听这个事件然后来响应一些播放的变化。
<video id="video" controls autoplay crossorigin="anonymous" src="/test.mp4" width="500" ></video> const handleTimeUpdate = () => { const video = document.querySelector("#video"); video.addEventListener("timeupdate", function () { const currentTime = video.currentTime; console.log("currentTime", currentTime); }); }; handleTimeUpdate();
从上面的小例子中可以看到onTimeUpdate
的触发机制以及结果。根据currentTime
的变化,可以找到当前时间落在了哪一条字幕中,这样就可以实现字幕渲染的功能。
先来改一下dom
结构,用一个div
来展示字幕
<div class="video-container"> <video id="video" controls autoplay crossorigin="anonymous" src="/test.mp4" width="500" ></video> <div class="subtitle"></div> </div>
然后解析字幕数据,配合onTimeUpdate
实现字幕的展示播放:
const findSubtitle = (currentTime, jsonContent) => { for (let i = 0; i < jsonContent.length; i++) { const subtitle = jsonContent[i]; const startTime = parseTime(subtitle.start); const endTime = parseTime(subtitle.end); if (currentTime >= startTime && currentTime < endTime) { return subtitle; } } return null; }; const parseTime = (timeStr) => { const [hours, minutes, seconds] = timeStr.split(":").map(parseFloat); return hours * 3600 + minutes * 60 + seconds; }; const handleTimeUpdate = () => { const video = document.querySelector("#video"); video.addEventListener("timeupdate", function () { const currentTime = video.currentTime; const res = findSubtitle(currentTime, jsonContent); const subTitle = res.text; const subTitleDom = document.querySelector(".subtitle"); if (res) { subTitleDom.innerHTML = subTitle; } else { subTitleDom.innerHTML = ""; } }); }; handleTimeUpdate();
这样我们就实现了字幕的功能,但这个实现方案是万无一失的吗?来思考一个场景:如果我在0.1s-0.3s
中有一句字幕,0.5s-0.8s
中有一句字幕,因为timeUpdate
是浏览器根据一定的频率去触发的,假设出现了这样的一个情况,第一次触发在0.4s
,第二次触发在0.9s
,那这两个字幕是不是就丢了呢?
Track
<track>
标签是 HTML5
中用于提供轨道的元素之一。主要用于提供视频和音频的文本轨道(如字幕或者描述性文本)以及音频描述。
一般情况下,<track>
元素用于在 <video>
或 <audio>
元素中嵌入外部资源,以增强媒体内容的可访问性和可理解性。它可以用来添加字幕、章节标题、描述性注释等内容。<track>
标签的常见属性包括:
src
:指定轨道文件的URL
。kind
:指定轨道的类型,可以是subtitles
(字幕)、captions
(标题)、descriptions
(描述)、metadata
(源数据)等。srclang
:指定轨道的语言,使用 ISO 639-1 语言代码表示。label
:指定轨道的标签或标题,用于在用户界面上显示。default
:可选属性,表示该轨道是否默认加载。如果存在多个轨道,只有一个可以设置为默认加载,通常用于选择用户首选的轨道。
track
如果用来添加字幕的话,需要与一些字幕文件搭配使用,比如VTT
文件,下面简单介绍一下VTT
文件。
VTT(WebVTT)
文件是一种用于表示视频文本轨道的格式。它是一种简单的文本文件,通常用于存储字幕、标题、描述性文本等内容,以便在 HTML5 视频和音频元素中进行显示。VTT
文件的名称通常以 .vtt
作为文件扩展名。
VTT
文件的结构相对简单,它由几个关键部分组成:
- 文件头(Header) :文件头通常包含
WEBVTT
字样,用于指示这是一个WebVTT
文件。 - 时间标识(Timing) :时间标识部分用于指定每个文本块的起始时间和结束时间。时间格式通常为
HH:MM:SS.fff
,其中HH
表示小时,MM
表示分钟,SS
表示秒,fff
表示毫秒。时间标识通常以-->
分隔起始时间和结束时间,例如:
00:00:01.000 --> 00:00:02.000
- 文本内容(Text) :文本内容部分包含在时间标识之后,用于表示在指定时间范围内要显示的文本内容。每个文本块可以包含一行或多行文本,它们将在指定的时间段内逐行显示。
下面是一个简单的 VTT
文件示例:
WEBVTT 00:00:01.000 --> 00:00:02.000 这是第一句字幕。 00:00:03.500 --> 00:00:04.000 这是第二句字幕。
然后可以按照下面的代码使用:
<video id="video" width="500" controls> <source src="/test.mp4" type="video/mp4"> <track src="/en.vtt" kind="subtitles" srclang="en" label="English"> <track src="/zh.vtt" kind="subtitles" srclang="zh" label="中文"> </video>
zh.vtt
的内容如下:
WEBVTT 00:00:01.000 --> 00:00:02.000 这是第一句字幕。 00:00:03.500 --> 00:00:04.000 这是第二句字幕。
en.vtt
的内容如下:
WEBVTT 00:00:01.000 --> 00:00:02.000 This is the first subtitle. 00:00:03.500 --> 00:00:04.000 This is the second subtitle.
而WebVTT
中还有一些精细化控制的API
,可以参照这个文档WebVTT。下面就简单举两个例子,其他的能力就请查阅文档,这里就不一一展开。
比如说可以通过line
跟position
来调整字幕的位置:
WEBVTT 00:00:01.000 --> 00:00:02.000 line:38% position:35% 这是第一句字幕。 00:00:03.500 --> 00:00:04.000 line:40% position:35% 这是第二句字幕。
line
属性用于设置文本的垂直位置。它表示文本相对于媒体区域高度的百分比位置。position
属性用于设置文本的水平位置。它表示文本相对于媒体区域宽度的百分比位置。
再比如通过伪类来设置字幕的一些样式:
#video::cue(c.red) { color: red; }
WEBVTT 00:00:01.000 --> 00:00:02.000 line:38% position:35% <c.red>这是第一句字幕。</c.red> 00:00:03.500 --> 00:00:04.000 line:40% position:35% 这是第二句字幕。
完全自定义字幕样式
但仔细看上面的字幕,可以发现字幕有一个半透明的黑色背景,我尝试了很多方法都无法将这个背景去掉,查阅资料也是说这是由于不同浏览器对于流媒体的字幕编码导致的,我们已经完全控制了字幕的时机,但是没有完完全全控制字幕的样式。
我们可以将track
标签的kind
属性赋值为metadata
,当字幕变更时会触发一个cueChange
事件,通过这个事件拿到字幕,然后塞进某个dom
中来完全控制字幕的样式。
const fileVtt = () => { const video = document.querySelector("#video"); const track = document.createElement("track"); track.default = true; track.kind = "metadata"; function convertJsonToVtt(jsonContent) { let vttContent = "WEBVTT\n\n"; jsonContent.forEach((item, index) => { vttContent += `${index + 1}`; vttContent += "\n"; vttContent += `${item.start} --> ${item.end}`; vttContent += "\n"; if (item.type) { vttContent += `<c.mn>${item.text}</c.mn>`; } else { vttContent += `${item.text}`; } vttContent += "\n\n"; }); return vttContent; } const vttContent = convertJsonToVtt(jsonContent); // 将VTT内容保存为文件 const blob = new Blob([vttContent], { type: "text/vtt;charset=utf-8", }); const url = URL.createObjectURL(blob); track.src = url; video.appendChild(track); setTimeout(() => { const track = video.textTracks[0]; track.addEventListener("cuechange", function () { const cues = track.activeCues; for (var i = 0; i < cues.length; i++) { const cue = cues[i]; const div = document.createElement("div"); div.append(cue.getCueAsHTML()); const subTitleDom = document.querySelector(".subtitle"); subTitleDom.innerHTML = `<div style="color:yellow">${div.innerHTML}<div>` } }); }); }; fileVtt();
简单介绍一下上面的实现:
- 将
json
格式的字幕数据转换成VTT
格式 - 监听
cueChange
事件获取到字幕 - 动态插入字幕
最后
以上就是本文对流媒体字幕的一些认识,如果你觉得有意思的话,点点关注点点赞吧。欢迎评论区交流