diff --git a/app/src/main/java/com/player/musicoo/App.kt b/app/src/main/java/com/player/musicoo/App.kt index e3b506f..1c4bdc6 100644 --- a/app/src/main/java/com/player/musicoo/App.kt +++ b/app/src/main/java/com/player/musicoo/App.kt @@ -11,6 +11,7 @@ import com.player.musicoo.database.CurrentAudioDatabase import com.player.musicoo.database.CurrentAudioManager import com.player.musicoo.database.DatabaseManager import com.player.musicoo.media.MediaControllerManager +import com.player.musicoo.util.CacheManager import com.player.musicoo.util.parseResources import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -105,6 +106,7 @@ class App : Application() { databaseManager = DatabaseManager.getInstance(this) initCurrentPlayingAudio() initImportAudio() + CacheManager.initializeCaches(this) } } \ No newline at end of file diff --git a/app/src/main/java/com/player/musicoo/activity/MoBaseActivity.kt b/app/src/main/java/com/player/musicoo/activity/MoBaseActivity.kt index 22e304b..75fc331 100644 --- a/app/src/main/java/com/player/musicoo/activity/MoBaseActivity.kt +++ b/app/src/main/java/com/player/musicoo/activity/MoBaseActivity.kt @@ -22,11 +22,13 @@ import androidx.appcompat.app.AppCompatActivity import androidx.media3.common.Player import androidx.media3.session.MediaController import com.bumptech.glide.Glide +import com.player.musicoo.App import com.player.musicoo.R import com.player.musicoo.innertube.Innertube import com.player.musicoo.innertube.Innertube.TAG import com.player.musicoo.media.MediaControllerManager import com.player.musicoo.sp.AppStore +import com.player.musicoo.util.DownloadUtil import com.player.musicoo.util.LogTag import com.player.musicoo.view.MusicPlayerView import kotlinx.coroutines.CoroutineScope 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 e3c22d1..d25d11f 100644 --- a/app/src/main/java/com/player/musicoo/activity/MoPlayDetailsActivity.kt +++ b/app/src/main/java/com/player/musicoo/activity/MoPlayDetailsActivity.kt @@ -14,6 +14,8 @@ import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.offline.Download +import androidx.media3.exoplayer.offline.DownloadManager import androidx.media3.exoplayer.offline.DownloadRequest import androidx.media3.exoplayer.offline.DownloadService import androidx.recyclerview.widget.LinearLayoutManager @@ -25,17 +27,12 @@ import com.player.musicoo.R import com.player.musicoo.adapter.PlayListAdapter 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.player import com.player.musicoo.media.MediaControllerManager import com.player.musicoo.media.SongRadio -import com.player.musicoo.service.LoginRequiredException import com.player.musicoo.service.MyDownloadService -import com.player.musicoo.service.PlayableFormatNotFoundException -import com.player.musicoo.service.UnplayableException -import com.player.musicoo.service.VideoIdMismatchException import com.player.musicoo.sp.AppStore -import com.player.musicoo.util.DemoUtil +import com.player.musicoo.util.DownloadUtil +import com.player.musicoo.util.FileSizeConverter import com.player.musicoo.util.LogTag.LogD import com.player.musicoo.util.PlayMode import com.player.musicoo.util.asMediaItem @@ -43,8 +40,8 @@ import com.player.musicoo.util.convertMillisToMinutesAndSecondsString import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.selects.select +import java.lang.Exception @OptIn(UnstableApi::class) class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { @@ -65,6 +62,7 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { private var comeFrom: Class<*>? = null private var playListAdapter: PlayListAdapter? = null + private var downloadManager: DownloadManager? = null private fun initImmersionBar() { immersionBar { @@ -79,6 +77,7 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { initImmersionBar() initClick() initPlayerListener() + initDownloadListener() initPlayListAdapter() updatePlayModeUi() val videoId = intent.getStringExtra(PLAY_DETAILS_VIDEO_ID) @@ -140,12 +139,62 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { updateInfoUi(meController.currentMediaItem) } } + if (meController != null && meController.currentMediaItem != null) { + LogD(TAG,"meController.currentMediaItem != null->${meController.currentMediaItem?.mediaId!!}") + updateDownloadUi(meController.currentMediaItem?.mediaId!!) + } else { + LogD(TAG,"currentVideoID->${currentVideoID}") + updateDownloadUi(currentVideoID) + } } + private fun updateDownloadUi(id: String) { + if (DownloadUtil.downloadResourceExist(id)) { + binding.downloadImg.setImageResource(R.drawable.download_done_icon) + } else { + binding.downloadImg.setImageResource(R.drawable.download_icon) + } + } + + private fun initPlayerListener() { meController?.addListener(playerListener) } + private fun initDownloadListener() { + downloadManager = DownloadUtil.getDownloadManager(this) + if (downloadManager != null) { + downloadManager?.addListener(object : DownloadManager.Listener { + override fun onDownloadChanged( + downloadManager: DownloadManager, + download: Download, + finalException: Exception? + ) { + when (download.state) { + Download.STATE_DOWNLOADING -> { + binding.downloadLoading.visibility = View.VISIBLE + binding.downloadImg.setImageResource(R.drawable.download_icon) + binding.downloadImg.visibility = View.GONE + } + + Download.STATE_COMPLETED -> { + binding.downloadLoading.visibility = View.GONE + binding.downloadImg.setImageResource(R.drawable.download_done_icon) + binding.downloadImg.visibility = View.VISIBLE + } + + else -> { + binding.downloadLoading.visibility = View.GONE + binding.downloadImg.setImageResource(R.drawable.error) + binding.downloadImg.visibility = View.VISIBLE + } + } + + } + }) + } + } + private fun updateCurrentMediaItemInfo() { if (meController != null && meController.currentMediaItem != null) { binding.playbackErrorLayout.visibility = View.GONE @@ -315,46 +364,24 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { binding.downloadBtn.setOnClickListener { if (meController != null && meController.currentMediaItem != null) { val contentId = meController.currentMediaItem?.mediaId!! - - val downloadManager = DemoUtil.getDownloadManager(this) - val downloadIndex = downloadManager.downloadIndex - downloadIndex.getDownloads() - .use { cursor -> - while (cursor.moveToNext()) { - val download = cursor.download - if(download.request.id == contentId){ - return@setOnClickListener - } - } - } - - LogD(TAG, "download get contentUrl") - val urlResult = runBlocking(Dispatchers.IO) { - Innertube.player(PlayerBody(videoId = contentId)) - }?.mapCatching { body -> - if (body.videoDetails?.videoId != contentId) { - 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 + //如果已经存在就不进行下载 + if (DownloadUtil.downloadResourceExist(contentId)) { + return@setOnClickListener + } + val currentDownload = DownloadUtil.currentDownload(contentId) + if (currentDownload != null) { + if (currentDownload.state == Download.STATE_DOWNLOADING) { + DownloadService.sendRemoveDownload( + this, + MyDownloadService::class.java, + contentId, + false ) } - } - - urlResult?.getOrThrow()?.let { url -> - val contentUrl = url.toUri() - LogD(TAG, "download contentUrl->${contentUrl}") - val downloadRequest = DownloadRequest.Builder(contentId, contentUrl) + } else { + val downloadRequest = DownloadRequest + .Builder(contentId, contentId.toUri()) + .setCustomCacheKey(contentId) .build() DownloadService.sendAddDownload( this, @@ -363,8 +390,8 @@ class MoPlayDetailsActivity : MoBaseActivity(), Player.Listener { false ) } - } + } } } diff --git a/app/src/main/java/com/player/musicoo/service/MyDownloadService.kt b/app/src/main/java/com/player/musicoo/service/MyDownloadService.kt index e4d80f5..4fd2df9 100644 --- a/app/src/main/java/com/player/musicoo/service/MyDownloadService.kt +++ b/app/src/main/java/com/player/musicoo/service/MyDownloadService.kt @@ -13,7 +13,7 @@ import androidx.media3.exoplayer.offline.DownloadNotificationHelper import androidx.media3.exoplayer.offline.DownloadService import androidx.media3.exoplayer.scheduler.PlatformScheduler import com.player.musicoo.R -import com.player.musicoo.util.DemoUtil +import com.player.musicoo.util.DownloadUtil @OptIn(UnstableApi::class) @@ -34,11 +34,12 @@ class MyDownloadService : DownloadService( @SuppressLint("ServiceCast") override fun getDownloadManager(): DownloadManager { - val downloadManager = DemoUtil.getDownloadManager(this) - val downloadNotificationHelper = DemoUtil.getDownloadNotificationHelper(this) + val downloadManager = DownloadUtil.getDownloadManager(this) + downloadManager!!.maxParallelDownloads = 3 + val downloadNotificationHelper = DownloadUtil.getDownloadNotificationHelper(this) downloadManager.addListener( TerminalStateNotificationHelper( - this, downloadNotificationHelper, FOREGROUND_NOTIFICATION_ID + 1 + this, downloadNotificationHelper!!, FOREGROUND_NOTIFICATION_ID + 1 ) ) return downloadManager @@ -55,7 +56,7 @@ class MyDownloadService : DownloadService( return DownloadNotificationHelper(this, DOWNLOAD_NOTIFICATION_CHANNEL_ID) .buildProgressNotification( this, - R.mipmap.musicoo_logo_img, + R.drawable.download_icon, null, null, downloads, @@ -76,22 +77,26 @@ class MyDownloadService : DownloadService( download: Download, finalException: Exception? ) { - val notification: Notification = if (download.state == Download.STATE_COMPLETED) { - notificationHelper.buildDownloadCompletedNotification( - context, - R.mipmap.ic_download_done, - null, - Util.fromUtf8Bytes(download.request.data) - ) - } else if (download.state == Download.STATE_FAILED) { - notificationHelper.buildDownloadFailedNotification( - context, - R.mipmap.ic_download_done, - null, - Util.fromUtf8Bytes(download.request.data) - ) - } else { - return + val notification: Notification = when (download.state) { + Download.STATE_COMPLETED -> { + notificationHelper.buildDownloadCompletedNotification( + context, + R.drawable.download_done_icon, + null, + Util.fromUtf8Bytes(download.request.data) + ) + } + Download.STATE_FAILED -> { + notificationHelper.buildDownloadFailedNotification( + context, + R.drawable.error, + null, + Util.fromUtf8Bytes(download.request.data) + ) + } + else -> { + return + } } NotificationUtil.setNotification(context, nextNotificationId++, notification) } 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 7fd7e03..f23adee 100644 --- a/app/src/main/java/com/player/musicoo/service/PlaybackService.kt +++ b/app/src/main/java/com/player/musicoo/service/PlaybackService.kt @@ -3,21 +3,16 @@ package com.player.musicoo.service import android.content.Intent 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 @@ -40,7 +35,8 @@ 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.sp.AppStore -import com.player.musicoo.util.ExoPlayerDiskCacheMaxSize +import com.player.musicoo.util.CacheManager +import com.player.musicoo.util.DownloadUtil import com.player.musicoo.util.LogTag import com.player.musicoo.util.LogTag.LogD import com.player.musicoo.util.PlayMode @@ -53,34 +49,16 @@ import kotlinx.coroutines.runBlocking class PlaybackService : MediaSessionService(), Player.Listener { private val TAG = LogTag.VO_SERVICE_LOG private var mediaSession: MediaSession? = null - private lateinit var cache: SimpleCache + private lateinit var player: ExoPlayer + private val playerCache = CacheManager.getPlayerCache() + private val downloadCache = CacheManager.getDownloadCache() override fun onCreate() { super.onCreate() - - val cacheEvictor = when (val size = ExoPlayerDiskCacheMaxSize.`2GB`) { - ExoPlayerDiskCacheMaxSize.Unlimited -> NoOpCacheEvictor() - else -> LeastRecentlyUsedCacheEvictor(size.bytes) - } - val directory = cacheDir.resolve("exoplayer").also { directory -> - if (directory.exists()) return@also - - 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()) + player = ExoPlayer.Builder(this) + .setMediaSourceFactory(createMediaSourceFactory()) + .setRenderersFactory(createRendersFactory()) .setHandleAudioBecomingNoisy(true) .setWakeMode(C.WAKE_MODE_LOCAL) .setAudioAttributes( @@ -98,10 +76,12 @@ class PlaybackService : MediaSessionService(), Player.Listener { player.repeatMode = Player.REPEAT_MODE_ALL player.shuffleModeEnabled = false } + PlayMode.SINGLE_LOOP.value -> { player.repeatMode = Player.REPEAT_MODE_ONE player.shuffleModeEnabled = false } + PlayMode.RANDOM.value -> { player.repeatMode = Player.REPEAT_MODE_ALL player.shuffleModeEnabled = true @@ -146,21 +126,24 @@ class PlaybackService : MediaSessionService(), Player.Listener { release() mediaSession = null } - cache.release() super.onDestroy() } - - 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 createCacheDataSource(): CacheDataSource.Factory = + CacheDataSource.Factory() + .setCache(downloadCache!!) + .setUpstreamDataSourceFactory( + CacheDataSource.Factory() + .setCache(playerCache!!) + .setUpstreamDataSourceFactory( + DefaultHttpDataSource.Factory() + .setConnectTimeoutMs(16000) + .setReadTimeoutMs(8000) + .setUserAgent("Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0") + ) ) - } - } + .setCacheWriteDataSinkFactory(null) + .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR) private fun createDataSourceFactory(): DataSource.Factory { val chunkLength = 512 * 1024L @@ -168,51 +151,62 @@ class PlaybackService : MediaSessionService(), Player.Listener { return ResolvingDataSource.Factory(createCacheDataSource()) { dataSpec -> val videoId = dataSpec.key ?: error("A key must be set") - 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() - } + val position = dataSpec.position + val length = if (dataSpec.length >= 0) dataSpec.length else 1 - when (val status = body.playabilityStatus?.status) { - "OK" -> body.streamingData?.highestQualityFormat?.let { format -> - format.url - } ?: throw PlayableFormatNotFoundException() + if (downloadCache!!.isCached(videoId, position, length)) { + LogD(TAG, "playbackService downloadCache contains data for $videoId at position $position") + return@Factory dataSpec + } - "UNPLAYABLE" -> throw UnplayableException() - "LOGIN_REQUIRED" -> throw LoginRequiredException() - else -> throw PlaybackException( - status, - null, - PlaybackException.ERROR_CODE_REMOTE_ERROR - ) - } + if (playerCache!!.isCached(videoId, position, chunkLength)) { + LogD(TAG, "playbackService playerCache contains data for $videoId at position $position") + return@Factory dataSpec + } + + when (videoId) { + ringBuffer.getOrNull(0)?.first -> return@Factory dataSpec.withUri(ringBuffer.getOrNull(0)!!.second) + ringBuffer.getOrNull(1)?.first -> return@Factory 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() } - LogD(TAG, "service 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 - ) + 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 + ) + } } + + LogD(TAG, "playbackService urlResult->$urlResult") + + urlResult?.getOrThrow()?.let { url -> + ringBuffer.append(videoId to url.toUri()) + return@Factory 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()) } @@ -227,7 +221,6 @@ class PlaybackService : MediaSessionService(), Player.Listener { val audioSink = DefaultAudioSink.Builder() .setEnableFloatOutput(false) .setEnableAudioTrackPlaybackParams(false) -// .setOffloadMode(DefaultAudioSink.OFFLOAD_MODE_DISABLED) .setAudioProcessorChain( DefaultAudioSink.DefaultAudioProcessorChain( emptyArray(), diff --git a/app/src/main/java/com/player/musicoo/util/CacheManager.kt b/app/src/main/java/com/player/musicoo/util/CacheManager.kt new file mode 100644 index 0000000..f902695 --- /dev/null +++ b/app/src/main/java/com/player/musicoo/util/CacheManager.kt @@ -0,0 +1,50 @@ +package com.player.musicoo.util + +import android.content.Context +import androidx.annotation.OptIn +import androidx.media3.common.util.UnstableApi +import androidx.media3.database.DatabaseProvider +import androidx.media3.database.StandaloneDatabaseProvider +import androidx.media3.datasource.cache.CacheEvictor +import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor +import androidx.media3.datasource.cache.NoOpCacheEvictor +import androidx.media3.datasource.cache.SimpleCache +import java.io.File + +@OptIn(UnstableApi::class) +object CacheManager { + private var playerCache: SimpleCache? = null + private var downloadCache: SimpleCache? = null + private var databaseProvider: DatabaseProvider? = null + private const val DOWNLOAD_CONTENT_DIRECTORY = "DownloadOO" + private const val PLAYER_CONTENT_DIRECTORY = "PlayerOO" + + fun initializeCaches(context: Context) { + databaseProvider = StandaloneDatabaseProvider(context) + + // Initialize player cache + val playerCacheDir = File(context.filesDir, PLAYER_CONTENT_DIRECTORY) + val cacheEvictor: CacheEvictor = when (val size = ExoPlayerDiskCacheMaxSize.`1GB`) { + ExoPlayerDiskCacheMaxSize.Unlimited -> NoOpCacheEvictor() + else -> LeastRecentlyUsedCacheEvictor(size.bytes) + } + + playerCache = SimpleCache( + playerCacheDir, + cacheEvictor, + databaseProvider!! + ) + + // Initialize download cache + val downloadCacheDir = File(context.filesDir, DOWNLOAD_CONTENT_DIRECTORY) + downloadCache = SimpleCache(downloadCacheDir, NoOpCacheEvictor(), databaseProvider!!) + } + + fun getPlayerCache(): SimpleCache? { + return playerCache + } + + fun getDownloadCache(): SimpleCache? { + return downloadCache + } +} diff --git a/app/src/main/java/com/player/musicoo/util/DemoUtil.java b/app/src/main/java/com/player/musicoo/util/DemoUtil.java deleted file mode 100644 index 887dbff..0000000 --- a/app/src/main/java/com/player/musicoo/util/DemoUtil.java +++ /dev/null @@ -1,182 +0,0 @@ -package com.player.musicoo.util; -import android.content.Context; - -import org.chromium.net.CronetEngine; - -import androidx.annotation.Nullable; -import androidx.annotation.OptIn; -import androidx.media3.database.DatabaseProvider; -import androidx.media3.database.StandaloneDatabaseProvider; -import androidx.media3.datasource.DataSource; -import androidx.media3.datasource.DefaultDataSource; -import androidx.media3.datasource.DefaultHttpDataSource; -import androidx.media3.datasource.cache.Cache; -import androidx.media3.datasource.cache.CacheDataSource; -import androidx.media3.datasource.cache.NoOpCacheEvictor; -import androidx.media3.datasource.cache.SimpleCache; -import androidx.media3.datasource.cronet.CronetDataSource; -import androidx.media3.datasource.cronet.CronetUtil; -import androidx.media3.exoplayer.DefaultRenderersFactory; -import androidx.media3.exoplayer.RenderersFactory; -import androidx.media3.exoplayer.offline.DownloadManager; -import androidx.media3.exoplayer.offline.DownloadNotificationHelper; -import java.io.File; -import java.net.CookieHandler; -import java.net.CookieManager; -import java.net.CookiePolicy; -import java.util.concurrent.Executors; - -/** Utility methods for the demo app. */ -public final class DemoUtil { - - public static final String DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel"; - - /** - * Whether the demo application uses Cronet for networking. Note that Cronet does not provide - * automatic support for cookies (https://github.com/google/ExoPlayer/issues/5975). - * - *
If set to false, the platform's default network stack is used with a {@link CookieManager}
- * configured in {@link #getHttpDataSourceFactory}.
- */
- private static final boolean USE_CRONET_FOR_NETWORKING = true;
-
- private static final String TAG = "DemoUtil";
- private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
-
- private static DataSource.Factory dataSourceFactory;
- private static DataSource.Factory httpDataSourceFactory;
- private static DatabaseProvider databaseProvider;
- private static File downloadDirectory;
- private static Cache downloadCache;
- private static DownloadManager downloadManager;
-// private static @MonotonicNonNull DownloadTracker downloadTracker;
- private static DownloadNotificationHelper downloadNotificationHelper;
-
- /** Returns whether extension renderers should be used. */
- public static boolean useExtensionRenderers() {
- return true;
- }
-
- @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
- public static RenderersFactory buildRenderersFactory(
- Context context, boolean preferExtensionRenderer) {
- @DefaultRenderersFactory.ExtensionRendererMode
- int extensionRendererMode =
- useExtensionRenderers()
- ? (preferExtensionRenderer
- ? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
- : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
- : DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
- return new DefaultRenderersFactory(context.getApplicationContext())
- .setExtensionRendererMode(extensionRendererMode);
- }
-
- public static synchronized DataSource.Factory getHttpDataSourceFactory(Context context) {
- if (httpDataSourceFactory == null) {
- if (USE_CRONET_FOR_NETWORKING) {
- context = context.getApplicationContext();
- @Nullable CronetEngine cronetEngine = CronetUtil.buildCronetEngine(context);
- if (cronetEngine != null) {
- httpDataSourceFactory =
- new CronetDataSource.Factory(cronetEngine, Executors.newSingleThreadExecutor());
- }
- }
- if (httpDataSourceFactory == null) {
- // We don't want to use Cronet, or we failed to instantiate a CronetEngine.
- CookieManager cookieManager = new CookieManager();
- cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER);
- CookieHandler.setDefault(cookieManager);
- httpDataSourceFactory = new DefaultHttpDataSource.Factory();
- }
- }
- return httpDataSourceFactory;
- }
-
- /** Returns a {@link DataSource.Factory}. */
- public static synchronized DataSource.Factory getDataSourceFactory(Context context) {
- if (dataSourceFactory == null) {
- context = context.getApplicationContext();
- DefaultDataSource.Factory upstreamFactory =
- new DefaultDataSource.Factory(context, getHttpDataSourceFactory(context));
- dataSourceFactory = buildReadOnlyCacheDataSource(upstreamFactory, getDownloadCache(context));
- }
- return dataSourceFactory;
- }
-
- @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
- public static synchronized DownloadNotificationHelper getDownloadNotificationHelper(
- Context context) {
- if (downloadNotificationHelper == null) {
- downloadNotificationHelper =
- new DownloadNotificationHelper(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID);
- }
- return downloadNotificationHelper;
- }
-
- public static synchronized DownloadManager getDownloadManager(Context context) {
- ensureDownloadManagerInitialized(context);
- return downloadManager;
- }
-
-// public static synchronized DownloadTracker getDownloadTracker(Context context) {
-// ensureDownloadManagerInitialized(context);
-// return downloadTracker;
-// }
-
- @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
- private static synchronized Cache getDownloadCache(Context context) {
- if (downloadCache == null) {
- File downloadContentDirectory =
- new File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY);
- downloadCache =
- new SimpleCache(
- downloadContentDirectory, new NoOpCacheEvictor(), getDatabaseProvider(context));
- }
- return downloadCache;
- }
-
- @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
- private static synchronized void ensureDownloadManagerInitialized(Context context) {
- if (downloadManager == null) {
- downloadManager =
- new DownloadManager(
- context,
- getDatabaseProvider(context),
- getDownloadCache(context),
- getHttpDataSourceFactory(context),
- Executors.newFixedThreadPool(/* nThreads= */ 6));
-// downloadTracker =
-// new DownloadTracker(context, getHttpDataSourceFactory(context), downloadManager);
- }
- }
-
- @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
- private static synchronized DatabaseProvider getDatabaseProvider(Context context) {
- if (databaseProvider == null) {
- databaseProvider = new StandaloneDatabaseProvider(context);
- }
- return databaseProvider;
- }
-
- private static synchronized File getDownloadDirectory(Context context) {
- if (downloadDirectory == null) {
- downloadDirectory = context.getExternalFilesDir(/* type= */ null);
- if (downloadDirectory == null) {
- downloadDirectory = context.getFilesDir();
- }
- }
- return downloadDirectory;
- }
-
- @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
- private static CacheDataSource.Factory buildReadOnlyCacheDataSource(
- DataSource.Factory upstreamFactory, Cache cache) {
- return new CacheDataSource.Factory()
- .setCache(cache)
- .setUpstreamDataSourceFactory(upstreamFactory)
- .setCacheWriteDataSinkFactory(null)
- .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);
- }
-
- private DemoUtil() {}
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/player/musicoo/util/DownloadUtil.kt b/app/src/main/java/com/player/musicoo/util/DownloadUtil.kt
new file mode 100644
index 0000000..983cbab
--- /dev/null
+++ b/app/src/main/java/com/player/musicoo/util/DownloadUtil.kt
@@ -0,0 +1,224 @@
+package com.player.musicoo.util
+
+import android.content.Context
+import android.net.Uri
+import androidx.annotation.OptIn
+import androidx.core.net.toUri
+import androidx.media3.common.PlaybackException
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.database.DatabaseProvider
+import androidx.media3.database.StandaloneDatabaseProvider
+import androidx.media3.datasource.DataSource
+import androidx.media3.datasource.DefaultDataSource
+import androidx.media3.datasource.DefaultHttpDataSource
+import androidx.media3.datasource.ResolvingDataSource
+import androidx.media3.datasource.cache.Cache
+import androidx.media3.datasource.cache.CacheDataSource
+import androidx.media3.datasource.cache.CacheEvictor
+import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
+import androidx.media3.datasource.cache.NoOpCacheEvictor
+import androidx.media3.datasource.cache.SimpleCache
+import androidx.media3.datasource.cronet.CronetDataSource
+import androidx.media3.datasource.cronet.CronetUtil
+import androidx.media3.exoplayer.offline.Download
+import androidx.media3.exoplayer.offline.DownloadManager
+import androidx.media3.exoplayer.offline.DownloadNotificationHelper
+import com.player.musicoo.innertube.Innertube
+import com.player.musicoo.innertube.Innertube.TAG
+import com.player.musicoo.innertube.models.bodies.PlayerBody
+import com.player.musicoo.innertube.requests.player
+import com.player.musicoo.service.LoginRequiredException
+import com.player.musicoo.service.PlayableFormatNotFoundException
+import com.player.musicoo.service.UnplayableException
+import com.player.musicoo.service.VideoIdMismatchException
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import java.net.CookieHandler
+import java.net.CookieManager
+import java.net.CookiePolicy
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+
+
+@OptIn(markerClass = [UnstableApi::class])
+object DownloadUtil {
+ private const val DOWNLOAD_NOTIFICATION_CHANNEL_ID = "download_channel"
+
+ private const val USE_CRONET_FOR_NETWORKING = true
+
+ private var httpDataSourceFactory: DataSource.Factory? = null
+ private var databaseProvider: DatabaseProvider? = null
+ private val playerCache = CacheManager.getPlayerCache()
+ private val downloadCache = CacheManager.getDownloadCache()
+ private var downloadManager: DownloadManager? = null
+
+ private var downloadNotificationHelper: DownloadNotificationHelper? = null
+
+ @Synchronized
+ fun getHttpDataSourceFactory(context: Context): DataSource.Factory? {
+ if (httpDataSourceFactory == null) {
+ if (USE_CRONET_FOR_NETWORKING) {
+ val cronetEngine = CronetUtil.buildCronetEngine(context.applicationContext)
+ if (cronetEngine != null) {
+ httpDataSourceFactory =
+ CronetDataSource.Factory(cronetEngine, Executors.newSingleThreadExecutor())
+ }
+ }
+ if (httpDataSourceFactory == null) {
+ // We don't want to use Cronet, or we failed to instantiate a CronetEngine.
+ val cookieManager = CookieManager()
+ cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER)
+ CookieHandler.setDefault(cookieManager)
+ httpDataSourceFactory = DefaultHttpDataSource.Factory()
+ }
+ }
+ return httpDataSourceFactory
+ }
+
+ /**
+ * Returns a [DataSource.Factory].
+ */
+ @Synchronized
+ fun getDataSourceFactory(context: Context): DataSource.Factory {
+ val upstreamFactory = DefaultDataSource.Factory(
+ context.applicationContext,
+ getHttpDataSourceFactory(context.applicationContext)!!
+ )
+ val chunkLength = 512 * 1024L
+ val ringBuffer = RingBuffer