diff --git a/Aerial.xcodeproj/project.pbxproj b/Aerial.xcodeproj/project.pbxproj index 7b713f5d..2c502bb9 100644 --- a/Aerial.xcodeproj/project.pbxproj +++ b/Aerial.xcodeproj/project.pbxproj @@ -1169,7 +1169,7 @@ CODE_SIGN_IDENTITY = "Developer ID Application: Guillaume Louel (3L54M5L5KK)"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1.5.1beta2; + CURRENT_PROJECT_VERSION = 1.5.1beta3; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 3L54M5L5KK; ENABLE_HARDENED_RUNTIME = YES; @@ -1177,7 +1177,7 @@ INSTALL_PATH = "$(HOME)/Library/Screen Savers"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.9; - MARKETING_VERSION = 1.5.1beta2; + MARKETING_VERSION = 1.5.1beta3; PRODUCT_BUNDLE_IDENTIFIER = com.johncoates.Aerial; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1198,7 +1198,7 @@ CODE_SIGN_IDENTITY = "Developer ID Application: Guillaume Louel (3L54M5L5KK)"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1.5.1beta2; + CURRENT_PROJECT_VERSION = 1.5.1beta3; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 3L54M5L5KK; ENABLE_HARDENED_RUNTIME = YES; @@ -1206,7 +1206,7 @@ INSTALL_PATH = "$(HOME)/Library/Screen Savers"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.9; - MARKETING_VERSION = 1.5.1beta2; + MARKETING_VERSION = 1.5.1beta3; OTHER_CODE_SIGN_FLAGS = "--timestamp"; PRODUCT_BUNDLE_IDENTIFIER = com.johncoates.Aerial; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Aerial/Source/Controllers/PWC Tabs/PWC+Advanced.swift b/Aerial/Source/Controllers/PWC Tabs/PWC+Advanced.swift index 7742d0b5..c45163e8 100644 --- a/Aerial/Source/Controllers/PWC Tabs/PWC+Advanced.swift +++ b/Aerial/Source/Controllers/PWC Tabs/PWC+Advanced.swift @@ -21,6 +21,9 @@ extension PreferencesWindowController { if preferences.logMilliseconds { logMillisecondsButton.state = .on } + if preferences.synchronizedMode { + synchronizedModeCheckbox.state = .on + } } // MARK: - Advanced panel @@ -103,6 +106,12 @@ extension PreferencesWindowController { showLogBottomClick.isHidden = false } + @IBAction func synchronizedModeClick(_ sender: NSButton) { + let onState = sender.state == .on + preferences.synchronizedMode = onState + debugLog("UI synchronizedMode \(onState)") + } + @IBAction func moveOldVideosClick(_ sender: Any) { ManifestLoader.instance.moveOldVideos() diff --git a/Aerial/Source/Controllers/PWC Tabs/PWC+Brightness.swift b/Aerial/Source/Controllers/PWC Tabs/PWC+Brightness.swift index 599a7bb4..c5ae90a1 100644 --- a/Aerial/Source/Controllers/PWC Tabs/PWC+Brightness.swift +++ b/Aerial/Source/Controllers/PWC Tabs/PWC+Brightness.swift @@ -29,9 +29,6 @@ extension PreferencesWindowController { if preferences.dimOnlyAtNight { dimOnlyAtNight.state = .on } - if preferences.synchronizedMode { - synchronizedModeCheckbox.state = .on - } dimStartFrom.doubleValue = preferences.startDim ?? 0.5 dimFadeTo.doubleValue = preferences.endDim ?? 0.1 dimFadeInMinutes.stringValue = String(preferences.dimInMinutes!) diff --git a/Aerial/Source/Controllers/PWC Tabs/PWC+Videos.swift b/Aerial/Source/Controllers/PWC Tabs/PWC+Videos.swift index f90e0b58..30e782d0 100644 --- a/Aerial/Source/Controllers/PWC Tabs/PWC+Videos.swift +++ b/Aerial/Source/Controllers/PWC Tabs/PWC+Videos.swift @@ -115,6 +115,15 @@ extension PreferencesWindowController { fadeInOutModePopup.selectItem(at: preferences.fadeMode!) + // We need catalina for HDR ! + if #available(OSX 10.15, *) { + if preferences.useHDR { + useHDRCheckbox.state = .off + } + } else { + useHDRCheckbox.state = .off + useHDRCheckbox.isEnabled = false + } } @IBAction func rightArrowKeyPlaysNextClick(_ sender: NSButton) { @@ -123,12 +132,6 @@ extension PreferencesWindowController { debugLog("UI allowSkips \(onState)") } - @IBAction func synchronizedModeClick(_ sender: NSButton) { - let onState = sender.state == .on - preferences.synchronizedMode = onState - debugLog("UI synchronizedMode \(onState)") - } - @IBAction func overrideOnBatteryClick(_ sender: NSButton) { let onState = sender.state == .on preferences.overrideOnBattery = onState @@ -164,6 +167,12 @@ extension PreferencesWindowController { outlineView.reloadData() } + @IBAction func useHDRChange(_ sender: NSButton) { + let onState = sender.state == .on + preferences.useHDR = onState + debugLog("UI useHDR \(onState)") + } + @IBAction func helpButtonClick(_ button: NSButton!) { popover.show(relativeTo: button.preparedContentRect, of: button, preferredEdge: .maxY) } diff --git a/Aerial/Source/Controllers/Preferences.swift b/Aerial/Source/Controllers/Preferences.swift index d98914c2..67054873 100644 --- a/Aerial/Source/Controllers/Preferences.swift +++ b/Aerial/Source/Controllers/Preferences.swift @@ -83,6 +83,7 @@ final class Preferences { case synchronizedMode = "synchronizedMode" case aspectMode = "aspectMode" + case useHDR = "useHDR" } enum AspectMode: Int { @@ -228,6 +229,7 @@ final class Preferences { defaultValues[.verticalMargin] = 0 defaultValues[.synchronizedMode] = false defaultValues[.aspectMode] = AspectMode.fill + defaultValues[.useHDR] = true // Set today's date as default let dateFormatter = DateFormatter() @@ -345,6 +347,15 @@ final class Preferences { } } + var useHDR: Bool { + get { + return value(forIdentifier: .useHDR) + } + set { + setValue(forIdentifier: .useHDR, value: newValue) + } + } + var synchronizedMode: Bool { get { return value(forIdentifier: .synchronizedMode) diff --git a/Aerial/Source/Controllers/PreferencesWindowController.swift b/Aerial/Source/Controllers/PreferencesWindowController.swift index 9b00aa01..d3fe1b96 100644 --- a/Aerial/Source/Controllers/PreferencesWindowController.swift +++ b/Aerial/Source/Controllers/PreferencesWindowController.swift @@ -56,6 +56,8 @@ final class PreferencesWindowController: NSWindowController, NSOutlineViewDataSo @IBOutlet var fadeInOutModePopup: NSPopUpButton! @IBOutlet var popupVideoFormat: NSPopUpButton! + @IBOutlet var useHDRCheckbox: NSButton! + @IBOutlet var overrideOnBatteryCheckbox: NSButton! @IBOutlet var alternatePopupVideoFormat: NSPopUpButton! @IBOutlet var powerSavingOnLowBatteryCheckbox: NSButton! diff --git a/Aerial/Source/Models/AerialVideo.swift b/Aerial/Source/Models/AerialVideo.swift index 737a00c8..ad9afd8d 100644 --- a/Aerial/Source/Models/AerialVideo.swift +++ b/Aerial/Source/Models/AerialVideo.swift @@ -90,9 +90,13 @@ final class AerialVideo: CustomStringConvertible, Equatable { let secondaryName: String let type: String let timeOfDay: String + var url1080pH264: String let url1080pHEVC: String + let url1080pHDR: String let url4KHEVC: String + let url4KHDR: String + var sources: [Manifests] let poi: [String: String] let communityPoi: [String: String] @@ -106,6 +110,7 @@ final class AerialVideo: CustomStringConvertible, Equatable { return VideoCache.isAvailableOffline(video: self) } + // MARK: - Public getter var url: URL { let preferences = Preferences.sharedInstance let timeManagement = TimeManagement.sharedInstance @@ -117,35 +122,56 @@ final class AerialVideo: CustomStringConvertible, Equatable { return getClosestAvailable(wanted: preferences.videoFormat!) } + // Returns the closest video we have in the manifests func getClosestAvailable(wanted: Int) -> URL { if wanted == Preferences.VideoFormat.v4KHEVC.rawValue { - if url4KHEVC != "" { - return URL(string: self.url4KHEVC)! - } else if url1080pHEVC != "" { - return URL(string: self.url1080pHEVC)! - } else { - return URL(string: self.url1080pH264)! - } + return getVideoFormatFrom(best: .v4KHEVC, option2: .v1080pHEVC, option3: .v1080pH264) } else if wanted == Preferences.VideoFormat.v1080pHEVC.rawValue { - if url1080pHEVC != "" { - return URL(string: self.url1080pHEVC)! - } else if url1080pH264 != "" { - return URL(string: self.url1080pH264)! - } else { - return URL(string: self.url4KHEVC)! - } + return getVideoFormatFrom(best: .v1080pHEVC, option2: .v1080pH264, option3: .v4KHEVC) } else { - if url1080pH264 != "" { - return URL(string: self.url1080pH264)! - } else if url1080pHEVC != "" { - // With the latest versions, we should always have a H.264 fallback so this is just for future proofing - return URL(string: self.url1080pHEVC)! - } else { - return URL(string: self.url4KHEVC)! - } + return getVideoFormatFrom(best: .v1080pH264, option2: .v1080pHEVC, option3: .v4KHEVC) + } + } + + // Helper to find the best available format from the 3 options given, in that order + func getVideoFormatFrom(best: Preferences.VideoFormat, option2: Preferences.VideoFormat, option3: Preferences.VideoFormat) -> URL { + if urlFor(videoFormat: best) != "" { + return getDynamicRange(wanted: best) + } else if urlFor(videoFormat: option2) != "" { + return getDynamicRange(wanted: option2) + } else { + return getDynamicRange(wanted: option3) + } + } + + // Helper to get the url for a given format + private func urlFor(videoFormat: Preferences.VideoFormat) -> String { + if videoFormat == .v4KHEVC { + return url4KHEVC + } else if videoFormat == .v1080pHEVC { + return url1080pHEVC + } else { + return url1080pH264 + } + } + + // Helper to get the correct Dynamic Range version based on Format, preferences, and OS availability + func getDynamicRange(wanted: Preferences.VideoFormat) -> URL { + let preferences = Preferences.sharedInstance + if #available(OSX 10.15, *), preferences.useHDR && wanted == .v4KHEVC { + return URL(string: url4KHDR)! + } else if wanted == .v4KHEVC { + return URL(string: url4KHEVC)! + } else if #available(OSX 10.15, *), preferences.useHDR && wanted == .v1080pHEVC { + return URL(string: url1080pHDR)! + } else if wanted == .v1080pHEVC { + return URL(string: url1080pHEVC)! + } else { + return URL(string: url1080pH264)! } } + // MARK: - Init init(id: String, name: String, secondaryName: String, @@ -153,7 +179,9 @@ final class AerialVideo: CustomStringConvertible, Equatable { timeOfDay: String, url1080pH264: String, url1080pHEVC: String, + url1080pHDR: String, url4KHEVC: String, + url4KHDR: String, manifest: Manifests, poi: [String: String], communityPoi: [String: String] @@ -189,13 +217,15 @@ final class AerialVideo: CustomStringConvertible, Equatable { self.url1080pH264 = url1080pH264 self.url1080pHEVC = url1080pHEVC + self.url1080pHDR = url1080pHDR self.url4KHEVC = url4KHEVC + self.url4KHDR = url4KHDR self.sources = [manifest] self.poi = poi self.communityPoi = communityPoi self.duration = 0 - updateDuration() + updateDuration() // We need to have the video duration } func updateDuration() { @@ -255,7 +285,9 @@ final class AerialVideo: CustomStringConvertible, Equatable { timeofDay=\(timeOfDay), url1080pH264=\(url1080pH264), url1080pHEVC=\(url1080pHEVC), + url1080pHDR=\(url1080pHDR), url4KHEVC=\(url4KHEVC)" + url4KHDR=\(url4KHDR)" """ } } diff --git a/Aerial/Source/Models/DisplayDetection.swift b/Aerial/Source/Models/DisplayDetection.swift index b2c88815..4dfe7288 100644 --- a/Aerial/Source/Models/DisplayDetection.swift +++ b/Aerial/Source/Models/DisplayDetection.swift @@ -99,6 +99,7 @@ final class DisplayDetection: NSObject { for screen in NSScreen.screens { debugLog("pass2: dict \(screen.deviceDescription)") debugLog(" bottomLeftFrame \(screen.frame)") + let dscreen = findScreenWith(frame: screen.frame) if dscreen != nil { diff --git a/Aerial/Source/Models/ManifestLoader.swift b/Aerial/Source/Models/ManifestLoader.swift index 6741c284..797e3b9e 100644 --- a/Aerial/Source/Models/ManifestLoader.swift +++ b/Aerial/Source/Models/ManifestLoader.swift @@ -103,7 +103,9 @@ class ManifestLoader { let mergeInfo = [ "2F11E857-4F77-4476-8033-4A1E4610AFCC": ["url-1080-SDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_2K_SDR_HEVC.mov", - "url-4K-SDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_4K_SDR_HEVC.mov", ], // Dubai night 2 + "url-1080-HDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_2K_HDR_HEVC.mov", + "url-4K-SDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_4K_SDR_HEVC.mov", + "url-4K-HDR": "https://sylvan.apple.com/Aerials/2x/Videos/DB_D011_C009_4K_HDR_HEVC.mov", ], // Dubai night 2 ] // Extra POI @@ -151,8 +153,7 @@ class ManifestLoader { playlistIsRestricted = isRestricted playlistRestrictedTo = restrictedTo - // Start with a shuffled list - //let shuffled = loadedManifest.shuffled() + // Start with a shuffled list, we may have synchronized seed shuffle var shuffled: [AerialVideo] let preferences = Preferences.sharedInstance if preferences.synchronizedMode { @@ -177,14 +178,12 @@ class ManifestLoader { let inRotation = preferences.videoIsInRotation(videoID: video.id) if !inRotation { - //debugLog("randomVideo: video is disabled: \(video)") continue } // Do we restrict video types by day/night ? if isRestricted { if video.timeOfDay != restrictedTo { - //debugLog("randomVideo: video is excluded as we only play \(restrictTo) (is: \(video.timeOfDay))") continue } } @@ -192,7 +191,6 @@ class ManifestLoader { // We may not want to stream if preferences.neverStreamVideos == true { if video.isAvailableOffline == false { - //debugLog("randomVideo: video is excluded because it's not available offline \(video)") continue } } @@ -201,7 +199,7 @@ class ManifestLoader { playlist.append(video) } - // On regenerating a new playlist, we try to avoid repeating + // On regenerating a new playlist, we try to avoid repeating the last thing we played! while playlist.count > 1 && lastPluckedFromPlaylist == playlist.first { playlist.shuffle() } @@ -210,15 +208,18 @@ class ManifestLoader { func randomVideo(excluding: [AerialVideo]) -> AerialVideo? { let timeManagement = TimeManagement.sharedInstance let (shouldRestrictByDayNight, restrictTo) = timeManagement.shouldRestrictPlaybackToDayNightVideo() - debugLog("shouldRestrictByDayNight : \(shouldRestrictByDayNight) (\(restrictTo))") + + // We may need to regenerate a playlist! if playlist.isEmpty || restrictTo != playlistRestrictedTo || shouldRestrictByDayNight != playlistIsRestricted { generatePlaylist(isRestricted: shouldRestrictByDayNight, restrictedTo: restrictTo) } + // If not pluck one from current playlist and return that if !playlist.isEmpty { lastPluckedFromPlaylist = playlist.removeFirst() return lastPluckedFromPlaylist } else { + // If we don't have any playlist, something's got awfully wrong so deal with that! return findBestEffortVideo() } } @@ -238,7 +239,7 @@ class ManifestLoader { warnLog("Empty playlist, not good !") if lastPluckedFromPlaylist != nil { - warnLog("returning last played video after condition change not met !") + warnLog("Repeating last played video, after condition change not met !") return lastPluckedFromPlaylist! } else { // Start with a shuffled list @@ -337,6 +338,7 @@ class ManifestLoader { } } + // MARK: - This will refetch the manifests online func reloadFiles() { moveOldManifests() @@ -364,17 +366,16 @@ class ManifestLoader { urls.append(URL(string: "http://a1.phobos.apple.com/us/r1000/000/Features/atv/AutumnResources/videos/entries.json")!) } + // Setup and start async fetching let completion = BlockOperation { debugLog("Fetching manifests all done") // We can now load from the newly cached files self.loadCachedManifests() } - for url in urls { let operation = downloadManager.queueDownload(url) completion.addDependency(operation) } - OperationQueue.main.addOperation(completion) } @@ -441,7 +442,9 @@ class ManifestLoader { timeOfDay: asset.time, url1080pH264: url1080p, url1080pHEVC: "", + url1080pHDR: "", url4KHEVC: url4K, + url4KHDR: "", manifest: .customVideos, poi: [:], communityPoi: asset.pointsOfInterest) @@ -526,6 +529,7 @@ class ManifestLoader { } } } + // MARK: - Manifests // Check if the Manifests have been loaded in this class already @@ -686,7 +690,9 @@ class ManifestLoader { let id = item["id"] as! String let url1080pH264 = item["url-1080-H264"] as? String let url1080pHEVC = item["url-1080-SDR"] as? String + let url1080pHDR = item["url-1080-HDR"] as? String let url4KHEVC = item["url-4K-SDR"] as? String + let url4KHDR = item["url-4K-HDR"] as? String let name = item["accessibilityLabel"] as! String var secondaryName = "" // We may have a secondary name @@ -719,7 +725,9 @@ class ManifestLoader { timeOfDay: timeOfDay, url1080pH264: url1080pH264 ?? "", url1080pHEVC: url1080pHEVC ?? "", + url1080pHDR: url1080pHDR ?? "", url4KHEVC: url4KHEVC ?? "", + url4KHDR: url4KHDR ?? "", manifest: manifest, poi: poi ?? [:], communityPoi: communityPoi) @@ -781,12 +789,16 @@ class ManifestLoader { } } } else { - var url4khevc = "" var url1080phevc = "" + var url1080phdr = "" + var url4khevc = "" + var url4khdr = "" // Check if we have some HEVC urls to merge if let val = mergeInfo[id] { url1080phevc = val["url-1080-SDR"]! + url1080phdr = val["url-1080-HDR"]! url4khevc = val["url-4K-SDR"]! + url4khdr = val["url-4K-HDR"]! } // Now we can finally add... @@ -797,7 +809,9 @@ class ManifestLoader { timeOfDay: timeOfDay, url1080pH264: url, url1080pHEVC: url1080phevc, + url1080pHDR: url1080phdr, url4KHEVC: url4khevc, + url4KHDR: url4khdr, manifest: manifest, poi: poi ?? [:], communityPoi: communityPoi) diff --git a/Resources/PreferencesWindow.xib b/Resources/PreferencesWindow.xib index 86574f52..c920367d 100644 --- a/Resources/PreferencesWindow.xib +++ b/Resources/PreferencesWindow.xib @@ -141,6 +141,7 @@ + @@ -170,7 +171,7 @@ - + @@ -194,12 +195,12 @@ - + - + @@ -213,7 +214,7 @@ - + @@ -236,7 +237,7 @@ - + @@ -255,13 +256,13 @@ - + - + @@ -270,7 +271,7 @@ - + @@ -280,7 +281,7 @@ - + @@ -383,10 +384,10 @@ is disabled - + - - + + @@ -395,7 +396,7 @@ is disabled - + @@ -405,14 +406,14 @@ is disabled - + - + @@ -459,7 +460,7 @@ is disabled - + @@ -491,7 +492,7 @@ is disabled - + @@ -573,7 +573,7 @@ is disabled - + @@ -1604,7 +1604,7 @@ Shift, but macOS 10.12.4 or above and a compatible Mac are required) - + @@ -1906,7 +1906,7 @@ Shift, but macOS 10.12.4 or above and a compatible Mac are required) - + @@ -1948,7 +1948,7 @@ Shift, but macOS 10.12.4 or above and a compatible Mac are required) - - - - - - If you are experiencing an issue with Aerial, we may ask you to enable the Debug and Log to disk options below. - - - - - - - - + @@ -2003,7 +1990,7 @@ Shift, but macOS 10.12.4 or above and a compatible Mac are required) - + + + + + @@ -2038,7 +2029,7 @@ Shift, but macOS 10.12.4 or above and a compatible Mac are required) + + + + + + + + + +