Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge 1.0.0 changes into the DRM branch #51

Merged
merged 12 commits into from
May 24, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -157,7 +158,6 @@ public void initExoPlayer() {
exoBuilder.setRenderersFactory(renderersFactory);
exoBuilder.setMediaSourceFactory(mediaSourceFactory);
exoBuilder.setTrackSelector(trackSelector);
return null;
})
.addMonitoringData(initMuxSats())
.addExoPlayerBinding(pBinding)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
50 changes: 43 additions & 7 deletions library/src/main/java/com/mux/player/MuxPlayer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -96,7 +110,7 @@ class MuxPlayer private constructor(
device = muxPlayerDevice,
network = network,
playerBinding = exoPlayerBinding,
)
)
}
}

Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand All @@ -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,
Expand All @@ -249,16 +269,32 @@ 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)
}

init {
setUpMediaSourceFactory(playerBuilder)
}

/**
* Use with [plusExoConfig] to configure MuxPlayer's underlying [ExoPlayer]
*/
fun interface PlusExoBuilder {
fun apply(builder: ExoPlayer.Builder)
}
}
}
22 changes: 21 additions & 1 deletion library/src/main/java/com/mux/player/internal/Constants.kt
Original file line number Diff line number Diff line change
@@ -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"
}

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"
}
2 changes: 1 addition & 1 deletion library/src/main/java/com/mux/player/internal/Logger.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -73,7 +73,7 @@ internal object CacheController {
return if (fileRecord == null) {
null
} else {
ReadHandle(
ReadHandle.create(
url = requestUrl,
fileRecord = fileRecord,
datastore = datastore,
Expand All @@ -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<String, List<String>>,
): WriteHandle {
return if (shouldCacheResponse(requestUrl, responseHeaders)) {
val tempFile = datastore.createTempDownloadFile(URL(requestUrl))

WriteHandle(
WriteHandle.create(
controller = this,
tempFile = tempFile,
responseHeaders = responseHeaders,
Expand All @@ -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,
Expand All @@ -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() }
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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))
}

Expand Down Expand Up @@ -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<String, List<String>>,
private val controller: CacheController,
Expand All @@ -269,6 +272,14 @@ internal class WriteHandle internal constructor(

companion object {
private const val TAG = "WriteHandle"

@JvmSynthetic internal fun create(
url: String,
responseHeaders: Map<String, List<String>>,
controller: CacheController,
datastore: CacheDatastore,
tempFile: File?,
): WriteHandle = WriteHandle(url, responseHeaders, controller, datastore, tempFile)
}

private val fileOutputStream = tempFile?.let { BufferedOutputStream(FileOutputStream(it)) }
Expand Down Expand Up @@ -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,
Expand All @@ -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
}
}
}
Expand Down
Loading
Loading