diff --git a/app/build.gradle b/app/build.gradle index 8214f0b6..68c7e905 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -175,13 +175,13 @@ dependencies { implementation 'androidx.media3:media3-exoplayer:1.2.0' implementation 'androidx.media3:media3-exoplayer-dash:1.2.0' implementation 'androidx.media3:media3-ui:1.2.0' - implementation 'androidx.media3:media3-session:1.2.0' implementation 'androidx.media3:media3-exoplayer-hls:1.2.0' implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.0' implementation 'androidx.media3:media3-exoplayer-smoothstreaming:1.2.0' implementation 'androidx.media3:media3-transformer:1.2.0' implementation 'androidx.navigation:navigation-fragment-ktx:2.7.5' implementation 'androidx.navigation:navigation-ui-ktx:2.7.5' + implementation 'androidx.media:media:1.7.0' //Other implementation 'org.jmdns:jmdns:3.5.1' diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index 7f2b20ee..fe1dad58 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -67,7 +67,7 @@ class VideoDownload { val videoEither: IPlatformVideo get() = videoDetails ?: video ?: throw IllegalStateException("Missing video?"); val id: PlatformID get() = videoEither.id val name: String get() = videoEither.name; - val thumbnail: String? get() = videoDetails?.thumbnails?.getHQThumbnail() ?: video?.thumbnails?.getHQThumbnail(); + val thumbnail: String? get() = videoDetails?.thumbnails?.getHQThumbnail(); var targetPixelCount: Long? = null; var targetBitrate: Long? = null; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 4be113ed..66f01215 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -12,6 +12,7 @@ import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.net.Uri +import android.support.v4.media.session.PlaybackStateCompat import android.text.Spanned import android.util.AttributeSet import android.util.Log @@ -571,9 +572,9 @@ class VideoDetailView : ConstraintLayout { } _playerProgress.player = _player.exoPlayer?.player; - _playerProgress.setProgressUpdateListener { _, _ -> - StatePlayer.instance.updateMediaSessionPlaybackState(); - } + _playerProgress.setProgressUpdateListener { position, _ -> + StatePlayer.instance.updateMediaSessionPlaybackState(_player.exoPlayer?.getPlaybackStateCompat() ?: PlaybackStateCompat.STATE_NONE, position); + }; StatePlayer.instance.onQueueChanged.subscribe(this) { if(!_destroyed) { @@ -1358,11 +1359,9 @@ class VideoDetailView : ConstraintLayout { } } - StatePlayer.instance.startOrUpdateMediaSession(context, video); StatePlayer.instance.setCurrentlyPlaying(video); - if(video.isLive && video.live != null) { loadLiveChat(video); } @@ -1791,7 +1790,7 @@ class VideoDetailView : ConstraintLayout { _cast.setIsPlaying(playing); } else { StatePlayer.instance.updateMediaSession( null); - StatePlayer.instance.updateMediaSessionPlaybackState(); + StatePlayer.instance.updateMediaSessionPlaybackState(_player.exoPlayer?.getPlaybackStateCompat() ?: PlaybackStateCompat.STATE_NONE, _player.exoPlayer?.player?.currentPosition ?: 0); } if(playing) { diff --git a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt index 7cf603ec..44045431 100644 --- a/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt +++ b/app/src/main/java/com/futo/platformplayer/helpers/VideoHelper.kt @@ -2,6 +2,13 @@ package com.futo.platformplayer.helpers import android.net.Uri import androidx.annotation.OptIn +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.ResolvingDataSource +import androidx.media3.exoplayer.dash.DashMediaSource +import androidx.media3.exoplayer.dash.manifest.DashManifestParser +import androidx.media3.exoplayer.source.MediaSource import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource @@ -14,12 +21,6 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource import com.futo.platformplayer.logging.Logger -import androidx.media3.common.MediaItem -import androidx.media3.common.util.UnstableApi -import androidx.media3.datasource.ResolvingDataSource -import androidx.media3.exoplayer.dash.DashMediaSource -import androidx.media3.exoplayer.dash.manifest.DashManifestParser -import androidx.media3.exoplayer.source.MediaSource import kotlin.math.abs class VideoHelper { @@ -146,6 +147,18 @@ class VideoHelper { })).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(videoSource.getVideoUrl())).build()) } + fun getMediaMetadata(media: IPlatformVideoDetails): MediaMetadata { + val builder = MediaMetadata.Builder() + .setArtist(media.author.name) + .setTitle(media.name) + + media.thumbnails.getHQThumbnail()?.let { + builder.setArtworkUri(Uri.parse(it)) + } + + return builder.build() + } + @OptIn(UnstableApi::class) fun convertItagSourceToChunkedDashSource(audioSource: JSAudioUrlRangeSource) : MediaSource { val manifestConfig = ProgressiveDashManifestCreator.fromAudioProgressiveStreamingUrl(audioSource.getAudioUrl(), diff --git a/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt b/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt index 437fb960..4a205283 100644 --- a/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt +++ b/app/src/main/java/com/futo/platformplayer/services/MediaPlaybackService.kt @@ -13,14 +13,15 @@ import android.graphics.drawable.Drawable import android.media.AudioFocusRequest import android.media.AudioManager import android.media.AudioManager.OnAudioFocusChangeListener +import android.media.MediaMetadata import android.os.Build import android.os.IBinder +import android.os.SystemClock +import android.support.v4.media.MediaMetadataCompat +import android.support.v4.media.session.MediaSessionCompat +import android.support.v4.media.session.PlaybackStateCompat import android.util.Log -import androidx.annotation.OptIn import androidx.core.app.NotificationCompat -import androidx.media3.common.util.UnstableApi -import androidx.media3.session.MediaSession -import androidx.media3.session.MediaStyleNotificationHelper import com.bumptech.glide.Glide import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition @@ -50,7 +51,7 @@ class MediaPlaybackService : Service() { private var _audioManager: AudioManager? = null; private var _notificationManager: NotificationManager? = null; private var _notificationChannel: NotificationChannel? = null; - private var _mediaSession: MediaSession? = null; + private var _mediaSession: MediaSessionCompat? = null; private var _hasFocus: Boolean = false; private var _focusRequest: AudioFocusRequest? = null; private var _audioFocusLossTime_ms: Long? = null @@ -81,7 +82,6 @@ class MediaPlaybackService : Service() { return START_STICKY; } - @OptIn(UnstableApi::class) fun setupNotificationRequirements() { _audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager; _notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager; @@ -91,54 +91,47 @@ class MediaPlaybackService : Service() { }; _notificationManager!!.createNotificationChannel(_notificationChannel!!); - _mediaSession = MediaSession.Builder(this, StatePlayer.instance.getPlayerOrCreate(this).player) - - .setCallback(object: MediaSession.Callback { - override fun onMediaButtonEvent(session: MediaSession, controllerInfo: MediaSession.ControllerInfo, intent: Intent): Boolean { - //TODO: Reimplement - return super.onMediaButtonEvent(session, controllerInfo, intent) - } - - /*override fun onSeekTo(pos: Long) { - super.onSeekTo(pos) - Logger.i(TAG, "Media session callback onSeekTo(pos = $pos)"); - MediaControlReceiver.onSeekToReceived.emit(pos); - } - - override fun onPlay() { - super.onPlay(); - Logger.i(TAG, "Media session callback onPlay()"); - MediaControlReceiver.onPlayReceived.emit(); - } - - override fun onPause() { - super.onPause(); - Logger.i(TAG, "Media session callback onPause()"); - MediaControlReceiver.onPauseReceived.emit(); - } - - override fun onStop() { - super.onStop(); - Logger.i(TAG, "Media session callback onStop()"); - MediaControlReceiver.onCloseReceived.emit(); - } - - override fun onSkipToPrevious() { - super.onSkipToPrevious(); - Logger.i(TAG, "Media session callback onSkipToPrevious()"); - MediaControlReceiver.onPreviousReceived.emit(); - } - - override fun onSkipToNext() { - super.onSkipToNext() - Logger.i(TAG, "Media session callback onSkipToNext()"); - MediaControlReceiver.onNextReceived.emit(); - }*/ - }) - .build(); - /*_mediaSession?.setPlaybackState(PlaybackStateCompat.Builder() + _mediaSession = MediaSessionCompat(this, "PlayerState"); + _mediaSession?.setPlaybackState(PlaybackStateCompat.Builder() .setState(PlaybackStateCompat.STATE_PLAYING, 0, 1f) - .build());*/ + .build()); + _mediaSession?.setCallback(object: MediaSessionCompat.Callback() { + override fun onSeekTo(pos: Long) { + super.onSeekTo(pos) + Logger.i(TAG, "Media session callback onSeekTo(pos = $pos)"); + MediaControlReceiver.onSeekToReceived.emit(pos); + } + + override fun onPlay() { + super.onPlay(); + Logger.i(TAG, "Media session callback onPlay()"); + MediaControlReceiver.onPlayReceived.emit(); + } + + override fun onPause() { + super.onPause(); + Logger.i(TAG, "Media session callback onPause()"); + MediaControlReceiver.onPauseReceived.emit(); + } + + override fun onStop() { + super.onStop(); + Logger.i(TAG, "Media session callback onStop()"); + MediaControlReceiver.onCloseReceived.emit(); + } + + override fun onSkipToPrevious() { + super.onSkipToPrevious(); + Logger.i(TAG, "Media session callback onSkipToPrevious()"); + MediaControlReceiver.onPreviousReceived.emit(); + } + + override fun onSkipToNext() { + super.onSkipToNext() + Logger.i(TAG, "Media session callback onSkipToNext()"); + MediaControlReceiver.onNextReceived.emit(); + } + }); } override fun onCreate() { @@ -193,7 +186,15 @@ class MediaPlaybackService : Service() { if(_notificationChannel == null || _mediaSession == null) setupNotificationRequirements(); + _mediaSession?.setMetadata( + MediaMetadataCompat.Builder() + .putString(MediaMetadata.METADATA_KEY_ARTIST, video.author.name) + .putString(MediaMetadata.METADATA_KEY_TITLE, video.name) + .putLong(MediaMetadata.METADATA_KEY_DURATION, video.duration * 1000) + .build()); + val thumbnail = video.thumbnails.getHQThumbnail(); + _notif_last_video = video; if(isUpdating) @@ -220,7 +221,6 @@ class MediaPlaybackService : Service() { private fun generateMediaAction(icon: Int, title: String, intent: PendingIntent) : NotificationCompat.Action { return NotificationCompat.Action.Builder(icon, title, intent).build(); } - @OptIn(UnstableApi::class) private fun notifyMediaSession(video: IPlatformVideo?, desiredBitmap: Bitmap?) { val channel = _notificationChannel ?: return; val session = _mediaSession ?: return; @@ -249,9 +249,14 @@ class MediaPlaybackService : Service() { .setOngoing(true) .setSilent(true) .setContentIntent(PendingIntent.getActivity(this, 5, bringUpIntent, PendingIntent.FLAG_IMMUTABLE)) - .setStyle( - if(hasQueue) MediaStyleNotificationHelper.MediaStyle(session)//.setShowActionsInCompactView(0, 1, 2) - else MediaStyleNotificationHelper.MediaStyle(session))//.setShowActionsInCompactView(0)) + .setStyle(if(hasQueue) + androidx.media.app.NotificationCompat.MediaStyle() + .setMediaSession(session.sessionToken) + .setShowActionsInCompactView(0, 1, 2) + else + androidx.media.app.NotificationCompat.MediaStyle() + .setMediaSession(session.sessionToken) + .setShowActionsInCompactView(0)) .setDeleteIntent(deleteIntent) .setChannelId(channel.id) @@ -298,7 +303,7 @@ class MediaPlaybackService : Service() { val notif = builder.build(); notif.flags = notif.flags or NotificationCompat.FLAG_ONGOING_EVENT or NotificationCompat.FLAG_NO_CLEAR; - Logger.i(TAG, "Updating notification bitmap=${if (bitmap != null) "yes" else "no."} channelId=${channel.id} icon=${icon} video=${video?.name ?: ""} playWhenReady=${playWhenReady} session.sessionToken=${session.token}"); + Logger.i(TAG, "Updating notification bitmap=${if (bitmap != null) "yes" else "no."} channelId=${channel.id} icon=${icon} video=${video?.name ?: ""} playWhenReady=${playWhenReady} session.sessionToken=${session.sessionToken}"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // For API 29 and above @@ -311,7 +316,20 @@ class MediaPlaybackService : Service() { _notif_last_bitmap = bitmap; } - fun updateMediaSessionPlaybackState() { + fun updateMediaSessionPlaybackState(state: Int, pos: Long) { + _mediaSession?.setPlaybackState( + PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_SEEK_TO or + PlaybackStateCompat.ACTION_PLAY or + PlaybackStateCompat.ACTION_PAUSE or + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or + PlaybackStateCompat.ACTION_SKIP_TO_NEXT or + PlaybackStateCompat.ACTION_PLAY_PAUSE + ) + .setState(state, pos, 1f, SystemClock.elapsedRealtime()) + .build()); + if(_focusRequest == null) setAudioFocus(); } diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt index 6f2ed300..7d642ac4 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlayer.kt @@ -107,8 +107,8 @@ class StatePlayer { fun updateMediaSession(videoUpdated: IPlatformVideoDetails?) { MediaPlaybackService.getService()?.updateMediaSession(videoUpdated); } - fun updateMediaSessionPlaybackState() { - MediaPlaybackService.getService()?.updateMediaSessionPlaybackState(); + fun updateMediaSessionPlaybackState(state: Int, pos: Long) { + MediaPlaybackService.getService()?.updateMediaSessionPlaybackState(state, pos); } fun closeMediaSession() { MediaPlaybackService.getService()?.closeMediaSession(); diff --git a/app/src/main/java/com/futo/platformplayer/video/PlayerManager.kt b/app/src/main/java/com/futo/platformplayer/video/PlayerManager.kt index 05a6eef0..22926841 100644 --- a/app/src/main/java/com/futo/platformplayer/video/PlayerManager.kt +++ b/app/src/main/java/com/futo/platformplayer/video/PlayerManager.kt @@ -1,5 +1,7 @@ package com.futo.platformplayer.video +import android.media.session.PlaybackState +import android.support.v4.media.session.PlaybackStateCompat import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer import androidx.media3.ui.PlayerView @@ -21,6 +23,15 @@ class PlayerManager { this.player = exoPlayer; } + + fun getPlaybackStateCompat() : Int { + return when(player.playbackState) { + ExoPlayer.STATE_READY -> if(player.playWhenReady) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED; + ExoPlayer.STATE_BUFFERING -> PlaybackState.STATE_BUFFERING; + else -> PlaybackState.STATE_NONE + } + } + @Synchronized fun attach(view: PlayerView, stateName: String) { if(view != _currentView) { diff --git a/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt b/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt index 52106dd6..aeca7334 100644 --- a/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/casting/CastView.kt @@ -1,6 +1,8 @@ package com.futo.platformplayer.views.casting import android.content.Context +import android.media.session.PlaybackState +import android.support.v4.media.session.PlaybackStateCompat import android.util.AttributeSet import android.util.TypedValue import android.view.LayoutInflater @@ -147,9 +149,10 @@ class CastView : ConstraintLayout { _buttonPlay.visibility = View.VISIBLE; } + val position = StateCasting.instance.activeDevice?.expectedCurrentTime?.times(1000.0)?.toLong(); if(StatePlayer.instance.hasMediaSession()) { StatePlayer.instance.updateMediaSession(null); - StatePlayer.instance.updateMediaSessionPlaybackState(); + StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), (position ?: 0)); } } @@ -195,7 +198,7 @@ class CastView : ConstraintLayout { fun setTime(ms: Long) { _textPosition.text = ms.toHumanTime(true); _timeBar.setPosition(ms / 1000); - StatePlayer.instance.updateMediaSessionPlaybackState(); + StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), ms); } fun cleanup() { @@ -205,4 +208,13 @@ class CastView : ConstraintLayout { _updateTimeJob = null; _scope.cancel(); } + + private fun getPlaybackStateCompat(): Int { + val d = StateCasting.instance.activeDevice ?: return PlaybackState.STATE_NONE; + + return when(d.isPlaying) { + true -> PlaybackStateCompat.STATE_PLAYING; + else -> PlaybackStateCompat.STATE_PAUSED; + } + } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragview_video_detail.xml b/app/src/main/res/layout/fragview_video_detail.xml index 607de6ed..7652198e 100644 --- a/app/src/main/res/layout/fragview_video_detail.xml +++ b/app/src/main/res/layout/fragview_video_detail.xml @@ -39,7 +39,7 @@ android:elevation="4dp" android:layout_marginBottom="6dp" /> - - - - - - - - -