diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..cffff878f Binary files /dev/null and b/.DS_Store differ diff --git a/packages/.DS_Store b/packages/.DS_Store new file mode 100644 index 000000000..bda52e9b9 Binary files /dev/null and b/packages/.DS_Store differ diff --git a/packages/audioplayers/example/ios/Flutter/AppFrameworkInfo.plist b/packages/audioplayers/example/ios/Flutter/AppFrameworkInfo.plist index 9625e105d..7c5696400 100644 --- a/packages/audioplayers/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/audioplayers/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/packages/audioplayers/example/ios/Podfile b/packages/audioplayers/example/ios/Podfile index fdcc671eb..3e44f9c6f 100644 --- a/packages/audioplayers/example/ios/Podfile +++ b/packages/audioplayers/example/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '11.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/packages/audioplayers/example/ios/Podfile.lock b/packages/audioplayers/example/ios/Podfile.lock index d7286d1ba..a351b72a5 100644 --- a/packages/audioplayers/example/ios/Podfile.lock +++ b/packages/audioplayers/example/ios/Podfile.lock @@ -41,9 +41,9 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - SDWebImage (5.17.0): - - SDWebImage/Core (= 5.17.0) - - SDWebImage/Core (5.17.0) + - SDWebImage (5.19.0): + - SDWebImage/Core (= 5.19.0) + - SDWebImage/Core (5.19.0) - SwiftyGif (5.4.4) DEPENDENCIES: @@ -76,13 +76,13 @@ SPEC CHECKSUMS: audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: ce3938a0df3cc1ef404671531facef740d03f920 - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 integration_test: 13825b8a9334a850581300559b8839134b124670 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + SDWebImage: 981fd7e860af070920f249fd092420006014c3eb SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f -PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189 +PODFILE CHECKSUM: a57f30d18f102dd3ce366b1d62a55ecbef2158e5 -COCOAPODS: 1.12.1 +COCOAPODS: 1.15.2 diff --git a/packages/audioplayers/example/ios/Runner.xcodeproj/project.pbxproj b/packages/audioplayers/example/ios/Runner.xcodeproj/project.pbxproj index 6c7340e14..1ac392591 100644 --- a/packages/audioplayers/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/audioplayers/example/ios/Runner.xcodeproj/project.pbxproj @@ -216,7 +216,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { @@ -453,7 +453,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -581,7 +581,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -630,7 +630,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/packages/audioplayers/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/audioplayers/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 87131a09b..8e3ca5dfe 100644 --- a/packages/audioplayers/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/audioplayers/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ + + + + diff --git a/packages/audioplayers_darwin/audioplayers_darwin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/audioplayers_darwin/audioplayers_darwin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/packages/audioplayers_darwin/audioplayers_darwin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/audioplayers_darwin/audioplayers_darwin.xcodeproj/project.xcworkspace/xcuserdata/julianalima.xcuserdatad/UserInterfaceState.xcuserstate b/packages/audioplayers_darwin/audioplayers_darwin.xcodeproj/project.xcworkspace/xcuserdata/julianalima.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 000000000..8eb96c2d1 Binary files /dev/null and b/packages/audioplayers_darwin/audioplayers_darwin.xcodeproj/project.xcworkspace/xcuserdata/julianalima.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/packages/audioplayers_darwin/darwin/Classes/SwiftAudioplayersDarwinPlugin.swift b/packages/audioplayers_darwin/darwin/Classes/SwiftAudioplayersDarwinPlugin.swift index d545e4264..e10e775ad 100644 --- a/packages/audioplayers_darwin/darwin/Classes/SwiftAudioplayersDarwinPlugin.swift +++ b/packages/audioplayers_darwin/darwin/Classes/SwiftAudioplayersDarwinPlugin.swift @@ -205,8 +205,7 @@ public class SwiftAudioplayersDarwinPlugin: NSObject, FlutterPlugin { code: "DarwinAudioError", message: "Null position received on seek", details: nil)) return } - let time = toCMTime(millis: position) - player.seek(time: time) { + player.seek(time: Float(position)) { result(1) } return @@ -223,18 +222,17 @@ public class SwiftAudioplayersDarwinPlugin: NSObject, FlutterPlugin { } player.setSourceUrl( - url: url!, isLocal: isLocal, - mimeType: mimeType, + url: url!, + isLocal: isLocal, completer: { player.eventHandler.onPrepared(isPrepared: true) }, - completerError: { error in - let errorStr: String = error != nil ? "\(error!)" : "Unknown error" + completerError: { player.eventHandler.onError( code: "DarwinAudioError", message: "Failed to set source. For troubleshooting, see " + "https://github.com/bluefireteam/audioplayers/blob/main/troubleshooting.md", - details: "AVPlayerItem.Status.failed on setSourceUrl: \(errorStr)") + details: "AVPlayerItem.Status.failed on setSourceUrl") }) result(1) return diff --git a/packages/audioplayers_darwin/darwin/Classes/WrappedMediaPlayer.swift b/packages/audioplayers_darwin/darwin/Classes/WrappedMediaPlayer.swift index 08fc7f12c..689c492fd 100644 --- a/packages/audioplayers_darwin/darwin/Classes/WrappedMediaPlayer.swift +++ b/packages/audioplayers_darwin/darwin/Classes/WrappedMediaPlayer.swift @@ -1,14 +1,13 @@ -import AVKit +import Foundation +import MediaPlayer private let defaultPlaybackRate: Double = 1.0 - private let defaultVolume: Double = 1.0 - private let defaultLooping: Bool = false typealias Completer = () -> Void - -typealias CompleterError = (Error?) -> Void +typealias CompleterError = () -> Void +typealias StateUpdateDelegate = (MPMusicPlayerController) -> Void class WrappedMediaPlayer { private(set) var eventHandler: AudioPlayersStreamHandler @@ -16,80 +15,84 @@ class WrappedMediaPlayer { var looping: Bool private var reference: SwiftAudioplayersDarwinPlugin - private var player: AVPlayer + private var player: MPMusicPlayerController private var playbackRate: Double private var volume: Double - private var url: String? + private var id: UInt64? - private var completionObserver: TimeObserver? - private var playerItemStatusObservation: NSKeyValueObservation? + var stateUpdateDelegate: StateUpdateDelegate? init( reference: SwiftAudioplayersDarwinPlugin, eventHandler: AudioPlayersStreamHandler, - player: AVPlayer = AVPlayer.init(), + player: MPMusicPlayerController = MPMusicPlayerController.applicationMusicPlayer, playbackRate: Double = defaultPlaybackRate, volume: Double = defaultVolume, looping: Bool = defaultLooping, - url: String? = nil + url: UInt64? = nil ) { self.reference = reference self.eventHandler = eventHandler self.player = player - self.completionObserver = nil - self.playerItemStatusObservation = nil self.isPlaying = false self.playbackRate = playbackRate self.volume = volume self.looping = looping - self.url = url + self.id = url + + self.startNotifications() } func setSourceUrl( url: String, isLocal: Bool, - mimeType: String? = nil, completer: Completer? = nil, completerError: CompleterError? = nil ) { - let playbackStatus = player.currentItem?.status - - if self.url != url || playbackStatus == .failed || playbackStatus == nil { - reset() - self.url = url - do { - let playerItem = try createPlayerItem(url: url, isLocal: isLocal, mimeType: mimeType) - // Need to observe item status immediately after creating: - setUpPlayerItemStatusObservation( - playerItem, - completer: completer, - completerError: completerError) - // Replacing the player item triggers completion in setUpPlayerItemStatusObservation - self.player.replaceCurrentItem(with: playerItem) - self.setUpSoundCompletedObserver(self.player, playerItem) - } catch { - completerError?(error) - } + let persistentId = UInt64(url) + let playbackStatus = player.playbackState + + if self.id != persistentId || persistentId != nil || playbackStatus == .interrupted || playbackStatus == .stopped { + reset() + self.id = persistentId + do { + let playerItem = try createPlayerItem(persistentId!, isLocal) + + // Replacing the player item triggers completion in setUpPlayerItemStatusObservation + replaceItem(with: playerItem) + + player.prepareToPlay(completionHandler: { error in + if error == nil { + self.updateDuration() + completer?() + } else { + self.reset() + completerError?() + } + }) + } catch { + completerError?() + } } else { - if playbackStatus == .readyToPlay { + if player.isPreparedToPlay { completer?() } } } func getDuration() -> Int? { - guard let duration = getDurationCMTime() else { + guard let duration = getDurationTimeInterval() else { return nil } - return fromCMTime(time: duration) + return Int(duration) * 1000 } func getCurrentPosition() -> Int? { - guard let time = getCurrentCMTime() else { + guard let time = getCurrentTimeInterval() else { return nil } - return fromCMTime(time: time) + return Int(time) * 1000 } func pause() { @@ -99,171 +102,103 @@ class WrappedMediaPlayer { func resume() { isPlaying = true - configParameters(player: player) - if #available(iOS 10.0, macOS 10.12, *) { - player.playImmediately(atRate: Float(playbackRate)) - } else { player.play() - } updateDuration() } - func setVolume(volume: Double) { - self.volume = volume - player.volume = Float(volume) - } + func setVolume(volume: Double) { + self.volume = volume + } func setPlaybackRate(playbackRate: Double) { self.playbackRate = playbackRate - if isPlaying { - // Setting the rate causes the player to resume playing. So setting it only, when already playing. - player.rate = Float(playbackRate) - } } - func seek(time: CMTime, completer: Completer? = nil) { - guard let currentItem = player.currentItem else { - completer?() - return - } - currentItem.seek(to: time) { - finished in - if !self.isPlaying { - self.player.pause() - } + func seek(time: Float, completer: Completer? = nil) { + player.currentPlaybackTime = TimeInterval(time/1000) self.eventHandler.onSeekComplete() - if finished { - completer?() - } - } + completer?() } func stop(completer: Completer? = nil) { pause() - seek(time: toCMTime(millis: 0), completer: completer) + seek(time: Float(0), completer: completer) } func release(completer: Completer? = nil) { stop { self.reset() - self.url = nil + self.id = nil completer?() } } func dispose(completer: Completer? = nil) { + player.endGeneratingPlaybackNotifications() release { + self.stopNotifications() completer?() } } - private func getDurationCMTime() -> CMTime? { - return player.currentItem?.asset.duration + private func getDurationTimeInterval() -> TimeInterval? { + return player.nowPlayingItem?.playbackDuration } - private func getCurrentCMTime() -> CMTime? { - return player.currentItem?.currentTime() + private func getCurrentTimeInterval() -> TimeInterval? { + return player.currentPlaybackTime } - private func createPlayerItem( - url: String, - isLocal: Bool, - mimeType: String? = nil - ) throws -> AVPlayerItem { - guard - let parsedUrl = isLocal - ? URL(fileURLWithPath: url.deletingPrefix("file://")) : URL(string: url) - else { - throw AudioPlayerError.error("Url not valid: \(url)") - } - - let playerItem: AVPlayerItem - - if let unwrappedMimeType = mimeType { - if #available(iOS 17, macOS 14.0, *) { - let asset = AVURLAsset( - url: parsedUrl, options: [AVURLAssetOverrideMIMETypeKey: unwrappedMimeType]) - playerItem = AVPlayerItem(asset: asset) + private func createPlayerItem(_ id: UInt64, _ isLocal: Bool) throws -> MPMediaItem { + let songFilter = MPMediaPropertyPredicate(value: id, forProperty: MPMediaItemPropertyPersistentID, comparisonType: .equalTo) + let query = MPMediaQuery(filterPredicates: Set([songFilter])) + if let items = query.items, let song = items.first { + return song } else { - let asset = AVURLAsset( - url: parsedUrl, options: ["AVURLAssetOutOfBandMIMETypeKey": unwrappedMimeType]) - playerItem = AVPlayerItem(asset: asset) + throw AudioPlayerError.error("ID not valid: \(id)") } - } else { - playerItem = AVPlayerItem(url: parsedUrl) - } - - playerItem.audioTimePitchAlgorithm = AVAudioTimePitchAlgorithm.timeDomain - return playerItem } - private func setUpPlayerItemStatusObservation( - _ playerItem: AVPlayerItem, - completer: Completer? = nil, - completerError: CompleterError? = nil - ) { - playerItemStatusObservation = playerItem.observe(\AVPlayerItem.status) { (playerItem, change) in - let status = playerItem.status - self.eventHandler.onLog(message: "player status: \(status), change: \(change)") - - switch playerItem.status { - case .readyToPlay: - self.updateDuration() - completer?() - case .failed: - self.reset() - completerError?(nil) - default: - break - } - } - } - - private func setUpSoundCompletedObserver(_ player: AVPlayer, _ playerItem: AVPlayerItem) { - let observer = NotificationCenter.default.addObserver( - forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, - object: playerItem, - queue: nil - ) { - [weak self] (notification) in - self?.onSoundComplete() - } - self.completionObserver = TimeObserver(player: player, observer: observer) - } - - private func configParameters(player: AVPlayer) { - if isPlaying { - player.volume = Float(volume) - player.rate = Float(playbackRate) - } - } + public func startNotifications() { + player.beginGeneratingPlaybackNotifications() + NotificationCenter.default.addObserver(self, + selector: #selector(stateChanged), + name: .MPMusicPlayerControllerPlaybackStateDidChange, + object: player) + NotificationCenter.default.addObserver(self, + selector: #selector(stateChanged), + name: .MPMusicPlayerControllerNowPlayingItemDidChange, + object: player) + } + + public func stopNotifications() { + player.endGeneratingPlaybackNotifications() + NotificationCenter.default.removeObserver(self, + name: .MPMusicPlayerControllerPlaybackStateDidChange, + object: player) + NotificationCenter.default.removeObserver(self, + name: .MPMusicPlayerControllerNowPlayingItemDidChange, + object: player) + } private func reset() { - playerItemStatusObservation?.invalidate() - playerItemStatusObservation = nil - if let cObserver = completionObserver { - NotificationCenter.default.removeObserver(cObserver.observer) - completionObserver = nil - } - player.replaceCurrentItem(with: nil) + stopNotifications() + replaceItem(with: nil) } - private func updateDuration() { - guard let duration = player.currentItem?.asset.duration else { - return - } - if CMTimeGetSeconds(duration) > 0 { - let millis = fromCMTime(time: duration) - eventHandler.onDuration(millis: millis) + private func updateDuration() { + let duration = getDuration() ?? 0 + if duration > 0 { + eventHandler.onDuration(millis: duration) + } } - } private func onSoundComplete() { if !isPlaying { return } - seek(time: toCMTime(millis: 0)) { + seek(time: 0) { if self.looping { self.resume() } else { @@ -274,4 +209,21 @@ class WrappedMediaPlayer { reference.controlAudioSession() eventHandler.onComplete() } + + private func replaceItem(with: MPMediaItem?) { + setQueue(song: with) + } + + private func setQueue(song: MPMediaItem?) { + if song == nil { + player.stop() + } else { + let descriptor = MPMusicPlayerMediaItemQueueDescriptor(itemCollection: MPMediaItemCollection(items: [song!])) + player.setQueue(with: descriptor) + } + } + + @objc private func stateChanged(notification: NSNotification) { + stateUpdateDelegate?(player) + } } diff --git a/packages/audioplayers_web/pubspec.yaml b/packages/audioplayers_web/pubspec.yaml index 1a2aa517a..19ed30111 100644 --- a/packages/audioplayers_web/pubspec.yaml +++ b/packages/audioplayers_web/pubspec.yaml @@ -25,5 +25,5 @@ dev_dependencies: sdk: flutter environment: - sdk: '>=3.3.0 <4.0.0' - flutter: '>=3.19.0' + sdk: '>=3.0.0 <4.0.0' + flutter: '>=3.13.0'