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'