This commit is contained in:
ocean 2024-05-09 18:55:15 +08:00
parent 2a0d2abfea
commit 619a588559
12 changed files with 560 additions and 227 deletions

View File

@ -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()
}
}

View File

@ -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}") }
}

View File

@ -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",
)
)

View File

@ -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 }

View File

@ -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
}
}

View File

@ -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
)
)
}
}
}

View File

@ -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
}

View 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)
}

View File

@ -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)

View 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>

View File

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

View 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>