import shaka from "shaka-player";
import "shaka-player/dist/controls.css";
import { HTML5Video } from "./HTML5Video.js";
import { ThumbnailVideoPlayer } from "../thumbnail/ThumbnailVideoPlayer.js";
import { VectorGraphics } from "./VectorGraphics.js";
import { VideoPlayer } from "./VideoPlayer.js";
import { ShakaPlayerErrorHandler } from "./ShakaPlayerErrorHandler.js";
export { ShakaPlayer }
/**
* Class to build the AWI version of the Shaka Player.
* @author ckraemme <ckraemme@awi.de>
*/
class ShakaPlayer extends VideoPlayer {
/**
* Build the player.
* @param {string/HTMLElement} target - HTML element.
* @param {Object.<string,Array>} options - Dictionary with input configuration for the player. The configuration can be set for the keys media, thumbnail, cover and poster. They contain the respective URLs.
*/
constructor(target, options) {
super(target, options);
this.manifest;
this.videoElement = document.createElement('video');
this.videoElement.className = "video-player-video";
this.videoContainer.appendChild(this.videoElement);
this.shakaPlayer = new shaka.Player(this.videoElement);
this.shakaUI = new shaka.ui.Overlay(this.shakaPlayer, this.videoContainer, this.videoElement);
this.shakaUIControls = this.shakaUI.getControls();
this.buildShakaPlayer_();
window.shakaPlayer = this.shakaPlayer;
window.shakaUI = this.shakaUI;
}
/**
* Configure, initialize and load media into the player. Because the Shaka UI library is used, the player instance is build automatically by the former, that triggers the event 'shaka-ui-loaded' when it is done.
* @private
*/
buildShakaPlayer_() {
this.initEventListeners_();
this.applyHTML5VideoSettings_();
this.addExperimentalABRRestriction_();
this.applyShakaSpecificPlayerConfigurations_();
this.loadVideoManifestAndSelectThumbnailManifest_();
}
/**
* Initialize events to the Shaka Player that extend its functionality, apply configuration and improve error logging.
* @private
*/
initEventListeners_() {
this.shakaPlayer.addEventListener('error', ShakaPlayerErrorHandler.onErrorEvent);
this.shakaUIControls.addEventListener('error', ShakaPlayerErrorHandler.onErrorEvent);
this.initEventsAfterVideoHasLoaded_();
}
/**
* Initialize events that should trigger when video has loaded.
* @private
*/
initEventsAfterVideoHasLoaded_() {
this.videoContainer.querySelector('video').addEventListener('loadeddata', () => {
this.videoContainer.querySelector('video').classList.add("loaded");
this.configureUIShakaPlayer_();
this.extendFunctionalityForAudio_();
if (this.webvttUrl)
new ThumbnailVideoPlayer(this.videoContainer, this.webvttUrl, 20, true, '.shaka-seek-bar-container .shaka-seek-bar');
}, false);
}
/**
* Configure adaptive bitrate restriction in the Shaka Player that are based on the video player size to reduce traffic and loading time. Experimental state.
* @private
*/
addExperimentalABRRestriction_() {
new ResizeObserver(() => {
if (typeof this.videoContainer !== 'undefined' && this.shakaPlayer.getVariantTracks() !== 'undefined') {
const realHeight = window.devicePixelRatio * this.videoContainer.clientHeight;
const manifestHeights = this.shakaPlayer.getVariantTracks().map((x) => {
return x.height;
});
const streamHeight = Math.min.apply(Math, manifestHeights.filter((x) => {
return x >= realHeight;
}));
this.shakaPlayer.configure({
'abr': {
'restrictions': {
'maxHeight': streamHeight
}
}
});
}
}).observe(this.videoElement);
}
/**
* Method for applying configurations to the Shaka Player provided by its developer.
* <ul>
* <li> <a href="https://shaka-player-demo.appspot.com/docs/api/shaka.extern.html#.StreamingConfiguration"> Streaming configuration </a> </li>
* <li> <a href="https://shaka-player-demo.appspot.com/docs/api/shaka.extern.html#.AbrConfiguration"> ABR configuration </a> </li>
* <li> <a href="https://shaka-player-demo.appspot.com/docs/api/shaka.extern.html#.PlayerConfiguration"> Further player configuration </a> </li>
* </ul>
* @private
*/
applyShakaSpecificPlayerConfigurations_() {
// Example: this.player.configure("streaming.rebufferingGoal", 4)
this.shakaPlayer.configure("preferredAudioLanguage", "en");
}
/**
* Selects the webVTT thumbnail manifest based on the video manifest file that can be loaded.
* @private
*/
loadVideoManifestAndSelectThumbnailManifest_() {
this.loadMedia_(this.shakaPlayer, this.mediaUrlList).then((manifest) => {
this.manifest = manifest;
let index = this.mediaUrlList.indexOf(manifest);
}).catch(error => {
console.error("Error", error.message);
});
}
/**
* Try to load a media file from a list of media paths as long as there are items in the list.
* @param {Object} player - Class instance of the player.
* @param {Array} files - Files with all media files that the player tries to load in ascending (array position) order.
* @return {string|Promise} When successful, return the path of the media file, else return Promise.reject(reason).
* @private
*/
loadMedia_(player, files) {
function load(path) {
return player.load(path).then(() => {
//console.info('Manifest loaded: ' + path);
return true;
})
.catch(error => {
console.info('Manifest can not be loaded: ' + path);
ShakaPlayerErrorHandler.onError(error);
return false;
});
}
files = files.filter(n => n !== null && n !== undefined);
return load(files[0]).then(success => {
if (success) {
return files[0];
} else {
const tmp = files.slice(1, files.length);
if (tmp == 0) {
this.setErrorImage_(player);
return Promise.reject(new Error('No supported manifest found! Playback cannot be started!'));
}
else
return this.loadMedia_(player, tmp);
}
});
}
/**
* Include user notification as poster in case of video error.
* @param {Object} player - Class instance of the player.
* @private
*/
setErrorImage_(player) {
const backgroundColor = '#F2F2F2';
player.getMediaElement().poster = VectorGraphics.errorImage('Video', backgroundColor);
player.getMediaElement().style.backgroundColor = backgroundColor;
player.getMediaElement().style.transition = undefined;
}
/**
* Configure the user interface with respect to the media content.
* <ul>
* <li> <a href="https://shaka-player-demo.appspot.com/docs/api/tutorial-ui-customization.html"> Overflow menu and control panel configuration </a> </li>
* <li> <a href="https://shaka-player-demo.appspot.com/docs/api/shaka.extern.html#.UIConfiguration"> UI configuration </a> </li>
* </ul>
* @private
*/
configureUIShakaPlayer_() {
let overflowMenu, controlPanel;
if (this.shakaPlayer.isAudioOnly()) {
overflowMenu = ["loop", "playback_rate"];
controlPanel = ["play_pause", "time_and_duration", "spacer", "mute", "volume", "overflow_menu"];
}
else {
overflowMenu = ["loop", "playback_rate", "language", "captions", "quality"];
controlPanel = ["play_pause", "time_and_duration", "spacer", "mute", "volume", "fullscreen", "overflow_menu"];
}
if (!HTML5Video.hasAudio(this.videoContainer)) {
overflowMenu = overflowMenu.filter(v => !['language'].includes(v));
controlPanel = controlPanel.filter(v => !["mute", "volume"].includes(v));
}
if (['m3u8', 'mpd'].includes(this.manifest.split('.').pop().toLowerCase())) {
if (this.shakaPlayer.isLive())
overflowMenu = overflowMenu.filter(v => !['loop', 'playback_rate'].includes(v));
if (!this.shakaPlayer.getAudioLanguages() || this.shakaPlayer.getAudioLanguages().length <= 1)
overflowMenu = overflowMenu.filter(v => !['language'].includes(v));
} else {
overflowMenu = overflowMenu.filter(v => !['language', 'quality'].includes(v));
}
const keyControlViaHTML5VideoElement = true;
let uiConfigurationOptions = {
'controlPanelElements': controlPanel,
'overflowMenuButtons': overflowMenu,
'enableKeyboardPlaybackControls': !keyControlViaHTML5VideoElement,
'playbackRates': [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2],
'showUnbufferedStart': true
};
this.shakaUI.configure(uiConfigurationOptions);
}
/**
* Extend the functionality of the shaka player by providing a method that prevents the control bar from hiding while playing audio.
* @private
*/
extendFunctionalityForAudio_() {
if (this.shakaPlayer.isAudioOnly()) {
const setUIAlwaysVisible = true;
const targetNode = this.videoContainer.querySelector('.shaka-controls-container');
const config = { attributes: true, childList: false, subtree: true };
const callback = (mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type === 'attributes' && mutation.attributeName == 'shown') {
if (mutation.target.getAttribute(mutation.attributeName) == undefined)
mutation.target.setAttribute("shown", setUIAlwaysVisible);
}
}
};
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
}
}
}