使用 imscJS polyfill
目前,您需要在 Web 上渲染 IMSC 来使用 polyfill。imscJS 是一个不错的选择,因为它维护积极,并且几乎完全覆盖了 IMSC 的所有功能。本文将向您展示如何使用 imscJS 以及如何将其集成到您自己的网站中。
介绍 imscJS
imscJS 是一个用于将 IMSC 文档渲染为 HTML 的 JavaScript 库。下面我们将首先通过一个示例来演示如何使用 imscJS,然后我们将看一个更复杂的示例,该示例实际上会在适当的时间将字幕渲染到视频之上。您可以在 GitHub 上找到第一个示例的源代码。
嵌入 imscJS
首先,您需要嵌入 imscJS 库
<script src="https://unpkg.com/imsc@1.1.0-beta.2/build/umd/imsc.all.min.js"></script>
imscJS 库加载后,可以使用它分三个独立的步骤来渲染 IMSC 文档,这些步骤将在下面的部分中进行解释。
解析 IMSC 文档
首先,IMSC 文档被解析为一个不可变的 JavaScript 对象(在我们的例子中为 doc)
const doc = imsc.fromXML(source);
此步骤对于每个 IMSC 文档只需要发生一次。doc 对象有一个方法 getMediaTimeEvents(),它返回一个时间偏移量(以秒为单位)的数组,指示 IMSC 文档的视觉表示在何时发生变化。
const t = doc.getMediaTimeEvents();
生成 IMSC 快照
第二步,使用 imsc.generateISD() 创建特定时间点的 IMSC 文档快照(isd)。
const isd = imsc.generateISD(doc, t[1]);
这个时间点不必是 getMediaTimeEvents() 返回的值之一,但通常是。在上面的示例中,快照是在 IMSC 文档更改的第二个时间点(t[1])创建的。在典型场景中,应用程序会在媒体播放之前,并为 getMediaTimeEvents() 返回的每个偏移量创建一个快照,并安排在指定偏移量进行展示。
渲染 IMSC 快照
第三步,也是最后一步,使用 imsc.renderHTML() 将快照渲染到 HTML <div> 元素中
const renderDiv = document.getElementById("render-div");
imsc.renderHTML(isd, renderDiv);
构建 IMSC 播放器
让我们来看一个更详细的示例,展示如何使用 imscJS 在嵌入的 HTML 视频上渲染字幕。例如,我们使用下面的带字幕的视频。
您可以在 MDN IMSC 示例存储库中找到 HTML 标记和 JavaScript 源代码。
访问 DOM
IMSC 字幕是通过带有内联 CSS 的 HTML 标记渲染的。它在相关媒体元素的时轴上表示特定时间段内的 IMSC 字幕。正如我们在上面的 渲染 IMSC 快照 部分所看到的,标记使用 renderHtml() 方法插入到 <div> 元素中。我们可以将这个 <div> 元素视为从 IMSC 代码生成的 HTML 的容器。稍后,我们将相应的 DOM 元素作为参数传递给 renderHtml() 方法。
为了方便起见,我们将此 DOM 元素分配给一个变量。
const renderDiv = document.getElementById("render-div");
我们使用与 HTML 文本轨道关联的 HTML 提示(cues),以便在 IMSC 字幕应该出现或消失时触发事件。在此示例中,我们使用在 HTML 标记中声明的 <track> 元素,但我们也可以动态创建文本轨道并将其添加到 <video> 元素中。
const myVideo = document.getElementById("imscVideo");
const myTrack = myVideo.textTracks[0];
我们将 <track> 元素的 src 属性用作指向包含字幕的 IMSC 文档的指针
const ttmlUrl = myVideo.getElementsByTagName("track")[0].src;
检索 IMSC 文件
浏览器不会自动为我们检索文档。目前,大多数浏览器只实现了 WebVTT。因此,这些浏览器期望 src 属性的值指向一个 WebVTT 文件。如果不是,它们就不会使用它,并且我们也无法直接访问 src 属性指向的文件。因此,我们仅使用 src 属性来存储 IMSC 文件的 URL。我们需要进行工作来检索文件并将其读取为 JavaScript 字符串。在示例中,我们使用 fetch() API 来完成此任务。
const response = await fetch(ttmlUrl);
initTrack(await response.text());
设置文本轨道模式
还有一个副作用。由于浏览器从 src 属性获取不到有效的 WebVTT 文件,因此它们会禁用该轨道。文本轨道的 mode 属性被设置为 disable 值。
但这并非我们想要的。在禁用模式下,提示在开始和结束时间不会触发事件。因为我们需要这些事件来渲染 IMSC 字幕,所以我们将文本轨道的模式更改为 hidden。在此模式下,浏览器将触发提示的事件,但不会渲染提示文本属性的值。
myTrack.mode = "hidden";
在设置好所有内容后,我们可以专注于实现 IMSC 字幕渲染。
生成“字幕状态”
上面我们解释了需要生成 IMSC 快照。在下一节中,我们将深入探讨这意味着什么以及为什么它是必要的。
正如我们在 解析 IMSC 文档 中学到的,第一步是将 IMSC 文档解析为 imscJS 对象。
const imscDoc = imsc.fromXML(text);
我们希望使用提示(cues)来渲染 IMSC 字幕。每个提示都有表示其开始时间和结束时间的属性。当媒体时轴到达提示的开始和结束时间时,浏览器引擎会触发事件。我们可以为这些事件注册函数调用。我们使用它们来渲染从 imscJS 生成的 HTML,并在需要时将其移除。
但是,IMSC 字幕与提示的开始和结束时间的映射并不像您想象的那么直接。当然,您可以使用带有 begin 和 end 属性的 <p> 元素。这将完美地映射到提示接口及其 start 和 end 属性。
但是,请看以下 IMSC 代码
<p>
<span begin="1s" end="3s">Hello</span> <span begin="2s" end="3s">world!</span>
</p>
这可以被视为“累积”字幕的一个例子,其中单词一个接一个地添加到一行。在一些国家,这是实时字幕的常见做法。
发生的情况如下
- 在第 0 秒时没有字幕。
- 在第 1 秒时,“Hello”文本应该出现。
- 在第 2 秒时,“Hello”文本仍然应该“保持显示”,但需要添加“world!”文本。因此,从第 2 秒到第 3 秒,我们有一个表示“Hello world!”文本的字幕。
为了将其映射到 HTML,我们需要至少两个提示:一个表示从第 1-2 秒的“Hello”文本,另一个表示从第 2-3 秒的“Hello world!”文本。
但这只是一个简化的简单场景。想象一下您还有 5 个单词在累积。它们可能具有相同的结束时间但不同的开始时间。或者想象一下您有一个不同位置的字幕(例如,代表不同的说话者)。此字幕与另一个字幕同时显示,但累积单词可能具有不同的开始时间,因此具有不同的时间间隔。
幸运的是,在 IMSC 和 imscJS 中,这种情况很容易处理,因为 IMSC 有一种无状态字幕渲染机制。
让我们更仔细地看看这意味着什么。
在我们的 HTML/CSS 实现中,我们可以将 IMSC 字幕视为叠加在视频之上的一个渲染层。在媒体时间轴的每个时间点,渲染层都有一个特定的状态。对于这些“状态”,IMSC 有一个概念模型,“中间同步文档格式”,它表示该层最终渲染的内容。每次渲染需要更改时,都会创建一个新的表示。创建的内容称为中间同步文档或ISD。此 ISD 与其之前或之后的 ISD 没有依赖关系。它完全是无状态的,并包含渲染字幕所需的所有信息。
那么,我们如何获取 ISD 发生变化的时间呢?
这很简单:我们只需调用 imscJS 文档对象的 getMediaTimeEvents() 方法(另请参阅 解析 IMSC 文档)。
const timeEvents = imscDoc.getMediaTimeEvents(); // timeEvents = [0,1,2,3]
要获取与时间事件对应的 ISD 文档,我们需要调用 imscJS 方法 generateISD()。我们在 生成 IMSC 快照 中简要解释过这一点。所以,对于第二秒的 ISD,我们需要这样做:
imsc.generateISD(imscDoc, 2);
创建文本轨道提示
现在,我们可以使用这两个方法生成 IMSC 渲染层的所有必要状态。我们这样做如下:
- 遍历我们从
getMediaEvents()返回的数组 - 对于每个时间事件
- 创建一个相应的提示。
- 使用
onenter事件来渲染 ISD。 - 使用
onexit事件再次移除渲染层。
for (let i = 0; i < timeEvents.length; i++) {
const Cue = window.VTTCue || window.TextTrackCue;
let myCue;
if (i < timeEvents.length - 1) {
myCue = new Cue(timeEvents[i], timeEvents[i + 1], "");
} else {
myCue = new Cue(timeEvents[i], myVideo.duration, "");
}
myCue.onenter = function () {
clearSubFromScreen();
const myIsd = imsc.generateISD(imscDoc, this.startTime);
imsc.renderHTML(myIsd, renderDiv);
};
myCue.onexit = function () {
clearSubFromScreen();
};
myTrack.addCue(myCue);
}
让我们详细看看。
当我们在 timeEvents 中循环时,我们可以将时间事件的值作为提示的开始时间。然后,我们可以使用下一个时间事件的值作为提示的结束时间,因为它表明渲染层需要更改。
myCue = new Cue(timeEvents[i], timeEvents[i + 1], "");
注意:在大多数浏览器中,文本轨道提示目前仅针对 WebVTT 格式实现。所以通常您会创建一个包含所有 WebVTT 属性(包括 WebVTT 文本属性)的提示。我们从不使用这些属性,但重要的是要记住它们仍然存在。在构造函数中,我们还必须将 VTTCue 文本作为第三个参数添加。
但是,最后一个时间事件的结束时间应该如何计算?它没有“下一个”时间事件可以用来确定结束时间。
如果没有进一步的时间事件,这实际上意味着渲染层一直处于活动状态,直到媒体播放结束。所以我们可以将结束时间设置为相关媒体的持续时间。
myCue = new Cue(timeEvents[i], myVideo.duration, "");
一旦我们构造了提示对象,我们就可以注册在提示“进入”时调用的函数。
myCue.onenter = function () {
clearSubFromScreen();
const myIsd = imsc.generateISD(imscDoc, this.startTime);
imsc.renderHTML(myIsd, renderDiv);
};
我们生成与提示关联的 ISD,然后使用 imscJS 方法 renderHTML() 在“渲染容器”中渲染其对应的 HTML。
为了确保没有剩余的字幕层,我们首先移除字幕层(如果存在)。为此,我们定义一个函数,该函数稍后可以在提示结束时重用。
function clearSubFromScreen() {
const subtitleActive = renderDiv.getElementsByTagName("div")[0];
if (subtitleActive) {
renderDiv.removeChild(subtitleActive);
}
}
一旦触发提示的 onexit 事件,我们就再次调用此函数。
myCue.onexit = function () {
clearSubFromScreen();
};
最后,我们只需要将生成的提示添加到文本轨道中。
myTrack.addCue(myCue);
使用原生视频播放器控件
通常,您希望为用户提供一些选项来控制视频播放。至少他们应该能够播放、暂停和跳转。最简单的方法是使用 Web 浏览器的原生视频控件,对吧?是的,这是正确的,当您不需要任何额外的功能时。
原生视频播放器控件是浏览器的一部分,而不是 HTML 标记。虽然它们对 DOM 事件做出反应并生成一些自己的事件,但作为 Web 开发者,您无法直接访问它们。
这在使用 imscJS 时会带来两个问题
- IMSC HTML 覆盖层覆盖了整个视频。它位于
<video>元素的顶部。虽然您可以看到播放器控件(因为大部分覆盖层是透明背景),但鼠标点击等指针事件不会传递到控件。由于无法通过标准 CSS 访问它们,因此您也无法更改控件的 z-index 来解决此问题。因此,如果您始终有字幕覆盖层,一旦视频开始播放,您将无法停止它。这将带来非常糟糕的用户体验。 - 通常,原生视频播放器控件会有一个字幕用户界面。您可以选择一个文本轨道或关闭字幕渲染。不幸的是,字幕界面仅控制 WebVTT 字幕的渲染。浏览器不知道我们正在使用 imscJS 进行字幕渲染,因此这些控件将无效。
对于第一个问题,有一个直接的 CSS 解决方案。我们需要将 CSS 属性 pointer-events 设置为 none(有关完整的 CSS,请参阅 GitHub 上的 示例代码)。
#render-div {
pointer-events: none;
}
这会产生指针事件“穿过”覆盖层(有关更多详细信息,请参阅 指针事件参考文档)。
字幕用户界面问题要解决起来有点困难。虽然我们可以监听事件,但通过字幕用户界面激活轨道也会激活相应 WebVTT 的渲染。由于我们正在使用 VTTCues 进行 IMSC 渲染,这可能会导致不期望的显示行为。VTTCue 的 text 属性的值始终为空字符串,但在某些浏览器中,这仍可能导致显示伪影。
最佳解决方案是构建自己的自定义控件。在我们的 创建跨浏览器视频播放器 教程中了解如何操作。