diff --git a/automatedtests/src/androidTest/java/com/mux/player/media3/automatedtests/SimplePlayerTestActivity.java b/automatedtests/src/androidTest/java/com/mux/player/media3/automatedtests/SimplePlayerTestActivity.java index cd67d8fe..475a7928 100644 --- a/automatedtests/src/androidTest/java/com/mux/player/media3/automatedtests/SimplePlayerTestActivity.java +++ b/automatedtests/src/androidTest/java/com/mux/player/media3/automatedtests/SimplePlayerTestActivity.java @@ -20,6 +20,7 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; import androidx.media3.common.Player; +import androidx.media3.datasource.DefaultDataSource; import androidx.media3.exoplayer.DefaultRenderersFactory; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.RenderersFactory; @@ -144,7 +145,7 @@ public void initExoPlayer() { DefaultTrackSelector.Parameters trackSelectorParameters = builder .build(); - mediaSourceFactory = new MuxMediaSourceFactory(this); + mediaSourceFactory = new MuxMediaSourceFactory(this, new DefaultDataSource.Factory(this)); trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory); trackSelector.setParameters(trackSelectorParameters); RenderersFactory renderersFactory = new DefaultRenderersFactory(/* context= */ this); @@ -157,7 +158,6 @@ public void initExoPlayer() { exoBuilder.setRenderersFactory(renderersFactory); exoBuilder.setMediaSourceFactory(mediaSourceFactory); exoBuilder.setTrackSelector(trackSelector); - return null; }) .addMonitoringData(initMuxSats()) .addExoPlayerBinding(pBinding) diff --git a/library/src/androidTest/java/com/mux/player/CacheDatastoreInstrumentationTests.kt b/library/src/androidTest/java/com/mux/player/CacheDatastoreInstrumentationTests.kt index 16f4d229..c201817d 100644 --- a/library/src/androidTest/java/com/mux/player/CacheDatastoreInstrumentationTests.kt +++ b/library/src/androidTest/java/com/mux/player/CacheDatastoreInstrumentationTests.kt @@ -4,7 +4,7 @@ import android.content.Context import android.util.Log import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.mux.player.internal.cache.CacheConstants +import com.mux.player.internal.Constants import com.mux.player.internal.cache.CacheDatastore import com.mux.player.internal.cache.FileRecord import com.mux.player.internal.cache.filesDirNoBackupCompat @@ -356,11 +356,11 @@ class CacheDatastoreInstrumentationTests { } private fun expectedFileTempDir(context: Context): File = - File(context.cacheDir, CacheConstants.TEMP_FILE_DIR) + File(context.cacheDir, Constants.TEMP_FILE_DIR) private fun expectedFileCacheDir(context: Context): File = - File(context.cacheDir, CacheConstants.CACHE_FILES_DIR) + File(context.cacheDir, Constants.CACHE_FILES_DIR) private fun expectedIndexDbDir(context: Context): File = - File(context.filesDirNoBackupCompat, CacheConstants.CACHE_BASE_DIR) + File(context.filesDirNoBackupCompat, Constants.CACHE_BASE_DIR) } diff --git a/library/src/main/java/com/mux/player/MuxPlayer.kt b/library/src/main/java/com/mux/player/MuxPlayer.kt index bd9b52e0..d32c55e5 100644 --- a/library/src/main/java/com/mux/player/MuxPlayer.kt +++ b/library/src/main/java/com/mux/player/MuxPlayer.kt @@ -13,14 +13,28 @@ import com.mux.player.internal.Logger import com.mux.player.internal.createNoLogger import com.mux.player.media.MuxDataSource import com.mux.player.media.MuxMediaSourceFactory +import com.mux.player.media.MediaItems import com.mux.stats.sdk.muxstats.ExoPlayerBinding import com.mux.stats.sdk.muxstats.INetworkRequest import com.mux.stats.sdk.muxstats.MuxDataSdk import com.mux.stats.sdk.muxstats.media3.BuildConfig as MuxDataBuildConfig /** - * An [ExoPlayer] with a few extra APIs for interacting with Mux Video (TODO: link?) - * This player also integrates transparently with Mux Data (TODO: link?) + * Mux player for native Android. An [ExoPlayer] with a few extra APIs for interacting with + * Mux Video. This player also integrates transparently with Mux Data when you play Mux Video Assets + * + * ### Basic Usage + * MuxPlayer is almost a direct drop-in replacement for [ExoPlayer]. To create instances of + * [MuxPlayer], use our [Builder] + * + * To play Mux Assets, you can create a MediaItem using [MediaItems.fromMuxPlaybackId], or + * [MediaItems.builderFromMuxPlaybackId] + * + * ### Customizing ExoPlayer + * The underlying [ExoPlayer.Builder] can be reached using [Builder.applyExoConfig] (java callers + * can use [Builder.plusExoConfig]). If you need to inject any custom objects into the underlying + * ExoPlayer, you are able to do so this way. Please note that doing this may interfere with Mux + * Player's features. */ class MuxPlayer private constructor( private val exoPlayer: ExoPlayer, @@ -96,7 +110,7 @@ class MuxPlayer private constructor( device = muxPlayerDevice, network = network, playerBinding = exoPlayerBinding, - ) + ) } } @@ -108,6 +122,11 @@ class MuxPlayer private constructor( * Mux provides some specially-configured media3 factories such as [MuxMediaSourceFactory] that * you should prefer to use with this SDK. * + * ### Customizing ExoPlayer + * If you need to customize the underlying exoplayer, you can use [applyExoConfig]. Note that this + * may interfere with Mux Player's features. See [MuxMediaSourceFactory] for more details on what + * we do to configure exoplayer if you are having issues + * * @see build * * @see MuxMediaSourceFactory @@ -203,8 +222,8 @@ class MuxPlayer private constructor( * @see MuxMediaSourceFactory */ @Suppress("MemberVisibilityCanBePrivate") - fun plusExoConfig(block: (ExoPlayer.Builder) -> Void): Builder { - block(playerBuilder) + fun plusExoConfig(plus: PlusExoBuilder): Builder { + plus.apply(playerBuilder) return this } @@ -230,6 +249,7 @@ class MuxPlayer private constructor( context = context, exoPlayer = this.playerBuilder.build(), muxDataKey = this.dataEnvKey, + muxCacheEnabled = enableSmartCache, logger = logger ?: createNoLogger(), initialCustomerData = customerData, network = network, @@ -249,10 +269,19 @@ class MuxPlayer private constructor( } private fun setUpMediaSourceFactory(builder: ExoPlayer.Builder) { + // For now, the only time to use MuxDataSource is when caching is enabled so do this check val mediaSourceFactory = if (enableSmartCache) { - MuxMediaSourceFactory(context, MuxDataSource.Factory()) + MuxMediaSourceFactory.create( + ctx = context, + logger = this.logger ?: createNoLogger(), + dataSourceFactory = DefaultDataSource.Factory(context, MuxDataSource.Factory()), + ) } else { - MuxMediaSourceFactory(context, DefaultDataSource.Factory(context)) + MuxMediaSourceFactory.create( + ctx = context, + logger = this.logger ?: createNoLogger(), + dataSourceFactory = DefaultDataSource.Factory(context), + ) } builder.setMediaSourceFactory(mediaSourceFactory) } @@ -260,5 +289,12 @@ class MuxPlayer private constructor( init { setUpMediaSourceFactory(playerBuilder) } + + /** + * Use with [plusExoConfig] to configure MuxPlayer's underlying [ExoPlayer] + */ + fun interface PlusExoBuilder { + fun apply(builder: ExoPlayer.Builder) + } } } diff --git a/library/src/main/java/com/mux/player/internal/Constants.kt b/library/src/main/java/com/mux/player/internal/Constants.kt index 84eb0522..0b5a27bc 100644 --- a/library/src/main/java/com/mux/player/internal/Constants.kt +++ b/library/src/main/java/com/mux/player/internal/Constants.kt @@ -1,7 +1,27 @@ package com.mux.player.internal internal object Constants { + const val BUNDLE_DRM_TOKEN = "drm token" const val BUNDLE_PLAYBACK_ID = "playback id" const val BUNDLE_PLAYBACK_DOMAIN = "playback domain" -} \ No newline at end of file + + const val MIME_TS = "video/MP2T" + const val MIME_M4S = "video/mp4" + const val MIME_M4S_ALT = "video/iso.segment" + const val EXT_M4S = "m4s" + const val EXT_TS = "ts" + + /** + * Can be rooted in cache or files dir on either internal or external storage + */ + const val CACHE_BASE_DIR = "mux/player" + /** + * In the cacheDir + */ + const val CACHE_FILES_DIR = "$CACHE_BASE_DIR/cache" + /** + * In the cacheDir + */ + const val TEMP_FILE_DIR = "$CACHE_BASE_DIR/cache/tmp" +} diff --git a/library/src/main/java/com/mux/player/internal/Logger.kt b/library/src/main/java/com/mux/player/internal/Logger.kt index 1f88f005..a2c4b661 100644 --- a/library/src/main/java/com/mux/player/internal/Logger.kt +++ b/library/src/main/java/com/mux/player/internal/Logger.kt @@ -6,7 +6,7 @@ import java.lang.Exception /** * An interface for logging events */ -internal interface Logger { +interface Logger { fun e(tag: String, message: String, exception: Exception? = null) fun w(tag: String, message: String, exception: Exception? = null) fun d(tag: String, message: String, exception: Exception? = null) diff --git a/library/src/main/java/com/mux/player/internal/cache/CacheConstants.kt b/library/src/main/java/com/mux/player/internal/cache/CacheConstants.kt deleted file mode 100644 index f15f2c3f..00000000 --- a/library/src/main/java/com/mux/player/internal/cache/CacheConstants.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.mux.player.internal.cache - -internal object CacheConstants { - const val MIME_TS = "video/MP2T" - const val MIME_M4S = "video/mp4" - const val MIME_M4S_ALT = "video/iso.segment" - const val EXT_M4S = "m4s" - const val EXT_TS = "ts" - - const val PROXY_PORT = 6000 - - /** - * Can be rooted in cache or files dir on either internal or external storage - */ - const val CACHE_BASE_DIR = "mux/player" - /** - * In the cacheDir - */ - const val CACHE_FILES_DIR = "$CACHE_BASE_DIR/cache" - /** - * In the cacheDir - */ - const val TEMP_FILE_DIR = "$CACHE_BASE_DIR/cache/tmp" -} diff --git a/library/src/main/java/com/mux/player/internal/cache/CacheController.kt b/library/src/main/java/com/mux/player/internal/cache/CacheController.kt index 44f7917d..243fda33 100644 --- a/library/src/main/java/com/mux/player/internal/cache/CacheController.kt +++ b/library/src/main/java/com/mux/player/internal/cache/CacheController.kt @@ -5,7 +5,7 @@ import android.annotation.TargetApi import android.content.Context import android.os.Build import android.util.Log -import com.mux.player.internal.cache.CacheController.downloadStarted +import com.mux.player.internal.cache.CacheController.startWriting import com.mux.player.internal.cache.CacheController.setup import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -73,7 +73,7 @@ internal object CacheController { return if (fileRecord == null) { null } else { - ReadHandle( + ReadHandle.create( url = requestUrl, fileRecord = fileRecord, datastore = datastore, @@ -84,16 +84,18 @@ internal object CacheController { /** * Call when you are about to download the body of a response. This method returns an object you - * can use to write your data. See [WriteHandle] for more information + * can use to write your data. When you are done writing, call [WriteHandle.finishedWriting]. + * + * @see [WriteHandle] */ - fun downloadStarted( + fun startWriting( requestUrl: String, responseHeaders: Map>, ): WriteHandle { return if (shouldCacheResponse(requestUrl, responseHeaders)) { val tempFile = datastore.createTempDownloadFile(URL(requestUrl)) - WriteHandle( + WriteHandle.create( controller = this, tempFile = tempFile, responseHeaders = responseHeaders, @@ -102,7 +104,7 @@ internal object CacheController { ) } else { // not supposed to cache, so the WriteHandle just writes to the player - WriteHandle( + WriteHandle.create( controller = this, tempFile = null, url = requestUrl, @@ -117,9 +119,7 @@ internal object CacheController { */ @JvmSynthetic internal fun onPlayerCreated() { - Log.d(TAG, "onPlayerCreated: called") val totalPlayersBefore = playersWithCache.getAndIncrement() - Log.d(TAG, "onPlayerCreated: had $totalPlayersBefore players") if (totalPlayersBefore == 0) { ioScope.launch { datastore.open() } } @@ -189,12 +189,8 @@ internal object CacheController { return false } - // todo - Need to specifically only cache segments. Check content-type first then url - // todo - additional logic here: // * check disk space against Content-Length? - // * check for headers like Age? - // * make sure the entry is not already expired by like a second or whatever (edge case) return true } @@ -204,9 +200,9 @@ internal object CacheController { * Object for reading from the Cache. The methods on this object will read bytes from a cache copy * of the remote resource. * - * Use [readAllInto] to read the entire file into an OutputStream. + * Obtain an instance via [CacheController.tryRead] */ -internal class ReadHandle internal constructor( +internal class ReadHandle private constructor( val url: String, val fileRecord: FileRecord, datastore: CacheDatastore, @@ -216,6 +212,13 @@ internal class ReadHandle internal constructor( companion object { const val READ_SIZE = 32 * 1024 private const val TAG = "ReadHandle" + + @JvmSynthetic internal fun create( + url: String, + fileRecord: FileRecord, + datastore: CacheDatastore, + directory: File, + ): ReadHandle = ReadHandle(url, fileRecord, datastore, directory) } private val cacheFile: File @@ -224,7 +227,6 @@ internal class ReadHandle internal constructor( init { // todo - Are we really doing relative paths here? We want to be cacheFile = File(datastore.fileCacheDir(), fileRecord.relativePath) - Log.v(TAG, "Reading from $cacheFile") fileInput = BufferedInputStream(FileInputStream(cacheFile)) } @@ -255,11 +257,12 @@ internal class ReadHandle internal constructor( } /** - * Object for writing to both the player and the cache. Call [downloadStarted] to get one of these - * for any given web response. Writes to this handle will go to the player and also to the cache - * if required + * Object for writing to both the player and the cache. Writes to this handle will go to the player + * and also to the cache if required + * + * Obtain an instance with [CacheController.startWriting] */ -internal class WriteHandle internal constructor( +internal class WriteHandle private constructor( val url: String, val responseHeaders: Map>, private val controller: CacheController, @@ -269,6 +272,14 @@ internal class WriteHandle internal constructor( companion object { private const val TAG = "WriteHandle" + + @JvmSynthetic internal fun create( + url: String, + responseHeaders: Map>, + controller: CacheController, + datastore: CacheDatastore, + tempFile: File?, + ): WriteHandle = WriteHandle(url, responseHeaders, controller, datastore, tempFile) } private val fileOutputStream = tempFile?.let { BufferedOutputStream(FileOutputStream(it)) } @@ -297,15 +308,12 @@ internal class WriteHandle internal constructor( val eTag = responseHeaders.getETag() if (cacheControl != null && eTag != null) { val cacheFile = datastore.moveFromTempFile(tempFile, URL(url)) - Log.d(TAG, "move to cache file with path ${cacheFile.path}") val nowUtc = nowUtc() val recordAge = responseHeaders.getAge()?.toLongOrNull() val maxAge = parseMaxAge(cacheControl) ?: parseSMaxAge(cacheControl) val relativePath = cacheFile.toRelativeString(datastore.fileCacheDir()) - Log.i(TAG, "Saving ${cacheFile.length()} to cache as: $relativePath") - val record = FileRecord( url = url, etag = eTag, @@ -325,10 +333,6 @@ internal class WriteHandle internal constructor( if (result.isSuccess) { datastore.evictByLru() } - - // todo - return a fail or throw somerthing - } else { - // todo: need a logger } } } diff --git a/library/src/main/java/com/mux/player/internal/cache/CacheDatastore.kt b/library/src/main/java/com/mux/player/internal/cache/CacheDatastore.kt index 5a93c744..671a7b02 100644 --- a/library/src/main/java/com/mux/player/internal/cache/CacheDatastore.kt +++ b/library/src/main/java/com/mux/player/internal/cache/CacheDatastore.kt @@ -5,7 +5,7 @@ import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import android.os.Build import android.util.Base64 -import android.util.Log +import com.mux.player.internal.Constants import com.mux.player.oneOf import java.io.Closeable import java.io.File @@ -121,8 +121,6 @@ internal class CacheDatastore( ) } - Log.v(TAG, "Wrote to row $rowId") - return if (rowId >= 0) { Result.success(Unit) } else { @@ -169,19 +167,19 @@ internal class CacheDatastore( * downloaded. Temp files are deleted on JVM exit. Every temp file is unique, preventing * collisions between multiple potential writers to the same file */ - fun fileTempDir(): File = File(context.cacheDir, CacheConstants.TEMP_FILE_DIR) + fun fileTempDir(): File = File(context.cacheDir, Constants.TEMP_FILE_DIR) /** * A subdirectory within the app's cache dir where we keep cached media files that have been * committed to the cache with `WriteHandle.finishedWriting`. Temp files are guaranteed not to be * open for writing or appending, and their content can be relied upon assuming the file exists */ - fun fileCacheDir(): File = File(context.cacheDir, CacheConstants.CACHE_FILES_DIR) + fun fileCacheDir(): File = File(context.cacheDir, Constants.CACHE_FILES_DIR) /** * A subdirectory within an app's no-backup files dir that contains the cache's index */ - fun indexDbDir(): File = File(context.filesDirNoBackupCompat, CacheConstants.CACHE_BASE_DIR) + fun indexDbDir(): File = File(context.filesDirNoBackupCompat, Constants.CACHE_BASE_DIR) /** * Generates a URL-safe cache key for a given URL. Delegates to [generateCacheKey] but encodes it @@ -232,7 +230,7 @@ internal class CacheDatastore( urlStr } else { val extension = matchResult.groups[3]!!.value - val isSegment = extension.oneOf(CacheConstants.EXT_TS, CacheConstants.EXT_M4S) + val isSegment = extension.oneOf(Constants.EXT_TS, Constants.EXT_M4S) if (isSegment) { // todo - we can pull out more-specific key parts using groups 2 and 1 if we want, but the @@ -270,13 +268,11 @@ internal class CacheDatastore( // todo - remember to skip files that CacheController knows are already being read val candidates = doReadLeastRecentFiles(db) - Log.i(TAG, "About to evict ${candidates.map { it.relativePath }}") candidates.forEach { candidate -> File(fileCacheDir(), candidate.relativePath).delete() } // remove last so we don't leave orphans val deleteResult = doDeleteRecords(db, candidates) - Log.v(TAG, "deleted $deleteResult records") if (deleteResult.isSuccess) { db.setTransactionSuccessful() @@ -432,12 +428,8 @@ internal class CacheDatastore( } return actualTask.get() } catch (e: Exception) { - // todo - get a Logger down here - Log.e("CacheDatastore", "failed to open cache", e) - // subsequent calls can attempt again openTask.set(null) - throw IOException(e) } } diff --git a/library/src/main/java/com/mux/player/internal/cache/CacheUtils.kt b/library/src/main/java/com/mux/player/internal/cache/CacheUtils.kt index 54f193c0..b83dceeb 100644 --- a/library/src/main/java/com/mux/player/internal/cache/CacheUtils.kt +++ b/library/src/main/java/com/mux/player/internal/cache/CacheUtils.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.content.Context import android.database.Cursor import android.os.Build +import com.mux.player.internal.Constants import java.io.File import java.io.IOException import java.io.InputStream @@ -49,9 +50,9 @@ internal fun InputStream.consumeInto(outputStream: OutputStream, readSize: Int = @JvmSynthetic internal fun isContentTypeSegment(contentTypeHeader: String?): Boolean { - return contentTypeHeader.equals(CacheConstants.MIME_TS, true) - || contentTypeHeader.equals(CacheConstants.MIME_M4S, true) - || contentTypeHeader.equals(CacheConstants.MIME_M4S_ALT, true) + return contentTypeHeader.equals(Constants.MIME_TS, true) + || contentTypeHeader.equals(Constants.MIME_M4S, true) + || contentTypeHeader.equals(Constants.MIME_M4S_ALT, true) } @JvmSynthetic diff --git a/library/src/main/java/com/mux/player/media/MediaItems.kt b/library/src/main/java/com/mux/player/media/MediaItems.kt index 58a389d9..0841447d 100644 --- a/library/src/main/java/com/mux/player/media/MediaItems.kt +++ b/library/src/main/java/com/mux/player/media/MediaItems.kt @@ -8,10 +8,7 @@ import androidx.media3.common.MediaItem.RequestMetadata import com.mux.player.internal.Constants /** - * Creates instances of [MediaItem] or [MediaItem.Builder] configured for easy use with - * `MuxPlayer` - * - * TODO: Alternative spelling: MuxMediaItems + * Creates instances of [MediaItem] or [MediaItem.Builder] configured for easy use with MuxPlayer`. */ object MediaItems { @@ -21,7 +18,7 @@ object MediaItems { * Default domain + tld for Mux Video */ @Suppress("MemberVisibilityCanBePrivate") - const val MUX_VIDEO_DEFAULT_DOMAIN = "mux.com" + internal const val MUX_VIDEO_DEFAULT_DOMAIN = "mux.com" internal const val MUX_VIDEO_SUBDOMAIN = "stream" internal const val EXTRA_VIDEO_DATA = "com.mux.video.customerdata" @@ -29,6 +26,17 @@ object MediaItems { /** * Creates a new [MediaItem] that points to a given Mux Playback ID. * + * ## Controlling resolution + * You can use the [maxResolution] and [minResolution] parameters to control the possible video + * resolutions that Mux Player can stream. You can use these parameters to control your overall + * playback experience and platform usage. Lower resolution generally means smoother playback + * experience and lower costs, higher resolution generally means nicer-looking videos that may + * take longer to start or stall on unfavorable networks. + * + * ## Custom domains + * If you are using Mux Video [custom domains](https://docs.mux.com/guides/use-a-custom-domain-for-streaming#use-your-own-domain-for-delivering-videos-and-images), + * you can configure your MediaItem with your custom domain using the [domain] parameter + * * ## DRM and Secure playback * Mux player provides two types of playback security, signed playback and DRM playback. Signed * playback protects your assets from being played by third parties by using a Playback Token @@ -43,9 +51,13 @@ object MediaItems { * To use DRM playback, you must provide *both* a valid [playbackToken] and a valid [drmToken] * * @param playbackId A playback ID for a Mux Asset + * @param maxResolution The maximum resolution that should be requested over the network + * @param minResolution The minimum resolution that should be requested over the network + * @param renditionOrder [RenditionOrder.Descending] to emphasize quality, [RenditionOrder.Ascending] to emphasize performance * @param domain Optional custom domain for Mux Video. The default is [MUX_VIDEO_DEFAULT_DOMAIN] * @param playbackToken Playback Token required for Secure Video Playback and DRM Playback * @param drmToken DRM Token required for DRM Playback. For DRM, you also need a [playbackToken] + * @param playbackToken Playback token for secure playback * * @see builderFromMuxPlaybackId */ @@ -56,7 +68,7 @@ object MediaItems { maxResolution: PlaybackResolution? = null, minResolution: PlaybackResolution? = null, renditionOrder: RenditionOrder? = null, - domain: String = MUX_VIDEO_DEFAULT_DOMAIN, + domain: String? = MUX_VIDEO_DEFAULT_DOMAIN, playbackToken: String? = null, drmToken: String? = null, ): MediaItem = builderFromMuxPlaybackId( @@ -73,6 +85,17 @@ object MediaItems { /** * Creates a new [MediaItem] that points to a given Mux Playback ID. * + * ## Controlling resolution + * You can use the [maxResolution] and [minResolution] parameters to control the possible video + * resolutions that Mux Player can stream. You can use these parameters to control your overall + * playback experience and platform usage. Lower resolution generally means smoother playback + * experience and lower costs, higher resolution generally means nicer-looking videos that may + * take longer to start or stall on unfavorable networks. + * + * ## Custom domains + * If you are using Mux Video [custom domains](https://docs.mux.com/guides/use-a-custom-domain-for-streaming#use-your-own-domain-for-delivering-videos-and-images), + * you can configure your MediaItem with your custom domain using the [domain] parameter + * * ## DRM and Secure playback * Mux player provides two types of playback security, signed playback and DRM playback. Signed * playback protects your assets from being played by third parties by using a Playback Token @@ -86,12 +109,26 @@ object MediaItems { * ### DRM Playback * To use DRM playback, you must provide *both* a valid [playbackToken] and a valid [drmToken] * + * ## Controlling resolution + * You can use the [maxResolution] and [minResolution] parameters to control the possible video + * resolutions that Mux Player can stream. You can use these parameters to control your overall + * playback experience and platform usage. Lower resolution generally means smoother playback + * experience and lower costs, higher resolution generally means nicer-looking videos that may + * take longer to start or stall on unfavorable networks. + * + * ## Custom domains + * If you are using Mux Video [custom domains](https://docs.mux.com/guides/use-a-custom-domain-for-streaming#use-your-own-domain-for-delivering-videos-and-images), + * you can configure your MediaItem with your custom domain using the [domain] parameter + * * @param playbackId A playback ID for a Mux Asset + * @param maxResolution The maximum resolution that should be requested over the network + * @param minResolution The minimum resolution that should be requested over the network + * @param renditionOrder [RenditionOrder.Descending] to emphasize quality, [RenditionOrder.Ascending] to emphasize performance * @param domain Optional custom domain for Mux Video. The default is [MUX_VIDEO_DEFAULT_DOMAIN] * @param playbackToken Playback Token required for Secure Video Playback and DRM Playback * @param drmToken DRM Token required for DRM Playback. For DRM, you also need a [playbackToken] * - * @see fromMuxPlaybackId + * @see builderFromMuxPlaybackId */ @JvmStatic @JvmOverloads @@ -100,7 +137,7 @@ object MediaItems { maxResolution: PlaybackResolution? = null, minResolution: PlaybackResolution? = null, renditionOrder: RenditionOrder? = null, - domain: String = MUX_VIDEO_DEFAULT_DOMAIN, + domain: String? = MUX_VIDEO_DEFAULT_DOMAIN, playbackToken: String? = null, drmToken: String? = null, ): MediaItem.Builder { @@ -108,7 +145,7 @@ object MediaItems { .setUri( createPlaybackUrl( playbackId = playbackId, - domain = domain, + domain = domain ?: MUX_VIDEO_DEFAULT_DOMAIN, maxResolution = maxResolution, minResolution = minResolution, renditionOrder = renditionOrder, @@ -141,19 +178,19 @@ object MediaItems { minResolution?.let { base.appendQueryParameter("min_resolution", resolutionValue(it)) } maxResolution?.let { base.appendQueryParameter("max_resolution", resolutionValue(it)) } - renditionOrder?.let { base.appendQueryParameter("rendition_order", resolutionValue(it)) } + renditionOrder?.takeIf { it != RenditionOrder.Default } + ?.let { base.appendQueryParameter("rendition_order", resolutionValue(it)) } playbackToken?.let { base.appendQueryParameter("token", it) } base.appendQueryParameter("redundant_streams", "true"); return base.build().toString() - .also { Log.d(TAG, "playback URI is $it") } } private fun resolutionValue(renditionOrder: RenditionOrder): String { return when (renditionOrder) { - RenditionOrder.Ascending -> "asc" RenditionOrder.Descending -> "desc" + else -> "" // should be avoided by createPlaybackUrl } } @@ -182,7 +219,18 @@ enum class PlaybackResolution { FOUR_K_2160, } +/** + * The order of preference for adaptive streaming. + */ enum class RenditionOrder { - Ascending, + /** + * The highest-resolution renditions will be chosen first, adjusting downward if needed. This + * setting emphasizes video quality, but may lead to more interruptions on unfavorable networks + */ Descending, -} \ No newline at end of file + + /** + * The default rendition order will be used, which may be optimized for delivery + */ + Default, +} diff --git a/library/src/main/java/com/mux/player/media/MuxDataSource.kt b/library/src/main/java/com/mux/player/media/MuxDataSource.kt index 44e773e4..6163b7f3 100644 --- a/library/src/main/java/com/mux/player/media/MuxDataSource.kt +++ b/library/src/main/java/com/mux/player/media/MuxDataSource.kt @@ -75,21 +75,17 @@ class MuxDataSource private constructor( override fun open(dataSpec: DataSpec): Long { this.dataSpec = dataSpec; - Log.i(TAG, "open(): Opening URI ${dataSpec.uri}") val readHandle = CacheController.tryRead(dataSpec.uri.toString()) val nowUtc = nowUtc() return if (readHandle == null) { // cache miss - Log.d(TAG, "cache MISS on url ${dataSpec.uri}") openAndInitFromRemote(dataSpec, upstreamSrcFac) } else if (CacheController.revalidateRequired(nowUtc, readHandle.fileRecord)) { // need to revalidate - Log.d(TAG, "cache VALIDATING url ${dataSpec.uri}") openAndInitRevalidating(dataSpec, readHandle) } else { // cache hit - Log.d(TAG, "cache HIT on url ${dataSpec.uri}") openAndInitFromCache(readHandle) } } @@ -115,25 +111,19 @@ class MuxDataSource private constructor( val upstreamBytes = openAndInitFromRemote(revalidateSpec, RevalidatingDataSource.Factory()) val upstream = this.upstream!! // set by initAndOpenUpstream - Log.d(TAG, "revalidation: HTTP ${upstream.responseCode}. $upstreamBytes available") return if (upstream.responseCode != 304) { // Entry wasn't valid anymore, but we did a GET so the body's ready to read and we're done - Log.d(TAG, "revalidation: Entry was NOT valid, getting from network") // todo - we *could* delete the row here, but consider that stale items can be used if // state-while-revalidate or stale-while-error or if we're disconnected (unless must-revalidate).. - // Right now, we saved time by assuming state-while-error and *not* must-revalidate so we - // would just wanna keep the stale entries around in case we can use them + // For now, we saved time by assuming state-while-error and *not* must-revalidate & keep it upstreamBytes } else { - Log.d(TAG, "revalidation: Entry WAS still valid, getting from cache") // Entry was still valid, so read from cache instead upstream.close() this.upstream = null - // todo - - openAndInitFromCache(readHandle) } } @@ -141,9 +131,10 @@ class MuxDataSource private constructor( private fun openAndInitFromRemote(dataSpec: DataSpec, fac: HttpDataSource.Factory): Long { respondingFromCache = false val upstream = fac.createDataSource() + this.upstream = upstream val available = upstream.open(dataSpec) - cacheWriter = CacheController.downloadStarted( + cacheWriter = CacheController.startWriting( dataSpec.uri.toString(), upstream.responseHeaders, ) @@ -217,6 +208,7 @@ private class RevalidatingDataSource : BaseDataSource(true), HttpDataSource { val msg = conn.responseMessage if (code == HttpURLConnection.HTTP_NOT_MODIFIED) { // not-modified, not an error, we don't have to download the body again + runCatching { conn.disconnect() } return 0 } else if (code < 200 || code > 299) { // some kind of error diff --git a/library/src/main/java/com/mux/player/media/MuxDrmSessionManagerProvider.kt b/library/src/main/java/com/mux/player/media/MuxDrmSessionManagerProvider.kt index ba1cc31c..66efb79a 100644 --- a/library/src/main/java/com/mux/player/media/MuxDrmSessionManagerProvider.kt +++ b/library/src/main/java/com/mux/player/media/MuxDrmSessionManagerProvider.kt @@ -18,7 +18,9 @@ import androidx.media3.exoplayer.drm.ExoMediaDrm.KeyRequest import androidx.media3.exoplayer.drm.FrameworkMediaDrm import androidx.media3.exoplayer.drm.MediaDrmCallback import com.mux.player.internal.Constants +import com.mux.player.internal.Logger import com.mux.player.internal.createLicenseUri +import com.mux.player.internal.createNoLogger import com.mux.player.internal.executePost import com.mux.player.internal.getDrmToken import com.mux.player.internal.getLicenseUrlHost @@ -31,6 +33,7 @@ import java.util.UUID @OptIn(UnstableApi::class) class MuxDrmSessionManagerProvider( val drmHttpDataSourceFactory: HttpDataSource.Factory, + val logger: Logger = createNoLogger(), ) : DrmSessionManagerProvider { companion object { @@ -67,13 +70,13 @@ class MuxDrmSessionManagerProvider( } private fun createSessionManager(mediaItem: MediaItem): DrmSessionManager { - Log.i(TAG, "createSessionManager: called with $mediaItem") + logger.i(TAG, "createSessionManager: called with $mediaItem") val playbackId = mediaItem.getPlaybackId() val drmToken = mediaItem.getDrmToken() - Log.v(TAG, "createSessionManager: for playbackId $playbackId") - Log.v(TAG, "createSessionManager: for drm token $drmToken") - Log.v(TAG, "createSessionManager: for custom video domain ${mediaItem.getPlaybackDomain()}") + logger.v(TAG, "createSessionManager: for playbackId $playbackId") + logger.v(TAG, "createSessionManager: for drm token $drmToken") + logger.v(TAG, "createSessionManager: for custom video domain ${mediaItem.getPlaybackDomain()}") // Mux Video requires both of these for its DRM system if (playbackId == null || drmToken == null) { @@ -89,6 +92,7 @@ class MuxDrmSessionManagerProvider( licenseEndpointHost = mediaItem.getLicenseUrlHost(), drmToken = drmToken, playbackId = playbackId, + logger = logger, ) ) } @@ -100,6 +104,7 @@ class MuxDrmCallback( private val licenseEndpointHost: String, // eg, 'license.mux.com' or 'license.custom.abc1234.com' private val drmToken: String, private val playbackId: String, + private val logger: Logger = createNoLogger(), ) : MediaDrmCallback { companion object { @@ -116,7 +121,7 @@ class MuxDrmCallback( } val uri = createLicenseUri(playbackId, drmToken, licenseEndpointHost) - Log.d(TAG, "executeProvisionRequest: license URI is $uri") + logger.d(TAG, "executeProvisionRequest: license URI is $uri") val headers = mapOf( "Content-Type" to listOf("application/octet-stream") ) @@ -128,19 +133,19 @@ class MuxDrmCallback( requestBody = request.data, dataSourceFactory = drmHttpDataSourceFactory, ).also { - Log.i(TAG, "License Response: ${Base64.encodeToString(it, Base64.NO_WRAP)}") + logger.i(TAG, "License Response: ${Base64.encodeToString(it, Base64.NO_WRAP)}") } } catch (e: InvalidResponseCodeException) { - Log.e(TAG, "Provisioning/License Request failed!", e) - Log.d(TAG, "Dumping data spec: ${e.dataSpec}") - Log.d(TAG, "Error Body Bytes: ${Base64.encodeToString(e.responseBody, Base64.NO_WRAP)}") + logger.e(TAG, "Provisioning/License Request failed!", e) + logger.d(TAG, "Dumping data spec: ${e.dataSpec}") + logger.d(TAG, "Error Body Bytes: ${Base64.encodeToString(e.responseBody, Base64.NO_WRAP)}") throw e } catch (e: HttpDataSourceException) { - Log.e(TAG, "Provisioning/License Request failed!", e) - Log.d(TAG, "Dumping data spec: ${e.dataSpec}") + logger.e(TAG, "Provisioning/License Request failed!", e) + logger.d(TAG, "Dumping data spec: ${e.dataSpec}") throw e } catch (e: Exception) { - Log.e(TAG, "Provisioning/License Request failed!", e) + logger.e(TAG, "Provisioning/License Request failed!", e) throw e } } @@ -158,7 +163,7 @@ class MuxDrmCallback( val headers = mapOf( "Content-Type" to listOf("application/octet-stream") ) - Log.d(TAG, "Key Request URI is $url") + logger.d(TAG, "Key Request URI is $url") try { return executePost( @@ -168,13 +173,13 @@ class MuxDrmCallback( dataSourceFactory = drmHttpDataSourceFactory, ) } catch (e: InvalidResponseCodeException) { - Log.e(TAG, "key request failed!", e) + logger.e(TAG, "key request failed!", e) throw e } catch (e: HttpDataSourceException) { - Log.e(TAG, "Key Request failed!", e) + logger.e(TAG, "Key Request failed!", e) throw e } catch (e: Exception) { - Log.e(TAG, "KEY Request failed!", e) + logger.e(TAG, "KEY Request failed!", e) throw e } } diff --git a/library/src/main/java/com/mux/player/media/MuxMediaSourceFactory.kt b/library/src/main/java/com/mux/player/media/MuxMediaSourceFactory.kt index e4cddec8..b1999f74 100644 --- a/library/src/main/java/com/mux/player/media/MuxMediaSourceFactory.kt +++ b/library/src/main/java/com/mux/player/media/MuxMediaSourceFactory.kt @@ -12,6 +12,8 @@ import androidx.media3.exoplayer.drm.DrmSessionManagerProvider import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.upstream.CmcdConfiguration +import com.mux.player.internal.Logger +import com.mux.player.internal.createNoLogger /** * A [MediaSource.Factory] configured to work best with Mux Video. @@ -29,12 +31,29 @@ import androidx.media3.exoplayer.upstream.CmcdConfiguration * [innerFactory] */ @OptIn(UnstableApi::class) -class MuxMediaSourceFactory @JvmOverloads constructor( +class MuxMediaSourceFactory private constructor( ctx: Context, - dataSourceFactory: DataSource.Factory = DefaultDataSource.Factory(ctx), + dataSourceFactory: DataSource.Factory, private val innerFactory: DefaultMediaSourceFactory = DefaultMediaSourceFactory(ctx), + private val logger: Logger, ) : MediaSource.Factory by innerFactory { + companion object { + @JvmSynthetic internal fun create( + ctx: Context, + dataSourceFactory: DataSource.Factory, + innerFactory: DefaultMediaSourceFactory = DefaultMediaSourceFactory(ctx), + logger: Logger, + ): MuxMediaSourceFactory = MuxMediaSourceFactory(ctx, dataSourceFactory, innerFactory, logger) + } + + @JvmOverloads + constructor( + ctx: Context, + dataSourceFactory: DataSource.Factory, + innerFactory: DefaultMediaSourceFactory = DefaultMediaSourceFactory(ctx), + ) : this (ctx, dataSourceFactory, innerFactory, createNoLogger()) + init { // basics innerFactory.setCmcdConfigurationFactory(CmcdConfiguration.Factory.DEFAULT) @@ -42,7 +61,8 @@ class MuxMediaSourceFactory @JvmOverloads constructor( // drm innerFactory.setDrmSessionManagerProvider(MuxDrmSessionManagerProvider( - drmHttpDataSourceFactory = DefaultHttpDataSource.Factory() + drmHttpDataSourceFactory = DefaultHttpDataSource.Factory(), + logger = logger )) } }