From 7c9cbbcd8dc99a89b8bca4cd61b9129b2a457d9b Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 29 Dec 2016 02:14:59 -0500 Subject: [PATCH 001/102] separate player selection into it's own script --- .../emby-apiclient/.bower.json | 8 +- .../emby-apiclient/apiclientex.js | 450 ++++++++++++++++++ dashboard-ui/components/playerselection.js | 217 +++++++++ dashboard-ui/components/remotecontrol.js | 4 +- dashboard-ui/scripts/librarymenu.js | 2 +- dashboard-ui/scripts/mediacontroller.js | 207 -------- dashboard-ui/scripts/mediaplayer-video.js | 8 +- dashboard-ui/scripts/site.js | 3 + 8 files changed, 685 insertions(+), 214 deletions(-) create mode 100644 dashboard-ui/bower_components/emby-apiclient/apiclientex.js create mode 100644 dashboard-ui/components/playerselection.js diff --git a/dashboard-ui/bower_components/emby-apiclient/.bower.json b/dashboard-ui/bower_components/emby-apiclient/.bower.json index 1a3df41565..39b3ac8daa 100644 --- a/dashboard-ui/bower_components/emby-apiclient/.bower.json +++ b/dashboard-ui/bower_components/emby-apiclient/.bower.json @@ -16,12 +16,12 @@ }, "devDependencies": {}, "ignore": [], - "version": "1.1.107", - "_release": "1.1.107", + "version": "1.1.108", + "_release": "1.1.108", "_resolution": { "type": "version", - "tag": "1.1.107", - "commit": "82a9be9ffc4359043cbbae83031491dc4d2182cc" + "tag": "1.1.108", + "commit": "ff2072d7422bd9e216a23e667c8fe08910c43265" }, "_source": "https://github.com/MediaBrowser/Emby.ApiClient.Javascript.git", "_target": "^1.1.51", diff --git a/dashboard-ui/bower_components/emby-apiclient/apiclientex.js b/dashboard-ui/bower_components/emby-apiclient/apiclientex.js new file mode 100644 index 0000000000..ab5fafcb58 --- /dev/null +++ b/dashboard-ui/bower_components/emby-apiclient/apiclientex.js @@ -0,0 +1,450 @@ +define(['apiclientcore', 'localassetmanager', 'events'], function (apiclientcorefactory, localassetmanager, events) { + 'use strict'; + + var localPrefix = 'local:'; + var localViewPrefix = 'localview:'; + + /** + * Creates a new api client instance + * @param {String} serverAddress + * @param {String} clientName s + * @param {String} applicationVersion + */ + return function (serverAddress, clientName, applicationVersion, deviceName, deviceId, devicePixelRatio) { + + var apiclientcore = new apiclientcorefactory(serverAddress, clientName, applicationVersion, deviceName, deviceId, devicePixelRatio); + + events.on(apiclientcore, 'websocketmessage', onWebSocketMessage); + + var self = this; + + + function getUserViews(userId) { + + return apiclientcore.getUserViews(userId).then(function (result) { + + var serverInfo = apiclientcore.serverInfo(); + + if (serverInfo) { + + return getLocalView(serverInfo.Id, userId).then(function (localView) { + + if (localView) { + + result.Items.push(localView); + result.TotalRecordCount++; + } + + return Promise.resolve(result); + }); + } + + return Promis.resolve(result); + }); + } + + function getLocalView(serverId, userId) { + + return localassetmanager.getViews(serverId, userId).then(function (views) { + + var localView = null; + + if (views.length > 0) { + + localView = { + Name: 'Offline Items', + ServerId: serverId, + Id: 'localview', + Type: 'localview' + }; + } + + return Promise.resolve(localView); + }); + } + + function getItems(userId, options) { + + var serverInfo = apiclientcore.serverInfo(); + + if (serverInfo && options.ParentId === 'localview') { + + return localassetmanager.getViews(serverInfo.Id, userId).then(function (items) { + var result = { + Items: items, + TotalRecordCount: items.length + }; + + return Promise.resolve(result); + }); + + } else if (serverInfo && options && startsWith(options.ParentId, localViewPrefix)) { + + return localassetmanager.getViewItems(serverInfo.Id, userId, options.ParentId).then(function (items) { + + items.forEach(function (item) { + item.Id = localPrefix + item.Id; + }); + + var result = { + Items: items, + TotalRecordCount: items.length + }; + + return Promise.resolve(result); + }); + } else if (options && options.ExcludeItemIds && options.ExcludeItemIds.length) { + + var exItems = options.ExcludeItemIds; + + for (var i = 0; i < exItems.length; i++) { + if (startsWith(exItems[i], localPrefix)) { + return Promise.resolve(this.createEmptyList()); + } + } + } + + return apiclientcore.getItems(userId, options); + } + + function getItem(userId, itemId) { + + if (itemId) { + itemId = itemId.toString(); + } + + if (startsWith(itemId, localViewPrefix)) { + + var serverInfo = apiclientcore.serverInfo(); + + if (serverInfo) { + return localassetmanager.getViews(serverInfo.Id, userId).then(function (items) { + + var views = items.filter(function (item) { + return item.Id === itemId; + }); + + if (views.length > 0) { + return Promise.resolve(views[0]); + } + + // TODO: Test consequence of this + return Promise.reject(); + }); + } + } + + if (startsWith(itemId, localPrefix)) { + + var serverInfo = apiclientcore.serverInfo(); + + if (serverInfo) { + return localassetmanager.getLocalItem(serverInfo.Id, stripStart(itemId, localPrefix)).then(function (item) { + + item.Item.Id = localPrefix + item.Item.Id; + + return Promise.resolve(item.Item); + }); + } + } + + return apiclientcore.getItem(userId, itemId); + } + + function getThemeMedia(userId, itemId, inherit) { + + if (startsWith(itemId, localViewPrefix) || startsWith(itemId, localPrefix)) { + return Promise.reject(); + } + + return apiclientcore.getThemeMedia(userId, itemId, inherit); + } + + function getSimilarItems(itemId, options) { + + if (startsWith(itemId, localPrefix)) { + return Promise.resolve(createEmptyList()); + } + + return apiclientcore.getSimilarItems(itemId, options); + } + + function updateFavoriteStatus(userId, itemId, isFavorite) { + + if (startsWith(itemId, localPrefix)) { + return Promise.resolve(); + } + + return apiclientcore.updateFavoriteStatus(userId, itemId, isFavorite); + } + + function getScaledImageUrl(itemId, options) { + + if (startsWith(itemId, localPrefix)) { + + var serverInfo = apiclientcore.serverInfo(); + var id = stripStart(itemId, localPrefix); + + return localassetmanager.getImageUrl(serverInfo.Id, id, options.type, 0); + } + + + return apiclientcore.getScaledImageUrl(itemId, options); + } + + function onWebSocketMessage(e, msg) { + + events.trigger(self, 'websocketmessage', [msg]); + } + + // **************** Helper functions + + function startsWith(str, find) { + + if (str && find && str.length > find.length) { + if (str.indexOf(find) === 0) { + return true; + } + } + + return false; + } + + function stripStart(str, find) { + if (startsWith(str, find)) { + return str.substr(find.length); + } + + return str; + } + + function createEmptyList() { + var result = { + Items: new Array(), + TotalRecordCount: 0 + } + + return result; + } + + // "Override" methods + self.getUserViews = getUserViews; + self.getItems = getItems; + self.getItem = getItem; + self.getThemeMedia = getThemeMedia; + self.getSimilarItems = getSimilarItems; + self.updateFavoriteStatus = updateFavoriteStatus; + self.getScaledImageUrl = getScaledImageUrl; + + // Map "base" methods + self.serverAddress = apiclientcore.serverAddress; + self.serverInfo = apiclientcore.serverInfo; + self.serverId = apiclientcore.serverId; + self.getCurrentUser = apiclientcore.getCurrentUser; + self.isLoggedIn = apiclientcore.isLoggedIn; + self.getCurrentUserId = apiclientcore.getCurrentUserId; + self.accessToken = apiclientcore.accessToken; + self.deviceName = apiclientcore.deviceName; + self.deviceId = apiclientcore.deviceId; + self.appName = apiclientcore.appName; + self.appVersion = apiclientcore.appVersion; + self.clearAuthenticationInfo = apiclientcore.clearAuthenticationInfo; + self.setAuthenticationInfo = apiclientcore.setAuthenticationInfo; + self.encodeName = apiclientcore.encodeName; + self.setRequestHeaders = apiclientcore.setRequestHeaders; + self.ajax = apiclientcore.ajax; + self.fetch = apiclientcore.fetch; + self.getJSON = apiclientcore.getJSON; + self.fetchWithFailover = apiclientcore.fetchWithFailover; + self.get = apiclientcore.get; + self.getUrl = apiclientcore.getUrl; + self.updateServerInfo = apiclientcore.updateServerInfo; + self.isWebSocketSupported = apiclientcore.isWebSocketSupported; + self.ensureWebSocket = apiclientcore.ensureWebSocket; + self.openWebSocket = apiclientcore.openWebSocket; + self.closeWebSocket = apiclientcore.closeWebSocket; + self.sendWebSocketMessage = apiclientcore.sendWebSocketMessage; + self.isWebSocketOpen = apiclientcore.isWebSocketOpen; + self.isWebSocketOpenOrConnecting = apiclientcore.isWebSocketOpenOrConnecting; + self.getProductNews = apiclientcore.getProductNews; + self.getDownloadSpeed = apiclientcore.getDownloadSpeed; + self.detectBitrate = apiclientcore.detectBitrate; + //self.getItem = apiclientcore.getItem; + self.getRootFolder = apiclientcore.getRootFolder; + self.getNotificationSummary = apiclientcore.getNotificationSummary; + self.getNotifications = apiclientcore.getNotifications; + self.markNotificationsRead = apiclientcore.markNotificationsRead; + self.logout = apiclientcore.logout; + self.getRemoteImageProviders = apiclientcore.getRemoteImageProviders; + self.getAvailableRemoteImages = apiclientcore.getAvailableRemoteImages; + self.downloadRemoteImage = apiclientcore.downloadRemoteImage; + self.getLiveTvInfo = apiclientcore.getLiveTvInfo; + self.getLiveTvGuideInfo = apiclientcore.getLiveTvGuideInfo; + self.getLiveTvChannel = apiclientcore.getLiveTvChannel; + self.getLiveTvChannels = apiclientcore.getLiveTvChannels; + self.getLiveTvPrograms = apiclientcore.getLiveTvPrograms; + self.getLiveTvRecommendedPrograms = apiclientcore.getLiveTvRecommendedPrograms; + self.getLiveTvRecordings = apiclientcore.getLiveTvRecordings; + self.getLiveTvRecordingSeries = apiclientcore.getLiveTvRecordingSeries; + self.getLiveTvRecordingGroups = apiclientcore.getLiveTvRecordingGroups; + self.getLiveTvRecordingGroup = apiclientcore.getLiveTvRecordingGroup; + self.getLiveTvRecording = apiclientcore.getLiveTvRecording; + self.getLiveTvProgram = apiclientcore.getLiveTvProgram; + self.deleteLiveTvRecording = apiclientcore.deleteLiveTvRecording; + self.cancelLiveTvTimer = apiclientcore.cancelLiveTvTimer; + self.getLiveTvTimers = apiclientcore.getLiveTvTimers; + self.getLiveTvTimer = apiclientcore.getLiveTvTimer; + self.getNewLiveTvTimerDefaults = apiclientcore.getNewLiveTvTimerDefaults; + self.createLiveTvTimer = apiclientcore.createLiveTvTimer; + self.updateLiveTvTimer = apiclientcore.updateLiveTvTimer; + self.resetLiveTvTuner = apiclientcore.resetLiveTvTuner; + self.getLiveTvSeriesTimers = apiclientcore.getLiveTvSeriesTimers; + self.getFileOrganizationResults = apiclientcore.getFileOrganizationResults; + self.deleteOriginalFileFromOrganizationResult = apiclientcore.deleteOriginalFileFromOrganizationResult; + self.clearOrganizationLog = apiclientcore.clearOrganizationLog; + self.performOrganization = apiclientcore.performOrganization; + self.performEpisodeOrganization = apiclientcore.performEpisodeOrganization; + self.getLiveTvSeriesTimer = apiclientcore.getLiveTvSeriesTimer; + self.cancelLiveTvSeriesTimer = apiclientcore.cancelLiveTvSeriesTimer; + self.createLiveTvSeriesTimer = apiclientcore.createLiveTvSeriesTimer; + self.updateLiveTvSeriesTimer = apiclientcore.updateLiveTvSeriesTimer; + self.getRegistrationInfo = apiclientcore.getRegistrationInfo; + self.getSystemInfo = apiclientcore.getSystemInfo; + self.getPublicSystemInfo = apiclientcore.getPublicSystemInfo; + self.getInstantMixFromItem = apiclientcore.getInstantMixFromItem; + self.getEpisodes = apiclientcore.getEpisodes; + self.getDisplayPreferences = apiclientcore.getDisplayPreferences; + self.updateDisplayPreferences = apiclientcore.updateDisplayPreferences; + self.getSeasons = apiclientcore.getSeasons; + //self.getSimilarItems = apiclientcore.getSimilarItems; + self.getCultures = apiclientcore.getCultures; + self.getCountries = apiclientcore.getCountries; + self.getPluginSecurityInfo = apiclientcore.getPluginSecurityInfo; + self.getDirectoryContents = apiclientcore.getDirectoryContents; + self.getNetworkShares = apiclientcore.getNetworkShares; + self.getParentPath = apiclientcore.getParentPath; + self.getDrives = apiclientcore.getDrives; + self.getNetworkDevices = apiclientcore.getNetworkDevices; + self.cancelPackageInstallation = apiclientcore.cancelPackageInstallation; + self.refreshItem = apiclientcore.refreshItem; + self.installPlugin = apiclientcore.installPlugin; + self.restartServer = apiclientcore.restartServer; + self.shutdownServer = apiclientcore.shutdownServer; + self.getPackageInfo = apiclientcore.getPackageInfo; + self.getAvailableApplicationUpdate = apiclientcore.getAvailableApplicationUpdate; + self.getAvailablePluginUpdates = apiclientcore.getAvailablePluginUpdates; + self.getVirtualFolders = apiclientcore.getVirtualFolders; + self.getPhysicalPaths = apiclientcore.getPhysicalPaths; + self.getServerConfiguration = apiclientcore.getServerConfiguration; + self.getDevicesOptions = apiclientcore.getDevicesOptions; + self.getContentUploadHistory = apiclientcore.getContentUploadHistory; + self.getNamedConfiguration = apiclientcore.getNamedConfiguration; + self.getScheduledTasks = apiclientcore.getScheduledTasks; + self.startScheduledTask = apiclientcore.startScheduledTask; + self.getScheduledTask = apiclientcore.getScheduledTask; + self.getNextUpEpisodes = apiclientcore.getNextUpEpisodes; + self.stopScheduledTask = apiclientcore.stopScheduledTask; + self.getPluginConfiguration = apiclientcore.getPluginConfiguration; + self.getAvailablePlugins = apiclientcore.getAvailablePlugins; + self.uninstallPlugin = apiclientcore.uninstallPlugin; + self.removeVirtualFolder = apiclientcore.removeVirtualFolder; + self.addVirtualFolder = apiclientcore.addVirtualFolder; + self.updateVirtualFolderOptions = apiclientcore.updateVirtualFolderOptions; + self.renameVirtualFolder = apiclientcore.renameVirtualFolder; + self.addMediaPath = apiclientcore.addMediaPath; + self.updateMediaPath = apiclientcore.updateMediaPath; + self.removeMediaPath = apiclientcore.removeMediaPath; + self.deleteUser = apiclientcore.deleteUser; + self.deleteUserImage = apiclientcore.deleteUserImage; + self.deleteItemImage = apiclientcore.deleteItemImage; + self.deleteItem = apiclientcore.deleteItem; + self.stopActiveEncodings = apiclientcore.stopActiveEncodings; + self.reportCapabilities = apiclientcore.reportCapabilities; + self.updateItemImageIndex = apiclientcore.updateItemImageIndex; + self.getItemImageInfos = apiclientcore.getItemImageInfos; + self.getCriticReviews = apiclientcore.getCriticReviews; + self.getSessions = apiclientcore.getSessions; + self.uploadUserImage = apiclientcore.uploadUserImage; + self.uploadItemImage = apiclientcore.uploadItemImage; + self.getInstalledPlugins = apiclientcore.getInstalledPlugins; + self.getUser = apiclientcore.getUser; + self.getOfflineUser = apiclientcore.getOfflineUser; + self.getStudio = apiclientcore.getStudio; + self.getGenre = apiclientcore.getGenre; + self.getMusicGenre = apiclientcore.getMusicGenre; + self.getGameGenre = apiclientcore.getGameGenre; + self.getArtist = apiclientcore.getArtist; + self.getPerson = apiclientcore.getPerson; + self.getPublicUsers = apiclientcore.getPublicUsers; + self.getUsers = apiclientcore.getUsers; + self.getParentalRatings = apiclientcore.getParentalRatings; + self.getDefaultImageQuality = apiclientcore.getDefaultImageQuality; + self.getUserImageUrl = apiclientcore.getUserImageUrl; + self.getImageUrl = apiclientcore.getImageUrl; + //self.getScaledImageUrl = apiclientcore.getScaledImageUrl; + self.getThumbImageUrl = apiclientcore.getThumbImageUrl; + self.authenticateUserByName = apiclientcore.authenticateUserByName; + self.updateUserPassword = apiclientcore.updateUserPassword; + self.updateEasyPassword = apiclientcore.updateEasyPassword; + self.resetUserPassword = apiclientcore.resetUserPassword; + self.resetEasyPassword = apiclientcore.resetEasyPassword; + self.updateServerConfiguration = apiclientcore.updateServerConfiguration; + self.updateNamedConfiguration = apiclientcore.updateNamedConfiguration; + self.updateItem = apiclientcore.updateItem; + self.updatePluginSecurityInfo = apiclientcore.updatePluginSecurityInfo; + self.createUser = apiclientcore.createUser; + self.updateUser = apiclientcore.updateUser; + self.updateUserPolicy = apiclientcore.updateUserPolicy; + self.updateUserConfiguration = apiclientcore.updateUserConfiguration; + self.updateScheduledTaskTriggers = apiclientcore.updateScheduledTaskTriggers; + self.updatePluginConfiguration = apiclientcore.updatePluginConfiguration; + self.getAncestorItems = apiclientcore.getAncestorItems; + //self.getItems = apiclientcore.getItems; + self.getMovieRecommendations = apiclientcore.getMovieRecommendations; + self.getUpcomingEpisodes = apiclientcore.getUpcomingEpisodes; + self.getChannels = apiclientcore.getChannels; + self.getLatestChannelItems = apiclientcore.getLatestChannelItems; + //self.getUserViews = apiclientcore.getUserViews; + self.getArtists = apiclientcore.getArtists; + self.getAlbumArtists = apiclientcore.getAlbumArtists; + self.getGenres = apiclientcore.getGenres; + self.getMusicGenres = apiclientcore.getMusicGenres; + self.getGameGenres = apiclientcore.getGameGenres; + self.getPeople = apiclientcore.getPeople; + self.getStudios = apiclientcore.getStudios; + self.getLocalTrailers = apiclientcore.getLocalTrailers; + self.getGameSystems = apiclientcore.getGameSystems; + self.getAdditionalVideoParts = apiclientcore.getAdditionalVideoParts; + //self.getThemeMedia = apiclientcore.getThemeMedia; + self.getSearchHints = apiclientcore.getSearchHints; + self.getSpecialFeatures = apiclientcore.getSpecialFeatures; + self.getDateParamValue = apiclientcore.getDateParamValue; + self.markPlayed = apiclientcore.markPlayed; + self.markUnplayed = apiclientcore.markUnplayed; + //self.updateFavoriteStatus = apiclientcore.updateFavoriteStatus; + self.updateUserItemRating = apiclientcore.updateUserItemRating; + self.getItemCounts = apiclientcore.getItemCounts; + self.clearUserItemRating = apiclientcore.clearUserItemRating; + self.reportPlaybackStart = apiclientcore.reportPlaybackStart; + self.reportPlaybackProgress = apiclientcore.reportPlaybackProgress; + self.reportOfflineActions = apiclientcore.reportOfflineActions; + self.syncData = apiclientcore.syncData; + self.getReadySyncItems = apiclientcore.getReadySyncItems; + self.reportSyncJobItemTransferred = apiclientcore.reportSyncJobItemTransferred; + self.cancelSyncItems = apiclientcore.cancelSyncItems; + self.reportPlaybackStopped = apiclientcore.reportPlaybackStopped; + self.sendPlayCommand = apiclientcore.sendPlayCommand; + self.sendCommand = apiclientcore.sendCommand; + self.sendMessageCommand = apiclientcore.sendMessageCommand; + self.sendPlayStateCommand = apiclientcore.sendPlayStateCommand; + self.createPackageReview = apiclientcore.createPackageReview; + self.getPackageReviews = apiclientcore.getPackageReviews; + self.getSmartMatchInfos = apiclientcore.getSmartMatchInfos; + self.deleteSmartMatchEntries = apiclientcore.deleteSmartMatchEntries; + self.createPin = apiclientcore.createPin; + self.getPinStatus = apiclientcore.getPinStatus; + self.exchangePin = apiclientcore.exchangePin; + + } + +}); \ No newline at end of file diff --git a/dashboard-ui/components/playerselection.js b/dashboard-ui/components/playerselection.js new file mode 100644 index 0000000000..d6f8e24615 --- /dev/null +++ b/dashboard-ui/components/playerselection.js @@ -0,0 +1,217 @@ +define(['appSettings', 'events', 'browser', 'libraryMenu', 'loading'], function (appSettings, events, browser, libraryMenu, loading) { + 'use strict'; + + var currentDisplayInfo; + + function mirrorItem(info) { + + var item = info.item; + + MediaController.getCurrentPlayer().displayContent({ + + ItemName: item.Name, + ItemId: item.Id, + ItemType: item.Type, + Context: info.context + }); + } + + function mirrorIfEnabled(info) { + + info = info || currentDisplayInfo; + + if (info && MediaController.enableDisplayMirroring()) { + + var player = MediaController.getPlayerInfo(); + + if (!player.isLocalPlayer && player.supportedCommands.indexOf('DisplayContent') != -1) { + mirrorItem(info); + } + } + } + + function showPlayerSelection(button, enableHistory) { + + var playerInfo = MediaController.getPlayerInfo(); + + if (!playerInfo.isLocalPlayer) { + showActivePlayerMenu(playerInfo); + return; + } + + loading.show(); + + MediaController.getTargets().then(function (targets) { + + var menuItems = targets.map(function (t) { + + var name = t.name; + + if (t.appName && t.appName != t.name) { + name += " - " + t.appName; + } + + return { + name: name, + id: t.id, + selected: playerInfo.id == t.id + }; + + }); + + require(['actionsheet'], function (actionsheet) { + + loading.hide(); + + var menuOptions = { + title: Globalize.translate('HeaderSelectPlayer'), + items: menuItems, + positionTo: button, + + resolveOnClick: true + + }; + + // Unfortunately we can't allow the url to change or chromecast will throw a security error + // Might be able to solve this in the future by moving the dialogs to hashbangs + if (!((enableHistory !== false && !browser.chrome) || AppInfo.isNativeApp)) { + menuOptions.enableHistory = false; + } + + actionsheet.show(menuOptions).then(function (id) { + + var target = targets.filter(function (t) { + return t.id == id; + })[0]; + + MediaController.trySetActivePlayer(target.playerName, target); + + mirrorIfEnabled(); + + }); + }); + }); + } + + function showActivePlayerMenu(playerInfo) { + + require(['dialogHelper', 'dialog', 'emby-checkbox', 'emby-button'], function (dialogHelper) { + showActivePlayerMenuInternal(dialogHelper, playerInfo); + }); + } + + function showActivePlayerMenuInternal(dialogHelper, playerInfo) { + + var html = ''; + + var dialogOptions = { + removeOnClose: true + }; + + dialogOptions.modal = false; + dialogOptions.entryAnimationDuration = 160; + dialogOptions.exitAnimationDuration = 160; + dialogOptions.autoFocus = false; + + var dlg = dialogHelper.createDialog(dialogOptions); + + dlg.classList.add('promptDialog'); + + html += '
'; + html += '

'; + html += (playerInfo.deviceName || playerInfo.name); + html += '

