playable
Version:
Video player based on HTML5Video
200 lines (158 loc) • 5.24 kB
text/typescript
import { UIEvent } from '../../../constants';
import logger from '../../../utils/logger';
import playerAPI from '../../../core/player-api-decorator';
import SubtitlesView from './subtitles.view';
import { ISubtitlesAPI, ISubtitles, ISubtitleConfig } from './types';
import { IEventEmitter } from '../../event-emitter/types';
import { IRootContainer } from '../../root-container/types';
import { IPlaybackEngine } from '../../playback-engine/types';
function isSameOrigin(url: string): boolean {
const { protocol, hostname, port } = window.location;
const a = document.createElement('a');
a.setAttribute('href', url);
return a.protocol === protocol && a.hostname === hostname && a.port === port;
}
class Subtitles implements ISubtitles {
static moduleName = 'subtitle';
static dependencies = ['rootContainer', 'engine', 'eventEmitter'];
static View = SubtitlesView;
isHidden: boolean;
view: SubtitlesView;
private _eventEmitter: IEventEmitter;
private _video: HTMLVideoElement;
private _activeSubtitleIndex: number | null = null;
private _trackList: Array<TextTrack> = [];
private _unbindEvents: () => void;
constructor({
rootContainer,
engine,
eventEmitter,
}: {
rootContainer: IRootContainer;
engine: IPlaybackEngine;
eventEmitter: IEventEmitter;
}) {
this._eventEmitter = eventEmitter;
this._video = engine.getElement();
this._initUI();
this._bindCallbacks();
this._bindEvents();
rootContainer.appendComponentElement(this.getElement());
}
setSubtitles(
subtitles: string | ISubtitleConfig | Array<ISubtitleConfig>,
): void {
this.removeSubtitles();
if (!subtitles) {
return;
}
const hasCrossOriginAttribute = this._video.hasAttribute('crossorigin');
let hasCrossOriginTrack = false;
(Array.isArray(subtitles) ? subtitles : [subtitles]).forEach(subtitle => {
subtitle = typeof subtitle === 'string' ? { src: subtitle } : subtitle;
hasCrossOriginTrack = hasCrossOriginTrack || !isSameOrigin(subtitle.src);
this._addSubtitle(subtitle);
});
if (!hasCrossOriginAttribute && hasCrossOriginTrack) {
logger.warn(
"Subtitle are being loaded from another origin but video crossorigin attribute isn't used. " +
'This may prevent text tracks from loading.',
);
}
}
setActiveSubtitle(index: number): void {
this._clearActiveSubtitle();
this._setActiveSubtitle(index);
}
showSubtitles(): void {
this.view.show();
}
hideSubtitles(): void {
this.view.hide();
}
private _addSubtitle(subtitle: ISubtitleConfig): void {
const track = document.createElement('track');
track.setAttribute('src', subtitle.src);
track.setAttribute('kind', 'subtitles');
if (subtitle.lang) {
track.setAttribute('srclang', subtitle.lang);
}
if (subtitle.label) {
track.setAttribute('label', subtitle.label);
}
this._video.appendChild(track);
this._trackList.push(
this._video.textTracks[this._video.textTracks.length - 1],
);
}
removeSubtitles(): void {
this._clearActiveSubtitle();
const subtitleTracks: NodeListOf<HTMLTrackElement> = this._video.querySelectorAll(
'track[kind="subtitles"]',
);
Array.prototype.forEach.call(
subtitleTracks,
(trackElement: HTMLTrackElement) => this._video.removeChild(trackElement),
);
this._trackList = [];
}
private _clearActiveSubtitle(): void {
if (this._activeSubtitleIndex) {
const textTrack: TextTrack = this._trackList[this._activeSubtitleIndex];
textTrack.removeEventListener('cuechange', this._showSubtitles);
textTrack.mode = 'disabled';
this._activeSubtitleIndex = null;
}
this.view.clearSubtitles();
}
private _setActiveSubtitle(index: number): void {
const textTrack: TextTrack = this._trackList[index];
if (textTrack) {
textTrack.mode = 'hidden';
textTrack.addEventListener('cuechange', this._showSubtitles);
textTrack.activeCues &&
this.view.showSubtitles(
Array.prototype.map.call(
textTrack.activeCues,
(cue: VTTCue) => cue.text,
),
);
}
this._activeSubtitleIndex = index;
}
getElement(): HTMLElement {
return this.view.getElement();
}
private _initUI(): void {
this.view = new Subtitles.View();
}
private _bindCallbacks(): void {
this._showSubtitles = this._showSubtitles.bind(this);
}
private _bindEvents(): void {
this._unbindEvents = this._eventEmitter.bindEvents(
[
[UIEvent.MAIN_BLOCK_SHOW, this.view.moveSubtitlesUp, this.view],
[UIEvent.MAIN_BLOCK_HIDE, this.view.moveSubtitlesDown, this.view],
],
this,
);
}
private _showSubtitles(event: TrackEvent): void {
const textTrack: TextTrack = event.target as TextTrack;
this.view.showSubtitles(
Array.prototype.map.call(textTrack.activeCues, (cue: VTTCue) => cue.text),
);
}
destroy(): void {
this._unbindEvents();
this.view.destroy();
}
}
export { ISubtitlesAPI };
export default Subtitles;