diff --git a/dashboard-ui/apiclient/connectionmanager.js b/dashboard-ui/apiclient/connectionmanager.js index c16bd64caa..924bfb112a 100644 --- a/dashboard-ui/apiclient/connectionmanager.js +++ b/dashboard-ui/apiclient/connectionmanager.js @@ -130,7 +130,7 @@ var servers = credentialProvider.credentials().Servers; - return servers.filter(function () { + return servers.filter(function (s) { return s.Id == id; diff --git a/dashboard-ui/apiclient/sync/contentuploader.js b/dashboard-ui/apiclient/sync/contentuploader.js index e1aeddf69b..aa1a6c45b7 100644 --- a/dashboard-ui/apiclient/sync/contentuploader.js +++ b/dashboard-ui/apiclient/sync/contentuploader.js @@ -2,11 +2,13 @@ function contentUploader(connectionManager) { + var self = this; + self.uploadImages = function (server) { var deferred = DeferredBuilder.Deferred(); - var apiClient = self.getApiClient(server.Id); + var apiClient = connectionManager.getApiClient(server.Id); apiClient.getDevicesOptions().done(function (devicesOptions) { diff --git a/dashboard-ui/apiclient/sync/mediasync.js b/dashboard-ui/apiclient/sync/mediasync.js new file mode 100644 index 0000000000..96ebde6c36 --- /dev/null +++ b/dashboard-ui/apiclient/sync/mediasync.js @@ -0,0 +1,23 @@ +(function (globalScope) { + + function mediaSync() { + + var self = this; + + self.sync = function (apiClient) { + + var deferred = DeferredBuilder.Deferred(); + + deferred.resolve(); + + return deferred.promise(); + }; + } + + if (!globalScope.MediaBrowser) { + globalScope.MediaBrowser = {}; + } + + globalScope.MediaBrowser.MediaSync = mediaSync; + +})(this); \ No newline at end of file diff --git a/dashboard-ui/apiclient/sync/multiserversync.js b/dashboard-ui/apiclient/sync/multiserversync.js index 16ec513f39..0747229fdb 100644 --- a/dashboard-ui/apiclient/sync/multiserversync.js +++ b/dashboard-ui/apiclient/sync/multiserversync.js @@ -2,6 +2,8 @@ function multiServerSync(connectionManager) { + var self = this; + self.sync = function () { var deferred = DeferredBuilder.Deferred(); @@ -23,11 +25,6 @@ return; } - if (progress.isCancelled) { - deferred.reject(); - return; - } - var server = servers[index]; // get fresh info from connection manager diff --git a/dashboard-ui/apiclient/sync/offlineusersync.js b/dashboard-ui/apiclient/sync/offlineusersync.js new file mode 100644 index 0000000000..2db47b06ea --- /dev/null +++ b/dashboard-ui/apiclient/sync/offlineusersync.js @@ -0,0 +1,23 @@ +(function (globalScope) { + + function offlineUserSync() { + + var self = this; + + self.sync = function (apiClient) { + + var deferred = DeferredBuilder.Deferred(); + + deferred.resolve(); + + return deferred.promise(); + }; + } + + if (!globalScope.MediaBrowser) { + globalScope.MediaBrowser = {}; + } + + globalScope.MediaBrowser.OfflineUserSync = offlineUserSync; + +})(this); \ No newline at end of file diff --git a/dashboard-ui/apiclient/sync/serversync.js b/dashboard-ui/apiclient/sync/serversync.js index 9352f416e6..40016a12b6 100644 --- a/dashboard-ui/apiclient/sync/serversync.js +++ b/dashboard-ui/apiclient/sync/serversync.js @@ -2,6 +2,8 @@ function serverSync(connectionManager) { + var self = this; + self.sync = function (server) { var deferred = DeferredBuilder.Deferred(); @@ -45,11 +47,57 @@ new MediaBrowser.ContentUploader(connectionManager).uploadImages(server).done(function () { + Logger.log("ContentUploaded succeeded to server: " + server.Id); + + syncOfflineUsers(server, deferred); + + }).fail(function () { + + Logger.log("ContentUploaded failed to server: " + server.Id); + + syncOfflineUsers(server, deferred); + }); + }); + } + + function syncOfflineUsers(server, deferred) { + + require(['offlineusersync'], function () { + + var apiClient = connectionManager.getApiClient(server.Id); + + new MediaBrowser.OfflineUserSync().sync(apiClient).done(function () { + + Logger.log("OfflineUserSync succeeded to server: " + server.Id); + + syncMedia(server, deferred); + + }).fail(function () { + + Logger.log("OfflineUserSync failed to server: " + server.Id); + + deferred.reject(); + }); + }); + } + + function syncMedia(server, deferred) { + + require(['mediasync'], function () { + + var apiClient = connectionManager.getApiClient(server.Id); + + new MediaBrowser.MediaSync().sync(apiClient).done(function () { + + Logger.log("MediaSync succeeded to server: " + server.Id); + deferred.resolve(); }).fail(function () { - deferred.resolve(); + Logger.log("MediaSync failed to server: " + server.Id); + + deferred.reject(); }); }); } diff --git a/dashboard-ui/cordova/ios/vlcplayer.js b/dashboard-ui/cordova/ios/vlcplayer.js new file mode 100644 index 0000000000..97679269c8 --- /dev/null +++ b/dashboard-ui/cordova/ios/vlcplayer.js @@ -0,0 +1,262 @@ +(function () { + + function vlcRenderer(options) { + + var self = this; + + self.enableProgressReporting = options.type == 'audio'; + + function onEnded() { + Events.trigger(self, 'ended'); + } + + function onTimeUpdate() { + Events.trigger(self, 'timeupdate'); + } + + function onVolumeChange() { + Events.trigger(self, 'volumechange'); + } + + function onPlaying() { + Events.trigger(self, 'playing'); + } + + function onPlay() { + Events.trigger(self, 'play'); + } + + function onPause() { + Events.trigger(self, 'pause'); + } + + function onClick() { + Events.trigger(self, 'click'); + } + + function onDblClick() { + Events.trigger(self, 'dblclick'); + } + + function onError() { + + var errorCode = this.error ? this.error.code : ''; + Logger.log('Media element error code: ' + errorCode); + + Events.trigger(self, 'error'); + } + + var playerState = {}; + + self.currentTime = function (val) { + + if (val != null) { + window.audioplayer.seekto(function () { + Logger.log('set currentTime succeeded!'); + }, function () { + Logger.log('set currentTime failed!'); + + }, val / 1000); + return; + } + + return playerState.currentTime; + }; + + self.duration = function (val) { + + if (playerState) { + return playerState.duration; + } + + return null; + }; + + self.stop = function () { + window.audioplayer.stop(function (result) { + Logger.log('Stop succeeded!'); + reportEvent('playbackstop', result); + + }, function () { + Logger.log('Stop failed!'); + }); + }; + + self.pause = function () { + window.audioplayer.pause(function (result) { + Logger.log('Pause succeeded!'); + reportEvent('sepaused', result); + + }, function () { + Logger.log('Pause failed!'); + }); + }; + + self.unpause = function () { + window.audioplayer.pause(function (result) { + Logger.log('Unpause succeeded!'); + reportEvent('playing', result); + + }, function () { + Logger.log('Unpause failed!'); + }); + }; + + self.volume = function (val) { + if (playerState) { + if (val != null) { + // TODO + return; + } + + return playerState.volume; + } + }; + + self.setCurrentSrc = function (val, item, mediaSource, tracks) { + + if (!val) { + return; + } + + var tIndex = val.indexOf('#t='); + var startPosMs = 0; + + if (tIndex != -1) { + startPosMs = val.substring(tIndex + 3); + startPosMs = parseFloat(startPosMs) * 1000; + val = val.split('#')[0]; + } + + if (options.type == 'audio') { + + // TODO + + //AndroidVlcPlayer.playAudioVlc(val, JSON.stringify(item), JSON.stringify(mediaSource), options.poster); + var artist = item.ArtistItems && item.ArtistItems.length ? item.ArtistItems[0].Name : null; + window.audioplayer.playstream(successHandler, function () { + + Logger.log('playstream failed!'); + //onError(); + }, + { + ios: val + }, + // metadata used for iOS lock screen, Android 'Now Playing' notification + { + "title": item.Name, + "artist": artist, + "image": { + "url": options.poster + }, + "imageThumbnail": { + "url": options.poster + }, + "name": item.Name, + "description": item.Overview + } + ); + + } else { + + } + + playerState.currentSrc = val; + reportEvent('playing', {}); + }; + + self.currentSrc = function () { + return playerState.currentSrc; + }; + + self.paused = function () { + + if (playerState) { + return playerState.paused; + } + + return false; + }; + + self.cleanup = function (destroyRenderer) { + + playerState = {}; + }; + + self.enableCustomVideoControls = function () { + + return false; + }; + + function reportEvent(eventName, result) { + + var duration = result.duration || 0; + var position = result.progress || 0; + + Logger.log('eventName: ' + eventName + '. position: ' + position); + + var isPaused = result.state == 3 || eventName == 'paused'; + + var state = playerState; + + state.duration = duration; + state.currentTime = position; + state.paused = isPaused; + state.volume = 0; + + if (eventName == 'playbackstop') { + onEnded(); + } + else if (eventName == 'positionchange') { + onTimeUpdate(); + } + else if (eventName == 'paused') { + onPause(); + } + else if (eventName == 'unpaused') { + onPlaying(); + } + else if (eventName == 'playing') { + onPlaying(); + } + } + + function successHandler(result) { + + if (!result) { + return; + } + + if (result.state == 4 || result.state == 6) { + reportEvent('playbackstop', result); + } + else { + + var eventName = 'positionchange'; + reportEvent(eventName, result); + } + } + + self.init = function () { + + var deferred = DeferredBuilder.Deferred(); + + window.audioplayer.configure(function () { + Logger.log('audioplayer.configure success'); + deferred.resolve(); + }, function () { + Logger.log('audioplayer.configure error'); + deferred.resolve(); + }); + return deferred.promise(); + }; + } + + window.AudioRenderer = function (options) { + options = options || {}; + options.type = 'audio'; + + return new vlcRenderer(options); + }; + +})(); \ No newline at end of file diff --git a/dashboard-ui/scripts/htmlmediarenderer.js b/dashboard-ui/scripts/htmlmediarenderer.js index f27e89a450..0ed9d215af 100644 --- a/dashboard-ui/scripts/htmlmediarenderer.js +++ b/dashboard-ui/scripts/htmlmediarenderer.js @@ -233,7 +233,8 @@ var requiresNativeControls = !self.enableCustomVideoControls(); - var poster = options.poster ? (' poster="' + options.poster + '"') : ''; + // Safari often displays the poster under the video and it doesn't look good + var poster = !$.browser.safari && options.poster ? (' poster="' + options.poster + '"') : ''; // Can't autoplay in these browsers so we need to use the full controls if (requiresNativeControls && AppInfo.isNativeApp && $.browser.android) { diff --git a/dashboard-ui/scripts/mediaplayer-video.js b/dashboard-ui/scripts/mediaplayer-video.js index 32f8da39d3..c7e966265c 100644 --- a/dashboard-ui/scripts/mediaplayer-video.js +++ b/dashboard-ui/scripts/mediaplayer-video.js @@ -962,6 +962,7 @@ } var mediaRenderer = new VideoRenderer({ + poster: self.getPosterUrl(item) }); diff --git a/dashboard-ui/scripts/mediaplayer.js b/dashboard-ui/scripts/mediaplayer.js index a8e09d0898..4f3eba1917 100644 --- a/dashboard-ui/scripts/mediaplayer.js +++ b/dashboard-ui/scripts/mediaplayer.js @@ -213,7 +213,7 @@ Protocol: 'hls' }); - if (canPlayAac && $.browser.safari) { + if (canPlayAac && $.browser.safari && !AppInfo.isNativeApp) { profile.TranscodingProfiles.push({ Container: 'ts', Type: 'Audio', @@ -255,6 +255,7 @@ }); if (canPlayAac && $.browser.safari) { + profile.TranscodingProfiles.push({ Container: 'aac', Type: 'Audio', @@ -262,6 +263,7 @@ Context: 'Streaming', Protocol: 'http' }); + profile.TranscodingProfiles.push({ Container: 'aac', Type: 'Audio', @@ -269,6 +271,7 @@ Context: 'Static', Protocol: 'http' }); + } else { profile.TranscodingProfiles.push({ Container: 'mp3', @@ -570,7 +573,6 @@ var media = document.createElement('video'); - // safari if (media.canPlayType('application/x-mpegURL').replace(/no/, '') || media.canPlayType('application/vnd.apple.mpegURL').replace(/no/, '')) { return true; @@ -1120,11 +1122,6 @@ self.getPosterUrl = function (item) { - // Safari often shows the poster under the video, which doesn't look good - if ($.browser.safari) { - return null; - } - var screenWidth = Math.max(screen.height, screen.width); if (item.BackdropImageTags && item.BackdropImageTags.length) { @@ -1859,10 +1856,6 @@ poster: self.getPosterUrl(item) }); - // Set volume first to avoid an audible change - mediaRenderer.volume(initialVolume); - mediaRenderer.setCurrentSrc(audioUrl, item, mediaSource); - Events.on(mediaRenderer, "volumechange.mediaplayerevent", function () { Logger.log('audio element event: volumechange'); @@ -1904,6 +1897,14 @@ self.currentMediaRenderer = mediaRenderer; self.currentDurationTicks = self.currentMediaSource.RunTimeTicks; + + mediaRenderer.init().done(function () { + + // Set volume first to avoid an audible change + mediaRenderer.volume(initialVolume); + + mediaRenderer.setCurrentSrc(audioUrl, item, mediaSource); + }); } var getItemFields = "MediaSources,Chapters"; diff --git a/dashboard-ui/scripts/movies.js b/dashboard-ui/scripts/movies.js index 8be2555c31..492735d6fe 100644 --- a/dashboard-ui/scripts/movies.js +++ b/dashboard-ui/scripts/movies.js @@ -133,7 +133,7 @@ lazy: true, cardLayout: true, showDetailsMenu: true - });re + }); } else if (view == "Timeline") { html = LibraryBrowser.getPosterViewHtml({ diff --git a/dashboard-ui/scripts/site.js b/dashboard-ui/scripts/site.js index 09d51a5b7d..96de994438 100644 --- a/dashboard-ui/scripts/site.js +++ b/dashboard-ui/scripts/site.js @@ -2032,6 +2032,10 @@ var AppInfo = {}; define("audiorenderer", ["cordova/android/vlcplayer"]); define("videorenderer", ["cordova/android/vlcplayer"]); } + else if (Dashboard.isRunningInCordova() && $.browser.safari) { + define("audiorenderer", ["cordova/ios/vlcplayer"]); + define("videorenderer", ["scripts/htmlmediarenderer"]); + } else { define("audiorenderer", ["scripts/htmlmediarenderer"]); define("videorenderer", ["scripts/htmlmediarenderer"]); @@ -2138,9 +2142,11 @@ var AppInfo = {}; define("cryptojs-sha1", ["apiclient/sha1"]); - define("contentuploader", ["apiclient/contentuploader"]); - define("serversync", ["apiclient/serversync"]); - define("multiserversync", ["apiclient/multiserversync"]); + define("contentuploader", ["apiclient/sync/contentuploader"]); + define("serversync", ["apiclient/sync/serversync"]); + define("multiserversync", ["apiclient/sync/multiserversync"]); + define("offlineusersync", ["apiclient/sync/offlineusersync"]); + define("mediasync", ["apiclient/sync/mediasync"]); var deps = []; @@ -2192,6 +2198,15 @@ var AppInfo = {}; AppInfo.directPlayVideoContainers = "m4v,3gp,ts,mpegts,mov,xvid,vob,mkv,wmv,asf,ogm,ogv,m2v,avi,mpg,mpeg,mp4,webm".split(','); } + else if (Dashboard.isRunningInCordova() && $.browser.safari) { + + AppInfo.directPlayAudioContainers = "aac,mp3,mpa,wav,wma,mp2,ogg,oga,webma,ape,opus".split(','); + + // TODO: This is going to exclude it from both playback and sync, so improve on this + if (AppSettings.syncLosslessAudio()) { + AppInfo.directPlayAudioContainers.push('flac'); + } + } capabilities.DeviceProfile = MediaPlayer.getDeviceProfile(Math.max(screen.height, screen.width)); createConnectionManager(capabilities).done(function () { onConnectionManagerCreated(deferred); }); diff --git a/dashboard-ui/strings/html/server.json b/dashboard-ui/strings/html/server.json index c8f1b3d243..31cf2677f0 100644 --- a/dashboard-ui/strings/html/server.json +++ b/dashboard-ui/strings/html/server.json @@ -351,7 +351,7 @@ "LabelImagesByNamePath": "Images by name path:", "LabelImagesByNamePathHelp": "Specify a custom location for downloaded actor, genre and studio images.", "LabelMetadataPath": "Metadata path:", - "LabelMetadataPathHelp": "Specify a custom location for downloaded artwork and metadata, if not saving within media folders.", + "LabelMetadataPathHelp": "Specify a custom location for downloaded artwork and metadata.", "LabelTranscodingTempPath": "Transcoding temporary path:", "LabelTranscodingTempPathHelp": "This folder contains working files used by the transcoder. Specify a custom path, or leave empty to use the default within the server's data folder.", "TabBasics": "Basics",