diff --git a/android/build.gradle b/android/build.gradle index be8e301..b44903d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -28,7 +28,6 @@ apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 31 - buildToolsVersion "33.0.0" sourceSets { main.java.srcDirs += 'src/main/kotlin' diff --git a/android/src/main/aidl/tech/soit/quiet/ISessionDataProvider.aidl b/android/src/main/aidl/tech/soit/quiet/ISessionDataProvider.aidl new file mode 100644 index 0000000..8c4c946 --- /dev/null +++ b/android/src/main/aidl/tech/soit/quiet/ISessionDataProvider.aidl @@ -0,0 +1,17 @@ +// ISessionDataProvider.aidl +package tech.soit.quiet; + +import tech.soit.quiet.player.MusicMetadata; +import tech.soit.quiet.MusicResult; + +parcelable ArtworkData; + +interface ISessionDataProvider { + + ArtworkData loadArtwork(in MusicMetadata metadata); + + String getPlayerUrl(String id, String fallbackUrl); + +} + + diff --git a/android/src/main/kotlin/tech/soit/quiet/ArtworkData.kt b/android/src/main/kotlin/tech/soit/quiet/ArtworkData.kt new file mode 100644 index 0000000..d89b727 --- /dev/null +++ b/android/src/main/kotlin/tech/soit/quiet/ArtworkData.kt @@ -0,0 +1,30 @@ +package tech.soit.quiet + +import android.os.Parcel +import android.os.Parcelable + +class ArtworkData( + val color: Int?, + val image: ByteArray +) : Parcelable { + + constructor(source: Parcel) : this( + source.readValue(Int::class.java.classLoader) as Int?, + source.createByteArray()!! + ) + + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { + writeValue(color) + writeByteArray(image) + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): ArtworkData = ArtworkData(source) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/tech/soit/quiet/MusicPlayerUiPlugin.kt b/android/src/main/kotlin/tech/soit/quiet/MusicPlayerUiPlugin.kt index 4bd919b..1b25714 100644 --- a/android/src/main/kotlin/tech/soit/quiet/MusicPlayerUiPlugin.kt +++ b/android/src/main/kotlin/tech/soit/quiet/MusicPlayerUiPlugin.kt @@ -4,7 +4,14 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection +import android.net.Uri import android.os.IBinder +import android.os.SystemClock +import com.google.android.exoplayer2.* +import com.google.android.exoplayer2.audio.AudioAttributes +import com.google.android.exoplayer2.source.ProgressiveMediaSource +import com.google.android.exoplayer2.upstream.DataSource +import com.google.android.exoplayer2.upstream.DefaultDataSource import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel @@ -23,11 +30,11 @@ private const val UI_PLUGIN_NAME = "tech.soit.quiet/player.ui" class MusicPlayerUiPlugin : FlutterPlugin { - private var playerUiChannel: MusicPlayerUiChannel? = null + private var playerUiChannel: AudioPlayerChannel? = null override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { val channel = MethodChannel(binding.binaryMessenger, UI_PLUGIN_NAME) - playerUiChannel = MusicPlayerUiChannel(channel, binding.applicationContext) + playerUiChannel = AudioPlayerChannel(channel, binding.applicationContext) channel.setMethodCallHandler(playerUiChannel) } @@ -38,6 +45,185 @@ class MusicPlayerUiPlugin : FlutterPlugin { } +class AudioPlayerChannel( + private val channel: MethodChannel, + private val context: Context +) : MethodChannel.MethodCallHandler { + + companion object { + var lastCreatedPlayerId = 0L + } + + init { + channel.setMethodCallHandler(this) + } + + private val players = mutableMapOf() + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + when (call.method) { + "createPlayer" -> { + val uri = Uri.parse(call.argument("uri")) + val playerId = ++lastCreatedPlayerId + players[playerId] = AudioPlayer(playerId, uri, context, channel) + result.success(playerId) + } + "prepare" -> { + val playerId = call.argument("id")!! + val playWhenReady = call.argument("playWhenReady")!! + players[playerId]?.prepare(playWhenReady) + result.success(null) + } + "setPlayWhenReady" -> { + val playerId = call.argument("id")!! + val playWhenReady = call.argument("playWhenReady")!! + players[playerId]?.setPlayWhenReady(playWhenReady) + result.success(null) + } + + "setVolume" -> { + val playerId = call.argument("id")!! + val volume = call.argument("volume")!! + players[playerId]?.setVolume(volume.toFloat()) + result.success(null) + } + "seekTo" -> { + val playerId = call.argument("id")!! + val position = call.argument("position")!! + players[playerId]?.seekTo(position) + result.success(null) + } + "getBufferedPosition" -> { + val playerId = call.argument("id")!! + val position = players[playerId]?.getBufferedPosition() + result.success(position) + } + "dispose" -> { + val playerId = call.argument("id")!! + players[playerId]?.release() + players.remove(playerId) + result.success(null) + } + else -> { + result.notImplemented() + } + + } + } + + fun destroy() { + players.values.forEach { it.release() } + players.clear() + } + +} + +class AudioPlayer( + private val playerId: Long, + uri: Uri, + context: Context, + private val channel: MethodChannel +) : Player.Listener { + + companion object { + private val audioAttribute = AudioAttributes.Builder() + .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) + .setUsage(C.USAGE_MEDIA) + .build() + } + + // Wrap a SimpleExoPlayer with a decorator to handle audio focus for us. + private val player: ExoPlayer = ExoPlayer.Builder(context) + .setAudioAttributes(audioAttribute, true) + .build() + + + init { + val factory: DataSource.Factory = DefaultDataSource.Factory(context) + player.setMediaSource( + ProgressiveMediaSource.Factory(factory) + .createMediaSource(MediaItem.fromUri(uri)) + ) + player.addListener(this) + } + + fun setPlayWhenReady(playWhenReady: Boolean) { + player.playWhenReady = playWhenReady + } + + fun prepare(playWhenReady: Boolean) { + player.prepare() + player.playWhenReady = playWhenReady + } + + fun setVolume(volume: Float) { + player.volume = volume + } + + fun seekTo(position: Long) { + player.seekTo(position) + } + + fun release() { + player.release() + } + + fun getBufferedPosition(): Long { + return player.bufferedPosition + } + + override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { + notifyPositionChanged() + channel.invokeMethod( + "onPlayWhenReadyChanged", mapOf( + "id" to playerId, + "playWhenReady" to playWhenReady, + "reason" to reason, + ) + ) + } + + private fun notifyPositionChanged() { + channel.invokeMethod( + "onPositionChanged", mapOf( + "id" to playerId, + "position" to player.currentPosition, + "duration" to player.duration, + "updateTime" to SystemClock.elapsedRealtime(), + ) + ) + } + + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + notifyPositionChanged() + } + + override fun onPlaybackStateChanged(playbackState: Int) { + channel.invokeMethod( + "onPlaybackStateChanged", mapOf( + "id" to playerId, + "playbackState" to playbackState, + "duration" to player.duration, + ) + ) + } + + override fun onPlayerError(error: PlaybackException) { + channel.invokeMethod( + "onPlayerError", mapOf( + "id" to playerId, + "errorCode" to error.errorCode, + "message" to error.message, + ) + ) + } + +} + private class MusicPlayerUiChannel( channel: MethodChannel, context: Context diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 97c6db3..0856080 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -48,7 +48,7 @@ android { applicationId "tech.soit.example" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion flutter.minSdkVersion + minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 245564b..ea58a1b 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -4,13 +4,10 @@ PODS: - Flutter - SwiftAudioEx (~> 0.15.3) - SwiftAudioEx (0.15.3) - - system_clock (0.0.1): - - Flutter DEPENDENCIES: - Flutter (from `Flutter`) - music_player (from `.symlinks/plugins/music_player/ios`) - - system_clock (from `.symlinks/plugins/system_clock/ios`) SPEC REPOS: trunk: @@ -21,14 +18,11 @@ EXTERNAL SOURCES: :path: Flutter music_player: :path: ".symlinks/plugins/music_player/ios" - system_clock: - :path: ".symlinks/plugins/system_clock/ios" SPEC CHECKSUMS: Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 music_player: 2cb8e84bc904013b04392560f0b94d1e96e71e14 SwiftAudioEx: 83eabba2940924fc1c0d5cb0896049921365229c - system_clock: 3efb51d18e565092e2a97bc605c6255f328bd13a PODFILE CHECKSUM: 4e8f8b2be68aeea4c0d5beb6ff1e79fface1d048 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index eaf4c04..bba17e1 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -221,6 +221,7 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -235,6 +236,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 55bfd55..9ad7a15 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -43,5 +43,7 @@ CADisableMinimumFrameDurationOnPhone + UIApplicationSupportsIndirectInputEvents + diff --git a/example/lib/main.dart b/example/lib/main.dart index f3a752e..6f870f5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; import 'package:music_player_example/page_play_queue.dart'; -import 'package:music_player_example/player/background.dart'; import 'package:overlay_support/overlay_support.dart'; +import 'player/music_metadata.dart'; import 'player/player.dart'; import 'player/player_bottom_controller.dart'; @@ -55,34 +54,6 @@ void main() { runApp(ExampleApp()); } -@pragma("vm:entry-point") -void playerBackgroundService() async { - debugPrint("start playerBackgroundService"); - Logger.root.onRecord.listen((record) { - print('${record.level.name}: ${record.time}: ${record.message}'); - }); - await Future.delayed(const Duration(milliseconds: 100)); - runBackgroundService( - config: Config( - pauseWhenTaskRemoved: false, - ), - playUriInterceptor: (mediaId, fallbackUrl) async { - debugPrint("get media play uri : $mediaId , $fallbackUrl"); - if (mediaId == 'rise') return "asset:///tracks/rise.mp3"; - return fallbackUrl; - }, - imageLoadInterceptor: (metadata) async { - debugPrint("load image for ${metadata.mediaId} , ${metadata.title}"); - if (metadata.mediaId == "bamboo") { - final data = await rootBundle.load("images/bamboo.jpg"); - return Uint8List.view(data.buffer); - } - return null; - }, - playQueueInterceptor: ExamplePlayQueueInterceptor(), - ); -} - class ExampleApp extends StatelessWidget { @override Widget build(BuildContext context) { diff --git a/example/lib/player/background.dart b/example/lib/player/background.dart index 56d10ea..4966242 100644 --- a/example/lib/player/background.dart +++ b/example/lib/player/background.dart @@ -4,9 +4,8 @@ import 'package:music_player_example/main.dart'; class ExamplePlayQueueInterceptor extends PlayQueueInterceptor { @override - Future> fetchMoreMusic( - BackgroundPlayQueue queue, PlayMode playMode) async { - if (queue.queueId == "fm" && queue.queue.length == 1) { + Future> fetchMoreMusic(PlayMode playMode) async { + if (playMode.index == 4) { return medias.getRange(1, 3).toList(); } throw MissingPluginException(); diff --git a/lib/src/player/music_metadata.dart b/example/lib/player/music_metadata.dart similarity index 82% rename from lib/src/player/music_metadata.dart rename to example/lib/player/music_metadata.dart index e954669..66edf5f 100644 --- a/lib/src/player/music_metadata.dart +++ b/example/lib/player/music_metadata.dart @@ -1,5 +1,7 @@ +import 'package:music_player_example/player/player.dart'; + /// Music metadata -class MusicMetadata { +class MusicMetadata extends PlayItem { final Map? extras; final String mediaId; final String? mediaUri; @@ -31,7 +33,7 @@ class MusicMetadata { ); } - Map toMap() { + Map toMap() { return { "extras": extras, "mediaId": mediaId, @@ -47,4 +49,9 @@ class MusicMetadata { String toString() { return 'MusicMetadata{mediaId: $mediaId, title: $title, subtitle: $subtitle}'; } + + @override + Future playerUrl() { + return Future.value(mediaUri!); + } } diff --git a/example/lib/player/queue.dart b/example/lib/player/queue.dart new file mode 100644 index 0000000..a6c316c --- /dev/null +++ b/example/lib/player/queue.dart @@ -0,0 +1,56 @@ +import 'package:music_player_example/player/player.dart'; + +class AppPlayQueue implements PlayQueue { + final String queueId; + + final List queue; + + bool get isEmpty => queue.isEmpty; + + const AppPlayQueue({ + required this.queueId, + required this.queue, + }); + + const AppPlayQueue.empty() + : this( + queueId: "empty", + queue: const [], + ); + + @override + Future getNext(PlayItem? current) async { + if (isEmpty) { + return null; + } + if (current == null) { + return queue.first; + } + final index = queue.indexOf(current); + if (index == -1) { + return null; + } + if (index == queue.length - 1) { + return null; + } + return queue[index + 1]; + } + + @override + Future getPrevious(PlayItem? current) async { + if (isEmpty) { + return null; + } + if (current == null) { + return queue.first; + } + final index = queue.indexOf(current); + if (index == -1) { + return null; + } + if (index == 0) { + return null; + } + return queue[index - 1]; + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 5ff43f9..eb0a1bf 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -213,5 +213,5 @@ packages: source: hosted version: "2.1.4" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.19.0 <3.0.0" flutter: ">=3.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9160d5c..6e45490 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -3,7 +3,7 @@ description: Demonstrates how to use the flutter_music_player plugin. publish_to: 'none' environment: - sdk: ">=2.6.0 <3.0.0" + sdk: ">=2.19.0 <3.0.0" dependencies: flutter: diff --git a/ios/Classes/MusicMetadata.swift b/ios/Classes/MusicMetadata.swift index ef39764..7f96f1a 100644 --- a/ios/Classes/MusicMetadata.swift +++ b/ios/Classes/MusicMetadata.swift @@ -9,7 +9,7 @@ class MusicMetadata: Equatable { private let data: [String: Any] public init(map: [String: Any]) { - self.data = map + data = map } convenience init?(any: Any?) { @@ -61,12 +61,12 @@ class MetadataAudioItem: AudioItem, RemoteCommandable { private let uri: String - private let servicePlugin: MusicPlayerServicePlugin + private let source: MusicPlayerSource - public init(metadata: MusicMetadata, uri: String, servicePlugin: MusicPlayerServicePlugin) { + public init(metadata: MusicMetadata, uri: String, source: MusicPlayerSource) { self.metadata = metadata self.uri = uri - self.servicePlugin = servicePlugin + self.source = source } func getSourceUrl() -> String { @@ -74,15 +74,7 @@ class MetadataAudioItem: AudioItem, RemoteCommandable { return uri } if "asset".caseInsensitiveCompare(url.scheme ?? "") == .orderedSame { - if let registrar = servicePlugin.registrar { - let assetKey = registrar.lookupKey(forAsset: url.path) - guard let path = Bundle.main.path(forResource: assetKey, ofType: nil) else { - debugPrint("resource not found : \(assetKey)") - return uri - } - debugPrint("resource found: \(path)") - return URL(fileURLWithPath: path).absoluteString - } + return source.loadAssetResource(url: url) } return uri } @@ -104,6 +96,6 @@ class MetadataAudioItem: AudioItem, RemoteCommandable { } func getArtwork(_ handler: @escaping (UIImage?) -> Void) { - servicePlugin.loadImage(metadata: metadata, completion: handler) + source.loadImage(metadata: metadata, completion: handler) } } diff --git a/ios/Classes/MusicPlayer.swift b/ios/Classes/MusicPlayer.swift index 3ff597c..3878be6 100644 --- a/ios/Classes/MusicPlayer.swift +++ b/ios/Classes/MusicPlayer.swift @@ -7,6 +7,19 @@ import Foundation import MediaPlayer import SwiftAudioEx +protocol MusicPlayerSource { + func getPlayUrl(mediaId: String, fallback: String?, completion: @escaping (String?) -> Void) + + func loadImage(metadata: MusicMetadata, completion: @escaping (UIImage?) -> Void) + + func onNextNoMoreMusic(_ queue: PlayQueue, _ mode: PlayMode, completion: @escaping (MusicMetadata?) -> Void) + + func onPreviousNoMoreMusic(_ queue: PlayQueue, _ mode: PlayMode, completion: @escaping (MusicMetadata?) -> Void) + + // load flutter asset resource path by player item metadata url. + func loadAssetResource(url: URL) -> String +} + /// /// Music Player. /// @@ -24,7 +37,7 @@ class MusicPlayer: NSObject, MusicPlayerSession { if reason == .playedUntilEnd { if self.playMode == .single { self.player.seek(to: 0) - self.player.play(); + self.player.play() return } self.skipToNext() @@ -78,7 +91,7 @@ class MusicPlayer: NSObject, MusicPlayerSession { private var playbackError: PlaybackError? - private let servicePlugin = MusicPlayerServicePlugin.shared + var playerSource: MusicPlayerSource? var playMode: PlayMode = .sequence { didSet { @@ -196,9 +209,16 @@ class MusicPlayer: NSObject, MusicPlayerSession { completion(nil) return } - servicePlugin.getPlayUrl(mediaId: metadata.mediaId, fallback: metadata.mediaUri) { url in + + guard let source = playerSource else { + assert(false, "playerSource not avaliable.") + completion(nil) + return + } + + source.getPlayUrl(mediaId: metadata.mediaId, fallback: metadata.mediaUri) { url in if let url = url { - completion(MetadataAudioItem(metadata: metadata, uri: url, servicePlugin: self.servicePlugin)) + completion(MetadataAudioItem(metadata: metadata, uri: url, source: source)) } else { completion(nil) } @@ -271,7 +291,11 @@ class MusicPlayer: NSObject, MusicPlayerSession { if metadata != nil { completion(metadata) } else { - servicePlugin.onNextNoMoreMusic(playQueue, playMode, completion: completion) + guard let playerSource = playerSource else { + completion(nil) + return + } + playerSource.onNextNoMoreMusic(playQueue, playMode, completion: completion) } } @@ -280,7 +304,11 @@ class MusicPlayer: NSObject, MusicPlayerSession { if metadata != nil { completion(metadata) } else { - servicePlugin.onPreviousNoMoreMusic(playQueue, playMode, completion: completion) + guard let playerSource = playerSource else { + completion(nil) + return + } + playerSource.onPreviousNoMoreMusic(playQueue, playMode, completion: completion) } } diff --git a/ios/Classes/SwiftMusicPlayerPlugin.swift b/ios/Classes/SwiftMusicPlayerPlugin.swift index 3be4165..d9c9d51 100644 --- a/ios/Classes/SwiftMusicPlayerPlugin.swift +++ b/ios/Classes/SwiftMusicPlayerPlugin.swift @@ -8,11 +8,15 @@ public class SwiftMusicPlayerUiPlugin: NSObject, FlutterPlugin { let playerCallback: MusicPlayerCallback + private let registrar: FlutterPluginRegistrar + public init(channel: FlutterMethodChannel, registrar: FlutterPluginRegistrar) { self.channel = channel - self.playerCallback = ChannelPlayerCallback(channel) + playerCallback = ChannelPlayerCallback(channel) + self.registrar = registrar super.init() player.addCallback(playerCallback) + player.playerSource = self } let player: MusicPlayer = MusicPlayer.shared @@ -23,7 +27,6 @@ public class SwiftMusicPlayerUiPlugin: NSObject, FlutterPlugin { let channel = FlutterMethodChannel(name: UI_CHANNEL_NAME, binaryMessenger: registrar.messenger()) let instance = SwiftMusicPlayerUiPlugin(channel: channel, registrar: registrar) registrar.addMethodCallDelegate(instance, channel: channel) - MusicPlayerServicePlugin.shared.start() } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { @@ -99,7 +102,7 @@ private class ChannelPlayerCallback: MusicPlayerCallback { private let methodChannel: FlutterMethodChannel init(_ channel: FlutterMethodChannel) { - self.methodChannel = channel + methodChannel = channel } func onPlaybackStateChanged(_ state: PlaybackState) { @@ -119,143 +122,68 @@ private class ChannelPlayerCallback: MusicPlayerCallback { } } -public class MusicPlayerServicePlugin: NSObject, FlutterPlugin { - public static let shared = MusicPlayerServicePlugin() - - public static func register(with registrar: FlutterPluginRegistrar) { - fatalError("do not call register!") - } - - private let engine: FlutterEngine - - private var started: Bool = false - - private var channel: FlutterMethodChannel? - - public var registrar: FlutterPluginRegistrar? - - override private init() { - engine = FlutterEngine(name: "player-service-engine") - super.init() - } - - func start() { - if started { - return - } - started = true - if !engine.run(withEntrypoint: "playerBackgroundService", libraryURI: nil) { - debugPrint("run 'playerBackgroundService' failed.") - } - DispatchQueue.main.async(execute: waitEngineRunning) - } - - private func waitEngineRunning() { - if engine.isolateId != nil { - debugPrint("service engine: \(engine.isolateId ?? "nil")") - let registrar = engine.registrar(forPlugin: String(describing: type(of: MusicPlayerServicePlugin.self)))! - let channel = FlutterMethodChannel(name: "tech.soit.quiet/background_callback", binaryMessenger: registrar.messenger()) - registrar.addMethodCallDelegate(self, channel: channel) - // invoke GeneratedPluginRegistrant by selector. - if let a = NSClassFromString("GeneratedPluginRegistrant") as? NSObject.Type { - a.perform(NSSelectorFromString("registerWithRegistry:"), with: engine as FlutterPluginRegistry) - } else { - debugPrint("Tried to automatically register plugins with FlutterEngine \(engine) but could not find and invoke the GeneratedPluginRegistrant.") - } - self.registrar = registrar - self.channel = channel - } else { - DispatchQueue.main.async(execute: waitEngineRunning) - } - } - - public func handle(_ call: FlutterMethodCall, result: FlutterResult) { - switch call.method { - case "insertToPlayQueue": - let arg = call.arguments as! [String: Any] - let list = (arg["list"] as! [[String: Any]]).map { map -> MusicMetadata in - MusicMetadata(map: map) - } - let index = arg["index"] as! Int - MusicPlayer.shared.insertMetadataList(list, index) - result(nil) - break - case "updateConfig": - // TODO: config handle - result(nil) - break - default: - result(FlutterMethodNotImplemented) - } - } - +extension SwiftMusicPlayerUiPlugin: MusicPlayerSource { func getPlayUrl(mediaId: String, fallback: String?, completion: @escaping (String?) -> Void) { - if let channel = self.channel { - channel.invokeMethod("getPlayUrl", arguments: ["id": mediaId, "url": fallback]) { any in - if let result = any as? String { - completion(result) - } else if FlutterMethodNotImplemented.isEqual(any) { - completion(fallback) - } else { - completion(nil) - } + channel.invokeMethod("getPlayUrl", arguments: ["id": mediaId, "url": fallback]) { any in + if let result = any as? String { + completion(result) + } else if FlutterMethodNotImplemented.isEqual(any) { + completion(fallback) + } else { + completion(nil) } - } else { - completion(fallback) } } func loadImage(metadata: MusicMetadata, completion: @escaping (UIImage?) -> Void) { - if let channel = self.channel { - channel.invokeMethod("loadImage", arguments: metadata.toMap()) { result in - if let result = result as? FlutterStandardTypedData { - completion(UIImage(data: result.data)) - } else { - completion(nil) - } + channel.invokeMethod("loadImage", arguments: metadata.toMap()) { result in + if let result = result as? FlutterStandardTypedData { + completion(UIImage(data: result.data)) + } else { + completion(nil) } - } else { - completion(nil) } } func onNextNoMoreMusic(_ queue: PlayQueue, _ mode: PlayMode, completion: @escaping (MusicMetadata?) -> Void) { - if let channel = channel { - channel.invokeMethod("onPlayNextNoMoreMusic", arguments: [ - "queue": queue.toMap(), - "playMode": mode.rawValue, - ]) { result in - if FlutterMethodNotImplemented.isEqual(result) { - if mode == .shuffle { - queue.generateShuffleList() - } - completion(queue.getNext(nil, playMode: mode)) - } else { - completion(MusicMetadata(any: result)) + channel.invokeMethod("onPlayNextNoMoreMusic", arguments: [ + "queue": queue.toMap(), + "playMode": mode.rawValue, + ]) { result in + if FlutterMethodNotImplemented.isEqual(result) { + if mode == .shuffle { + queue.generateShuffleList() } + completion(queue.getNext(nil, playMode: mode)) + } else { + completion(MusicMetadata(any: result)) } - } else { - completion(queue.getNext(nil, playMode: mode)) } } func onPreviousNoMoreMusic(_ queue: PlayQueue, _ mode: PlayMode, completion: @escaping (MusicMetadata?) -> Void) { - if let channel = channel { - channel.invokeMethod("onPlayPreviousNoMoreMusic", arguments: [ - "queue": queue.toMap(), - "playMode": mode.rawValue, - ]) { result in - if FlutterMethodNotImplemented.isEqual(result) { - if mode == .shuffle { - queue.generateShuffleList() - } - completion(queue.getPrevious(nil, playMode: mode)) - } else { - completion(MusicMetadata(any: result)) + channel.invokeMethod("onPlayPreviousNoMoreMusic", arguments: [ + "queue": queue.toMap(), + "playMode": mode.rawValue, + ]) { result in + if FlutterMethodNotImplemented.isEqual(result) { + if mode == .shuffle { + queue.generateShuffleList() } + completion(queue.getPrevious(nil, playMode: mode)) + } else { + completion(MusicMetadata(any: result)) } - } else { - completion(queue.getPrevious(nil, playMode: mode)) } } + + func loadAssetResource(url: URL) -> String { + let assetKey = registrar.lookupKey(forAsset: url.path) + guard let path = Bundle.main.path(forResource: assetKey, ofType: nil) else { + debugPrint("resource not found : \(assetKey)") + return url.absoluteString + } + debugPrint("resource found: \(path)") + return URL(fileURLWithPath: path).absoluteString + } } diff --git a/lib/music_player.dart b/lib/music_player.dart index 45b8bdc..d9c68cf 100644 --- a/lib/music_player.dart +++ b/lib/music_player.dart @@ -1,9 +1 @@ -export 'src/channel_service.dart'; -export 'src/channel_ui.dart'; -export "src/model/playback_error.dart"; -export 'src/player/music_metadata.dart'; -export 'src/player/play_mode.dart'; -export 'src/player/play_queue.dart'; -export 'src/player/playback_state.dart'; -export 'src/player/transport_controls.dart'; -export 'src/utils/progress_track_container.dart'; +export 'src/queue.dart'; diff --git a/lib/src/channel_service.dart b/lib/src/channel_service.dart deleted file mode 100644 index 8489c1b..0000000 --- a/lib/src/channel_service.dart +++ /dev/null @@ -1,212 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:logging/logging.dart'; -import 'package:music_player/music_player.dart'; -import 'package:music_player/src/internal/serialization.dart'; -import 'package:music_player/src/player/music_player.dart'; - -/// -/// Interceptor when player try to load a media source -/// -/// -/// [mediaId] The id of media. [MediaMetadata.mediaId] -/// [fallbackUri] media origin uri. -/// -/// @return media uri which should -/// -typedef PlayUriInterceptor = Future Function( - String? mediaId, String? fallbackUri); - -typedef ImageLoadInterceptor = Future Function( - MusicMetadata metadata); - -class Config { - final bool enableCache; - - final String? userAgent; - - /// For android only. - /// pause player when the user has removed a task - /// that comes from the service's application. - final bool pauseWhenTaskRemoved; - - const Config({ - this.enableCache = false, - this.userAgent, - this.pauseWhenTaskRemoved = true, - }); - - Map toMap() { - return { - 'enableCache': enableCache, - 'userAgent': userAgent, - 'pauseWhenTaskRemoved': pauseWhenTaskRemoved, - }; - } -} - -/// -/// handle background callback -/// -Future runBackgroundService({ - Config config = const Config(), - PlayUriInterceptor? playUriInterceptor, - ImageLoadInterceptor? imageLoadInterceptor, - PlayQueueInterceptor? playQueueInterceptor, -}) async { - final log = Logger("runBackgroundService"); - WidgetsFlutterBinding.ensureInitialized(); - // decrease background image memory - PaintingBinding.instance.imageCache.maximumSize = 20 << 20; // 20 MB - const serviceChannel = MethodChannel("tech.soit.quiet/background_callback"); - final player = BackgroundMusicPlayer._internal(serviceChannel, MusicPlayer()); - playQueueInterceptor?._player = player; - serviceChannel.setMethodCallHandler((call) async { - log.fine("background: ${call.method} args = ${call.arguments}"); - switch (call.method) { - case 'loadImage': - if (imageLoadInterceptor != null) { - return await imageLoadInterceptor( - MusicMetadata.fromMap(call.arguments)); - } - throw MissingPluginException(); - case 'getPlayUrl': - if (playUriInterceptor != null) { - final String? id = call.arguments['id']; - final String? fallbackUrl = call.arguments['url']; - return await playUriInterceptor(id, fallbackUrl); - } - throw MissingPluginException(); - case "onPlayNextNoMoreMusic": - if (playQueueInterceptor != null) { - return (await playQueueInterceptor.onPlayNextNoMoreMusic( - createBackgroundQueue(call.arguments["queue"] as Map), - PlayMode(call.arguments["playMode"] as int?), - )) - ?.toMap(); - } - throw MissingPluginException(); - case "onPlayPreviousNoMoreMusic": - if (playQueueInterceptor != null) { - return (await playQueueInterceptor.onPlayPreviousNoMoreMusic( - createBackgroundQueue(call.arguments["queue"] as Map), - PlayMode(call.arguments["playMode"] as int?), - )) - .toMap(); - } - throw MissingPluginException(); - default: - throw MissingPluginException("can not hanle : ${call.method} "); - } - }); - serviceChannel.invokeMethod('updateConfig', config.toMap()); -} - -class BackgroundMusicPlayer extends Player { - final MethodChannel _serviceChannel; - - final MusicPlayer _player; - - BackgroundMusicPlayer._internal(this._serviceChannel, this._player); - - @override - ValueListenable get metadataListenable => - _player.metadataListenable; - - @override - MusicPlayerValue get value => _player.value; - - @override - ValueListenable get playModeListenable => - _player.playModeListenable; - - @override - ValueListenable get playbackStateListenable => - _player.playbackStateListenable; - - @override - ValueListenable get queueListenable => _player.queueListenable; - - Future insertToPlayQueue(List list, int index) async { - assert(() { - if (index < 0 || index > value.queue.queue.length) { - throw RangeError.range(index, 0, value.queue.queue.length); - } - return true; - }()); - await _serviceChannel.invokeMethod("insertToPlayQueue", { - "index": index, - "list": list.map((e) => e.toMap()).toList(), - }); - } -} - -abstract class BackgroundPlayerCallback { - void onPlaybackStateChanged( - BackgroundMusicPlayer player, PlaybackState state); - - void onMetadataChange(BackgroundMusicPlayer player, MusicMetadata metadata); - - void onPlayQueueChanged(BackgroundMusicPlayer player, PlayQueue queue); - - void onPlayModeChanged(BackgroundMusicPlayer player, PlayMode playMode); -} - -class PlayQueueInterceptor { - void noImplement() { - throw MissingPluginException(); - } - - BackgroundMusicPlayer? _player; - - BackgroundMusicPlayer? get player => _player; - - Future onPlayNextNoMoreMusic( - BackgroundPlayQueue queue, PlayMode playMode) async { - final list = await fetchMoreMusic(queue, playMode); - debugPrint("fetched : $list"); - if (list.isNotEmpty) { - await player!.insertToPlayQueue(list, player!.value.queue.queue.length); - return list.first; - } else { - return null; - } - } - - /// - /// Throw MissingPluginException() to use default playNext behavior. - /// Default Behavior: - /// 1. playMode is [PlayMode.sequence], auto play queue first item - /// 2. playMode is [PlayMode.shuffle], auto generate a new shuffle list, then play from first. - /// 3. playMode is [PlayMode.undefined(any)]. stop play. - /// - /// return null to stop play. - /// - Future> fetchMoreMusic( - BackgroundPlayQueue queue, PlayMode playMode) { - throw MissingPluginException(); - } - - Future onPlayPreviousNoMoreMusic( - BackgroundPlayQueue queue, PlayMode playMode) { - throw MissingPluginException(); - } -} - -class BackgroundPlayQueue extends PlayQueue { - /// shuffle mediaId list for shuffle playMode - final List shuffleQueue; - - BackgroundPlayQueue({ - required this.shuffleQueue, - required String queueId, - required String queueTitle, - Map? extras, - required List queue, - }) : super( - queueId: queueId, - queueTitle: queueTitle, - extras: extras, - queue: queue); -} diff --git a/lib/src/channel_ui.dart b/lib/src/channel_ui.dart deleted file mode 100644 index d50acf5..0000000 --- a/lib/src/channel_ui.dart +++ /dev/null @@ -1,145 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:logging/logging.dart'; -import 'package:music_player/music_player.dart'; -import 'package:music_player/src/player/music_player.dart'; - -import 'internal/serialization.dart'; - -/// MusicPlayer for UI interaction. -class MusicPlayer extends Player { - final log = Logger('MusicPlayer'); - - static const _uiChannel = MethodChannel("tech.soit.quiet/player.ui"); - - static MusicPlayer? _player; - - MusicPlayer._internal() : super() { - _uiChannel.setMethodCallHandler(_handleRemoteCall); - final initResult = _uiChannel.invokeMethod("init"); - _initCompleter.complete(initResult.then((value) => value ?? false)); - - _queue.addListener(notifyListeners); - _playMode.addListener(notifyListeners); - _playbackState.addListener(notifyListeners); - _metadata.addListener(notifyListeners); - } - - factory MusicPlayer() { - return _player ??= MusicPlayer._internal(); - } - - Future setPlayQueue(PlayQueue queue) async { - await _uiChannel.invokeMethod("setPlayQueue", queue.toMap()); - } - - Future getNextMusic(MusicMetadata anchor) async { - final Map map = await _uiChannel.invokeMethod("getNext", anchor.toMap()); - return MusicMetadata.fromMap(map); - } - - Future getPreviousMusic(MusicMetadata metadata) async { - final Map map = - await _uiChannel.invokeMethod("getPrevious", metadata.toMap()); - return MusicMetadata.fromMap(map); - } - - @override - ValueListenable get queueListenable => _queue; - - @override - ValueListenable get playbackStateListenable => _playbackState; - - @override - ValueListenable get playModeListenable => _playMode; - - @override - ValueListenable get metadataListenable => _metadata; - - final ValueNotifier _queue = - ValueNotifier(const PlayQueue.empty()); - final ValueNotifier _playbackState = - ValueNotifier(const PlaybackState.none()); - final ValueNotifier _playMode = ValueNotifier(PlayMode.sequence); - final ValueNotifier _metadata = ValueNotifier(null); - - Future _handleRemoteCall(MethodCall call) async { - log.fine("on MethodCall: ${call.method} args = ${call.arguments}"); - switch (call.method) { - case 'onPlaybackStateChanged': - _playbackState.value = createPlaybackState(call.arguments); - break; - case 'onMetadataChanged': - _metadata.value = call.arguments == null - ? null - : MusicMetadata.fromMap(call.arguments); - break; - case 'onPlayQueueChanged': - _queue.value = PlayQueue.fromMap(call.arguments); - break; - case 'onPlayModeChanged': - _playMode.value = PlayMode(call.arguments as int?); - break; - default: - throw UnimplementedError(); - } - } - - TransportControls transportControls = TransportControls(_uiChannel); - - final Completer _initCompleter = Completer(); - - Future insertToNext(MusicMetadata metadata) { - return _uiChannel.invokeMethod("insertToNext", metadata.toMap()); - } - - Future playWithQueue( - PlayQueue playQueue, { - MusicMetadata? metadata, - }) async { - await setPlayQueue(playQueue); - if (playQueue.isEmpty) { - return; - } - metadata = metadata ?? playQueue.queue.first; - log.fine("playFromMediaId : ${metadata.mediaId}"); - await transportControls.playFromMediaId(metadata.mediaId); - } - - void removeMusicItem(MusicMetadata metadata) {} - - /// Check whether music service already running. - Future isMusicServiceAvailable() { - return _initCompleter.future; - } -} - -class MusicPlayerValue { - final PlayQueue queue; - - final PlayMode playMode; - - final PlaybackState playbackState; - - final MusicMetadata? metadata; - - MusicPlayerValue({ - required this.queue, - this.playMode = PlayMode.sequence, - this.metadata, - this.playbackState = const PlaybackState.none(), - }); - - static final _empty = MusicPlayerValue( - queue: const PlayQueue.empty(), - playMode: PlayMode.sequence, - metadata: null, - playbackState: const PlaybackState.none(), - ); - - factory MusicPlayerValue.none() { - return _empty; - } -} diff --git a/lib/src/internal/ext_copy.dart b/lib/src/internal/ext_copy.dart deleted file mode 100644 index 79fdbb0..0000000 --- a/lib/src/internal/ext_copy.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:music_player/music_player.dart'; - -extension MusicPlayerValueCopy on MusicPlayerValue { - MusicPlayerValue copy({ - PlayMode? playMode, - MusicMetadata? metadata, - PlayQueue? queue, - PlaybackState? state, - }) { - return MusicPlayerValue( - playMode: playMode ?? this.playMode, - metadata: metadata ?? this.metadata, - queue: queue ?? this.queue, - playbackState: state ?? playbackState, - ); - } -} diff --git a/lib/src/internal/player_callback_adapter.dart b/lib/src/internal/player_callback_adapter.dart deleted file mode 100644 index 87770ea..0000000 --- a/lib/src/internal/player_callback_adapter.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/services.dart'; -import 'package:music_player/music_player.dart'; -import 'package:music_player/src/player/music_player_callback.dart'; - -import 'ext_copy.dart'; -import 'serialization.dart'; - -mixin ChannelPlayerCallbackAdapter on ValueNotifier - implements MusicPlayerCallback { - bool handleRemoteCall(MethodCall call) { - switch (call.method) { - case 'onPlaybackStateChanged': - onPlaybackStateChanged(createPlaybackState(call.arguments)); - break; - case 'onMetadataChanged': - onMetadataChange(MusicMetadata.fromMap(call.arguments)); - break; - case 'onPlayQueueChanged': - onPlayQueueChanged(PlayQueue.fromMap(call.arguments)); - break; - case 'onPlayModeChanged': - onPlayModeChanged(PlayMode(call.arguments as int?)); - break; - default: - return false; - } - return true; - } - - @override - void onMetadataChange(MusicMetadata metadata) { - value = value.copy(metadata: metadata); - } - - @override - void onPlayModeChanged(PlayMode playMode) { - value = value.copy(playMode: playMode); - } - - @override - void onPlayQueueChanged(PlayQueue queue) { - value = value.copy(queue: queue); - } - - @override - void onPlaybackStateChanged(PlaybackState state) { - value = value.copy(state: state); - } -} diff --git a/lib/src/internal/serialization.dart b/lib/src/internal/serialization.dart deleted file mode 100644 index b9f33ec..0000000 --- a/lib/src/internal/serialization.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:music_player/music_player.dart'; - -const String objectKeyPrefix = 'tech.soit.queit.player'; - -BackgroundPlayQueue createBackgroundQueue(Map map) { - return BackgroundPlayQueue( - queueId: map['queueId'], - queueTitle: map['queueTitle'], - extras: map['extras'], - queue: (map['queue'] as List) - .cast() - .map((e) => MusicMetadata.fromMap(e)) - .toList(), - shuffleQueue: (map['shuffleQueue'] as List).cast(), - ); -} - -PlaybackState createPlaybackState(Map map) { - return PlaybackState( - state: PlayerState.values[map['state'] as int], - position: map['position'] as int, - bufferedPosition: map['bufferedPosition'] as int, - speed: map['speed'] as double, - error: _createPlaybackError(map['error'] as Map?), - updateTime: map['updateTime'] as int, - ); -} - -PlaybackError? _createPlaybackError(Map? map) { - if (map == null) { - return null; - } - return PlaybackError( - type: ErrorType.values[map["type"] as int], - message: map["message"] ?? "Unknown Error.", - ); -} diff --git a/lib/src/model/playback_error.dart b/lib/src/model/playback_error.dart deleted file mode 100644 index 3260291..0000000 --- a/lib/src/model/playback_error.dart +++ /dev/null @@ -1,37 +0,0 @@ -/// Error description when a non-recoverable playback failure occurs. -class PlaybackError { - /// error type - final ErrorType type; - - /// error message. - final String message; - - PlaybackError({required this.type, required this.message}); - - @override - String toString() { - return 'PlaybackError{type: $type, message: $message}'; - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is PlaybackError && - runtimeType == other.runtimeType && - type == other.type && - message == other.message; - - @override - int get hashCode => type.hashCode ^ message.hashCode; -} - -enum ErrorType { - /// The error occurred when loading data. - source, - - /// The error occurred when renderer data. - renderer, - - /// the error is unknown. - unknown, -} diff --git a/lib/src/player/music_player.dart b/lib/src/player/music_player.dart deleted file mode 100644 index b859ca1..0000000 --- a/lib/src/player/music_player.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:music_player/music_player.dart'; - -abstract class Player extends ChangeNotifier - implements ValueListenable { - /// Current player state value. - @override - MusicPlayerValue get value => MusicPlayerValue( - queue: queue, - playMode: playMode, - playbackState: playbackState, - metadata: metadata, - ); - - /// See [metadataListenable]. - MusicMetadata? get metadata => metadataListenable.value; - - /// See [queueListenable]. - PlayQueue get queue => queueListenable.value; - - /// See [playbackStateListenable]. - PlaybackState get playbackState => playbackStateListenable.value; - - /// See [playModeListenable] - PlayMode get playMode => playModeListenable.value; - - /// Current playing music metadata. - ValueListenable get metadataListenable; - - /// Current playing music queue. - ValueListenable get queueListenable; - - /// Current player's playback state. - ValueListenable get playbackStateListenable; - - /// Current play mode. - /// determine how to play [queueListenable]. - ValueListenable get playModeListenable; -} diff --git a/lib/src/player/music_player_callback.dart b/lib/src/player/music_player_callback.dart deleted file mode 100644 index 8a98a99..0000000 --- a/lib/src/player/music_player_callback.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'music_metadata.dart'; -import 'play_queue.dart'; -import 'playback_state.dart'; -import 'play_mode.dart'; - -abstract class MusicPlayerCallback { - void onPlaybackStateChanged(PlaybackState state); - - void onMetadataChange(MusicMetadata metadata); - - void onPlayQueueChanged(PlayQueue queue); - - void onPlayModeChanged(PlayMode playMode); -} diff --git a/lib/src/player/play_mode.dart b/lib/src/player/play_mode.dart deleted file mode 100644 index 254bc5b..0000000 --- a/lib/src/player/play_mode.dart +++ /dev/null @@ -1,47 +0,0 @@ -class PlayMode { - final int? index; - - const PlayMode._internal(this.index); - - static const shuffle = PlayMode._internal(0); - static const single = PlayMode._internal(1); - static const sequence = PlayMode._internal(2); - - factory PlayMode.undefined(int index) { - assert(!const [0, 1, 2].contains(index), "index can not be 0,1,2"); - return PlayMode(index); - } - - factory PlayMode(int? index) { - if (index == 0) { - return shuffle; - } else if (index == 1) { - return single; - } else if (index == 2) { - return sequence; - } else { - return PlayMode._internal(index); - } - } - - @override - bool operator ==(Object other) => - identical(this, other) || other is PlayMode && index == other.index; - - @override - int get hashCode => index.hashCode; - - @override - String toString() { - switch (index) { - case 0: - return "PlayMode.shuffle"; - case 1: - return "PlayMode.single"; - case 2: - return "PlayMode.sequence"; - default: - return "PlayMode.undefined($index)"; - } - } -} diff --git a/lib/src/player/play_queue.dart b/lib/src/player/play_queue.dart deleted file mode 100644 index cfce5ec..0000000 --- a/lib/src/player/play_queue.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'music_metadata.dart'; - -class PlayQueue { - final String queueId; - - /// nullable - final String queueTitle; - - /// nullable - final Map? extras; - - final List queue; - - bool get isEmpty => queue.isEmpty; - - const PlayQueue( - {required this.queueId, - required this.queueTitle, - this.extras, - required this.queue}); - - factory PlayQueue.fromMap(Map map) { - return PlayQueue( - queueId: map['queueId'], - queueTitle: map['queueTitle'], - extras: map['extras'], - queue: (map['queue'] as List) - .cast() - .map((e) => MusicMetadata.fromMap(e)) - .toList(), - ); - } - - Map toMap() { - return { - "queueId": queueId, - "queueTitle": queueTitle, - "extras": extras, - "queue": queue.map((e) => e.toMap()).toList(), - }; - } - - const PlayQueue.empty() - : this( - queueId: "empty", - queueTitle: "empty", - queue: const [], - extras: const {}); -} diff --git a/lib/src/player/playback_state.dart b/lib/src/player/playback_state.dart deleted file mode 100644 index e11f36c..0000000 --- a/lib/src/player/playback_state.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:music_player/music_player.dart'; -import 'package:system_clock/system_clock.dart'; - -class PlaybackState { - final PlayerState state; - - final int position; - - final int bufferedPosition; - - final double speed; - - final PlaybackError? error; - - final int updateTime; - - /// - /// Get the real position in current time stamp. - /// - /// discuss: https://github.com/boyan01/flutter-music-player/issues/1 - /// - int get computedPosition { - var append = state == PlayerState.playing - ? (SystemClock.uptime().inMilliseconds - updateTime) - : 0; - append = (append * speed).toInt(); - return position + append; - } - - const PlaybackState({ - required this.state, - required this.position, - required this.bufferedPosition, - required this.speed, - this.error, - required this.updateTime, - }); - - const PlaybackState.none() - : this( - state: PlayerState.none, - position: 0, - bufferedPosition: 0, - speed: 1, - updateTime: 0, - ); - - @override - String toString() { - return 'PlaybackState{state: $state, position: $position, bufferedPosition: $bufferedPosition, speed: $speed, error: $error, updateTime: $updateTime}'; - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is PlaybackState && - runtimeType == other.runtimeType && - state == other.state && - position == other.position && - bufferedPosition == other.bufferedPosition && - speed == other.speed && - error == other.error && - updateTime == other.updateTime; - - @override - int get hashCode => - state.hashCode ^ - position.hashCode ^ - bufferedPosition.hashCode ^ - speed.hashCode ^ - error.hashCode ^ - updateTime.hashCode; -} - -enum PlayerState { - none, - paused, - playing, - buffering, - error, -} diff --git a/lib/src/player/queue.dart b/lib/src/player/queue.dart deleted file mode 100644 index ce6877c..0000000 --- a/lib/src/player/queue.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:music_player/src/player/play_queue.dart'; - -import 'music_metadata.dart'; - -abstract class PlaySequenceDelegate { - /// [anchor] could be null - Future? getNext( - PlayQueue queue, int playMode, MusicMetadata anchor); - - Future? getPrevious( - PlayQueue queue, int playMode, MusicMetadata anchor); -} - -class DefaultPlaySequenceDelegate extends PlaySequenceDelegate { - @override - Future? getNext( - PlayQueue queue, int playMode, MusicMetadata anchor) { - return null; - } - - @override - Future? getPrevious( - PlayQueue queue, int playMode, MusicMetadata anchor) { - return null; - } -} diff --git a/lib/src/player/transport_controls.dart b/lib/src/player/transport_controls.dart deleted file mode 100644 index ebf0350..0000000 --- a/lib/src/player/transport_controls.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:music_player/music_player.dart'; -import 'play_mode.dart'; - -/// -/// Interface for controlling media playback. -/// -/// [play] -/// [pause] -/// [playFromMediaId] -/// [skipToNext] -/// [skipToPrevious] -/// [seekTo] -/// [setPlayMode] -/// -/// -class TransportControls { - final MethodChannel _channel; - - TransportControls(this._channel); - - Future play() async { - await _channel.invokeMethod("play"); - } - - Future playFromMediaId(String mediaId) async { - await _channel.invokeMethod("playFromMediaId", mediaId); - } - - Future prepareFromMediaId(String mediaId) async { - await _channel.invokeMethod("prepareFromMediaId", mediaId); - } - - Future pause() async { - await _channel.invokeMethod("pause"); - } - - Future seekTo(int pos) async { - await _channel.invokeMethod("seekTo", pos); - } - - Future skipToNext() async { - await _channel.invokeMethod("skipToNext"); - } - - Future skipToPrevious() async { - await _channel.invokeMethod("skipToPrevious"); - } - - Future setPlayMode(PlayMode playMode) async { - await _channel.invokeMethod("setPlayMode", playMode.index); - } - - Future setPlaybackSpeed(double rate) async { - await _channel.invokeMethod("setPlaybackSpeed", rate); - } -} diff --git a/lib/src/queue.dart b/lib/src/queue.dart new file mode 100644 index 0000000..dbfa18c --- /dev/null +++ b/lib/src/queue.dart @@ -0,0 +1,379 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:music_player/src/utils/extension.dart'; +import 'package:system_clock/system_clock.dart'; + +import 'utils/logger.dart'; + +abstract class PlayItem { + Future playerUrl(); + + PlayItemMetadata? metadata() => null; +} + +class PlayItemMetadata { + final String title; + final String artist; + final String album; + final String albumArtUrl; + final Duration duration; + + PlayItemMetadata( + this.title, + this.artist, + this.album, + this.albumArtUrl, + this.duration, + ); +} + +const _methodChannel = MethodChannel('tech.soit.quiet/player'); + +class PlayerChannel { + void initialize() { + _methodChannel.setMethodCallHandler((call) async { + try { + return await _handleMethodCall(call); + } catch (error, stacktrace) { + logger.severe('Error in method call handler', error, stacktrace); + } + }); + } + + Future _handleMethodCall(MethodCall call) async { + switch (call.method) { + case 'onPlayerStateChanged': + final playerId = call.arguments['id'] as int; + final state = call.arguments['state'] as int; + final player = + _audioPlayers.firstWhereOrNull((it) => it._id == playerId); + if (player == null) { + logger.warning('player not found. $playerId'); + return; + } + final stateE = PlayerState.values[state]; + player._state.value = stateE; + break; + case 'onPositionChanged': + final playerId = call.arguments['id'] as int; + final currentPosition = call.arguments['currentPosition'] as int; + final duration = call.arguments['duration'] as int; + final updateTime = call.arguments['updateTime'] as int; + final player = + _audioPlayers.firstWhereOrNull((it) => it._id == playerId); + if (player == null) { + logger.warning('player not found. $playerId'); + return; + } + player._positionUpdateTime = Duration(milliseconds: updateTime); + player._position = Duration(milliseconds: currentPosition); + player._duration = Duration(milliseconds: duration); + break; + case 'onPlayerError': + final playerId = call.arguments['id'] as int; + final errorCode = call.arguments['errorCode'] as int; + final message = call.arguments['message'] as String; + final player = + _audioPlayers.firstWhereOrNull((it) => it._id == playerId); + if (player == null) { + logger.warning('player not found. $playerId'); + return; + } + player._state.value = PlayerState.idle; + player._error.value = PlaybackException(errorCode, message); + break; + + case 'onPlayWhenReadyChanged': + final playerId = call.arguments['id'] as int; + final playWhenReady = call.arguments['playWhenReady'] as bool; + final player = + _audioPlayers.firstWhereOrNull((it) => it._id == playerId); + if (player == null) { + logger.warning('player not found. $playerId'); + return; + } + player._playWhenReady.value = playWhenReady; + break; + default: + throw MissingPluginException(); + } + } +} + +enum PlayerState { + idle, + buffering, + ready, + ended, +} + +class PlaybackException implements Exception { + PlaybackException(this.errorCode, this.message); + + final String message; + + // TODO(bin): define error code + final int errorCode; + + @override + String toString() => 'PlaybackException($errorCode, $message)'; +} + +final _audioPlayers = []; + +class AudioPlayer { + AudioPlayer._private(this._channel, this.url) { + scheduleMicrotask(() async { + try { + final playerId = await _channel.invokeMethod('createPlayer'); + if (playerId == null || playerId < 0) { + logger.warning('create player failed. $playerId'); + _error.value = PlaybackException(0, 'create player failed'); + return; + } + _id = playerId; + } catch (error, stacktrace) { + logger.severe('Error in create player', error, stacktrace); + _state.value = PlayerState.idle; + _error.value = PlaybackException(0, error.toString()); + } finally { + _initialized.complete(); + } + }); + } + + factory AudioPlayer(String url) { + final player = AudioPlayer._private(_methodChannel, url); + _audioPlayers.add(player); + return player; + } + + final _state = ValueNotifier(PlayerState.idle); + + final _error = ValueNotifier(null); + + var _duration = Duration.zero; + + var _position = Duration.zero; + + var _positionUpdateTime = Duration.zero; + + final String url; + + final MethodChannel _channel; + + final _initialized = Completer(); + + int _id = -1; + + bool get _valid => _id >= 0; + + final _playWhenReady = ValueNotifier(false); + + bool get playWhenReady => _playWhenReady.value; + + ChangeNotifier get playWhenReadyNotifier => _playWhenReady; + + PlayerState get state => _state.value; + + ChangeNotifier get stateNotifier => _state; + + PlaybackException? get playerError => _error.value; + + ChangeNotifier get errorNotifier => _error; + + Duration get currentPosition { + final isPlaying = state == PlayerState.ready && playWhenReady; + if (!isPlaying) { + return _position; + } + if (_positionUpdateTime <= Duration.zero) { + logger.warning('position update time not set'); + return _position; + } + final offset = SystemClock.elapsedRealtime() - _positionUpdateTime; + return _position + offset; + } + + Duration get duration => _duration; + + set playWhenReady(bool playWhenReady) { + _playWhenReady.value = playWhenReady; + if (!_valid) { + return; + } + _channel.invokeMethod('setPlayWhenReady', { + 'id': _id, + 'playWhenReady': playWhenReady, + }); + } + + Future setPlayUrl(String url) async { + if (!_valid) { + logger.warning('player not initialized'); + return; + } + await _channel.invokeMethod('setPlayUrl', { + 'id': _id, + 'url': url, + }); + } + + Future prepare() async { + await _initialized.future; + if (!_valid) { + logger.warning('player not initialized'); + return; + } + await _channel.invokeMethod('prepare', { + 'id': _id, + 'playWhenReady': _playWhenReady, + }); + } + + Future seekTo(Duration position) async { + if (!_valid) { + logger.warning('player not initialized'); + return; + } + await _channel.invokeMethod('seekTo', { + 'id': _id, + 'position': position.inMilliseconds, + }); + } + + Future get bufferedPosition async { + if (!_valid) { + logger.warning('player not initialized'); + return Duration.zero; + } + final position = await _channel.invokeMethod('getBufferedPosition', { + 'id': _id, + }); + return Duration(milliseconds: position ?? 0); + } + + Future dispose() async { + if (!_valid) { + logger.warning('player not initialized'); + return; + } + _state.dispose(); + _error.dispose(); + _playWhenReady.dispose(); + _audioPlayers.remove(this); + final id = _id; + _id = -1; + await _channel.invokeMethod('dispose', {'id': id}); + } +} + +@immutable +abstract class PlayQueue { + Future getNext(PlayItem? current); + + Future getPrevious(PlayItem? current); +} + +class MusicPlayer { + PlayQueue? _queue; + AudioPlayer? _player; + + MusicPlayer(); + + PlayItem? _current; + + final _state = ValueNotifier(PlayerState.idle); + final _error = ValueNotifier(null); + final _playWhenReady = ValueNotifier(false); + + PlayerState get state => _state.value; + + ChangeNotifier get stateNotifier => _state; + + PlaybackException? get playerError => _error.value; + + ChangeNotifier get errorNotifier => _error; + + Duration get currentPosition => _player?.currentPosition ?? Duration.zero; + + Duration get duration => _player?.duration ?? Duration.zero; + + bool get playWhenReady => _playWhenReady.value; + + ChangeNotifier get playWhenReadyNotifier => _playWhenReady; + + set playWhenReady(bool playWhenReady) { + _playWhenReady.value = playWhenReady; + _player?.playWhenReady = playWhenReady; + } + + Future _play(PlayItem item) async { + if (_player != null) { + unawaited(_player!.dispose()); + } + _current = item; + _state.value = PlayerState.idle; + final audioPlayer = AudioPlayer(await item.playerUrl()); + _player = audioPlayer; + audioPlayer.playWhenReady = playWhenReady; + audioPlayer.stateNotifier.addListener(() { + final state = audioPlayer.state; + if (state == PlayerState.ended) { + skipToNext(); + } + _state.value = state; + }); + audioPlayer.playWhenReadyNotifier.addListener(() { + _playWhenReady.value = audioPlayer.playWhenReady; + }); + audioPlayer.errorNotifier.addListener(() { + _error.value = audioPlayer.playerError; + _state.value = PlayerState.idle; + _player?.dispose(); + _player = null; + }); + await audioPlayer.prepare(); + audioPlayer.playWhenReady = true; + } + + Future skipToNext() async { + if (_queue == null) { + logger.warning('queue not set'); + return; + } + final item = await _queue!.getNext(_current); + if (item == null) { + logger.info('no next item'); + return; + } + await _play(item); + } + + Future skipToPrevious() async { + if (_queue == null) { + logger.warning('queue not set'); + return; + } + final item = await _queue!.getPrevious(_current); + if (item == null) { + logger.info('no previous item'); + return; + } + await _play(item); + } + + Future setQueue(PlayQueue queue) async { + _queue = queue; + await skipToNext(); + } + + Future dispose() async { + if (_player != null) { + await _player!.dispose(); + } + } +} diff --git a/lib/src/utils/extension.dart b/lib/src/utils/extension.dart new file mode 100644 index 0000000..6b85403 --- /dev/null +++ b/lib/src/utils/extension.dart @@ -0,0 +1,10 @@ +extension ListExt on List { + T? firstWhereOrNull(bool Function(T element) test) { + for (final element in this) { + if (test(element)) { + return element; + } + } + return null; + } +} diff --git a/lib/src/utils/logger.dart b/lib/src/utils/logger.dart new file mode 100644 index 0000000..f1e303e --- /dev/null +++ b/lib/src/utils/logger.dart @@ -0,0 +1,3 @@ +import 'package:logging/logging.dart'; + +final logger = Logger('music_player'); diff --git a/lib/src/utils/progress_track_container.dart b/lib/src/utils/progress_track_container.dart index 6396a7a..96409ae 100644 --- a/lib/src/utils/progress_track_container.dart +++ b/lib/src/utils/progress_track_container.dart @@ -26,7 +26,9 @@ class _ProgressTrackingContainerState extends State @override void initState() { super.initState(); - _player = widget.player..addListener(_onStateChanged); + _player = widget.player; + _player.stateNotifier.addListener(_onStateChanged); + _player.playWhenReadyNotifier.addListener(_onStateChanged); _ticker = createTicker((elapsed) { setState(() {}); }); @@ -34,8 +36,8 @@ class _ProgressTrackingContainerState extends State } void _onStateChanged() { - final needTrack = widget.player.playbackStateListenable.value.state == - PlayerState.playing; + final needTrack = + _player.state == PlayerState.ready && _player.playWhenReady; if (_ticker.isActive == needTrack) return; if (_ticker.isActive) { _ticker.stop(); @@ -46,7 +48,8 @@ class _ProgressTrackingContainerState extends State @override void dispose() { - _player.removeListener(_onStateChanged); + _player.stateNotifier.removeListener(_onStateChanged); + _player.playWhenReadyNotifier.removeListener(_onStateChanged); _ticker.dispose(); super.dispose(); } diff --git a/pubspec.yaml b/pubspec.yaml index 2f3c58c..9354930 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.0.1 environment: sdk: '>=2.17.0 <3.0.0' - flutter: ">=3.0.0" + flutter: ">=3.0.0" dependencies: flutter: