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

Improving Chromecast Integration #1298

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
4 changes: 3 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@
</intent-filter>
</activity>

<service android:name=".webapp.RemotePlayerService" />
<service android:name=".webapp.RemotePlayerService"
android:enabled="true"
android:foregroundServiceType="mediaPlayback"/>

<service
android:name="org.jellyfin.mobile.player.audio.MediaService"
Expand Down
8 changes: 4 additions & 4 deletions app/src/main/java/org/jellyfin/mobile/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,6 @@ class MainActivity : AppCompatActivity() {
return
}

// Bind player service
bindService(Intent(this, RemotePlayerService::class.java), serviceConnection, Service.BIND_AUTO_CREATE)

// Subscribe to activity events
with(activityEventHandler) { subscribe() }

Expand All @@ -141,6 +138,9 @@ class MainActivity : AppCompatActivity() {

override fun onStart() {
super.onStart()
// Bind player service
bindService(Intent(this, RemotePlayerService::class.java), serviceConnection, Service.BIND_AUTO_CREATE)

orientationListener.enable()
}

Expand Down Expand Up @@ -194,11 +194,11 @@ class MainActivity : AppCompatActivity() {

override fun onStop() {
super.onStop()
if (serviceBinder != null) unbindService(serviceConnection)
orientationListener.disable()
}

override fun onDestroy() {
unbindService(serviceConnection)
chromecast.destroy()
super.onDestroy()
}
Expand Down
13 changes: 11 additions & 2 deletions app/src/main/java/org/jellyfin/mobile/bridge/NativeInterface.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.content.Intent
import android.media.session.PlaybackState
import android.net.Uri
import android.webkit.JavascriptInterface
import androidx.core.content.ContextCompat
import org.jellyfin.mobile.events.ActivityEvent
import org.jellyfin.mobile.events.ActivityEventHandler
import org.jellyfin.mobile.utils.Constants
Expand Down Expand Up @@ -98,7 +99,11 @@ class NativeInterface(private val context: Context) : KoinComponent {
putExtra(EXTRA_IS_LOCAL_PLAYER, options.optBoolean(EXTRA_IS_LOCAL_PLAYER, true))
putExtra(EXTRA_IS_PAUSED, options.optBoolean(EXTRA_IS_PAUSED, true))
}
context.startService(intent)

ContextCompat.startForegroundService(
context,
intent,
)

// We may need to request bluetooth permission to react to bluetooth disconnect events
activityEventHandler.emit(ActivityEvent.RequestBluetoothPermission)
Expand All @@ -111,7 +116,11 @@ class NativeInterface(private val context: Context) : KoinComponent {
action = Constants.ACTION_REPORT
putExtra(EXTRA_PLAYER_ACTION, "playbackstop")
}
context.startService(intent)

ContextCompat.startForegroundService(
context,
intent,
)
return true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,10 @@ class MediaService : MediaBrowserServiceCompat() {
private inner class PlayerNotificationListener : PlayerNotificationManager.NotificationListener {
override fun onNotificationPosted(notificationId: Int, notification: Notification, ongoing: Boolean) {
if (ongoing && !isForegroundService) {
val serviceIntent = Intent(applicationContext, [email protected])
ContextCompat.startForegroundService(applicationContext, serviceIntent)
ContextCompat.startForegroundService(
applicationContext,
Intent(applicationContext, [email protected]),
)

startForeground(notificationId, notification)
isForegroundService = true
Expand Down
72 changes: 59 additions & 13 deletions app/src/main/java/org/jellyfin/mobile/webapp/RemotePlayerService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ import org.jellyfin.mobile.utils.applyDefaultLocalAudioAttributes
import org.jellyfin.mobile.utils.createMediaNotificationChannel
import org.jellyfin.mobile.utils.setPlaybackState
import org.koin.android.ext.android.inject
import timber.log.Timber
import java.util.Timer
import java.util.TimerTask
import kotlin.coroutines.CoroutineContext

class RemotePlayerService : Service(), CoroutineScope {
Expand All @@ -85,6 +88,7 @@ class RemotePlayerService : Service(), CoroutineScope {
private var currentItemId: String? = null

val playbackState: PlaybackState? get() = mediaSession?.controller?.playbackState
var shutdownTimer: Timer? = null

private val receiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Expand Down Expand Up @@ -116,6 +120,7 @@ class RemotePlayerService : Service(), CoroutineScope {

override fun onCreate() {
super.onCreate()
Timber.d("RemotePlayerService onCreate")
job = Job()

// Create wakelock for the service
Expand All @@ -142,7 +147,7 @@ class RemotePlayerService : Service(), CoroutineScope {
}

override fun onUnbind(intent: Intent): Boolean {
onStopped()
onStopped(true)
return super.onUnbind(intent)
}

Expand All @@ -155,22 +160,24 @@ class RemotePlayerService : Service(), CoroutineScope {
}

private fun startWakelock() {
if (!wakeLock.isHeld) {
@Suppress("MagicNumber")
wakeLock.acquire(4 * 60 * 60 * 1000L /* 4 hours */)
}
Timber.d("RemotePlayerService requested wake lock")
@Suppress("MagicNumber")
wakeLock.acquire(15 * 60 * 1000L /* 5 minutes */)
}

private fun stopWakelock() {
Timber.d("RemotePlayerService wake lock released")
if (wakeLock.isHeld) wakeLock.release()
}

private fun handleIntent(intent: Intent?) {
if (intent?.action == null) {
return
}
Timber.d("RemotePlayerService handle intent " + intent.action)
val action = intent.action
if (action == Constants.ACTION_REPORT) {
startWakelock()
notify(intent)
return
}
Expand All @@ -188,17 +195,23 @@ class RemotePlayerService : Service(), CoroutineScope {
Constants.ACTION_REWIND -> transportControls.rewind()
Constants.ACTION_PREVIOUS -> transportControls.skipToPrevious()
Constants.ACTION_NEXT -> transportControls.skipToNext()
Constants.ACTION_STOP -> transportControls.stop()
Constants.ACTION_STOP -> {
transportControls.stop()
stopWakelock()
}
}
}

@Suppress("ComplexMethod", "LongMethod")
private fun notify(handledIntent: Intent) {
if (handledIntent.getStringExtra(EXTRA_PLAYER_ACTION) == "playbackstop") {
onStopped()
Timber.d("RemotePlayerService notify playbackstop")
onStopped(false)
return
}

Timber.d("RemotePlayerService notify " + handledIntent.getStringExtra(EXTRA_PLAYER_ACTION))

launch {
val mediaSession = mediaSession!!

Expand Down Expand Up @@ -342,8 +355,20 @@ class RemotePlayerService : Service(), CoroutineScope {
// Post notification
notificationManager.notify(MEDIA_PLAYER_NOTIFICATION_ID, notification)

if (!isPaused) {
Timber.d("RemotePlayerService starting foreground")
startForeground(MEDIA_PLAYER_NOTIFICATION_ID, notification)
} else {
Timber.d("RemotePlayerService stopping foreground")
if (AndroidVersion.isAtLeastN) {
stopForeground(STOP_FOREGROUND_DETACH)
} else {
stopForeground(false)
}
}
// Activate MediaSession
mediaSession.isActive = true
shutdownTimer?.cancel()
}
}

Expand Down Expand Up @@ -423,7 +448,7 @@ class RemotePlayerService : Service(), CoroutineScope {

override fun onStop() {
webappFunctionChannel.callPlaybackManagerAction(PLAYBACK_MANAGER_COMMAND_STOP)
onStopped()
onStopped(false)
}

override fun onSeekTo(pos: Long) {
Expand All @@ -438,14 +463,35 @@ class RemotePlayerService : Service(), CoroutineScope {
}
}

private fun onStopped() {
notificationManager.cancel(MEDIA_PLAYER_NOTIFICATION_ID)
mediaSession?.isActive = false
stopWakelock()
stopSelf()
private fun onStopped(instant: Boolean) {
Timber.d("RemotePlayerService onStopped")
// wait for 15 seconds
// if after 15 seconds there has been no new notify, shut down
shutdownTimer?.cancel()
shutdownTimer = Timer()
if (instant) {
notificationManager.cancel(MEDIA_PLAYER_NOTIFICATION_ID)
mediaSession?.isActive = false
stopWakelock()
stopSelf()
} else {
shutdownTimer?.schedule(
object : TimerTask() {
override fun run() {
notificationManager.cancel(MEDIA_PLAYER_NOTIFICATION_ID)
mediaSession?.isActive = false
stopWakelock()
stopSelf()
}
},
@Suppress("MagicNumber")
15000,
)
}
}

override fun onDestroy() {
Timber.d("RemotePlayerService onDestroy")
unregisterReceiver(receiver)
job.cancel()
mediaSession?.release()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ public void sendMessage(String namespace, String message, JavascriptCallback cal
return;
}
activity.runOnUiThread(() -> session.sendMessage(namespace, message).setResultCallback(result -> {
if (!result.isSuccess()) {
if (result.isSuccess()) {
callback.success();
} else {
callback.error(result.toString());
Expand Down
Loading