'; + + html += '
'; + + if (playerInfo.supportedCommands.indexOf('DisplayContent') != -1) { + + html += ''; + } + + html += '
'; + + html += '
'; + + html += ''; + html += ''; + html += ''; + html += '
'; + + html += '
'; + dlg.innerHTML = html; + + var chkMirror = dlg.querySelector('.chkMirror'); + + if (chkMirror) { + chkMirror.addEventListener('change', onMirrorChange); + } + + var destination = ''; + + var btnRemoteControl = dlg.querySelector('.btnRemoteControl'); + if (btnRemoteControl) { + btnRemoteControl.addEventListener('click', function () { + destination = 'nowplaying.html'; + dialogHelper.close(dlg); + }); + } + + dlg.querySelector('.btnDisconnect').addEventListener('click', function () { + MediaController.disconnectFromPlayer(); + dialogHelper.close(dlg); + }); + + dlg.querySelector('.btnCancel').addEventListener('click', function () { + dialogHelper.close(dlg); + }); + + dialogHelper.open(dlg).then(function () { + if (destination) { + Dashboard.navigate(destination); + } + }); + } + + function onMirrorChange() { + MediaController.enableDisplayMirroring(this.checked); + } + + function onCastButtonClicked() { + + showPlayerSelection(this); + } + + function bindCastButton() { + var btnCast = document.querySelector('.headerButton-btnCast'); + + if (btnCast) { + btnCast.removeEventListener('click', onCastButtonClicked); + btnCast.addEventListener('click', onCastButtonClicked); + } + } + + document.addEventListener('headercreated', bindCastButton); + bindCastButton(); + + pageClassOn('pagebeforeshow', "page", function () { + + var page = this; + + currentDisplayInfo = null; + }); + + pageClassOn('displayingitem', "libraryPage", function (e) { + + var info = e.detail; + mirrorIfEnabled(info); + }); + + return { + show: showPlayerSelection + }; +}); \ No newline at end of file diff --git a/dashboard-ui/components/remotecontrol.js b/dashboard-ui/components/remotecontrol.js index 9ea80c70ba..54cf846430 100644 --- a/dashboard-ui/components/remotecontrol.js +++ b/dashboard-ui/components/remotecontrol.js @@ -748,7 +748,9 @@ context.querySelector('.typeTextForm').addEventListener('submit', onSendStringSubmit); context.querySelector('.nowPlayingCastIcon').addEventListener('click', function () { - MediaController.showPlayerSelection(); + require(['playerSelectionMenu'], function (playerSelectionMenu) { + playerSelectionMenu.show(); + }); }); context.querySelector('.btnExitRemoteControl').addEventListener('click', function () { diff --git a/dashboard-ui/scripts/librarymenu.js b/dashboard-ui/scripts/librarymenu.js index 8808f8fe80..da2784635c 100644 --- a/dashboard-ui/scripts/librarymenu.js +++ b/dashboard-ui/scripts/librarymenu.js @@ -27,7 +27,7 @@ html += '
'; html += ''; - html += ''; + html += ''; html += ''; diff --git a/dashboard-ui/scripts/mediacontroller.js b/dashboard-ui/scripts/mediacontroller.js index 73e7dfaccb..27d3b421c4 100644 --- a/dashboard-ui/scripts/mediacontroller.js +++ b/dashboard-ui/scripts/mediacontroller.js @@ -1,36 +1,8 @@ define(['appSettings', 'events', 'browser'], function (appSettings, events, browser) { 'use strict'; - var currentDisplayInfo; var datetime; - function mirrorItem(info) { - - var item = info.item; - - MediaController.getCurrentPlayer().displayContent({ - - ItemName: item.Name, - ItemId: item.Id, - ItemType: item.Type, - Context: info.context - }); - } - - function mirrorIfEnabled(info) { - - info = info || currentDisplayInfo; - - if (info && MediaController.enableDisplayMirroring()) { - - var player = MediaController.getPlayerInfo(); - - if (!player.isLocalPlayer && player.supportedCommands.indexOf('DisplayContent') != -1) { - mirrorItem(info); - } - } - } - function monitorPlayer(player) { events.on(player, 'playbackstart', function (e, state) { @@ -67,157 +39,6 @@ }); } - function showPlayerSelection(button, enableHistory) { - - var playerInfo = MediaController.getPlayerInfo(); - - if (!playerInfo.isLocalPlayer) { - showActivePlayerMenu(playerInfo); - return; - } - - Dashboard.showLoadingMsg(); - - MediaController.getTargets().then(function (targets) { - - var menuItems = targets.map(function (t) { - - var name = t.name; - - if (t.appName && t.appName != t.name) { - name += " - " + t.appName; - } - - return { - name: name, - id: t.id, - selected: playerInfo.id == t.id - }; - - }); - - require(['actionsheet'], function (actionsheet) { - - Dashboard.hideLoadingMsg(); - - var menuOptions = { - title: Globalize.translate('HeaderSelectPlayer'), - items: menuItems, - positionTo: button, - - resolveOnClick: true - - }; - - // Unfortunately we can't allow the url to change or chromecast will throw a security error - // Might be able to solve this in the future by moving the dialogs to hashbangs - if (!((enableHistory !== false && !browser.chrome) || AppInfo.isNativeApp)) { - menuOptions.enableHistory = false; - } - - actionsheet.show(menuOptions).then(function (id) { - - var target = targets.filter(function (t) { - return t.id == id; - })[0]; - - MediaController.trySetActivePlayer(target.playerName, target); - - mirrorIfEnabled(); - - }); - }); - }); - } - - function showActivePlayerMenu(playerInfo) { - - require(['dialogHelper', 'dialog', 'emby-checkbox', 'emby-button'], function (dialogHelper) { - showActivePlayerMenuInternal(dialogHelper, playerInfo); - }); - } - - function showActivePlayerMenuInternal(dialogHelper, playerInfo) { - - var html = ''; - - var dialogOptions = { - removeOnClose: true - }; - - dialogOptions.modal = false; - dialogOptions.entryAnimationDuration = 160; - dialogOptions.exitAnimationDuration = 160; - dialogOptions.autoFocus = false; - - var dlg = dialogHelper.createDialog(dialogOptions); - - dlg.classList.add('promptDialog'); - - html += '
'; - html += '

'; - html += (playerInfo.deviceName || playerInfo.name); - html += '

