package com.suno.android.media import android.content.Context import androidx.annotation.OptIn import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.datasource.DefaultDataSource import androidx.media3.datasource.DefaultHttpDataSource import androidx.media3.datasource.FileDataSource import androidx.media3.datasource.cache.CacheDataSink 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.datasource.okhttp.OkHttpDataSource import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.session.MediaLibraryService.MediaLibrarySession import com.suno.android.common_core_utils.SunoLogger import com.suno.android.common_networking.di.Api import com.suno.android.media.medialibrarysession.CorePlaybackMediaLibrarySessionFactory import com.suno.android.media.medialibrarysession.MediaLibrarySessionFactory import com.suno.android.media.medialibrarysession.SharedMediaLibrarySession import com.suno.android.media.player.CorePlaybackPlayerFactory import com.suno.android.media.player.EditorPlayerFactory import com.suno.android.media.player.PlayerFactory import com.suno.android.media.player.SharedPlayer import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import okhttp3.OkHttpClient import java.io.File import java.util.concurrent.TimeUnit import javax.inject.Singleton private const val EXOPLAYER_CACHE_VIDEO_DOWNLOAD_CONTENT_DIRECTORY = "exoplayer_video_downloads" private const val EXOPLAYER_CACHE_VIDEO_HOOKS_ONBOARDING_CONTENT_DIRECTORY = "exoplayer_hooks_onboarding" private const val EXOPLAYER_CACHE_MEDIA_PLAYBACK_DOWNLOAD_CONTENT_DIRECTORY = "exoplayer_media_playback_downloads" // This is small for videos but works as long as we're just supporting 10s video covers private const val EXOPLAYER_CACHE_SIZE_LIMIT_BYTES = 10L * 1024 * 1024 // 10MB /** * (todo: allow users to adjust this from their settings) * At roughly 3-4MB per song, this will have a user caching roughly 25-33 songs... */ private const val EXOPLAYER_MEDIA_PLAYBACK_CACHE_SIZE_LIMIT_BYTES = 100L * 1024 * 1024 // 100MB @Module @InstallIn(SingletonComponent::class) abstract class MediaModule { @Binds abstract fun bindCastManager( defaultCastManager: DefaultCastManager, ): CastManager @Binds internal abstract fun bindMediaAnalyticsManagerImpl( impl: MediaAnalyticsManagerImpl, ): MediaAnalyticsManager @Binds @HooksOnboardingVideoPlayer internal abstract fun bindHooksOnboardingVideoPlayerApiClient( @Api okHttpClient: OkHttpClient, ): OkHttpClient @Binds @Singleton abstract fun bindMediaManager( impl: MediaManagerImpl, ): MediaManager @Suppress("TooManyFunctions") companion object { @UnstableApi @Provides @Singleton fun provideDatabaseProvider( @ApplicationContext context: Context, ): StandaloneDatabaseProvider = StandaloneDatabaseProvider(context) @OptIn(UnstableApi::class) @Provides @VideoPreview @Singleton fun provideVideoCache( @ApplicationContext context: Context, databaseProvider: StandaloneDatabaseProvider, ): SimpleCache { val downloadContentDirectory = File(context.externalCacheDir, EXOPLAYER_CACHE_VIDEO_DOWNLOAD_CONTENT_DIRECTORY) return SimpleCache( downloadContentDirectory, LeastRecentlyUsedCacheEvictor(EXOPLAYER_CACHE_SIZE_LIMIT_BYTES), databaseProvider, ) } @UnstableApi @Provides @CoreMediaPlayback @Singleton fun provideMediaPlaybackCache( @ApplicationContext context: Context, databaseProvider: StandaloneDatabaseProvider, ): SimpleCache { val downloadContentDirectory = File( context.externalCacheDir, EXOPLAYER_CACHE_MEDIA_PLAYBACK_DOWNLOAD_CONTENT_DIRECTORY, ) return SimpleCache( downloadContentDirectory, LeastRecentlyUsedCacheEvictor(EXOPLAYER_MEDIA_PLAYBACK_CACHE_SIZE_LIMIT_BYTES), databaseProvider, ) } @UnstableApi @Provides @VideoPreview @Singleton fun provideVideoPreviewCacheDataSourceFactory( @ApplicationContext context: Context, @VideoPreview downloadCache: SimpleCache, ): CacheDataSource.Factory { val cacheSink = CacheDataSink.Factory().setCache(downloadCache) val upstreamFactory = DefaultDataSource.Factory(context, DefaultHttpDataSource.Factory()) val downStreamFactory = FileDataSource.Factory() return CacheDataSource.Factory().setCache(downloadCache).setCacheWriteDataSinkFactory(cacheSink) .setCacheReadDataSourceFactory(downStreamFactory).setUpstreamDataSourceFactory(upstreamFactory) .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR) } @UnstableApi @Provides @CoreMediaPlayback @Singleton fun provideMediaPlaybackCacheDataSourceFactory( @ApplicationContext context: Context, @CoreMediaPlayback coreMediaPlaybackDownloadCache: SimpleCache, ): CacheDataSource.Factory { val cacheSink = CacheDataSink.Factory().setCache(coreMediaPlaybackDownloadCache) val upstreamFactory = DefaultDataSource.Factory( context, OkHttpDataSource.Factory( OkHttpClient.Builder() // the default read timeout causes streaming media to cancel streaming, // since the connection is open for the duration of the media // this timeout is long enough to keep waiting for more media .readTimeout(10, TimeUnit.MINUTES).build(), ), ) val downStreamFactory = FileDataSource.Factory() return CacheDataSource.Factory().setCache(coreMediaPlaybackDownloadCache) .setCacheWriteDataSinkFactory(cacheSink).setCacheReadDataSourceFactory(downStreamFactory) .setUpstreamDataSourceFactory(upstreamFactory).setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR) } @OptIn(UnstableApi::class) @Provides @Singleton @VideoPreview fun provideVideoPreviewExoPlayer( @ApplicationContext context: Context, @VideoPreview exoCacheDataSourceFactory: CacheDataSource.Factory, ): ExoPlayer = ExoPlayer.Builder(context).setMediaSourceFactory( DefaultMediaSourceFactory(context).setDataSourceFactory(exoCacheDataSourceFactory), ).build() @OptIn(UnstableApi::class) @Provides @Editor fun provideEditorExoPlayer( @ApplicationContext context: Context, ): ExoPlayer = ExoPlayer.Builder(context).build() @OptIn(UnstableApi::class) @Provides @Editor fun provideEditorPlayerFactory( editorPlayerFactory: EditorPlayerFactory, ): PlayerFactory = editorPlayerFactory @OptIn(UnstableApi::class) @Provides @Editor fun provideEditorLazyPlayer( loggerFactory: SunoLogger.Factory, @Editor playerFactory: PlayerFactory, ): SharedPlayer = SharedPlayer( loggerFactory = loggerFactory, playerFactory = playerFactory, ) @OptIn(UnstableApi::class) @Provides @Singleton @CoreMediaPlayback fun provideMediaPlaybackPlayerFactory( @ApplicationContext context: Context, @CoreMediaPlayback mediaPlaybackExoCacheDataSourceFactory: CacheDataSource.Factory, ): PlayerFactory = CorePlaybackPlayerFactory( context = context, mediaPlaybackExoCacheDataSourceFactory = mediaPlaybackExoCacheDataSourceFactory, ) @OptIn(UnstableApi::class) @Provides @Singleton @CoreMediaPlayback fun provideMediaPlaybackSharedPlayer( loggerFactory: SunoLogger.Factory, @CoreMediaPlayback playerFactory: PlayerFactory, ): SharedPlayer = SharedPlayer( loggerFactory = loggerFactory, playerFactory = playerFactory, ) @Provides @Singleton fun provideMediaLibraryCallback(): MediaLibrarySession.Callback = MediaLibrarySessionCallback() @OptIn(UnstableApi::class) @Provides @Singleton @CoreMediaPlayback fun provideMediaLibrarySessionFactory( @ApplicationContext context: Context, @CoreMediaPlayback sharedPlayer: SharedPlayer, mediaCallback: MediaLibrarySession.Callback, ): MediaLibrarySessionFactory = CorePlaybackMediaLibrarySessionFactory( context = context, sharedPlayer = sharedPlayer, mediaCallback = mediaCallback, ) @Provides @Singleton @CoreMediaPlayback fun provideSharedMediaLibrarySession( @CoreMediaPlayback factory: MediaLibrarySessionFactory, ): SharedMediaLibrarySession = SharedMediaLibrarySession(factory) @OptIn(UnstableApi::class) @Provides @HooksEditor fun provideHooksEditorExoPlayer( @ApplicationContext context: Context, ): Player = ExoPlayer.Builder(context).build() @UnstableApi @Provides @HooksOnboardingVideoPlayer @Singleton fun provideHooksOnboardingVideoPlayerCacheDataSourceFactory( @ApplicationContext context: Context, databaseProvider: StandaloneDatabaseProvider, @HooksOnboardingVideoPlayer okHttpClient: OkHttpClient, ): CacheDataSource.Factory { val downloadContentDirectory = File( context.externalCacheDir, EXOPLAYER_CACHE_VIDEO_HOOKS_ONBOARDING_CONTENT_DIRECTORY, ) val cache = SimpleCache( downloadContentDirectory, NoOpCacheEvictor(), // cache is evicted manually databaseProvider, ) val cacheSink = CacheDataSink.Factory().setCache(cache) val upstreamFactory = DefaultDataSource.Factory( context, OkHttpDataSource.Factory( okHttpClient, ), ) val downStreamFactory = FileDataSource.Factory() return CacheDataSource.Factory() .setCache(cache) .setCacheWriteDataSinkFactory(cacheSink) .setCacheReadDataSourceFactory(downStreamFactory) .setUpstreamDataSourceFactory(upstreamFactory) } } }