From 619a588559e42fedcfc82c8e7831313004bbfd08 Mon Sep 17 00:00:00 2001 From: ocean <503259349@qq.com> Date: Thu, 9 May 2024 18:55:15 +0800 Subject: [PATCH] update --- .../musicoo/activity/MoPlayDetailsActivity.kt | 337 +++++++++++++----- .../player/musicoo/fragment/MoHomeFragment.kt | 40 ++- .../musicoo/innertube/models/Context.kt | 2 +- .../innertube/models/PlayerResponse.kt | 2 +- .../musicoo/media/MediaControllerManager.kt | 30 +- .../player/musicoo/service/PlaybackService.kt | 243 +++++++++---- .../musicoo/util/ExoPlayerDiskCacheMaxSize.kt | 22 ++ .../com/player/musicoo/util/RingBuffer.kt | 11 + .../java/com/player/musicoo/util/Utils.kt | 4 + .../res/drawable/rounded_progress_bar.xml | 16 + .../res/layout/activity_mo_play_details.xml | 70 ++-- app/src/main/res/values/styles.xml | 10 + 12 files changed, 560 insertions(+), 227 deletions(-) create mode 100644 app/src/main/java/com/player/musicoo/util/ExoPlayerDiskCacheMaxSize.kt create mode 100644 app/src/main/java/com/player/musicoo/util/RingBuffer.kt create mode 100644 app/src/main/res/drawable/rounded_progress_bar.xml create mode 100644 app/src/main/res/values/styles.xml diff --git a/app/src/main/java/com/player/musicoo/activity/MoPlayDetailsActivity.kt b/app/src/main/java/com/player/musicoo/activity/MoPlayDetailsActivity.kt index d68bde3..d28577b 100644 --- a/app/src/main/java/com/player/musicoo/activity/MoPlayDetailsActivity.kt +++ b/app/src/main/java/com/player/musicoo/activity/MoPlayDetailsActivity.kt @@ -5,20 +5,19 @@ import android.os.Looper import android.os.Message import android.util.Log import android.view.View -import android.widget.SeekBar -import android.widget.SeekBar.OnSeekBarChangeListener import androidx.annotation.OptIn import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi +import androidx.media3.session.MediaController import com.bumptech.glide.Glide import com.gyf.immersionbar.ktx.immersionBar import com.player.musicoo.R import com.player.musicoo.databinding.ActivityMoPlayDetailsBinding import com.player.musicoo.innertube.Innertube import com.player.musicoo.innertube.models.bodies.PlayerBody -import com.player.musicoo.innertube.requests.moNextPage import com.player.musicoo.innertube.requests.player import com.player.musicoo.media.MediaControllerManager import com.player.musicoo.media.SongRadio @@ -62,8 +61,11 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { initClick() val videoId = intent.getStringExtra(PLAY_DETAILS_VIDEO_ID) + Log.d(TAG, "MoPlayDetailsActivity main videoId->$videoId") val playlistId = intent.getStringExtra(PLAY_DETAILS_PLAY_LIST_ID) + Log.d(TAG, "MoPlayDetailsActivity main playlistId->$playlistId") val playlistSetVideoId = intent.getStringExtra(PLAY_DETAILS_PLAY_LIST_SET_VIDEO_ID) + Log.d(TAG, "MoPlayDetailsActivity main playlistSetVideoId->$playlistSetVideoId") val params = intent.getStringExtra(PLAY_DETAILS_PLAY_PARAMS) binding.nameTv.text = intent.getStringExtra(PLAY_DETAILS_NAME) @@ -73,7 +75,9 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { finish() return } + //传入进来的ID,就是进入此界面的当前ID currentVideoID = videoId + //根据进来界面的当前ID来获取资源。 initData( videoId, playlistId, @@ -83,27 +87,53 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { } private fun initClick() { + val currentPlayer = MediaControllerManager.getController() + binding.disableClicksLayout.setOnClickListener { } binding.backBtn.setOnClickListener { finish() } - - binding.playImg.setOnClickListener { - Log.d(TAG, "点击了播放按钮") + binding.playLayoutBtn.setOnClickListener { + if (currentPlayer != null) { + if (currentPlayer.isPlaying) { + currentPlayer.pause() + updatePlayState(false) + } else { + currentPlayer.play() + updatePlayState(true) + } + updateProgressState() + } } - - binding.sbProgress.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - + binding.playSkipBackBtn.setOnClickListener { + if (currentPlayer != null) { + currentPlayer.seekToPreviousMediaItem() + updateProgressUi() + updateInfoUi(currentPlayer.currentMediaItem) } - - override fun onStartTrackingTouch(seekBar: SeekBar?) { - + } + binding.playSkipForwardBtn.setOnClickListener { + if (currentPlayer != null) { + currentPlayer.seekToNextMediaItem() + updateProgressUi() + updateInfoUi(currentPlayer.currentMediaItem) } - - override fun onStopTrackingTouch(seekBar: SeekBar?) { - + } + binding.listLayoutBtn.setOnClickListener { + Log.d(TAG, "currentPlayer?.mediaItemCount->${currentPlayer?.mediaItemCount}") + } + binding.progressBar.progress = 0 + binding.sbProgress.value = 0f + binding.sbProgress.addOnChangeListener { slider, value, fromUser -> + if (fromUser) { + if (currentPlayer != null) { + currentPlayer.seekTo(value.toLong()) + val ss = currentPlayer.isPlaying + if (!ss) { + currentPlayer.play() + } + } } - }) + } } private fun initData( @@ -119,9 +149,9 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { parameters ).let { launch(Dispatchers.Main) { - val ss = it.process() + val ss = it.process()//获取到的资源集合 - if (ss.isEmpty()) { + if (ss.isEmpty()) {//集合为空则展示错误提示 binding.loadingView.visibility = View.GONE binding.playbackErrorLayout.visibility = View.VISIBLE binding.disableClicksLayout.visibility = View.VISIBLE @@ -133,102 +163,139 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { var mediaItem: MediaItem? = null Log.d(TAG, "size = ${ss.size}") - + Log.d(TAG, "MoPlayDetailsActivity initData currentVideoID->$currentVideoID") for (song: Innertube.SongItem in ss) { - if (song.key == currentVideoID) { + if (song.key == currentVideoID) {//判断当前ID得到一个mediaItem mediaItem = song.asMediaItem break } } if (mediaItem != null) { - binding.loadingView.visibility = View.GONE - Glide.with(this@MoPlayDetailsActivity) - .load(mediaItem.mediaMetadata.artworkUri) - .into(binding.thumbnail) + updateInfoUi(mediaItem) - binding.nameTv.text = mediaItem.mediaMetadata.title - binding.descTv.text = mediaItem.mediaMetadata.artist + binding.playbackErrorLayout.visibility = View.GONE + binding.disableClicksLayout.visibility = View.GONE - val urlResult = runBlocking(Dispatchers.IO) { - Innertube.player(PlayerBody(videoId = videoId)) - }?.mapCatching { body -> - if (body.videoDetails?.videoId != videoId) { - throw VideoIdMismatchException() - } - when (val status = body.playabilityStatus?.status) { - "OK" -> body.streamingData?.highestQualityFormat?.let { format -> - format.url - } ?: throw PlayableFormatNotFoundException() + binding.totalDurationTv.visibility = View.GONE - "UNPLAYABLE" -> throw UnplayableException() - "LOGIN_REQUIRED" -> throw LoginRequiredException() - else -> throw PlaybackException( - status, - null, - PlaybackException.ERROR_CODE_REMOTE_ERROR + + val newMediaItem = + MediaItem.Builder() + .setMediaId(videoId) + .setUri(videoId) + .setCustomCacheKey(videoId) + .setMediaMetadata( + mediaItem.mediaMetadata ) - } - } - val url = urlResult?.getOrNull() - if (url != null) { - binding.playbackErrorLayout.visibility = View.GONE - binding.disableClicksLayout.visibility = View.GONE - - binding.totalDurationTv.visibility = View.GONE - - val newMediaItem = - MediaItem.Builder() - .setUri(url) - .setMediaMetadata(mediaItem.mediaMetadata) - .build() - - MediaControllerManager.getController()?.let { - it.addListener(object : Player.Listener { - override fun onPlayerError(error: PlaybackException) { - Log.d(TAG, "onPlayerError = $error") - } - - override fun onPlayWhenReadyChanged( - playWhenReady: Boolean, - reason: Int - ) { - Log.d(TAG, "onPlayWhenReadyChanged = $playWhenReady") - updateProgressState() - } - - override fun onPlaybackStateChanged(playbackState: Int) { - Log.d(TAG, "onPlaybackStateChanged = $playbackState") - if (playbackState == Player.STATE_READY) { - binding.totalDurationTv.visibility = View.VISIBLE - binding.totalDurationTv.text = - convertMillisToMinutesAndSecondsString( - MediaControllerManager.getDuration() - ) - binding.sbProgress.max = - MediaControllerManager.getDuration().toInt() - } - updateProgressState() - } - }) - it.setMediaItem(newMediaItem) - it.repeatMode = Player.REPEAT_MODE_ALL - it.prepare() - it.play() - } - } else { - binding.playbackErrorLayout.visibility = View.VISIBLE - binding.disableClicksLayout.visibility = View.VISIBLE + .build() + + val meController = MediaControllerManager.getController() + meController?.let { + it.addListener(playerListener) + it.setMediaItem(newMediaItem, true) + it.prepare() + it.play() + + it.addMediaItems(ss.map(Innertube.SongItem::asMediaItem).drop(1)) } + } else { + binding.playbackErrorLayout.visibility = View.VISIBLE + binding.disableClicksLayout.visibility = View.VISIBLE } } } } + private val playerListener = object : Player.Listener { + override fun onPositionDiscontinuity( + oldPosition: Player.PositionInfo, + newPosition: Player.PositionInfo, + reason: Int + ) { + if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) { + val meController = MediaControllerManager.getController() + updateInfoUi(meController?.currentMediaItem) + } + } + + override fun onPlayerError(error: PlaybackException) { + Log.d(TAG, "onPlayerError error= $error") + binding.playbackErrorLayout.visibility = View.VISIBLE//展示错误提示 + } + + override fun onPlayWhenReadyChanged( + playWhenReady: Boolean, + reason: Int + ) { + Log.d(TAG, "onPlayWhenReadyChanged = $playWhenReady") + updatePlayState(playWhenReady) + updateProgressState() + } + + override fun onPlaybackStateChanged(playbackState: Int) { + Log.d(TAG, "onPlaybackStateChanged = $playbackState") + + when (playbackState) { + Player.STATE_BUFFERING -> { + binding.loadingView.visibility = View.VISIBLE + } + + Player.STATE_READY -> { + binding.playbackErrorLayout.visibility = View.GONE + binding.loadingView.visibility = View.GONE + binding.totalDurationTv.visibility = View.VISIBLE + binding.totalDurationTv.text = + convertMillisToMinutesAndSecondsString( + MediaControllerManager.getDuration() + ) + binding.sbProgress.valueTo = + MediaControllerManager.getDuration().toFloat() + binding.progressBar.max = + MediaControllerManager.getDuration().toInt() + updateProgressBufferingState() + } + + else -> { + binding.loadingView.visibility = View.GONE + } + } + updateProgressState() + } + } + + override fun onDestroy() { + super.onDestroy() + MediaControllerManager.getController()?.removeListener(playerListener) + } + + private fun updateProgressUi() { + binding.sbProgress.value = 0f + binding.progressBar.progress = 0 + binding.progressDurationTv.text = convertMillisToMinutesAndSecondsString(0L) + } + + private fun updateInfoUi(mediaItem: MediaItem?) { + if (mediaItem == null) { + binding.playbackErrorLayout.visibility = View.VISIBLE + return + } + Glide.with(this@MoPlayDetailsActivity) + .load(mediaItem.mediaMetadata.artworkUri) + .into(binding.thumbnail) + + binding.nameTv.text = mediaItem.mediaMetadata.title + binding.descTv.text = mediaItem.mediaMetadata.artist + } + + /** + * 更新播放进度 + */ private fun updateProgressState() { val currentPlayer = MediaControllerManager.getController() + //判断是否ready与播放中,否则停止更新进度 if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) { -// updatePlayState(currentPlayer.isPlaying) + updatePlayState(currentPlayer.isPlaying) progressHandler.removeCallbacksAndMessages(null) progressHandler.sendEmptyMessage(1) } else { @@ -236,18 +303,52 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { } } + /** + * 播放进度 + */ private val progressHandler = object : Handler(Looper.myLooper()!!) { override fun handleMessage(msg: Message) { val currentPlayer = MediaControllerManager.getController() + //判断是否ready与播放中,否则停止更新进度 if (currentPlayer != null && currentPlayer.playbackState == Player.STATE_READY && currentPlayer.isPlaying) { val currentPosition = currentPlayer.currentPosition val currentString = convertMillisToMinutesAndSecondsString(currentPosition) binding.progressDurationTv.text = currentString - // 更新 SeekBar 的进度 - binding.sbProgress.progress = currentPosition.toInt() + val currentBufferedPosition = currentPlayer.bufferedPosition + binding.progressBar.progress = currentBufferedPosition.toInt() - sendEmptyMessageDelayed(1, 1000) + // 更新 SeekBar 的进度 + binding.sbProgress.value = currentPosition.toFloat() + + sendEmptyMessageDelayed(1, 50) + } + } + } + + /** + * 更新缓冲进度 + */ + private fun updateProgressBufferingState() { + val currentPlayer = MediaControllerManager.getController() + if (currentPlayer != null && currentPlayer.isLoading) { + progressBufferingHandler.removeCallbacksAndMessages(null) + progressBufferingHandler.sendEmptyMessage(1) + } else { + progressBufferingHandler.removeCallbacksAndMessages(null) + } + } + + /** + * 缓冲进度 + */ + private val progressBufferingHandler = object : Handler(Looper.myLooper()!!) { + override fun handleMessage(msg: Message) { + val currentPlayer = MediaControllerManager.getController() + if (currentPlayer != null && currentPlayer.isLoading) { + val currentBufferedPosition = currentPlayer.bufferedPosition + binding.progressBar.progress = currentBufferedPosition.toInt() + sendEmptyMessageDelayed(1, 50) } } } @@ -259,4 +360,44 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { binding.playImg.setImageResource(R.drawable.play_green_icon) } } + + private fun getSourceUrl(videoId: String): String { + return runBlocking(Dispatchers.IO) { + Innertube.player(PlayerBody(videoId = videoId)) + }?.mapCatching { body -> + if (body.videoDetails?.videoId != videoId) { + throw VideoIdMismatchException() + } + when (val status = body.playabilityStatus?.status) { + "OK" -> body.streamingData?.highestQualityFormat?.let { format -> + format.url + } ?: throw PlayableFormatNotFoundException() + + "UNPLAYABLE" -> throw UnplayableException() + "LOGIN_REQUIRED" -> throw LoginRequiredException() + else -> throw PlaybackException( + status, + null, + PlaybackException.ERROR_CODE_REMOTE_ERROR + ) + } + }?.getOrNull() ?: "" + } + + private fun retryPlayback( + player: MediaController, + mediaId: String, + url: String, + mediaMetadata: MediaMetadata + ) { + val mediaItem = + MediaItem.Builder() + .setMediaId(mediaId) + .setUri(url) + .setMediaMetadata(mediaMetadata) + .build() + player.setMediaItem(mediaItem) + player.prepare() + player.play() + } } \ No newline at end of file diff --git a/app/src/main/java/com/player/musicoo/fragment/MoHomeFragment.kt b/app/src/main/java/com/player/musicoo/fragment/MoHomeFragment.kt index 41166b5..3225707 100644 --- a/app/src/main/java/com/player/musicoo/fragment/MoHomeFragment.kt +++ b/app/src/main/java/com/player/musicoo/fragment/MoHomeFragment.kt @@ -30,29 +30,33 @@ class MoHomeFragment : MoBaseFragment() { private suspend fun initView() { Innertube.homePage()?.onSuccess { - for (home: Innertube.HomePage in it.homePage) { - for (content: MusicCarouselShelfRenderer.Content in home.contents) { - if (content.musicResponsiveListItemRenderer != null) { - binding.contentLayout.addView( - MusicResponsiveListView( - requireActivity(), - home + if (it.homePage.isNotEmpty()) { + for (home: Innertube.HomePage in it.homePage) { + for (content: MusicCarouselShelfRenderer.Content in home.contents) { + if (content.musicResponsiveListItemRenderer != null) { + binding.contentLayout.addView( + MusicResponsiveListView( + requireActivity(), + home + ) ) - ) - break - } - if (content.musicTwoRowItemRenderer != null) { - binding.contentLayout.addView( - MusicTowRowListView( - requireActivity(), - home + break + } + if (content.musicTwoRowItemRenderer != null) { + binding.contentLayout.addView( + MusicTowRowListView( + requireActivity(), + home + ) ) - ) - break + break + } } } + initHomeDataMore(it) + } else { + Log.d(TAG, "homePage size 0") } - initHomeDataMore(it) }?.onFailure { Log.d(TAG, "homePage onFailure->${it}") } } diff --git a/app/src/main/java/com/player/musicoo/innertube/models/Context.kt b/app/src/main/java/com/player/musicoo/innertube/models/Context.kt index 3554cc8..8de5d06 100644 --- a/app/src/main/java/com/player/musicoo/innertube/models/Context.kt +++ b/app/src/main/java/com/player/musicoo/innertube/models/Context.kt @@ -29,7 +29,7 @@ data class Context( val DefaultWeb = Context( client = Client( clientName = "WEB_REMIX", - clientVersion = "1.20220918", + clientVersion = "1.20240506.01.00", platform = "DESKTOP", ) ) diff --git a/app/src/main/java/com/player/musicoo/innertube/models/PlayerResponse.kt b/app/src/main/java/com/player/musicoo/innertube/models/PlayerResponse.kt index 599937e..336e391 100644 --- a/app/src/main/java/com/player/musicoo/innertube/models/PlayerResponse.kt +++ b/app/src/main/java/com/player/musicoo/innertube/models/PlayerResponse.kt @@ -30,7 +30,7 @@ data class PlayerResponse( @Serializable data class StreamingData( - val adaptiveFormats: List? + val adaptiveFormats: List?, ) { val highestQualityFormat: AdaptiveFormat? get() = adaptiveFormats?.findLast { it.itag == 251 || it.itag == 140 } diff --git a/app/src/main/java/com/player/musicoo/media/MediaControllerManager.kt b/app/src/main/java/com/player/musicoo/media/MediaControllerManager.kt index 4f105bd..85bcad9 100644 --- a/app/src/main/java/com/player/musicoo/media/MediaControllerManager.kt +++ b/app/src/main/java/com/player/musicoo/media/MediaControllerManager.kt @@ -3,6 +3,7 @@ package com.player.musicoo.media import android.content.ComponentName import android.content.Context import android.net.Uri +import android.os.Binder import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata import androidx.media3.common.Player @@ -43,28 +44,6 @@ object MediaControllerManager { } } - fun setupMedia(id: String, listener: Player.Listener) { - val mediaItem = - MediaItem.Builder() - .setUri(id) - .setMediaMetadata( - MediaMetadata.Builder() - .setArtist("测试") - .setTitle("测试") - .build() - ) - .build() - if (isConnected()) { - mediaController?.let { - it.addListener(listener) - it.setMediaItem(mediaItem) - it.repeatMode = Player.REPEAT_MODE_ALL - it.prepare() - it.play() - } - } - } - fun setupMedia(context: Context, audio: Audio, listener: Player.Listener) { if (currentAudioFile != audio.file) { currentAudioFile = audio.file @@ -220,4 +199,11 @@ object MediaControllerManager { } return 0 } + + fun getTotalBufferedDuration():Long{ + mediaController?.let { + return it.totalBufferedDuration + } + return 0 + } } diff --git a/app/src/main/java/com/player/musicoo/service/PlaybackService.kt b/app/src/main/java/com/player/musicoo/service/PlaybackService.kt index 2f500ff..183d750 100644 --- a/app/src/main/java/com/player/musicoo/service/PlaybackService.kt +++ b/app/src/main/java/com/player/musicoo/service/PlaybackService.kt @@ -1,54 +1,110 @@ package com.player.musicoo.service import android.content.Intent -import android.os.Bundle +import android.net.Uri +import android.os.Handler import android.util.Log +import androidx.core.net.toUri +import androidx.core.text.isDigitsOnly import androidx.media3.common.AudioAttributes +import androidx.media3.common.C +import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi +import androidx.media3.database.StandaloneDatabaseProvider +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.DefaultHttpDataSource +import androidx.media3.datasource.ResolvingDataSource +import androidx.media3.datasource.cache.CacheDataSource +import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor +import androidx.media3.datasource.cache.NoOpCacheEvictor +import androidx.media3.datasource.cache.SimpleCache import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.RenderersFactory +import androidx.media3.exoplayer.audio.AudioRendererEventListener +import androidx.media3.exoplayer.audio.DefaultAudioSink +import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer +import androidx.media3.exoplayer.audio.SilenceSkippingAudioProcessor +import androidx.media3.exoplayer.audio.SonicAudioProcessor +import androidx.media3.exoplayer.mediacodec.MediaCodecSelector import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.extractor.ExtractorsFactory +import androidx.media3.extractor.mkv.MatroskaExtractor +import androidx.media3.extractor.mp4.FragmentedMp4Extractor +import androidx.media3.session.DefaultMediaNotificationProvider import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService -import androidx.media3.session.SessionCommand -import androidx.media3.session.SessionResult -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture import com.player.musicoo.R +import com.player.musicoo.innertube.Innertube +import com.player.musicoo.innertube.models.bodies.PlayerBody +import com.player.musicoo.innertube.requests.player +import com.player.musicoo.util.ExoPlayerDiskCacheMaxSize import com.player.musicoo.util.LogTag +import com.player.musicoo.util.RingBuffer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking @UnstableApi -class PlaybackService : MediaSessionService() { +class PlaybackService : MediaSessionService(), Player.Listener { private val TAG = LogTag.VO_SERVICE_LOG private var mediaSession: MediaSession? = null - private val notificationCustomButtons = - NotificationCustomButton.entries.map { command -> command.commandButton } + private lateinit var cache: SimpleCache + private lateinit var player: ExoPlayer override fun onCreate() { super.onCreate() - val player = ExoPlayer.Builder(this, createMediaSourceFactory()) - .setAudioAttributes(AudioAttributes.DEFAULT, true) - .setHandleAudioBecomingNoisy(true) - .build() - - mediaSession = MediaSession.Builder(this, player) -// .setCallback(MyCallback()) -// .setCustomLayout(notificationCustomButtons) - .build() - - val customMediaNotificationProvider = CustomMediaNotificationProvider(this).apply { - setSmallIcon(R.mipmap.musicoo_logo_img) + val cacheEvictor = when (val size = ExoPlayerDiskCacheMaxSize.`2GB`){ + ExoPlayerDiskCacheMaxSize.Unlimited -> NoOpCacheEvictor() + else -> LeastRecentlyUsedCacheEvictor(size.bytes) } -// setMediaNotificationProvider(customMediaNotificationProvider) - } + // TODO: Remove in a future release + val directory = cacheDir.resolve("exoplayer").also { directory -> + if (directory.exists()) return@also - private fun createMediaSourceFactory(): MediaSource.Factory { - Log.d(TAG, "createMediaSourceFactory") - return DefaultMediaSourceFactory(this) + directory.mkdir() + + cacheDir.listFiles()?.forEach { file -> + if (file.isDirectory && file.name.length == 1 && file.name.isDigitsOnly() || file.extension == "uid") { + if (!file.renameTo(directory.resolve(file.name))) { + file.deleteRecursively() + } + } + } + + filesDir.resolve("coil").deleteRecursively() + } + cache = SimpleCache(directory, cacheEvictor, StandaloneDatabaseProvider(this)) + + + player = ExoPlayer.Builder(this, createRendersFactory(), createMediaSourceFactory()) + .setHandleAudioBecomingNoisy(true) + .setWakeMode(C.WAKE_MODE_LOCAL) + .setAudioAttributes( + AudioAttributes.Builder() + .setUsage(C.USAGE_MEDIA) + .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) + .build(), + true + ) + .setUsePlatformDiagnostics(false) + .build() + player.repeatMode = Player.REPEAT_MODE_ALL + player.addListener(this) + + mediaSession = MediaSession.Builder(this, player) + .build() + + +// val customMediaNotificationProvider = CustomMediaNotificationProvider(this).apply { +// setSmallIcon(R.mipmap.musicoo_logo_img) +// } + setMediaNotificationProvider(DefaultMediaNotificationProvider(this).apply { + setSmallIcon(R.mipmap.musicoo_logo_img) + }) } // The user dismissed the app from the recent tasks @@ -74,51 +130,108 @@ class PlaybackService : MediaSessionService() { release() mediaSession = null } + cache.release() super.onDestroy() } - private inner class MyCallback : MediaSession.Callback { - override fun onConnect( - session: MediaSession, - controller: MediaSession.ControllerInfo - ): MediaSession.ConnectionResult { - val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() - if (session.isMediaNotificationController(controller)) { - val playerCommands = - MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() - .removeAll() - val connectionResult = super.onConnect(session, controller) - val defaultPlayerCommands = connectionResult.availablePlayerCommands - Log.d(TAG, defaultPlayerCommands.toString()) - notificationCustomButtons.forEach { commandButton -> - commandButton.sessionCommand?.let(sessionCommands::add) - } - - return MediaSession.ConnectionResult.accept( - sessionCommands.build(), - playerCommands.build() - ) - } else if (session.isAutoCompanionController(controller)) { - return MediaSession.ConnectionResult.AcceptedResultBuilder(session) - .setAvailableSessionCommands(sessionCommands.build()) - .build() - } - return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build() - } - - override fun onCustomCommand( - session: MediaSession, - controller: MediaSession.ControllerInfo, - customCommand: SessionCommand, - args: Bundle - ): ListenableFuture { - when (customCommand.customAction) { - NotificationCustomButton.SKIP_BACK.customAction -> mediaSession?.player?.seekToPrevious() - NotificationCustomButton.SKIP_FORWARD.customAction -> mediaSession?.player?.seekToNext() - } - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + private fun createCacheDataSource(): DataSource.Factory { + return CacheDataSource.Factory().setCache(cache).apply { + setUpstreamDataSourceFactory( + DefaultHttpDataSource.Factory() + .setConnectTimeoutMs(16000) + .setReadTimeoutMs(8000) + .setUserAgent("Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0") + ) } } + private fun createDataSourceFactory(): DataSource.Factory { + val chunkLength = 512 * 1024L + val ringBuffer = RingBuffer?>(2) { null } + + return ResolvingDataSource.Factory(createCacheDataSource()) { dataSpec -> + val videoId = dataSpec.key ?: error("A key must be set") + Log.d(TAG,"111111 videoId") + if (cache.isCached(videoId, dataSpec.position, chunkLength)) { + dataSpec + } else { + when (videoId) { + ringBuffer.getOrNull(0)?.first -> dataSpec.withUri(ringBuffer.getOrNull(0)!!.second) + ringBuffer.getOrNull(1)?.first -> dataSpec.withUri(ringBuffer.getOrNull(1)!!.second) + else -> { + val urlResult = runBlocking(Dispatchers.IO) { + Innertube.player(PlayerBody(videoId = videoId)) + }?.mapCatching { body -> + if (body.videoDetails?.videoId != videoId) { + throw VideoIdMismatchException() + } + + when (val status = body.playabilityStatus?.status) { + "OK" -> body.streamingData?.highestQualityFormat?.let { format -> + format.url + } ?: throw PlayableFormatNotFoundException() + + "UNPLAYABLE" -> throw UnplayableException() + "LOGIN_REQUIRED" -> throw LoginRequiredException() + else -> throw PlaybackException( + status, + null, + PlaybackException.ERROR_CODE_REMOTE_ERROR + ) + } + } + Log.d(TAG,"1111 urlResult->$urlResult") + + urlResult?.getOrThrow()?.let { url -> + ringBuffer.append(videoId to url.toUri()) + dataSpec.withUri(url.toUri()) + .subrange(dataSpec.uriPositionOffset, chunkLength) + } ?: throw PlaybackException( + null, + urlResult?.exceptionOrNull(), + PlaybackException.ERROR_CODE_REMOTE_ERROR + ) + } + } + } + } + } + + private fun createMediaSourceFactory(): MediaSource.Factory { + return DefaultMediaSourceFactory(createDataSourceFactory(), createExtractorsFactory()) + } + + private fun createExtractorsFactory(): ExtractorsFactory { + return ExtractorsFactory { + arrayOf(MatroskaExtractor(), FragmentedMp4Extractor()) + } + } + + private fun createRendersFactory(): RenderersFactory { + val audioSink = DefaultAudioSink.Builder() + .setEnableFloatOutput(false) + .setEnableAudioTrackPlaybackParams(false) +// .setOffloadMode(DefaultAudioSink.OFFLOAD_MODE_DISABLED) + .setAudioProcessorChain( + DefaultAudioSink.DefaultAudioProcessorChain( + emptyArray(), + SilenceSkippingAudioProcessor(2_000_000, 20_000, 256), + SonicAudioProcessor() + ) + ) + .build() + + return RenderersFactory { handler: Handler?, _, audioListener: AudioRendererEventListener?, _, _ -> + arrayOf( + MediaCodecAudioRenderer( + this, + MediaCodecSelector.DEFAULT, + handler, + audioListener, + audioSink + ) + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/player/musicoo/util/ExoPlayerDiskCacheMaxSize.kt b/app/src/main/java/com/player/musicoo/util/ExoPlayerDiskCacheMaxSize.kt new file mode 100644 index 0000000..40e4546 --- /dev/null +++ b/app/src/main/java/com/player/musicoo/util/ExoPlayerDiskCacheMaxSize.kt @@ -0,0 +1,22 @@ +package com.player.musicoo.util + +enum class ExoPlayerDiskCacheMaxSize { + `32MB`, + `512MB`, + `1GB`, + `2GB`, + `4GB`, + `8GB`, + Unlimited; + + val bytes: Long + get() = when (this) { + `32MB` -> 32 + `512MB` -> 512 + `1GB` -> 1024 + `2GB` -> 2048 + `4GB` -> 4096 + `8GB` -> 8192 + Unlimited -> 0 + } * 1000 * 1000L +} diff --git a/app/src/main/java/com/player/musicoo/util/RingBuffer.kt b/app/src/main/java/com/player/musicoo/util/RingBuffer.kt new file mode 100644 index 0000000..63cea4d --- /dev/null +++ b/app/src/main/java/com/player/musicoo/util/RingBuffer.kt @@ -0,0 +1,11 @@ +package com.player.musicoo.util + +class RingBuffer(val size: Int, init: (index: Int) -> T) { + private val list = MutableList(size, init) + + private var index = 0 + + fun getOrNull(index: Int): T? = list.getOrNull(index) + + fun append(element: T) = list.set(index++ % size, element) +} diff --git a/app/src/main/java/com/player/musicoo/util/Utils.kt b/app/src/main/java/com/player/musicoo/util/Utils.kt index 903b4f6..f5041b7 100644 --- a/app/src/main/java/com/player/musicoo/util/Utils.kt +++ b/app/src/main/java/com/player/musicoo/util/Utils.kt @@ -7,14 +7,18 @@ import androidx.core.net.toUri import androidx.core.os.bundleOf import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata +import androidx.media3.common.util.UnstableApi import com.player.musicoo.innertube.Innertube import com.player.musicoo.innertube.models.bodies.ContinuationBody import com.player.musicoo.innertube.requests.playlistPage import com.player.musicoo.innertube.utils.plus val Innertube.SongItem.asMediaItem: MediaItem + @UnstableApi get() = MediaItem.Builder() .setMediaId(key) + .setUri(key) + .setCustomCacheKey(key) .setMediaMetadata( MediaMetadata.Builder() .setTitle(info?.name) diff --git a/app/src/main/res/drawable/rounded_progress_bar.xml b/app/src/main/res/drawable/rounded_progress_bar.xml new file mode 100644 index 0000000..a2a1b0e --- /dev/null +++ b/app/src/main/res/drawable/rounded_progress_bar.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_mo_play_details.xml b/app/src/main/res/layout/activity_mo_play_details.xml index 0014331..64a2a95 100644 --- a/app/src/main/res/layout/activity_mo_play_details.xml +++ b/app/src/main/res/layout/activity_mo_play_details.xml @@ -107,19 +107,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - + + @@ -192,14 +193,35 @@ android:layout_weight="1" android:orientation="vertical"> - + android:layout_height="wrap_content"> + + + + + + + android:textSize="12dp" + android:visibility="gone" /> @@ -258,6 +280,7 @@ + + + \ No newline at end of file