'; - - html += '
'; - - if (playerInfo.supportedCommands.indexOf('DisplayContent') != -1) { - - html += ''; - } - - html += '
'; - - html += '
'; - - html += ''; - html += ''; - html += ''; - html += '
'; - - html += '
'; - dlg.innerHTML = html; - - var chkMirror = dlg.querySelector('.chkMirror'); - - if (chkMirror) { - chkMirror.addEventListener('change', onMirrorChange); - } - - var destination = ''; - - var btnRemoteControl = dlg.querySelector('.btnRemoteControl'); - if (btnRemoteControl) { - btnRemoteControl.addEventListener('click', function () { - destination = 'nowplaying.html'; - dialogHelper.close(dlg); - }); - } - - dlg.querySelector('.btnDisconnect').addEventListener('click', function () { - MediaController.disconnectFromPlayer(); - dialogHelper.close(dlg); - }); - - dlg.querySelector('.btnCancel').addEventListener('click', function () { - dialogHelper.close(dlg); - }); - - dialogHelper.open(dlg).then(function () { - if (destination) { - Dashboard.navigate(destination); - } - }); - } - - function onMirrorChange() { - MediaController.enableDisplayMirroring(this.checked); - } - function mediaController() { var self = this; @@ -1021,8 +842,6 @@ } }); }; - - self.showPlayerSelection = showPlayerSelection; } window.MediaController = new mediaController(); @@ -1057,30 +876,4 @@ initializeApiClient(apiClient); }); }; - - function onCastButtonClicked() { - - showPlayerSelection(this); - } - - document.addEventListener('headercreated', function () { - - var btnCast = document.querySelector('.viewMenuBar .btnCast'); - btnCast.removeEventListener('click', onCastButtonClicked); - btnCast.addEventListener('click', onCastButtonClicked); - }); - - pageClassOn('pagebeforeshow', "page", function () { - - var page = this; - - currentDisplayInfo = null; - }); - - pageClassOn('displayingitem', "libraryPage", function (e) { - - var info = e.detail; - mirrorIfEnabled(info); - }); - }); \ No newline at end of file diff --git a/dashboard-ui/scripts/mediaplayer-video.js b/dashboard-ui/scripts/mediaplayer-video.js index e33ac6d98b..3291d91b85 100644 --- a/dashboard-ui/scripts/mediaplayer-video.js +++ b/dashboard-ui/scripts/mediaplayer-video.js @@ -727,7 +727,7 @@ html += ''; html += '
'; // guide - html += ''; + html += ''; html += ''; html += ''; @@ -941,6 +941,12 @@ } } + self.showPlayerSelection = function (button) { + require(['playerSelectionMenu'], function (playerSelectionMenu) { + playerSelectionMenu.show(button, false); + }); + }; + // Replace audio version self.cleanup = function (mediaRenderer) { diff --git a/dashboard-ui/scripts/site.js b/dashboard-ui/scripts/site.js index d2d077b221..0106f21ffe 100644 --- a/dashboard-ui/scripts/site.js +++ b/dashboard-ui/scripts/site.js @@ -1239,6 +1239,7 @@ var AppInfo = {}; define("directorybrowser", ["components/directorybrowser/directorybrowser"], returnFirstDependency); define("metadataEditor", [embyWebComponentsBowerPath + "/metadataeditor/metadataeditor"], returnFirstDependency); define("personEditor", [embyWebComponentsBowerPath + "/metadataeditor/personeditor"], returnFirstDependency); + define("playerSelectionMenu", ["components/playerselection"], returnFirstDependency); define("libraryMenu", ["scripts/librarymenu"], returnFirstDependency); @@ -2731,6 +2732,8 @@ var AppInfo = {}; } } + postInitDependencies.push('playerSelectionMenu'); + require(postInitDependencies); upgradeLayouts(); initAutoSync(); From d58436bfbac997dc12d7434c6e67a7e645bd7871 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 29 Dec 2016 14:16:33 -0500 Subject: [PATCH 002/102] update components --- .../emby-apiclient/.bower.json | 8 +- .../emby-apiclient/apiclientex.js | 12 +- .../emby-webcomponents/.bower.json | 8 +- .../emby-webcomponents/pluginmanager.js | 152 ++++++++++++++++++ .../emby-webcomponents/sync/syncjoblist.js | 19 ++- 5 files changed, 182 insertions(+), 17 deletions(-) create mode 100644 dashboard-ui/bower_components/emby-webcomponents/pluginmanager.js diff --git a/dashboard-ui/bower_components/emby-apiclient/.bower.json b/dashboard-ui/bower_components/emby-apiclient/.bower.json index 39b3ac8daa..448b2fcb3a 100644 --- a/dashboard-ui/bower_components/emby-apiclient/.bower.json +++ b/dashboard-ui/bower_components/emby-apiclient/.bower.json @@ -16,12 +16,12 @@ }, "devDependencies": {}, "ignore": [], - "version": "1.1.108", - "_release": "1.1.108", + "version": "1.1.109", + "_release": "1.1.109", "_resolution": { "type": "version", - "tag": "1.1.108", - "commit": "ff2072d7422bd9e216a23e667c8fe08910c43265" + "tag": "1.1.109", + "commit": "04821a9677127cb08a156986385d3345ea9d7e74" }, "_source": "https://github.com/MediaBrowser/Emby.ApiClient.Javascript.git", "_target": "^1.1.51", diff --git a/dashboard-ui/bower_components/emby-apiclient/apiclientex.js b/dashboard-ui/bower_components/emby-apiclient/apiclientex.js index ab5fafcb58..aa0552d83a 100644 --- a/dashboard-ui/bower_components/emby-apiclient/apiclientex.js +++ b/dashboard-ui/bower_components/emby-apiclient/apiclientex.js @@ -113,9 +113,11 @@ itemId = itemId.toString(); } + var serverInfo; + if (startsWith(itemId, localViewPrefix)) { - var serverInfo = apiclientcore.serverInfo(); + serverInfo = apiclientcore.serverInfo(); if (serverInfo) { return localassetmanager.getViews(serverInfo.Id, userId).then(function (items) { @@ -136,7 +138,7 @@ if (startsWith(itemId, localPrefix)) { - var serverInfo = apiclientcore.serverInfo(); + serverInfo = apiclientcore.serverInfo(); if (serverInfo) { return localassetmanager.getLocalItem(serverInfo.Id, stripStart(itemId, localPrefix)).then(function (item) { @@ -220,9 +222,9 @@ function createEmptyList() { var result = { - Items: new Array(), + Items: [], TotalRecordCount: 0 - } + }; return result; } @@ -445,6 +447,6 @@ self.getPinStatus = apiclientcore.getPinStatus; self.exchangePin = apiclientcore.exchangePin; - } + }; }); \ No newline at end of file diff --git a/dashboard-ui/bower_components/emby-webcomponents/.bower.json b/dashboard-ui/bower_components/emby-webcomponents/.bower.json index 43c84e47cf..162327891e 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/.bower.json +++ b/dashboard-ui/bower_components/emby-webcomponents/.bower.json @@ -14,12 +14,12 @@ }, "devDependencies": {}, "ignore": [], - "version": "1.4.406", - "_release": "1.4.406", + "version": "1.4.407", + "_release": "1.4.407", "_resolution": { "type": "version", - "tag": "1.4.406", - "commit": "5ef7b315244a1804f2892269a42db94a52a86ea8" + "tag": "1.4.407", + "commit": "12e9ff329e1589da29b56316810c2984eaf681ce" }, "_source": "https://github.com/MediaBrowser/emby-webcomponents.git", "_target": "^1.2.1", diff --git a/dashboard-ui/bower_components/emby-webcomponents/pluginmanager.js b/dashboard-ui/bower_components/emby-webcomponents/pluginmanager.js new file mode 100644 index 0000000000..57344ae0ea --- /dev/null +++ b/dashboard-ui/bower_components/emby-webcomponents/pluginmanager.js @@ -0,0 +1,152 @@ +define(['events'], function (Events) { + 'use strict'; + + function pluginManager() { + + var self = this; + var plugins = []; + + // In lieu of automatic discovery, plugins will register dynamic objects + // Each object will have the following properties: + // name + // type (skin, screensaver, etc) + self.register = function (obj) { + + plugins.push(obj); + Events.trigger(self, 'registered', [obj]); + }; + + self.ofType = function (type) { + + return plugins.filter(function (o) { + return o.type === type; + }); + }; + + self.plugins = function () { + return plugins; + }; + + self.mapRoute = function (plugin, route) { + + if (typeof plugin === 'string') { + plugin = plugins.filter(function (p) { + return (p.id || p.packageName) === plugin; + })[0]; + } + + route = route.path || route; + + if (route.toLowerCase().indexOf('http') === 0) { + return route; + } + + return '/plugins/' + plugin.id + '/' + route; + }; + + // TODO: replace with each plugin version + var cacheParam = new Date().getTime(); + + self.mapPath = function (plugin, path, addCacheParam) { + + if (typeof plugin === 'string') { + plugin = plugins.filter(function (p) { + return (p.id || p.packageName) === plugin; + })[0]; + } + + var url = plugin.baseUrl + '/' + path; + + if (addCacheParam) { + url += url.indexOf('?') === -1 ? '?' : '&'; + url += 'v=' + cacheParam; + } + + return url; + }; + + function loadStrings(plugin, globalize) { + var strings = plugin.getTranslations ? plugin.getTranslations() : []; + return globalize.loadStrings({ + name: plugin.id || plugin.packageName, + strings: strings + }); + } + + function definePluginRoute(route, plugin) { + + route.contentPath = self.mapPath(plugin, route.path); + route.path = self.mapRoute(plugin, route); + + Emby.App.defineRoute(route, plugin.id); + } + + self.loadPlugin = function (url) { + + console.log('Loading plugin: ' + url); + + return new Promise(function (resolve, reject) { + + require([url, 'globalize'], function (pluginFactory, globalize) { + + var plugin = new pluginFactory(); + + // See if it's already installed + var existing = plugins.filter(function (p) { + return p.id === plugin.id; + })[0]; + + if (existing) { + resolve(url); + return; + } + + plugin.installUrl = url; + + var urlLower = url.toLowerCase(); + if (urlLower.indexOf('http:') === -1 && urlLower.indexOf('https:') === -1 && urlLower.indexOf('file:') === -1) { + if (url.indexOf(Emby.Page.baseUrl()) !== 0) { + + url = Emby.Page.baseUrl() + '/' + url; + } + } + + var separatorIndex = Math.max(url.lastIndexOf('/'), url.lastIndexOf('\\')); + plugin.baseUrl = url.substring(0, separatorIndex); + + var paths = {}; + paths[plugin.id] = plugin.baseUrl; + + requirejs.config({ + waitSeconds: 0, + paths: paths + }); + + self.register(plugin); + + if (plugin.getRoutes) { + plugin.getRoutes().forEach(function (route) { + definePluginRoute(route, plugin); + }); + } + + if (plugin.type === 'skin') { + + // translations won't be loaded for skins until needed + resolve(plugin); + } else { + + loadStrings(plugin, globalize).then(function () { + resolve(plugin); + }, reject); + } + }); + }); + }; + } + + var instance = new pluginManager(); + window.Emby = window.Emby || {}; + window.Emby.PluginManager = instance; + return instance; +}); \ No newline at end of file diff --git a/dashboard-ui/bower_components/emby-webcomponents/sync/syncjoblist.js b/dashboard-ui/bower_components/emby-webcomponents/sync/syncjoblist.js index 870df1b4ce..0732004a1d 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/sync/syncjoblist.js +++ b/dashboard-ui/bower_components/emby-webcomponents/sync/syncjoblist.js @@ -1,4 +1,4 @@ -define(['serverNotifications', 'events', 'loading', 'connectionManager', 'imageLoader', 'dom', 'globalize', 'registrationServices', 'listViewStyle'], function (serverNotifications, events, loading, connectionManager, imageLoader, dom, globalize, registrationServices) { +define(['serverNotifications', 'events', 'loading', 'connectionManager', 'imageLoader', 'dom', 'globalize', 'registrationServices', 'layoutManager', 'listViewStyle'], function (serverNotifications, events, loading, connectionManager, imageLoader, dom, globalize, registrationServices, layoutManager) { 'use strict'; function onSyncJobsUpdated(e, apiClient, data) { @@ -79,7 +79,16 @@ globalize.translate('sharedcomponents#CancelSyncJobConfirmation'); var html = ''; - html += '
'; + var tagName = layoutManager.tv ? 'button' : 'div'; + var typeAttribute = tagName === 'button' ? ' type="button"' : ''; + + var listItemClass = 'listItem'; + + if (layoutManager.tv) { + listItemClass += ' listItem-button listItem-focusscale'; + } + + html += '<' + tagName + typeAttribute + ' class="' + listItemClass + '" data-id="' + job.Id + '" data-status="' + job.Status + '">'; var progress = job.Progress || 0; @@ -126,9 +135,11 @@ globalize.translate('sharedcomponents#CancelSyncJobConfirmation'); html += '
'; - html += ''; + if (!layoutManager.tv) { + html += ''; + } - html += ''; + html += ''; return html; } From d00ed609eae78ea678acd67b7f2fe6bddc0eb628 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 29 Dec 2016 14:28:15 -0500 Subject: [PATCH 003/102] add pluginmanager --- dashboard-ui/scripts/site.js | 1 + 1 file changed, 1 insertion(+) diff --git a/dashboard-ui/scripts/site.js b/dashboard-ui/scripts/site.js index 0106f21ffe..bfc24c229c 100644 --- a/dashboard-ui/scripts/site.js +++ b/dashboard-ui/scripts/site.js @@ -1204,6 +1204,7 @@ var AppInfo = {}; itemHelper: embyWebComponentsBowerPath + '/itemhelper', itemShortcuts: embyWebComponentsBowerPath + "/shortcuts", serverNotifications: embyWebComponentsBowerPath + '/servernotifications', + pluginManager: embyWebComponentsBowerPath + '/pluginmanager', webAnimations: bowerPath + '/web-animations-js/web-animations-next-lite.min' }; From 94f609da91a0f26a1bb0ab4cb052bcb2a0acf187 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 29 Dec 2016 14:56:19 -0500 Subject: [PATCH 004/102] update components --- .../emby-webcomponents/.bower.json | 8 +++--- .../emby-webcomponents/guide/guide.css | 26 ++++++++----------- .../emby-webcomponents/guide/guide.js | 2 +- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/dashboard-ui/bower_components/emby-webcomponents/.bower.json b/dashboard-ui/bower_components/emby-webcomponents/.bower.json index 162327891e..26c6aebb29 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/.bower.json +++ b/dashboard-ui/bower_components/emby-webcomponents/.bower.json @@ -14,12 +14,12 @@ }, "devDependencies": {}, "ignore": [], - "version": "1.4.407", - "_release": "1.4.407", + "version": "1.4.408", + "_release": "1.4.408", "_resolution": { "type": "version", - "tag": "1.4.407", - "commit": "12e9ff329e1589da29b56316810c2984eaf681ce" + "tag": "1.4.408", + "commit": "d522bced383a879f2d9f9fdf6344ed8e739d93e0" }, "_source": "https://github.com/MediaBrowser/emby-webcomponents.git", "_target": "^1.2.1", diff --git a/dashboard-ui/bower_components/emby-webcomponents/guide/guide.css b/dashboard-ui/bower_components/emby-webcomponents/guide/guide.css index 108519e265..d5232cd524 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/guide/guide.css +++ b/dashboard-ui/bower_components/emby-webcomponents/guide/guide.css @@ -79,7 +79,7 @@ width: 100%; height: 2px; display: flex; - margin-left: .65vh; + margin-left: .25em; background-color: #52B54B; height: 2px; transform-origin: left; @@ -88,10 +88,10 @@ .currentTimeIndicatorArrowContainer { position: absolute; - bottom: -1vh; + bottom: -.4em; width: 100%; color: #52B54B; - margin-left: .65vh; + margin-left: .25em; transform-origin: left; transition: transform 500ms ease-out; } @@ -102,11 +102,11 @@ } .currentTimeIndicatorArrow { - width: 3vh; - height: 3vh; - font-size: 3vh; + width: 1em; + height: 1em; + font-size: 1.2em; color: #52B54B; - margin-left: -1.5vh; + margin-left: -.52em; } .channelPrograms, .timeslotHeadersInner { @@ -196,7 +196,7 @@ } .channelHeaderCell { - border-bottom: .65vh solid #121212 !important; + border-bottom: .25em solid #121212 !important; background-size: auto 70%; background-position: 92% center; background-repeat: no-repeat; @@ -255,8 +255,8 @@ position: absolute; top: 0; /* Unfortunately the borders using vh get rounded while the bottom property doesn't. So this is a little hack to try and make them even*/ - bottom: .59vh; - border-left: .65vh solid #121212 !important; + bottom: .236em; + border-left: .25em solid #121212 !important; background-color: rgba(32, 32, 32, .95); display: flex; text-decoration: none; @@ -364,14 +364,10 @@ .channelsContainer { display: flex; flex-shrink: 0; -} - -.channelList { - display: flex; flex-direction: column; } -.channelList, .programGrid { +.channelsContainer, .programGrid { contain: layout style; } diff --git a/dashboard-ui/bower_components/emby-webcomponents/guide/guide.js b/dashboard-ui/bower_components/emby-webcomponents/guide/guide.js index 4461279b0a..d0cf9f9b25 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/guide/guide.js +++ b/dashboard-ui/bower_components/emby-webcomponents/guide/guide.js @@ -623,7 +623,7 @@ html += ''; } - var channelList = context.querySelector('.channelList'); + var channelList = context.querySelector('.channelsContainer'); channelList.innerHTML = html; imageLoader.lazyChildren(channelList); } From 54fdf950fe690300d53794dd1af06c8e3c640c91 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Thu, 29 Dec 2016 16:50:53 -0500 Subject: [PATCH 005/102] update logging --- .../emby-webcomponents/playbackmanager.js | 2038 +++++++++++++++++ 1 file changed, 2038 insertions(+) create mode 100644 dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js diff --git a/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js b/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js new file mode 100644 index 0000000000..fa24115ffc --- /dev/null +++ b/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js @@ -0,0 +1,2038 @@ +define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'globalize', 'connectionManager', 'loading', 'serverNotifications'], function (events, datetime, appSettings, pluginManager, userSettings, globalize, connectionManager, loading, serverNotifications) { + 'use strict'; + + function playbackManager() { + + var self = this; + + var currentPlayer; + var lastLocalPlayer; + var repeatMode = 'RepeatNone'; + var playlist = []; + var currentPlaylistIndex; + var currentPlayOptions; + var playNextAfterEnded = true; + var playerStates = {}; + + self.currentItem = function (player) { + var data = getPlayerData(player); + return data.streamInfo ? data.streamInfo.item : null; + }; + + self.currentMediaSource = function (player) { + var data = getPlayerData(player); + return data.streamInfo ? data.streamInfo.mediaSource : null; + }; + + function getCurrentSubtitleStream(player) { + + var index = getPlayerData(player).subtitleStreamIndex; + + if (index == null || index === -1) { + return null; + } + + return getSubtitleStream(player, index); + } + + function getSubtitleStream(player, index) { + return self.currentMediaSource(player).MediaStreams.filter(function (s) { + return s.Type === 'Subtitle' && s.Index === index; + })[0]; + } + + self.audioTracks = function (player) { + var mediaSource = self.currentMediaSource(player); + + var mediaStreams = (mediaSource || {}).MediaStreams || []; + return mediaStreams.filter(function (s) { + return s.Type === 'Audio'; + }); + }; + + self.subtitleTracks = function (player) { + var mediaSource = self.currentMediaSource(player); + + var mediaStreams = (mediaSource || {}).MediaStreams || []; + return mediaStreams.filter(function (s) { + return s.Type === 'Subtitle'; + }); + }; + + self.playlist = function () { + return playlist.slice(0); + }; + + self.currentPlayer = function () { + return currentPlayer; + }; + + function setCurrentPlayer(player) { + currentPlayer = player; + if (player && player.isLocalPlayer) { + lastLocalPlayer = player; + } + } + + self.isPlaying = function () { + var player = currentPlayer; + return player != null && player.currentSrc() != null; + }; + + self.isPlayingVideo = function () { + if (self.isPlaying()) { + var playerData = getPlayerData(currentPlayer); + + return playerData.streamInfo.mediaType === 'Video'; + } + + return false; + }; + + self.isPlayingAudio = function () { + if (self.isPlaying()) { + var playerData = getPlayerData(currentPlayer); + + return playerData.streamInfo.mediaType === 'Audio'; + } + + return false; + }; + + self.getPlayers = function () { + + var players = pluginManager.ofType('mediaplayer'); + + players.sort(function (a, b) { + + return (a.priority || 0) - (b.priority || 0); + }); + + return players; + }; + + self.canPlay = function (item) { + + var itemType = item.Type; + var locationType = item.LocationType; + var mediaType = item.MediaType; + + if (itemType === "MusicGenre" || itemType === "Season" || itemType === "Series" || itemType === "BoxSet" || itemType === "MusicAlbum" || itemType === "MusicArtist" || itemType === "Playlist") { + return true; + } + + if (locationType === "Virtual") { + if (itemType !== "Program") { + return false; + } + } + + if (itemType === "Program") { + if (new Date().getTime() > datetime.parseISO8601Date(item.EndDate).getTime() || new Date().getTime() < datetime.parseISO8601Date(item.StartDate).getTime()) { + return false; + } + } + + return self.getPlayers().filter(function (p) { + + return p.canPlayMediaType(mediaType); + + }).length; + }; + + self.canQueue = function (item) { + + if (item.Type === 'MusicAlbum' || item.Type === 'MusicArtist' || item.Type === 'MusicGenre') { + return self.canQueueMediaType('Audio'); + } + return self.canQueueMediaType(item.MediaType); + }; + + self.canQueueMediaType = function (mediaType) { + + if (currentPlayer) { + return currentPlayer.canPlayMediaType(mediaType); + } + + return false; + }; + + self.isMuted = function () { + + if (currentPlayer) { + return currentPlayer.isMuted(); + } + + return false; + }; + + self.setMute = function (mute) { + + if (currentPlayer) { + currentPlayer.setMute(mute); + } + }; + + self.toggleMute = function (mute) { + + if (currentPlayer) { + self.setMute(!self.isMuted()); + } + }; + + self.volume = function (val) { + + if (currentPlayer) { + return currentPlayer.volume(val); + } + }; + + self.volumeUp = function () { + + if (currentPlayer) { + currentPlayer.volumeUp(); + } + }; + + self.volumeDown = function () { + + if (currentPlayer) { + currentPlayer.volumeDown(); + } + }; + + self.setAudioStreamIndex = function (index) { + + var player = currentPlayer; + + if (getPlayerData(player).streamInfo.playMethod === 'Transcode' || !player.canSetAudioStreamIndex()) { + + changeStream(player, getCurrentTicks(player), { AudioStreamIndex: index }); + getPlayerData(player).audioStreamIndex = index; + + } else { + player.setAudioStreamIndex(index); + getPlayerData(player).audioStreamIndex = index; + } + }; + + self.setSubtitleStreamIndex = function (index) { + + var player = currentPlayer; + var currentStream = getCurrentSubtitleStream(player); + + var newStream = getSubtitleStream(player, index); + + if (!currentStream && !newStream) { + return; + } + + var selectedTrackElementIndex = -1; + + if (currentStream && !newStream) { + + if (currentStream.DeliveryMethod === 'Encode') { + + // Need to change the transcoded stream to remove subs + changeStream(player, getCurrentTicks(player), { SubtitleStreamIndex: -1 }); + } + } + else if (!currentStream && newStream) { + + if (newStream.DeliveryMethod === 'External' || newStream.DeliveryMethod === 'Embed') { + selectedTrackElementIndex = index; + } else { + + // Need to change the transcoded stream to add subs + changeStream(player, getCurrentTicks(player), { SubtitleStreamIndex: index }); + } + } + else if (currentStream && newStream) { + + if (newStream.DeliveryMethod === 'External' || newStream.DeliveryMethod === 'Embed') { + selectedTrackElementIndex = index; + + if (currentStream.DeliveryMethod !== 'External' && currentStream.DeliveryMethod !== '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.toggleDisplayMirroring = function () { + self.enableDisplayMirroring(!self.enableDisplayMirroring()); + }; + + self.enableDisplayMirroring = function (enabled) { + + if (enabled != null) { + + var val = enabled ? '1' : '0'; + appSettings.set('displaymirror--' + Dashboard.getCurrentUserId(), val); + + if (enabled) { + mirrorIfEnabled(); + } + return; + } + + return (appSettings.get('displaymirror--' + Dashboard.getCurrentUserId()) || '') != '0'; + }; + + self.stop = function () { + if (currentPlayer) { + playNextAfterEnded = false; + currentPlayer.stop(true, true); + } + }; + + self.playPause = function () { + if (currentPlayer) { + + if (currentPlayer.paused()) { + self.unpause(); + } else { + self.pause(); + } + } + }; + + self.paused = function () { + + if (currentPlayer) { + return currentPlayer.paused(); + } + }; + + self.pause = function () { + if (currentPlayer) { + currentPlayer.pause(); + } + }; + + self.unpause = function () { + if (currentPlayer) { + currentPlayer.unpause(); + } + }; + + self.seek = function (ticks) { + + var player = self.currentPlayer(); + + changeStream(player, ticks); + }; + + self.nextChapter = function () { + + var player = self.currentPlayer(); + var item = self.currentItem(player); + + var ticks = getCurrentTicks(player); + + var nextChapter = (item.Chapters || []).filter(function (i) { + + return i.StartPositionTicks > ticks; + + })[0]; + + if (nextChapter) { + self.seek(nextChapter.StartPositionTicks); + } else { + self.nextTrack(); + } + }; + + self.previousChapter = function () { + var player = self.currentPlayer(); + var item = self.currentItem(player); + + var ticks = getCurrentTicks(player); + + // Go back 10 seconds + ticks -= 100000000; + + var previousChapters = (item.Chapters || []).filter(function (i) { + + return i.StartPositionTicks <= ticks; + }); + + if (previousChapters.length) { + self.seek(previousChapters[previousChapters.length - 1].StartPositionTicks); + } else { + self.previousTrack(); + } + }; + + self.fastForward = function () { + + var player = self.currentPlayer(); + + if (player.fastForward != null) { + player.fastForward(userSettings.skipForwardLength()); + return; + } + + var ticks = getCurrentTicks(player); + + // Go back 15 seconds + ticks += userSettings.skipForwardLength() * 10000; + + var data = getPlayerData(player).streamInfo; + var mediaSource = data.mediaSource; + + if (mediaSource) { + var runTimeTicks = mediaSource.RunTimeTicks || 0; + + if (ticks < runTimeTicks) { + self.seek(ticks); + } + } + }; + + self.rewind = function () { + + var player = self.currentPlayer(); + + if (player.rewind != null) { + player.rewind(userSettings.skipBackLength()); + return; + } + + var ticks = getCurrentTicks(player); + + // Go back 15 seconds + ticks -= userSettings.skipBackLength() * 10000; + + self.seek(Math.max(0, ticks)); + }; + + // Returns true if the player can seek using native client-side seeking functions + function canPlayerSeek(player) { + + var currentSrc = (player.currentSrc() || '').toLowerCase(); + + if (currentSrc.indexOf('.m3u8') !== -1) { + + return true; + + } else { + var duration = player.duration(); + return duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY; + } + } + + function changeStream(player, ticks, params) { + + if (canPlayerSeek(player) && params == null) { + + player.currentTime(parseInt(ticks / 10000)); + return; + } + + params = params || {}; + + var currentSrc = player.currentSrc(); + + var liveStreamId = getPlayerData(player).streamInfo.liveStreamId; + var playSessionId = getPlayerData(player).streamInfo.playSessionId; + + var playerData = getPlayerData(player); + var currentItem = playerData.streamInfo.item; + + player.getDeviceProfile(currentItem).then(function (deviceProfile) { + + var audioStreamIndex = params.AudioStreamIndex == null ? getPlayerData(player).audioStreamIndex : params.AudioStreamIndex; + var subtitleStreamIndex = params.SubtitleStreamIndex == null ? getPlayerData(player).subtitleStreamIndex : params.SubtitleStreamIndex; + + var currentMediaSource = playerData.streamInfo.mediaSource; + var apiClient = connectionManager.getApiClient(currentItem.ServerId); + + if (ticks) { + ticks = parseInt(ticks); + } + + getPlaybackInfo(apiClient, currentItem.Id, deviceProfile, appSettings.maxStreamingBitrate(), ticks, currentMediaSource, audioStreamIndex, subtitleStreamIndex, liveStreamId).then(function (result) { + + if (validatePlaybackInfoResult(result)) { + + currentMediaSource = result.MediaSources[0]; + createStreamInfo(apiClient, currentItem.MediaType, currentItem, currentMediaSource, ticks).then(function (streamInfo) { + + streamInfo.fullscreen = currentPlayOptions.fullscreen; + + if (!streamInfo.url) { + showPlaybackInfoErrorMessage('NoCompatibleStream'); + self.nextTrack(); + return; + } + + getPlayerData(player).subtitleStreamIndex = subtitleStreamIndex; + getPlayerData(player).audioStreamIndex = audioStreamIndex; + + changeStreamToUrl(apiClient, player, playSessionId, streamInfo); + }); + } + }); + }); + } + + function changeStreamToUrl(apiClient, player, playSessionId, streamInfo, newPositionTicks) { + + clearProgressInterval(player); + + getPlayerData(player).isChangingStream = true; + + if (getPlayerData(player).MediaType === "Video") { + apiClient.stopActiveEncodings(playSessionId).then(function () { + + setSrcIntoPlayer(apiClient, player, streamInfo); + }); + + } else { + + setSrcIntoPlayer(apiClient, player, streamInfo); + } + } + + function setSrcIntoPlayer(apiClient, player, streamInfo) { + + player.play(streamInfo).then(function () { + + getPlayerData(player).isChangingStream = false; + getPlayerData(player).streamInfo = streamInfo; + + startProgressInterval(player); + sendProgressUpdate(player); + }); + } + + self.seekPercent = function (percent, player) { + + var data = getPlayerData(player).streamInfo; + var mediaSource = data.mediaSource; + + if (mediaSource) { + var ticks = mediaSource.RunTimeTicks || 0; + + percent /= 100; + ticks *= percent; + self.seek(parseInt(ticks)); + } + }; + + self.playTrailers = function (item) { + + var apiClient = connectionManager.getApiClient(item.ServerId); + + if (item.LocalTrailerCount) { + apiClient.getLocalTrailers(apiClient.getCurrentUserId(), item.Id).then(function (result) { + + self.play({ + items: result + }); + }); + } else { + var remoteTrailers = item.RemoteTrailers || []; + + if (!remoteTrailers.length) { + return; + } + + self.play({ + items: remoteTrailers.map(function (t) { + return { + Name: t.Name || (item.Name + ' Trailer'), + Url: t.Url, + MediaType: 'Video', + Type: 'Trailer' + }; + }) + }); + } + }; + + self.play = function (options) { + + if (typeof (options) === 'string') { + options = { ids: [options] }; + } + + return playItems(options); + }; + + self.instantMix = function (id, serverId) { + + if (typeof id !== 'string') { + var item = id; + id = item.Id; + serverId = item.ServerId; + } + + var apiClient = connectionManager.getApiClient(serverId); + + var options = {}; + options.UserId = apiClient.getCurrentUserId(); + options.Fields = 'MediaSources'; + + apiClient.getInstantMixFromItem(id, options).then(function (result) { + self.play({ + items: result.Items + }); + }); + }; + + self.shuffle = function (id, serverId) { + + if (typeof id !== 'string') { + var item = id; + id = item.Id; + serverId = item.ServerId; + } + + var apiClient = connectionManager.getApiClient(serverId); + + apiClient.getItem(apiClient.getCurrentUserId(), id).then(function (item) { + + var query = { + Fields: "MediaSources,Chapters", + Limit: 100, + Filters: "IsNotFolder", + Recursive: true, + SortBy: "Random" + }; + + if (item.Type === "MusicArtist") { + + query.MediaTypes = "Audio"; + query.ArtistIds = item.Id; + + } + else if (item.Type === "MusicGenre") { + + query.MediaTypes = "Audio"; + query.Genres = item.Name; + + } + else if (item.IsFolder) { + query.ParentId = id; + + } + else { + return; + } + + getItemsForPlayback(item.ServerId, query).then(function (result) { + + self.play({ items: result.Items }); + + }); + }); + }; + + function getPlayerData(player) { + + if (!player) { + throw new Error('player cannot be null'); + } + if (!player.name) { + throw new Error('player name cannot be null'); + } + var state = playerStates[player.name]; + + if (!state) { + playerStates[player.name] = {}; + state = playerStates[player.name]; + } + + return player; + } + + self.getPlayerState = function (player) { + + player = player || currentPlayer; + var playerData = getPlayerData(player); + var item = playerData.streamInfo.item; + var mediaSource = playerData.streamInfo.mediaSource; + + var state = { + PlayState: {} + }; + + if (player) { + + state.PlayState.VolumeLevel = player.volume(); + state.PlayState.IsMuted = player.isMuted(); + state.PlayState.IsPaused = player.paused(); + state.PlayState.PositionTicks = getCurrentTicks(player); + state.PlayState.RepeatMode = self.getRepeatMode(); + + var currentSrc = player.currentSrc(); + + if (currentSrc) { + + state.PlayState.SubtitleStreamIndex = playerData.subtitleStreamIndex; + state.PlayState.AudioStreamIndex = playerData.audioStreamIndex; + + state.PlayState.PlayMethod = playerData.streamInfo.playMethod; + + if (mediaSource) { + state.PlayState.LiveStreamId = mediaSource.LiveStreamId; + } + state.PlayState.PlaySessionId = playerData.streamInfo.playSessionId; + } + } + + 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); + } + + return state; + }; + + self.currentTime = function (player) { + return getCurrentTicks(player); + }; + + function getCurrentTicks(player) { + + var playerTime = Math.floor(10000 * (player || currentPlayer).currentTime()); + playerTime += getPlayerData(player).streamInfo.transcodingOffsetTicks || 0; + + return playerTime; + } + + function getNowPlayingItemForReporting(player, item, mediaSource) { + + var nowPlayingItem = {}; + + if (mediaSource) { + nowPlayingItem.RunTimeTicks = mediaSource.RunTimeTicks; + } else { + nowPlayingItem.RunTimeTicks = player.duration() * 10000; + } + + nowPlayingItem.Id = item.Id; + nowPlayingItem.MediaType = item.MediaType; + nowPlayingItem.Type = item.Type; + nowPlayingItem.Name = item.Name; + + nowPlayingItem.IndexNumber = item.IndexNumber; + nowPlayingItem.IndexNumberEnd = item.IndexNumberEnd; + nowPlayingItem.ParentIndexNumber = item.ParentIndexNumber; + nowPlayingItem.ProductionYear = item.ProductionYear; + nowPlayingItem.PremiereDate = item.PremiereDate; + nowPlayingItem.SeriesName = item.SeriesName; + nowPlayingItem.Album = item.Album; + nowPlayingItem.Artists = item.ArtistItems; + + var imageTags = item.ImageTags || {}; + + if (item.SeriesPrimaryImageTag) { + + nowPlayingItem.PrimaryImageItemId = item.SeriesId; + nowPlayingItem.PrimaryImageTag = item.SeriesPrimaryImageTag; + } + else if (imageTags.Primary) { + + nowPlayingItem.PrimaryImageItemId = item.Id; + nowPlayingItem.PrimaryImageTag = imageTags.Primary; + } + else if (item.AlbumPrimaryImageTag) { + + nowPlayingItem.PrimaryImageItemId = item.AlbumId; + nowPlayingItem.PrimaryImageTag = item.AlbumPrimaryImageTag; + } + else if (item.SeriesPrimaryImageTag) { + + nowPlayingItem.PrimaryImageItemId = item.SeriesId; + nowPlayingItem.PrimaryImageTag = item.SeriesPrimaryImageTag; + } + + if (item.BackdropImageTags && item.BackdropImageTags.length) { + + nowPlayingItem.BackdropItemId = item.Id; + nowPlayingItem.BackdropImageTag = item.BackdropImageTags[0]; + } + else if (item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) { + nowPlayingItem.BackdropItemId = item.ParentBackdropItemId; + nowPlayingItem.BackdropImageTag = item.ParentBackdropImageTags[0]; + } + + if (imageTags.Thumb) { + + nowPlayingItem.ThumbItemId = item.Id; + nowPlayingItem.ThumbImageTag = imageTags.Thumb; + } + + if (imageTags.Logo) { + + nowPlayingItem.LogoItemId = item.Id; + nowPlayingItem.LogoImageTag = imageTags.Logo; + } + else if (item.ParentLogoImageTag) { + + nowPlayingItem.LogoItemId = item.ParentLogoItemId; + nowPlayingItem.LogoImageTag = item.ParentLogoImageTag; + } + + return nowPlayingItem; + } + + function playItems(options, method) { + + normalizePlayOptions(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(); + } + + return getItemsForPlayback(options.serverId, { + + Ids: options.ids.join(',') + + }).then(function (result) { + + return translateItemsForPlayback(result.Items, options).then(function (items) { + + return playWithIntros(items, options); + }); + + }); + } + } + + function translateItemsForPlayback(items, options) { + + var firstItem = items[0]; + var promise; + + var serverId = firstItem.ServerId; + + if (firstItem.Type === "Program") { + + promise = getItemsForPlayback(serverId, { + Ids: firstItem.ChannelId, + }); + } + else if (firstItem.Type === "Playlist") { + + promise = getItemsForPlayback(serverId, { + ParentId: firstItem.Id, + }); + } + else if (firstItem.Type === "MusicArtist") { + + promise = getItemsForPlayback(serverId, { + ArtistIds: firstItem.Id, + Filters: "IsNotFolder", + Recursive: true, + SortBy: "SortName", + MediaTypes: "Audio" + }); + + } + else if (firstItem.Type === "MusicGenre") { + + promise = getItemsForPlayback(serverId, { + Genres: firstItem.Name, + Filters: "IsNotFolder", + Recursive: true, + SortBy: "SortName", + MediaTypes: "Audio" + }); + } + else if (firstItem.IsFolder) { + + promise = getItemsForPlayback(serverId, { + ParentId: firstItem.Id, + Filters: "IsNotFolder", + Recursive: true, + SortBy: "SortName", + MediaTypes: "Audio,Video" + }); + } + else if (firstItem.Type === "Episode" && items.length === 1 && getPlayer(firstItem, options).supportsProgress !== false) { + + promise = new Promise(function (resolve, reject) { + var 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: "MediaSources,Chapters" + + }).then(function (episodesResult) { + + var 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); + } + } + + function playWithIntros(items, options, user) { + + var firstItem = items[0]; + + if (firstItem.MediaType === "Video") { + + //Dashboard.showModalLoadingMsg(); + } + + var afterPlayInternal = function () { + setPlaylistState(0, items); + loading.hide(); + }; + + if (options.startPositionTicks || firstItem.MediaType !== 'Video' || !isServerItem(firstItem) || options.fullscreen === false || !userSettings.enableCinemaMode()) { + + currentPlayOptions = options; + return playInternal(firstItem, options, afterPlayInternal); + } + + var apiClient = connectionManager.getApiClient(firstItem.ServerId); + + return apiClient.getJSON(apiClient.getUrl('Users/' + apiClient.getCurrentUserId() + '/Items/' + firstItem.Id + '/Intros')).then(function (intros) { + + items = intros.Items.concat(items); + currentPlayOptions = options; + return playInternal(items[0], options, afterPlayInternal); + }); + } + + function isServerItem(item) { + if (!item.Id) { + return false; + } + return true; + } + + // Set currentPlaylistIndex and playlist. Using a method allows for overloading in derived player implementations + function setPlaylistState(i, items) { + if (!isNaN(i)) { + currentPlaylistIndex = i; + } + if (items) { + playlist = items.slice(0); + } + } + + function playInternal(item, playOptions, callback) { + + if (item.IsPlaceHolder) { + loading.hide(); + showPlaybackInfoErrorMessage('PlaceHolder', true); + return Promise.reject(); + } + + // Normalize defaults to simplfy checks throughout the process + normalizePlayOptions(playOptions); + + return runInterceptors(item, playOptions).then(function () { + + if (playOptions.fullscreen) { + loading.show(); + } + + if (item.MediaType === 'Video' && isServerItem(item) && appSettings.enableAutomaticBitrateDetection()) { + + var apiClient = connectionManager.getApiClient(item.ServerId); + return apiClient.detectBitrate().then(function (bitrate) { + + appSettings.maxStreamingBitrate(bitrate); + + return playAfterBitrateDetect(connectionManager, bitrate, item, playOptions).then(callback); + + }, function () { + + return playAfterBitrateDetect(connectionManager, appSettings.maxStreamingBitrate(), item, playOptions).then(callback); + }); + + } else { + + return playAfterBitrateDetect(connectionManager, appSettings.maxStreamingBitrate(), item, playOptions).then(callback); + } + + }, function () { + + var player = currentPlayer; + + if (player) { + player.destroy(); + } + setCurrentPlayer(null); + + events.trigger(self, 'playbackcancelled'); + + return Promise.reject(); + }); + } + + function runInterceptors(item, playOptions) { + + return new Promise(function (resolve, reject) { + + var interceptors = pluginManager.ofType('preplayintercept'); + + interceptors.sort(function (a, b) { + return (a.order || 0) - (b.order || 0); + }); + + if (!interceptors.length) { + resolve(); + return; + } + + loading.hide(); + + var 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; + } + + var interceptor = interceptors[index]; + + interceptor.intercept(options).then(function () { + + runNextPrePlay(interceptors, index + 1, options, resolve, reject); + + }, reject); + } + + function playAfterBitrateDetect(connectionManager, maxBitrate, item, playOptions) { + + var startPosition = playOptions.startPositionTicks; + + var player = getPlayer(item, playOptions); + var activePlayer = currentPlayer; + + var promise; + + if (activePlayer) { + + // TODO: if changing players within the same playlist, this will cause nextItem to be null + playNextAfterEnded = false; + promise = onPlaybackChanging(activePlayer, player, item); + } else { + promise = Promise.resolve(); + } + + if (!isServerItem(item) || item.MediaType === 'Game') { + return promise.then(function () { + var streamInfo = createStreamInfoFromUrlItem(item); + streamInfo.fullscreen = playOptions.fullscreen; + getPlayerData(player).isChangingStream = false; + return player.play(streamInfo).then(function () { + onPlaybackStarted(player, streamInfo); + loading.hide(); + return Promise.resolve(); + }); + }); + } + + return Promise.all([promise, player.getDeviceProfile(item)]).then(function (responses) { + + var deviceProfile = responses[1]; + + var apiClient = connectionManager.getApiClient(item.ServerId); + return getPlaybackMediaSource(apiClient, deviceProfile, maxBitrate, item, startPosition).then(function (mediaSource) { + + return createStreamInfo(apiClient, item.MediaType, item, mediaSource, startPosition).then(function (streamInfo) { + + streamInfo.fullscreen = playOptions.fullscreen; + + getPlayerData(player).isChangingStream = false; + + return player.play(streamInfo).then(function () { + onPlaybackStarted(player, streamInfo, mediaSource); + loading.hide(); + return Promise.resolve(); + }); + }); + }); + }); + } + + function createStreamInfoFromUrlItem(item) { + + // Check item.Path for games + return { + url: item.Url || item.Path, + playMethod: 'DirectPlay', + item: item, + textTracks: [], + mediaType: item.MediaType + }; + } + + 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 createStreamInfo(apiClient, type, item, mediaSource, startPosition) { + + var mediaUrl; + var contentType; + var transcodingOffsetTicks = 0; + var playerStartPositionTicks = startPosition; + var liveStreamId; + + var playMethod = 'Transcode'; + + var mediaSourceContainer = (mediaSource.Container || '').toLowerCase(); + var directOptions; + + if (type === 'Video') { + + contentType = getMimeType('video', mediaSourceContainer); + + if (mediaSource.enableDirectPlay) { + mediaUrl = mediaSource.Path; + + playMethod = 'DirectPlay'; + + } 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; + liveStreamId = mediaSource.LiveStreamId; + } + + mediaUrl = apiClient.getUrl('Videos/' + 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 { + + playerStartPositionTicks = null; + contentType = getMimeType('video', mediaSource.TranscodingContainer); + + if (mediaUrl.toLowerCase().indexOf('copytimestamps=true') === -1) { + transcodingOffsetTicks = startPosition || 0; + } + } + } + } + + } else if (type === 'Audio') { + + contentType = getMimeType('audio', mediaSourceContainer); + + if (mediaSource.enableDirectPlay) { + + mediaUrl = mediaSource.Path; + + playMethod = 'DirectPlay'; + + } else { + + var isDirectStream = mediaSource.SupportsDirectStream; + + if (isDirectStream) { + + 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; + liveStreamId = mediaSource.LiveStreamId; + } + + mediaUrl = apiClient.getUrl('Audio/' + 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 { + + transcodingOffsetTicks = startPosition || 0; + playerStartPositionTicks = null; + contentType = getMimeType('audio', mediaSource.TranscodingContainer); + } + } + } + } else if (type === 'Game') { + + mediaUrl = mediaSource.Path; + playMethod = 'DirectPlay'; + } + + var resultInfo = { + url: mediaUrl, + mimeType: contentType, + transcodingOffsetTicks: transcodingOffsetTicks, + playMethod: playMethod, + playerStartPositionTicks: playerStartPositionTicks, + item: item, + mediaSource: mediaSource, + textTracks: getTextTracks(apiClient, mediaSource), + // duplicate this temporarily + tracks: getTextTracks(apiClient, mediaSource), + mediaType: type, + liveStreamId: liveStreamId, + playSessionId: getParam('playSessionId', mediaUrl), + title: item.Name + }; + + var backdropUrl = backdropImageUrl(apiClient, item, {}); + if (backdropUrl) { + resultInfo.backdropUrl = backdropUrl; + } + + return Promise.resolve(resultInfo); + } + + function getParam(name, url) { + name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]"); + var regexS = "[\\?&]" + name + "=([^&#]*)"; + var regex = new RegExp(regexS, "i"); + + var results = regex.exec(url); + if (results == null) { + return ""; + } + else { + return decodeURIComponent(results[1].replace(/\+/g, " ")); + } + } + + self.getSubtitleUrl = function (textStream, serverId) { + + var apiClient = connectionManager.getApiClient(serverId); + var textStreamUrl = !textStream.IsExternalUrl ? apiClient.getUrl(textStream.DeliveryUrl) : textStream.DeliveryUrl; + return textStreamUrl; + }; + + function getTextTracks(apiClient, mediaSource) { + + var subtitleStreams = mediaSource.MediaStreams.filter(function (s) { + return s.Type === 'Subtitle'; + }); + + var textStreams = subtitleStreams.filter(function (s) { + return s.DeliveryMethod === 'External'; + }); + + var tracks = []; + + for (var i = 0, length = textStreams.length; i < length; i++) { + + var textStream = textStreams[i]; + var 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(apiClient, deviceProfile, maxBitrate, item, startPosition, callback) { + + if (item.MediaType === "Video") { + + //Dashboard.showModalLoadingMsg(); + } + + return getPlaybackInfo(apiClient, item.Id, deviceProfile, maxBitrate, startPosition).then(function (playbackInfoResult) { + + if (validatePlaybackInfoResult(playbackInfoResult)) { + + return getOptimalMediaSource(apiClient, item, playbackInfoResult.MediaSources).then(function (mediaSource) { + if (mediaSource) { + + if (mediaSource.RequiresOpening) { + + return getLiveStream(apiClient, item.Id, playbackInfoResult.PlaySessionId, deviceProfile, startPosition, mediaSource, null, null).then(function (openLiveStreamResult) { + + return supportsDirectPlay(apiClient, openLiveStreamResult.MediaSource).then(function (result) { + + openLiveStreamResult.MediaSource.enableDirectPlay = result; + return openLiveStreamResult.MediaSource; + }); + + }); + + } else { + return mediaSource; + } + } else { + //Dashboard.hideModalLoadingMsg(); + showPlaybackInfoErrorMessage('NoCompatibleStream'); + return Promise.reject(); + } + }); + } else { + return Promise.reject(); + } + }); + } + + function getPlaybackInfo(apiClient, itemId, deviceProfile, maxBitrate, startPosition, mediaSource, audioStreamIndex, subtitleStreamIndex, liveStreamId) { + + var postData = { + DeviceProfile: deviceProfile + }; + + var query = { + UserId: apiClient.getCurrentUserId(), + StartTimeTicks: startPosition || 0 + }; + + if (audioStreamIndex != null) { + query.AudioStreamIndex = audioStreamIndex; + } + if (subtitleStreamIndex != null) { + query.SubtitleStreamIndex = subtitleStreamIndex; + } + if (mediaSource) { + query.MediaSourceId = mediaSource.Id; + } + if (liveStreamId) { + query.LiveStreamId = liveStreamId; + } + if (maxBitrate) { + query.MaxStreamingBitrate = maxBitrate; + } + + return apiClient.ajax({ + url: apiClient.getUrl('Items/' + itemId + '/PlaybackInfo', query), + type: 'POST', + data: JSON.stringify(postData), + contentType: "application/json", + dataType: "json" + + }); + } + + function getOptimalMediaSource(apiClient, item, versions) { + + var promises = versions.map(function (v) { + return supportsDirectPlay(apiClient, v); + }); + + if (!promises.length) { + return Promise.reject(); + } + + return Promise.all(promises).then(function (results) { + + for (var i = 0, length = versions.length; i < length; i++) { + versions[i].enableDirectPlay = results[i] || false; + } + var 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; + }); + } + + function getLiveStream(apiClient, itemId, playSessionId, deviceProfile, startPosition, mediaSource, audioStreamIndex, subtitleStreamIndex) { + + var postData = { + DeviceProfile: deviceProfile, + OpenToken: mediaSource.OpenToken + }; + + var query = { + UserId: apiClient.getCurrentUserId(), + StartTimeTicks: startPosition || 0, + ItemId: itemId, + PlaySessionId: playSessionId + }; + + if (audioStreamIndex != null) { + query.AudioStreamIndex = audioStreamIndex; + } + if (subtitleStreamIndex != null) { + query.SubtitleStreamIndex = subtitleStreamIndex; + } + + return apiClient.ajax({ + url: apiClient.getUrl('LiveStreams/Open', query), + type: 'POST', + data: JSON.stringify(postData), + contentType: "application/json", + dataType: "json" + + }); + } + + function supportsDirectPlay(apiClient, mediaSource) { + + return new Promise(function (resolve, reject) { + + if (mediaSource.SupportsDirectPlay) { + + 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) { + resolve(true); + } + else { + var val = mediaSource.Path.toLowerCase().replace('https:', 'http').indexOf(apiClient.serverAddress().toLowerCase().replace('https:', 'http').substring(0, 14)) === 0; + resolve(val); + } + } + + if (mediaSource.Protocol === 'File') { + + // Determine if the file can be accessed directly + require(['filesystem'], function (filesystem) { + + var method = mediaSource.VideoType === 'BluRay' || mediaSource.VideoType === 'Dvd' || mediaSource.VideoType === 'HdDvd' ? + 'directoryExists' : + 'fileExists'; + + filesystem[method](mediaSource.Path).then(function () { + resolve(true); + }, function () { + resolve(false); + }); + + }); + } + } + else { + resolve(false); + } + }); + } + + function validatePlaybackInfoResult(result) { + + if (result.ErrorCode) { + + showPlaybackInfoErrorMessage(result.ErrorCode); + return false; + } + + return true; + } + + function showPlaybackInfoErrorMessage(errorCode, playNextTrack) { + + require(['alert'], function (alert) { + alert({ + text: globalize.translate('core#MessagePlaybackError' + errorCode), + title: globalize.translate('core#HeaderPlaybackError') + }).then(function () { + + if (playNextTrack) { + self.nextTrack(); + } + }); + }); + } + + function normalizePlayOptions(playOptions) { + playOptions.fullscreen = playOptions.fullscreen !== false; + } + + function getPlayer(item, playOptions) { + + var players = self.getPlayers(); + + var serverItem = isServerItem(item); + + return self.getPlayers().filter(function (p) { + + if (p.canPlayMediaType(item.MediaType)) { + + if (serverItem) { + if (p.canPlayItem) { + return p.canPlayItem(item, playOptions); + } + return true; + } + + else if (p.canPlayUrl) { + return p.canPlayUrl(item.Url); + } + } + + return false; + + })[0]; + } + + function getItemsForPlayback(serverId, query) { + + var apiClient = connectionManager.getApiClient(serverId); + + if (query.Ids && query.Ids.split(',').length === 1) { + + var itemId = query.Ids.split(','); + + return apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(function (item) { + + return { + Items: [item], + TotalRecordCount: 1 + }; + }); + } + else { + + query.Limit = query.Limit || 100; + query.Fields = "MediaSources,Chapters"; + query.ExcludeLocationTypes = "Virtual"; + + return apiClient.getItems(apiClient.getCurrentUserId(), query); + } + } + + // Gets or sets the current playlist index + self.currentPlaylistIndex = function (i) { + + if (i == null) { + return currentPlaylistIndex; + } + + var newItem = playlist[i]; + + var playOptions = Object.assign({}, currentPlayOptions, { + startPositionTicks: 0 + }); + + playInternal(newItem, playOptions, function () { + self.setPlaylistState(i); + }); + }; + + self.setRepeatMode = function (value) { + repeatMode = value; + events.trigger(self, 'repeatmodechange'); + }; + + self.getRepeatMode = function () { + return repeatMode; + }; + + function getNextItemInfo() { + + var newIndex; + var playlistLength = playlist.length; + + switch (self.getRepeatMode()) { + + case 'RepeatOne': + newIndex = currentPlaylistIndex; + break; + case 'RepeatAll': + newIndex = currentPlaylistIndex + 1; + if (newIndex >= playlistLength) { + newIndex = 0; + } + break; + default: + newIndex = currentPlaylistIndex + 1; + break; + } + + if (newIndex < 0 || newIndex >= playlistLength) { + return null; + } + + var item = playlist[newIndex]; + + if (!item) { + return null; + } + + return { + item: item, + index: newIndex + }; + } + + self.nextTrack = function () { + + var newItemInfo = getNextItemInfo(); + + if (newItemInfo) { + + console.log('playing next track'); + + var playOptions = Object.assign({}, currentPlayOptions, { + startPositionTicks: 0 + }); + + playInternal(newItemInfo.item, playOptions, function () { + setPlaylistState(newItemInfo.index); + }); + } + }; + + self.previousTrack = function () { + var newIndex = currentPlaylistIndex - 1; + if (newIndex >= 0) { + var newItem = playlist[newIndex]; + + if (newItem) { + + var playOptions = Object.assign({}, currentPlayOptions, { + startPositionTicks: 0 + }); + + playInternal(newItem, playOptions, function () { + setPlaylistState(newIndex); + }); + } + } + }; + + self.queue = function (options) { + queue(options); + }; + + self.queueNext = function (options) { + queue(options, 'next'); + }; + + function queue(options, mode) { + + if (!currentPlayer) { + self.play(options); + return; + } + + if (typeof (options) === 'string') { + options = { ids: [options] }; + } + + // TODO + } + + function onPlaybackStarted(player, streamInfo, mediaSource) { + + setCurrentPlayer(player); + getPlayerData(player).streamInfo = streamInfo; + + if (mediaSource) { + getPlayerData(player).audioStreamIndex = mediaSource.DefaultAudioStreamIndex; + getPlayerData(player).subtitleStreamIndex = mediaSource.DefaultSubtitleStreamIndex; + } else { + getPlayerData(player).audioStreamIndex = null; + getPlayerData(player).subtitleStreamIndex = null; + } + + playNextAfterEnded = true; + + var state = self.getPlayerState(player); + + reportPlayback(state, getPlayerData(player).streamInfo.item.ServerId, 'reportPlaybackStart'); + + startProgressInterval(player); + + events.trigger(self, 'playbackstart', [player]); + } + + function onPlaybackError(e, error) { + + var player = this; + error = error || {}; + + var menuItems = []; + menuItems.push({ + name: globalize.translate('Resume'), + id: 'resume' + }); + menuItems.push({ + name: globalize.translate('Stop'), + id: 'stop' + }); + + var msg; + + if (error.type === 'network') { + msg = 'A network error has occurred. Please check your connection and try again.'; + } else { + msg = 'A network error has occurred. Please check your connection and try again.'; + } + + require(['actionsheet'], function (actionsheet) { + + actionsheet.show({ + + items: menuItems, + text: msg + + }).then(function (id) { + switch (id) { + + case 'stop': + self.stop(); + break; + case 'resume': + player.resume(); + break; + default: + break; + } + }); + }); + } + + function onPlaybackStopped(e) { + + var player = this; + + if (getPlayerData(player).isChangingStream) { + return; + } + + // User clicked stop or content ended + var state = self.getPlayerState(player); + var streamInfo = getPlayerData(player).streamInfo; + + if (isServerItem(streamInfo.item)) { + + if (player.supportsProgress === false && state.PlayState && !state.PlayState.PositionTicks) { + state.PlayState.PositionTicks = streamInfo.item.RunTimeTicks; + } + + reportPlayback(state, streamInfo.item.ServerId, 'reportPlaybackStopped'); + } + + clearProgressInterval(player); + + var nextItem = playNextAfterEnded ? getNextItemInfo() : null; + + var nextMediaType = (nextItem ? nextItem.item.MediaType : null); + + var playbackStopInfo = { + player: player, + state: state, + nextItem: (nextItem ? nextItem.item : null), + nextMediaType: nextMediaType + }; + + events.trigger(self, 'playbackstop', [playbackStopInfo]); + + var newPlayer = nextItem ? getPlayer(nextItem.item, currentPlayOptions) : null; + + if (newPlayer !== player) { + player.destroy(); + setCurrentPlayer(null); + } + + if (nextItem) { + self.nextTrack(); + } + } + + function onPlaybackChanging(activePlayer, newPlayer, newItem) { + + var state = self.getPlayerState(activePlayer); + var serverId = getPlayerData(activePlayer).streamInfo.item.ServerId; + + // User started playing something new while existing content is playing + var promise; + + if (activePlayer === newPlayer) { + + // If we're staying with the same player, stop it + promise = activePlayer.stop(false, false); + + } else { + + // If we're switching players, tear down the current one + promise = activePlayer.stop(true, false); + } + + return promise.then(function () { + reportPlayback(state, serverId, 'reportPlaybackStopped'); + + clearProgressInterval(activePlayer); + + events.trigger(self, 'playbackstop', [{ + player: activePlayer, + state: state, + nextItem: newItem, + nextMediaType: newItem.MediaType + }]); + }); + } + + function initMediaPlayer(plugin) { + plugin.currentState = {}; + + events.on(plugin, 'error', onPlaybackError); + events.on(plugin, 'stopped', onPlaybackStopped); + } + + events.on(pluginManager, 'registered', function (e, plugin) { + + if (plugin.type === 'mediaplayer') { + + initMediaPlayer(plugin); + } + }); + + pluginManager.ofType('mediaplayer').map(initMediaPlayer); + + function startProgressInterval(player) { + + clearProgressInterval(player); + + var intervalTime = 800; + player.lastProgressReport = 0; + + getPlayerData(player).currentProgressInterval = setInterval(function () { + + if ((new Date().getTime() - player.lastProgressReport) > intervalTime) { + + sendProgressUpdate(player); + } + + }, 500); + } + + function sendProgressUpdate(player) { + + player.lastProgressReport = new Date().getTime(); + + var state = self.getPlayerState(player); + var currentItem = getPlayerData(player).streamInfo.item; + reportPlayback(state, currentItem.ServerId, 'reportPlaybackProgress'); + } + + function reportPlayback(state, serverId, method) { + + if (!serverId) { + // Not a server item + // We can expand on this later and possibly report them + return; + } + + var info = { + QueueableMediaTypes: state.NowPlayingItem.MediaType, + ItemId: state.NowPlayingItem.Id + }; + + for (var i in state.PlayState) { + info[i] = state.PlayState[i]; + } + //console.log(method + '-' + JSON.stringify(info)); + var apiClient = connectionManager.getApiClient(serverId); + apiClient[method](info); + } + + function clearProgressInterval(player) { + + if (getPlayerData(player).currentProgressInterval) { + clearTimeout(getPlayerData(player).currentProgressInterval); + getPlayerData(player).currentProgressInterval = null; + } + } + + window.addEventListener("beforeunload", function (e) { + + var player = currentPlayer; + + // Try to report playback stopped before the browser closes + if (player && getPlayerData(player).currentProgressInterval) { + playNextAfterEnded = false; + onPlaybackStopped.call(player); + } + }); + + events.on(serverNotifications, 'ServerShuttingDown', function (e, apiClient, data) { + self.setDefaultPlayerActive(); + }); + + events.on(serverNotifications, 'ServerRestarting', function (e, apiClient, data) { + self.setDefaultPlayerActive(); + }); + } + + return new playbackManager(); +}); From 42d4a8cfaa4741c9848575609c0687c77041cbec Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 30 Dec 2016 03:34:08 -0500 Subject: [PATCH 006/102] update components --- .../emby-webcomponents/.bower.json | 8 +- .../emby-webcomponents/inputmanager.js | 4 +- .../emby-webcomponents/listview/listview.js | 6 +- .../emby-webcomponents/playbackmanager.js | 224 +++++++++++++++++- 4 files changed, 227 insertions(+), 15 deletions(-) diff --git a/dashboard-ui/bower_components/emby-webcomponents/.bower.json b/dashboard-ui/bower_components/emby-webcomponents/.bower.json index 26c6aebb29..b52f127f11 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/.bower.json +++ b/dashboard-ui/bower_components/emby-webcomponents/.bower.json @@ -14,12 +14,12 @@ }, "devDependencies": {}, "ignore": [], - "version": "1.4.408", - "_release": "1.4.408", + "version": "1.4.409", + "_release": "1.4.409", "_resolution": { "type": "version", - "tag": "1.4.408", - "commit": "d522bced383a879f2d9f9fdf6344ed8e739d93e0" + "tag": "1.4.409", + "commit": "6a6a35e136f70989be0a63846b02bff62bab6367" }, "_source": "https://github.com/MediaBrowser/emby-webcomponents.git", "_target": "^1.2.1", diff --git a/dashboard-ui/bower_components/emby-webcomponents/inputmanager.js b/dashboard-ui/bower_components/emby-webcomponents/inputmanager.js index 35da6c6717..51504cc32a 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/inputmanager.js +++ b/dashboard-ui/bower_components/emby-webcomponents/inputmanager.js @@ -146,10 +146,10 @@ define(['playbackManager', 'focusManager', 'embyRouter', 'dom'], function (playb embyRouter.showLiveTV(); break; case 'mute': - playbackManager.mute(); + playbackManager.setMute(true); break; case 'unmute': - playbackManager.unMute(); + playbackManager.setMute(false); break; case 'togglemute': playbackManager.toggleMute(); diff --git a/dashboard-ui/bower_components/emby-webcomponents/listview/listview.js b/dashboard-ui/bower_components/emby-webcomponents/listview/listview.js index fd6ae9dbd7..af8ed6e2dd 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/listview/listview.js +++ b/dashboard-ui/bower_components/emby-webcomponents/listview/listview.js @@ -234,8 +234,12 @@ define(['itemHelper', 'mediaInfo', 'indicators', 'connectionManager', 'layoutMan var imageClass = isLargeStyle ? 'listItemImage listItemImage-large' : 'listItemImage'; + if (!clickEntireItem) { + imageClass += ' itemAction'; + } + if (imgUrl) { - html += '
'; + html += '
'; } else { html += '
'; } diff --git a/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js b/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js index fa24115ffc..bf885fcc3c 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js +++ b/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js @@ -5,8 +5,12 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g var self = this; + var players = []; var currentPlayer; + var currentTargetInfo; var lastLocalPlayer; + var currentPairingId = null; + var repeatMode = 'RepeatNone'; var playlist = []; var currentPlaylistIndex; @@ -24,6 +28,200 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return data.streamInfo ? data.streamInfo.mediaSource : null; }; + function triggerPlayerChange(newPlayer, newTarget, previousPlayer) { + + events.trigger(self, 'playerchange', [newPlayer, newTarget, previousPlayer]); + } + + self.setActivePlayer = function (player, targetInfo) { + + if (typeof (player) === 'string') { + player = players.filter(function (p) { + return p.name == player; + })[0]; + } + + if (!player) { + throw new Error('null player'); + } + + var previousPlayer = currentPlayer; + + currentPairingId = null; + currentPlayer = player; + currentTargetInfo = targetInfo; + + console.log('Active player: ' + JSON.stringify(currentTargetInfo)); + + triggerPlayerChange(player, targetInfo, previousPlayer); + }; + + self.trySetActivePlayer = function (player, targetInfo) { + + 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; + + player.tryPair(targetInfo).then(function () { + + var previousPlayer = currentPlayer; + + currentPlayer = player; + currentTargetInfo = targetInfo; + + console.log('Active player: ' + JSON.stringify(currentTargetInfo)); + + triggerPlayerChange(player, targetInfo, previousPlayer); + }, function () { + + if (currentPairingId == targetInfo.id) { + currentPairingId = null; + } + }); + }; + + self.trySetActiveDeviceName = function (name) { + + function normalizeName(t) { + return t.toLowerCase().replace(' ', ''); + } + + name = normalizeName(name); + + self.getTargets().then(function (result) { + + var target = result.filter(function (p) { + return normalizeName(p.name) == name; + })[0]; + + if (target) { + self.trySetActivePlayer(target.playerName, target); + } + + }); + }; + + self.setDefaultPlayerActive = function () { + + var player = self.getDefaultPlayer(); + + player.getTargets().then(function (targets) { + + self.setActivePlayer(player, targets[0]); + }); + }; + + self.removeActivePlayer = function (name) { + + if (self.getPlayerInfo().name == name) { + self.setDefaultPlayerActive(); + } + + }; + + self.removeActiveTarget = function (id) { + + if (self.getPlayerInfo().id == id) { + self.setDefaultPlayerActive(); + } + }; + + self.disconnectFromPlayer = function () { + + var playerInfo = self.getPlayerInfo(); + + if (playerInfo.supportedCommands.indexOf('EndSession') != -1) { + + require(['dialog'], function (dialog) { + + var menuItems = []; + + menuItems.push({ + name: Globalize.translate('ButtonYes'), + id: 'yes' + }); + menuItems.push({ + name: Globalize.translate('ButtonNo'), + id: 'no' + }); + + dialog({ + buttons: menuItems, + //positionTo: positionTo, + text: Globalize.translate('ConfirmEndPlayerSession') + + }).then(function (id) { + switch (id) { + + case 'yes': + MediaController.getCurrentPlayer().endSession(); + self.setDefaultPlayerActive(); + break; + case 'no': + self.setDefaultPlayerActive(); + break; + default: + break; + } + }); + + }); + + + } else { + + self.setDefaultPlayerActive(); + } + }; + + self.getTargets = function () { + + var promises = players.map(function (p) { + return p.getTargets(); + }); + + return Promise.all(promises).then(function (responses) { + + var targets = []; + + for (var i = 0; i < responses.length; i++) { + + var subTargets = responses[i]; + + for (var j = 0; j < subTargets.length; j++) { + + targets.push(subTargets[j]); + } + + } + + targets = targets.sort(function (a, b) { + + var aVal = a.isLocalPlayer ? 0 : 1; + var bVal = b.isLocalPlayer ? 0 : 1; + + aVal = aVal.toString() + a.name; + bVal = bVal.toString() + b.name; + + return aVal.localeCompare(bVal); + }); + + return targets; + }); + }; + function getCurrentSubtitleStream(player) { var index = getPlayerData(player).subtitleStreamIndex; @@ -101,13 +299,6 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.getPlayers = function () { - var players = pluginManager.ofType('mediaplayer'); - - players.sort(function (a, b) { - - return (a.priority || 0) - (b.priority || 0); - }); - return players; }; @@ -175,8 +366,14 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.toggleMute = function (mute) { + var player = currentPlayer; if (currentPlayer) { - self.setMute(!self.isMuted()); + + if (player.toggleMute) { + player.toggleMute(); + } else { + player.setMute(!player.isMuted()); + } } }; @@ -1943,6 +2140,17 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g } function initMediaPlayer(plugin) { + + players.push(player); + players.sort(function (a, b) { + + return (a.priority || 0) - (b.priority || 0); + }); + + if (player.isLocalPlayer !== false) { + player.isLocalPlayer = true; + } + plugin.currentState = {}; events.on(plugin, 'error', onPlaybackError); From 1d176e480f79ba31bf1c454db2a12cdbeaffa903 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 30 Dec 2016 13:37:18 -0500 Subject: [PATCH 007/102] update components --- .../emby-webcomponents/.bower.json | 8 +- .../htmlaudioplayer/blank.mp3 | Bin 0 -> 16853 bytes .../htmlaudioplayer/plugin.js | 367 ++++++++++++++++++ .../emby-webcomponents/playbackmanager.js | 54 ++- 4 files changed, 409 insertions(+), 20 deletions(-) create mode 100644 dashboard-ui/bower_components/emby-webcomponents/htmlaudioplayer/blank.mp3 create mode 100644 dashboard-ui/bower_components/emby-webcomponents/htmlaudioplayer/plugin.js diff --git a/dashboard-ui/bower_components/emby-webcomponents/.bower.json b/dashboard-ui/bower_components/emby-webcomponents/.bower.json index b52f127f11..97107bed5f 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/.bower.json +++ b/dashboard-ui/bower_components/emby-webcomponents/.bower.json @@ -14,12 +14,12 @@ }, "devDependencies": {}, "ignore": [], - "version": "1.4.409", - "_release": "1.4.409", + "version": "1.4.411", + "_release": "1.4.411", "_resolution": { "type": "version", - "tag": "1.4.409", - "commit": "6a6a35e136f70989be0a63846b02bff62bab6367" + "tag": "1.4.411", + "commit": "50fdd44728ec2a1faa1aaf44ccb16df821b8295f" }, "_source": "https://github.com/MediaBrowser/emby-webcomponents.git", "_target": "^1.2.1", diff --git a/dashboard-ui/bower_components/emby-webcomponents/htmlaudioplayer/blank.mp3 b/dashboard-ui/bower_components/emby-webcomponents/htmlaudioplayer/blank.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..dbf05cbdd513ae4e7376516744b8ec53e974eb52 GIT binary patch literal 16853 zcmeI&u}Z^G7zN-f9UL889Pparpl=2RwyK(w?>)&bOv_=`~FGdWm|`sl6WB(|;Hz*~0Nd&k3@`=_jKo z?4H=*z@FuD*?JzPPszA7nxvh;_w9$r_QY{)u4a?exnC@%=dSzri|%~;r_FTg&E2RI z+}L02+Y`=)+2g#(-`00u_O03bJSs1yy?Qm~`oiyetut@HS7m`ZRw?rL_2%Qdv9>^4 zAJeuzrqYVHy9MeJ%72O#g?0||9fjm=FcJ!3ILIw1ByWR}Pzb|8Zb2b=8;pcP7!Gm^ z3d!4GBoxAMkXukl-UcI~5Qc-?fxv~>_N3Lz+H9Vn!&gOE`OK|$+4A#ELmj6w(sS_cYg>mXzl ZLQv2;P)J(`A)^q2g4TgT+B*L=`vnh6-C+O# literal 0 HcmV?d00001 diff --git a/dashboard-ui/bower_components/emby-webcomponents/htmlaudioplayer/plugin.js b/dashboard-ui/bower_components/emby-webcomponents/htmlaudioplayer/plugin.js new file mode 100644 index 0000000000..3f6c741b26 --- /dev/null +++ b/dashboard-ui/bower_components/emby-webcomponents/htmlaudioplayer/plugin.js @@ -0,0 +1,367 @@ +define(['events', 'browser', 'pluginManager', 'apphost'], function (events, browser, pluginManager, appHost) { + "use strict"; + + return function () { + + var self = this; + + self.name = 'Html Audio Player'; + self.type = 'mediaplayer'; + self.id = 'htmlaudioplayer'; + + // Let any players created by plugins take priority + self.priority = 1; + + var mediaElement; + var currentSrc; + + self.canPlayMediaType = function (mediaType) { + + return (mediaType || '').toLowerCase() === 'audio'; + }; + + self.getDeviceProfile = function () { + + return new Promise(function (resolve, reject) { + + require(['browserdeviceprofile'], function (profileBuilder) { + + var profile = profileBuilder({ + supportsCustomSeeking: true + }); + resolve(profile); + }); + }); + }; + + self.currentSrc = function () { + return currentSrc; + }; + + self.play = function (options) { + + var elem = createMediaElement(); + + var val = options.url; + + elem.crossOrigin = getCrossOriginValue(options.mediaSource); + elem.title = options.title; + + // Opera TV guidelines suggest using source elements, so let's do that if we have a valid mimeType + if (options.mimeType && browser.operaTv) { + + // Need to do this or we won't be able to restart a new stream + if (elem.currentSrc) { + elem.src = ''; + elem.removeAttribute('src'); + } + + elem.innerHTML = ''; + } else { + elem.src = val; + } + + currentSrc = val; + + // Chrome now returns a promise + var promise = elem.play(); + + if (promise && promise.then) { + return promise; + } + return Promise.resolve(); + }; + + function getCrossOriginValue(mediaSource) { + + return 'anonymous'; + } + + // Save this for when playback stops, because querying the time at that point might return 0 + self.currentTime = function (val) { + + if (mediaElement) { + if (val != null) { + mediaElement.currentTime = val / 1000; + return; + } + + return (mediaElement.currentTime || 0) * 1000; + } + }; + + self.duration = function (val) { + + if (mediaElement) { + return mediaElement.duration * 1000; + } + + return null; + }; + + function supportsFade() { + + if (browser.tv) { + // Not working on tizen. + // We could possibly enable on other tv's, but all smart tv browsers tend to be pretty primitive + return false; + } + + return true; + } + + self.stop = function (destroyPlayer, reportEnded) { + + cancelFadeTimeout(); + + var elem = mediaElement; + var src = currentSrc; + + if (elem && src) { + + if (!destroyPlayer || !supportsFade()) { + + if (!elem.paused) { + elem.pause(); + } + elem.src = ''; + elem.innerHTML = ''; + elem.removeAttribute("src"); + onEndedInternal(reportEnded); + return Promise.resolve(); + } + + var originalVolume = elem.volume; + + return fade(elem, function () { + + }).then(function () { + if (!elem.paused) { + elem.pause(); + } + elem.src = ''; + elem.innerHTML = ''; + elem.removeAttribute("src"); + + elem.volume = originalVolume; + onEndedInternal(reportEnded); + }); + } + return Promise.resolve(); + }; + + self.destroy = function () { + + }; + + var fadeTimeout; + + function fade(elem) { + + var newVolume = Math.max(0, elem.volume - 0.15); + console.log('fading volume to ' + newVolume); + elem.volume = newVolume; + + if (!elem.volume) { + return Promise.resolve(); + } + + return new Promise(function (resolve, reject) { + + cancelFadeTimeout(); + + fadeTimeout = setTimeout(function () { + fade(elem).then(resolve, reject); + }, 100); + }); + } + + function cancelFadeTimeout() { + var timeout = fadeTimeout; + if (timeout) { + clearTimeout(timeout); + fadeTimeout = null; + } + } + + self.pause = function () { + if (mediaElement) { + mediaElement.pause(); + } + }; + + // This is a retry after error + self.resume = function () { + if (mediaElement) { + mediaElement.play(); + } + }; + + self.unpause = function () { + if (mediaElement) { + mediaElement.play(); + } + }; + + self.paused = function () { + + if (mediaElement) { + return mediaElement.paused; + } + + return false; + }; + + self.volume = function (val) { + if (mediaElement) { + if (val != null) { + mediaElement.volume = val / 100; + return; + } + + return mediaElement.volume * 100; + } + }; + + self.volumeUp = function () { + self.volume(Math.min(self.volume() + 2, 100)); + }; + + self.volumeDown = function () { + self.volume(Math.max(self.volume() - 2, 0)); + }; + + self.setMute = function (mute) { + + if (mute) { + self.volume(0); + } else { + + if (self.isMuted()) { + self.volume(50); + } + } + }; + + self.isMuted = function () { + return self.volume() === 0; + }; + + function onEnded() { + + onEndedInternal(true); + } + + function onEndedInternal(triggerEnded) { + + if (triggerEnded) { + var stopInfo = { + src: currentSrc + }; + + events.trigger(self, 'stopped', [stopInfo]); + } + + currentSrc = null; + } + + function onTimeUpdate() { + + events.trigger(self, 'timeupdate'); + } + + function onVolumeChange() { + + if (!fadeTimeout) { + events.trigger(self, 'volumechange'); + } + } + + function onPlaying() { + + events.trigger(self, 'playing'); + } + + function onPause() { + events.trigger(self, 'pause'); + } + + function onError() { + + var errorCode = this.error ? this.error.code : ''; + errorCode = (errorCode || '').toString(); + console.log('Media element error code: ' + errorCode); + + var type; + + switch (errorCode) { + case 1: + // MEDIA_ERR_ABORTED + // This will trigger when changing media while something is playing + return; + case 2: + // MEDIA_ERR_NETWORK + type = 'network'; + break; + case 3: + // MEDIA_ERR_DECODE + break; + case 4: + // MEDIA_ERR_SRC_NOT_SUPPORTED + break; + } + + //events.trigger(self, 'error', [ + //{ + // type: type + //}]); + } + + function createMediaElement() { + + var elem = document.querySelector('.mediaPlayerAudio'); + + if (!elem) { + elem = document.createElement('audio'); + elem.classList.add('mediaPlayerAudio'); + elem.classList.add('hide'); + + document.body.appendChild(elem); + + elem.addEventListener('timeupdate', onTimeUpdate); + elem.addEventListener('ended', onEnded); + elem.addEventListener('volumechange', onVolumeChange); + elem.addEventListener('pause', onPause); + elem.addEventListener('playing', onPlaying); + elem.addEventListener('error', onError); + } + + mediaElement = elem; + + return elem; + } + + function onDocumentClick() { + document.removeEventListener('click', onDocumentClick); + + var elem = document.createElement('audio'); + elem.classList.add('mediaPlayerAudio'); + elem.classList.add('hide'); + + document.body.appendChild(elem); + + elem.src = pluginManager.mapPath(self, 'blank.mp3'); + elem.play(); + + setTimeout(function () { + elem.src = ''; + elem.removeAttribute("src"); + }, 1000); + } + + // Mobile browsers don't allow autoplay, so this is a nice workaround + if (!appHost.supports('htmlaudioautoplay')) { + document.addEventListener('click', onDocumentClick); + } + }; +}); \ No newline at end of file diff --git a/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js b/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js index bf885fcc3c..9026edc766 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js +++ b/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js @@ -33,11 +33,32 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g events.trigger(self, 'playerchange', [newPlayer, newTarget, previousPlayer]); } + self.getPlayerInfo = function () { + + var player = currentPlayer; + + if (!player) { + return null; + } + + var 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 (typeof (player) === 'string') { player = players.filter(function (p) { - return p.name == player; + return p.name === player; })[0]; } @@ -60,7 +81,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g if (typeof (player) === 'string') { player = players.filter(function (p) { - return p.name == player; + return p.name === player; })[0]; } @@ -68,7 +89,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g throw new Error('null player'); } - if (currentPairingId == targetInfo.id) { + if (currentPairingId === targetInfo.id) { return; } @@ -86,7 +107,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g triggerPlayerChange(player, targetInfo, previousPlayer); }, function () { - if (currentPairingId == targetInfo.id) { + if (currentPairingId === targetInfo.id) { currentPairingId = null; } }); @@ -103,7 +124,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.getTargets().then(function (result) { var target = result.filter(function (p) { - return normalizeName(p.name) == name; + return normalizeName(p.name) === name; })[0]; if (target) { @@ -125,7 +146,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.removeActivePlayer = function (name) { - if (self.getPlayerInfo().name == name) { + if (self.getPlayerInfo().name === name) { self.setDefaultPlayerActive(); } @@ -133,7 +154,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.removeActiveTarget = function (id) { - if (self.getPlayerInfo().id == id) { + if (self.getPlayerInfo().id === id) { self.setDefaultPlayerActive(); } }; @@ -142,7 +163,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g var playerInfo = self.getPlayerInfo(); - if (playerInfo.supportedCommands.indexOf('EndSession') != -1) { + if (playerInfo.supportedCommands.indexOf('EndSession') !== -1) { require(['dialog'], function (dialog) { @@ -166,7 +187,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g switch (id) { case 'yes': - MediaController.getCurrentPlayer().endSession(); + self.getCurrentPlayer().endSession(); self.setDefaultPlayerActive(); break; case 'no': @@ -261,7 +282,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return playlist.slice(0); }; - self.currentPlayer = function () { + self.getCurrentPlayer = function () { return currentPlayer; }; @@ -481,7 +502,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return; } - return (appSettings.get('displaymirror--' + Dashboard.getCurrentUserId()) || '') != '0'; + return (appSettings.get('displaymirror--' + Dashboard.getCurrentUserId()) || '') !== '0'; }; self.stop = function () { @@ -523,14 +544,14 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.seek = function (ticks) { - var player = self.currentPlayer(); + var player = currentPlayer; changeStream(player, ticks); }; self.nextChapter = function () { - var player = self.currentPlayer(); + var player = currentPlayer; var item = self.currentItem(player); var ticks = getCurrentTicks(player); @@ -549,7 +570,8 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g }; self.previousChapter = function () { - var player = self.currentPlayer(); + + var player = currentPlayer; var item = self.currentItem(player); var ticks = getCurrentTicks(player); @@ -571,7 +593,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.fastForward = function () { - var player = self.currentPlayer(); + var player = currentPlayer; if (player.fastForward != null) { player.fastForward(userSettings.skipForwardLength()); @@ -597,7 +619,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.rewind = function () { - var player = self.currentPlayer(); + var player = currentPlayer; if (player.rewind != null) { player.rewind(userSettings.skipBackLength()); From 0b4a2c009266266e2cba57106fb39b37f78ddb34 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Fri, 30 Dec 2016 15:15:30 -0500 Subject: [PATCH 008/102] update components --- .../emby-webcomponents/.bower.json | 8 +- .../emby-webcomponents/packagemanager.js | 148 ++++++++++++++++++ .../emby-webcomponents/playbackmanager.js | 57 +++++-- .../emby-webcomponents/pluginmanager.js | 10 +- 4 files changed, 205 insertions(+), 18 deletions(-) create mode 100644 dashboard-ui/bower_components/emby-webcomponents/packagemanager.js diff --git a/dashboard-ui/bower_components/emby-webcomponents/.bower.json b/dashboard-ui/bower_components/emby-webcomponents/.bower.json index 97107bed5f..0f2433c2be 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/.bower.json +++ b/dashboard-ui/bower_components/emby-webcomponents/.bower.json @@ -14,12 +14,12 @@ }, "devDependencies": {}, "ignore": [], - "version": "1.4.411", - "_release": "1.4.411", + "version": "1.4.412", + "_release": "1.4.412", "_resolution": { "type": "version", - "tag": "1.4.411", - "commit": "50fdd44728ec2a1faa1aaf44ccb16df821b8295f" + "tag": "1.4.412", + "commit": "a3f1a92bdff2edcffb16833836c60613fba0e889" }, "_source": "https://github.com/MediaBrowser/emby-webcomponents.git", "_target": "^1.2.1", diff --git a/dashboard-ui/bower_components/emby-webcomponents/packagemanager.js b/dashboard-ui/bower_components/emby-webcomponents/packagemanager.js new file mode 100644 index 0000000000..fc59392f9d --- /dev/null +++ b/dashboard-ui/bower_components/emby-webcomponents/packagemanager.js @@ -0,0 +1,148 @@ +define(['appSettings', 'pluginManager'], function (appSettings, pluginManager) { + 'use strict'; + + function packageManager() { + + var self = this; + var settingsKey = 'installedpackages1'; + + var packages = []; + + self.packages = function () { + return packages.slice(0); + }; + + function addPackage(pkg) { + + packages = packages.filter(function (p) { + + return p.name !== pkg.name; + }); + + packages.push(pkg); + } + + self.install = function (url) { + + return loadPackage(url, true).then(function (pkg) { + + var manifestUrls = JSON.parse(appSettings.get(settingsKey) || '[]'); + + if (manifestUrls.indexOf(url) === -1) { + manifestUrls.push(url); + appSettings.set(settingsKey, JSON.stringify(manifestUrls)); + } + + return pkg; + }); + }; + + self.uninstall = function (name) { + + var pkg = packages.filter(function (p) { + + return p.name === name; + })[0]; + + if (pkg) { + + packages = packages.filter(function (p) { + + return p.name !== name; + }); + + removeUrl(pkg.url); + } + + return Promise.resolve(); + }; + + function removeUrl(url) { + + var manifestUrls = JSON.parse(appSettings.get(settingsKey) || '[]'); + + manifestUrls = manifestUrls.filter(function (i) { + return i !== url; + }); + + appSettings.set(settingsKey, JSON.stringify(manifestUrls)); + } + + self.init = function () { + var manifestUrls = JSON.parse(appSettings.get(settingsKey) || '[]'); + + return Promise.all(manifestUrls.map(loadPackage)).then(function () { + return Promise.resolve(); + }, function () { + return Promise.resolve(); + }); + }; + + function loadPackage(url, throwError) { + + return new Promise(function (resolve, reject) { + + var xhr = new XMLHttpRequest(); + var originalUrl = url; + url += url.indexOf('?') === -1 ? '?' : '&'; + url += 't=' + new Date().getTime(); + + xhr.open('GET', url, true); + + var onError = function () { + + if (throwError === true) { + reject(); + } else { + removeUrl(originalUrl); + resolve(); + } + }; + + xhr.onload = function (e) { + if (this.status < 400) { + + var pkg = JSON.parse(this.response); + pkg.url = originalUrl; + + addPackage(pkg); + + var plugins = pkg.plugins || []; + if (pkg.plugin) { + plugins.push(pkg.plugin); + } + var promises = plugins.map(function (pluginUrl) { + return pluginManager.loadPlugin(self.mapPath(pkg, pluginUrl)); + }); + Promise.all(promises).then(resolve, resolve); + + } else { + onError(); + } + }; + + xhr.onerror = onError; + + xhr.send(); + }); + } + + self.mapPath = function (pkg, pluginUrl) { + + var urlLower = pluginUrl.toLowerCase(); + if (urlLower.indexOf('http:') === 0 || urlLower.indexOf('https:') === 0 || urlLower.indexOf('file:') === 0) { + return pluginUrl; + } + + var packageUrl = pkg.url; + packageUrl = packageUrl.substring(0, packageUrl.lastIndexOf('/')); + + packageUrl += '/'; + packageUrl += pluginUrl; + + return packageUrl; + }; + } + + return new packageManager(); +}); \ No newline at end of file diff --git a/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js b/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js index 9026edc766..8e2a50caea 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js +++ b/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js @@ -1,6 +1,16 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'globalize', 'connectionManager', 'loading', 'serverNotifications'], function (events, datetime, appSettings, pluginManager, userSettings, globalize, connectionManager, loading, serverNotifications) { 'use strict'; + function enableLocalPlaylistManagement(player) { + + if (player.isLocalPlayer) { + + return true; + } + + return false; + } + function playbackManager() { var self = this; @@ -95,7 +105,11 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g currentPairingId = targetInfo.id; - player.tryPair(targetInfo).then(function () { + var promise = player.tryPair ? + player.tryPair(targetInfo) : + Promise.resolve(); + + promise.then(function () { var previousPlayer = currentPlayer; @@ -134,11 +148,36 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g }); }; + function getSupportedCommands(player) { + + if (player.isLocalPlayer) { + return Dashboard.getSupportedRemoteCommands(); + } + + throw new Error('player must define supported commands'); + } + + function getPlayerTargets(player) { + if (player.getTargets) { + return player.getTargets(); + } + + return Promise.resolve([{ + + name: player.name, + id: player.id, + playerName: player.name, + playableMediaTypes: ['Audio', 'Video', 'Game'].map(player.canPlayMediaType), + isLocalPlayer: player.isLocalPlayer, + supportedCommands: getSupportedCommands(player) + }]); + } + self.setDefaultPlayerActive = function () { var player = self.getDefaultPlayer(); - player.getTargets().then(function (targets) { + getPlayerTargets(player).then(function (targets) { self.setActivePlayer(player, targets[0]); }); @@ -209,9 +248,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.getTargets = function () { - var promises = players.map(function (p) { - return p.getTargets(); - }); + var promises = players.map(getPlayerTargets); return Promise.all(promises).then(function (responses) { @@ -2161,7 +2198,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g }); } - function initMediaPlayer(plugin) { + function initMediaPlayer(player) { players.push(player); players.sort(function (a, b) { @@ -2173,10 +2210,12 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g player.isLocalPlayer = true; } - plugin.currentState = {}; + player.currentState = {}; - events.on(plugin, 'error', onPlaybackError); - events.on(plugin, 'stopped', onPlaybackStopped); + if (enableLocalPlaylistManagement(player)) { + events.on(player, 'error', onPlaybackError); + events.on(player, 'stopped', onPlaybackStopped); + } } events.on(pluginManager, 'registered', function (e, plugin) { diff --git a/dashboard-ui/bower_components/emby-webcomponents/pluginmanager.js b/dashboard-ui/bower_components/emby-webcomponents/pluginmanager.js index 57344ae0ea..bb07c27c8f 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/pluginmanager.js +++ b/dashboard-ui/bower_components/emby-webcomponents/pluginmanager.js @@ -1,4 +1,4 @@ -define(['events'], function (Events) { +define(['events'], function (events) { 'use strict'; function pluginManager() { @@ -13,7 +13,7 @@ define(['events'], function (Events) { self.register = function (obj) { plugins.push(obj); - Events.trigger(self, 'registered', [obj]); + events.trigger(self, 'registered', [obj]); }; self.ofType = function (type) { @@ -87,7 +87,7 @@ define(['events'], function (Events) { return new Promise(function (resolve, reject) { - require([url, 'globalize'], function (pluginFactory, globalize) { + require([url, 'globalize', 'embyRouter'], function (pluginFactory, globalize, embyRouter) { var plugin = new pluginFactory(); @@ -105,9 +105,9 @@ define(['events'], function (Events) { var urlLower = url.toLowerCase(); if (urlLower.indexOf('http:') === -1 && urlLower.indexOf('https:') === -1 && urlLower.indexOf('file:') === -1) { - if (url.indexOf(Emby.Page.baseUrl()) !== 0) { + if (url.indexOf(embyRouter.baseUrl()) !== 0) { - url = Emby.Page.baseUrl() + '/' + url; + url = embyRouter.baseUrl() + '/' + url; } } From 4b90291afc48211e098a1861ccd914187cbabbbf Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sun, 1 Jan 2017 17:34:44 -0500 Subject: [PATCH 009/102] update unified playback manager --- .../emby-webcomponents/.bower.json | 8 +- .../browserdeviceprofile.js | 2 +- .../emby-webcomponents/chromecastplayer.js | 937 ++++++++++++++++++ .../htmlaudioplayer/plugin.js | 52 +- .../emby-webcomponents/input/api.js | 2 +- .../emby-webcomponents/mediainfo/mediainfo.js | 26 +- .../playback/nowplayinghelper.js | 87 ++ .../{ => playback}/playbackmanager.js | 645 ++++++++---- .../emby-webcomponents/playmenu.js | 4 +- .../recordingcreator/recordingcreator.js | 5 +- .../emby-webcomponents/sessionplayer.js | 368 +++++++ .../emby-webcomponents/shortcuts.js | 5 +- 12 files changed, 1880 insertions(+), 261 deletions(-) create mode 100644 dashboard-ui/bower_components/emby-webcomponents/chromecastplayer.js create mode 100644 dashboard-ui/bower_components/emby-webcomponents/playback/nowplayinghelper.js rename dashboard-ui/bower_components/emby-webcomponents/{ => playback}/playbackmanager.js (81%) create mode 100644 dashboard-ui/bower_components/emby-webcomponents/sessionplayer.js diff --git a/dashboard-ui/bower_components/emby-webcomponents/.bower.json b/dashboard-ui/bower_components/emby-webcomponents/.bower.json index 0f2433c2be..8f3312d93d 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/.bower.json +++ b/dashboard-ui/bower_components/emby-webcomponents/.bower.json @@ -14,12 +14,12 @@ }, "devDependencies": {}, "ignore": [], - "version": "1.4.412", - "_release": "1.4.412", + "version": "1.4.415", + "_release": "1.4.415", "_resolution": { "type": "version", - "tag": "1.4.412", - "commit": "a3f1a92bdff2edcffb16833836c60613fba0e889" + "tag": "1.4.415", + "commit": "ef218c1a08315f961adbbc08515089198e885972" }, "_source": "https://github.com/MediaBrowser/emby-webcomponents.git", "_target": "^1.2.1", diff --git a/dashboard-ui/bower_components/emby-webcomponents/browserdeviceprofile.js b/dashboard-ui/bower_components/emby-webcomponents/browserdeviceprofile.js index c73c9071d4..146be0ce49 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/browserdeviceprofile.js +++ b/dashboard-ui/bower_components/emby-webcomponents/browserdeviceprofile.js @@ -396,7 +396,7 @@ define(['browser'], function (browser) { } // Can't use mkv on mobile because we have to use the native player controls and they won't be able to seek it - if (canPlayMkv && options.supportsCustomSeeking && !browser.tizen && options.enableMkvProgressive !== false) { + if (canPlayMkv && !browser.tizen && options.enableMkvProgressive !== false) { profile.TranscodingProfiles.push({ Container: 'mkv', Type: 'Video', diff --git a/dashboard-ui/bower_components/emby-webcomponents/chromecastplayer.js b/dashboard-ui/bower_components/emby-webcomponents/chromecastplayer.js new file mode 100644 index 0000000000..c6d2bc41ed --- /dev/null +++ b/dashboard-ui/bower_components/emby-webcomponents/chromecastplayer.js @@ -0,0 +1,937 @@ +define(['appSettings', 'playbackManager', 'connectionManager', 'globalize', 'events'], function (appSettings, playbackManager, connectionManager, globalize, events) { + 'use strict'; + + // Based on https://github.com/googlecast/CastVideos-chrome/blob/master/CastVideos.js + var currentResolve; + var currentReject; + + var PlayerName = 'Chromecast'; + + function sendConnectionResult(isOk) { + + var resolve = currentResolve; + var reject = currentReject; + + currentResolve = null; + currentReject = null; + + if (isOk) { + if (resolve) { + resolve(); + } + } else { + if (reject) { + reject(); + } else { + playbackManager.removeActivePlayer(PlayerName); + } + } + } + + /** + * Constants of states for Chromecast device + **/ + var DEVICE_STATE = { + 'IDLE': 0, + 'ACTIVE': 1, + 'WARNING': 2, + 'ERROR': 3 + }; + + /** + * Constants of states for CastPlayer + **/ + var PLAYER_STATE = { + 'IDLE': 'IDLE', + 'LOADING': 'LOADING', + 'LOADED': 'LOADED', + 'PLAYING': 'PLAYING', + 'PAUSED': 'PAUSED', + 'STOPPED': 'STOPPED', + 'SEEKING': 'SEEKING', + 'ERROR': 'ERROR' + }; + + var applicationID = "2D4B1DA3"; + + // This is the beta version used for testing new changes + + //applicationID = '27C4EB5B'; + + var messageNamespace = 'urn:x-cast:com.connectsdk'; + + var CastPlayer = function () { + + /* device variables */ + // @type {DEVICE_STATE} A state for device + this.deviceState = DEVICE_STATE.IDLE; + + /* Cast player variables */ + // @type {Object} a chrome.cast.media.Media object + this.currentMediaSession = null; + + // @type {string} a chrome.cast.Session object + this.session = null; + // @type {PLAYER_STATE} A state for Cast media player + this.castPlayerState = PLAYER_STATE.IDLE; + + this.hasReceivers = false; + + // bind once - commit 2ebffc2271da0bc5e8b13821586aee2a2e3c7753 + this.errorHandler = this.onError.bind(this); + this.mediaStatusUpdateHandler = this.onMediaStatusUpdate.bind(this); + + this.initializeCastPlayer(); + }; + + /** + * Initialize Cast media player + * Initializes the API. Note that either successCallback and errorCallback will be + * invoked once the API has finished initialization. The sessionListener and + * receiverListener may be invoked at any time afterwards, and possibly more than once. + */ + CastPlayer.prototype.initializeCastPlayer = function () { + + var chrome = window.chrome; + + if (!chrome) { + return; + } + + if (!chrome.cast || !chrome.cast.isAvailable) { + + setTimeout(this.initializeCastPlayer.bind(this), 1000); + return; + } + + // request session + var sessionRequest = new chrome.cast.SessionRequest(applicationID); + var apiConfig = new chrome.cast.ApiConfig(sessionRequest, + this.sessionListener.bind(this), + this.receiverListener.bind(this), + "origin_scoped"); + + console.log('chromecast.initialize'); + + chrome.cast.initialize(apiConfig, this.onInitSuccess.bind(this), this.errorHandler); + + }; + + /** + * Callback function for init success + */ + CastPlayer.prototype.onInitSuccess = function () { + this.isInitialized = true; + console.log("chromecast init success"); + }; + + /** + * Generic error callback function + */ + CastPlayer.prototype.onError = function () { + console.log("chromecast error"); + }; + + /** + * @param {!Object} e A new session + * This handles auto-join when a page is reloaded + * When active session is detected, playback will automatically + * join existing session and occur in Cast mode and media + * status gets synced up with current media of the session + */ + CastPlayer.prototype.sessionListener = function (e) { + + this.session = e; + if (this.session) { + + console.log('sessionListener ' + JSON.stringify(e)); + + if (this.session.media[0]) { + this.onMediaDiscovered('activeSession', this.session.media[0]); + } + + this.onSessionConnected(e); + } + }; + + function alertText(text, title) { + require(['alert'], function (alert) { + alert({ + text: text, + title: title + }); + }); + } + + CastPlayer.prototype.messageListener = function (namespace, message) { + + if (typeof (message) === 'string') { + message = JSON.parse(message); + } + + if (message.type == 'playbackerror') { + + var errorCode = message.data; + + setTimeout(function () { + alertText(globalize.translate('MessagePlaybackError' + errorCode), globalize.translate('HeaderPlaybackError')); + }, 300); + + } + else if (message.type == 'connectionerror') { + + setTimeout(function () { + alertText(globalize.translate('MessageChromecastConnectionError'), globalize.translate('HeaderError')); + }, 300); + + } + else if (message.type) { + events.trigger(this, message.type, [message.data]); + } + }; + + /** + * @param {string} e Receiver availability + * This indicates availability of receivers but + * does not provide a list of device IDs + */ + CastPlayer.prototype.receiverListener = function (e) { + + if (e === 'available') { + console.log("chromecast receiver found"); + this.hasReceivers = true; + } + else { + console.log("chromecast receiver list empty"); + this.hasReceivers = false; + } + }; + + /** + * session update listener + */ + CastPlayer.prototype.sessionUpdateListener = function (isAlive) { + + console.log('sessionUpdateListener alive: ' + isAlive); + + if (isAlive) { + } + else { + this.session = null; + this.deviceState = DEVICE_STATE.IDLE; + this.castPlayerState = PLAYER_STATE.IDLE; + + console.log('sessionUpdateListener: setting currentMediaSession to null'); + this.currentMediaSession = null; + + sendConnectionResult(false); + } + }; + + /** + * Requests that a receiver application session be created or joined. By default, the SessionRequest + * passed to the API at initialization time is used; this may be overridden by passing a different + * session request in opt_sessionRequest. + */ + CastPlayer.prototype.launchApp = function () { + console.log("chromecast launching app..."); + chrome.cast.requestSession(this.onRequestSessionSuccess.bind(this), this.onLaunchError.bind(this)); + }; + + /** + * Callback function for request session success + * @param {Object} e A chrome.cast.Session object + */ + CastPlayer.prototype.onRequestSessionSuccess = function (e) { + + console.log("chromecast session success: " + e.sessionId); + this.onSessionConnected(e); + }; + + CastPlayer.prototype.onSessionConnected = function (session) { + + this.session = session; + + this.deviceState = DEVICE_STATE.ACTIVE; + + this.session.addMessageListener(messageNamespace, this.messageListener.bind(this)); + this.session.addMediaListener(this.sessionMediaListener.bind(this)); + this.session.addUpdateListener(this.sessionUpdateListener.bind(this)); + + events.trigger(this, 'connect'); + + this.sendMessage({ + options: {}, + command: 'Identify' + }); + }; + + /** + * session update listener + */ + CastPlayer.prototype.sessionMediaListener = function (e) { + + console.log('sessionMediaListener'); + this.currentMediaSession = e; + this.currentMediaSession.addUpdateListener(this.mediaStatusUpdateHandler); + }; + + /** + * Callback function for launch error + */ + CastPlayer.prototype.onLaunchError = function () { + console.log("chromecast launch error"); + this.deviceState = DEVICE_STATE.ERROR; + + sendConnectionResult(false); + }; + + /** + * Stops the running receiver application associated with the session. + */ + CastPlayer.prototype.stopApp = function () { + + if (this.session) { + this.session.stop(this.onStopAppSuccess.bind(this, 'Session stopped'), + this.errorHandler); + } + + }; + + /** + * Callback function for stop app success + */ + CastPlayer.prototype.onStopAppSuccess = function (message) { + console.log(message); + this.deviceState = DEVICE_STATE.IDLE; + this.castPlayerState = PLAYER_STATE.IDLE; + + console.log('onStopAppSuccess: setting currentMediaSession to null'); + this.currentMediaSession = null; + }; + + /** + * Loads media into a running receiver application + * @param {Number} mediaIndex An index number to indicate current media content + */ + CastPlayer.prototype.loadMedia = function (options, command) { + + if (!this.session) { + console.log("no session"); + return Promise.reject(); + } + + // Convert the items to smaller stubs to send the minimal amount of information + options.items = options.items.map(function (i) { + + return { + Id: i.Id, + Name: i.Name, + Type: i.Type, + MediaType: i.MediaType, + IsFolder: i.IsFolder + }; + }); + + return this.sendMessage({ + options: options, + command: command + }); + }; + + CastPlayer.prototype.sendMessage = function (message) { + + var player = this; + + var receiverName = null; + + var session = player.session; + + if (session && session.receiver && session.receiver.friendlyName) { + receiverName = session.receiver.friendlyName; + } + + message = Object.assign(message, { + userId: ApiClient.getCurrentUserId(), + deviceId: ApiClient.deviceId(), + accessToken: ApiClient.accessToken(), + serverAddress: ApiClient.serverAddress(), + receiverName: receiverName + }); + + var bitrateSetting = appSettings.maxChromecastBitrate(); + if (bitrateSetting) { + message.maxBitrate = bitrateSetting; + } + + return new Promise(function (resolve, reject) { + + require(['chromecasthelpers'], function (chromecasthelpers) { + + chromecasthelpers.getServerAddress(ApiClient).then(function (serverAddress) { + message.serverAddress = serverAddress; + player.sendMessageInternal(message).then(resolve, reject); + + }, reject); + }); + }); + }; + + CastPlayer.prototype.sendMessageInternal = function (message) { + + message = JSON.stringify(message); + //console.log(message); + + this.session.sendMessage(messageNamespace, message, this.onPlayCommandSuccess.bind(this), this.errorHandler); + return Promise.resolve(); + }; + + CastPlayer.prototype.onPlayCommandSuccess = function () { + console.log('Message was sent to receiver ok.'); + }; + + /** + * Callback function for loadMedia success + * @param {Object} mediaSession A new media object. + */ + CastPlayer.prototype.onMediaDiscovered = function (how, mediaSession) { + + console.log("chromecast new media session ID:" + mediaSession.mediaSessionId + ' (' + how + ')'); + this.currentMediaSession = mediaSession; + + if (how == 'loadMedia') { + this.castPlayerState = PLAYER_STATE.PLAYING; + } + + if (how == 'activeSession') { + this.castPlayerState = mediaSession.playerState; + } + + this.currentMediaSession.addUpdateListener(this.mediaStatusUpdateHandler); + }; + + /** + * Callback function for media status update from receiver + * @param {!Boolean} e true/false + */ + CastPlayer.prototype.onMediaStatusUpdate = function (e) { + + if (e == false) { + this.castPlayerState = PLAYER_STATE.IDLE; + } + console.log("chromecast updating media: " + e); + }; + + /** + * Set media volume in Cast mode + * @param {Boolean} mute A boolean + */ + CastPlayer.prototype.setReceiverVolume = function (mute, vol) { + + if (!this.currentMediaSession) { + console.log('this.currentMediaSession is null'); + return; + } + + if (!mute) { + + this.session.setReceiverVolumeLevel((vol || 1), + this.mediaCommandSuccessCallback.bind(this), + this.errorHandler); + } + else { + this.session.setReceiverMuted(true, + this.mediaCommandSuccessCallback.bind(this), + this.errorHandler); + } + }; + + /** + * Mute CC + */ + CastPlayer.prototype.mute = function () { + this.setReceiverVolume(true); + }; + + /** + * Callback function for media command success + */ + CastPlayer.prototype.mediaCommandSuccessCallback = function (info, e) { + console.log(info); + }; + + function chromecastPlayer() { + + var self = this; + // Create Cast Player + var castPlayer; + + // playbackManager needs this + self.name = PlayerName; + self.type = 'mediaplayer'; + self.id = 'chromecast'; + self.isLocalPlayer = false; + + self.getItemsForPlayback = function (query) { + + var userId = ApiClient.getCurrentUserId(); + + if (query.Ids && query.Ids.split(',').length == 1) { + return ApiClient.getItem(userId, query.Ids.split(',')).then(function (item) { + return { + Items: [item], + TotalRecordCount: 1 + }; + }); + } + else { + + query.Limit = query.Limit || 100; + query.ExcludeLocationTypes = "Virtual"; + + return ApiClient.getItems(userId, query); + } + }; + + function initializeChromecast() { + + fileref.loaded = true; + castPlayer = new CastPlayer(); + + // To allow the native android app to override + document.dispatchEvent(new CustomEvent("chromecastloaded", { + detail: { + player: self + } + })); + + events.on(castPlayer, "connect", function (e) { + + if (currentResolve) { + sendConnectionResult(true); + } else { + playbackManager.setActivePlayer(PlayerName, self.getCurrentTargetInfo()); + } + + console.log('cc: connect'); + // Reset this so the next query doesn't make it appear like content is playing. + self.lastPlayerData = {}; + }); + + events.on(castPlayer, "playbackstart", function (e, data) { + + console.log('cc: playbackstart'); + + castPlayer.initializeCastPlayer(); + + var state = self.getPlayerStateInternal(data); + events.trigger(self, "playbackstart", [state]); + }); + + events.on(castPlayer, "playbackstop", function (e, data) { + + console.log('cc: playbackstop'); + var state = self.getPlayerStateInternal(data); + + events.trigger(self, "playbackstop", [state]); + + // Reset this so the next query doesn't make it appear like content is playing. + self.lastPlayerData = {}; + }); + + events.on(castPlayer, "playbackprogress", function (e, data) { + + console.log('cc: positionchange'); + var state = self.getPlayerStateInternal(data); + + events.trigger(self, "timeupdate", [state]); + }); + + events.on(castPlayer, "volumechange", function (e, data) { + + console.log('cc: volumechange'); + var state = self.getPlayerStateInternal(data); + + events.trigger(self, "volumechange", [state]); + }); + + events.on(castPlayer, "playstatechange", function (e, data) { + + console.log('cc: playstatechange'); + var state = self.getPlayerStateInternal(data); + + events.trigger(self, "pause", [state]); + }); + } + + self.play = function (options) { + + return ApiClient.getCurrentUser().then(function (user) { + + if (options.items) { + + return self.playWithCommand(options, 'PlayNow'); + + } else { + + return self.getItemsForPlayback({ + + Ids: options.ids.join(',') + + }).then(function (result) { + + options.items = result.Items; + return self.playWithCommand(options, 'PlayNow'); + + }); + } + + }); + + }; + + self.playWithCommand = function (options, command) { + + if (!options.items) { + var apiClient = connectionManager.getApiClient(options.serverId); + return apiClient.getItem(apiClient.getCurrentUserId(), options.ids[0]).then(function (item) { + + options.items = [item]; + return self.playWithCommand(options, command); + }); + } + + return castPlayer.loadMedia(options, command); + }; + + self.unpause = function () { + castPlayer.sendMessage({ + options: {}, + command: 'Unpause' + }); + }; + + self.pause = function () { + castPlayer.sendMessage({ + options: {}, + command: 'Pause' + }); + }; + + self.shuffle = function (item) { + + var apiClient = connectionManager.getApiClient(item.ServerId); + var userId = apiClient.getCurrentUserId(); + + apiClient.getItem(userId, item.Id).then(function (item) { + + self.playWithCommand({ + + items: [item] + + }, 'Shuffle'); + + }); + + }; + + self.instantMix = function (item) { + + var apiClient = connectionManager.getApiClient(item.ServerId); + var userId = apiClient.getCurrentUserId(); + + apiClient.getItem(userId, item.Id).then(function (item) { + + self.playWithCommand({ + + items: [item] + + }, 'InstantMix'); + + }); + + }; + + self.canPlayMediaType = function (mediaType) { + + mediaType = (mediaType || '').toLowerCase(); + return mediaType === 'audio' || mediaType === 'video'; + }; + + self.canQueueMediaType = function (mediaType) { + return self.canPlayMediaType(mediaType); + }; + + self.queue = function (options) { + self.playWithCommand(options, 'PlayLast'); + }; + + self.queueNext = function (options) { + self.playWithCommand(options, 'PlayNext'); + }; + + self.stop = function () { + castPlayer.sendMessage({ + options: {}, + command: 'Stop' + }); + }; + + self.displayContent = function (options) { + + castPlayer.sendMessage({ + options: options, + command: 'DisplayContent' + }); + }; + + self.currentTime = function (val) { + + if (val != null) { + return self.seek(val); + } + + var state = self.lastPlayerData || {}; + state = state.PlayState || {}; + return state.PositionTicks; + }; + + self.paused = function () { + var state = self.lastPlayerData || {}; + state = state.PlayState || {}; + + return state.IsPaused; + }; + + self.isMuted = function () { + var state = self.lastPlayerData || {}; + state = state.PlayState || {}; + + return state.IsMuted; + }; + + self.setMute = function (isMuted) { + + if (isMuted) { + castPlayer.sendMessage({ + options: {}, + command: 'Mute' + }); + //castPlayer.setMute(true); + } else { + self.setVolume(self.getVolume() + 2); + } + }; + + self.setRepeatMode = function (mode) { + castPlayer.sendMessage({ + options: { + RepeatMode: mode + }, + command: 'SetRepeatMode' + }); + }; + + self.toggleMute = function () { + + if (self.isMuted()) { + self.setMute(false); + } else { + self.setMute(true); + } + }; + + self.getTargets = function () { + + var targets = []; + + if (castPlayer.hasReceivers) { + targets.push(self.getCurrentTargetInfo()); + } + + return Promise.resolve(targets); + }; + + self.getCurrentTargetInfo = function () { + + var appName = null; + + if (castPlayer.session && castPlayer.session.receiver && castPlayer.session.receiver.friendlyName) { + appName = castPlayer.session.receiver.friendlyName; + } + + return { + name: PlayerName, + id: PlayerName, + playerName: PlayerName, + playableMediaTypes: ["Audio", "Video"], + isLocalPlayer: false, + appName: PlayerName, + deviceName: appName, + supportedCommands: ["VolumeUp", + "VolumeDown", + "Mute", + "Unmute", + "ToggleMute", + "SetVolume", + "SetAudioStreamIndex", + "SetSubtitleStreamIndex", + "DisplayContent", + "SetRepeatMode", + "EndSession"] + }; + }; + + self.seek = function (position) { + + position = parseInt(position); + + position = position / 10000000; + + castPlayer.sendMessage({ + options: { + position: position + }, + command: 'Seek' + }); + }; + + self.setAudioStreamIndex = function (index) { + castPlayer.sendMessage({ + options: { + index: index + }, + command: 'SetAudioStreamIndex' + }); + }; + + self.setSubtitleStreamIndex = function (index) { + castPlayer.sendMessage({ + options: { + index: index + }, + command: 'SetSubtitleStreamIndex' + }); + }; + + self.nextTrack = function () { + castPlayer.sendMessage({ + options: {}, + command: 'NextTrack' + }); + }; + + self.previousTrack = function () { + castPlayer.sendMessage({ + options: {}, + command: 'PreviousTrack' + }); + }; + + self.beginPlayerUpdates = function () { + // Setup polling here + }; + + self.endPlayerUpdates = function () { + // Stop polling here + }; + + self.getVolume = function () { + + var state = self.lastPlayerData || {}; + state = state.PlayState || {}; + + return state.VolumeLevel == null ? 100 : state.VolumeLevel; + }; + + self.volumeDown = function () { + + castPlayer.sendMessage({ + options: {}, + command: 'VolumeDown' + }); + }; + + self.endSession = function () { + + self.stop(); + setTimeout(function () { + castPlayer.stopApp(); + }, 1000); + }; + + self.volumeUp = function () { + + castPlayer.sendMessage({ + options: {}, + command: 'VolumeUp' + }); + }; + + self.setVolume = function (vol) { + + vol = Math.min(vol, 100); + vol = Math.max(vol, 0); + + //castPlayer.setReceiverVolume(false, (vol / 100)); + castPlayer.sendMessage({ + options: { + volume: vol + }, + command: 'SetVolume' + }); + }; + + self.getPlayerState = function () { + + var result = self.getPlayerStateInternal(); + return Promise.resolve(result); + }; + + self.lastPlayerData = {}; + + self.getPlayerStateInternal = function (data) { + + data = data || self.lastPlayerData; + self.lastPlayerData = data; + + console.log(JSON.stringify(data)); + return data; + }; + + self.tryPair = function (target) { + + if (castPlayer.deviceState != DEVICE_STATE.ACTIVE && castPlayer.isInitialized) { + + return new Promise(function (resolve, reject) { + currentResolve = resolve; + currentReject = reject; + + castPlayer.launchApp(); + }); + } else { + + currentResolve = null; + currentReject = null; + + return Promise.reject(); + } + }; + + if (fileref.loaded) { + initializeChromecast(); + } else { + fileref.onload = initializeChromecast; + } + } + + var fileref = document.createElement('script'); + fileref.setAttribute("type", "text/javascript"); + fileref.onload = function () { + fileref.loaded = true; + }; + fileref.setAttribute("src", "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js"); + document.querySelector('head').appendChild(fileref); + + return chromecastPlayer; +}); \ No newline at end of file diff --git a/dashboard-ui/bower_components/emby-webcomponents/htmlaudioplayer/plugin.js b/dashboard-ui/bower_components/emby-webcomponents/htmlaudioplayer/plugin.js index 3f6c741b26..d9ce518470 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/htmlaudioplayer/plugin.js +++ b/dashboard-ui/bower_components/emby-webcomponents/htmlaudioplayer/plugin.js @@ -27,7 +27,6 @@ define(['events', 'browser', 'pluginManager', 'apphost'], function (events, brow require(['browserdeviceprofile'], function (profileBuilder) { var profile = profileBuilder({ - supportsCustomSeeking: true }); resolve(profile); }); @@ -110,7 +109,7 @@ define(['events', 'browser', 'pluginManager', 'apphost'], function (events, brow return true; } - self.stop = function (destroyPlayer, reportEnded) { + self.stop = function (destroyPlayer) { cancelFadeTimeout(); @@ -127,7 +126,7 @@ define(['events', 'browser', 'pluginManager', 'apphost'], function (events, brow elem.src = ''; elem.innerHTML = ''; elem.removeAttribute("src"); - onEndedInternal(reportEnded); + onEnded(); return Promise.resolve(); } @@ -144,7 +143,7 @@ define(['events', 'browser', 'pluginManager', 'apphost'], function (events, brow elem.removeAttribute("src"); elem.volume = originalVolume; - onEndedInternal(reportEnded); + onEnded(); }); } return Promise.resolve(); @@ -212,56 +211,47 @@ define(['events', 'browser', 'pluginManager', 'apphost'], function (events, brow return false; }; - self.volume = function (val) { + self.setVolume = function (val) { if (mediaElement) { - if (val != null) { - mediaElement.volume = val / 100; - return; - } + mediaElement.volume = val / 100; + } + }; + self.getVolume = function () { + if (mediaElement) { return mediaElement.volume * 100; } }; self.volumeUp = function () { - self.volume(Math.min(self.volume() + 2, 100)); + self.setVolume(Math.min(self.getVolume() + 2, 100)); }; self.volumeDown = function () { - self.volume(Math.max(self.volume() - 2, 0)); + self.setVolume(Math.max(self.getVolume() - 2, 0)); }; self.setMute = function (mute) { - if (mute) { - self.volume(0); - } else { - - if (self.isMuted()) { - self.volume(50); - } + if (mediaElement) { + mediaElement.muted = mute; } }; self.isMuted = function () { - return self.volume() === 0; + if (mediaElement) { + return mediaElement.muted; + } + return false; }; function onEnded() { - onEndedInternal(true); - } - - function onEndedInternal(triggerEnded) { - - if (triggerEnded) { - var stopInfo = { - src: currentSrc - }; - - events.trigger(self, 'stopped', [stopInfo]); - } + var stopInfo = { + src: currentSrc + }; + events.trigger(self, 'stopped', [stopInfo]); currentSrc = null; } diff --git a/dashboard-ui/bower_components/emby-webcomponents/input/api.js b/dashboard-ui/bower_components/emby-webcomponents/input/api.js index c91f54ae4a..19cda4d7b9 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/input/api.js +++ b/dashboard-ui/bower_components/emby-webcomponents/input/api.js @@ -89,7 +89,7 @@ define(['connectionManager', 'playbackManager', 'events', 'inputManager', 'focus return; case 'SetVolume': notifyApp(); - playbackManager.volume(cmd.Arguments.Volume); + playbackManager.setVolume(cmd.Arguments.Volume); break; case 'SetAudioStreamIndex': notifyApp(); diff --git a/dashboard-ui/bower_components/emby-webcomponents/mediainfo/mediainfo.js b/dashboard-ui/bower_components/emby-webcomponents/mediainfo/mediainfo.js index 8ba0d9e806..f641fe2364 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/mediainfo/mediainfo.js +++ b/dashboard-ui/bower_components/emby-webcomponents/mediainfo/mediainfo.js @@ -527,21 +527,7 @@ define(['datetime', 'globalize', 'embyRouter', 'itemHelper', 'material-icons', ' var list = []; - if (item.DateCreated && itemHelper.enableDateAddedDisplay(item)) { - list.push({ - type: 'added', - text: globalize.translate('sharedcomponents#AddedOnValue', datetime.toLocaleDateString(datetime.parseISO8601Date(item.DateCreated))) - }); - } - - if (!item.MediaSources) { - return list; - } - - var mediaSource = item.MediaSources[0]; - if (!mediaSource) { - return list; - } + var mediaSource = (item.MediaSources || [])[0] || {}; var videoStream = (mediaSource.MediaStreams || []).filter(function (i) { return i.Type === 'Video'; @@ -620,6 +606,16 @@ define(['datetime', 'globalize', 'embyRouter', 'itemHelper', 'material-icons', ' }); } + if (item.DateCreated && itemHelper.enableDateAddedDisplay(item)) { + + var dateCreated = datetime.parseISO8601Date(item.DateCreated); + + list.push({ + type: 'added', + text: globalize.translate('sharedcomponents#AddedOnValue', datetime.toLocaleDateString(dateCreated) + ' ' + datetime.getDisplayTime(dateCreated)) + }); + } + return list; } diff --git a/dashboard-ui/bower_components/emby-webcomponents/playback/nowplayinghelper.js b/dashboard-ui/bower_components/emby-webcomponents/playback/nowplayinghelper.js new file mode 100644 index 0000000000..72f2023d63 --- /dev/null +++ b/dashboard-ui/bower_components/emby-webcomponents/playback/nowplayinghelper.js @@ -0,0 +1,87 @@ +define([], function () { + 'use strict'; + + function getNowPlayingNames(nowPlayingItem, includeNonNameInfo) { + + var topItem = nowPlayingItem; + var bottomItem = null; + var topText = nowPlayingItem.Name; + + if (nowPlayingItem.AlbumId && nowPlayingItem.MediaType == 'Audio') { + topItem = { + Id: nowPlayingItem.AlbumId, + Name: nowPlayingItem.Album, + Type: 'MusicAlbum', + IsFolder: true + }; + } + + if (nowPlayingItem.MediaType == 'Video') { + if (nowPlayingItem.IndexNumber != null) { + topText = nowPlayingItem.IndexNumber + " - " + topText; + } + if (nowPlayingItem.ParentIndexNumber != null) { + topText = nowPlayingItem.ParentIndexNumber + "." + topText; + } + } + + var bottomText = ''; + + if (nowPlayingItem.Artists && nowPlayingItem.Artists.length) { + + if (nowPlayingItem.ArtistItems && nowPlayingItem.ArtistItems.length) { + + bottomItem = { + Id: nowPlayingItem.ArtistItems[0].Id, + Name: nowPlayingItem.ArtistItems[0].Name, + Type: 'MusicArtist', + IsFolder: true + }; + + bottomText = bottomItem.Name; + } else { + bottomText = nowPlayingItem.Artists[0]; + } + } + else if (nowPlayingItem.SeriesName || nowPlayingItem.Album) { + bottomText = topText; + topText = nowPlayingItem.SeriesName || nowPlayingItem.Album; + + bottomItem = topItem; + + if (nowPlayingItem.SeriesId) { + topItem = { + Id: nowPlayingItem.SeriesId, + Name: nowPlayingItem.SeriesName, + Type: 'Series', + IsFolder: true + }; + } else { + topItem = null; + } + } + else if (nowPlayingItem.ProductionYear && includeNonNameInfo !== false) { + bottomText = nowPlayingItem.ProductionYear; + } + + var list = []; + + list.push({ + text: topText, + item: topItem + }); + + if (bottomText) { + list.push({ + text: bottomText, + item: bottomItem + }); + } + + return list; + } + + return { + getNowPlayingNames: getNowPlayingNames + }; +}); diff --git a/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js b/dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js similarity index 81% rename from dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js rename to dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js index 8e2a50caea..893c339c4f 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/playbackmanager.js +++ b/dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js @@ -11,7 +11,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return false; } - function playbackManager() { + function PlaybackManager() { var self = this; @@ -38,11 +38,34 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return data.streamInfo ? data.streamInfo.mediaSource : null; }; - function triggerPlayerChange(newPlayer, newTarget, previousPlayer) { + function triggerPlayerChange(newPlayer, newTarget, previousPlayer, previousTargetInfo) { + + if (!newPlayer && !previousPlayer) { + return; + } + + if (newTarget && previousTargetInfo) { + + if (newTarget.id === previousTargetInfo.id) { + return; + } + } events.trigger(self, 'playerchange', [newPlayer, newTarget, previousPlayer]); } + self.beginPlayerUpdates = function (player) { + if (player.beginPlayerUpdates) { + player.beginPlayerUpdates(); + } + }; + + self.endPlayerUpdates = function (player) { + if (player.endPlayerUpdates) { + player.endPlayerUpdates(); + } + }; + self.getPlayerInfo = function () { var player = currentPlayer; @@ -66,6 +89,11 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.setActivePlayer = function (player, targetInfo) { + if (player === 'localplayer' || player.name === 'localplayer') { + setCurrentPlayerInternal(null, null); + return; + } + if (typeof (player) === 'string') { player = players.filter(function (p) { return p.name === player; @@ -76,19 +104,21 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g throw new Error('null player'); } - var previousPlayer = currentPlayer; - - currentPairingId = null; - currentPlayer = player; - currentTargetInfo = targetInfo; - - console.log('Active player: ' + JSON.stringify(currentTargetInfo)); - - triggerPlayerChange(player, targetInfo, previousPlayer); + setCurrentPlayerInternal(player, targetInfo); }; + function displayPlayerInLocalGroup(player) { + + return player.isLocalPlayer; + } + self.trySetActivePlayer = function (player, targetInfo) { + if (player === 'localplayer' || player.name === 'localplayer') { + setCurrentPlayerInternal(null, null); + return; + } + if (typeof (player) === 'string') { player = players.filter(function (p) { return p.name === player; @@ -105,20 +135,13 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g currentPairingId = targetInfo.id; - var promise = player.tryPair ? + var promise = player.tryPair ? player.tryPair(targetInfo) : Promise.resolve(); promise.then(function () { - var previousPlayer = currentPlayer; - - currentPlayer = player; - currentTargetInfo = targetInfo; - - console.log('Active player: ' + JSON.stringify(currentTargetInfo)); - - triggerPlayerChange(player, targetInfo, previousPlayer); + setCurrentPlayerInternal(player, targetInfo); }, function () { if (currentPairingId === targetInfo.id) { @@ -151,36 +174,51 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g function getSupportedCommands(player) { if (player.isLocalPlayer) { - return Dashboard.getSupportedRemoteCommands(); + // Full list + // https://github.com/MediaBrowser/MediaBrowser/blob/master/MediaBrowser.Model/Session/GeneralCommand.cs + return [ + "GoHome", + "GoToSettings", + "VolumeUp", + "VolumeDown", + "Mute", + "Unmute", + "ToggleMute", + "SetVolume", + "SetAudioStreamIndex", + "SetSubtitleStreamIndex", + "DisplayContent", + "GoToSearch", + "DisplayMessage", + "SetRepeatMode" + ]; } throw new Error('player must define supported commands'); } + function createTarget(player) { + return { + name: player.name, + id: player.id, + playerName: player.name, + playableMediaTypes: ['Audio', 'Video', 'Game'].map(player.canPlayMediaType), + isLocalPlayer: player.isLocalPlayer, + supportedCommands: getSupportedCommands(player) + }; + } + function getPlayerTargets(player) { if (player.getTargets) { return player.getTargets(); } - return Promise.resolve([{ - - name: player.name, - id: player.id, - playerName: player.name, - playableMediaTypes: ['Audio', 'Video', 'Game'].map(player.canPlayMediaType), - isLocalPlayer: player.isLocalPlayer, - supportedCommands: getSupportedCommands(player) - }]); + return Promise.resolve([createTarget(player)]); } self.setDefaultPlayerActive = function () { - var player = self.getDefaultPlayer(); - - getPlayerTargets(player).then(function (targets) { - - self.setActivePlayer(player, targets[0]); - }); + self.setActivePlayer('localplayer'); }; self.removeActivePlayer = function (name) { @@ -209,18 +247,18 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g var menuItems = []; menuItems.push({ - name: Globalize.translate('ButtonYes'), + name: globalize.translate('ButtonYes'), id: 'yes' }); menuItems.push({ - name: Globalize.translate('ButtonNo'), + name: globalize.translate('ButtonNo'), id: 'no' }); dialog({ buttons: menuItems, //positionTo: positionTo, - text: Globalize.translate('ConfirmEndPlayerSession') + text: globalize.translate('ConfirmEndPlayerSession') }).then(function (id) { switch (id) { @@ -248,12 +286,25 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.getTargets = function () { - var promises = players.map(getPlayerTargets); + var promises = players.filter(function (p) { + return !displayPlayerInLocalGroup(p); + }).map(getPlayerTargets); return Promise.all(promises).then(function (responses) { var targets = []; + targets.push({ + name: globalize.translate('sharedcomponents#MyDevice'), + id: 'localplayer', + playerName: 'localplayer', + playableMediaTypes: ['Audio', 'Video', 'Game'], + isLocalPlayer: true, + supportedCommands: getSupportedCommands({ + isLocalPlayer: true + }) + }); + for (var i = 0; i < responses.length; i++) { var subTargets = responses[i]; @@ -280,6 +331,60 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g }); }; + self.displayContent = function (options, player) { + player = player || currentPlayer; + if (player && player.displayContent) { + player.displayContent(options); + } + } + + self.sendCommand = function (cmd, player) { + + // Full list + // https://github.com/MediaBrowser/MediaBrowser/blob/master/MediaBrowser.Model/Session/GeneralCommand.cs#L23 + console.log('MediaController received command: ' + cmd.Name); + switch (cmd.Name) { + + case 'SetRepeatMode': + self.setRepeatMode(cmd.Arguments.RepeatMode, player); + break; + case 'VolumeUp': + self.volumeUp(player); + break; + case 'VolumeDown': + self.volumeDown(player); + break; + case 'Mute': + self.setMute(true, player); + break; + case 'Unmute': + self.setMute(false, player); + break; + case 'ToggleMute': + self.toggleMute(player); + break; + case 'SetVolume': + self.setVolume(cmd.Arguments.Volume, player); + break; + case 'SetAudioStreamIndex': + self.setAudioStreamIndex(parseInt(cmd.Arguments.Index), player); + break; + case 'SetSubtitleStreamIndex': + self.setSubtitleStreamIndex(parseInt(cmd.Arguments.Index), player); + break; + case 'ToggleFullscreen': + self.toggleFullscreen(player); + break; + default: + { + if (player.sendCommand) { + player.sendCommand(cmd); + } + break; + } + } + }; + function getCurrentSubtitleStream(player) { var index = getPlayerData(player).subtitleStreamIndex; @@ -323,11 +428,32 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return currentPlayer; }; - function setCurrentPlayer(player) { + function setCurrentPlayerInternal(player, targetInfo) { + + var previousPlayer = currentPlayer; + var previousTargetInfo = currentTargetInfo; + + if (player && !targetInfo && player.isLocalPlayer) { + targetInfo = createTarget(player); + } + + if (player && !targetInfo) { + throw new Error('targetInfo cannot be null'); + } + + currentPairingId = null; currentPlayer = player; + currentTargetInfo = targetInfo; + + if (targetInfo) { + console.log('Active player: ' + JSON.stringify(targetInfo)); + } + if (player && player.isLocalPlayer) { lastLocalPlayer = player; } + + triggerPlayerChange(player, targetInfo, previousPlayer, previousTargetInfo); } self.isPlaying = function () { @@ -360,6 +486,16 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return players; }; + function getAutomaticPlayers() { + + var player = currentPlayer; + if (player && !enableLocalPlaylistManagement(player)) { + return [player]; + } + + return self.getPlayers().filter(enableLocalPlaylistManagement); + } + self.canPlay = function (item) { var itemType = item.Type; @@ -382,7 +518,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g } } - return self.getPlayers().filter(function (p) { + return getAutomaticPlayers().filter(function (p) { return p.canPlayMediaType(mediaType); @@ -406,26 +542,30 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return false; }; - self.isMuted = function () { + self.isMuted = function (player) { - if (currentPlayer) { - return currentPlayer.isMuted(); + player = player || currentPlayer; + + if (player) { + return player.isMuted(); } return false; }; - self.setMute = function (mute) { + self.setMute = function (mute, player) { - if (currentPlayer) { - currentPlayer.setMute(mute); + player = player || currentPlayer; + + if (player) { + player.setMute(mute); } }; - self.toggleMute = function (mute) { + self.toggleMute = function (mute, player) { - var player = currentPlayer; - if (currentPlayer) { + player = player || currentPlayer; + if (player) { if (player.toggleMute) { player.toggleMute(); @@ -435,30 +575,48 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g } }; - self.volume = function (val) { + self.setVolume = function (val, player) { - if (currentPlayer) { - return currentPlayer.volume(val); + player = player || currentPlayer; + + if (player) { + player.setVolume(val); } }; - self.volumeUp = function () { + self.getVolume = function (player) { - if (currentPlayer) { - currentPlayer.volumeUp(); + player = player || currentPlayer; + + if (player) { + return player.getVolume(); } }; - self.volumeDown = function () { + self.volumeUp = function (player) { - if (currentPlayer) { - currentPlayer.volumeDown(); + player = player || currentPlayer; + + if (player) { + player.volumeUp(); } }; - self.setAudioStreamIndex = function (index) { + self.volumeDown = function (player) { - var player = currentPlayer; + player = player || currentPlayer; + + if (player) { + player.volumeDown(); + } + }; + + self.setAudioStreamIndex = function (index, player) { + + player = player || currentPlayer; + if (player && !enableLocalPlaylistManagement(player)) { + return player.setAudioStreamIndex(index); + } if (getPlayerData(player).streamInfo.playMethod === 'Transcode' || !player.canSetAudioStreamIndex()) { @@ -471,9 +629,13 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g } }; - self.setSubtitleStreamIndex = function (index) { + self.setSubtitleStreamIndex = function (index, player) { + + player = player || currentPlayer; + if (player && !enableLocalPlaylistManagement(player)) { + return player.setSubtitleStreamIndex(index); + } - var player = currentPlayer; var currentStream = getCurrentSubtitleStream(player); var newStream = getSubtitleStream(player, index); @@ -531,7 +693,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g if (enabled != null) { var val = enabled ? '1' : '0'; - appSettings.set('displaymirror--' + Dashboard.getCurrentUserId(), val); + appSettings.set('displaymirror', val); if (enabled) { mirrorIfEnabled(); @@ -539,13 +701,17 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return; } - return (appSettings.get('displaymirror--' + Dashboard.getCurrentUserId()) || '') !== '0'; + return (appSettings.get('displaymirror') || '') !== '0'; }; - self.stop = function () { - if (currentPlayer) { + self.stop = function (player) { + + player = player || currentPlayer; + + if (player) { playNextAfterEnded = false; - currentPlayer.stop(true, true); + // TODO: remove second param + player.stop(true, true); } }; @@ -579,9 +745,12 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g } }; - self.seek = function (ticks) { + self.seek = function (ticks, player) { - var player = currentPlayer; + player = player || currentPlayer; + if (currentPlayer && !enableLocalPlaylistManagement(player)) { + return player.seek(ticks); + } changeStream(player, ticks); }; @@ -818,19 +987,55 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.play = function (options) { - if (typeof (options) === 'string') { - options = { ids: [options] }; + normalizePlayOptions(options); + + if (currentPlayer) { + if (options.enableRemotePlayers === false && !currentPlayer.isLocalPlayer) { + return Promise.reject(); + } + + if (!enableLocalPlaylistManagement(currentPlayer)) { + return currentPlayer.play(options); + } } - return playItems(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); + }); + + }); + } }; - self.instantMix = function (id, serverId) { + self.instantMix = function (item, player) { - if (typeof id !== 'string') { - var item = id; - id = item.Id; - serverId = item.ServerId; + player = player || currentPlayer; + if (!enableLocalPlaylistManagement(player)) { + return player.instantMix(item); } var apiClient = connectionManager.getApiClient(serverId); @@ -839,24 +1044,23 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g options.UserId = apiClient.getCurrentUserId(); options.Fields = 'MediaSources'; - apiClient.getInstantMixFromItem(id, options).then(function (result) { + apiClient.getInstantMixFromItem(id, item).then(function (result) { self.play({ items: result.Items }); }); }; - self.shuffle = function (id, serverId) { + self.shuffle = function (shuffleItem, player) { - if (typeof id !== 'string') { - var item = id; - id = item.Id; - serverId = item.ServerId; + player = player || currentPlayer; + if (!enableLocalPlaylistManagement(player)) { + return player.shuffle(shuffleItem); } - var apiClient = connectionManager.getApiClient(serverId); + var apiClient = connectionManager.getApiClient(shuffleItem.ServerId); - apiClient.getItem(apiClient.getCurrentUserId(), id).then(function (item) { + apiClient.getItem(apiClient.getCurrentUserId(), shuffleItem.Id).then(function (item) { var query = { Fields: "MediaSources,Chapters", @@ -915,9 +1119,15 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.getPlayerState = function (player) { player = player || currentPlayer; + + if (!enableLocalPlaylistManagement(player)) { + return player.getPlayerState(); + } + var playerData = getPlayerData(player); - var item = playerData.streamInfo.item; - var mediaSource = playerData.streamInfo.mediaSource; + var streamInfo = playerData.streamInfo; + var item = streamInfo ? streamInfo.item : null; + var mediaSource = streamInfo ? streamInfo.mediaSource : null; var state = { PlayState: {} @@ -925,15 +1135,13 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g if (player) { - state.PlayState.VolumeLevel = player.volume(); + state.PlayState.VolumeLevel = player.getVolume(); state.PlayState.IsMuted = player.isMuted(); state.PlayState.IsPaused = player.paused(); - state.PlayState.PositionTicks = getCurrentTicks(player); state.PlayState.RepeatMode = self.getRepeatMode(); - var currentSrc = player.currentSrc(); - - if (currentSrc) { + if (streamInfo) { + state.PlayState.PositionTicks = getCurrentTicks(player); state.PlayState.SubtitleStreamIndex = playerData.subtitleStreamIndex; state.PlayState.AudioStreamIndex = playerData.audioStreamIndex; @@ -963,10 +1171,16 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g state.NowPlayingItem = getNowPlayingItemForReporting(player, item, mediaSource); } - return state; + return Promise.resolve(state); }; self.currentTime = function (player) { + + player = player || currentPlayer; + if (currentPlayer && !enableLocalPlaylistManagement(player)) { + return player.currentTime(); + } + return getCurrentTicks(player); }; @@ -1000,7 +1214,9 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g nowPlayingItem.PremiereDate = item.PremiereDate; nowPlayingItem.SeriesName = item.SeriesName; nowPlayingItem.Album = item.Album; - nowPlayingItem.Artists = item.ArtistItems; + nowPlayingItem.AlbumId = item.AlbumId; + nowPlayingItem.Artists = item.Artists; + nowPlayingItem.ArtistItems = item.ArtistItems; var imageTags = item.ImageTags || {}; @@ -1055,42 +1271,6 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g return nowPlayingItem; } - function playItems(options, method) { - - normalizePlayOptions(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(); - } - - return getItemsForPlayback(options.serverId, { - - Ids: options.ids.join(',') - - }).then(function (result) { - - return translateItemsForPlayback(result.Items, options).then(function (items) { - - return playWithIntros(items, options); - }); - - }); - } - } - function translateItemsForPlayback(items, options) { var firstItem = items[0]; @@ -1281,7 +1461,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g if (player) { player.destroy(); } - setCurrentPlayer(null); + setCurrentPlayerInternal(null); events.trigger(self, 'playbackcancelled'); @@ -1874,11 +2054,9 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g function getPlayer(item, playOptions) { - var players = self.getPlayers(); - var serverItem = isServerItem(item); - return self.getPlayers().filter(function (p) { + return getAutomaticPlayers().filter(function (p) { if (p.canPlayMediaType(item.MediaType)) { @@ -1943,12 +2121,24 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g }); }; - self.setRepeatMode = function (value) { + self.setRepeatMode = function (value, player) { + + player = player || currentPlayer; + if (player && !enableLocalPlaylistManagement(player)) { + return player.setRepeatMode(value); + } + repeatMode = value; events.trigger(self, 'repeatmodechange'); }; - self.getRepeatMode = function () { + self.getRepeatMode = function (player) { + + player = player || currentPlayer; + if (player && !enableLocalPlaylistManagement(player)) { + return player.getRepeatMode(); + } + return repeatMode; }; @@ -1989,7 +2179,12 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g }; } - self.nextTrack = function () { + self.nextTrack = function (player) { + + player = player || currentPlayer; + if (player && !enableLocalPlaylistManagement(player)) { + return player.nextTrack(); + } var newItemInfo = getNextItemInfo(); @@ -2007,7 +2202,13 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g } }; - self.previousTrack = function () { + self.previousTrack = function (player) { + + player = player || currentPlayer; + if (player && !enableLocalPlaylistManagement(player)) { + return player.previousTrack(); + } + var newIndex = currentPlaylistIndex - 1; if (newIndex >= 0) { var newItem = playlist[newIndex]; @@ -2025,23 +2226,28 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g } }; - self.queue = function (options) { - queue(options); + self.queue = function (options, player) { + queue(options, '', player); }; - self.queueNext = function (options) { - queue(options, 'next'); + self.queueNext = function (options, player) { + queue(options, 'next', player); }; - function queue(options, mode) { + function queue(options, mode, player) { - if (!currentPlayer) { - self.play(options); - return; + player = player || currentPlayer; + + if (!player) { + return self.play(options); } - if (typeof (options) === 'string') { - options = { ids: [options] }; + if (!enableLocalPlaylistManagement(player)) { + + if (mode === 'next') { + return player.queueNext(item); + } + return player.queue(item); } // TODO @@ -2049,7 +2255,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g function onPlaybackStarted(player, streamInfo, mediaSource) { - setCurrentPlayer(player); + setCurrentPlayerInternal(player); getPlayerData(player).streamInfo = streamInfo; if (mediaSource) { @@ -2062,13 +2268,15 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g playNextAfterEnded = true; - var state = self.getPlayerState(player); + self.getPlayerState(player).then(function (state) { - reportPlayback(state, getPlayerData(player).streamInfo.item.ServerId, 'reportPlaybackStart'); + reportPlayback(state, getPlayerData(player).streamInfo.item.ServerId, 'reportPlaybackStart'); - startProgressInterval(player); + startProgressInterval(player); - events.trigger(self, 'playbackstart', [player]); + events.trigger(player, 'playbackstart', [state]); + events.trigger(self, 'playbackstart', [player]); + }); } function onPlaybackError(e, error) { @@ -2126,78 +2334,102 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g } // User clicked stop or content ended - var state = self.getPlayerState(player); - var streamInfo = getPlayerData(player).streamInfo; + self.getPlayerState(player).then(function (state) { - if (isServerItem(streamInfo.item)) { + var streamInfo = getPlayerData(player).streamInfo; - if (player.supportsProgress === false && state.PlayState && !state.PlayState.PositionTicks) { - state.PlayState.PositionTicks = streamInfo.item.RunTimeTicks; + if (isServerItem(streamInfo.item)) { + + if (player.supportsProgress === false && state.PlayState && !state.PlayState.PositionTicks) { + state.PlayState.PositionTicks = streamInfo.item.RunTimeTicks; + } + + reportPlayback(state, streamInfo.item.ServerId, 'reportPlaybackStopped'); } - reportPlayback(state, streamInfo.item.ServerId, 'reportPlaybackStopped'); - } + clearProgressInterval(player); - clearProgressInterval(player); + var nextItem = playNextAfterEnded ? getNextItemInfo() : null; - var nextItem = playNextAfterEnded ? getNextItemInfo() : null; + var nextMediaType = (nextItem ? nextItem.item.MediaType : null); - var nextMediaType = (nextItem ? nextItem.item.MediaType : null); + var playbackStopInfo = { + player: player, + state: state, + nextItem: (nextItem ? nextItem.item : null), + nextMediaType: nextMediaType + }; - var playbackStopInfo = { - player: player, - state: state, - nextItem: (nextItem ? nextItem.item : null), - nextMediaType: nextMediaType - }; + events.trigger(player, 'playbackstop', [state]); + events.trigger(self, 'playbackstop', [playbackStopInfo]); - events.trigger(self, 'playbackstop', [playbackStopInfo]); + var newPlayer = nextItem ? getPlayer(nextItem.item, currentPlayOptions) : null; - var newPlayer = nextItem ? getPlayer(nextItem.item, currentPlayOptions) : null; + if (newPlayer !== player) { + player.destroy(); + setCurrentPlayerInternal(null); + } - if (newPlayer !== player) { - player.destroy(); - setCurrentPlayer(null); - } - - if (nextItem) { - self.nextTrack(); - } + if (nextItem) { + self.nextTrack(); + } + }); } function onPlaybackChanging(activePlayer, newPlayer, newItem) { - var state = self.getPlayerState(activePlayer); - var serverId = getPlayerData(activePlayer).streamInfo.item.ServerId; + return self.getPlayerState(activePlayer).then(function (state) { + var serverId = getPlayerData(activePlayer).streamInfo.item.ServerId; - // User started playing something new while existing content is playing - var promise; + // User started playing something new while existing content is playing + var promise; - if (activePlayer === newPlayer) { + unbindStopped(activePlayer); - // If we're staying with the same player, stop it - promise = activePlayer.stop(false, false); + if (activePlayer === newPlayer) { - } else { + // If we're staying with the same player, stop it + // TODO: remove second param + promise = activePlayer.stop(false, true); - // If we're switching players, tear down the current one - promise = activePlayer.stop(true, false); - } + } else { - return promise.then(function () { - reportPlayback(state, serverId, 'reportPlaybackStopped'); + // If we're switching players, tear down the current one + // TODO: remove second param + promise = activePlayer.stop(true, true); + } - clearProgressInterval(activePlayer); + return promise.then(function () { - events.trigger(self, 'playbackstop', [{ - player: activePlayer, - state: state, - nextItem: newItem, - nextMediaType: newItem.MediaType - }]); + bindStopped(activePlayer); + + reportPlayback(state, serverId, 'reportPlaybackStopped'); + + clearProgressInterval(activePlayer); + + 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 unbindStopped(player) { + + events.off(player, 'stopped', onPlaybackStopped); + } + function initMediaPlayer(player) { players.push(player); @@ -2214,8 +2446,8 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g if (enableLocalPlaylistManagement(player)) { events.on(player, 'error', onPlaybackError); - events.on(player, 'stopped', onPlaybackStopped); } + bindStopped(player); } events.on(pluginManager, 'registered', function (e, plugin) { @@ -2249,9 +2481,10 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g player.lastProgressReport = new Date().getTime(); - var state = self.getPlayerState(player); - var currentItem = getPlayerData(player).streamInfo.item; - reportPlayback(state, currentItem.ServerId, 'reportPlaybackProgress'); + self.getPlayerState(player).then(function (state) { + var currentItem = getPlayerData(player).streamInfo.item; + reportPlayback(state, currentItem.ServerId, 'reportPlaybackProgress'); + }); } function reportPlayback(state, serverId, method) { @@ -2303,5 +2536,5 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g }); } - return new playbackManager(); + return new PlaybackManager(); }); diff --git a/dashboard-ui/bower_components/emby-webcomponents/playmenu.js b/dashboard-ui/bower_components/emby-webcomponents/playmenu.js index 9a708893a9..d3b5dadb1e 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/playmenu.js +++ b/dashboard-ui/bower_components/emby-webcomponents/playmenu.js @@ -98,7 +98,9 @@ define(['actionsheet', 'datetime', 'playbackManager', 'globalize', 'appSettings' }); break; case 'queue': - playbackManager.queue(item); + playbackManager.queue({ + items: [item] + }); break; case 'instantmix': playbackManager.instantMix(item); diff --git a/dashboard-ui/bower_components/emby-webcomponents/recordingcreator/recordingcreator.js b/dashboard-ui/bower_components/emby-webcomponents/recordingcreator/recordingcreator.js index 3106b61a7d..2821946b6a 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/recordingcreator/recordingcreator.js +++ b/dashboard-ui/bower_components/emby-webcomponents/recordingcreator/recordingcreator.js @@ -119,7 +119,10 @@ apiClient.getLiveTvProgram(programId, apiClient.getCurrentUserId()).then(function (item) { - playbackManager.play(item.ChannelId, serverId); + playbackManager.play({ + ids: [item.ChannelId], + serverId: serverId + }); }); }); return; diff --git a/dashboard-ui/bower_components/emby-webcomponents/sessionplayer.js b/dashboard-ui/bower_components/emby-webcomponents/sessionplayer.js new file mode 100644 index 0000000000..82fec2c8a3 --- /dev/null +++ b/dashboard-ui/bower_components/emby-webcomponents/sessionplayer.js @@ -0,0 +1,368 @@ +define(['playbackManager', 'events', 'serverNotifications'], function (playbackManager, events, serverNotifications) { + 'use strict'; + + function sendPlayCommand(options, playType) { + + var sessionId = playbackManager.getPlayerInfo().id; + + var ids = options.ids || options.items.map(function (i) { + return i.Id; + }); + + var remoteOptions = { + ItemIds: ids.join(','), + + PlayCommand: playType + }; + + if (options.startPositionTicks) { + remoteOptions.startPositionTicks = options.startPositionTicks; + } + + return ApiClient.sendPlayCommand(sessionId, remoteOptions); + } + + function sendPlayStateCommand(command, options) { + + var sessionId = playbackManager.getPlayerInfo().id; + + ApiClient.sendPlayStateCommand(sessionId, command, options); + } + + function RemoteControlPlayer() { + + var self = this; + + self.name = 'Remote Control'; + self.type = 'mediaplayer'; + self.isLocalPlayer = false; + self.id = 'remoteplayer'; + + function sendCommandByName(name, options) { + + var command = { + Name: name + }; + + if (options) { + command.Arguments = options; + } + + self.sendCommand(command); + } + + self.sendCommand = function (command) { + + var sessionId = playbackManager.getPlayerInfo().id; + + ApiClient.sendCommand(sessionId, command); + }; + + self.play = function (options) { + + return sendPlayCommand(options, 'PlayNow'); + }; + + self.shuffle = function (id) { + + sendPlayCommand({ ids: [id] }, 'PlayShuffle'); + }; + + self.instantMix = function (id) { + + sendPlayCommand({ ids: [id] }, 'PlayInstantMix'); + }; + + self.queue = function (options) { + + sendPlayCommand(options, 'PlayNext'); + }; + + self.queueNext = function (options) { + + sendPlayCommand(options, 'PlayLast'); + }; + + self.canQueueMediaType = function (mediaType) { + + return mediaType == 'Audio' || mediaType == 'Video'; + }; + + self.stop = function () { + sendPlayStateCommand('stop'); + }; + + self.nextTrack = function () { + sendPlayStateCommand('nextTrack'); + }; + + self.previousTrack = function () { + sendPlayStateCommand('previousTrack'); + }; + + self.seek = function (positionTicks) { + sendPlayStateCommand('seek', + { + SeekPositionTicks: positionTicks + }); + }; + + self.pause = function () { + sendPlayStateCommand('Pause'); + }; + + self.unpause = function () { + sendPlayStateCommand('Unpause'); + }; + + self.setMute = function (isMuted) { + + if (isMuted) { + sendCommandByName('Mute'); + } else { + sendCommandByName('Unmute'); + } + }; + + self.toggleMute = function () { + sendCommandByName('ToggleMute'); + }; + + self.setVolume = function (vol) { + sendCommandByName('SetVolume', { + Volume: vol + }); + }; + + self.volumeUp = function () { + sendCommandByName('VolumeUp'); + }; + + self.volumeDown = function () { + sendCommandByName('VolumeDown'); + }; + + self.toggleFullscreen = function () { + sendCommandByName('ToggleFullscreen'); + }; + + self.setAudioStreamIndex = function (index) { + sendCommandByName('SetAudioStreamIndex', { + Index: index + }); + }; + + self.setSubtitleStreamIndex = function (index) { + sendCommandByName('SetSubtitleStreamIndex', { + Index: index + }); + }; + + self.setRepeatMode = function (mode) { + + sendCommandByName('SetRepeatMode', { + RepeatMode: mode + }); + }; + + self.displayContent = function (options) { + + sendCommandByName('DisplayContent', options); + }; + + self.getPlayerState = function () { + + var apiClient = window.ApiClient; + + if (apiClient) { + return apiClient.getSessions().then(function (sessions) { + + var currentTargetId = playbackManager.getPlayerInfo().id; + + // Update existing data + //updateSessionInfo(popup, msg.Data); + var session = sessions.filter(function (s) { + return s.Id == currentTargetId; + })[0]; + + if (session) { + session = getPlayerState(session); + } + + return session; + }); + } else { + return Promise.resolve({}); + } + }; + + var pollInterval; + + function onPollIntervalFired() { + + if (!ApiClient.isWebSocketOpen()) { + var apiClient = window.ApiClient; + + if (apiClient) { + apiClient.getSessions().then(processUpdatedSessions); + } + } + } + + self.subscribeToPlayerUpdates = function () { + + self.isUpdating = true; + + if (ApiClient.isWebSocketOpen()) { + + ApiClient.sendWebSocketMessage("SessionsStart", "100,800"); + } + if (pollInterval) { + clearInterval(pollInterval); + pollInterval = null; + } + pollInterval = setInterval(onPollIntervalFired, 5000); + }; + + function unsubscribeFromPlayerUpdates() { + + self.isUpdating = true; + + if (ApiClient.isWebSocketOpen()) { + + ApiClient.sendWebSocketMessage("SessionsStop"); + } + if (pollInterval) { + clearInterval(pollInterval); + pollInterval = null; + } + } + + var playerListenerCount = 0; + self.beginPlayerUpdates = function () { + + if (playerListenerCount <= 0) { + + playerListenerCount = 0; + + self.subscribeToPlayerUpdates(); + } + + playerListenerCount++; + }; + + self.endPlayerUpdates = function () { + + playerListenerCount--; + + if (playerListenerCount <= 0) { + + unsubscribeFromPlayerUpdates(); + playerListenerCount = 0; + } + }; + + self.getTargets = function () { + + var apiClient = window.ApiClient; + + var sessionQuery = { + ControllableByUserId: apiClient.getCurrentUserId() + }; + + if (apiClient) { + return apiClient.getSessions(sessionQuery).then(function (sessions) { + + return sessions.filter(function (s) { + return s.DeviceId != apiClient.deviceId(); + + }).map(function (s) { + return { + name: s.DeviceName, + deviceName: s.DeviceName, + id: s.Id, + playerName: self.name, + appName: s.Client, + playableMediaTypes: s.PlayableMediaTypes, + isLocalPlayer: false, + supportedCommands: s.SupportedCommands + }; + }); + + }); + + } else { + return Promise.resolve([]); + } + }; + + self.tryPair = function(target) { + + return Promise.resolve(); + }; + + function getPlayerState(session) { + + return session; + } + + function firePlaybackEvent(name, session) { + + events.trigger(self, name, [getPlayerState(session)]); + } + + function onWebSocketConnectionChange() { + + // Reconnect + if (self.isUpdating) { + self.subscribeToPlayerUpdates(); + } + } + + function processUpdatedSessions(sessions) { + + var currentTargetId = playbackManager.getPlayerInfo().id; + + // Update existing data + //updateSessionInfo(popup, msg.Data); + var session = sessions.filter(function (s) { + return s.Id == currentTargetId; + })[0]; + + if (session) { + firePlaybackEvent('timeupdate', session); + firePlaybackEvent('pause', session); + } + } + + events.on(serverNotifications, 'Sessions', function (e, apiClient, data) { + processUpdatedSessions(data); + }); + + events.on(serverNotifications, 'SessionEnded', function (e, apiClient, data) { + console.log("Server reports another session ended"); + + if (playbackManager.getPlayerInfo().id == data.Id) { + playbackManager.setDefaultPlayerActive(); + } + }); + + events.on(serverNotifications, 'PlaybackStart', function (e, apiClient, data) { + if (data.DeviceId != apiClient.deviceId()) { + if (playbackManager.getPlayerInfo().id == data.Id) { + firePlaybackEvent('playbackstart', data); + } + } + }); + + events.on(serverNotifications, 'PlaybackStopped', function (e, apiClient, data) { + if (data.DeviceId != apiClient.deviceId()) { + if (playbackManager.getPlayerInfo().id == data.Id) { + firePlaybackEvent('playbackstop', data); + } + } + }); + } + + return RemoteControlPlayer; +}); \ No newline at end of file diff --git a/dashboard-ui/bower_components/emby-webcomponents/shortcuts.js b/dashboard-ui/bower_components/emby-webcomponents/shortcuts.js index 885921a40f..ab8eeb330a 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/shortcuts.js +++ b/dashboard-ui/bower_components/emby-webcomponents/shortcuts.js @@ -240,7 +240,10 @@ define(['playbackManager', 'inputManager', 'connectionManager', 'embyRouter', 'g } else if (action === 'instantmix') { - playbackManager.instantMix(playableItemId, serverId); + playbackManager.instantMix({ + Id: playableItemId, + ServerId: serverId + }); } else if (action === 'play') { From 511d4882c14cb0ce573292c30e62bcdbffe3001a Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sun, 1 Jan 2017 17:40:32 -0500 Subject: [PATCH 010/102] update components --- .../emby-webcomponents/.bower.json | 8 +- .../emby-webcomponents/input/mouse.js | 117 ++++++++++++++++++ .../emby-webcomponents/strings/en-US.json | 3 +- 3 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 dashboard-ui/bower_components/emby-webcomponents/input/mouse.js diff --git a/dashboard-ui/bower_components/emby-webcomponents/.bower.json b/dashboard-ui/bower_components/emby-webcomponents/.bower.json index 8f3312d93d..a0a27568b3 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/.bower.json +++ b/dashboard-ui/bower_components/emby-webcomponents/.bower.json @@ -14,12 +14,12 @@ }, "devDependencies": {}, "ignore": [], - "version": "1.4.415", - "_release": "1.4.415", + "version": "1.4.417", + "_release": "1.4.417", "_resolution": { "type": "version", - "tag": "1.4.415", - "commit": "ef218c1a08315f961adbbc08515089198e885972" + "tag": "1.4.417", + "commit": "d03cd494c958c2fcbc45447fab8b6ab58c925cba" }, "_source": "https://github.com/MediaBrowser/emby-webcomponents.git", "_target": "^1.2.1", diff --git a/dashboard-ui/bower_components/emby-webcomponents/input/mouse.js b/dashboard-ui/bower_components/emby-webcomponents/input/mouse.js new file mode 100644 index 0000000000..2ce6ec56e7 --- /dev/null +++ b/dashboard-ui/bower_components/emby-webcomponents/input/mouse.js @@ -0,0 +1,117 @@ +define(['inputManager', 'focusManager', 'browser', 'layoutManager', 'events', 'dom'], function (inputmanager, focusManager, browser, layoutManager, events, dom) { + 'use strict'; + + var self = {}; + + var lastMouseInputTime = new Date().getTime(); + var isMouseIdle; + + function mouseIdleTime() { + return new Date().getTime() - lastMouseInputTime; + } + + function notifyApp() { + + inputmanager.notifyMouseMove(); + } + + var lastMouseMoveData; + dom.addEventListener(document, 'mousemove', function (e) { + + var eventX = e.screenX; + var eventY = e.screenY; + + // if coord don't exist how could it move + if (typeof eventX === "undefined" && typeof eventY === "undefined") { + return; + } + + var obj = lastMouseMoveData; + if (!obj) { + lastMouseMoveData = { + x: eventX, + y: eventY + }; + return; + } + + // if coord are same, it didn't move + if (Math.abs(eventX - obj.x) < 10 && Math.abs(eventY - obj.y) < 10) { + return; + } + + obj.x = eventX; + obj.y = eventY; + + lastMouseInputTime = new Date().getTime(); + notifyApp(); + + if (isMouseIdle) { + isMouseIdle = false; + document.body.classList.remove('mouseIdle'); + events.trigger(self, 'mouseactive'); + } + }, { + passive: true + }); + + function onMouseEnter(e) { + + var parent = focusManager.focusableParent(e.target); + if (parent) { + focusManager.focus(e.target); + } + } + + function enableFocusWithMouse() { + + if (!layoutManager.tv) { + return false; + } + + if (browser.xboxOne) { + return true; + } + + if (browser.ps4) { + return true; + } + + if (browser.tv) { + return true; + } + + return false; + } + + function initMouseFocus() { + + dom.removeEventListener(document, 'mouseenter', onMouseEnter, { + capture: true, + passive: true + }); + + if (enableFocusWithMouse()) { + dom.addEventListener(document, 'mouseenter', onMouseEnter, { + capture: true, + passive: true + }); + } + } + + initMouseFocus(); + + events.on(layoutManager, 'modechange', initMouseFocus); + + setInterval(function () { + + if (mouseIdleTime() >= 5000) { + isMouseIdle = true; + document.body.classList.add('mouseIdle'); + events.trigger(self, 'mouseidle'); + } + + }, 5000); + + return self; +}); \ No newline at end of file diff --git a/dashboard-ui/bower_components/emby-webcomponents/strings/en-US.json b/dashboard-ui/bower_components/emby-webcomponents/strings/en-US.json index 124c0ccc27..c8dbdcef5e 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/strings/en-US.json +++ b/dashboard-ui/bower_components/emby-webcomponents/strings/en-US.json @@ -376,5 +376,6 @@ "SyncJobItemStatusFailed": "Failed", "SyncJobItemStatusRemovedFromDevice": "Removed from device", "SyncJobItemStatusCancelled": "Cancelled", - "Retry": "Retry" + "Retry": "Retry", + "MyDevice": "My device" } \ No newline at end of file From 80b537e30b8053b2cd85fff504f1192cbc36f9d4 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sun, 1 Jan 2017 17:55:57 -0500 Subject: [PATCH 011/102] update components --- .../bower_components/emby-webcomponents/.bower.json | 8 ++++---- .../emby-webcomponents/playback/playbackmanager.js | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dashboard-ui/bower_components/emby-webcomponents/.bower.json b/dashboard-ui/bower_components/emby-webcomponents/.bower.json index a0a27568b3..29d8f16f40 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/.bower.json +++ b/dashboard-ui/bower_components/emby-webcomponents/.bower.json @@ -14,12 +14,12 @@ }, "devDependencies": {}, "ignore": [], - "version": "1.4.417", - "_release": "1.4.417", + "version": "1.4.419", + "_release": "1.4.419", "_resolution": { "type": "version", - "tag": "1.4.417", - "commit": "d03cd494c958c2fcbc45447fab8b6ab58c925cba" + "tag": "1.4.419", + "commit": "e65e31848b76b3cab4193f201517212050de8c53" }, "_source": "https://github.com/MediaBrowser/emby-webcomponents.git", "_target": "^1.2.1", diff --git a/dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js b/dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js index 893c339c4f..0db58da59b 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js +++ b/dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js @@ -748,7 +748,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.seek = function (ticks, player) { player = player || currentPlayer; - if (currentPlayer && !enableLocalPlaylistManagement(player)) { + if (player && !enableLocalPlaylistManagement(player)) { return player.seek(ticks); } @@ -1034,17 +1034,17 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.instantMix = function (item, player) { player = player || currentPlayer; - if (!enableLocalPlaylistManagement(player)) { + if (player && !enableLocalPlaylistManagement(player)) { return player.instantMix(item); } - var apiClient = connectionManager.getApiClient(serverId); + var apiClient = connectionManager.getApiClient(item.ServerId); var options = {}; options.UserId = apiClient.getCurrentUserId(); options.Fields = 'MediaSources'; - apiClient.getInstantMixFromItem(id, item).then(function (result) { + apiClient.getInstantMixFromItem(item.Id, options).then(function (result) { self.play({ items: result.Items }); @@ -1054,7 +1054,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.shuffle = function (shuffleItem, player) { player = player || currentPlayer; - if (!enableLocalPlaylistManagement(player)) { + if (player && !enableLocalPlaylistManagement(player)) { return player.shuffle(shuffleItem); } @@ -1177,7 +1177,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g self.currentTime = function (player) { player = player || currentPlayer; - if (currentPlayer && !enableLocalPlaylistManagement(player)) { + if (player && !enableLocalPlaylistManagement(player)) { return player.currentTime(); } From fd27601fd0d177d2f9947094cf022a03f66de7bd Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Sun, 1 Jan 2017 18:20:21 -0500 Subject: [PATCH 012/102] update components --- .../bower_components/emby-webcomponents/.bower.json | 8 ++++---- .../emby-webcomponents/playback/playbackmanager.js | 13 +++++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/dashboard-ui/bower_components/emby-webcomponents/.bower.json b/dashboard-ui/bower_components/emby-webcomponents/.bower.json index 29d8f16f40..d90eb6229e 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/.bower.json +++ b/dashboard-ui/bower_components/emby-webcomponents/.bower.json @@ -14,12 +14,12 @@ }, "devDependencies": {}, "ignore": [], - "version": "1.4.419", - "_release": "1.4.419", + "version": "1.4.420", + "_release": "1.4.420", "_resolution": { "type": "version", - "tag": "1.4.419", - "commit": "e65e31848b76b3cab4193f201517212050de8c53" + "tag": "1.4.420", + "commit": "f8f968afa3b3dd424bf9c59277ca70d52387471c" }, "_source": "https://github.com/MediaBrowser/emby-webcomponents.git", "_target": "^1.2.1", diff --git a/dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js b/dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js index 0db58da59b..a6dd088e1d 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js +++ b/dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js @@ -2430,6 +2430,15 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g 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); @@ -2444,6 +2453,10 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g player.currentState = {}; + if (!player.getVolume || !player.setVolume) { + initLegacyVolumeMethods(player); + } + if (enableLocalPlaylistManagement(player)) { events.on(player, 'error', onPlaybackError); } From b861fdc8162595477ee710114fc023cb8036e944 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Mon, 2 Jan 2017 01:06:05 -0500 Subject: [PATCH 013/102] update components --- .../emby-webcomponents/.bower.json | 8 +- .../emby-webcomponents/LICENSE.md | 351 ++++++++++++++- .../emby-webcomponents/browser.js | 4 + .../playback/playbackmanager.js | 8 +- .../youtubeplayer/plugin.js | 405 ++++++++++++++++++ .../youtubeplayer/style.css | 21 + 6 files changed, 770 insertions(+), 27 deletions(-) create mode 100644 dashboard-ui/bower_components/emby-webcomponents/youtubeplayer/plugin.js create mode 100644 dashboard-ui/bower_components/emby-webcomponents/youtubeplayer/style.css diff --git a/dashboard-ui/bower_components/emby-webcomponents/.bower.json b/dashboard-ui/bower_components/emby-webcomponents/.bower.json index d90eb6229e..925b3aef77 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/.bower.json +++ b/dashboard-ui/bower_components/emby-webcomponents/.bower.json @@ -14,12 +14,12 @@ }, "devDependencies": {}, "ignore": [], - "version": "1.4.420", - "_release": "1.4.420", + "version": "1.4.422", + "_release": "1.4.422", "_resolution": { "type": "version", - "tag": "1.4.420", - "commit": "f8f968afa3b3dd424bf9c59277ca70d52387471c" + "tag": "1.4.422", + "commit": "308a34c32d4476d82f2d29684132a9806ab8c34e" }, "_source": "https://github.com/MediaBrowser/emby-webcomponents.git", "_target": "^1.2.1", diff --git a/dashboard-ui/bower_components/emby-webcomponents/LICENSE.md b/dashboard-ui/bower_components/emby-webcomponents/LICENSE.md index 8f5a547ed2..4522ba0659 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/LICENSE.md +++ b/dashboard-ui/bower_components/emby-webcomponents/LICENSE.md @@ -1,22 +1,339 @@ -The MIT License +GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 -Copyright (c) Emby https://emby.media + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Preamble -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {{description}} + Copyright (C) {{year}} {{fullname}} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/dashboard-ui/bower_components/emby-webcomponents/browser.js b/dashboard-ui/bower_components/emby-webcomponents/browser.js index cacaa3a57e..02b634c323 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/browser.js +++ b/dashboard-ui/bower_components/emby-webcomponents/browser.js @@ -248,6 +248,10 @@ browser.tv = true; } + if (userAgent.toLowerCase().indexOf("embytheaterpi") !== -1) { + browser.slow = true; + } + if (isMobile(userAgent)) { browser.mobile = true; } diff --git a/dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js b/dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js index a6dd088e1d..296c063d03 100644 --- a/dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js +++ b/dashboard-ui/bower_components/emby-webcomponents/playback/playbackmanager.js @@ -500,7 +500,6 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g var itemType = item.Type; var locationType = item.LocationType; - var mediaType = item.MediaType; if (itemType === "MusicGenre" || itemType === "Season" || itemType === "Series" || itemType === "BoxSet" || itemType === "MusicAlbum" || itemType === "MusicArtist" || itemType === "Playlist") { return true; @@ -518,11 +517,8 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g } } - return getAutomaticPlayers().filter(function (p) { - - return p.canPlayMediaType(mediaType); - - }).length; + //var mediaType = item.MediaType; + return getPlayer(item, {}) != null; }; self.canQueue = function (item) { diff --git a/dashboard-ui/bower_components/emby-webcomponents/youtubeplayer/plugin.js b/dashboard-ui/bower_components/emby-webcomponents/youtubeplayer/plugin.js new file mode 100644 index 0000000000..755cc52415 --- /dev/null +++ b/dashboard-ui/bower_components/emby-webcomponents/youtubeplayer/plugin.js @@ -0,0 +1,405 @@ +define(['pluginManager', 'events', 'browser', 'embyRouter'], function (pluginManager, Events, browser, embyRouter) { + "use strict"; + + return function () { + + var self = this; + + self.name = 'Youtube Player'; + self.type = 'mediaplayer'; + self.id = 'youtubeplayer'; + + // Let any players created by plugins take priority + self.priority = 1; + + var videoDialog; + var currentSrc; + var started = false; + + var currentYoutubePlayer; + var timeUpdateInterval; + + self.canPlayMediaType = function (mediaType) { + + mediaType = (mediaType || '').toLowerCase(); + + return mediaType === 'audio' || mediaType === 'video'; + }; + + self.canPlayItem = function (item) { + + // Does not play server items + return false; + }; + + self.canPlayUrl = function (url) { + + return url.toLowerCase().indexOf('youtube.com') !== -1; + }; + + self.getDeviceProfile = function () { + + return Promise.resolve({}); + }; + + self.currentSrc = function () { + return currentSrc; + }; + + self.play = function (options) { + + started = false; + + return createMediaElement(options).then(function (elem) { + + return setCurrentSrc(elem, options); + }); + }; + + function setCurrentSrc(elem, options) { + + return new Promise(function (resolve, reject) { + + require(['queryString'], function (queryString) { + + + currentSrc = options.url; + var params = queryString.parse(options.url.split('?')[1]); + // 3. This function creates an