videojs-shaka
Version:
video.js shaka player tech
730 lines (582 loc) • 20.1 kB
JavaScript
/*! @name videojs-shaka @version 1.1.2 @license MIT */
import videojs from 'video.js';
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
subClass.__proto__ = superClass;
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
function _getQuality(tech, shakaPlayer) {
var tracks = [];
var levels = shakaPlayer.getVariantTracks().filter(function (t) {
return t.type === 'variant';
});
if (levels.length > 1) {
var autoLevel = {
id: -1,
label: 'auto',
selected: true
};
tracks.push(autoLevel);
}
levels.forEach(function (level, index) {
var track = level;
var label = '';
if (level.height >= 2160) {
label = ' (4k)';
} else if (level.height >= 1440) {
label = ' (2k)';
} else if (level.height >= 720) {
label = ' (HD)';
}
track.label = level.height + 'p' + label;
tracks.push(track);
}); // group tracks by langugage b/c we will need to only display the tracks associated with the current audio track
var sortedTracks = tracks.sort(function (track1, track2) {
if (track1.language > track2.language) {
return -1;
}
if (track2.language > track1.language) {
return 1;
}
if (track1.height > track2.height) {
return -1;
}
if (track2.height > track1.height) {
return 1;
}
if (track1.bandwidth > track2.bandwidth) {
return -1;
}
if (track2.bandwidth > track1.bandwidth) {
return 1;
}
return 0;
}).reduce(function (accumulator, track) {
if (track.height !== accumulator.previousHeight || track.height === accumulator.previousHeight && track.language !== accumulator.previousLanguage) {
accumulator.previousHeight = track.height;
accumulator.previousLanguage = track.language;
accumulator.list.push(track);
}
return accumulator;
}, {
previousHeight: null,
previousLanguage: null,
list: []
}).list;
return sortedTracks;
}
function setupQualityTracks(tech, shakaPlayer) {
tech.trigger('loadedqualitydata', {
qualityData: {
video: _getQuality(tech, shakaPlayer)
},
qualitySwitchCallback: function qualitySwitchCallback(id, type) {
// Update the adaptation.
shakaPlayer.configure({
abr: {
enabled: id === -1
}
}); // Is auto?
if (id === -1) return;
var tracks = shakaPlayer.getVariantTracks().filter(function (t) {
return t.id === id && t.type === 'variant';
});
shakaPlayer.selectVariantTrack(tracks[0],
/* clearBuffer */
true); // fire `variantchanged` event - only supports debug mode right now
// todo - need to figure out how to do this in non debug mode
if (shaka.util.FakeEvent) {
var event = new shaka.util.FakeEvent('variantchanged');
shakaPlayer.dispatchEvent(event);
}
}
});
}
function find(l, f) {
for (var i = 0; i < l.length; i++) {
if (f(l[i])) {
return l[i];
}
}
}
/*
* Attach text tracks from dash.js to videojs
*
* @param {videojs} tech the videojs player tech instance
* @param {array} tracks the tracks loaded by dash.js to attach to videojs
*
* @private
*/
function attachDashTextTracksToVideojs(tech, shakaPlayer, tracks) {
var trackDictionary = []; // Add remote tracks
var tracksAttached = tracks // Map input data to match HTMLTrackElement spec
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLTrackElement
.map(function (track) {
return {
dashTrack: track,
trackConfig: {
label: track.label || track.language,
language: track.language,
srclang: track.language,
kind: track.kind
}
};
}) // Add track to videojs track list
.map(function (_ref) {
var trackConfig = _ref.trackConfig,
dashTrack = _ref.dashTrack;
var remoteTextTrack = tech.addRemoteTextTrack(trackConfig, false);
trackDictionary.push({
textTrack: remoteTextTrack.track,
dashTrack: dashTrack
}); // Don't add the cues becuase we're going to let dash handle it natively. This will ensure
// that dash handle external time text files and fragmented text tracks.
//
// Example file with external time text files:
// https://storage.googleapis.com/shaka-demo-assets/sintel-mp4-wvtt/dash.mpd
return remoteTextTrack;
});
/*
* Scan `videojs.textTracks()` to find one that is showing. Set the dash text track.
*/
function updateActiveDashTextTrack() {
var dashTrackToActivate;
var textTracks = tech.textTracks(); // Iterate through the tracks and find the one marked as showing. If none are showing,
// disable text tracks.
var _loop = function _loop(i) {
var textTrack = textTracks[i];
if (textTrack.mode === 'showing') {
// Find the dash track we want to use
/* jshint loopfunc: true */
var dictionaryLookupResult = find(trackDictionary, function (track) {
return track.textTrack === textTrack;
});
/* jshint loopfunc: false */
dashTrackToActivate = dictionaryLookupResult ? dictionaryLookupResult.dashTrack : null;
}
};
for (var i = 0; i < textTracks.length; i += 1) {
_loop(i);
} // If the text track has changed, then set it in shaka
if (dashTrackToActivate) {
shakaPlayer.selectTextTrack(dashTrackToActivate);
shakaPlayer.setTextTrackVisibility(true);
} else {
shakaPlayer.setTextTrackVisibility(false);
}
} // Update dash when videojs's selected text track changes.
tech.textTracks().on('change', updateActiveDashTextTrack); // Cleanup event listeners whenever we start loading a new source
shakaPlayer.addEventListener('unloading', function () {
tech.textTracks().off('change', updateActiveDashTextTrack);
}); // Initialize the text track on our first run-through
updateActiveDashTextTrack();
return tracksAttached;
}
function setupTextTracks(tech, shakaPlayer) {
// Store the tracks that we've added so we can remove them later.
var dashTracksAttachedToVideoJs = []; // Clear the tracks that we added. We don't clear them all because someone else can add tracks.
function clearDashTracks() {
dashTracksAttachedToVideoJs.forEach(tech.removeRemoteTextTrack.bind(tech));
dashTracksAttachedToVideoJs = [];
}
function handleTextTracksAdded(tracks) {
// Cleanup old tracks
clearDashTracks(); // Don't try to add text tracks if there aren't any or if the app is sideloading webvtt files
if (!tracks.length || tech.options_.sideload) {
shakaPlayer.setTextTrackVisibility(false);
return;
} // Save the tracks so we can remove them later
dashTracksAttachedToVideoJs = attachDashTextTracksToVideojs(tech, shakaPlayer, tracks);
}
handleTextTracksAdded(shakaPlayer.getTextTracks());
}
/**
* Setup audio tracks. Take the tracks from dash and add the tracks to videojs. Listen for when
* videojs changes tracks and apply that to the dash player because videojs doesn't do this
* natively.
*
* @private
* @param {videojs} tech the videojs player tech instance
* @param {videojs.tech} tech the videojs tech being used
*/
function handleAudioTracksAdded(tech, shakaPlayer, tracks) {
var videojsAudioTracks = tech.audioTracks();
function generateIdFromTrackIndex(index) {
return "dash-audio-" + index;
}
function generateLabelFromTrack(track) {
var label = track.language;
if (track.role) {
label += " (" + track.role + ")";
}
return label;
}
function findDashAudioTrack(subDashAudioTracks, videojsAudioTrack) {
return subDashAudioTracks.find(function (track) {
return generateLabelFromTrack(track) === videojsAudioTrack.label;
});
} // Safari creates a single native `AudioTrack` (not `videojs.AudioTrack`) when loading. Clear all
// automatically generated audio tracks so we can create them all ourself.
if (videojsAudioTracks.length) {
tech.clearTracks(['audio']);
}
var currentAudioTrack = tracks[0];
tracks.forEach(function (dashTrack, index) {
var label = generateLabelFromTrack(dashTrack);
if (dashTrack === currentAudioTrack) {
tech.trigger('shakaaudiotrackchange', {
language: dashTrack.language
});
} // Add the track to the player's audio track list.
videojsAudioTracks.addTrack(new videojs.AudioTrack({
enabled: dashTrack === currentAudioTrack,
id: generateIdFromTrackIndex(index),
kind: 'main',
label: label,
language: dashTrack.language
}));
});
var audioTracksChangeHandler = function audioTracksChangeHandler() {
for (var i = 0; i < videojsAudioTracks.length; i++) {
var track = videojsAudioTracks[i];
if (track.enabled) {
// Find the audio track we just selected by the id
var dashAudioTrack = findDashAudioTrack(tracks, track);
if (dashAudioTrack) {
// Set is as the current track
tech.trigger('shakaaudiotrackchange', {
language: dashAudioTrack.language
});
shakaPlayer.selectAudioLanguage(dashAudioTrack.language, dashAudioTrack.role); // Stop looping
continue;
}
}
}
};
videojsAudioTracks.addEventListener('change', audioTracksChangeHandler);
shakaPlayer.addEventListener('unloading', function () {
videojsAudioTracks.removeEventListener('change', audioTracksChangeHandler);
});
}
function setupAudioTracks(tech, shakaPlayer) {
handleAudioTracksAdded(tech, shakaPlayer, shakaPlayer.getAudioLanguagesAndRoles());
}
var version = "1.1.2";
var Html5 = videojs.getTech('Html5'); // Default options for the plugin.
// const defaults = {};
/**
* Shaka Media Controller - Wrapper for HTML5 Media API
*
* @mixes Html5~SourceHandlerAdditions
* @extends Html5
*/
var Shaka =
/*#__PURE__*/
function (_Html) {
_inheritsLoose(Shaka, _Html);
/**
* Create a Shaka plugin instance.
*
* @param {Player} player
* A Video.js Player instance.
*
* @param {Object} [options]
* An optional options object.
*
* While not a core part of the Video.js plugin architecture, a
* second argument of options is a convenient way to accept inputs
* from your plugin's caller.
*/
/**
* Create an instance of this Tech.
*
* @param {Object} [options]
* The key/value store of player options.
*
* @param {Component~ReadyCallback} ready
* Callback function to call when the `HTML5` Tech is ready.
*/
function Shaka(options, ready) {
var _this;
_this = _Html.call(this, options, ready) || this;
_this.vjsPlayer = videojs(options.playerId);
_this.player_.ready(function () {
_this.player_.addClass('vjs-shaka');
});
return _this;
}
var _proto = Shaka.prototype;
_proto.createEl = function createEl() {
this.el_ = Html5.prototype.createEl.apply(this, arguments); // Install built-in polyfills to patch browser incompatibilities.
shaka.polyfill.installAll(); // set debug log level
if (shaka.log) {
if (this.options_.debug) {
shaka.log.setLevel(shaka.log.Level.DEBUG);
} else {
shaka.log.setLevel(shaka.log.Level.ERROR);
}
}
this.shaka_ = new shaka.Player(this.el_);
this.el_.tech = this;
return this.el_;
};
_proto.setSrc = function setSrc(src) {
var me = this;
var shakaOptions = this.options_.configuration || {};
if (typeof shakaOptions.drm === 'function') {
shakaOptions.drm = shakaOptions.drm();
} else {
shakaOptions.drm = shakaOptions.drm || {};
}
if (!shakaOptions.abr) {
shakaOptions.abr = {
enabled: true
};
}
this.shaka_.configure(shakaOptions);
if (this.options_.licenseServerAuth) {
this.shaka_.getNetworkingEngine().registerRequestFilter(this.options_.licenseServerAuth);
}
this.shaka_.addEventListener('buffering', function (event) {
if (event.buffering) {
me.vjsPlayer.trigger('waiting');
} else {
me.vjsPlayer.trigger('playing');
}
});
this.shaka_.addEventListener('error', function (event) {
me.retriggerError(event.detail);
});
this.shaka_.load(src).then(function () {
me.initShakaMenus();
}).catch(me.retriggerError.bind(this));
};
_proto.dispose = function dispose() {
if (this.shaka_) {
this.shaka_.unload();
this.shaka_.destroy();
}
};
_proto.initShakaMenus = function initShakaMenus() {
setupQualityTracks(this, this.shaka_);
setupTextTracks(this, this.shaka_);
setupAudioTracks(this, this.shaka_);
};
_proto.retriggerError = function retriggerError(event) {
var _this2 = this;
var code; // map the shaka player error to the appropriate video.js error
if (event.message && (event.message.indexOf('UNSUPPORTED') > -1 || event.message.indexOf('NOT_SUPPORTED') > -1)) {
code = 4;
} else {
switch (event.category) {
case 1:
code = 2;
break;
case 2:
case 3:
case 4:
code = 3;
break;
case 5:
code = 1;
break;
case 6:
code = 5;
break;
case 7:
case 8:
case 9:
code = 0;
break;
}
}
this.vjsPlayer.error({
code: code,
message: event.code + " - " + event.message
}); // only reset the shaka player in 10ms async, so that the rest of the
// calling function finishes
setTimeout(function () {
_this2.dispose();
}, 10);
};
return Shaka;
}(Html5); // Define default values for the plugin's `state` object here.
Shaka.defaultState = {}; // Include the version number.
Shaka.VERSION = version;
Shaka.isSupported = function () {
return !!window.MediaSource;
};
Shaka.canPlaySource = function (source, tech) {
var dashTypeRE = /^(application\/dash\+xml|application\/x-mpegURL)/i;
if (dashTypeRE.test(source.type)) {
return 'probably';
}
return '';
};
var VjsMenu = videojs.getComponent('Menu');
var QualityMenu =
/*#__PURE__*/
function (_VjsMenu) {
_inheritsLoose(QualityMenu, _VjsMenu);
function QualityMenu(player, options) {
var _this;
_this = _VjsMenu.call(this, player, options) || this;
var me = _assertThisInitialized(_assertThisInitialized(_this));
player.tech_.on('shakaaudiotrackchange', function (event, _ref) {
var language = _ref.language;
me.children().forEach(function (menuItem) {
if (menuItem.options_.label === 'auto') {
menuItem.selected(true);
menuItem.show();
} else if (menuItem.options_.language === language) {
menuItem.selected(false);
menuItem.show();
} else {
menuItem.selected(false);
menuItem.hide();
}
});
});
player.on('qualitytrackchange', function () {
me.hide();
});
return _this;
}
var _proto = QualityMenu.prototype;
_proto.addItem = function addItem(component) {
var _this2 = this;
_VjsMenu.prototype.addItem.call(this, component);
component.on(['tap', 'click'], function () {
var children = _this2.children();
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (component !== child) {
child.selected(false);
}
}
});
};
return QualityMenu;
}(VjsMenu);
var VjsMenuItem = videojs.getComponent('MenuItem');
var QualityMenuItem =
/*#__PURE__*/
function (_VjsMenuItem) {
_inheritsLoose(QualityMenuItem, _VjsMenuItem);
function QualityMenuItem() {
return _VjsMenuItem.apply(this, arguments) || this;
}
var _proto = QualityMenuItem.prototype;
_proto.handleClick = function handleClick() {
_VjsMenuItem.prototype.handleClick.call(this);
this.player_.trigger('qualitytrackchange', this.options_);
this.options_.qualitySwitchCallback(this.options_.id, this.options_.trackType);
};
return QualityMenuItem;
}(VjsMenuItem);
var VjsButton = videojs.getComponent('MenuButton');
var TRACK_CLASS = {
video: 'vjs-icon-hd',
audio: 'vjs-icon-cog',
subtitle: 'vjs-icon-subtitles'
};
var QualityPickerButton =
/*#__PURE__*/
function (_VjsButton) {
_inheritsLoose(QualityPickerButton, _VjsButton);
function QualityPickerButton(player, options) {
return _VjsButton.call(this, player, options) || this;
}
var _proto = QualityPickerButton.prototype;
_proto.createMenu = function createMenu() {
var menu = new QualityMenu(this.player_, this.options_);
var menuItem;
var options;
for (var i = 0; i < this.options_.qualityList.length; i++) {
var quality = this.options_.qualityList[i];
var _this$options_ = this.options_,
qualitySwitchCallback = _this$options_.qualitySwitchCallback,
trackType = _this$options_.trackType;
options = _extends({
qualitySwitchCallback: qualitySwitchCallback,
trackType: trackType
}, quality, {
selectable: true
});
menuItem = new QualityMenuItem(this.player_, options);
menu.addItem(menuItem);
}
return menu;
};
_proto.buildCSSClass = function buildCSSClass() {
return "vjs-quality-button " + TRACK_CLASS[this.options_.trackType] + " vjs-icon-placeholder " + _VjsButton.prototype.buildCSSClass.call(this);
};
_proto.buildWrapperCSSClass = function buildWrapperCSSClass() {
return "vjs-quality-button " + _VjsButton.prototype.buildWrapperCSSClass.call(this);
};
return QualityPickerButton;
}(VjsButton);
function qualityPickerPlugin() {
var player = this;
var SUPPORTED_TRACKS = ['video', 'audio', 'subtitle']; // On later versions `player.tech` is undefined before this...
if (player.tech_) {
player.tech_.on('loadedqualitydata', onQualityData);
} else {
player.ready(function () {
player.tech_.on('loadedqualitydata', onQualityData);
}, true);
}
function onQualityData(event, _ref) {
var qualityData = _ref.qualityData,
qualitySwitchCallback = _ref.qualitySwitchCallback;
var fullscreenToggle = player.controlBar.getChild('fullscreenToggle');
player.controlBar.removeChild(fullscreenToggle);
for (var i = 0; i < SUPPORTED_TRACKS.length; i++) {
var track = SUPPORTED_TRACKS[i];
var name = track + 'PickerButton'; // videojs.utils.toTitleCase
name = name.charAt(0).toUpperCase() + name.slice(1);
var qualityPickerButton = player.controlBar.getChild(name);
if (qualityPickerButton) {
qualityPickerButton.dispose();
player.controlBar.removeChild(qualityPickerButton);
}
if (qualityData[track] && qualityData[track].length > 1) {
qualityPickerButton = new QualityPickerButton(player, {
name: name,
qualityList: qualityData[track],
qualitySwitchCallback: qualitySwitchCallback,
trackType: track
});
player.controlBar.addChild(qualityPickerButton);
}
}
if (fullscreenToggle) {
player.controlBar.addChild(fullscreenToggle);
}
}
}
var Tech = videojs.getTech('Tech'); // Register Shaka as a Tech;
Tech.registerTech('Shaka', Shaka); // Register quality picker plugin
var registerPlugin = videojs.registerPlugin || videojs.plugin;
registerPlugin('qualityPickerPlugin', qualityPickerPlugin);
export default Shaka;