Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Development/media3 #98

Merged
merged 2 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package me.sithiramunasinghe.flutter.flutter_radio_player

import android.app.Activity
import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.AssetManager
import android.net.Uri
Expand All @@ -15,6 +18,8 @@ import androidx.media3.session.SessionToken
import com.google.common.util.concurrent.MoreExecutors
import io.flutter.embedding.engine.loader.FlutterLoader
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
Expand All @@ -27,20 +32,38 @@ import me.sithiramunasinghe.flutter.flutter_radio_player.core.PlaybackService
import me.sithiramunasinghe.flutter.flutter_radio_player.data.FlutterRadioPlayerSource
import java.io.InputStream

class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {

class FlutterRadioPlayerPlugin : FlutterPlugin, ActivityAware, MethodCallHandler {

private lateinit var channel: MethodChannel
private var applicationContext: Context? = null
private var mediaController: MediaController? = null
private var isMediaControllerAvailable = false
private val pendingCalls = mutableListOf<Pair<MethodCall, Result>>()

companion object {
private var isMediaControllerAvailable = false
lateinit var sessionActivity: PendingIntent

var playBackEventSink: EventChannel.EventSink? = null
var nowPlayingEventSink: EventChannel.EventSink? = null
var playbackVolumeControl: EventChannel.EventSink? = null
private fun getSessionActivity(context: Context, activity: Activity) {
sessionActivity = PendingIntent.getActivity(
context, 0, Intent(context, activity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
}


override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_radio_player")
channel.setMethodCallHandler(this)
applicationContext = flutterPluginBinding.applicationContext

println("aaaaa")
initEventChannels(flutterPluginBinding.binaryMessenger, EventChannelSink.getInstance())
initializeEventSink()

val token = SessionToken(
flutterPluginBinding.applicationContext, ComponentName(
Expand All @@ -63,72 +86,165 @@ class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {

@OptIn(UnstableApi::class)
override fun onMethodCall(call: MethodCall, result: Result) {
println("Media controller status = $isMediaControllerAvailable")
if (!isMediaControllerAvailable) {
if (!isMediaControllerAvailable || playBackEventSink == null) {
pendingCalls.add(Pair(call, result))
return
}
if (call.method == "initialize") {
val sources = call.argument<String>("sources")
val playWhenReady = call.argument<Boolean>("playWhenReady")
val decodedSources: List<FlutterRadioPlayerSource> =
Json.decodeFromString(sources!!)
mediaController!!.volume = 0.5F
mediaController!!.playWhenReady = playWhenReady!!
if (decodedSources.isNotEmpty()) {
mediaController!!.setMediaItems(decodedSources.map {
val mediaItemBuilder = MediaItem.Builder().setUri(it.url)
val mediaMeta = MediaMetadata.Builder()
if (it.title.isNullOrEmpty()) {
mediaMeta.setArtist(getAppName())
} else {
mediaMeta.setArtist(it.title)
}
if (!it.artwork.isNullOrEmpty()) {
if ((it.artwork!!.contains("http") || it.artwork!!.contains("https"))) {
mediaMeta.setArtworkUri(Uri.parse(it.artwork))
when (call.method) {
"initialize" -> {
if (mediaController!!.isPlaying) {
playBackEventSink!!.success(true)
return
}
val sources = call.argument<String>("sources")
val playWhenReady = call.argument<Boolean>("playWhenReady")
val decodedSources: List<FlutterRadioPlayerSource> =
Json.decodeFromString(sources!!)
mediaController!!.volume = 0.5F
mediaController!!.playWhenReady = playWhenReady!!
if (decodedSources.isNotEmpty()) {
mediaController!!.setMediaItems(decodedSources.map {
val mediaItemBuilder = MediaItem.Builder().setUri(it.url)
val mediaMeta = MediaMetadata.Builder()
if (it.title.isNullOrEmpty()) {
mediaMeta.setArtist(getAppName())
} else {
mediaMeta.setArtworkData(
Util.toByteArray(getBitmapFromAssets(it.artwork)!!),
MediaMetadata.PICTURE_TYPE_FRONT_COVER
)
mediaMeta.setArtist(it.title)
}
}
mediaItemBuilder.setMediaMetadata(mediaMeta.build())
mediaItemBuilder.build()
})
mediaController!!.prepare()
if (!it.artwork.isNullOrEmpty()) {
if ((it.artwork!!.contains("http") || it.artwork!!.contains("https"))) {
mediaMeta.setArtworkUri(Uri.parse(it.artwork))
} else {
mediaMeta.setArtworkData(
Util.toByteArray(getBitmapFromAssets(it.artwork)!!),
MediaMetadata.PICTURE_TYPE_FRONT_COVER
)
}
}
mediaItemBuilder.setMediaMetadata(mediaMeta.build())
mediaItemBuilder.build()
})
mediaController!!.prepare()
}
}
} else if (call.method == "playOrPause") {
if (mediaController!!.mediaItemCount != 0) {
if (mediaController!!.isPlaying) {
mediaController!!.pause()
return

"playOrPause" -> {
if (mediaController!!.mediaItemCount != 0) {
if (mediaController!!.isPlaying) {
mediaController!!.pause()
return
}
mediaController!!.play()
}
}

"play" -> {
mediaController!!.play()
}
} else if (call.method == "play") {
mediaController!!.play()
} else if (call.method == "pause") {
mediaController!!.pause()
} else if (call.method == "changeVolume") {
val volume = call.argument<Double>("volume")
mediaController!!.volume = volume!!.toFloat()
} else if (call.method == "getVolume") {
result.success(mediaController!!.volume)
} else if (call.method == "nextSource") {
mediaController!!.seekToNextMediaItem()
} else if (call.method == "prevSource") {
mediaController!!.seekToPreviousMediaItem()
} else {
result.notImplemented()
}

"pause" -> {
mediaController!!.pause()
}

"changeVolume" -> {
val volume = call.argument<Double>("volume")
mediaController!!.volume = volume!!.toFloat()
}

"getVolume" -> {
result.success(mediaController!!.volume)
}

"nextSource" -> {
mediaController!!.seekToNextMediaItem()
}

"prevSource" -> {
mediaController!!.seekToPreviousMediaItem()
}

"sourceAtIndex" -> {
val index = call.argument<Int>("index")
mediaController!!.seekToDefaultPosition(index!!)
}

else -> result.notImplemented()
}
}

override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
println("onDetachedFromEngine")
channel.setMethodCallHandler(null)

EventChannelSink.getInstance().playbackEventChannel = null
EventChannelSink.getInstance().nowPlayingEventChannel = null
EventChannelSink.getInstance().playbackVolumeChannel = null

playbackVolumeControl = null
nowPlayingEventSink = null
playBackEventSink = null
mediaController!!.release()
isMediaControllerAvailable = false
}

override fun onAttachedToActivity(binding: ActivityPluginBinding) {
println("onAttachedToActivity")
getSessionActivity(binding.activity.applicationContext, binding.activity)
}

override fun onDetachedFromActivityForConfigChanges() {
println("onDetachedFromActivityForConfigChanges")
}

override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
println("onReattachedToActivityForConfigChanges")
getSessionActivity(binding.activity.applicationContext, binding.activity)
}

override fun onDetachedFromActivity() {
println("onDetachedFromActivity")
}

/**
* Initialize events sink and event channels
*/
private fun initializeEventSink() {
EventChannelSink.getInstance().playbackEventChannel?.setStreamHandler(object :
EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
playBackEventSink = events
pendingCalls.forEach { (call, result) ->
onMethodCall(call, result)
}
pendingCalls.clear()
}

override fun onCancel(arguments: Any?) {
playBackEventSink = null
}
})

EventChannelSink.getInstance().nowPlayingEventChannel?.setStreamHandler(object :
EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
nowPlayingEventSink = events
}

override fun onCancel(arguments: Any?) {
nowPlayingEventSink = null
}
})

EventChannelSink.getInstance().playbackVolumeChannel?.setStreamHandler(object :
EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
playbackVolumeControl = events
}

override fun onCancel(arguments: Any?) {
playbackVolumeControl = null
}
})
}

private fun initEventChannels(
Expand Down Expand Up @@ -164,15 +280,10 @@ class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {
*/
private fun getBitmapFromAssets(assetPath: String?): InputStream? {
try {
// Initialize the FlutterLoader
val flutterLoader = FlutterLoader()
flutterLoader.startInitialization(applicationContext!!)
flutterLoader.ensureInitializationComplete(applicationContext!!, null)

// Get the asset path
val assetLookupKey = flutterLoader.getLookupKeyForAsset(assetPath!!)

// Access the asset manager and load the bitmap
val assetManager: AssetManager = applicationContext!!.assets
return assetManager.open(assetLookupKey)
// val inputStream = assetManager.open(assetLookupKey)
Expand All @@ -183,6 +294,11 @@ class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {
}
}

/**
* Get the application name
*
* @return App name
*/
private fun getAppName(): String? {
try {
val packageManager: PackageManager = applicationContext!!.packageManager
Expand All @@ -195,8 +311,3 @@ class FlutterRadioPlayerPlugin : FlutterPlugin, MethodCallHandler {
}
}
}

// https://github.com/oguzhaneksi/RadioRoam/tree/master
// https://medium.com/@ouzhaneki/basic-background-playback-implementation-with-media3-mediasessionservice-4d571f15bdc2
// https://developer.android.com/media/media3/session/background-playback
// https://medium.com/@debz_exe/implementation-of-media-3-mastering-background-playback-with-mediasessionservice-and-5e130272c39e
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import io.flutter.plugin.common.EventChannel


class EventChannelSink private constructor() {
lateinit var playbackEventChannel: EventChannel
lateinit var nowPlayingEventChannel: EventChannel
lateinit var playbackVolumeChannel: EventChannel
var playbackEventChannel: EventChannel? = null
var nowPlayingEventChannel: EventChannel? = null
var playbackVolumeChannel: EventChannel? = null
companion object {

@Volatile
Expand Down
Loading