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" />
-
-
-
-
-
-
-
-
-