使用 imscJS polyfill

您目前需要一个 polyfill 才能在 Web 上渲染 IMSC。imscJS 是一个不错的选择,因为它得到了积极维护,并且几乎涵盖了 IMSC 的所有功能。本文将向您展示如何使用 imscJS 以及如何在您自己的网站上集成它。

介绍 imscJS

imscJS 是一个用于将 IMSC 文档渲染到 HTML 的 JavaScript 库。下面我们将首先介绍一个简单的 imscJS 使用示例,然后我们将查看一个更复杂的示例,该示例实际上会在适当的时间在视频顶部渲染字幕。您可以在 GitHub 上找到第一个示例的源代码

嵌入 imscJS

首先,您需要嵌入 imscJS 库

html
<script src="https://unpkg.com/[email protected]/build/umd/imsc.all.min.js">

加载 imscJS 库后,它可以通过三个不同的步骤来渲染 IMSC 文档,如下节所述。

解析 IMSC 文档

首先,IMSC 文档被解析为一个不可变的 JavaScript 对象(在本例中为 doc

js
const doc = imsc.fromXML(source);

此步骤每个 IMSC 文档只需要执行一次。doc 对象有一个方法 getMediaTimeEvents(),它返回一个时间偏移量(以秒为单位)数组,指示 IMSC 文档的可视表示发生更改的位置。

js
const t = doc.getMediaTimeEvents();

生成 IMSC 快照

在第二步中,使用 imsc.generateISD() 创建 IMSC 文档在特定时间点(isd)的快照。

js
const isd = imsc.generateISD(doc, t[1]);

此时间点不必是 getMediaTimeEvents() 返回的值之一,但通常是。在上面的示例中,快照是在 IMSC 文档更改的第二个时间点(t[1])创建的。在典型情况下,应用程序将在媒体播放之前,并且对于 getMediaTimeEvents() 返回的每个偏移量,创建一个快照并在指定的偏移量安排其显示。

渲染 IMSC 快照

在第三步也是最后一步中,快照使用 imsc.renderHTML() 渲染到 HTML <div>

js
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 元素分配给一个变量。

js
const renderDiv = document.getElementById("render-div");

我们使用与 HTML 文本轨道关联的 HTML 提示,在 IMSC 字幕应该出现或消失时触发事件。在本例中,我们使用我们在 HTML 标记中声明的 <track> 元素,但我们也可以动态创建文本轨道并将其添加到 <video> 中。

js
const myVideo = document.getElementById("imscVideo");
const myTrack = myVideo.textTracks[0];

我们使用 <track> 元素的 src 属性作为指向包含我们字幕的 IMSC 文档的指针

js
const ttmlUrl = myVideo.getElementsByTagName("track")[0].src;

检索 IMSC 文件

浏览器不会自动为我们检索文档。目前,大多数浏览器只实现了 WebVTT。因此,这些浏览器期望 src 属性的值指向 WebVTT 文件。如果不是,它们就不会使用它,我们也没有直接访问 src 属性指向的文件。因此,我们使用 src 属性来存储 IMSC 文件的 URL。我们需要完成检索文件并将文件读入 JavaScript 字符串的工作。在本例中,我们使用 fetch() API 来完成此任务

js
const response = await fetch(ttmlUrl);
initTrack(await response.text());

设置文本轨道模式

还有一个副作用。由于浏览器从 src 属性中没有获取有效的 WebVTT 文件,因此它们禁用了轨道。文本轨道的 mode 属性设置为值 disable

但这不是我们想要的。在禁用模式下,提示不会在其开始和结束时间触发事件。因为我们需要这些事件来渲染 IMSC 字幕,所以我们将文本轨道的模式更改为 hidden。在此模式下,浏览器将触发提示的事件,但不会渲染提示文本属性的值。

js
myTrack.mode = "hidden";

设置好所有内容后,我们可以专注于实现 IMSC 字幕渲染。

生成“字幕状态”

上面我们解释了我们需要生成 IMSC 快照。在下一节中,我们将更深入地探讨其含义以及为什么需要这样做。

正如我们在 解析 IMSC 文档 中了解到的,第一步是将 IMSC 文档解析为 imscJS 对象。

js
const imscDoc = imsc.fromXML(text);

我们希望使用提示来渲染 IMSC 字幕。每个提示都具有表示其开始时间和结束时间的属性。当媒体的时间线到达提示的开始和结束时间时,浏览器引擎会触发事件。我们可以为这些事件注册函数调用。我们使用它们来渲染从 imscJS 生成的 HTML,并在需要时将其再次移除。

但是,IMSC 字幕到提示的开始和结束时间的映射并不像您想象的那样简单。当然,您可以只使用带有 beginend 属性的 <p> 元素。这将完美地映射到带有 startend 属性的提示接口。

但请看以下 IMSC 代码

html
<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 文档

js
const timeEvents = imscDoc.getMediaTimeEvents(); // timeEvents = [0,1,2,3]

要获取与时间事件相对应的 ISD 文档,我们需要调用 imscJS 方法 generateISD()。我们在 生成 IMSC 快照 中简要介绍了这一点。因此,对于第 2 秒的 ISD,我们需要执行以下操作

js
imsc.generateISD(imscDoc, 2);

创建文本轨道提示

使用这两种方法,我们现在可以生成 IMSC 渲染层的所有必要状态。我们按如下方式执行此操作

  • 迭代我们从 getMediaEvents() 中获得的数组
  • 对于每个时间事件
    • 创建一个相应的提示。
    • 使用 onenter 事件渲染 ISD。
    • 使用 onexit 事件再次删除渲染层。
js
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 时,可以将时间事件的值作为提示的开始时间。然后,我们可以使用下一个时间事件的值作为提示的结束时间,因为这表示渲染层需要更改。

js
myCue = new Cue(timeEvents[i], timeEvents[i + 1], "");

注意:在大多数浏览器中,文本轨道提示目前仅适用于 WebVTT 格式。因此,通常您会使用所有 WebVTT 属性(包括 WebVTT 文本属性)创建一个提示。我们从不使用这些属性,但重要的是要记住它们仍然存在。在构造函数中,我们还必须将 VTTCue 文本作为第三个参数添加。

但是我们应该如何计算最后一个时间事件的结束时间呢?它没有我们可以从中获取结束时间的“下一个”时间事件。

如果没有进一步的时间事件,这实际上意味着渲染层处于活动状态,直到媒体播放时间的结束。因此,我们可以将结束时间设置为关联媒体的时长。

js
myCue = new Cue(timeEvents[i], myVideo.duration, "");

一旦我们构建了提示对象,我们就可以注册名为“进入”提示时的函数。

js
myCue.onenter = function () {
  clearSubFromScreen();
  const myIsd = imsc.generateISD(imscDoc, this.startTime);
  imsc.renderHTML(myIsd, renderDiv);
};

我们生成与提示关联的 ISD,然后使用 imscJS 方法 renderHTML() 在“渲染容器”中呈现其相应的 HTML。

为了确保没有剩余的字幕层,我们首先删除字幕层(如果存在)。为此,我们定义了一个函数,稍后在提示结束时可以重复使用。

js
function clearSubFromScreen() {
  const subtitleActive = renderDiv.getElementsByTagName("div")[0];
  if (subtitleActive) {
    renderDiv.removeChild(subtitleActive);
  }
}

在提示的 onexit 事件触发后,我们再次调用此函数。

js
myCue.onexit = function () {
  clearSubFromScreen();
};

最后,我们只需要将生成的提示添加到文本轨道中。

js
myTrack.addCue(myCue);

使用原生视频播放器控件

通常,您希望为用户提供一些选项来控制视频播放。至少他们应该能够播放、暂停和寻求。最简单的方法是使用 Web 浏览器的原生视频控件,对吧?是的,这是真的,当您不需要任何其他功能时。

原生视频播放器控件是浏览器的一部分,而不是 HTML 标记。尽管它们对 DOM 事件做出反应并生成一些自己的事件,但您作为 Web 开发人员无法直接访问它们。

这在使用 imscJS 时会导致两个问题。

  1. IMSC HTML 覆盖层覆盖了整个视频。它位于 <video> 元素的顶部。尽管您可以看到播放器控件(因为大多数覆盖层具有透明背景),但鼠标点击等指针事件无法传递到控件。由于无法通过标准 CSS 访问它们,因此您也无法更改控件的 z-index 来解决此问题。因此,如果您始终拥有字幕覆盖层,则将无法在视频开始后停止它。这将是一种非常糟糕的用户体验。
  2. 通常,原生视频播放器控件具有字幕用户界面。您可以选择文本轨道或关闭字幕的渲染。不幸的是,字幕界面仅控制 WebVTT 字幕的渲染。浏览器不知道我们正在使用 imscJS 渲染字幕,因此这些控件将不起作用。

对于第一个问题,有一个简单的 CSS 解决方案。我们需要将 CSS 属性 pointer-events 设置为 none(有关完整的 CSS,请参阅 GitHub 上的 示例代码)。

css
#render-div {
  pointer-events: none;
}

这样可以使指针事件“穿过”覆盖层(有关更多详细信息,请参阅 指针事件的参考文档)。

字幕用户界面问题有点难以解决。尽管我们可以监听事件,但使用字幕用户界面激活轨道也会激活相应 WebVTT 的渲染。由于我们使用 VTTCues 进行 IMSC 渲染,因此这可能会导致不希望的呈现行为。VTTCue 的 text 属性始终具有空字符串作为值,但在某些浏览器中,这仍然可能导致伪影的渲染。

最好的解决方案是构建您自己的自定义控件。在我们的 创建跨浏览器视频播放器 教程中了解如何操作。