update
This commit is contained in:
parent
2a0d2abfea
commit
619a588559
@ -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()
|
||||
}
|
||||
}
|
||||
@ -30,29 +30,33 @@ class MoHomeFragment : MoBaseFragment<FragmentMoHomeBinding>() {
|
||||
|
||||
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}") }
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
)
|
||||
)
|
||||
|
||||
@ -30,7 +30,7 @@ data class PlayerResponse(
|
||||
|
||||
@Serializable
|
||||
data class StreamingData(
|
||||
val adaptiveFormats: List<AdaptiveFormat>?
|
||||
val adaptiveFormats: List<AdaptiveFormat>?,
|
||||
) {
|
||||
val highestQualityFormat: AdaptiveFormat?
|
||||
get() = adaptiveFormats?.findLast { it.itag == 251 || it.itag == 140 }
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<SessionResult> {
|
||||
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<Pair<String, Uri>?>(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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
11
app/src/main/java/com/player/musicoo/util/RingBuffer.kt
Normal file
11
app/src/main/java/com/player/musicoo/util/RingBuffer.kt
Normal file
@ -0,0 +1,11 @@
|
||||
package com.player.musicoo.util
|
||||
|
||||
class RingBuffer<T>(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)
|
||||
}
|
||||
@ -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)
|
||||
|
||||
16
app/src/main/res/drawable/rounded_progress_bar.xml
Normal file
16
app/src/main/res/drawable/rounded_progress_bar.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- 背景 -->
|
||||
<item android:id="@android:id/background">
|
||||
<shape>
|
||||
<corners android:radius="20dp" /> <!-- 设置圆角半径 -->
|
||||
</shape>
|
||||
</item>
|
||||
<!-- 进度 -->
|
||||
<item android:id="@android:id/progress">
|
||||
<clip>
|
||||
<shape>
|
||||
<corners android:radius="20dp" /> <!-- 设置圆角半径 -->
|
||||
</shape>
|
||||
</clip>
|
||||
</item>
|
||||
</layer-list>
|
||||
@ -107,19 +107,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_error_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_margin="24dp"
|
||||
android:fontFamily="@font/medium_font"
|
||||
android:gravity="center"
|
||||
android:text="@string/playback_error"
|
||||
android:textColor="@color/white_80"
|
||||
android:textSize="16dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/thumbnail"
|
||||
android:layout_width="match_parent"
|
||||
@ -132,6 +119,20 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/playback_error_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerInParent="true"
|
||||
android:background="@color/black_60"
|
||||
android:padding="24dp"
|
||||
android:fontFamily="@font/medium_font"
|
||||
android:gravity="center"
|
||||
android:text="@string/playback_error"
|
||||
android:textColor="@color/white_80"
|
||||
android:textSize="16dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@ -192,14 +193,35 @@
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/sbProgress"
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="32dp"
|
||||
android:maxHeight="4dp"
|
||||
android:minHeight="4dp"
|
||||
android:progressDrawable="@drawable/bg_playing_playback_progress"
|
||||
android:thumb="@drawable/ic_playing_playback_progress_thumb" />
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="@style/RoundedProgressBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:max="100"
|
||||
android:progress="50" />
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/sbProgress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:value="10"
|
||||
android:valueTo="100"
|
||||
app:labelBehavior="gone"
|
||||
app:thumbColor="@color/white"
|
||||
app:thumbRadius="6dp"
|
||||
app:trackColorActive="@color/white"
|
||||
app:trackColorInactive="#00000000"
|
||||
app:trackHeight="3dp" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@ -226,11 +248,11 @@
|
||||
android:id="@+id/total_duration_tv"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:fontFamily="@font/medium_font"
|
||||
android:text="00:00"
|
||||
android:textColor="#D9FFFFFF"
|
||||
android:textSize="12dp" />
|
||||
android:textSize="12dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
@ -258,6 +280,7 @@
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/playSkipBackBtn"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
@ -270,6 +293,7 @@
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/playLayoutBtn"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="66dp"
|
||||
android:layout_weight="1"
|
||||
@ -283,6 +307,7 @@
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/playSkipForwardBtn"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
@ -295,6 +320,7 @@
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/listLayoutBtn"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
|
||||
10
app/src/main/res/values/styles.xml
Normal file
10
app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="RoundedProgressBar" parent="Widget.AppCompat.ProgressBar.Horizontal">
|
||||
<item name="android:progressDrawable">@drawable/rounded_progress_bar</item>
|
||||
<item name="android:progressTint">#4DFFFFFF</item>
|
||||
<item name="android:progressBackgroundTint">#33FFFFFF</item>
|
||||
<item name="android:minHeight">4dp</item> <!-- 设置进度条高度 -->
|
||||
<item name="android:maxHeight">4dp</item> <!-- 设置进度条高度 -->
|
||||
</style>
|
||||
</resources>
|
||||
Loading…
Reference in New Issue
Block a user