Skip to content

Commit

Permalink
fix(android): refactor source, fix random DRM issue and crop start on…
Browse files Browse the repository at this point in the history
… local asset (#3835)

* fix: refactor source parameter parsing

Also fix a side issue when using a local file cropping props were not applied
Also fix random DRM issue by refactoring initializePlayerSource #3082

* chore: restore metadata checks before appling them
  • Loading branch information
freeboub authored May 30, 2024
1 parent 1b51c15 commit bdf3e55
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 180 deletions.
210 changes: 210 additions & 0 deletions android/src/main/java/com/brentvatne/common/api/Source.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package com.brentvatne.common.api

import android.annotation.SuppressLint
import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources
import android.net.Uri
import android.text.TextUtils
import com.brentvatne.common.toolbox.DebugLog
import com.brentvatne.common.toolbox.DebugLog.e
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetArray
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetInt
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetMap
import com.brentvatne.common.toolbox.ReactBridgeUtils.safeGetString
import com.facebook.react.bridge.ReadableMap
import java.util.Locale

/**
* Class representing Source props for host.
* Only generic code here, no reference to the player.
*/
class Source {
/** String value of source to playback */
private var uriString: String? = null

/** Parsed value of source to playback */
var uri: Uri? = null

/** Start position of playback used to resume playback */
var startPositionMs: Int = -1

/** Will crop content start at specified position */
var cropStartMs: Int = -1

/** Will crop content end at specified position */
var cropEndMs: Int = -1

/** Allow to force stream content, necessary when uri doesn't contain content type (.mlp4, .m3u, ...) */
var extension: String? = null

/** Metadata to display in notification */
var metadata: Metadata? = null

/** http header list */
val headers: MutableMap<String, String> = HashMap()

/** return true if this and src are equals */
override fun equals(other: Any?): Boolean {
if (other == null || other !is Source) return false
return (
uri == other.uri &&
cropStartMs == other.cropStartMs &&
cropEndMs == other.cropEndMs &&
startPositionMs == other.startPositionMs &&
extension == other.extension
)
}

/** return true if this and src are equals */
fun isEquals(source: Source): Boolean {
return this == source
}

/** Metadata to display in notification */
class Metadata {
/** Metadata title */
var title: String? = null

/** Metadata subtitle */
var subtitle: String? = null

/** Metadata description */
var description: String? = null

/** Metadata artist */
var artist: String? = null

/** image uri to display */
var imageUri: Uri? = null

companion object {
private const val PROP_SRC_METADATA_TITLE = "title"
private const val PROP_SRC_METADATA_SUBTITLE = "subtitle"
private const val PROP_SRC_METADATA_DESCRIPTION = "description"
private const val PROP_SRC_METADATA_ARTIST = "artist"
private const val PROP_SRC_METADATA_IMAGE_URI = "imageUri"

/** parse metadata object */
@JvmStatic
fun parse(src: ReadableMap?): Metadata? {
if (src != null) {
val metadata = Metadata()
metadata.title = safeGetString(src, PROP_SRC_METADATA_TITLE)
metadata.subtitle = safeGetString(src, PROP_SRC_METADATA_SUBTITLE)
metadata.description = safeGetString(src, PROP_SRC_METADATA_DESCRIPTION)
metadata.artist = safeGetString(src, PROP_SRC_METADATA_ARTIST)
val imageUriString = safeGetString(src, PROP_SRC_METADATA_IMAGE_URI)
try {
metadata.imageUri = Uri.parse(imageUriString)
} catch (e: Exception) {
e(TAG, "Could not parse imageUri in metadata")
}
return metadata
}
return null
}
}
}

companion object {
private const val TAG = "Source"
private const val PROP_SRC_URI = "uri"
private const val PROP_SRC_START_POSITION = "startPosition"
private const val PROP_SRC_CROP_START = "cropStart"
private const val PROP_SRC_CROP_END = "cropEnd"
private const val PROP_SRC_TYPE = "type"
private const val PROP_SRC_METADATA = "metadata"
private const val PROP_SRC_HEADERS = "requestHeaders"

@SuppressLint("DiscouragedApi")
private fun getUriFromAssetId(context: Context, uriString: String): Uri? {
val resources: Resources = context.resources
val packageName: String = context.packageName
var identifier = resources.getIdentifier(
uriString,
"drawable",
packageName
)
if (identifier == 0) {
identifier = resources.getIdentifier(
uriString,
"raw",
packageName
)
}

if (identifier <= 0) {
// cannot find identifier of content
DebugLog.d(TAG, "cannot find identifier")
return null
}
return Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE).path(identifier.toString()).build()
}

/** parse the source ReadableMap received from app */
@JvmStatic
fun parse(src: ReadableMap?, context: Context): Source {
val source = Source()

if (src != null) {
val uriString = safeGetString(src, PROP_SRC_URI, null)
if (uriString == null || TextUtils.isEmpty(uriString)) {
DebugLog.d(TAG, "isEmpty uri:$uriString")
return source
}
var uri = Uri.parse(uriString)
if (uri == null) {
// return an empty source
DebugLog.d(TAG, "Invalid uri:$uriString")
return source
} else if (!isValidScheme(uri.scheme)) {
uri = getUriFromAssetId(context, uriString)
if (uri == null) {
// cannot find identifier of content
DebugLog.d(TAG, "cannot find identifier")
return source
}
}
source.uriString = uriString
source.uri = uri
source.startPositionMs = safeGetInt(src, PROP_SRC_START_POSITION, -1)
source.cropStartMs = safeGetInt(src, PROP_SRC_CROP_START, -1)
source.cropEndMs = safeGetInt(src, PROP_SRC_CROP_END, -1)
source.extension = safeGetString(src, PROP_SRC_TYPE, null)

val propSrcHeadersArray = safeGetArray(src, PROP_SRC_HEADERS)
if (propSrcHeadersArray != null) {
if (propSrcHeadersArray.size() > 0) {
for (i in 0 until propSrcHeadersArray.size()) {
val current = propSrcHeadersArray.getMap(i)
val key = if (current.hasKey("key")) current.getString("key") else null
val value = if (current.hasKey("value")) current.getString("value") else null
if (key != null && value != null) {
source.headers[key] = value
}
}
}
}
source.metadata = Metadata.parse(safeGetMap(src, PROP_SRC_METADATA))
}
return source
}

/** return true if rui scheme is supported for android playback */
private fun isValidScheme(scheme: String?): Boolean {
if (scheme == null) {
return false
}
val lowerCaseUri = scheme.lowercase(Locale.getDefault())
return (
lowerCaseUri == "http" ||
lowerCaseUri == "https" ||
lowerCaseUri == "content" ||
lowerCaseUri == "file" ||
lowerCaseUri == "rtsp" ||
lowerCaseUri == "asset"
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.brentvatne.exoplayer

import androidx.media3.common.MediaItem.LiveConfiguration
import androidx.media3.common.MediaMetadata
import com.brentvatne.common.api.BufferConfig
import com.brentvatne.common.api.BufferConfig.Live
import com.brentvatne.common.api.Source

/**
* Helper functions to create exoplayer configuration
Expand Down Expand Up @@ -33,4 +35,22 @@ object ConfigurationUtils {
}
return liveConfiguration
}

/**
* Generate exoplayer MediaMetadata from source.Metadata
*/
@JvmStatic
fun buildCustomMetadata(metadata: Source.Metadata?): MediaMetadata? {
var customMetadata: MediaMetadata? = null
if (metadata != null) {
customMetadata = MediaMetadata.Builder()
.setTitle(metadata.title)
.setSubtitle(metadata.subtitle)
.setDescription(metadata.description)
.setArtist(metadata.artist)
.setArtworkUri(metadata.imageUri)
.build()
}
return customMetadata
}
}
Loading

0 comments on commit bdf3e55

Please sign in to comment.