define(['appSettings', 'userSettings', 'appStorage', 'datetime'], function (appSettings, userSettings, appStorage, datetime) { function mediaPlayer() { var self = this; var currentProgressInterval; var currentPlaylistIndex = -1; self.currentMediaRenderer = null; self.currentItem = null; self.currentMediaSource = null; self.currentDurationTicks = null; self.startTimeTicksOffset = null; self.playlist = []; self.isLocalPlayer = true; self.isDefaultPlayer = true; self.streamInfo = {}; self.name = 'Html5 Player'; self.getTargets = function () { return new Promise(function (resolve, reject) { resolve(self.getTargetsInternal()); }); }; self.getTargetsInternal = function () { var targets = [{ name: Globalize.translate('MyDevice'), id: ConnectionManager.deviceId(), playerName: self.name, playableMediaTypes: ['Audio', 'Video'], isLocalPlayer: true, supportedCommands: Dashboard.getSupportedRemoteCommands() }]; return targets; }; var supportsTextTracks; self.supportsTextTracks = function () { if (supportsTextTracks == null) { supportsTextTracks = document.createElement('video').textTracks != null; } // For now, until ready return supportsTextTracks; }; // Returns true if the player can seek using native client-side seeking functions function canPlayerSeek() { var mediaRenderer = self.currentMediaRenderer; var currentSrc = (self.getCurrentSrc(mediaRenderer) || '').toLowerCase(); if (currentSrc.indexOf('.m3u8') != -1) { if (currentSrc.indexOf('forcelivestream=true') == -1) { return true; } } else { var duration = mediaRenderer.duration(); return duration && !isNaN(duration) && duration != Number.POSITIVE_INFINITY && duration != Number.NEGATIVE_INFINITY; } } self.getCurrentSrc = function (mediaRenderer) { return mediaRenderer.currentSrc(); }; self.getCurrentTicks = function (mediaRenderer) { var playerTime = Math.floor(10000 * (mediaRenderer || self.currentMediaRenderer).currentTime()); playerTime += self.startTimeTicksOffset; return playerTime; }; self.playNextAfterEnded = function () { console.log('playNextAfterEnded'); Events.off(this, 'ended', self.playNextAfterEnded); self.nextTrack(); }; self.startProgressInterval = function () { clearProgressInterval(); var intervalTime = ApiClient.isWebSocketOpen() ? 1200 : 5000; // Ease up with safari because it doesn't perform as well if (browserInfo.safari) { intervalTime = Math.max(intervalTime, 5000); } self.lastProgressReport = 0; currentProgressInterval = setInterval(function () { if (self.currentMediaRenderer) { if ((new Date().getTime() - self.lastProgressReport) > intervalTime) { self.lastProgressReport = new Date().getTime(); sendProgressUpdate(); } } }, 250); }; self.getCurrentMediaExtension = function (currentSrc) { currentSrc = currentSrc.split('?')[0]; return currentSrc.substring(currentSrc.lastIndexOf('.')); }; self.canPlayNativeHls = function () { if (AppInfo.isNativeApp) { return true; } var media = document.createElement('video'); if (media.canPlayType('application/x-mpegURL').replace(/no/, '') || media.canPlayType('application/vnd.apple.mpegURL').replace(/no/, '')) { return true; } return false; }; self.canPlayHls = function () { if (self.canPlayNativeHls()) { return true; } // viblast can help us here //return true; return window.MediaSource && !browserInfo.firefox; }; self.changeStream = function (ticks, params) { var mediaRenderer = self.currentMediaRenderer; if (canPlayerSeek() && params == null) { mediaRenderer.currentTime(ticks / 10000); return; } params = params || {}; var currentSrc = mediaRenderer.currentSrc(); var playSessionId = getParameterByName('PlaySessionId', currentSrc); var liveStreamId = getParameterByName('LiveStreamId', currentSrc); Dashboard.getDeviceProfile().then(function (deviceProfile) { var audioStreamIndex = params.AudioStreamIndex == null ? (getParameterByName('AudioStreamIndex', currentSrc) || null) : params.AudioStreamIndex; if (typeof (audioStreamIndex) == 'string') { audioStreamIndex = parseInt(audioStreamIndex); } var subtitleStreamIndex = params.SubtitleStreamIndex == null ? (getParameterByName('SubtitleStreamIndex', currentSrc) || null) : params.SubtitleStreamIndex; if (typeof (subtitleStreamIndex) == 'string') { subtitleStreamIndex = parseInt(subtitleStreamIndex); } MediaController.getPlaybackInfo(self.currentItem.Id, deviceProfile, ticks, self.currentMediaSource, audioStreamIndex, subtitleStreamIndex, liveStreamId).then(function (result) { if (validatePlaybackInfoResult(result)) { self.currentMediaSource = result.MediaSources[0]; self.createStreamInfo(self.currentItem.MediaType, self.currentItem, self.currentMediaSource, ticks).then(function (streamInfo) { if (!streamInfo.url) { MediaController.showPlaybackInfoErrorMessage('NoCompatibleStream'); self.stop(); return; } self.currentSubtitleStreamIndex = subtitleStreamIndex; changeStreamToUrl(mediaRenderer, playSessionId, streamInfo); }); } }); }); }; function changeStreamToUrl(mediaRenderer, playSessionId, streamInfo) { clearProgressInterval(); Events.off(mediaRenderer, 'ended', self.onPlaybackStopped); Events.off(mediaRenderer, 'ended', self.playNextAfterEnded); function onPlayingOnce() { Events.off(mediaRenderer, "play", onPlayingOnce); Events.on(mediaRenderer, 'ended', self.onPlaybackStopped); Events.on(mediaRenderer, 'ended', self.playNextAfterEnded); self.startProgressInterval(); sendProgressUpdate(); } Events.on(mediaRenderer, "play", onPlayingOnce); if (self.currentItem.MediaType == "Video") { ApiClient.stopActiveEncodings(playSessionId).then(function () { self.setSrcIntoRenderer(mediaRenderer, streamInfo, self.currentItem, self.currentMediaSource); }); } else { self.setSrcIntoRenderer(mediaRenderer, streamInfo, self.currentItem, self.currentMediaSource); } } self.setSrcIntoRenderer = function (mediaRenderer, streamInfo, item, 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 }); } self.startTimeTicksOffset = streamInfo.startTimeTicksOffset || 0; mediaRenderer.setCurrentSrc(streamInfo, item, mediaSource, tracks); self.streamInfo = streamInfo; //self.updateTextStreamUrls(streamInfo.startTimeTicksOffset || 0); }; self.setCurrentTime = function (ticks, positionSlider, currentTimeElement) { // Convert to ticks ticks = Math.floor(ticks); var timeText = datetime.getDisplayRunningTime(ticks); var mediaRenderer = self.currentMediaRenderer; if (self.currentDurationTicks) { timeText += " / " + datetime.getDisplayRunningTime(self.currentDurationTicks); if (positionSlider) { var percent = ticks / self.currentDurationTicks; percent *= 100; positionSlider.value = percent; } } if (positionSlider) { positionSlider.disabled = !((self.currentDurationTicks || 0) > 0 || canPlayerSeek()); } if (currentTimeElement) { currentTimeElement.innerHTML = timeText; } var state = self.getPlayerStateInternal(mediaRenderer, self.currentItem, self.currentMediaSource); Events.trigger(self, 'positionchange', [state]); }; self.canQueueMediaType = function (mediaType) { return self.currentItem && self.currentItem.MediaType == mediaType; }; function translateItemsForPlayback(items, smart) { var firstItem = items[0]; var promise; if (firstItem.Type == "Playlist") { promise = self.getItemsForPlayback({ ParentId: firstItem.Id, }); } else if (firstItem.Type == "MusicArtist") { promise = self.getItemsForPlayback({ ArtistIds: firstItem.Id, Filters: "IsNotFolder", Recursive: true, SortBy: "SortName", MediaTypes: "Audio" }); } else if (firstItem.Type == "MusicGenre") { promise = self.getItemsForPlayback({ Genres: firstItem.Name, Filters: "IsNotFolder", Recursive: true, SortBy: "SortName", MediaTypes: "Audio" }); } else if (firstItem.IsFolder) { promise = self.getItemsForPlayback({ ParentId: firstItem.Id, Filters: "IsNotFolder", Recursive: true, SortBy: "SortName", MediaTypes: "Audio,Video" }); } else if (smart && firstItem.Type == "Episode" && items.length == 1) { promise = ApiClient.getCurrentUser().then(function (user) { if (!user.Configuration.EnableNextEpisodeAutoPlay) { return null; } return ApiClient.getEpisodes(firstItem.SeriesId, { IsVirtualUnaired: false, IsMissing: false, UserId: ApiClient.getCurrentUserId(), Fields: getItemFields }).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; return episodesResult; }); }); } if (promise) { return promise.then(function (result) { return result ? result.Items : items; }); } else { return Promise.resolve(items); } } self.play = function (options) { Dashboard.showLoadingMsg(); Dashboard.getCurrentUser().then(function (user) { if (options.items) { translateItemsForPlayback(options.items, true).then(function (items) { self.playWithIntros(items, options, user); }); } else { self.getItemsForPlayback({ Ids: options.ids.join(',') }).then(function (result) { translateItemsForPlayback(result.Items, true).then(function (items) { self.playWithIntros(items, options, user); }); }); } }); }; self.playWithIntros = function (items, options, user) { var firstItem = items[0]; if (firstItem.MediaType === "Video") { Dashboard.showLoadingMsg(); } if (options.startPositionTicks || firstItem.MediaType !== 'Video' || !userSettings.enableCinemaMode()) { self.playInternal(firstItem, options.startPositionTicks, function () { self.setPlaylistState(0, items); }); return; } ApiClient.getJSON(ApiClient.getUrl('Users/' + user.Id + '/Items/' + firstItem.Id + '/Intros')).then(function (intros) { items = intros.Items.concat(items); self.playInternal(items[0], options.startPositionTicks, function () { self.setPlaylistState(0, items); }); }); }; function getOptimalMediaSource(mediaType, versions) { var promises = versions.map(function (v) { return MediaController.supportsDirectPlay(v); }); return Promise.all(promises).then(function (responses) { for (var i = 0, length = versions.length; i < length; i++) { versions[i].enableDirectPlay = responses[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; }); } self.createStreamInfo = function (type, item, mediaSource, startPosition) { return new Promise(function (resolve, reject) { var mediaUrl; var contentType; var startTimeTicksOffset = 0; var startPositionInSeekParam = startPosition ? (startPosition / 10000000) : 0; var seekParam = startPositionInSeekParam ? '#t=' + startPositionInSeekParam : ''; var playMethod = 'Transcode'; if (type == 'Video') { contentType = 'video/' + mediaSource.Container; if (mediaSource.enableDirectPlay) { mediaUrl = mediaSource.Path; playMethod = 'DirectPlay'; } else { if (mediaSource.SupportsDirectStream) { var directOptions = { Static: true, mediaSourceId: mediaSource.Id, deviceId: ApiClient.deviceId(), api_key: ApiClient.accessToken() }; if (mediaSource.LiveStreamId) { directOptions.LiveStreamId = mediaSource.LiveStreamId; } mediaUrl = ApiClient.getUrl('Videos/' + item.Id + '/stream.' + mediaSource.Container, directOptions); mediaUrl += seekParam; playMethod = 'DirectStream'; } else if (mediaSource.SupportsTranscoding) { mediaUrl = ApiClient.getUrl(mediaSource.TranscodingUrl); if (mediaSource.TranscodingSubProtocol == 'hls') { if (mediaUrl.toLowerCase().indexOf('forcelivestream=true') != -1) { startPositionInSeekParam = 0; startTimeTicksOffset = startPosition || 0; } contentType = 'application/x-mpegURL'; } else { if (mediaUrl.toLowerCase().indexOf('copytimestamps=true') == -1) { startPositionInSeekParam = 0; startTimeTicksOffset = startPosition || 0; } contentType = 'video/' + mediaSource.TranscodingContainer; } } } } else { contentType = 'audio/' + mediaSource.Container; if (mediaSource.enableDirectPlay) { mediaUrl = mediaSource.Path; playMethod = 'DirectPlay'; } else { var isDirectStream = mediaSource.SupportsDirectStream; if (isDirectStream) { var outputContainer = (mediaSource.Container || '').toLowerCase(); var directOptions = { Static: true, mediaSourceId: mediaSource.Id, deviceId: ApiClient.deviceId(), api_key: ApiClient.accessToken() }; if (mediaSource.LiveStreamId) { directOptions.LiveStreamId = mediaSource.LiveStreamId; } mediaUrl = ApiClient.getUrl('Audio/' + item.Id + '/stream.' + outputContainer, directOptions); mediaUrl += seekParam; playMethod = 'DirectStream'; } else if (mediaSource.SupportsTranscoding) { mediaUrl = ApiClient.getUrl(mediaSource.TranscodingUrl); if (mediaSource.TranscodingSubProtocol == 'hls') { mediaUrl += seekParam; contentType = 'application/x-mpegURL'; } else { startTimeTicksOffset = startPosition || 0; contentType = 'audio/' + mediaSource.TranscodingContainer; } } } } var resultInfo = { url: mediaUrl, mimeType: contentType, startTimeTicksOffset: startTimeTicksOffset, startPositionInSeekParam: startPositionInSeekParam, playMethod: playMethod }; if (playMethod == 'DirectPlay' && mediaSource.Protocol == 'File') { require(['localassetmanager'], function () { LocalAssetManager.translateFilePath(resultInfo.url).then(function (path) { resultInfo.url = path; console.log('LocalAssetManager.translateFilePath: path: ' + resultInfo.url + ' result: ' + path); resolve(resultInfo); }); }); } else { resolve(resultInfo); } }); }; self.lastBitrateDetections = {}; self.playInternal = function (item, startPosition, callback) { if (item == null) { throw new Error("item cannot be null"); } if (self.isPlaying()) { self.stop(false); } if (item.MediaType !== 'Audio' && item.MediaType !== 'Video') { throw new Error("Unrecognized media type"); } if (item.IsPlaceHolder) { Dashboard.hideLoadingMsg(); MediaController.showPlaybackInfoErrorMessage('PlaceHolder'); return; } var onBitrateDetected = function () { Dashboard.getDeviceProfile().then(function (deviceProfile) { playOnDeviceProfileCreated(deviceProfile, item, startPosition, callback); }); }; var bitrateDetectionKey = ApiClient.serverAddress(); if (item.MediaType == 'Video' && appSettings.enableAutomaticBitrateDetection() && (new Date().getTime() - (self.lastBitrateDetections[bitrateDetectionKey] || 0)) > 300000) { Dashboard.showLoadingMsg(); ApiClient.detectBitrate().then(function (bitrate) { console.log('Max bitrate auto detected to ' + bitrate); self.lastBitrateDetections[bitrateDetectionKey] = new Date().getTime(); appSettings.maxStreamingBitrate(bitrate); onBitrateDetected(); }, onBitrateDetected); } else { onBitrateDetected(); } }; self.tryStartPlayback = function (deviceProfile, item, startPosition, callback) { if (item.MediaType === "Video") { Dashboard.showLoadingMsg(); } MediaController.getPlaybackInfo(item.Id, deviceProfile, startPosition).then(function (playbackInfoResult) { if (validatePlaybackInfoResult(playbackInfoResult)) { getOptimalMediaSource(item.MediaType, playbackInfoResult.MediaSources).then(function (mediaSource) { if (mediaSource) { if (mediaSource.RequiresOpening) { MediaController.getLiveStream(item.Id, playbackInfoResult.PlaySessionId, deviceProfile, startPosition, mediaSource, null, null).then(function (openLiveStreamResult) { MediaController.supportsDirectPlay(openLiveStreamResult.MediaSource).then(function (result) { openLiveStreamResult.MediaSource.enableDirectPlay = result; callback(openLiveStreamResult.MediaSource); }); }); } else { callback(mediaSource); } } else { Dashboard.hideLoadingMsg(); MediaController.showPlaybackInfoErrorMessage('NoCompatibleStream'); } }); } }); }; function playOnDeviceProfileCreated(deviceProfile, item, startPosition, callback) { self.tryStartPlayback(deviceProfile, item, startPosition, function (mediaSource) { playInternalPostMediaSourceSelection(item, mediaSource, startPosition, callback); }); } function playInternalPostMediaSourceSelection(item, mediaSource, startPosition, callback) { Dashboard.hideLoadingMsg(); self.currentMediaSource = mediaSource; self.currentItem = item; if (item.MediaType === "Video") { requirejs(['videorenderer', 'scripts/mediaplayer-video'], function () { self.playVideo(item, self.currentMediaSource, startPosition, callback); }); } else if (item.MediaType === "Audio") { playAudio(item, self.currentMediaSource, startPosition, callback); } } function validatePlaybackInfoResult(result) { if (result.ErrorCode) { MediaController.showPlaybackInfoErrorMessage(result.ErrorCode); return false; } return true; } self.getPosterUrl = function (item) { var screenWidth = Math.max(screen.height, screen.width); if (item.BackdropImageTags && item.BackdropImageTags.length) { return ApiClient.getScaledImageUrl(item.Id, { type: "Backdrop", index: 0, maxWidth: screenWidth, tag: item.BackdropImageTags[0] }); } else if (item.ParentBackdropItemId && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) { return ApiClient.getScaledImageUrl(item.ParentBackdropItemId, { type: 'Backdrop', index: 0, maxWidth: screenWidth, tag: item.ParentBackdropImageTags[0] }); } return null; }; self.displayContent = function (options) { // Handle it the same as a remote control command Dashboard.onBrowseCommand(options); }; self.getItemsForPlayback = function (query) { var userId = Dashboard.getCurrentUserId(); if (query.Ids && query.Ids.split(',').length == 1) { return new Promise(function (resolve, reject) { ApiClient.getItem(userId, query.Ids.split(',')).then(function (item) { resolve({ Items: [item], TotalRecordCount: 1 }); }); }); } else { query.Limit = query.Limit || 100; query.Fields = getItemFields; query.ExcludeLocationTypes = "Virtual"; return ApiClient.getItems(userId, query); } }; self.removeFromPlaylist = function (index) { self.playlist.remove(index); }; // Gets or sets the current playlist index self.currentPlaylistIndex = function (i) { if (i == null) { return currentPlaylistIndex; } var newItem = self.playlist[i]; self.playInternal(newItem, 0, function () { self.setPlaylistState(i); }); }; // Set currentPlaylistIndex and playlist. Using a method allows for overloading in derived player implementations self.setPlaylistState = function (i, items) { if (!isNaN(i)) { currentPlaylistIndex = i; } if (items) { self.playlist = items; } if (self.updatePlaylistUi) { self.updatePlaylistUi(); } }; self.nextTrack = function () { var newIndex; switch (self.getRepeatMode()) { case 'RepeatOne': newIndex = currentPlaylistIndex; break; case 'RepeatAll': newIndex = currentPlaylistIndex + 1; if (newIndex >= self.playlist.length) { newIndex = 0; } break; default: newIndex = currentPlaylistIndex + 1; break; } var newItem = self.playlist[newIndex]; if (newItem) { console.log('playing next track'); self.playInternal(newItem, 0, function () { self.setPlaylistState(newIndex); }); } }; self.previousTrack = function () { var newIndex = currentPlaylistIndex - 1; if (newIndex >= 0) { var newItem = self.playlist[newIndex]; if (newItem) { self.playInternal(newItem, 0, function () { self.setPlaylistState(newIndex); }); } } }; self.queueItemsNext = function (items) { var insertIndex = 1; for (var i = 0, length = items.length; i < length; i++) { self.playlist.splice(insertIndex, 0, items[i]); insertIndex++; } }; self.queueItems = function (items) { for (var i = 0, length = items.length; i < length; i++) { self.playlist.push(items[i]); } }; self.queue = function (options) { if (!self.playlist.length) { self.play(options); return; } Dashboard.getCurrentUser().then(function (user) { if (options.items) { translateItemsForPlayback(options.items).then(function (items) { self.queueItems(items); }); } else { self.getItemsForPlayback({ Ids: options.ids.join(',') }).then(function (result) { translateItemsForPlayback(result.Items).then(function (items) { self.queueItems(items); }); }); } }); }; self.queueNext = function (options) { if (!self.playlist.length) { self.play(options); return; } Dashboard.getCurrentUser().then(function (user) { if (options.items) { self.queueItemsNext(options.items); } else { self.getItemsForPlayback({ Ids: options.ids.join(',') }).then(function (result) { options.items = result.Items; self.queueItemsNext(options.items); }); } }); }; self.pause = function () { self.currentMediaRenderer.pause(); }; self.unpause = function () { self.currentMediaRenderer.unpause(); }; self.seek = function (position) { self.changeStream(position); }; self.mute = function () { self.setVolume(0); }; self.unMute = function () { self.setVolume(self.getSavedVolume() * 100); }; self.volume = function () { return self.currentMediaRenderer.volume() * 100; }; self.toggleMute = function () { if (self.currentMediaRenderer) { console.log('MediaPlayer toggling mute'); if (self.volume()) { self.mute(); } else { self.unMute(); } } }; self.volumeDown = function () { if (self.currentMediaRenderer) { self.setVolume(Math.max(self.volume() - 2, 0)); } }; self.volumeUp = function () { if (self.currentMediaRenderer) { self.setVolume(Math.min(self.volume() + 2, 100)); } }; // Sets volume using a 0-100 scale self.setVolume = function (val) { if (self.currentMediaRenderer) { console.log('MediaPlayer setting volume to ' + val); self.currentMediaRenderer.volume(val / 100); self.onVolumeChanged(self.currentMediaRenderer); //self.saveVolume(); } }; self.saveVolume = function (val) { if (val) { appStorage.setItem("volume", val); } }; self.getSavedVolume = function () { return appStorage.getItem("volume") || 0.5; }; self.shuffle = function (id) { var userId = Dashboard.getCurrentUserId(); ApiClient.getItem(userId, id).then(function (item) { var query = { UserId: userId, Fields: getItemFields, 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; } self.getItemsForPlayback(query).then(function (result) { self.play({ items: result.Items }); }); }); }; self.instantMix = function (id) { var itemLimit = 100; ApiClient.getInstantMixFromItem(id, { UserId: Dashboard.getCurrentUserId(), Fields: getItemFields, Limit: itemLimit }).then(function (result) { self.play({ items: result.Items }); }); }; self.stop = function (destroyRenderer) { var mediaRenderer = self.currentMediaRenderer; if (mediaRenderer) { Events.off(mediaRenderer, 'ended', self.playNextAfterEnded); var stopTranscoding = false; if (!currentProgressInterval) { stopTranscoding = true; } mediaRenderer.stop(); Events.trigger(mediaRenderer, "ended"); //self.onPlaybackStopped.call(mediaRenderer); // TODO: Unbind video events unBindAudioEvents(mediaRenderer); mediaRenderer.cleanup(destroyRenderer); self.currentMediaRenderer = null; self.currentItem = null; self.currentSubtitleStreamIndex = null; self.streamInfo = {}; self.currentMediaSource = null; if (stopTranscoding) { ApiClient.stopActiveEncodings(); } } else { self.currentMediaRenderer = null; self.currentItem = null; self.currentMediaSource = null; self.currentSubtitleStreamIndex = null; self.streamInfo = {}; } if (self.resetEnhancements) { self.resetEnhancements(); } }; function unBindAudioEvents(mediaRenderer) { Events.off(mediaRenderer, "volumechange", onVolumeChange); Events.off(mediaRenderer, "pause", onPause); Events.off(mediaRenderer, "playing", onPlaying); Events.off(mediaRenderer, "timeupdate", onTimeUpdate); } self.isPlaying = function () { return self.playlist.length > 0; }; self.getPlayerState = function () { return new Promise(function (resolve, reject) { var result = self.getPlayerStateInternal(self.currentMediaRenderer, self.currentItem, self.currentMediaSource); resolve(result); }); }; self.getPlayerStateInternal = function (mediaRenderer, item, mediaSource) { var state = { PlayState: {} }; if (mediaRenderer) { state.PlayState.VolumeLevel = mediaRenderer.volume() * 100; state.PlayState.IsMuted = mediaRenderer.volume() == 0; state.PlayState.IsPaused = mediaRenderer.paused(); state.PlayState.PositionTicks = self.getCurrentTicks(mediaRenderer); state.PlayState.RepeatMode = self.getRepeatMode(); var currentSrc = mediaRenderer.currentSrc(); if (currentSrc) { var audioStreamIndex = getParameterByName('AudioStreamIndex', currentSrc); if (audioStreamIndex) { state.PlayState.AudioStreamIndex = parseInt(audioStreamIndex); } state.PlayState.SubtitleStreamIndex = self.currentSubtitleStreamIndex; state.PlayState.PlayMethod = self.streamInfo.playMethod; state.PlayState.PlaySessionId = getParameterByName('PlaySessionId', currentSrc); } } if (mediaSource) { state.PlayState.MediaSourceId = mediaSource.Id; state.PlayState.LiveStreamId = mediaSource.LiveStreamId; state.NowPlayingItem = { RunTimeTicks: mediaSource.RunTimeTicks }; state.PlayState.CanSeek = (mediaSource.RunTimeTicks || 0) > 0 || canPlayerSeek(); } if (item) { state.NowPlayingItem = self.getNowPlayingItemForReporting(item, mediaSource); } return state; }; self.getNowPlayingItemForReporting = function (item, mediaSource) { var nowPlayingItem = {}; nowPlayingItem.RunTimeTicks = mediaSource.RunTimeTicks; 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.Artists; 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; }; self.beginPlayerUpdates = function () { // Nothing to setup here }; self.endPlayerUpdates = function () { // Nothing to setup here }; self.onBeforePlaybackStart = function (mediaRenderer, item, mediaSource) { var state = self.getPlayerStateInternal(mediaRenderer, item, mediaSource); Events.trigger(self, 'beforeplaybackstart', [state]); }; self.onPlaybackStart = function (mediaRenderer, item, mediaSource) { var state = self.getPlayerStateInternal(mediaRenderer, item, mediaSource); Events.trigger(self, 'playbackstart', [state]); self.startProgressInterval(); }; self.onVolumeChanged = function (mediaRenderer) { self.saveVolume(mediaRenderer.volume()); var state = self.getPlayerStateInternal(mediaRenderer, self.currentItem, self.currentMediaSource); Events.trigger(self, 'volumechange', [state]); }; self.cleanup = function () { }; self.onPlaybackStopped = function () { console.log('playback stopped'); document.body.classList.remove('bodyWithPopupOpen'); var mediaRenderer = this; // TODO: Unbind other events unBindAudioEvents(mediaRenderer); Events.off(mediaRenderer, 'ended', self.onPlaybackStopped); var item = self.currentItem; var mediaSource = self.currentMediaSource; var state = self.getPlayerStateInternal(mediaRenderer, item, mediaSource); self.cleanup(mediaRenderer); clearProgressInterval(); if (item.MediaType == "Video") { self.resetEnhancements(); } Events.trigger(self, 'playbackstop', [state]); }; self.onPlaystateChange = function (mediaRenderer) { console.log('mediaplayer onPlaystateChange'); var state = self.getPlayerStateInternal(mediaRenderer, self.currentItem, self.currentMediaSource); Events.trigger(self, 'playstatechange', [state]); }; function onAppClose() { // Try to report playback stopped before the browser closes if (self.currentItem && self.currentMediaRenderer) { if (currentProgressInterval) { self.onPlaybackStopped.call(self.currentMediaRenderer); } else { ApiClient.stopActiveEncodings(); } } } window.addEventListener("beforeunload", onAppClose); //if (browserInfo.safari) { // document.addEventListener("pause", onAppClose); //} function sendProgressUpdate() { var mediaRenderer = self.currentMediaRenderer; if (mediaRenderer.enableProgressReporting === false) { return; } var state = self.getPlayerStateInternal(mediaRenderer, self.currentItem, self.currentMediaSource); var info = { QueueableMediaTypes: state.NowPlayingItem.MediaType, ItemId: state.NowPlayingItem.Id, NowPlayingItem: state.NowPlayingItem }; info = Object.assign(info, state.PlayState); //console.log(JSON.stringify(info)); ApiClient.reportPlaybackProgress(info); } function clearProgressInterval() { if (currentProgressInterval) { clearTimeout(currentProgressInterval); currentProgressInterval = null; } } self.canAutoPlayAudio = function () { if (AppInfo.isNativeApp) { return true; } if (browserInfo.mobile) { return false; } return true; }; var repeatMode = 'RepeatNone'; self.getRepeatMode = function () { return repeatMode; }; self.setRepeatMode = function (mode) { repeatMode = mode; }; function onTimeUpdate() { var currentTicks = self.getCurrentTicks(this); self.setCurrentTime(currentTicks); } function playAudio(item, mediaSource, startPositionTicks, callback) { requirejs(['audiorenderer'], function () { playAudioInternal(item, mediaSource, startPositionTicks); if (callback) { callback(); } }); } function playAudioInternal(item, mediaSource, startPositionTicks) { self.createStreamInfo('Audio', item, mediaSource, startPositionTicks).then(function (streamInfo) { self.startTimeTicksOffset = streamInfo.startTimeTicksOffset; var initialVolume = self.getSavedVolume(); var mediaRenderer = new AudioRenderer({ poster: self.getPosterUrl(item) }); function onPlayingOnce() { Events.off(mediaRenderer, "playing", onPlayingOnce); console.log('audio element event: playing'); // For some reason this is firing at the start, so don't bind until playback has begun Events.on(mediaRenderer, 'ended', self.onPlaybackStopped); Events.on(mediaRenderer, 'ended', self.playNextAfterEnded); self.onPlaybackStart(mediaRenderer, item, mediaSource); } Events.on(mediaRenderer, "volumechange", onVolumeChange); Events.on(mediaRenderer, "playing", onPlayingOnce); Events.on(mediaRenderer, "pause", onPause); Events.on(mediaRenderer, "playing", onPlaying); Events.on(mediaRenderer, "timeupdate", onTimeUpdate); self.currentMediaRenderer = mediaRenderer; self.currentDurationTicks = self.currentMediaSource.RunTimeTicks; mediaRenderer.init().then(function () { // Set volume first to avoid an audible change mediaRenderer.volume(initialVolume); self.onBeforePlaybackStart(mediaRenderer, item, mediaSource); mediaRenderer.setCurrentSrc(streamInfo, item, mediaSource); self.streamInfo = streamInfo; }); }); } function onVolumeChange() { console.log('audio element event: pause'); self.onPlaystateChange(this); // In the event timeupdate isn't firing, at least we can update when this happens self.setCurrentTime(self.getCurrentTicks()); } function onPause() { console.log('audio element event: pause'); self.onPlaystateChange(this); // In the event timeupdate isn't firing, at least we can update when this happens self.setCurrentTime(self.getCurrentTicks()); } function onPlaying() { console.log('audio element event: playing'); self.onPlaystateChange(this); // In the event timeupdate isn't firing, at least we can update when this happens self.setCurrentTime(self.getCurrentTicks()); } var getItemFields = "MediaSources,Chapters"; self.tryPair = function (target) { return new Promise(function (resolve, reject) { resolve(); }); }; } window.MediaPlayer = new mediaPlayer(); window.MediaPlayer.init = function () { window.MediaController.registerPlayer(window.MediaPlayer); window.MediaController.setActivePlayer(window.MediaPlayer, window.MediaPlayer.getTargetsInternal()[0]); }; });