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

Releases/v1.0.0 #50

Merged
merged 4 commits into from
May 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
38 changes: 33 additions & 5 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 @@ -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,8 +269,9 @@ class MuxPlayer private constructor(
}

private fun setUpMediaSourceFactory(builder: ExoPlayer.Builder) {
// todo - probably always use MuxMediaSource
val mediaSourceFactory = if (enableSmartCache) {
MuxMediaSourceFactory(context, MuxDataSource.Factory())
MuxMediaSourceFactory(context, DefaultDataSource.Factory(context, MuxDataSource.Factory()))
} else {
MuxMediaSourceFactory(context, DefaultDataSource.Factory(context))
}
Expand All @@ -260,5 +281,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)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package com.mux.player.internal.cache
package com.mux.player.internal

internal object CacheConstants {
internal object Constants {
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
*/
Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -121,8 +121,6 @@ internal class CacheDatastore(
)
}

Log.v(TAG, "Wrote to row $rowId")

return if (rowId >= 0) {
Result.success(Unit)
} else {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}
}
Expand Down
Loading
Loading