diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 387e0d0..2bdccff 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -145,19 +145,19 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0800; + LastUpgradeCheck = 1020; ORGANIZATIONNAME = teodorpatras; TargetAttributes = { 137DF0851D2043F400C15E86 = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = 752CFWL5Y4; - LastSwiftMigration = 0800; + LastSwiftMigration = 1020; + ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 137DF0811D2043F400C15E86 /* Build configuration list for PBXProject "Example" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, @@ -253,19 +253,28 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -288,7 +297,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -301,19 +310,28 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -330,7 +348,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; @@ -343,12 +361,14 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = 752CFWL5Y4; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Example/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.teodorpatras.jukebox.example; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -356,12 +376,14 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = 752CFWL5Y4; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Example/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.teodorpatras.jukebox.example; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; }; name = Release; }; diff --git a/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme b/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme index dd14de0..5cb3266 100644 --- a/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme +++ b/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme @@ -1,6 +1,6 @@ Bool { - // Override point for customization after application launch. - application.isStatusBarHidden = true + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { return true } - - func applicationWillResignActive(_ application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. - } - - func applicationDidEnterBackground(_ application: UIApplication) { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - } - - func applicationWillEnterForeground(_ application: UIApplication) { - // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. - } - - func applicationDidBecomeActive(_ application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - } - - func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - } - - } - diff --git a/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json index 6d214d2..787b899 100644 --- a/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,5 +1,15 @@ { "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, { "size" : "29x29", "idiom" : "iphone", @@ -36,6 +46,16 @@ "filename" : "AppIcon60x60@3x.png", "scale" : "3x" }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, { "idiom" : "ipad", "size" : "29x29", @@ -73,6 +93,11 @@ "idiom" : "ipad", "filename" : "AppIcon60x60@3x-1.png", "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" } ], "info" : { diff --git a/Example/Example/Base.lproj/Main.storyboard b/Example/Example/Base.lproj/Main.storyboard index c01af82..2e0b583 100644 --- a/Example/Example/Base.lproj/Main.storyboard +++ b/Example/Example/Base.lproj/Main.storyboard @@ -1,47 +1,55 @@ - - + + + + + - + + - + - + - + + + + - - + + diff --git a/Example/Example/ViewController.swift b/Example/Example/ViewController.swift index b485355..3ae83a0 100644 --- a/Example/Example/ViewController.swift +++ b/Example/Example/ViewController.swift @@ -40,6 +40,8 @@ class ViewController: UIViewController, JukeboxDelegate { JukeboxItem(URL: URL(string: "http://www.noiseaddicts.com/samples_1w72b820/2958.mp3")!) ])! + + /// Later add another item let delay = DispatchTime.now() + Double(Int64(3 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) DispatchQueue.main.asyncAfter(deadline: delay) { @@ -51,14 +53,13 @@ class ViewController: UIViewController, JukeboxDelegate { return true } - func configureUI () - { + func configureUI () { resetUI() let color = UIColor(red:0.84, green:0.09, blue:0.1, alpha:1) indicator.color = color - slider.setThumbImage(UIImage(named: "sliderThumb"), for: UIControlState()) + slider.setThumbImage(UIImage(named: "sliderThumb"), for: UIControl.State()) slider.minimumTrackTintColor = color slider.maximumTrackTintColor = UIColor.black @@ -99,9 +100,9 @@ class ViewController: UIViewController, JukeboxDelegate { }) if jukebox.state == .ready { - playPauseButton.setImage(UIImage(named: "playBtn"), for: UIControlState()) + playPauseButton.setImage(UIImage(named: "playBtn"), for: UIControl.State()) } else if jukebox.state == .loading { - playPauseButton.setImage(UIImage(named: "pauseBtn"), for: UIControlState()) + playPauseButton.setImage(UIImage(named: "pauseBtn"), for: UIControl.State()) } else { volumeSlider.value = jukebox.volume let imageName: String @@ -111,7 +112,7 @@ class ViewController: UIViewController, JukeboxDelegate { case .paused, .failed, .ready: imageName = "playBtn" } - playPauseButton.setImage(UIImage(named: imageName), for: UIControlState()) + playPauseButton.setImage(UIImage(named: imageName), for: UIControl.State()) } print("Jukebox state changed to \(jukebox.state)") @@ -167,9 +168,12 @@ class ViewController: UIViewController, JukeboxDelegate { jukebox.playPrevious() } } + var current = 1.0 @IBAction func nextAction() { - jukebox.playNext() + jukebox.changeSpeedTo(value: current + 0.5) + current += 0.5 +// jukebox.playNext() } @IBAction func playPauseAction() { diff --git a/Jukebox.podspec b/Jukebox.podspec index c9acda5..a73e314 100644 --- a/Jukebox.podspec +++ b/Jukebox.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |s| s.source = { :git => "https://github.com/teodorpatras/Jukebox.git", :tag => s.version.to_s } s.social_media_url = 'https://twitter.com/teodorpatras' - s.platform = :ios, '8.0' + s.platform = :ios, '10.0' s.requires_arc = true s.source_files = 'Source/*.swift' diff --git a/Jukebox.xcodeproj/project.pbxproj b/Jukebox.xcodeproj/project.pbxproj index aa0ec86..4eab8c4 100644 --- a/Jukebox.xcodeproj/project.pbxproj +++ b/Jukebox.xcodeproj/project.pbxproj @@ -171,7 +171,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 0800; + LastUpgradeCheck = 1020; ORGANIZATIONNAME = teodorpatras; TargetAttributes = { 137DF0621D2041F500C15E86 = { @@ -180,16 +180,17 @@ }; 137DF06C1D2041F500C15E86 = { CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 0800; + LastSwiftMigration = 1020; }; }; }; buildConfigurationList = 137DF05D1D2041F500C15E86 /* Build configuration list for PBXProject "Jukebox" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, + Base, ); mainGroup = 137DF0591D2041F500C15E86; productRefGroup = 137DF0641D2041F500C15E86 /* Products */; @@ -259,14 +260,22 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -290,11 +299,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -310,14 +320,22 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -335,10 +353,11 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; @@ -357,12 +376,13 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.teodorpatras.Jukebox; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -377,11 +397,12 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = Source/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.teodorpatras.Jukebox; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -392,7 +413,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.teodorpatras.JukeboxTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -403,7 +424,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.teodorpatras.JukeboxTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; }; name = Release; }; diff --git a/Jukebox.xcodeproj/xcshareddata/xcschemes/Jukebox.xcscheme b/Jukebox.xcodeproj/xcshareddata/xcschemes/Jukebox.xcscheme index 78aaa1f..17bb157 100644 --- a/Jukebox.xcodeproj/xcshareddata/xcschemes/Jukebox.xcscheme +++ b/Jukebox.xcodeproj/xcshareddata/xcschemes/Jukebox.xcscheme @@ -1,6 +1,6 @@ + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Source/Jukebox.swift b/Source/Jukebox.swift index b7d0200..5222f56 100644 --- a/Source/Jukebox.swift +++ b/Source/Jukebox.swift @@ -90,7 +90,7 @@ extension Jukebox { invalidatePlayback() state = .ready UIApplication.shared.endBackgroundTask(backgroundIdentifier) - backgroundIdentifier = UIBackgroundTaskInvalid + backgroundIdentifier = UIBackgroundTaskIdentifier.invalid } /** @@ -136,10 +136,15 @@ extension Jukebox { public func seek(toSecond second: Int, shouldPlay: Bool = false) { guard let player = player, let item = currentItem else {return} - player.seek(to: CMTimeMake(Int64(second), 1)) + player.seek(to: CMTimeMake(value: Int64(second), timescale: 1)) item.update() if shouldPlay { - player.play() + + if #available(iOS 10.0, *) { + player.playImmediately(atRate: 1.0) + } else { + player.play() + } if state != .playing { state = .playing } @@ -160,14 +165,14 @@ extension Jukebox { item.loadPlayerItem() } } - - /** - Removes an item from the play queue - - parameter item: item to be removed - */ + /** + Removes an item from the play queue + + - parameter item: item to be removed + */ public func remove(item: JukeboxItem) { - if let index = queuedItems.index(where: {$0.identifier == item.identifier}) { + if let index = queuedItems.firstIndex(where: {$0.identifier == item.identifier}) { queuedItems.remove(at: index) } } @@ -190,6 +195,10 @@ extension Jukebox { open class Jukebox: NSObject, JukeboxItemDelegate { + deinit { + NotificationCenter.default.removeObserver(self) + } + public enum State: Int, CustomStringConvertible { case ready = 0 case playing @@ -198,9 +207,8 @@ open class Jukebox: NSObject, JukeboxItemDelegate { case failed public var description: String { - get{ - switch self - { + get { + switch self { case .ready: return "Ready" case .playing: @@ -211,35 +219,33 @@ open class Jukebox: NSObject, JukeboxItemDelegate { return "Paused" case .loading: return "Loading" - } } } } - // MARK:- Properties - - - fileprivate var player : AVPlayer? - fileprivate var progressObserver : AnyObject! - fileprivate var backgroundIdentifier = UIBackgroundTaskInvalid - fileprivate(set) open weak var delegate : JukeboxDelegate? + // MARK: Properties + fileprivate var player: AVPlayer? + fileprivate var progressObserver: AnyObject! + fileprivate var backgroundIdentifier = UIBackgroundTaskIdentifier.invalid + fileprivate(set) open weak var delegate: JukeboxDelegate? - fileprivate (set) open var playIndex = 0 - fileprivate (set) open var queuedItems : [JukeboxItem]! - fileprivate (set) open var state = State.ready { + fileprivate (set) open var playIndex = 0 + fileprivate (set) open var queuedItems: [JukeboxItem]! + fileprivate (set) open var state: State = .ready { didSet { delegate?.jukeboxStateDidChange(self) } } - // MARK: Computed + + // MARK: Computed + open var bufferTime: TimeInterval { + return availableDuration() + } open var volume: Float{ - get { - return player?.volume ?? 0 - } - set { - player?.volume = newValue - } + get { return player?.volume ?? 0 } + set { player?.volume = newValue } } open var currentItem: JukeboxItem? { @@ -256,13 +262,13 @@ open class Jukebox: NSObject, JukeboxItemDelegate { // MARK:- Initializer - /** - Create an instance with a delegate and a list of items without loading their assets. - - - parameter delegate: jukebox delegate - - parameter items: array of items to be added to the play queue - - - returns: Jukebox instance - */ + Create an instance with a delegate and a list of items without loading their assets. + + - parameter delegate: jukebox delegate + - parameter items: array of items to be added to the play queue + + - returns: Jukebox instance + */ public required init?(delegate: JukeboxDelegate? = nil, items: [JukeboxItem] = [JukeboxItem]()) { self.delegate = delegate super.init() @@ -278,12 +284,13 @@ open class Jukebox: NSObject, JukeboxItemDelegate { configureObservers() } - deinit{ - NotificationCenter.default.removeObserver(self) + public func changeSpeedTo(value: Double) { + guard let player = player else { return } + + player.rate = Float(value) } // MARK:- JukeboxItemDelegate - - func jukeboxItemDidFail(_ item: JukeboxItem) { stop() state = .failed @@ -297,10 +304,8 @@ open class Jukebox: NSObject, JukeboxItemDelegate { func jukeboxItemDidLoadPlayerItem(_ item: JukeboxItem) { delegate?.jukeboxDidLoadItem(self, item: item) - let index = queuedItems.index{$0 === item} - - guard let playItem = item.playerItem - , state == .loading && playIndex == index else {return} + let index = queuedItems.firstIndex{$0 === item} + guard let playItem = item.playerItem, state == .loading && playIndex == index else {return} registerForPlayToEndNotification(withItem: playItem) startNewPlayer(forItem: playItem) @@ -337,7 +342,12 @@ open class Jukebox: NSObject, JukeboxItemDelegate { } if let img = currentItem?.meta.artwork { - nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image: img) + + let artwork = MPMediaItemArtwork.init(boundsSize: img.size, requestHandler: { (size) -> UIImage in + return img + }) + + nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork } MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo @@ -354,10 +364,14 @@ open class Jukebox: NSObject, JukeboxItemDelegate { if state != .playing { startProgressTimer() if let player = player { - player.play() - } else { - currentItem!.refreshPlayerItem(withAsset: currentItem!.playerItem!.asset) - startNewPlayer(forItem: currentItem!.playerItem!) + if #available(iOS 10.0, *) { + player.playImmediately(atRate: 1.0) + } else { + player.play() + } + } else if let currentItem = currentItem, let playerItem = currentItem.playerItem { + currentItem.refreshPlayerItem(withAsset: playerItem.asset) + startNewPlayer(forItem: playerItem) } state = .playing } @@ -366,20 +380,29 @@ open class Jukebox: NSObject, JukeboxItemDelegate { fileprivate func invalidatePlayback(shouldResetIndex resetIndex: Bool = true) { stopProgressTimer() player?.pause() - player = nil - + if resetIndex { playIndex = 0 } } - fileprivate func startNewPlayer(forItem item : AVPlayerItem) { + fileprivate func startNewPlayer(forItem item: AVPlayerItem) { invalidatePlayback(shouldResetIndex: false) player = AVPlayer(playerItem: item) - player?.allowsExternalPlayback = false + player?.allowsExternalPlayback = true + + startProgressTimer() seek(toSecond: 0, shouldPlay: true) updateInfoCenter() + + // Pre-Fetching increased + if #available(iOS 10.0, *) { + let duration = TimeInterval(CMTimeGetSeconds(item.duration)) + if duration > 0 { + item.preferredForwardBufferDuration = duration + } + } } // MARK: Items related @@ -415,12 +438,29 @@ open class Jukebox: NSObject, JukeboxItemDelegate { } // MARK: Progress tracking + fileprivate func availableDuration() -> TimeInterval { + if let range = self.player?.currentItem?.loadedTimeRanges.first { + let timeRage = range.timeRangeValue + let startTime = CMTimeGetSeconds(timeRage.start) + let loadedDuration = CMTimeGetSeconds(timeRage.duration) + + return TimeInterval(startTime + loadedDuration) + } + + return 0.0 + } - fileprivate func startProgressTimer(){ - guard let player = player , player.currentItem?.duration.isValid == true else {return} - progressObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(0.05, Int32(NSEC_PER_SEC)), queue: nil, using: { [unowned self] (time : CMTime) -> Void in - self.timerAction() - }) as AnyObject! + fileprivate func startProgressTimer() { + guard let player = player, player.currentItem?.duration.isValid == true else { return } + + progressObserver = player.addPeriodicTimeObserver( + forInterval: CMTimeMakeWithSeconds(0.05, preferredTimescale: Int32(NSEC_PER_SEC)), + queue: nil, + using: { [weak self] (time: CMTime) in + guard let `self` = self else { return } + + self.timerAction() + }) as AnyObject } fileprivate func stopProgressTimer() { @@ -434,49 +474,60 @@ open class Jukebox: NSObject, JukeboxItemDelegate { // MARK: Configurations fileprivate func configureBackgroundAudioTask() { - backgroundIdentifier = UIApplication.shared.beginBackgroundTask (expirationHandler: { () -> Void in + backgroundIdentifier = UIApplication.shared.beginBackgroundTask (expirationHandler: { () -> Void in UIApplication.shared.endBackgroundTask(self.backgroundIdentifier) - self.backgroundIdentifier = UIBackgroundTaskInvalid + self.backgroundIdentifier = UIBackgroundTaskIdentifier.invalid }) } fileprivate func configureAudioSession() throws { - try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback) - try AVAudioSession.sharedInstance().setMode(AVAudioSessionModeDefault) - try AVAudioSession.sharedInstance().setActive(true) + do { + try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default) + try AVAudioSession.sharedInstance().setActive(true) + } catch { } } fileprivate func configureObservers() { NotificationCenter.default.addObserver(self, selector: #selector(Jukebox.handleStall), name: NSNotification.Name.AVPlayerItemPlaybackStalled, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(handleAudioSessionInterruption), name: NSNotification.Name.AVAudioSessionInterruption, object: AVAudioSession.sharedInstance()) + NotificationCenter.default.addObserver(self, selector: #selector(handleAudioSessionInterruption), + name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance()) } // MARK:- Notifications - - func handleAudioSessionInterruption(_ notification : Notification) { + @objc func handleAudioSessionInterruption(_ notification : Notification) { guard let userInfo = notification.userInfo as? [String: AnyObject] else { return } guard let rawInterruptionType = userInfo[AVAudioSessionInterruptionTypeKey] as? NSNumber else { return } - guard let interruptionType = AVAudioSessionInterruptionType(rawValue: rawInterruptionType.uintValue) else { return } - + guard let interruptionType = AVAudioSession.InterruptionType(rawValue: rawInterruptionType.uintValue) else { return } + switch interruptionType { case .began: //interruption started self.pause() case .ended: //interruption ended if let rawInterruptionOption = userInfo[AVAudioSessionInterruptionOptionKey] as? NSNumber { - let interruptionOption = AVAudioSessionInterruptionOptions(rawValue: rawInterruptionOption.uintValue) - if interruptionOption == AVAudioSessionInterruptionOptions.shouldResume { + let interruptionOption = AVAudioSession.InterruptionOptions(rawValue: rawInterruptionOption.uintValue) + if interruptionOption == AVAudioSession.InterruptionOptions.shouldResume { self.resumePlayback() } } + @unknown default: + break } } - func handleStall() { - player?.pause() - player?.play() + @objc func handleStall() { + guard let player = player else { return } + + player.pause() + + if #available(iOS 10.0, *) { + player.playImmediately(atRate: 1.0) + } else { + player.play() + } } - func playerItemDidPlayToEnd(_ notification : Notification){ + @objc func playerItemDidPlayToEnd(_ notification : Notification){ if playIndex >= queuedItems.count - 1 { stop() } else { diff --git a/Source/JukeboxItem.swift b/Source/JukeboxItem.swift index 0157bfa..ea65e42 100644 --- a/Source/JukeboxItem.swift +++ b/Source/JukeboxItem.swift @@ -43,30 +43,30 @@ open class JukeboxItem: NSObject { // MARK:- Properties - - let identifier: String - var delegate: JukeboxItemDelegate? - fileprivate var didLoad = false - open var localTitle: String? - open let URL: Foundation.URL + let identifier: String + var delegate: JukeboxItemDelegate? + + public var localTitle: String? + public let URL: Foundation.URL fileprivate(set) open var playerItem: AVPlayerItem? fileprivate (set) open var currentTime: Double? fileprivate(set) open lazy var meta = Meta() - + fileprivate var didLoad = false fileprivate var timer: Timer? fileprivate let observedValue = "timedMetadata" // MARK:- Initializer - /** - Create an instance with an URL and local title - - - parameter URL: local or remote URL of the audio file - - parameter localTitle: an optional title for the file - - - returns: JukeboxItem instance - */ + Create an instance with an URL and local title + + - parameter URL: local or remote URL of the audio file + - parameter localTitle: an optional title for the file + + - returns: JukeboxItem instance + */ public required init(URL : Foundation.URL, localTitle : String? = nil) { self.URL = URL self.identifier = UUID().uuidString @@ -76,7 +76,7 @@ open class JukeboxItem: NSObject { } override open func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - + if change?[NSKeyValueChangeKey(rawValue:"name")] is NSNull { delegate?.jukeboxItemDidFail(self) return @@ -136,7 +136,7 @@ open class JukeboxItem: NSObject { } open override var description: String { - return "" + return "" } // MARK:- Private methods - @@ -161,7 +161,7 @@ open class JukeboxItem: NSObject { timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(JukeboxItem.notifyDelegate), userInfo: nil, repeats: false) } - func notifyDelegate() { + @objc func notifyDelegate() { timer?.invalidate() timer = nil self.delegate?.jukeboxItemDidUpdate(self) @@ -177,20 +177,17 @@ open class JukeboxItem: NSObject { }) } - fileprivate func configureMetadata() - { - - DispatchQueue.global(qos: .background).async { + fileprivate func configureMetadata() { + DispatchQueue.global(qos: .background).async { let metadataArray = AVPlayerItem(url: self.URL).asset.commonMetadata - for item in metadataArray - { - item.loadValuesAsynchronously(forKeys: [AVMetadataKeySpaceCommon], completionHandler: { () -> Void in + for item in metadataArray { + item.loadValuesAsynchronously(forKeys: [AVMetadataKeySpace.common.rawValue]) { self.meta.process(metaItem: item) DispatchQueue.main.async { self.scheduleNotification() } - }) + } } } } @@ -198,19 +195,14 @@ open class JukeboxItem: NSObject { private extension JukeboxItem.Meta { mutating func process(metaItem item: AVMetadataItem) { + guard let commonKey = item.commonKey else { return } - switch item.commonKey - { - case "title"? : - title = item.value as? String - case "albumName"? : - album = item.value as? String - case "artist"? : - artist = item.value as? String - case "artwork"? : - processArtwork(fromMetadataItem : item) - default : - break + switch commonKey.rawValue { + case "title": title = item.value as? String + case "albumName": album = item.value as? String + case "artist": artist = item.value as? String + case "artwork": processArtwork(fromMetadataItem : item) + default : break } }