jellyfish-web/src/components/playback/playbackmanager.js
Jeff Sharkey afa56c18af
Support for faster playback rates.
The HTML5 video element already has a well-supported "playbackRate" attribute
which can be used to increase playback rate.  This change wires up that control
to be displayed in the Jellyfish web player.

The playback rates offered are between 0.5x and 2x in 0.25x increments, which
matches the YouTube player.  This change also wires up the ">" and "<" key
events to increase and decrease the playback rate, which mirrors the keyboard
shortcuts supported by YouTube.
2020-08-14 18:25:56 -03:00

3773 lines
130 KiB
JavaScript

import events from 'events';
import datetime from 'datetime';
import appSettings from 'appSettings';
import itemHelper from 'itemHelper';
import pluginManager from 'pluginManager';
import PlayQueueManager from 'playQueueManager';
import * as userSettings from 'userSettings';
import globalize from 'globalize';
import connectionManager from 'connectionManager';
import loading from 'loading';
import appHost from 'apphost';
import screenfull from 'screenfull';
function enableLocalPlaylistManagement(player) {
if (player.getPlaylist) {
return false;
}
if (player.isLocalPlayer) {
return true;
}
return false;
}
function bindToFullscreenChange(player) {
if (screenfull.isEnabled) {
screenfull.on('change', function () {
events.trigger(player, 'fullscreenchange');
});
} else {
// iOS Safari
document.addEventListener('webkitfullscreenchange', function () {
events.trigger(player, 'fullscreenchange');
}, false);
}
}
function triggerPlayerChange(playbackManagerInstance, newPlayer, newTarget, previousPlayer, previousTargetInfo) {
if (!newPlayer && !previousPlayer) {
return;
}
if (newTarget && previousTargetInfo) {
if (newTarget.id === previousTargetInfo.id) {
return;
}
}
events.trigger(playbackManagerInstance, 'playerchange', [newPlayer, newTarget, previousPlayer]);
}
function reportPlayback(playbackManagerInstance, state, player, reportPlaylist, serverId, method, progressEventName) {
if (!serverId) {
// Not a server item
// We can expand on this later and possibly report them
events.trigger(playbackManagerInstance, 'reportplayback', [false]);
return;
}
const info = Object.assign({}, state.PlayState);
info.ItemId = state.NowPlayingItem.Id;
if (progressEventName) {
info.EventName = progressEventName;
}
if (reportPlaylist) {
addPlaylistToPlaybackReport(playbackManagerInstance, info, player, serverId);
}
const apiClient = connectionManager.getApiClient(serverId);
const reportPlaybackPromise = apiClient[method](info);
// Notify that report has been sent
reportPlaybackPromise.then(() => {
events.trigger(playbackManagerInstance, 'reportplayback', [true]);
});
}
function getPlaylistSync(playbackManagerInstance, player) {
player = player || playbackManagerInstance._currentPlayer;
if (player && !enableLocalPlaylistManagement(player)) {
return player.getPlaylistSync();
}
return playbackManagerInstance._playQueueManager.getPlaylist();
}
function addPlaylistToPlaybackReport(playbackManagerInstance, info, player, serverId) {
info.NowPlayingQueue = getPlaylistSync(playbackManagerInstance, player).map(function (i) {
const itemInfo = {
Id: i.Id,
PlaylistItemId: i.PlaylistItemId
};
if (i.ServerId !== serverId) {
itemInfo.ServerId = i.ServerId;
}
return itemInfo;
});
}
function normalizeName(t) {
return t.toLowerCase().replace(' ', '');
}
function getItemsForPlayback(serverId, query) {
const apiClient = connectionManager.getApiClient(serverId);
if (query.Ids && query.Ids.split(',').length === 1) {
const itemId = query.Ids.split(',');
return apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(function (item) {
return {
Items: [item],
TotalRecordCount: 1
};
});
} else {
query.Limit = query.Limit || 300;
query.Fields = 'Chapters';
query.ExcludeLocationTypes = 'Virtual';
query.EnableTotalRecordCount = false;
query.CollapseBoxSetItems = false;
return apiClient.getItems(apiClient.getCurrentUserId(), query);
}
}
function createStreamInfoFromUrlItem(item) {
// Check item.Path for games
return {
url: item.Url || item.Path,
playMethod: 'DirectPlay',
item: item,
textTracks: [],
mediaType: item.MediaType
};
}
function mergePlaybackQueries(obj1, obj2) {
const query = Object.assign(obj1, obj2);
const filters = query.Filters ? query.Filters.split(',') : [];
if (filters.indexOf('IsNotFolder') === -1) {
filters.push('IsNotFolder');
}
query.Filters = filters.join(',');
return query;
}
function backdropImageUrl(apiClient, item, options) {
options = options || {};
options.type = options.type || 'Backdrop';
// If not resizing, get the original image
if (!options.maxWidth && !options.width && !options.maxHeight && !options.height) {
options.quality = 100;
}
if (item.BackdropImageTags && item.BackdropImageTags.length) {
options.tag = item.BackdropImageTags[0];
return apiClient.getScaledImageUrl(item.Id, options);
}
if (item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) {
options.tag = item.ParentBackdropImageTags[0];
return apiClient.getScaledImageUrl(item.ParentBackdropItemId, options);
}
return null;
}
function getMimeType(type, container) {
container = (container || '').toLowerCase();
if (type === 'audio') {
if (container === 'opus') {
return 'audio/ogg';
}
if (container === 'webma') {
return 'audio/webm';
}
if (container === 'm4a') {
return 'audio/mp4';
}
} else if (type === 'video') {
if (container === 'mkv') {
return 'video/x-matroska';
}
if (container === 'm4v') {
return 'video/mp4';
}
if (container === 'mov') {
return 'video/quicktime';
}
if (container === 'mpg') {
return 'video/mpeg';
}
if (container === 'flv') {
return 'video/x-flv';
}
}
return type + '/' + container;
}
function getParam(name, url) {
name = name.replace(/[\[]/, '\\\[').replace(/[\]]/, '\\\]');
const regexS = '[\\?&]' + name + '=([^&#]*)';
const regex = new RegExp(regexS, 'i');
const results = regex.exec(url);
if (results == null) {
return '';
} else {
return decodeURIComponent(results[1].replace(/\+/g, ' '));
}
}
function isAutomaticPlayer(player) {
if (player.isLocalPlayer) {
return true;
}
return false;
}
function getAutomaticPlayers(instance, forceLocalPlayer) {
if (!forceLocalPlayer) {
const player = instance._currentPlayer;
if (player && !isAutomaticPlayer(player)) {
return [player];
}
}
return instance.getPlayers().filter(isAutomaticPlayer);
}
function isServerItem(item) {
if (!item.Id) {
return false;
}
return true;
}
function enableIntros(item) {
if (item.MediaType !== 'Video') {
return false;
}
if (item.Type === 'TvChannel') {
return false;
}
// disable for in-progress recordings
if (item.Status === 'InProgress') {
return false;
}
return isServerItem(item);
}
function getIntros(firstItem, apiClient, options) {
if (options.startPositionTicks || options.startIndex || options.fullscreen === false || !enableIntros(firstItem) || !userSettings.enableCinemaMode()) {
return Promise.resolve({
Items: []
});
}
return apiClient.getIntros(firstItem.Id).then(function (result) {
return result;
}, function (err) {
return Promise.resolve({
Items: []
});
});
}
function getAudioMaxValues(deviceProfile) {
// TODO - this could vary per codec and should be done on the server using the entire profile
let maxAudioSampleRate = null;
let maxAudioBitDepth = null;
let maxAudioBitrate = null;
deviceProfile.CodecProfiles.map(function (codecProfile) {
if (codecProfile.Type === 'Audio') {
(codecProfile.Conditions || []).map(function (condition) {
if (condition.Condition === 'LessThanEqual' && condition.Property === 'AudioBitDepth') {
return maxAudioBitDepth = condition.Value;
} else if (condition.Condition === 'LessThanEqual' && condition.Property === 'AudioSampleRate') {
return maxAudioSampleRate = condition.Value;
} else if (condition.Condition === 'LessThanEqual' && condition.Property === 'AudioBitrate') {
return maxAudioBitrate = condition.Value;
}
});
}
});
return {
maxAudioSampleRate: maxAudioSampleRate,
maxAudioBitDepth: maxAudioBitDepth,
maxAudioBitrate: maxAudioBitrate
};
}
let startingPlaySession = new Date().getTime();
function getAudioStreamUrl(item, transcodingProfile, directPlayContainers, maxBitrate, apiClient, maxAudioSampleRate, maxAudioBitDepth, maxAudioBitrate, startPosition) {
const url = 'Audio/' + item.Id + '/universal';
startingPlaySession++;
return apiClient.getUrl(url, {
UserId: apiClient.getCurrentUserId(),
DeviceId: apiClient.deviceId(),
MaxStreamingBitrate: maxAudioBitrate || maxBitrate,
Container: directPlayContainers,
TranscodingContainer: transcodingProfile.Container || null,
TranscodingProtocol: transcodingProfile.Protocol || null,
AudioCodec: transcodingProfile.AudioCodec,
MaxAudioSampleRate: maxAudioSampleRate,
MaxAudioBitDepth: maxAudioBitDepth,
api_key: apiClient.accessToken(),
PlaySessionId: startingPlaySession,
StartTimeTicks: startPosition || 0,
EnableRedirection: true,
EnableRemoteMedia: appHost.supports('remoteaudio')
});
}
function getAudioStreamUrlFromDeviceProfile(item, deviceProfile, maxBitrate, apiClient, startPosition) {
const transcodingProfile = deviceProfile.TranscodingProfiles.filter(function (p) {
return p.Type === 'Audio' && p.Context === 'Streaming';
})[0];
let directPlayContainers = '';
deviceProfile.DirectPlayProfiles.map(function (p) {
if (p.Type === 'Audio') {
if (directPlayContainers) {
directPlayContainers += ',' + p.Container;
} else {
directPlayContainers = p.Container;
}
if (p.AudioCodec) {
directPlayContainers += '|' + p.AudioCodec;
}
}
});
const maxValues = getAudioMaxValues(deviceProfile);
return getAudioStreamUrl(item, transcodingProfile, directPlayContainers, maxBitrate, apiClient, maxValues.maxAudioSampleRate, maxValues.maxAudioBitDepth, maxValues.maxAudioBitrate, startPosition);
}
function getStreamUrls(items, deviceProfile, maxBitrate, apiClient, startPosition) {
const audioTranscodingProfile = deviceProfile.TranscodingProfiles.filter(function (p) {
return p.Type === 'Audio' && p.Context === 'Streaming';
})[0];
let audioDirectPlayContainers = '';
deviceProfile.DirectPlayProfiles.map(function (p) {
if (p.Type === 'Audio') {
if (audioDirectPlayContainers) {
audioDirectPlayContainers += ',' + p.Container;
} else {
audioDirectPlayContainers = p.Container;
}
if (p.AudioCodec) {
audioDirectPlayContainers += '|' + p.AudioCodec;
}
}
});
const maxValues = getAudioMaxValues(deviceProfile);
const streamUrls = [];
for (let i = 0, length = items.length; i < length; i++) {
const item = items[i];
let streamUrl;
if (item.MediaType === 'Audio' && !itemHelper.isLocalItem(item)) {
streamUrl = getAudioStreamUrl(item, audioTranscodingProfile, audioDirectPlayContainers, maxBitrate, apiClient, maxValues.maxAudioSampleRate, maxValues.maxAudioBitDepth, maxValues.maxAudioBitrate, startPosition);
}
streamUrls.push(streamUrl || '');
if (i === 0) {
startPosition = 0;
}
}
return Promise.resolve(streamUrls);
}
function setStreamUrls(items, deviceProfile, maxBitrate, apiClient, startPosition) {
return getStreamUrls(items, deviceProfile, maxBitrate, apiClient, startPosition).then(function (streamUrls) {
for (let i = 0, length = items.length; i < length; i++) {
const item = items[i];
const streamUrl = streamUrls[i];
if (streamUrl) {
item.PresetMediaSource = {
StreamUrl: streamUrl,
Id: item.Id,
MediaStreams: [],
RunTimeTicks: item.RunTimeTicks
};
}
}
});
}
function getPlaybackInfo(player,
apiClient,
item,
deviceProfile,
maxBitrate,
startPosition,
isPlayback,
mediaSourceId,
audioStreamIndex,
subtitleStreamIndex,
liveStreamId,
enableDirectPlay,
enableDirectStream,
allowVideoStreamCopy,
allowAudioStreamCopy) {
if (!itemHelper.isLocalItem(item) && item.MediaType === 'Audio') {
return Promise.resolve({
MediaSources: [
{
StreamUrl: getAudioStreamUrlFromDeviceProfile(item, deviceProfile, maxBitrate, apiClient, startPosition),
Id: item.Id,
MediaStreams: [],
RunTimeTicks: item.RunTimeTicks
}]
});
}
if (item.PresetMediaSource) {
return Promise.resolve({
MediaSources: [item.PresetMediaSource]
});
}
const itemId = item.Id;
const query = {
UserId: apiClient.getCurrentUserId(),
StartTimeTicks: startPosition || 0
};
if (isPlayback) {
query.IsPlayback = true;
query.AutoOpenLiveStream = true;
} else {
query.IsPlayback = false;
query.AutoOpenLiveStream = false;
}
if (audioStreamIndex != null) {
query.AudioStreamIndex = audioStreamIndex;
}
if (subtitleStreamIndex != null) {
query.SubtitleStreamIndex = subtitleStreamIndex;
}
if (enableDirectPlay != null) {
query.EnableDirectPlay = enableDirectPlay;
}
if (enableDirectStream != null) {
query.EnableDirectStream = enableDirectStream;
}
if (allowVideoStreamCopy != null) {
query.AllowVideoStreamCopy = allowVideoStreamCopy;
}
if (allowAudioStreamCopy != null) {
query.AllowAudioStreamCopy = allowAudioStreamCopy;
}
if (mediaSourceId) {
query.MediaSourceId = mediaSourceId;
}
if (liveStreamId) {
query.LiveStreamId = liveStreamId;
}
if (maxBitrate) {
query.MaxStreamingBitrate = maxBitrate;
}
if (player.enableMediaProbe && !player.enableMediaProbe(item)) {
query.EnableMediaProbe = false;
}
// lastly, enforce player overrides for special situations
if (query.EnableDirectStream !== false) {
if (player.supportsPlayMethod && !player.supportsPlayMethod('DirectStream', item)) {
query.EnableDirectStream = false;
}
}
if (player.getDirectPlayProtocols) {
query.DirectPlayProtocols = player.getDirectPlayProtocols();
}
return apiClient.getPlaybackInfo(itemId, query, deviceProfile);
}
function getOptimalMediaSource(apiClient, item, versions) {
const promises = versions.map(function (v) {
return supportsDirectPlay(apiClient, item, v);
});
if (!promises.length) {
return Promise.reject();
}
return Promise.all(promises).then(function (results) {
for (let i = 0, length = versions.length; i < length; i++) {
versions[i].enableDirectPlay = results[i] || false;
}
let optimalVersion = versions.filter(function (v) {
return v.enableDirectPlay;
})[0];
if (!optimalVersion) {
optimalVersion = versions.filter(function (v) {
return v.SupportsDirectStream;
})[0];
}
optimalVersion = optimalVersion || versions.filter(function (s) {
return s.SupportsTranscoding;
})[0];
return optimalVersion || versions[0];
});
}
function getLiveStream(player, apiClient, item, playSessionId, deviceProfile, maxBitrate, startPosition, mediaSource, audioStreamIndex, subtitleStreamIndex) {
const postData = {
DeviceProfile: deviceProfile,
OpenToken: mediaSource.OpenToken
};
const query = {
UserId: apiClient.getCurrentUserId(),
StartTimeTicks: startPosition || 0,
ItemId: item.Id,
PlaySessionId: playSessionId
};
if (maxBitrate) {
query.MaxStreamingBitrate = maxBitrate;
}
if (audioStreamIndex != null) {
query.AudioStreamIndex = audioStreamIndex;
}
if (subtitleStreamIndex != null) {
query.SubtitleStreamIndex = subtitleStreamIndex;
}
// lastly, enforce player overrides for special situations
if (query.EnableDirectStream !== false) {
if (player.supportsPlayMethod && !player.supportsPlayMethod('DirectStream', item)) {
query.EnableDirectStream = false;
}
}
return apiClient.ajax({
url: apiClient.getUrl('LiveStreams/Open', query),
type: 'POST',
data: JSON.stringify(postData),
contentType: 'application/json',
dataType: 'json'
});
}
function isHostReachable(mediaSource, apiClient) {
if (mediaSource.IsRemote) {
return Promise.resolve(true);
}
return apiClient.getEndpointInfo().then(function (endpointInfo) {
if (endpointInfo.IsInNetwork) {
if (!endpointInfo.IsLocal) {
const path = (mediaSource.Path || '').toLowerCase();
if (path.indexOf('localhost') !== -1 || path.indexOf('127.0.0.1') !== -1) {
// This will only work if the app is on the same machine as the server
return Promise.resolve(false);
}
}
return Promise.resolve(true);
}
// media source is in network, but connection is out of network
return Promise.resolve(false);
});
}
function supportsDirectPlay(apiClient, item, mediaSource) {
// folder rip hacks due to not yet being supported by the stream building engine
const isFolderRip = mediaSource.VideoType === 'BluRay' || mediaSource.VideoType === 'Dvd' || mediaSource.VideoType === 'HdDvd';
if (mediaSource.SupportsDirectPlay || isFolderRip) {
if (mediaSource.IsRemote && !appHost.supports('remotevideo')) {
return Promise.resolve(false);
}
if (mediaSource.Protocol === 'Http' && !mediaSource.RequiredHttpHeaders.length) {
// If this is the only way it can be played, then allow it
if (!mediaSource.SupportsDirectStream && !mediaSource.SupportsTranscoding) {
return Promise.resolve(true);
} else {
return isHostReachable(mediaSource, apiClient);
}
} else if (mediaSource.Protocol === 'File') {
return new Promise(function (resolve, reject) {
// Determine if the file can be accessed directly
import('filesystem').then((filesystem) => {
const method = isFolderRip ?
'directoryExists' :
'fileExists';
filesystem[method](mediaSource.Path).then(function () {
resolve(true);
}, function () {
resolve(false);
});
});
});
}
}
return Promise.resolve(false);
}
function validatePlaybackInfoResult(instance, result) {
if (result.ErrorCode) {
showPlaybackInfoErrorMessage(instance, result.ErrorCode);
return false;
}
return true;
}
function showPlaybackInfoErrorMessage(instance, errorCode, playNextTrack) {
import('alert').then(({ default: alert }) => {
alert({
text: globalize.translate('PlaybackError' + errorCode),
title: globalize.translate('HeaderPlaybackError')
}).then(function () {
if (playNextTrack) {
instance.nextTrack();
}
});
});
}
function normalizePlayOptions(playOptions) {
playOptions.fullscreen = playOptions.fullscreen !== false;
}
function truncatePlayOptions(playOptions) {
return {
fullscreen: playOptions.fullscreen,
mediaSourceId: playOptions.mediaSourceId,
audioStreamIndex: playOptions.audioStreamIndex,
subtitleStreamIndex: playOptions.subtitleStreamIndex,
startPositionTicks: playOptions.startPositionTicks
};
}
function getNowPlayingItemForReporting(player, item, mediaSource) {
const nowPlayingItem = Object.assign({}, item);
if (mediaSource) {
nowPlayingItem.RunTimeTicks = mediaSource.RunTimeTicks;
nowPlayingItem.MediaStreams = mediaSource.MediaStreams;
// not needed
nowPlayingItem.MediaSources = null;
}
nowPlayingItem.RunTimeTicks = nowPlayingItem.RunTimeTicks || player.duration() * 10000;
return nowPlayingItem;
}
function displayPlayerIndividually(player) {
return !player.isLocalPlayer;
}
function createTarget(instance, player) {
return {
name: player.name,
id: player.id,
playerName: player.name,
playableMediaTypes: ['Audio', 'Video', 'Photo', 'Book'].map(player.canPlayMediaType),
isLocalPlayer: player.isLocalPlayer,
supportedCommands: instance.getSupportedCommands(player)
};
}
function getPlayerTargets(player) {
if (player.getTargets) {
return player.getTargets();
}
return Promise.resolve([createTarget(player)]);
}
function sortPlayerTargets(a, b) {
let aVal = a.isLocalPlayer ? 0 : 1;
let bVal = b.isLocalPlayer ? 0 : 1;
aVal = aVal.toString() + a.name;
bVal = bVal.toString() + b.name;
return aVal.localeCompare(bVal);
}
class PlaybackManager {
constructor() {
const self = this;
const players = [];
let currentTargetInfo;
let currentPairingId = null;
this._playNextAfterEnded = true;
const playerStates = {};
this._playQueueManager = new PlayQueueManager();
self.currentItem = function (player) {
if (!player) {
throw new Error('player cannot be null');
}
if (player.currentItem) {
return player.currentItem();
}
const data = getPlayerData(player);
return data.streamInfo ? data.streamInfo.item : null;
};
self.currentMediaSource = function (player) {
if (!player) {
throw new Error('player cannot be null');
}
if (player.currentMediaSource) {
return player.currentMediaSource();
}
const data = getPlayerData(player);
return data.streamInfo ? data.streamInfo.mediaSource : null;
};
self.playMethod = function (player) {
if (!player) {
throw new Error('player cannot be null');
}
if (player.playMethod) {
return player.playMethod();
}
const data = getPlayerData(player);
return data.streamInfo ? data.streamInfo.playMethod : null;
};
self.playSessionId = function (player) {
if (!player) {
throw new Error('player cannot be null');
}
if (player.playSessionId) {
return player.playSessionId();
}
const data = getPlayerData(player);
return data.streamInfo ? data.streamInfo.playSessionId : null;
};
self.getPlayerInfo = function () {
const player = self._currentPlayer;
if (!player) {
return null;
}
const target = currentTargetInfo || {};
return {
name: player.name,
isLocalPlayer: player.isLocalPlayer,
id: target.id,
deviceName: target.deviceName,
playableMediaTypes: target.playableMediaTypes,
supportedCommands: target.supportedCommands
};
};
self.setActivePlayer = function (player, targetInfo) {
if (player === 'localplayer' || player.name === 'localplayer') {
if (self._currentPlayer && self._currentPlayer.isLocalPlayer) {
return;
}
setCurrentPlayerInternal(null, null);
return;
}
if (typeof (player) === 'string') {
player = players.filter(function (p) {
return p.name === player;
})[0];
}
if (!player) {
throw new Error('null player');
}
setCurrentPlayerInternal(player, targetInfo);
};
self.trySetActivePlayer = function (player, targetInfo) {
if (player === 'localplayer' || player.name === 'localplayer') {
if (self._currentPlayer && self._currentPlayer.isLocalPlayer) {
return;
}
return;
}
if (typeof (player) === 'string') {
player = players.filter(function (p) {
return p.name === player;
})[0];
}
if (!player) {
throw new Error('null player');
}
if (currentPairingId === targetInfo.id) {
return;
}
currentPairingId = targetInfo.id;
const promise = player.tryPair ?
player.tryPair(targetInfo) :
Promise.resolve();
events.trigger(self, 'pairing');
promise.then(function () {
events.trigger(self, 'paired');
setCurrentPlayerInternal(player, targetInfo);
}, function () {
events.trigger(self, 'pairerror');
if (currentPairingId === targetInfo.id) {
currentPairingId = null;
}
});
};
self.getTargets = function () {
const promises = players.filter(displayPlayerIndividually).map(getPlayerTargets);
return Promise.all(promises).then(function (responses) {
return connectionManager.currentApiClient().getCurrentUser().then(function (user) {
const targets = [];
targets.push({
name: globalize.translate('HeaderMyDevice'),
id: 'localplayer',
playerName: 'localplayer',
playableMediaTypes: ['Audio', 'Video', 'Photo', 'Book'],
isLocalPlayer: true,
supportedCommands: self.getSupportedCommands({
isLocalPlayer: true
}),
user: user
});
for (let i = 0; i < responses.length; i++) {
const subTargets = responses[i];
for (let j = 0; j < subTargets.length; j++) {
targets.push(subTargets[j]);
}
}
return targets.sort(sortPlayerTargets);
});
});
};
function getCurrentSubtitleStream(player) {
if (!player) {
throw new Error('player cannot be null');
}
const index = getPlayerData(player).subtitleStreamIndex;
if (index == null || index === -1) {
return null;
}
return getSubtitleStream(player, index);
}
function getSubtitleStream(player, index) {
return self.subtitleTracks(player).filter(function (s) {
return s.Type === 'Subtitle' && s.Index === index;
})[0];
}
self.getPlaylist = function (player) {
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player)) {
if (player.getPlaylistSync) {
return Promise.resolve(player.getPlaylistSync());
}
return player.getPlaylist();
}
return Promise.resolve(self._playQueueManager.getPlaylist());
};
function removeCurrentPlayer(player) {
const previousPlayer = self._currentPlayer;
if (!previousPlayer || player.id === previousPlayer.id) {
setCurrentPlayerInternal(null);
}
}
function setCurrentPlayerInternal(player, targetInfo) {
const previousPlayer = self._currentPlayer;
const previousTargetInfo = currentTargetInfo;
if (player && !targetInfo && player.isLocalPlayer) {
targetInfo = createTarget(self, player);
}
if (player && !targetInfo) {
throw new Error('targetInfo cannot be null');
}
currentPairingId = null;
self._currentPlayer = player;
currentTargetInfo = targetInfo;
if (targetInfo) {
console.debug('Active player: ' + JSON.stringify(targetInfo));
}
if (previousPlayer) {
self.endPlayerUpdates(previousPlayer);
}
if (player) {
self.beginPlayerUpdates(player);
}
triggerPlayerChange(self, player, targetInfo, previousPlayer, previousTargetInfo);
}
self.isPlaying = function (player) {
player = player || self._currentPlayer;
if (player) {
if (player.isPlaying) {
return player.isPlaying();
}
}
return player != null && player.currentSrc() != null;
};
self.isPlayingMediaType = function (mediaType, player) {
player = player || self._currentPlayer;
if (player) {
if (player.isPlaying) {
return player.isPlaying(mediaType);
}
}
if (self.isPlaying(player)) {
const playerData = getPlayerData(player);
return playerData.streamInfo.mediaType === mediaType;
}
return false;
};
self.isPlayingLocally = function (mediaTypes, player) {
player = player || self._currentPlayer;
if (!player || !player.isLocalPlayer) {
return false;
}
return mediaTypes.filter(function (mediaType) {
return self.isPlayingMediaType(mediaType, player);
}).length > 0;
};
self.isPlayingVideo = function (player) {
return self.isPlayingMediaType('Video', player);
};
self.isPlayingAudio = function (player) {
return self.isPlayingMediaType('Audio', player);
};
self.getPlayers = function () {
return players;
};
function getDefaultPlayOptions() {
return {
fullscreen: true
};
}
self.canPlay = function (item) {
const itemType = item.Type;
if (itemType === 'PhotoAlbum' || itemType === 'MusicGenre' || itemType === 'Season' || itemType === 'Series' || itemType === 'BoxSet' || itemType === 'MusicAlbum' || itemType === 'MusicArtist' || itemType === 'Playlist') {
return true;
}
if (item.LocationType === 'Virtual') {
if (itemType !== 'Program') {
return false;
}
}
if (itemType === 'Program') {
if (!item.EndDate || !item.StartDate) {
return false;
}
if (new Date().getTime() > datetime.parseISO8601Date(item.EndDate).getTime() || new Date().getTime() < datetime.parseISO8601Date(item.StartDate).getTime()) {
return false;
}
}
//var mediaType = item.MediaType;
return getPlayer(item, getDefaultPlayOptions()) != null;
};
self.toggleAspectRatio = function (player) {
player = player || self._currentPlayer;
if (player) {
const current = self.getAspectRatio(player);
const supported = self.getSupportedAspectRatios(player);
let index = -1;
for (let i = 0, length = supported.length; i < length; i++) {
if (supported[i].id === current) {
index = i;
break;
}
}
index++;
if (index >= supported.length) {
index = 0;
}
self.setAspectRatio(supported[index].id, player);
}
};
self.setAspectRatio = function (val, player) {
player = player || self._currentPlayer;
if (player && player.setAspectRatio) {
player.setAspectRatio(val);
}
};
self.getSupportedAspectRatios = function (player) {
player = player || self._currentPlayer;
if (player && player.getSupportedAspectRatios) {
return player.getSupportedAspectRatios();
}
return [];
};
self.getAspectRatio = function (player) {
player = player || self._currentPlayer;
if (player && player.getAspectRatio) {
return player.getAspectRatio();
}
};
self.increasePlaybackRate = function (player) {
player = player || self._currentPlayer;
if (player) {
let current = self.getPlaybackRate(player);
let supported = self.getSupportedPlaybackRates(player);
let index = -1;
for (let i = 0, length = supported.length; i < length; i++) {
if (supported[i].id === current) {
index = i;
break;
}
}
index = Math.min(index + 1, supported.length - 1);
self.setPlaybackRate(supported[index].id, player);
}
};
self.decreasePlaybackRate = function (player) {
player = player || self._currentPlayer;
if (player) {
let current = self.getPlaybackRate(player);
let supported = self.getSupportedPlaybackRates(player);
let index = -1;
for (let i = 0, length = supported.length; i < length; i++) {
if (supported[i].id === current) {
index = i;
break;
}
}
index = Math.max(index - 1, 0);
self.setPlaybackRate(supported[index].id, player);
}
};
self.getSupportedPlaybackRates = function (player) {
player = player || self._currentPlayer;
if (player && player.getSupportedPlaybackRates) {
return player.getSupportedPlaybackRates();
}
return [];
};
let brightnessOsdLoaded;
self.setBrightness = function (val, player) {
player = player || self._currentPlayer;
if (player) {
if (!brightnessOsdLoaded) {
brightnessOsdLoaded = true;
// TODO: Have this trigger an event instead to get the osd out of here
import('brightnessOsd').then();
}
player.setBrightness(val);
}
};
self.getBrightness = function (player) {
player = player || self._currentPlayer;
if (player) {
return player.getBrightness();
}
};
self.setVolume = function (val, player) {
player = player || self._currentPlayer;
if (player) {
player.setVolume(val);
}
};
self.getVolume = function (player) {
player = player || self._currentPlayer;
if (player) {
return player.getVolume();
}
};
self.volumeUp = function (player) {
player = player || self._currentPlayer;
if (player) {
player.volumeUp();
}
};
self.volumeDown = function (player) {
player = player || self._currentPlayer;
if (player) {
player.volumeDown();
}
};
self.changeAudioStream = function (player) {
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player)) {
return player.changeAudioStream();
}
if (!player) {
return;
}
const currentMediaSource = self.currentMediaSource(player);
const mediaStreams = [];
for (let i = 0, length = currentMediaSource.MediaStreams.length; i < length; i++) {
if (currentMediaSource.MediaStreams[i].Type === 'Audio') {
mediaStreams.push(currentMediaSource.MediaStreams[i]);
}
}
// Nothing to change
if (mediaStreams.length <= 1) {
return;
}
const currentStreamIndex = self.getAudioStreamIndex(player);
let indexInList = -1;
for (let i = 0, length = mediaStreams.length; i < length; i++) {
if (mediaStreams[i].Index === currentStreamIndex) {
indexInList = i;
break;
}
}
let nextIndex = indexInList + 1;
if (nextIndex >= mediaStreams.length) {
nextIndex = 0;
}
nextIndex = nextIndex === -1 ? -1 : mediaStreams[nextIndex].Index;
self.setAudioStreamIndex(nextIndex, player);
};
self.changeSubtitleStream = function (player) {
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player)) {
return player.changeSubtitleStream();
}
if (!player) {
return;
}
const currentMediaSource = self.currentMediaSource(player);
const mediaStreams = [];
for (let i = 0, length = currentMediaSource.MediaStreams.length; i < length; i++) {
if (currentMediaSource.MediaStreams[i].Type === 'Subtitle') {
mediaStreams.push(currentMediaSource.MediaStreams[i]);
}
}
// No known streams, nothing to change
if (!mediaStreams.length) {
return;
}
const currentStreamIndex = self.getSubtitleStreamIndex(player);
let indexInList = -1;
for (let i = 0, length = mediaStreams.length; i < length; i++) {
if (mediaStreams[i].Index === currentStreamIndex) {
indexInList = i;
break;
}
}
let nextIndex = indexInList + 1;
if (nextIndex >= mediaStreams.length) {
nextIndex = -1;
}
nextIndex = nextIndex === -1 ? -1 : mediaStreams[nextIndex].Index;
self.setSubtitleStreamIndex(nextIndex, player);
};
self.getAudioStreamIndex = function (player) {
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player)) {
return player.getAudioStreamIndex();
}
return getPlayerData(player).audioStreamIndex;
};
function isAudioStreamSupported(mediaSource, index, deviceProfile) {
let mediaStream;
const mediaStreams = mediaSource.MediaStreams;
for (let i = 0, length = mediaStreams.length; i < length; i++) {
if (mediaStreams[i].Type === 'Audio' && mediaStreams[i].Index === index) {
mediaStream = mediaStreams[i];
break;
}
}
if (!mediaStream) {
return false;
}
const codec = (mediaStream.Codec || '').toLowerCase();
if (!codec) {
return false;
}
const profiles = deviceProfile.DirectPlayProfiles || [];
return profiles.filter(function (p) {
if (p.Type === 'Video') {
if (!p.AudioCodec) {
return true;
}
// This is an exclusion filter
if (p.AudioCodec.indexOf('-') === 0) {
return p.AudioCodec.toLowerCase().indexOf(codec) === -1;
}
return p.AudioCodec.toLowerCase().indexOf(codec) !== -1;
}
return false;
}).length > 0;
}
self.setAudioStreamIndex = function (index, player) {
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player)) {
return player.setAudioStreamIndex(index);
}
if (self.playMethod(player) === 'Transcode' || !player.canSetAudioStreamIndex()) {
changeStream(player, getCurrentTicks(player), { AudioStreamIndex: index });
getPlayerData(player).audioStreamIndex = index;
} else {
// See if the player supports the track without transcoding
player.getDeviceProfile(self.currentItem(player)).then(function (profile) {
if (isAudioStreamSupported(self.currentMediaSource(player), index, profile)) {
player.setAudioStreamIndex(index);
getPlayerData(player).audioStreamIndex = index;
} else {
changeStream(player, getCurrentTicks(player), { AudioStreamIndex: index });
getPlayerData(player).audioStreamIndex = index;
}
});
}
};
function getSavedMaxStreamingBitrate(apiClient, mediaType) {
if (!apiClient) {
// This should hopefully never happen
apiClient = connectionManager.currentApiClient();
}
const endpointInfo = apiClient.getSavedEndpointInfo() || {};
return appSettings.maxStreamingBitrate(endpointInfo.IsInNetwork, mediaType);
}
self.getMaxStreamingBitrate = function (player) {
player = player || self._currentPlayer;
if (player && player.getMaxStreamingBitrate) {
return player.getMaxStreamingBitrate();
}
const playerData = getPlayerData(player);
if (playerData.maxStreamingBitrate) {
return playerData.maxStreamingBitrate;
}
const mediaType = playerData.streamInfo ? playerData.streamInfo.mediaType : null;
const currentItem = self.currentItem(player);
const apiClient = currentItem ? connectionManager.getApiClient(currentItem.ServerId) : connectionManager.currentApiClient();
return getSavedMaxStreamingBitrate(apiClient, mediaType);
};
self.enableAutomaticBitrateDetection = function (player) {
player = player || self._currentPlayer;
if (player && player.enableAutomaticBitrateDetection) {
return player.enableAutomaticBitrateDetection();
}
const playerData = getPlayerData(player);
const mediaType = playerData.streamInfo ? playerData.streamInfo.mediaType : null;
const currentItem = self.currentItem(player);
const apiClient = currentItem ? connectionManager.getApiClient(currentItem.ServerId) : connectionManager.currentApiClient();
const endpointInfo = apiClient.getSavedEndpointInfo() || {};
return appSettings.enableAutomaticBitrateDetection(endpointInfo.IsInNetwork, mediaType);
};
self.setMaxStreamingBitrate = function (options, player) {
player = player || self._currentPlayer;
if (player && player.setMaxStreamingBitrate) {
return player.setMaxStreamingBitrate(options);
}
const apiClient = connectionManager.getApiClient(self.currentItem(player).ServerId);
apiClient.getEndpointInfo().then(function (endpointInfo) {
const playerData = getPlayerData(player);
const mediaType = playerData.streamInfo ? playerData.streamInfo.mediaType : null;
let promise;
if (options.enableAutomaticBitrateDetection) {
appSettings.enableAutomaticBitrateDetection(endpointInfo.IsInNetwork, mediaType, true);
promise = apiClient.detectBitrate(true);
} else {
appSettings.enableAutomaticBitrateDetection(endpointInfo.IsInNetwork, mediaType, false);
promise = Promise.resolve(options.maxBitrate);
}
promise.then(function (bitrate) {
appSettings.maxStreamingBitrate(endpointInfo.IsInNetwork, mediaType, bitrate);
changeStream(player, getCurrentTicks(player), {
MaxStreamingBitrate: bitrate
});
});
});
};
self.isFullscreen = function (player) {
player = player || self._currentPlayer;
if (!player.isLocalPlayer || player.isFullscreen) {
return player.isFullscreen();
}
if (!screenfull.isEnabled) {
// iOS Safari
return document.webkitIsFullScreen;
}
return screenfull.isFullscreen;
};
self.toggleFullscreen = function (player) {
player = player || self._currentPlayer;
if (!player.isLocalPlayer || player.toggleFulscreen) {
return player.toggleFulscreen();
}
if (screenfull.isEnabled) {
screenfull.toggle();
} else {
// iOS Safari
if (document.webkitIsFullScreen && document.webkitCancelFullscreen) {
document.webkitCancelFullscreen();
} else {
const elem = document.querySelector('video');
if (elem && elem.webkitEnterFullscreen) {
elem.webkitEnterFullscreen();
}
}
}
};
self.togglePictureInPicture = function (player) {
player = player || self._currentPlayer;
return player.togglePictureInPicture();
};
self.toggleAirPlay = function (player) {
player = player || self._currentPlayer;
return player.toggleAirPlay();
};
self.getSubtitleStreamIndex = function (player) {
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player)) {
return player.getSubtitleStreamIndex();
}
if (!player) {
throw new Error('player cannot be null');
}
return getPlayerData(player).subtitleStreamIndex;
};
function getDeliveryMethod(subtitleStream) {
// This will be null for internal subs for local items
if (subtitleStream.DeliveryMethod) {
return subtitleStream.DeliveryMethod;
}
return subtitleStream.IsExternal ? 'External' : 'Embed';
}
self.setSubtitleStreamIndex = function (index, player) {
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player)) {
return player.setSubtitleStreamIndex(index);
}
const currentStream = getCurrentSubtitleStream(player);
const newStream = getSubtitleStream(player, index);
if (!currentStream && !newStream) {
return;
}
let selectedTrackElementIndex = -1;
const currentPlayMethod = self.playMethod(player);
if (currentStream && !newStream) {
if (getDeliveryMethod(currentStream) === 'Encode' || (getDeliveryMethod(currentStream) === 'Embed' && currentPlayMethod === 'Transcode')) {
// Need to change the transcoded stream to remove subs
changeStream(player, getCurrentTicks(player), { SubtitleStreamIndex: -1 });
}
} else if (!currentStream && newStream) {
if (getDeliveryMethod(newStream) === 'External') {
selectedTrackElementIndex = index;
} else if (getDeliveryMethod(newStream) === 'Embed' && currentPlayMethod !== 'Transcode') {
selectedTrackElementIndex = index;
} else {
// Need to change the transcoded stream to add subs
changeStream(player, getCurrentTicks(player), { SubtitleStreamIndex: index });
}
} else if (currentStream && newStream) {
// Switching tracks
// We can handle this clientside if the new track is external or the new track is embedded and we're not transcoding
if (getDeliveryMethod(newStream) === 'External' || (getDeliveryMethod(newStream) === 'Embed' && currentPlayMethod !== 'Transcode')) {
selectedTrackElementIndex = index;
// But in order to handle this client side, if the previous track is being added via transcoding, we'll have to remove it
if (getDeliveryMethod(currentStream) !== 'External' && getDeliveryMethod(currentStream) !== 'Embed') {
changeStream(player, getCurrentTicks(player), { SubtitleStreamIndex: -1 });
}
} else {
// Need to change the transcoded stream to add subs
changeStream(player, getCurrentTicks(player), { SubtitleStreamIndex: index });
}
}
player.setSubtitleStreamIndex(selectedTrackElementIndex);
getPlayerData(player).subtitleStreamIndex = index;
};
self.supportSubtitleOffset = function (player) {
player = player || self._currentPlayer;
return player && 'setSubtitleOffset' in player;
};
self.enableShowingSubtitleOffset = function (player) {
player = player || self._currentPlayer;
player.enableShowingSubtitleOffset();
};
self.disableShowingSubtitleOffset = function (player) {
player = player || self._currentPlayer;
if (player.disableShowingSubtitleOffset) {
player.disableShowingSubtitleOffset();
}
};
self.isShowingSubtitleOffsetEnabled = function (player) {
player = player || self._currentPlayer;
return player.isShowingSubtitleOffsetEnabled();
};
self.isSubtitleStreamExternal = function (index, player) {
const stream = getSubtitleStream(player, index);
return stream ? getDeliveryMethod(stream) === 'External' : false;
};
self.setSubtitleOffset = function (value, player) {
player = player || self._currentPlayer;
if (player.setSubtitleOffset) {
player.setSubtitleOffset(value);
}
};
self.getPlayerSubtitleOffset = function (player) {
player = player || self._currentPlayer;
if (player.getSubtitleOffset) {
return player.getSubtitleOffset();
}
};
self.canHandleOffsetOnCurrentSubtitle = function (player) {
const index = self.getSubtitleStreamIndex(player);
return index !== -1 && self.isSubtitleStreamExternal(index, player);
};
self.seek = function (ticks, player) {
ticks = Math.max(0, ticks);
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player)) {
if (player.isLocalPlayer) {
return player.seek((ticks || 0) / 10000);
} else {
return player.seek(ticks);
}
}
changeStream(player, ticks);
};
self.seekRelative = function (offsetTicks, player) {
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player) && player.seekRelative) {
if (player.isLocalPlayer) {
return player.seekRelative((ticks || 0) / 10000);
} else {
return player.seekRelative(ticks);
}
}
const ticks = getCurrentTicks(player) + offsetTicks;
return this.seek(ticks, player);
};
// Returns true if the player can seek using native client-side seeking functions
function canPlayerSeek(player) {
if (!player) {
throw new Error('player cannot be null');
}
const playerData = getPlayerData(player);
const currentSrc = (playerData.streamInfo.url || '').toLowerCase();
if (currentSrc.indexOf('.m3u8') !== -1) {
return true;
}
if (player.seekable) {
return player.seekable();
}
const isPlayMethodTranscode = self.playMethod(player) === 'Transcode';
if (isPlayMethodTranscode) {
return false;
}
return player.duration();
}
function changeStream(player, ticks, params) {
if (canPlayerSeek(player) && params == null) {
player.currentTime(parseInt(ticks / 10000));
return;
}
params = params || {};
const liveStreamId = getPlayerData(player).streamInfo.liveStreamId;
const lastMediaInfoQuery = getPlayerData(player).streamInfo.lastMediaInfoQuery;
const playSessionId = self.playSessionId(player);
const currentItem = self.currentItem(player);
player.getDeviceProfile(currentItem, {
isRetry: params.EnableDirectPlay === false
}).then(function (deviceProfile) {
const audioStreamIndex = params.AudioStreamIndex == null ? getPlayerData(player).audioStreamIndex : params.AudioStreamIndex;
const subtitleStreamIndex = params.SubtitleStreamIndex == null ? getPlayerData(player).subtitleStreamIndex : params.SubtitleStreamIndex;
let currentMediaSource = self.currentMediaSource(player);
const apiClient = connectionManager.getApiClient(currentItem.ServerId);
if (ticks) {
ticks = parseInt(ticks);
}
const maxBitrate = params.MaxStreamingBitrate || self.getMaxStreamingBitrate(player);
const currentPlayOptions = currentItem.playOptions || getDefaultPlayOptions();
getPlaybackInfo(player, apiClient, currentItem, deviceProfile, maxBitrate, ticks, true, currentMediaSource.Id, audioStreamIndex, subtitleStreamIndex, liveStreamId, params.EnableDirectPlay, params.EnableDirectStream, params.AllowVideoStreamCopy, params.AllowAudioStreamCopy).then(function (result) {
if (validatePlaybackInfoResult(self, result)) {
currentMediaSource = result.MediaSources[0];
const streamInfo = createStreamInfo(apiClient, currentItem.MediaType, currentItem, currentMediaSource, ticks);
streamInfo.fullscreen = currentPlayOptions.fullscreen;
streamInfo.lastMediaInfoQuery = lastMediaInfoQuery;
if (!streamInfo.url) {
showPlaybackInfoErrorMessage(self, 'NoCompatibleStream', true);
return;
}
getPlayerData(player).subtitleStreamIndex = subtitleStreamIndex;
getPlayerData(player).audioStreamIndex = audioStreamIndex;
getPlayerData(player).maxStreamingBitrate = maxBitrate;
changeStreamToUrl(apiClient, player, playSessionId, streamInfo);
}
});
});
}
function changeStreamToUrl(apiClient, player, playSessionId, streamInfo, newPositionTicks) {
const playerData = getPlayerData(player);
playerData.isChangingStream = true;
if (playerData.streamInfo && playSessionId) {
apiClient.stopActiveEncodings(playSessionId).then(function () {
// Stop the first transcoding afterwards because the player may still send requests to the original url
const afterSetSrc = function () {
apiClient.stopActiveEncodings(playSessionId);
};
setSrcIntoPlayer(apiClient, player, streamInfo).then(afterSetSrc, afterSetSrc);
});
} else {
setSrcIntoPlayer(apiClient, player, streamInfo);
}
}
function setSrcIntoPlayer(apiClient, player, streamInfo) {
return player.play(streamInfo).then(function () {
const playerData = getPlayerData(player);
playerData.isChangingStream = false;
playerData.streamInfo = streamInfo;
streamInfo.started = true;
streamInfo.ended = false;
sendProgressUpdate(player, 'timeupdate');
}, function (e) {
const playerData = getPlayerData(player);
playerData.isChangingStream = false;
onPlaybackError.call(player, e, {
type: 'mediadecodeerror',
streamInfo: streamInfo
});
});
}
function translateItemsForPlayback(items, options) {
if (items.length > 1 && options && options.ids) {
// Use the original request id array for sorting the result in the proper order
items.sort(function (a, b) {
return options.ids.indexOf(a.Id) - options.ids.indexOf(b.Id);
});
}
const firstItem = items[0];
let promise;
const serverId = firstItem.ServerId;
const queryOptions = options.queryOptions || {};
if (firstItem.Type === 'Program') {
promise = getItemsForPlayback(serverId, {
Ids: firstItem.ChannelId
});
} else if (firstItem.Type === 'Playlist') {
promise = getItemsForPlayback(serverId, {
ParentId: firstItem.Id,
SortBy: options.shuffle ? 'Random' : null
});
} else if (firstItem.Type === 'MusicArtist') {
promise = getItemsForPlayback(serverId, {
ArtistIds: firstItem.Id,
Filters: 'IsNotFolder',
Recursive: true,
SortBy: options.shuffle ? 'Random' : 'SortName',
MediaTypes: 'Audio'
});
} else if (firstItem.MediaType === 'Photo') {
promise = getItemsForPlayback(serverId, {
ParentId: firstItem.ParentId,
Filters: 'IsNotFolder',
// Setting this to true may cause some incorrect sorting
Recursive: false,
SortBy: options.shuffle ? 'Random' : 'SortName',
MediaTypes: 'Photo,Video'
}).then(function (result) {
const items = result.Items;
let index = items.map(function (i) {
return i.Id;
}).indexOf(firstItem.Id);
if (index === -1) {
index = 0;
}
options.startIndex = index;
return Promise.resolve(result);
});
} else if (firstItem.Type === 'PhotoAlbum') {
promise = getItemsForPlayback(serverId, {
ParentId: firstItem.Id,
Filters: 'IsNotFolder',
// Setting this to true may cause some incorrect sorting
Recursive: false,
SortBy: options.shuffle ? 'Random' : 'SortName',
MediaTypes: 'Photo,Video',
Limit: 1000
});
} else if (firstItem.Type === 'MusicGenre') {
promise = getItemsForPlayback(serverId, {
GenreIds: firstItem.Id,
Filters: 'IsNotFolder',
Recursive: true,
SortBy: options.shuffle ? 'Random' : 'SortName',
MediaTypes: 'Audio'
});
} else if (firstItem.IsFolder) {
promise = getItemsForPlayback(serverId, mergePlaybackQueries({
ParentId: firstItem.Id,
Filters: 'IsNotFolder',
Recursive: true,
// These are pre-sorted
SortBy: options.shuffle ? 'Random' : (['BoxSet'].indexOf(firstItem.Type) === -1 ? 'SortName' : null),
MediaTypes: 'Audio,Video'
}, queryOptions));
} else if (firstItem.Type === 'Episode' && items.length === 1 && getPlayer(firstItem, options).supportsProgress !== false) {
promise = new Promise(function (resolve, reject) {
const apiClient = connectionManager.getApiClient(firstItem.ServerId);
apiClient.getCurrentUser().then(function (user) {
if (!user.Configuration.EnableNextEpisodeAutoPlay || !firstItem.SeriesId) {
resolve(null);
return;
}
apiClient.getEpisodes(firstItem.SeriesId, {
IsVirtualUnaired: false,
IsMissing: false,
UserId: apiClient.getCurrentUserId(),
Fields: 'Chapters'
}).then(function (episodesResult) {
let foundItem = false;
episodesResult.Items = episodesResult.Items.filter(function (e) {
if (foundItem) {
return true;
}
if (e.Id === firstItem.Id) {
foundItem = true;
return true;
}
return false;
});
episodesResult.TotalRecordCount = episodesResult.Items.length;
resolve(episodesResult);
}, reject);
});
});
}
if (promise) {
return promise.then(function (result) {
return result ? result.Items : items;
});
} else {
return Promise.resolve(items);
}
}
self.play = function (options) {
normalizePlayOptions(options);
if (self._currentPlayer) {
if (options.enableRemotePlayers === false && !self._currentPlayer.isLocalPlayer) {
return Promise.reject();
}
if (!self._currentPlayer.isLocalPlayer) {
return self._currentPlayer.play(options);
}
}
if (options.fullscreen) {
loading.show();
}
if (options.items) {
return translateItemsForPlayback(options.items, options).then(function (items) {
return playWithIntros(items, options);
});
} else {
if (!options.serverId) {
throw new Error('serverId required!');
}
return getItemsForPlayback(options.serverId, {
Ids: options.ids.join(',')
}).then(function (result) {
return translateItemsForPlayback(result.Items, options).then(function (items) {
return playWithIntros(items, options);
});
});
}
};
function getPlayerData(player) {
if (!player) {
throw new Error('player cannot be null');
}
if (!player.name) {
throw new Error('player name cannot be null');
}
let state = playerStates[player.name];
if (!state) {
playerStates[player.name] = {};
state = playerStates[player.name];
}
return player;
}
self.getPlayerState = function (player, item, mediaSource) {
player = player || self._currentPlayer;
if (!player) {
throw new Error('player cannot be null');
}
if (!enableLocalPlaylistManagement(player) && player.getPlayerState) {
return player.getPlayerState();
}
item = item || self.currentItem(player);
mediaSource = mediaSource || self.currentMediaSource(player);
const state = {
PlayState: {}
};
if (player) {
state.PlayState.VolumeLevel = player.getVolume();
state.PlayState.IsMuted = player.isMuted();
state.PlayState.IsPaused = player.paused();
state.PlayState.RepeatMode = self.getRepeatMode(player);
state.PlayState.ShuffleMode = self.getQueueShuffleMode(player);
state.PlayState.MaxStreamingBitrate = self.getMaxStreamingBitrate(player);
state.PlayState.PositionTicks = getCurrentTicks(player);
state.PlayState.PlaybackStartTimeTicks = self.playbackStartTime(player);
state.PlayState.SubtitleStreamIndex = self.getSubtitleStreamIndex(player);
state.PlayState.AudioStreamIndex = self.getAudioStreamIndex(player);
state.PlayState.BufferedRanges = self.getBufferedRanges(player);
state.PlayState.PlayMethod = self.playMethod(player);
if (mediaSource) {
state.PlayState.LiveStreamId = mediaSource.LiveStreamId;
}
state.PlayState.PlaySessionId = self.playSessionId(player);
state.PlayState.PlaylistItemId = self.getCurrentPlaylistItemId(player);
}
if (mediaSource) {
state.PlayState.MediaSourceId = mediaSource.Id;
state.NowPlayingItem = {
RunTimeTicks: mediaSource.RunTimeTicks
};
state.PlayState.CanSeek = (mediaSource.RunTimeTicks || 0) > 0 || canPlayerSeek(player);
}
if (item) {
state.NowPlayingItem = getNowPlayingItemForReporting(player, item, mediaSource);
}
state.MediaSource = mediaSource;
return state;
};
self.duration = function (player) {
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player) && !player.isLocalPlayer) {
return player.duration();
}
if (!player) {
throw new Error('player cannot be null');
}
const mediaSource = self.currentMediaSource(player);
if (mediaSource && mediaSource.RunTimeTicks) {
return mediaSource.RunTimeTicks;
}
let playerDuration = player.duration();
if (playerDuration) {
playerDuration *= 10000;
}
return playerDuration;
};
function getCurrentTicks(player) {
if (!player) {
throw new Error('player cannot be null');
}
let playerTime = Math.floor(10000 * (player).currentTime());
const streamInfo = getPlayerData(player).streamInfo;
if (streamInfo) {
playerTime += getPlayerData(player).streamInfo.transcodingOffsetTicks || 0;
}
return playerTime;
}
// Only used internally
self.getCurrentTicks = getCurrentTicks;
function playOther(items, options, user) {
const playStartIndex = options.startIndex || 0;
const player = getPlayer(items[playStartIndex], options);
loading.hide();
options.items = items;
return player.play(options);
}
function playWithIntros(items, options, user) {
let playStartIndex = options.startIndex || 0;
let firstItem = items[playStartIndex];
// If index was bad, reset it
if (!firstItem) {
playStartIndex = 0;
firstItem = items[playStartIndex];
}
// If it's still null then there's nothing to play
if (!firstItem) {
showPlaybackInfoErrorMessage(self, 'NoCompatibleStream', false);
return Promise.reject();
}
if (firstItem.MediaType === 'Photo' || firstItem.MediaType === 'Book') {
return playOther(items, options, user);
}
const apiClient = connectionManager.getApiClient(firstItem.ServerId);
return getIntros(firstItem, apiClient, options).then(function (introsResult) {
const introItems = introsResult.Items;
let introPlayOptions;
firstItem.playOptions = truncatePlayOptions(options);
if (introItems.length) {
introPlayOptions = {
fullscreen: firstItem.playOptions.fullscreen
};
} else {
introPlayOptions = firstItem.playOptions;
}
items = introItems.concat(items);
// Needed by players that manage their own playlist
introPlayOptions.items = items;
introPlayOptions.startIndex = playStartIndex;
return playInternal(items[playStartIndex], introPlayOptions, function () {
self._playQueueManager.setPlaylist(items);
setPlaylistState(items[playStartIndex].PlaylistItemId, playStartIndex);
loading.hide();
});
});
}
// Set playlist state. Using a method allows for overloading in derived player implementations
function setPlaylistState(playlistItemId, index) {
if (!isNaN(index)) {
self._playQueueManager.setPlaylistState(playlistItemId, index);
}
}
function playInternal(item, playOptions, onPlaybackStartedFn) {
if (item.IsPlaceHolder) {
loading.hide();
showPlaybackInfoErrorMessage(self, 'PlaceHolder', true);
return Promise.reject();
}
// Normalize defaults to simplfy checks throughout the process
normalizePlayOptions(playOptions);
if (playOptions.isFirstItem) {
playOptions.isFirstItem = false;
} else {
playOptions.isFirstItem = true;
}
return runInterceptors(item, playOptions).then(function () {
if (playOptions.fullscreen) {
loading.show();
}
// TODO: This should be the media type requested, not the original media type
const mediaType = item.MediaType;
const onBitrateDetectionFailure = function () {
return playAfterBitrateDetect(getSavedMaxStreamingBitrate(connectionManager.getApiClient(item.ServerId), mediaType), item, playOptions, onPlaybackStartedFn);
};
if (!isServerItem(item) || itemHelper.isLocalItem(item)) {
return onBitrateDetectionFailure();
}
const apiClient = connectionManager.getApiClient(item.ServerId);
apiClient.getEndpointInfo().then(function (endpointInfo) {
if ((mediaType === 'Video' || mediaType === 'Audio') && appSettings.enableAutomaticBitrateDetection(endpointInfo.IsInNetwork, mediaType)) {
return apiClient.detectBitrate().then(function (bitrate) {
appSettings.maxStreamingBitrate(endpointInfo.IsInNetwork, mediaType, bitrate);
return playAfterBitrateDetect(bitrate, item, playOptions, onPlaybackStartedFn);
}, onBitrateDetectionFailure);
} else {
onBitrateDetectionFailure();
}
}, onBitrateDetectionFailure);
}, onInterceptorRejection);
}
function onInterceptorRejection() {
const player = self._currentPlayer;
if (player) {
destroyPlayer(player);
removeCurrentPlayer(player);
}
events.trigger(self, 'playbackcancelled');
return Promise.reject();
}
function destroyPlayer(player) {
player.destroy();
}
function runInterceptors(item, playOptions) {
return new Promise(function (resolve, reject) {
const interceptors = pluginManager.ofType('preplayintercept');
interceptors.sort(function (a, b) {
return (a.order || 0) - (b.order || 0);
});
if (!interceptors.length) {
resolve();
return;
}
loading.hide();
const options = Object.assign({}, playOptions);
options.mediaType = item.MediaType;
options.item = item;
runNextPrePlay(interceptors, 0, options, resolve, reject);
});
}
function runNextPrePlay(interceptors, index, options, resolve, reject) {
if (index >= interceptors.length) {
resolve();
return;
}
const interceptor = interceptors[index];
interceptor.intercept(options).then(function () {
runNextPrePlay(interceptors, index + 1, options, resolve, reject);
}, reject);
}
function sendPlaybackListToPlayer(player, items, deviceProfile, maxBitrate, apiClient, startPositionTicks, mediaSourceId, audioStreamIndex, subtitleStreamIndex, startIndex) {
return setStreamUrls(items, deviceProfile, maxBitrate, apiClient, startPositionTicks).then(function () {
loading.hide();
return player.play({
items: items,
startPositionTicks: startPositionTicks || 0,
mediaSourceId: mediaSourceId,
audioStreamIndex: audioStreamIndex,
subtitleStreamIndex: subtitleStreamIndex,
startIndex: startIndex
});
});
}
function playAfterBitrateDetect(maxBitrate, item, playOptions, onPlaybackStartedFn) {
const startPosition = playOptions.startPositionTicks;
const player = getPlayer(item, playOptions);
const activePlayer = self._currentPlayer;
let promise;
if (activePlayer) {
// TODO: if changing players within the same playlist, this will cause nextItem to be null
self._playNextAfterEnded = false;
promise = onPlaybackChanging(activePlayer, player, item);
} else {
promise = Promise.resolve();
}
if (!isServerItem(item) || item.MediaType === 'Book') {
return promise.then(function () {
const streamInfo = createStreamInfoFromUrlItem(item);
streamInfo.fullscreen = playOptions.fullscreen;
getPlayerData(player).isChangingStream = false;
return player.play(streamInfo).then(function () {
loading.hide();
onPlaybackStartedFn();
onPlaybackStarted(player, playOptions, streamInfo);
}, function () {
// TODO: show error message
self.stop(player);
});
});
}
return Promise.all([promise, player.getDeviceProfile(item)]).then(function (responses) {
const deviceProfile = responses[1];
const apiClient = connectionManager.getApiClient(item.ServerId);
const mediaSourceId = playOptions.mediaSourceId;
const audioStreamIndex = playOptions.audioStreamIndex;
const subtitleStreamIndex = playOptions.subtitleStreamIndex;
if (player && !enableLocalPlaylistManagement(player)) {
return sendPlaybackListToPlayer(player, playOptions.items, deviceProfile, maxBitrate, apiClient, startPosition, mediaSourceId, audioStreamIndex, subtitleStreamIndex, playOptions.startIndex);
}
// this reference was only needed by sendPlaybackListToPlayer
playOptions.items = null;
return getPlaybackMediaSource(player, apiClient, deviceProfile, maxBitrate, item, startPosition, mediaSourceId, audioStreamIndex, subtitleStreamIndex).then(function (mediaSource) {
const streamInfo = createStreamInfo(apiClient, item.MediaType, item, mediaSource, startPosition);
streamInfo.fullscreen = playOptions.fullscreen;
getPlayerData(player).isChangingStream = false;
getPlayerData(player).maxStreamingBitrate = maxBitrate;
return player.play(streamInfo).then(function () {
loading.hide();
onPlaybackStartedFn();
onPlaybackStarted(player, playOptions, streamInfo, mediaSource);
}, function (err) {
// TODO: Improve this because it will report playback start on a failure
onPlaybackStartedFn();
onPlaybackStarted(player, playOptions, streamInfo, mediaSource);
setTimeout(function () {
onPlaybackError.call(player, err, {
type: 'mediadecodeerror',
streamInfo: streamInfo
});
}, 100);
});
});
});
}
self.getPlaybackInfo = function (item, options) {
options = options || {};
const startPosition = options.startPositionTicks || 0;
const mediaType = options.mediaType || item.MediaType;
const player = getPlayer(item, options);
const apiClient = connectionManager.getApiClient(item.ServerId);
// Call this just to ensure the value is recorded, it is needed with getSavedMaxStreamingBitrate
return apiClient.getEndpointInfo().then(function () {
const maxBitrate = getSavedMaxStreamingBitrate(connectionManager.getApiClient(item.ServerId), mediaType);
return player.getDeviceProfile(item).then(function (deviceProfile) {
return getPlaybackMediaSource(player, apiClient, deviceProfile, maxBitrate, item, startPosition, options.mediaSourceId, options.audioStreamIndex, options.subtitleStreamIndex).then(function (mediaSource) {
return createStreamInfo(apiClient, item.MediaType, item, mediaSource, startPosition);
});
});
});
};
self.getPlaybackMediaSources = function (item, options) {
options = options || {};
const startPosition = options.startPositionTicks || 0;
const mediaType = options.mediaType || item.MediaType;
// TODO: Remove the true forceLocalPlayer hack
const player = getPlayer(item, options, true);
const apiClient = connectionManager.getApiClient(item.ServerId);
// Call this just to ensure the value is recorded, it is needed with getSavedMaxStreamingBitrate
return apiClient.getEndpointInfo().then(function () {
const maxBitrate = getSavedMaxStreamingBitrate(connectionManager.getApiClient(item.ServerId), mediaType);
return player.getDeviceProfile(item).then(function (deviceProfile) {
return getPlaybackInfo(player, apiClient, item, deviceProfile, maxBitrate, startPosition, false, null, null, null, null).then(function (playbackInfoResult) {
return playbackInfoResult.MediaSources;
});
});
});
};
function createStreamInfo(apiClient, type, item, mediaSource, startPosition) {
let mediaUrl;
let contentType;
let transcodingOffsetTicks = 0;
const playerStartPositionTicks = startPosition;
const liveStreamId = mediaSource.LiveStreamId;
let playMethod = 'Transcode';
const mediaSourceContainer = (mediaSource.Container || '').toLowerCase();
let directOptions;
if (type === 'Video' || type === 'Audio') {
contentType = getMimeType(type.toLowerCase(), mediaSourceContainer);
if (mediaSource.enableDirectPlay) {
mediaUrl = mediaSource.Path;
playMethod = 'DirectPlay';
} else if (mediaSource.StreamUrl) {
// Only used for audio
playMethod = 'Transcode';
mediaUrl = mediaSource.StreamUrl;
} else if (mediaSource.SupportsDirectStream) {
directOptions = {
Static: true,
mediaSourceId: mediaSource.Id,
deviceId: apiClient.deviceId(),
api_key: apiClient.accessToken()
};
if (mediaSource.ETag) {
directOptions.Tag = mediaSource.ETag;
}
if (mediaSource.LiveStreamId) {
directOptions.LiveStreamId = mediaSource.LiveStreamId;
}
const prefix = type === 'Video' ? 'Videos' : 'Audio';
mediaUrl = apiClient.getUrl(prefix + '/' + item.Id + '/stream.' + mediaSourceContainer, directOptions);
playMethod = 'DirectStream';
} else if (mediaSource.SupportsTranscoding) {
mediaUrl = apiClient.getUrl(mediaSource.TranscodingUrl);
if (mediaSource.TranscodingSubProtocol === 'hls') {
contentType = 'application/x-mpegURL';
} else {
contentType = getMimeType(type.toLowerCase(), mediaSource.TranscodingContainer);
if (mediaUrl.toLowerCase().indexOf('copytimestamps=true') === -1) {
transcodingOffsetTicks = startPosition || 0;
}
}
}
} else {
// All other media types
mediaUrl = mediaSource.Path;
playMethod = 'DirectPlay';
}
// Fallback (used for offline items)
if (!mediaUrl && mediaSource.SupportsDirectPlay) {
mediaUrl = mediaSource.Path;
playMethod = 'DirectPlay';
}
const resultInfo = {
url: mediaUrl,
mimeType: contentType,
transcodingOffsetTicks: transcodingOffsetTicks,
playMethod: playMethod,
playerStartPositionTicks: playerStartPositionTicks,
item: item,
mediaSource: mediaSource,
textTracks: getTextTracks(apiClient, item, mediaSource),
// TODO: Deprecate
tracks: getTextTracks(apiClient, item, mediaSource),
mediaType: type,
liveStreamId: liveStreamId,
playSessionId: getParam('playSessionId', mediaUrl),
title: item.Name
};
const backdropUrl = backdropImageUrl(apiClient, item, {});
if (backdropUrl) {
resultInfo.backdropUrl = backdropUrl;
}
return resultInfo;
}
function getTextTracks(apiClient, item, mediaSource) {
const subtitleStreams = mediaSource.MediaStreams.filter(function (s) {
return s.Type === 'Subtitle';
});
const textStreams = subtitleStreams.filter(function (s) {
return s.DeliveryMethod === 'External';
});
const tracks = [];
for (let i = 0, length = textStreams.length; i < length; i++) {
const textStream = textStreams[i];
let textStreamUrl;
if (itemHelper.isLocalItem(item)) {
textStreamUrl = textStream.Path;
} else {
textStreamUrl = !textStream.IsExternalUrl ? apiClient.getUrl(textStream.DeliveryUrl) : textStream.DeliveryUrl;
}
tracks.push({
url: textStreamUrl,
language: (textStream.Language || 'und'),
isDefault: textStream.Index === mediaSource.DefaultSubtitleStreamIndex,
index: textStream.Index,
format: textStream.Codec
});
}
return tracks;
}
function getPlaybackMediaSource(player, apiClient, deviceProfile, maxBitrate, item, startPosition, mediaSourceId, audioStreamIndex, subtitleStreamIndex) {
return getPlaybackInfo(player, apiClient, item, deviceProfile, maxBitrate, startPosition, true, mediaSourceId, audioStreamIndex, subtitleStreamIndex, null).then(function (playbackInfoResult) {
if (validatePlaybackInfoResult(self, playbackInfoResult)) {
return getOptimalMediaSource(apiClient, item, playbackInfoResult.MediaSources).then(function (mediaSource) {
if (mediaSource) {
if (mediaSource.RequiresOpening && !mediaSource.LiveStreamId) {
return getLiveStream(player, apiClient, item, playbackInfoResult.PlaySessionId, deviceProfile, maxBitrate, startPosition, mediaSource, null, null).then(function (openLiveStreamResult) {
return supportsDirectPlay(apiClient, item, openLiveStreamResult.MediaSource).then(function (result) {
openLiveStreamResult.MediaSource.enableDirectPlay = result;
return openLiveStreamResult.MediaSource;
});
});
} else {
return mediaSource;
}
} else {
showPlaybackInfoErrorMessage(self, 'NoCompatibleStream');
return Promise.reject();
}
});
} else {
return Promise.reject();
}
});
}
function getPlayer(item, playOptions, forceLocalPlayers) {
const serverItem = isServerItem(item);
return getAutomaticPlayers(self, forceLocalPlayers).filter(function (p) {
if (p.canPlayMediaType(item.MediaType)) {
if (serverItem) {
if (p.canPlayItem) {
return p.canPlayItem(item, playOptions);
}
return true;
} else if (item.Url && p.canPlayUrl) {
return p.canPlayUrl(item.Url);
}
}
return false;
})[0];
}
self.setCurrentPlaylistItem = function (playlistItemId, player) {
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player)) {
return player.setCurrentPlaylistItem(playlistItemId);
}
let newItem;
let newItemIndex;
const playlist = self._playQueueManager.getPlaylist();
for (let i = 0, length = playlist.length; i < length; i++) {
if (playlist[i].PlaylistItemId === playlistItemId) {
newItem = playlist[i];
newItemIndex = i;
break;
}
}
if (newItem) {
const newItemPlayOptions = newItem.playOptions || getDefaultPlayOptions();
playInternal(newItem, newItemPlayOptions, function () {
setPlaylistState(newItem.PlaylistItemId, newItemIndex);
});
}
};
self.removeFromPlaylist = function (playlistItemIds, player) {
if (!playlistItemIds) {
throw new Error('Invalid playlistItemIds');
}
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player)) {
return player.removeFromPlaylist(playlistItemIds);
}
const removeResult = self._playQueueManager.removeFromPlaylist(playlistItemIds);
if (removeResult.result === 'empty') {
return self.stop(player);
}
const isCurrentIndex = removeResult.isCurrentIndex;
events.trigger(player, 'playlistitemremove', [
{
playlistItemIds: playlistItemIds
}
]);
if (isCurrentIndex) {
return self.setCurrentPlaylistItem(self._playQueueManager.getPlaylist()[0].PlaylistItemId, player);
}
return Promise.resolve();
};
self.movePlaylistItem = function (playlistItemId, newIndex, player) {
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player)) {
return player.movePlaylistItem(playlistItemId, newIndex);
}
const moveResult = self._playQueueManager.movePlaylistItem(playlistItemId, newIndex);
if (moveResult.result === 'noop') {
return;
}
events.trigger(player, 'playlistitemmove', [
{
playlistItemId: moveResult.playlistItemId,
newIndex: moveResult.newIndex
}
]);
};
self.getCurrentPlaylistIndex = function (player) {
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player)) {
return player.getCurrentPlaylistIndex();
}
return self._playQueueManager.getCurrentPlaylistIndex();
};
self.getCurrentPlaylistItemId = function (player) {
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player)) {
return player.getCurrentPlaylistItemId();
}
return self._playQueueManager.getCurrentPlaylistItemId();
};
self.channelUp = function (player) {
player = player || self._currentPlayer;
return self.nextTrack(player);
};
self.channelDown = function (player) {
player = player || self._currentPlayer;
return self.previousTrack(player);
};
self.nextTrack = function (player) {
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player)) {
return player.nextTrack();
}
const newItemInfo = self._playQueueManager.getNextItemInfo();
if (newItemInfo) {
console.debug('playing next track');
const newItemPlayOptions = newItemInfo.item.playOptions || getDefaultPlayOptions();
playInternal(newItemInfo.item, newItemPlayOptions, function () {
setPlaylistState(newItemInfo.item.PlaylistItemId, newItemInfo.index);
});
}
};
self.previousTrack = function (player) {
player = player || self._currentPlayer;
if (player && !enableLocalPlaylistManagement(player)) {
return player.previousTrack();
}
const newIndex = self.getCurrentPlaylistIndex(player) - 1;
if (newIndex >= 0) {
const playlist = self._playQueueManager.getPlaylist();
const newItem = playlist[newIndex];
if (newItem) {
const newItemPlayOptions = newItem.playOptions || getDefaultPlayOptions();
newItemPlayOptions.startPositionTicks = 0;
playInternal(newItem, newItemPlayOptions, function () {
setPlaylistState(newItem.PlaylistItemId, newIndex);
});
}
}
};
self.queue = function (options, player = this._currentPlayer) {
queue(options, '', player);
};
self.queueNext = function (options, player = this._currentPlayer) {
queue(options, 'next', player);
};
function queue(options, mode, player) {
player = player || self._currentPlayer;
if (!player) {
return self.play(options);
}
if (options.items) {
return translateItemsForPlayback(options.items, options).then(function (items) {
// TODO: Handle options.startIndex for photos
queueAll(items, mode, player);
});
} else {
if (!options.serverId) {
throw new Error('serverId required!');
}
return getItemsForPlayback(options.serverId, {
Ids: options.ids.join(',')
}).then(function (result) {
return translateItemsForPlayback(result.Items, options).then(function (items) {
// TODO: Handle options.startIndex for photos
queueAll(items, mode, player);
});
});
}
}
function queueAll(items, mode, player) {
if (!items.length) {
return;
}
if (!player.isLocalPlayer) {
if (mode === 'next') {
player.queueNext({
items: items
});
} else {
player.queue({
items: items
});
}
return;
}
const queueDirectToPlayer = player && !enableLocalPlaylistManagement(player);
if (queueDirectToPlayer) {
const apiClient = connectionManager.getApiClient(items[0].ServerId);
player.getDeviceProfile(items[0]).then(function (profile) {
setStreamUrls(items, profile, self.getMaxStreamingBitrate(player), apiClient, 0).then(function () {
if (mode === 'next') {
player.queueNext(items);
} else {
player.queue(items);
}
});
});
return;
}
if (mode === 'next') {
self._playQueueManager.queueNext(items);
} else {
self._playQueueManager.queue(items);
}
events.trigger(player, 'playlistitemadd');
}
function onPlayerProgressInterval() {
const player = this;
sendProgressUpdate(player, 'timeupdate');
}
function startPlaybackProgressTimer(player) {
stopPlaybackProgressTimer(player);
player._progressInterval = setInterval(onPlayerProgressInterval.bind(player), 10000);
}
function stopPlaybackProgressTimer(player) {
if (player._progressInterval) {
clearInterval(player._progressInterval);
player._progressInterval = null;
}
}
function onPlaybackStarted(player, playOptions, streamInfo, mediaSource) {
if (!player) {
throw new Error('player cannot be null');
}
setCurrentPlayerInternal(player);
const playerData = getPlayerData(player);
playerData.streamInfo = streamInfo;
streamInfo.playbackStartTimeTicks = new Date().getTime() * 10000;
if (mediaSource) {
playerData.audioStreamIndex = mediaSource.DefaultAudioStreamIndex;
playerData.subtitleStreamIndex = mediaSource.DefaultSubtitleStreamIndex;
} else {
playerData.audioStreamIndex = null;
playerData.subtitleStreamIndex = null;
}
self._playNextAfterEnded = true;
const isFirstItem = playOptions.isFirstItem;
const fullscreen = playOptions.fullscreen;
const state = self.getPlayerState(player, streamInfo.item, streamInfo.mediaSource);
reportPlayback(self, state, player, true, state.NowPlayingItem.ServerId, 'reportPlaybackStart');
state.IsFirstItem = isFirstItem;
state.IsFullscreen = fullscreen;
events.trigger(player, 'playbackstart', [state]);
events.trigger(self, 'playbackstart', [player, state]);
// only used internally as a safeguard to avoid reporting other events to the server before playback start
streamInfo.started = true;
startPlaybackProgressTimer(player);
}
function onPlaybackStartedFromSelfManagingPlayer(e, item, mediaSource) {
const player = this;
setCurrentPlayerInternal(player);
const playOptions = item.playOptions || getDefaultPlayOptions();
const isFirstItem = playOptions.isFirstItem;
const fullscreen = playOptions.fullscreen;
playOptions.isFirstItem = false;
const playerData = getPlayerData(player);
playerData.streamInfo = {};
const streamInfo = playerData.streamInfo;
streamInfo.playbackStartTimeTicks = new Date().getTime() * 10000;
const state = self.getPlayerState(player, item, mediaSource);
reportPlayback(self, state, player, true, state.NowPlayingItem.ServerId, 'reportPlaybackStart');
state.IsFirstItem = isFirstItem;
state.IsFullscreen = fullscreen;
events.trigger(player, 'playbackstart', [state]);
events.trigger(self, 'playbackstart', [player, state]);
// only used internally as a safeguard to avoid reporting other events to the server before playback start
streamInfo.started = true;
startPlaybackProgressTimer(player);
}
function onPlaybackStoppedFromSelfManagingPlayer(e, playerStopInfo) {
const player = this;
stopPlaybackProgressTimer(player);
const state = self.getPlayerState(player, playerStopInfo.item, playerStopInfo.mediaSource);
const nextItem = playerStopInfo.nextItem;
const nextMediaType = playerStopInfo.nextMediaType;
const playbackStopInfo = {
player: player,
state: state,
nextItem: (nextItem ? nextItem.item : null),
nextMediaType: nextMediaType
};
state.NextMediaType = nextMediaType;
const streamInfo = getPlayerData(player).streamInfo;
// only used internally as a safeguard to avoid reporting other events to the server after playback stopped
streamInfo.ended = true;
if (isServerItem(playerStopInfo.item)) {
state.PlayState.PositionTicks = (playerStopInfo.positionMs || 0) * 10000;
reportPlayback(self, state, player, true, playerStopInfo.item.ServerId, 'reportPlaybackStopped');
}
state.NextItem = playbackStopInfo.nextItem;
events.trigger(player, 'playbackstop', [state]);
events.trigger(self, 'playbackstop', [playbackStopInfo]);
const nextItemPlayOptions = nextItem ? (nextItem.item.playOptions || getDefaultPlayOptions()) : getDefaultPlayOptions();
const newPlayer = nextItem ? getPlayer(nextItem.item, nextItemPlayOptions) : null;
if (newPlayer !== player) {
destroyPlayer(player);
removeCurrentPlayer(player);
}
}
function enablePlaybackRetryWithTranscoding(streamInfo, errorType, currentlyPreventsVideoStreamCopy, currentlyPreventsAudioStreamCopy) {
// mediadecodeerror, medianotsupported, network, servererror
if (streamInfo.mediaSource.SupportsTranscoding && (!currentlyPreventsVideoStreamCopy || !currentlyPreventsAudioStreamCopy)) {
return true;
}
return false;
}
function onPlaybackError(e, error) {
const player = this;
error = error || {};
// network
// mediadecodeerror
// medianotsupported
const errorType = error.type;
console.debug('playbackmanager playback error type: ' + (errorType || ''));
const streamInfo = error.streamInfo || getPlayerData(player).streamInfo;
if (streamInfo) {
const currentlyPreventsVideoStreamCopy = streamInfo.url.toLowerCase().indexOf('allowvideostreamcopy=false') !== -1;
const currentlyPreventsAudioStreamCopy = streamInfo.url.toLowerCase().indexOf('allowaudiostreamcopy=false') !== -1;
// Auto switch to transcoding
if (enablePlaybackRetryWithTranscoding(streamInfo, errorType, currentlyPreventsVideoStreamCopy, currentlyPreventsAudioStreamCopy)) {
const startTime = getCurrentTicks(player) || streamInfo.playerStartPositionTicks;
changeStream(player, startTime, {
// force transcoding
EnableDirectPlay: false,
EnableDirectStream: false,
AllowVideoStreamCopy: false,
AllowAudioStreamCopy: currentlyPreventsAudioStreamCopy || currentlyPreventsVideoStreamCopy ? false : null
});
return;
}
}
const displayErrorCode = 'NoCompatibleStream';
onPlaybackStopped.call(player, e, displayErrorCode);
}
function onPlaybackStopped(e, displayErrorCode) {
const player = this;
if (getPlayerData(player).isChangingStream) {
return;
}
stopPlaybackProgressTimer(player);
// User clicked stop or content ended
const state = self.getPlayerState(player);
const data = getPlayerData(player);
const streamInfo = data.streamInfo;
const nextItem = self._playNextAfterEnded ? self._playQueueManager.getNextItemInfo() : null;
const nextMediaType = (nextItem ? nextItem.item.MediaType : null);
const playbackStopInfo = {
player: player,
state: state,
nextItem: (nextItem ? nextItem.item : null),
nextMediaType: nextMediaType
};
state.NextMediaType = nextMediaType;
if (isServerItem(streamInfo.item)) {
if (player.supportsProgress === false && state.PlayState && !state.PlayState.PositionTicks) {
state.PlayState.PositionTicks = streamInfo.item.RunTimeTicks;
}
// only used internally as a safeguard to avoid reporting other events to the server after playback stopped
streamInfo.ended = true;
reportPlayback(self, state, player, true, streamInfo.item.ServerId, 'reportPlaybackStopped');
}
state.NextItem = playbackStopInfo.nextItem;
if (!nextItem) {
self._playQueueManager.reset();
}
events.trigger(player, 'playbackstop', [state]);
events.trigger(self, 'playbackstop', [playbackStopInfo]);
const nextItemPlayOptions = nextItem ? (nextItem.item.playOptions || getDefaultPlayOptions()) : getDefaultPlayOptions();
const newPlayer = nextItem ? getPlayer(nextItem.item, nextItemPlayOptions) : null;
if (newPlayer !== player) {
destroyPlayer(player);
removeCurrentPlayer(player);
}
if (displayErrorCode && typeof (displayErrorCode) === 'string') {
showPlaybackInfoErrorMessage(self, displayErrorCode, nextItem);
} else if (nextItem) {
self.nextTrack();
} else {
// Nothing more to play - clear data
data.streamInfo = null;
}
}
function onPlaybackChanging(activePlayer, newPlayer, newItem) {
const state = self.getPlayerState(activePlayer);
const serverId = self.currentItem(activePlayer).ServerId;
// User started playing something new while existing content is playing
let promise;
stopPlaybackProgressTimer(activePlayer);
unbindStopped(activePlayer);
if (activePlayer === newPlayer) {
// If we're staying with the same player, stop it
promise = activePlayer.stop(false);
} else {
// If we're switching players, tear down the current one
promise = activePlayer.stop(true);
}
return promise.then(function () {
bindStopped(activePlayer);
if (enableLocalPlaylistManagement(activePlayer)) {
reportPlayback(self, state, activePlayer, true, serverId, 'reportPlaybackStopped');
}
events.trigger(self, 'playbackstop', [{
player: activePlayer,
state: state,
nextItem: newItem,
nextMediaType: newItem.MediaType
}]);
});
}
function bindStopped(player) {
if (enableLocalPlaylistManagement(player)) {
events.off(player, 'stopped', onPlaybackStopped);
events.on(player, 'stopped', onPlaybackStopped);
}
}
function onPlaybackTimeUpdate(e) {
const player = this;
sendProgressUpdate(player, 'timeupdate');
}
function onPlaybackPause(e) {
const player = this;
sendProgressUpdate(player, 'pause');
}
function onPlaybackUnpause(e) {
const player = this;
sendProgressUpdate(player, 'unpause');
}
function onPlaybackVolumeChange(e) {
const player = this;
sendProgressUpdate(player, 'volumechange');
}
function onRepeatModeChange(e) {
const player = this;
sendProgressUpdate(player, 'repeatmodechange');
}
function onShuffleQueueModeChange() {
const player = this;
sendProgressUpdate(player, 'shufflequeuemodechange');
}
function onPlaylistItemMove(e) {
const player = this;
sendProgressUpdate(player, 'playlistitemmove', true);
}
function onPlaylistItemRemove(e) {
const player = this;
sendProgressUpdate(player, 'playlistitemremove', true);
}
function onPlaylistItemAdd(e) {
const player = this;
sendProgressUpdate(player, 'playlistitemadd', true);
}
function unbindStopped(player) {
events.off(player, 'stopped', onPlaybackStopped);
}
function initLegacyVolumeMethods(player) {
player.getVolume = function () {
return player.volume();
};
player.setVolume = function (val) {
return player.volume(val);
};
}
function initMediaPlayer(player) {
players.push(player);
players.sort(function (a, b) {
return (a.priority || 0) - (b.priority || 0);
});
if (player.isLocalPlayer !== false) {
player.isLocalPlayer = true;
}
player.currentState = {};
if (!player.getVolume || !player.setVolume) {
initLegacyVolumeMethods(player);
}
if (enableLocalPlaylistManagement(player)) {
events.on(player, 'error', onPlaybackError);
events.on(player, 'timeupdate', onPlaybackTimeUpdate);
events.on(player, 'pause', onPlaybackPause);
events.on(player, 'unpause', onPlaybackUnpause);
events.on(player, 'volumechange', onPlaybackVolumeChange);
events.on(player, 'repeatmodechange', onRepeatModeChange);
events.on(player, 'shufflequeuemodechange', onShuffleQueueModeChange);
events.on(player, 'playlistitemmove', onPlaylistItemMove);
events.on(player, 'playlistitemremove', onPlaylistItemRemove);
events.on(player, 'playlistitemadd', onPlaylistItemAdd);
} else if (player.isLocalPlayer) {
events.on(player, 'itemstarted', onPlaybackStartedFromSelfManagingPlayer);
events.on(player, 'itemstopped', onPlaybackStoppedFromSelfManagingPlayer);
events.on(player, 'timeupdate', onPlaybackTimeUpdate);
events.on(player, 'pause', onPlaybackPause);
events.on(player, 'unpause', onPlaybackUnpause);
events.on(player, 'volumechange', onPlaybackVolumeChange);
events.on(player, 'repeatmodechange', onRepeatModeChange);
events.on(player, 'shufflequeuemodechange', onShuffleQueueModeChange);
events.on(player, 'playlistitemmove', onPlaylistItemMove);
events.on(player, 'playlistitemremove', onPlaylistItemRemove);
events.on(player, 'playlistitemadd', onPlaylistItemAdd);
}
if (player.isLocalPlayer) {
bindToFullscreenChange(player);
}
bindStopped(player);
}
events.on(pluginManager, 'registered', function (e, plugin) {
if (plugin.type === 'mediaplayer') {
initMediaPlayer(plugin);
}
});
pluginManager.ofType('mediaplayer').map(initMediaPlayer);
function sendProgressUpdate(player, progressEventName, reportPlaylist) {
if (!player) {
throw new Error('player cannot be null');
}
const state = self.getPlayerState(player);
if (state.NowPlayingItem) {
const serverId = state.NowPlayingItem.ServerId;
const streamInfo = getPlayerData(player).streamInfo;
if (streamInfo && streamInfo.started && !streamInfo.ended) {
reportPlayback(self, state, player, reportPlaylist, serverId, 'reportPlaybackProgress', progressEventName);
}
if (streamInfo && streamInfo.liveStreamId) {
if (new Date().getTime() - (streamInfo.lastMediaInfoQuery || 0) >= 600000) {
getLiveStreamMediaInfo(player, streamInfo, self.currentMediaSource(player), streamInfo.liveStreamId, serverId);
}
}
}
}
function getLiveStreamMediaInfo(player, streamInfo, mediaSource, liveStreamId, serverId) {
console.debug('getLiveStreamMediaInfo');
streamInfo.lastMediaInfoQuery = new Date().getTime();
const apiClient = connectionManager.getApiClient(serverId);
if (!apiClient.isMinServerVersion('3.2.70.7')) {
return;
}
connectionManager.getApiClient(serverId).getLiveStreamMediaInfo(liveStreamId).then(function (info) {
mediaSource.MediaStreams = info.MediaStreams;
events.trigger(player, 'mediastreamschange');
}, function () {
});
}
self.onAppClose = function () {
const player = this._currentPlayer;
// Try to report playback stopped before the app closes
if (player && this.isPlaying(player)) {
this._playNextAfterEnded = false;
onPlaybackStopped.call(player);
}
};
self.playbackStartTime = function (player = this._currentPlayer) {
if (player && !enableLocalPlaylistManagement(player) && !player.isLocalPlayer) {
return player.playbackStartTime();
}
const streamInfo = getPlayerData(player).streamInfo;
return streamInfo ? streamInfo.playbackStartTimeTicks : null;
};
if (appHost.supports('remotecontrol')) {
import('serverNotifications').then(({ default: serverNotifications }) => {
events.on(serverNotifications, 'ServerShuttingDown', self.setDefaultPlayerActive.bind(self));
events.on(serverNotifications, 'ServerRestarting', self.setDefaultPlayerActive.bind(self));
});
}
}
getCurrentPlayer() {
return this._currentPlayer;
}
currentTime(player = this._currentPlayer) {
if (player && !enableLocalPlaylistManagement(player) && !player.isLocalPlayer) {
return player.currentTime();
}
return this.getCurrentTicks(player);
}
nextItem(player = this._currentPlayer) {
if (player && !enableLocalPlaylistManagement(player)) {
return player.nextItem();
}
const nextItem = this._playQueueManager.getNextItemInfo();
if (!nextItem || !nextItem.item) {
return Promise.reject();
}
const apiClient = connectionManager.getApiClient(nextItem.item.ServerId);
return apiClient.getItem(apiClient.getCurrentUserId(), nextItem.item.Id);
}
canQueue(item) {
if (item.Type === 'MusicAlbum' || item.Type === 'MusicArtist' || item.Type === 'MusicGenre') {
return this.canQueueMediaType('Audio');
}
return this.canQueueMediaType(item.MediaType);
}
canQueueMediaType(mediaType) {
if (this._currentPlayer) {
return this._currentPlayer.canPlayMediaType(mediaType);
}
return false;
}
isMuted(player = this._currentPlayer) {
if (player) {
return player.isMuted();
}
return false;
}
setMute(mute, player = this._currentPlayer) {
if (player) {
player.setMute(mute);
}
}
toggleMute(mute, player = this._currentPlayer) {
if (player) {
if (player.toggleMute) {
player.toggleMute();
} else {
player.setMute(!player.isMuted());
}
}
}
toggleDisplayMirroring() {
this.enableDisplayMirroring(!this.enableDisplayMirroring());
}
enableDisplayMirroring(enabled) {
if (enabled != null) {
const val = enabled ? '1' : '0';
appSettings.set('displaymirror', val);
return;
}
return (appSettings.get('displaymirror') || '') !== '0';
}
nextChapter(player = this._currentPlayer) {
const item = this.currentItem(player);
const ticks = this.getCurrentTicks(player);
const nextChapter = (item.Chapters || []).filter(function (i) {
return i.StartPositionTicks > ticks;
})[0];
if (nextChapter) {
this.seek(nextChapter.StartPositionTicks, player);
} else {
this.nextTrack(player);
}
}
previousChapter(player = this._currentPlayer) {
const item = this.currentItem(player);
let ticks = this.getCurrentTicks(player);
// Go back 10 seconds
ticks -= 100000000;
// If there's no previous track, then at least rewind to beginning
if (this.getCurrentPlaylistIndex(player) === 0) {
ticks = Math.max(ticks, 0);
}
const previousChapters = (item.Chapters || []).filter(function (i) {
return i.StartPositionTicks <= ticks;
});
if (previousChapters.length) {
this.seek(previousChapters[previousChapters.length - 1].StartPositionTicks, player);
} else {
this.previousTrack(player);
}
}
fastForward(player = this._currentPlayer) {
if (player.fastForward != null) {
player.fastForward(userSettings.skipForwardLength());
return;
}
// Go back 15 seconds
const offsetTicks = userSettings.skipForwardLength() * 10000;
this.seekRelative(offsetTicks, player);
}
rewind(player = this._currentPlayer) {
if (player.rewind != null) {
player.rewind(userSettings.skipBackLength());
return;
}
// Go back 15 seconds
const offsetTicks = 0 - (userSettings.skipBackLength() * 10000);
this.seekRelative(offsetTicks, player);
}
seekPercent(percent, player = this._currentPlayer) {
let ticks = this.duration(player) || 0;
percent /= 100;
ticks *= percent;
this.seek(parseInt(ticks), player);
}
seekMs(ms, player = this._currentPlayer) {
const ticks = ms * 10000;
this.seek(ticks, player);
}
playTrailers(item) {
const player = this._currentPlayer;
if (player && player.playTrailers) {
return player.playTrailers(item);
}
const apiClient = connectionManager.getApiClient(item.ServerId);
const instance = this;
if (item.LocalTrailerCount) {
return apiClient.getLocalTrailers(apiClient.getCurrentUserId(), item.Id).then(function (result) {
return instance.play({
items: result
});
});
} else {
const remoteTrailers = item.RemoteTrailers || [];
if (!remoteTrailers.length) {
return Promise.reject();
}
return this.play({
items: remoteTrailers.map(function (t) {
return {
Name: t.Name || (item.Name + ' Trailer'),
Url: t.Url,
MediaType: 'Video',
Type: 'Trailer',
ServerId: apiClient.serverId()
};
})
});
}
}
getSubtitleUrl(textStream, serverId) {
const apiClient = connectionManager.getApiClient(serverId);
return !textStream.IsExternalUrl ? apiClient.getUrl(textStream.DeliveryUrl) : textStream.DeliveryUrl;
}
stop(player) {
player = player || this._currentPlayer;
if (player) {
if (enableLocalPlaylistManagement(player)) {
this._playNextAfterEnded = false;
}
// TODO: remove second param
return player.stop(true, true);
}
return Promise.resolve();
}
getBufferedRanges(player = this._currentPlayer) {
if (player) {
if (player.getBufferedRanges) {
return player.getBufferedRanges();
}
}
return [];
}
playPause(player = this._currentPlayer) {
if (player) {
if (player.playPause) {
return player.playPause();
}
if (player.paused()) {
return this.unpause(player);
} else {
return this.pause(player);
}
}
}
paused(player = this._currentPlayer) {
if (player) {
return player.paused();
}
}
pause(player = this._currentPlayer) {
if (player) {
player.pause();
}
}
unpause(player = this._currentPlayer) {
if (player) {
player.unpause();
}
}
setPlaybackRate(value, player = this._currentPlayer) {
if (player && player.setPlaybackRate) {
player.setPlaybackRate(value);
}
}
getPlaybackRate(player = this._currentPlayer) {
if (player && player.getPlaybackRate) {
return player.getPlaybackRate();
}
return null;
}
instantMix(item, player = this._currentPlayer) {
if (player && player.instantMix) {
return player.instantMix(item);
}
const apiClient = connectionManager.getApiClient(item.ServerId);
const options = {};
options.UserId = apiClient.getCurrentUserId();
options.Limit = 200;
const instance = this;
apiClient.getInstantMixFromItem(item.Id, options).then(function (result) {
instance.play({
items: result.Items
});
});
}
shuffle(shuffleItem, player = this._currentPlayer) {
if (player && player.shuffle) {
return player.shuffle(shuffleItem);
}
return this.play({ items: [shuffleItem], shuffle: true });
}
audioTracks(player = this._currentPlayer) {
if (player.audioTracks) {
const result = player.audioTracks();
if (result) {
return result;
}
}
const mediaSource = this.currentMediaSource(player);
const mediaStreams = (mediaSource || {}).MediaStreams || [];
return mediaStreams.filter(function (s) {
return s.Type === 'Audio';
});
}
subtitleTracks(player = this._currentPlayer) {
if (player.subtitleTracks) {
const result = player.subtitleTracks();
if (result) {
return result;
}
}
const mediaSource = this.currentMediaSource(player);
const mediaStreams = (mediaSource || {}).MediaStreams || [];
return mediaStreams.filter(function (s) {
return s.Type === 'Subtitle';
});
}
getSupportedCommands(player) {
player = player || this._currentPlayer || { isLocalPlayer: true };
if (player.isLocalPlayer) {
const list = [
'GoHome',
'GoToSettings',
'VolumeUp',
'VolumeDown',
'Mute',
'Unmute',
'ToggleMute',
'SetVolume',
'SetAudioStreamIndex',
'SetSubtitleStreamIndex',
'SetMaxStreamingBitrate',
'DisplayContent',
'GoToSearch',
'DisplayMessage',
'SetRepeatMode',
'SetShuffleQueue',
'PlayMediaSource',
'PlayTrailers'
];
if (appHost.supports('fullscreenchange')) {
list.push('ToggleFullscreen');
}
if (player.supports) {
if (player.supports('PictureInPicture')) {
list.push('PictureInPicture');
}
if (player.supports('AirPlay')) {
list.push('AirPlay');
}
if (player.supports('SetBrightness')) {
list.push('SetBrightness');
}
if (player.supports('SetAspectRatio')) {
list.push('SetAspectRatio');
}
if (player.supports('PlaybackRate')) {
list.push('PlaybackRate');
}
}
return list;
}
const info = this.getPlayerInfo();
return info ? info.supportedCommands : [];
}
setRepeatMode(value, player = this._currentPlayer) {
if (player && !enableLocalPlaylistManagement(player)) {
return player.setRepeatMode(value);
}
this._playQueueManager.setRepeatMode(value);
events.trigger(player, 'repeatmodechange');
}
getRepeatMode(player = this._currentPlayer) {
if (player && !enableLocalPlaylistManagement(player)) {
return player.getRepeatMode();
}
return this._playQueueManager.getRepeatMode();
}
setQueueShuffleMode(value, player = this._currentPlayer) {
if (player && !enableLocalPlaylistManagement(player)) {
return player.setQueueShuffleMode(value);
}
this._playQueueManager.setShuffleMode(value);
events.trigger(player, 'shufflequeuemodechange');
}
getQueueShuffleMode(player = this._currentPlayer) {
if (player && !enableLocalPlaylistManagement(player)) {
return player.getQueueShuffleMode();
}
return this._playQueueManager.getShuffleMode();
}
toggleQueueShuffleMode(player = this._currentPlayer) {
let currentvalue;
if (player && !enableLocalPlaylistManagement(player)) {
currentvalue = player.getQueueShuffleMode();
switch (currentvalue) {
case 'Shuffle':
player.setQueueShuffleMode('Sorted');
break;
case 'Sorted':
player.setQueueShuffleMode('Shuffle');
break;
default:
throw new TypeError('current value for shufflequeue is invalid');
}
} else {
this._playQueueManager.toggleShuffleMode();
}
events.trigger(player, 'shufflequeuemodechange');
}
clearQueue(clearCurrentItem = false, player = this._currentPlayer) {
if (player && !enableLocalPlaylistManagement(player)) {
return player.clearQueue(clearCurrentItem);
}
this._playQueueManager.clearPlaylist(clearCurrentItem);
events.trigger(player, 'playlistitemremove');
}
trySetActiveDeviceName(name) {
name = normalizeName(name);
const instance = this;
instance.getTargets().then(function (result) {
const target = result.filter(function (p) {
return normalizeName(p.name) === name;
})[0];
if (target) {
instance.trySetActivePlayer(target.playerName, target);
}
});
}
displayContent(options, player = this._currentPlayer) {
if (player && player.displayContent) {
player.displayContent(options);
}
}
beginPlayerUpdates(player) {
if (player.beginPlayerUpdates) {
player.beginPlayerUpdates();
}
}
endPlayerUpdates(player) {
if (player.endPlayerUpdates) {
player.endPlayerUpdates();
}
}
setDefaultPlayerActive() {
this.setActivePlayer('localplayer');
}
removeActivePlayer(name) {
const playerInfo = this.getPlayerInfo();
if (playerInfo) {
if (playerInfo.name === name) {
this.setDefaultPlayerActive();
}
}
}
removeActiveTarget(id) {
const playerInfo = this.getPlayerInfo();
if (playerInfo) {
if (playerInfo.id === id) {
this.setDefaultPlayerActive();
}
}
}
sendCommand(cmd, player) {
console.debug('MediaController received command: ' + cmd.Name);
switch (cmd.Name) {
case 'SetRepeatMode':
this.setRepeatMode(cmd.Arguments.RepeatMode, player);
break;
case 'SetShuffleQueue':
this.setQueueShuffleMode(cmd.Arguments.ShuffleMode, player);
break;
case 'VolumeUp':
this.volumeUp(player);
break;
case 'VolumeDown':
this.volumeDown(player);
break;
case 'Mute':
this.setMute(true, player);
break;
case 'Unmute':
this.setMute(false, player);
break;
case 'ToggleMute':
this.toggleMute(player);
break;
case 'SetVolume':
this.setVolume(cmd.Arguments.Volume, player);
break;
case 'SetAspectRatio':
this.setAspectRatio(cmd.Arguments.AspectRatio, player);
break;
case 'PlaybackRate':
this.setPlaybackRate(cmd.Arguments.PlaybackRate, player);
break;
case 'SetBrightness':
this.setBrightness(cmd.Arguments.Brightness, player);
break;
case 'SetAudioStreamIndex':
this.setAudioStreamIndex(parseInt(cmd.Arguments.Index), player);
break;
case 'SetSubtitleStreamIndex':
this.setSubtitleStreamIndex(parseInt(cmd.Arguments.Index), player);
break;
case 'SetMaxStreamingBitrate':
this.setMaxStreamingBitrate(parseInt(cmd.Arguments.Bitrate), player);
break;
case 'ToggleFullscreen':
this.toggleFullscreen(player);
break;
default:
if (player.sendCommand) {
player.sendCommand(cmd);
}
break;
}
}
}
export default new PlaybackManager();