Media3 migration.

This commit is contained in:
Koen 2023-12-11 17:54:18 +01:00
commit 3b8d256bad
15 changed files with 213 additions and 239 deletions

View file

@ -172,13 +172,14 @@ dependencies {
implementation("com.caoccao.javet:javet-android:2.2.1") implementation("com.caoccao.javet:javet-android:2.2.1")
//Exoplayer //Exoplayer
implementation 'com.google.android.exoplayer:exoplayer-core:2.19.1' implementation 'androidx.media3:media3-exoplayer:1.2.0'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.19.1' implementation 'androidx.media3:media3-exoplayer-dash:1.2.0'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.19.1' implementation 'androidx.media3:media3-ui:1.2.0'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.19.1' implementation 'androidx.media3:media3-session:1.2.0'
implementation 'com.google.android.exoplayer:exoplayer-rtsp:2.19.1' implementation 'androidx.media3:media3-exoplayer-hls:1.2.0'
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.19.1' implementation 'androidx.media3:media3-exoplayer-rtsp:1.2.0'
implementation 'com.google.android.exoplayer:exoplayer-transformer:2.19.1' 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-fragment-ktx:2.7.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.5' implementation 'androidx.navigation:navigation-ui-ktx:2.7.5'

View file

@ -1,7 +1,7 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.api.media.platforms.js.models.sources package com.futo.platformplayer.api.media.platforms.js.models.sources
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.HttpDataSource
import com.caoccao.javet.values.V8Value import com.caoccao.javet.values.V8Value
import com.caoccao.javet.values.reference.V8ValueObject import com.caoccao.javet.values.reference.V8ValueObject
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
@ -11,8 +11,6 @@ import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.engine.V8Plugin import com.futo.platformplayer.engine.V8Plugin
import com.futo.platformplayer.orNull import com.futo.platformplayer.orNull
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.upstream.HttpDataSource
abstract class JSSource { abstract class JSSource {
protected val _config: IV8PluginConfig; protected val _config: IV8PluginConfig;

View file

@ -1,5 +1,3 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.fragment.mainactivity.main package com.futo.platformplayer.fragment.mainactivity.main
import android.app.PictureInPictureParams import android.app.PictureInPictureParams
@ -14,7 +12,6 @@ import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.net.Uri import android.net.Uri
import android.support.v4.media.session.PlaybackStateCompat
import android.text.Spanned import android.text.Spanned
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log import android.util.Log
@ -32,6 +29,12 @@ import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C
import androidx.media3.common.Format
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.HttpDataSource
import androidx.media3.ui.PlayerControlView
import androidx.media3.ui.TimeBar
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
@ -138,11 +141,6 @@ import com.futo.polycentric.core.ContentType
import com.futo.polycentric.core.Models import com.futo.polycentric.core.Models
import com.futo.polycentric.core.Opinion import com.futo.polycentric.core.Opinion
import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.futo.polycentric.core.toURLInfoSystemLinkUrl
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.Format
import com.google.android.exoplayer2.ui.PlayerControlView
import com.google.android.exoplayer2.ui.TimeBar
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -155,7 +153,6 @@ import java.time.OffsetDateTime
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.roundToLong import kotlin.math.roundToLong
class VideoDetailView : ConstraintLayout { class VideoDetailView : ConstraintLayout {
private val TAG = "VideoDetailView" private val TAG = "VideoDetailView"
@ -303,7 +300,7 @@ class VideoDetailView : ConstraintLayout {
Pair(0, 10) //around live, try every 10 seconds Pair(0, 10) //around live, try every 10 seconds
); );
@androidx.annotation.OptIn(UnstableApi::class)
constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) { constructor(context: Context, attrs : AttributeSet? = null) : super(context, attrs) {
inflate(context, R.layout.fragview_video_detail, this); inflate(context, R.layout.fragview_video_detail, this);
@ -312,7 +309,7 @@ class VideoDetailView : ConstraintLayout {
_cast = findViewById(R.id.videodetail_cast); _cast = findViewById(R.id.videodetail_cast);
_player = findViewById(R.id.videodetail_player); _player = findViewById(R.id.videodetail_player);
_playerProgress = findViewById(R.id.videodetail_progress); _playerProgress = findViewById(R.id.videodetail_progress);
_timeBar = _playerProgress.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress); _timeBar = _playerProgress.findViewById(androidx.media3.ui.R.id.exo_progress);
_title = findViewById(R.id.videodetail_title); _title = findViewById(R.id.videodetail_title);
_subTitle = findViewById(R.id.videodetail_meta); _subTitle = findViewById(R.id.videodetail_meta);
_platform = findViewById(R.id.videodetail_platform); _platform = findViewById(R.id.videodetail_platform);
@ -574,8 +571,8 @@ class VideoDetailView : ConstraintLayout {
} }
_playerProgress.player = _player.exoPlayer?.player; _playerProgress.player = _player.exoPlayer?.player;
_playerProgress.setProgressUpdateListener { position, _ -> _playerProgress.setProgressUpdateListener { _, _ ->
StatePlayer.instance.updateMediaSessionPlaybackState(_player.exoPlayer?.getPlaybackStateCompat() ?: PlaybackStateCompat.STATE_NONE, position); StatePlayer.instance.updateMediaSessionPlaybackState();
} }
StatePlayer.instance.onQueueChanged.subscribe(this) { StatePlayer.instance.onQueueChanged.subscribe(this) {
@ -1497,6 +1494,7 @@ class VideoDetailView : ConstraintLayout {
} }
//Events //Events
@androidx.annotation.OptIn(UnstableApi::class)
private fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean){ private fun onSourceChanged(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean){
Logger.i(TAG, "onSourceChanged(videoSource=$videoSource, audioSource=$audioSource, resume=$resume)") Logger.i(TAG, "onSourceChanged(videoSource=$videoSource, audioSource=$audioSource, resume=$resume)")
@ -1532,7 +1530,7 @@ class VideoDetailView : ConstraintLayout {
private var _didTriggerDatasourceError = false; private var _didTriggerDatasourceError = false;
private fun onDataSourceError(exception: Throwable) { private fun onDataSourceError(exception: Throwable) {
Logger.e(TAG, "onDataSourceError", exception); Logger.e(TAG, "onDataSourceError", exception);
if(exception.cause != null && exception.cause is InvalidResponseCodeException && (exception.cause!! as InvalidResponseCodeException).responseCode == 403) { if(exception.cause != null && exception.cause is HttpDataSource.InvalidResponseCodeException && (exception.cause!! as HttpDataSource.InvalidResponseCodeException).responseCode == 403) {
val currentVideo = video val currentVideo = video
if(currentVideo == null || currentVideo !is IPluginSourced) if(currentVideo == null || currentVideo !is IPluginSourced)
return; return;
@ -1611,6 +1609,7 @@ class VideoDetailView : ConstraintLayout {
val v = video ?: return; val v = video ?: return;
updateQualitySourcesOverlay(v, videoLocal, liveStreamVideoFormats, liveStreamAudioFormats); updateQualitySourcesOverlay(v, videoLocal, liveStreamVideoFormats, liveStreamAudioFormats);
} }
@androidx.annotation.OptIn(UnstableApi::class)
private fun updateQualitySourcesOverlay(videoDetails: IPlatformVideoDetails?, videoLocal: VideoLocal? = null, liveStreamVideoFormats: List<Format>? = null, liveStreamAudioFormats: List<Format>? = null) { private fun updateQualitySourcesOverlay(videoDetails: IPlatformVideoDetails?, videoLocal: VideoLocal? = null, liveStreamVideoFormats: List<Format>? = null, liveStreamAudioFormats: List<Format>? = null) {
Logger.i(TAG, "updateQualitySourcesOverlay"); Logger.i(TAG, "updateQualitySourcesOverlay");
@ -1730,7 +1729,7 @@ class VideoDetailView : ConstraintLayout {
{ handleSelectAudioTrack(it) }); { handleSelectAudioTrack(it) });
}.toList().toTypedArray()) }.toList().toTypedArray())
else null, else null,
if(video?.subtitles?.isNotEmpty() ?: false && video != null) if(video?.subtitles?.isNotEmpty() == true)
SlideUpMenuGroup(this.context, context.getString(R.string.subtitles), "subtitles", SlideUpMenuGroup(this.context, context.getString(R.string.subtitles), "subtitles",
*video.subtitles *video.subtitles
.map { .map {
@ -1792,7 +1791,7 @@ class VideoDetailView : ConstraintLayout {
_cast.setIsPlaying(playing); _cast.setIsPlaying(playing);
} else { } else {
StatePlayer.instance.updateMediaSession( null); StatePlayer.instance.updateMediaSession( null);
StatePlayer.instance.updateMediaSessionPlaybackState(_player.exoPlayer?.getPlaybackStateCompat() ?: PlaybackStateCompat.STATE_NONE, _player.exoPlayer?.player?.currentPosition ?: 0); StatePlayer.instance.updateMediaSessionPlaybackState();
} }
if(playing) { if(playing) {

View file

@ -1,8 +1,7 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.helpers package com.futo.platformplayer.helpers
import android.net.Uri import android.net.Uri
import androidx.annotation.OptIn
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor 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.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
@ -15,11 +14,12 @@ 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.JSAudioUrlRangeSource
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.google.android.exoplayer2.MediaItem import androidx.media3.common.MediaItem
import com.google.android.exoplayer2.source.MediaSource import androidx.media3.common.util.UnstableApi
import com.google.android.exoplayer2.source.dash.DashMediaSource import androidx.media3.datasource.ResolvingDataSource
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser import androidx.media3.exoplayer.dash.DashMediaSource
import com.google.android.exoplayer2.upstream.ResolvingDataSource import androidx.media3.exoplayer.dash.manifest.DashManifestParser
import androidx.media3.exoplayer.source.MediaSource
import kotlin.math.abs import kotlin.math.abs
class VideoHelper { class VideoHelper {
@ -121,7 +121,7 @@ class VideoHelper {
return bestSource; return bestSource;
} }
@Suppress("DEPRECATION") @OptIn(UnstableApi::class)
fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : MediaSource { fun convertItagSourceToChunkedDashSource(videoSource: JSVideoUrlRangeSource) : MediaSource {
val urlToUse = videoSource.getVideoUrl(); val urlToUse = videoSource.getVideoUrl();
val manifestConfig = ProgressiveDashManifestCreator.fromVideoProgressiveStreamingUrl(urlToUse, val manifestConfig = ProgressiveDashManifestCreator.fromVideoProgressiveStreamingUrl(urlToUse,
@ -140,14 +140,13 @@ class VideoHelper {
); );
val manifest = DashManifestParser().parse(Uri.parse(""), manifestConfig.byteInputStream()); val manifest = DashManifestParser().parse(Uri.parse(""), manifestConfig.byteInputStream());
return DashMediaSource.Factory(ResolvingDataSource.Factory(videoSource.getHttpDataSourceFactory(), ResolvingDataSource.Resolver { dataSpec -> return DashMediaSource.Factory(ResolvingDataSource.Factory(videoSource.getHttpDataSourceFactory(), ResolvingDataSource.Resolver { dataSpec ->
Logger.v("PLAYBACK", "Video REQ Range [" + dataSpec.position + "-" + (dataSpec.position + dataSpec.length) + "](" + dataSpec.length + ")", null); Logger.v("PLAYBACK", "Video REQ Range [" + dataSpec.position + "-" + (dataSpec.position + dataSpec.length) + "](" + dataSpec.length + ")", null);
return@Resolver dataSpec; return@Resolver dataSpec;
})).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(videoSource.getVideoUrl())).build()) })).createMediaSource(manifest, MediaItem.Builder().setUri(Uri.parse(videoSource.getVideoUrl())).build())
} }
@Suppress("DEPRECATION") @OptIn(UnstableApi::class)
fun convertItagSourceToChunkedDashSource(audioSource: JSAudioUrlRangeSource) : MediaSource { fun convertItagSourceToChunkedDashSource(audioSource: JSAudioUrlRangeSource) : MediaSource {
val manifestConfig = ProgressiveDashManifestCreator.fromAudioProgressiveStreamingUrl(audioSource.getAudioUrl(), val manifestConfig = ProgressiveDashManifestCreator.fromAudioProgressiveStreamingUrl(audioSource.getAudioUrl(),
audioSource.duration?.times(1000) ?: 0, audioSource.duration?.times(1000) ?: 0,

View file

@ -17,7 +17,6 @@ class InstallReceiver : BroadcastReceiver() {
val onReceiveResult = Event1<String?>(); val onReceiveResult = Event1<String?>();
} }
@Suppress("DEPRECATION")
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1); val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1);
Logger.i(TAG, "Received status $status."); Logger.i(TAG, "Received status $status.");

View file

@ -13,15 +13,14 @@ import android.graphics.drawable.Drawable
import android.media.AudioFocusRequest import android.media.AudioFocusRequest
import android.media.AudioManager import android.media.AudioManager
import android.media.AudioManager.OnAudioFocusChangeListener import android.media.AudioManager.OnAudioFocusChangeListener
import android.media.MediaMetadata
import android.os.Build import android.os.Build
import android.os.IBinder 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 android.util.Log
import androidx.annotation.OptIn
import androidx.core.app.NotificationCompat 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.Glide
import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition import com.bumptech.glide.request.transition.Transition
@ -51,7 +50,7 @@ class MediaPlaybackService : Service() {
private var _audioManager: AudioManager? = null; private var _audioManager: AudioManager? = null;
private var _notificationManager: NotificationManager? = null; private var _notificationManager: NotificationManager? = null;
private var _notificationChannel: NotificationChannel? = null; private var _notificationChannel: NotificationChannel? = null;
private var _mediaSession: MediaSessionCompat? = null; private var _mediaSession: MediaSession? = null;
private var _hasFocus: Boolean = false; private var _hasFocus: Boolean = false;
private var _focusRequest: AudioFocusRequest? = null; private var _focusRequest: AudioFocusRequest? = null;
private var _audioFocusLossTime_ms: Long? = null private var _audioFocusLossTime_ms: Long? = null
@ -82,6 +81,7 @@ class MediaPlaybackService : Service() {
return START_STICKY; return START_STICKY;
} }
@OptIn(UnstableApi::class)
fun setupNotificationRequirements() { fun setupNotificationRequirements() {
_audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager; _audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager;
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager; _notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
@ -91,47 +91,54 @@ class MediaPlaybackService : Service() {
}; };
_notificationManager!!.createNotificationChannel(_notificationChannel!!); _notificationManager!!.createNotificationChannel(_notificationChannel!!);
_mediaSession = MediaSessionCompat(this, "PlayerState"); _mediaSession = MediaSession.Builder(this, StatePlayer.instance.getPlayerOrCreate(this).player)
_mediaSession?.setPlaybackState(PlaybackStateCompat.Builder()
.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()
.setState(PlaybackStateCompat.STATE_PLAYING, 0, 1f) .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() { override fun onCreate() {
@ -186,15 +193,7 @@ class MediaPlaybackService : Service() {
if(_notificationChannel == null || _mediaSession == null) if(_notificationChannel == null || _mediaSession == null)
setupNotificationRequirements(); 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(); val thumbnail = video.thumbnails.getHQThumbnail();
_notif_last_video = video; _notif_last_video = video;
if(isUpdating) if(isUpdating)
@ -221,6 +220,7 @@ class MediaPlaybackService : Service() {
private fun generateMediaAction(icon: Int, title: String, intent: PendingIntent) : NotificationCompat.Action { private fun generateMediaAction(icon: Int, title: String, intent: PendingIntent) : NotificationCompat.Action {
return NotificationCompat.Action.Builder(icon, title, intent).build(); return NotificationCompat.Action.Builder(icon, title, intent).build();
} }
@OptIn(UnstableApi::class)
private fun notifyMediaSession(video: IPlatformVideo?, desiredBitmap: Bitmap?) { private fun notifyMediaSession(video: IPlatformVideo?, desiredBitmap: Bitmap?) {
val channel = _notificationChannel ?: return; val channel = _notificationChannel ?: return;
val session = _mediaSession ?: return; val session = _mediaSession ?: return;
@ -249,14 +249,9 @@ class MediaPlaybackService : Service() {
.setOngoing(true) .setOngoing(true)
.setSilent(true) .setSilent(true)
.setContentIntent(PendingIntent.getActivity(this, 5, bringUpIntent, PendingIntent.FLAG_IMMUTABLE)) .setContentIntent(PendingIntent.getActivity(this, 5, bringUpIntent, PendingIntent.FLAG_IMMUTABLE))
.setStyle(if(hasQueue) .setStyle(
androidx.media.app.NotificationCompat.MediaStyle() if(hasQueue) MediaStyleNotificationHelper.MediaStyle(session)//.setShowActionsInCompactView(0, 1, 2)
.setMediaSession(session.sessionToken) else MediaStyleNotificationHelper.MediaStyle(session))//.setShowActionsInCompactView(0))
.setShowActionsInCompactView(0, 1, 2)
else
androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(session.sessionToken)
.setShowActionsInCompactView(0))
.setDeleteIntent(deleteIntent) .setDeleteIntent(deleteIntent)
.setChannelId(channel.id) .setChannelId(channel.id)
@ -303,7 +298,7 @@ class MediaPlaybackService : Service() {
val notif = builder.build(); val notif = builder.build();
notif.flags = notif.flags or NotificationCompat.FLAG_ONGOING_EVENT or NotificationCompat.FLAG_NO_CLEAR; 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.sessionToken}"); 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}");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// For API 29 and above // For API 29 and above
@ -316,20 +311,7 @@ class MediaPlaybackService : Service() {
_notif_last_bitmap = bitmap; _notif_last_bitmap = bitmap;
} }
fun updateMediaSessionPlaybackState(state: Int, pos: Long) { fun updateMediaSessionPlaybackState() {
_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) if(_focusRequest == null)
setAudioFocus(); setAudioFocus();
} }

View file

@ -1,8 +1,12 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.states package com.futo.platformplayer.states
import android.content.Context import android.content.Context
import androidx.annotation.OptIn
import androidx.media3.common.C
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.upstream.DefaultAllocator
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
@ -13,10 +17,6 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.services.MediaPlaybackService import com.futo.platformplayer.services.MediaPlaybackService
import com.futo.platformplayer.video.PlayerManager import com.futo.platformplayer.video.PlayerManager
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.upstream.DefaultAllocator
import kotlin.random.Random import kotlin.random.Random
/*** /***
@ -107,8 +107,8 @@ class StatePlayer {
fun updateMediaSession(videoUpdated: IPlatformVideoDetails?) { fun updateMediaSession(videoUpdated: IPlatformVideoDetails?) {
MediaPlaybackService.getService()?.updateMediaSession(videoUpdated); MediaPlaybackService.getService()?.updateMediaSession(videoUpdated);
} }
fun updateMediaSessionPlaybackState(state: Int, pos: Long) { fun updateMediaSessionPlaybackState() {
MediaPlaybackService.getService()?.updateMediaSessionPlaybackState(state, pos); MediaPlaybackService.getService()?.updateMediaSessionPlaybackState();
} }
fun closeMediaSession() { fun closeMediaSession() {
MediaPlaybackService.getService()?.closeMediaSession(); MediaPlaybackService.getService()?.closeMediaSession();
@ -557,21 +557,23 @@ class StatePlayer {
} }
//Player Initialization //Player Initialization
fun getPlayerOrCreate(context : Context) : PlayerManager { fun getPlayerOrCreate(context: Context) : PlayerManager {
if(_exoplayer == null) { if(_exoplayer == null) {
val player = createExoPlayer(context); val player = createExoPlayer(context);
_exoplayer = PlayerManager(player); _exoplayer = PlayerManager(player);
} }
return _exoplayer!!; return _exoplayer!!;
} }
fun getThumbnailPlayerOrCreate(context : Context) : PlayerManager { fun getThumbnailPlayerOrCreate(context: Context) : PlayerManager {
if(_thumbnailExoPlayer == null) { if(_thumbnailExoPlayer == null) {
val player = createExoPlayer(context); val player = createExoPlayer(context);
_thumbnailExoPlayer = PlayerManager(player); _thumbnailExoPlayer = PlayerManager(player);
} }
return _thumbnailExoPlayer!!; return _thumbnailExoPlayer!!;
} }
private fun createExoPlayer(context : Context) : ExoPlayer {
@OptIn(UnstableApi::class)
private fun createExoPlayer(context : Context): ExoPlayer {
return ExoPlayer.Builder(context) return ExoPlayer.Builder(context)
.setLoadControl( .setLoadControl(
DefaultLoadControl.Builder() DefaultLoadControl.Builder()
@ -589,7 +591,6 @@ class StatePlayer {
.build(); .build();
} }
fun dispose(){ fun dispose(){
val player = _exoplayer; val player = _exoplayer;
val thumbPlayer = _thumbnailExoPlayer; val thumbPlayer = _thumbnailExoPlayer;

View file

@ -1,15 +1,11 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.video package com.futo.platformplayer.video
import android.media.session.PlaybackState import androidx.media3.common.Player
import android.support.v4.media.session.PlaybackStateCompat import androidx.media3.exoplayer.ExoPlayer
import com.google.android.exoplayer2.ExoPlayer import androidx.media3.ui.PlayerView
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.StyledPlayerView
class PlayerManager { class PlayerManager {
private var _currentView: StyledPlayerView? = null; private var _currentView: PlayerView? = null;
private val _stateMap = HashMap<String, PlayerState>(); private val _stateMap = HashMap<String, PlayerState>();
private var _currentState: PlayerState? = null; private var _currentState: PlayerState? = null;
val currentState: PlayerState get() { val currentState: PlayerState get() {
@ -25,16 +21,8 @@ class PlayerManager {
this.player = exoPlayer; 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 @Synchronized
fun attach(view: StyledPlayerView, stateName: String) { fun attach(view: PlayerView, stateName: String) {
if(view != _currentView) { if(view != _currentView) {
_currentView?.player = null; _currentView?.player = null;
switchState(stateName); switchState(stateName);

View file

@ -1,10 +1,6 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.views.casting package com.futo.platformplayer.views.casting
import android.content.Context import android.content.Context
import android.media.session.PlaybackState
import android.support.v4.media.session.PlaybackStateCompat
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
@ -13,7 +9,11 @@ import android.widget.FrameLayout
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.OptIn
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.DefaultTimeBar
import androidx.media3.ui.TimeBar
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
@ -23,9 +23,6 @@ import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.toHumanTime import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.views.behavior.GestureControlView import com.futo.platformplayer.views.behavior.GestureControlView
import com.google.android.exoplayer2.ui.DefaultTimeBar
import com.google.android.exoplayer2.ui.TimeBar
import com.google.android.exoplayer2.ui.TimeBar.OnScrubListener
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -54,6 +51,7 @@ class CastView : ConstraintLayout {
val onMinimizeClick = Event0(); val onMinimizeClick = Event0();
val onSettingsClick = Event0(); val onSettingsClick = Event0();
@OptIn(UnstableApi::class)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
LayoutInflater.from(context).inflate(R.layout.view_cast, this, true); LayoutInflater.from(context).inflate(R.layout.view_cast, this, true);
@ -83,7 +81,7 @@ class CastView : ConstraintLayout {
} }
_buttonLoop.setImageResource(if(StatePlayer.instance.loopVideo) R.drawable.ic_loop_active else R.drawable.ic_loop); _buttonLoop.setImageResource(if(StatePlayer.instance.loopVideo) R.drawable.ic_loop_active else R.drawable.ic_loop);
_timeBar.addListener(object : OnScrubListener { _timeBar.addListener(object : TimeBar.OnScrubListener {
override fun onScrubStart(timeBar: TimeBar, position: Long) { override fun onScrubStart(timeBar: TimeBar, position: Long) {
StateCasting.instance.videoSeekTo(position.toDouble()); StateCasting.instance.videoSeekTo(position.toDouble());
} }
@ -149,11 +147,9 @@ class CastView : ConstraintLayout {
_buttonPlay.visibility = View.VISIBLE; _buttonPlay.visibility = View.VISIBLE;
} }
val position = StateCasting.instance.activeDevice?.expectedCurrentTime?.times(1000.0)?.toLong();
if(StatePlayer.instance.hasMediaSession()) { if(StatePlayer.instance.hasMediaSession()) {
StatePlayer.instance.updateMediaSession(null); StatePlayer.instance.updateMediaSession(null);
StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), (position ?: 0)); StatePlayer.instance.updateMediaSessionPlaybackState();
} }
} }
@ -183,6 +179,7 @@ class CastView : ConstraintLayout {
} }
} }
@OptIn(UnstableApi::class)
fun setVideoDetails(video: IPlatformVideoDetails, position: Long) { fun setVideoDetails(video: IPlatformVideoDetails, position: Long) {
Glide.with(_thumbnail) Glide.with(_thumbnail)
.load(video.thumbnails.getHQThumbnail()) .load(video.thumbnails.getHQThumbnail())
@ -194,10 +191,11 @@ class CastView : ConstraintLayout {
_timeBar.setDuration(video.duration); _timeBar.setDuration(video.duration);
} }
@OptIn(UnstableApi::class)
fun setTime(ms: Long) { fun setTime(ms: Long) {
_textPosition.text = ms.toHumanTime(true); _textPosition.text = ms.toHumanTime(true);
_timeBar.setPosition(ms / 1000); _timeBar.setPosition(ms / 1000);
StatePlayer.instance.updateMediaSessionPlaybackState(getPlaybackStateCompat(), ms); StatePlayer.instance.updateMediaSessionPlaybackState();
} }
fun cleanup() { fun cleanup() {
@ -207,13 +205,4 @@ class CastView : ConstraintLayout {
_updateTimeJob = null; _updateTimeJob = null;
_scope.cancel(); _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;
}
}
} }

View file

@ -1,5 +1,3 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.views.video package com.futo.platformplayer.views.video
import android.content.Context import android.content.Context
@ -9,6 +7,10 @@ import android.view.View
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.PlayerControlView
import androidx.media3.ui.PlayerView
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
@ -17,8 +19,6 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.helpers.VideoHelper import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.toHumanTime import com.futo.platformplayer.toHumanTime
import com.futo.platformplayer.video.PlayerManager import com.futo.platformplayer.video.PlayerManager
import com.google.android.exoplayer2.ui.PlayerControlView
import com.google.android.exoplayer2.ui.StyledPlayerView
class FutoThumbnailPlayer : FutoVideoPlayerBase { class FutoThumbnailPlayer : FutoVideoPlayerBase {
@ -28,7 +28,7 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
} }
//Views //Views
private val videoView : StyledPlayerView; private val videoView : PlayerView;
private val videoControls : PlayerControlView; private val videoControls : PlayerControlView;
private val buttonMute : ImageButton; private val buttonMute : ImageButton;
private val buttonUnMute : ImageButton; private val buttonUnMute : ImageButton;
@ -41,7 +41,8 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
private val _evMuteChanged = mutableListOf<(FutoThumbnailPlayer, Boolean)->Unit>(); private val _evMuteChanged = mutableListOf<(FutoThumbnailPlayer, Boolean)->Unit>();
constructor(context : Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) { @OptIn(UnstableApi::class)
constructor(context: Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) {
LayoutInflater.from(context).inflate(R.layout.thumbnail_video_view, this, true); LayoutInflater.from(context).inflate(R.layout.thumbnail_video_view, this, true);
videoView = findViewById(R.id.video_player); videoView = findViewById(R.id.video_player);
@ -70,7 +71,7 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
} }
} }
fun setLive(live : Boolean) { fun setLive(live: Boolean) {
if(live) { if(live) {
containerDuration.visibility = GONE; containerDuration.visibility = GONE;
containerLive.visibility = VISIBLE; containerLive.visibility = VISIBLE;
@ -81,7 +82,8 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
} }
} }
fun setPlayer(player : PlayerManager?){ @OptIn(UnstableApi::class)
fun setPlayer(player: PlayerManager?){
changePlayer(player); changePlayer(player);
player?.attach(videoView, PLAYER_STATE_NAME); player?.attach(videoView, PLAYER_STATE_NAME);
videoControls.player = player?.player; videoControls.player = player?.player;

View file

@ -1,5 +1,3 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.views.video package com.futo.platformplayer.views.video
import android.content.Context import android.content.Context
@ -15,6 +13,7 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.TextView import android.widget.TextView
import androidx.annotation.OptIn
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.setMargins import androidx.core.view.setMargins
import com.futo.platformplayer.R import com.futo.platformplayer.R
@ -31,13 +30,14 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.behavior.GestureControlView import com.futo.platformplayer.views.behavior.GestureControlView
import com.google.android.exoplayer2.ExoPlayer import androidx.media3.common.PlaybackParameters
import com.google.android.exoplayer2.PlaybackParameters import androidx.media3.common.VideoSize
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout import androidx.media3.common.util.UnstableApi
import com.google.android.exoplayer2.ui.PlayerControlView import androidx.media3.exoplayer.ExoPlayer
import com.google.android.exoplayer2.ui.StyledPlayerView import androidx.media3.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.TimeBar import androidx.media3.ui.PlayerControlView
import com.google.android.exoplayer2.video.VideoSize import androidx.media3.ui.PlayerView
import androidx.media3.ui.TimeBar
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -58,7 +58,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
//Views //Views
private val _root: ConstraintLayout; private val _root: ConstraintLayout;
private val _videoView: StyledPlayerView; private val _videoView: PlayerView;
val videoControls: PlayerControlView; val videoControls: PlayerControlView;
private val _videoControls_fullscreen: PlayerControlView; private val _videoControls_fullscreen: PlayerControlView;
@ -127,6 +127,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
val onVideoClicked = Event0(); val onVideoClicked = Event0();
val onTimeBarChanged = Event2<Long, Long>(); val onTimeBarChanged = Event2<Long, Long>();
@OptIn(UnstableApi::class)
constructor(context: Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) { constructor(context: Context, attrs: AttributeSet? = null) : super(PLAYER_STATE_NAME, context, attrs) {
LayoutInflater.from(context).inflate(R.layout.video_view, this, true); LayoutInflater.from(context).inflate(R.layout.video_view, this, true);
_root = findViewById(R.id.videoview_root); _root = findViewById(R.id.videoview_root);
@ -139,8 +140,8 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_control_rotate_lock = videoControls.findViewById(R.id.exo_rotate_lock); _control_rotate_lock = videoControls.findViewById(R.id.exo_rotate_lock);
_control_loop = videoControls.findViewById(R.id.exo_loop); _control_loop = videoControls.findViewById(R.id.exo_loop);
_control_cast = videoControls.findViewById(R.id.exo_cast); _control_cast = videoControls.findViewById(R.id.exo_cast);
_control_play = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_play); _control_play = videoControls.findViewById(androidx.media3.ui.R.id.exo_play);
_time_bar = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress); _time_bar = videoControls.findViewById(androidx.media3.ui.R.id.exo_progress);
_control_chapter = videoControls.findViewById(R.id.text_chapter_current); _control_chapter = videoControls.findViewById(R.id.text_chapter_current);
_buttonNext = videoControls.findViewById(R.id.button_next); _buttonNext = videoControls.findViewById(R.id.button_next);
_buttonPrevious = videoControls.findViewById(R.id.button_previous); _buttonPrevious = videoControls.findViewById(R.id.button_previous);
@ -152,9 +153,9 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
_control_rotate_lock_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_rotate_lock); _control_rotate_lock_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_rotate_lock);
_control_loop_fullscreen = videoControls.findViewById(R.id.exo_loop); _control_loop_fullscreen = videoControls.findViewById(R.id.exo_loop);
_control_cast_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_cast); _control_cast_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_cast);
_control_play_fullscreen = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_play); _control_play_fullscreen = videoControls.findViewById(androidx.media3.ui.R.id.exo_play);
_control_chapter_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_chapter_current); _control_chapter_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_chapter_current);
_time_bar_fullscreen = _videoControls_fullscreen.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress); _time_bar_fullscreen = _videoControls_fullscreen.findViewById(androidx.media3.ui.R.id.exo_progress);
_buttonPrevious_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_previous); _buttonPrevious_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_previous);
_buttonNext_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_next); _buttonNext_fullscreen = _videoControls_fullscreen.findViewById(R.id.button_next);
@ -404,14 +405,15 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
return false; return false;
} }
@OptIn(UnstableApi::class)
fun setArtwork(drawable: Drawable?) { fun setArtwork(drawable: Drawable?) {
if (drawable != null) { if (drawable != null) {
_videoView.defaultArtwork = drawable; _videoView.defaultArtwork = drawable;
_videoView.artworkDisplayMode = StyledPlayerView.ARTWORK_DISPLAY_MODE_FILL; _videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_FILL;
fitOrFill(isFullScreen); fitOrFill(isFullScreen);
} else { } else {
_videoView.defaultArtwork = null; _videoView.defaultArtwork = null;
_videoView.artworkDisplayMode = StyledPlayerView.ARTWORK_DISPLAY_MODE_OFF; _videoView.artworkDisplayMode = PlayerView.ARTWORK_DISPLAY_MODE_OFF;
} }
} }
@ -436,6 +438,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
return exoPlayer?.player?.playbackParameters?.speed ?: 1.0f; return exoPlayer?.player?.playbackParameters?.speed ?: 1.0f;
} }
@OptIn(UnstableApi::class)
fun setFullScreen(fullScreen: Boolean) { fun setFullScreen(fullScreen: Boolean) {
if (isFullScreen == fullScreen) { if (isFullScreen == fullScreen) {
return; return;
@ -538,6 +541,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
} }
//Sizing //Sizing
@OptIn(UnstableApi::class)
fun fitHeight(videoSize : VideoSize? = null){ fun fitHeight(videoSize : VideoSize? = null){
Logger.i(TAG, "Video Fit Height"); Logger.i(TAG, "Video Fit Height");
if(_originalBottomMargin != 0) { if(_originalBottomMargin != 0) {

View file

@ -1,11 +1,27 @@
@file:Suppress("DEPRECATION")
package com.futo.platformplayer.views.video package com.futo.platformplayer.views.video
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.annotation.OptIn
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.VideoSize
import androidx.media3.common.text.CueGroup
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.dash.DashMediaSource
import androidx.media3.exoplayer.hls.HlsMediaSource
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.MergingMediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.exoplayer.source.SingleSampleMediaSource
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
@ -28,22 +44,6 @@ import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.video.PlayerManager import com.futo.platformplayer.video.PlayerManager
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.MergingMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.source.SingleSampleMediaSource
import com.google.android.exoplayer2.source.dash.DashMediaSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.text.CueGroup
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.upstream.DefaultDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.video.VideoSize
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -251,6 +251,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
_targetTrackAudioBitrate = bitrate; _targetTrackAudioBitrate = bitrate;
updateTrackSelector(); updateTrackSelector();
} }
@OptIn(UnstableApi::class)
private fun updateTrackSelector() { private fun updateTrackSelector() {
var builder = DefaultTrackSelector.Parameters.Builder(context); var builder = DefaultTrackSelector.Parameters.Builder(context);
if(_targetTrackVideoHeight > 0) { if(_targetTrackVideoHeight > 0) {
@ -298,6 +299,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
return loadSelectedSources(play, resume); return loadSelectedSources(play, resume);
} }
@OptIn(UnstableApi::class)
fun swapSubtitles(scope: CoroutineScope, subtitles: ISubtitleSource?) { fun swapSubtitles(scope: CoroutineScope, subtitles: ISubtitleSource?) {
if(subtitles == null) if(subtitles == null)
clearSubtitles(); clearSubtitles();
@ -369,6 +371,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
//Video loads //Video loads
@OptIn(UnstableApi::class)
private fun swapVideoSourceLocal(videoSource: LocalVideoSource) { private fun swapVideoSourceLocal(videoSource: LocalVideoSource) {
Logger.i(TAG, "Loading VideoSource [Local]"); Logger.i(TAG, "Loading VideoSource [Local]");
val file = File(videoSource.filePath); val file = File(videoSource.filePath);
@ -377,14 +380,13 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
_lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context)) _lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
.createMediaSource(MediaItem.fromUri(Uri.fromFile(file))); .createMediaSource(MediaItem.fromUri(Uri.fromFile(file)));
} }
@OptIn(UnstableApi::class)
private fun swapVideoSourceUrlRange(videoSource: JSVideoUrlRangeSource) { private fun swapVideoSourceUrlRange(videoSource: JSVideoUrlRangeSource) {
Logger.i(TAG, "Loading JSVideoUrlRangeSource"); Logger.i(TAG, "Loading JSVideoUrlRangeSource");
if(videoSource.hasItag) { if(videoSource.hasItag) {
//Temporary workaround for Youtube //Temporary workaround for Youtube
try { try {
_lastVideoMediaSource = VideoHelper.convertItagSourceToChunkedDashSource(videoSource); _lastVideoMediaSource = VideoHelper.convertItagSourceToChunkedDashSource(videoSource);
if(_lastVideoMediaSource == null)
throw java.lang.IllegalStateException("Dash manifest workaround failed");
return; return;
} }
//If it fails to create the dash workaround, fallback to standard progressive //If it fails to create the dash workaround, fallback to standard progressive
@ -397,18 +399,21 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
else throw IllegalArgumentException("source without itag data..."); else throw IllegalArgumentException("source without itag data...");
} }
@OptIn(UnstableApi::class)
private fun swapVideoSourceUrl(videoSource: IVideoUrlSource) { private fun swapVideoSourceUrl(videoSource: IVideoUrlSource) {
Logger.i(TAG, "Loading VideoSource [Url]"); Logger.i(TAG, "Loading VideoSource [Url]");
_lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory() _lastVideoMediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory()
.setUserAgent(DEFAULT_USER_AGENT)) .setUserAgent(DEFAULT_USER_AGENT))
.createMediaSource(MediaItem.fromUri(videoSource.getVideoUrl())); .createMediaSource(MediaItem.fromUri(videoSource.getVideoUrl()));
} }
@OptIn(UnstableApi::class)
private fun swapVideoSourceDash(videoSource: IDashManifestSource) { private fun swapVideoSourceDash(videoSource: IDashManifestSource) {
Logger.i(TAG, "Loading VideoSource [Dash]"); Logger.i(TAG, "Loading VideoSource [Dash]");
_lastVideoMediaSource = DashMediaSource.Factory(DefaultHttpDataSource.Factory() _lastVideoMediaSource = DashMediaSource.Factory(DefaultHttpDataSource.Factory()
.setUserAgent(DEFAULT_USER_AGENT)) .setUserAgent(DEFAULT_USER_AGENT))
.createMediaSource(MediaItem.fromUri(videoSource.url)) .createMediaSource(MediaItem.fromUri(videoSource.url))
} }
@OptIn(UnstableApi::class)
private fun swapVideoSourceHLS(videoSource: IHLSManifestSource) { private fun swapVideoSourceHLS(videoSource: IHLSManifestSource) {
Logger.i(TAG, "Loading VideoSource [HLS]"); Logger.i(TAG, "Loading VideoSource [HLS]");
_lastVideoMediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory() _lastVideoMediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory()
@ -416,7 +421,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
.createMediaSource(MediaItem.fromUri(videoSource.url)); .createMediaSource(MediaItem.fromUri(videoSource.url));
} }
//Audio loads //Audio loads
@OptIn(UnstableApi::class)
private fun swapAudioSourceLocal(audioSource: LocalAudioSource) { private fun swapAudioSourceLocal(audioSource: LocalAudioSource) {
Logger.i(TAG, "Loading AudioSource [Local]"); Logger.i(TAG, "Loading AudioSource [Local]");
val file = File(audioSource.filePath); val file = File(audioSource.filePath);
@ -425,6 +432,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
_lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context)) _lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
.createMediaSource(MediaItem.fromUri(Uri.fromFile(file))); .createMediaSource(MediaItem.fromUri(Uri.fromFile(file)));
} }
@OptIn(UnstableApi::class)
private fun swapAudioSourceUrlRange(audioSource: JSAudioUrlRangeSource) { private fun swapAudioSourceUrlRange(audioSource: JSAudioUrlRangeSource) {
Logger.i(TAG, "Loading JSAudioUrlRangeSource"); Logger.i(TAG, "Loading JSAudioUrlRangeSource");
if(audioSource.hasItag) { if(audioSource.hasItag) {
@ -444,12 +452,14 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
} }
else throw IllegalArgumentException("source without itag data...") else throw IllegalArgumentException("source without itag data...")
} }
@OptIn(UnstableApi::class)
private fun swapAudioSourceUrl(audioSource: IAudioUrlSource) { private fun swapAudioSourceUrl(audioSource: IAudioUrlSource) {
Logger.i(TAG, "Loading AudioSource [Url]"); Logger.i(TAG, "Loading AudioSource [Url]");
_lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory() _lastAudioMediaSource = ProgressiveMediaSource.Factory(DefaultHttpDataSource.Factory()
.setUserAgent(DEFAULT_USER_AGENT)) .setUserAgent(DEFAULT_USER_AGENT))
.createMediaSource(MediaItem.fromUri(audioSource.getAudioUrl())); .createMediaSource(MediaItem.fromUri(audioSource.getAudioUrl()));
} }
@OptIn(UnstableApi::class)
private fun swapAudioSourceHLS(audioSource: IHLSManifestAudioSource) { private fun swapAudioSourceHLS(audioSource: IHLSManifestAudioSource) {
Logger.i(TAG, "Loading AudioSource [HLS]"); Logger.i(TAG, "Loading AudioSource [HLS]");
_lastAudioMediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory() _lastAudioMediaSource = HlsMediaSource.Factory(DefaultHttpDataSource.Factory()
@ -479,6 +489,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
return VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, preferredLanguage); return VideoHelper.selectBestAudioSource(video.video, PREFERED_AUDIO_CONTAINERS, preferredLanguage);
} }
@OptIn(UnstableApi::class)
private fun loadSelectedSources(play: Boolean, resume: Boolean): Boolean { private fun loadSelectedSources(play: Boolean, resume: Boolean): Boolean {
val sourceVideo = if(!isAudioMode || _lastAudioMediaSource == null) _lastVideoMediaSource else null; val sourceVideo = if(!isAudioMode || _lastAudioMediaSource == null) _lastVideoMediaSource else null;
val sourceAudio = _lastAudioMediaSource; val sourceAudio = _lastAudioMediaSource;
@ -506,11 +517,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
return true; return true;
} }
@OptIn(UnstableApi::class)
private fun reloadMediaSource(play: Boolean = false, resume: Boolean = true) { private fun reloadMediaSource(play: Boolean = false, resume: Boolean = true) {
val player = exoPlayer val player = exoPlayer ?: return
if (player == null)
return;
val positionBefore = player.player.currentPosition; val positionBefore = player.player.currentPosition;
if(_mediaSource != null) { if(_mediaSource != null) {
player.player.setMediaSource(_mediaSource!!); player.player.setMediaSource(_mediaSource!!);

View file

@ -1,26 +1,28 @@
package com.futo.platformplayer.views.video.datasources; package com.futo.platformplayer.views.video.datasources;
import static com.google.android.exoplayer2.upstream.HttpUtil.buildRangeRequestHeader; import static androidx.media3.common.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static androidx.media3.common.util.Util.castNonNull;
import static com.google.android.exoplayer2.util.Util.castNonNull; import static androidx.media3.datasource.HttpUtil.buildRangeRequestHeader;
import static java.lang.Math.min; import static java.lang.Math.min;
import android.net.Uri; import android.net.Uri;
import android.util.Log;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier; import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier;
import com.google.android.exoplayer2.C; import androidx.media3.common.C;
import com.google.android.exoplayer2.PlaybackException; import androidx.media3.common.PlaybackException;
import com.google.android.exoplayer2.upstream.BaseDataSource; import androidx.media3.common.util.UnstableApi;
import com.google.android.exoplayer2.upstream.DataSourceException; import androidx.media3.common.util.Util;
import com.google.android.exoplayer2.upstream.DataSpec; import androidx.media3.datasource.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod; import androidx.media3.datasource.DataSourceException;
import com.google.android.exoplayer2.upstream.HttpDataSource; import androidx.media3.datasource.DataSpec;
import com.google.android.exoplayer2.upstream.HttpUtil; import androidx.media3.datasource.HttpDataSource;
import com.google.android.exoplayer2.upstream.TransferListener; import androidx.media3.datasource.HttpUtil;
import com.google.android.exoplayer2.util.Log; import androidx.media3.datasource.TransferListener;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.ForwardingMap; import com.google.common.collect.ForwardingMap;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
@ -45,6 +47,7 @@ import java.util.zip.GZIPInputStream;
* Based on the default ExoPlayer DefaultHttpDataSource * Based on the default ExoPlayer DefaultHttpDataSource
*/ */
@UnstableApi
public class JSHttpDataSource extends BaseDataSource implements HttpDataSource { public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
public static final class Factory implements HttpDataSource.Factory { public static final class Factory implements HttpDataSource.Factory {
@ -142,7 +145,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
/** /**
* Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a * Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
* {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link * {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link
* JSHttpDataSource#open(com.google.android.exoplayer2.upstream.DataSpec)}. * JSHttpDataSource#open(androidx.media3.datasource.DataSpec)}.
* *
* <p>The default is {@code null}. * <p>The default is {@code null}.
* *
@ -160,7 +163,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
* *
* <p>The default is {@code null}. * <p>The default is {@code null}.
* *
* <p>See {@link com.google.android.exoplayer2.upstream.DataSource#addTransferListener(TransferListener)}. * <p>See {@link androidx.media3.datasource.DataSource#addTransferListener(TransferListener)}.
* *
* @param transferListener The listener that will be used. * @param transferListener The listener that will be used.
* @return This factory. * @return This factory.
@ -367,12 +370,12 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
if (dataSpec.length != C.LENGTH_UNSET) { if (dataSpec.length != C.LENGTH_UNSET) {
bytesToRead = dataSpec.length; bytesToRead = dataSpec.length;
} else { } else {
long contentLength = long contentLength = HttpUtil.getContentLength(
HttpUtil.getContentLength( connection.getHeaderField(HttpHeaders.CONTENT_LENGTH),
connection.getHeaderField(HttpHeaders.CONTENT_LENGTH), connection.getHeaderField(HttpHeaders.CONTENT_RANGE)
connection.getHeaderField(HttpHeaders.CONTENT_RANGE)); );
bytesToRead =
contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET; bytesToRead = contentLength != C.LENGTH_UNSET ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
} }
} else { } else {
// Gzip is enabled. If the server opts to use gzip then the content length in the response // Gzip is enabled. If the server opts to use gzip then the content length in the response
@ -457,7 +460,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
/** Establishes a connection, following redirects to do so where permitted. */ /** Establishes a connection, following redirects to do so where permitted. */
private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
URL url = new URL(dataSpec.uri.toString()); URL url = new URL(dataSpec.uri.toString());
@HttpMethod int httpMethod = dataSpec.httpMethod; @DataSpec.HttpMethod int httpMethod = dataSpec.httpMethod;
@Nullable byte[] httpBody = dataSpec.httpBody; @Nullable byte[] httpBody = dataSpec.httpBody;
long position = dataSpec.position; long position = dataSpec.position;
long length = dataSpec.length; long length = dataSpec.length;
@ -543,7 +546,7 @@ public class JSHttpDataSource extends BaseDataSource implements HttpDataSource {
*/ */
private HttpURLConnection makeConnection( private HttpURLConnection makeConnection(
URL url, URL url,
@HttpMethod int httpMethod, @DataSpec.HttpMethod int httpMethod,
@Nullable byte[] httpBody, @Nullable byte[] httpBody,
long position, long position,
long length, long length,

View file

@ -4,7 +4,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/transparent" android:background="@color/transparent"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.exoplayer2.ui.StyledPlayerView <com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/video_player" android:id="@+id/video_player"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View file

@ -6,7 +6,7 @@
android:id="@+id/videoview_root" android:id="@+id/videoview_root"
android:background="@color/transparent" android:background="@color/transparent"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.exoplayer2.ui.StyledPlayerView <com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/video_player" android:id="@+id/video_player"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"