使用 imscJS polyfill
您目前需要一个 polyfill 才能在 Web 上渲染 IMSC。imscJS 是一个不错的选择,因为它得到了积极维护,并且几乎涵盖了 IMSC 的所有功能。本文将向您展示如何使用 imscJS 以及如何在您自己的网站上集成它。
介绍 imscJS
imscJS 是一个用于将 IMSC 文档渲染到 HTML 的 JavaScript 库。下面我们将首先介绍一个简单的 imscJS 使用示例,然后我们将查看一个更复杂的示例,该示例实际上会在适当的时间在视频顶部渲染字幕。您可以在 GitHub 上找到第一个示例的源代码。
嵌入 imscJS
首先,您需要嵌入 imscJS 库
<script src="https://unpkg.com/[email protected]/build/umd/imsc.all.min.js">
加载 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 vdiv = document.getElementById("render-div");
imsc.renderHTML(isd, vdiv);
构建 IMSC 播放器
让我们来看一个更扩展的示例,并向您展示如何使用 imscJS 在嵌入的 HTML 视频上渲染字幕。例如,我们使用以下带有字幕的视频。
您可以在 MDN 存储库中找到 HTML 标记 和 JavaScript 源代码。
访问 DOM
IMSC 字幕由带有内联 CSS 的 HTML 标记渲染。它表示关联媒体元素时间线上特定时间段内的 IMSC 字幕。正如我们在上面的 渲染 IMSC 快照 部分中看到的,标记使用 renderHtml()
方法插入到 <div>
元素中。我们可以将此 <div>
元素视为从 IMSC 代码生成的 HTML 的容器。稍后,我们将相应的 DOM 元素作为参数传递给 renderHtml()
方法。
为方便起见,我们将此 DOM 元素分配给一个变量。
const renderDiv = document.getElementById("render-div");
我们使用与 HTML 文本轨道关联的 HTML 提示,在 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 文件
设置文本轨道模式
还有一个副作用。由于浏览器从 src
属性中没有获取有效的 WebVTT 文件,因此它们禁用了轨道。文本轨道的 mode
属性设置为值 disable
。
但这不是我们想要的。在禁用模式下,提示不会在其开始和结束时间触发事件。因为我们需要这些事件来渲染 IMSC 字幕,所以我们将文本轨道的模式更改为 hidden
。在此模式下,浏览器将触发提示的事件,但不会渲染提示文本属性的值。
myTrack.mode = "hidden";
设置好所有内容后,我们可以专注于实现 IMSC 字幕渲染。
生成“字幕状态”
上面我们解释了我们需要生成 IMSC 快照。在下一节中,我们将更深入地探讨其含义以及为什么需要这样做。
正如我们在 解析 IMSC 文档 中了解到的,第一步是将 IMSC 文档解析为 imscJS 对象。
const imscDoc = imsc.fromXML(text);
我们希望使用提示来渲染 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 快照 中简要介绍了这一点。因此,对于第 2 秒的 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 属性始终具有空字符串作为值,但在某些浏览器中,这仍然可能导致伪影的渲染。
最好的解决方案是构建您自己的自定义控件。在我们的 创建跨浏览器视频播放器 教程中了解如何操作。