使用 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 屬性的值始終為空字串,但在某些瀏覽器中,這仍可能導致顯示偽影。
最佳解決方案是構建自己的自定義控制元件。在我們的 建立跨瀏覽器影片播放器 教程中瞭解如何操作。