Skip to content
Closed
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Fixed

- Fixed an issue where re-registering a content protection integration for the same key system kept the previously registered factory active, causing stale DRM integrations to be used for subsequent sources.
- Fixed `liveOffset` in `PlayerConfiguration` not being applied on Android; it is now used as the default live offset for sources that do not specify their own `liveOffset`.
- Fixed `sessionID` in the `ads.ima` player configuration being ignored on Android.

## [11.7.0] - 18-09-26

Expand Down
147 changes: 60 additions & 87 deletions android/src/main/java/com/theoplayer/PlayerConfigAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.theoplayer.media.MediaSessionConfigAdapter

private const val PROP_LICENSE = "license"
private const val PROP_LICENSE_URL = "licenseUrl"
private const val PROP_LIVE_OFFSET = "liveOffset"
private const val PROP_PRELOAD = "preload"
private const val PROP_UI_ENABLED = "uiEnabled"
private const val PROP_CAST_STRATEGY = "strategy"
Expand Down Expand Up @@ -59,18 +60,18 @@ private const val PROP_CMCD_ENDPOINT_URL = "url"
class PlayerConfigAdapter(private val configProps: ReadableMap?) {

/** Returns whether the player facade is enabled at creation. Missing or null values default to false. */
fun usePlayerFacade(): Boolean = configProps?.let {
it.hasKey(PROP_USE_PLAYER_FACADE) && !it.isNull(PROP_USE_PLAYER_FACADE) && it.getBoolean(PROP_USE_PLAYER_FACADE)
} ?: false
fun usePlayerFacade(): Boolean = configProps?.getBooleanOrNull(PROP_USE_PLAYER_FACADE) ?: false

/**
* Whether debug logs from the native SDK should be enabled.
*/
fun debugLogsEnabled(): Boolean {
return configProps?.let {
if (it.hasKey(PROP_DEBUG_LOGS_ENABLED)) it.getBoolean(PROP_DEBUG_LOGS_ENABLED) else false
} ?: false
}
fun debugLogsEnabled(): Boolean = configProps?.getBooleanOrNull(PROP_DEBUG_LOGS_ENABLED) ?: false

/**
* The offset in seconds used to determine the live point, or null when not configured.
* The value is applied as the default live offset for sources that do not specify their own.
*/
fun liveOffset(): Double? = configProps?.getDoubleOrNull(PROP_LIVE_OFFSET)

/**
* Get general THEOplayerConfig object; these properties apply:
Expand All @@ -80,11 +81,14 @@ class PlayerConfigAdapter(private val configProps: ReadableMap?) {
*/
fun playerConfig(): THEOplayerConfig {
return THEOplayerConfig.Builder().apply {
pip(PipConfiguration.Builder().build())
// Opt-out for auto-integrations for now
autoIntegrations(false)
configProps?.run {
getString(PROP_LICENSE)?.let { license ->
getStringOrNull(PROP_LICENSE)?.let { license ->
license(license)
}
getString(PROP_LICENSE_URL)?.let { licenseUrl ->
getStringOrNull(PROP_LICENSE_URL)?.let { licenseUrl ->
licenseUrl(licenseUrl)
}
if (hasKey(PROP_RETRY_CONFIG) || hasKey(PROP_USE_HTTPENGINE)) {
Expand All @@ -93,18 +97,9 @@ class PlayerConfigAdapter(private val configProps: ReadableMap?) {
if (hasKey(PROP_THEOLIVE_CONFIG)) {
theoLive(theoLiveConfig())
}
pip(PipConfiguration.Builder().build())
// Opt-out for auto-integrations for now
autoIntegrations(false)
if (hasKey(PROP_HLS_DATERANGE)) {
hlsDateRange(getBoolean(PROP_HLS_DATERANGE))
}
if (hasKey(PROP_MULTIMEDIA_TUNNELING_ENABLED)) {
tunnelingEnabled(getBoolean(PROP_MULTIMEDIA_TUNNELING_ENABLED))
}
if (hasKey(PROP_SYSTEM_CAPTION_STYLE)) {
useSystemCaptionStyle(getBoolean(PROP_SYSTEM_CAPTION_STYLE))
}
getBooleanOrNull(PROP_HLS_DATERANGE)?.let { hlsDateRange(it) }
getBooleanOrNull(PROP_MULTIMEDIA_TUNNELING_ENABLED)?.let { tunnelingEnabled(it) }
getBooleanOrNull(PROP_SYSTEM_CAPTION_STYLE)?.let { useSystemCaptionStyle(it) }
if (hasKey(PROP_CMCD)) {
cmcd(cmcdConfig())
}
Expand All @@ -122,21 +117,11 @@ class PlayerConfigAdapter(private val configProps: ReadableMap?) {
private fun networkConfig(): NetworkConfiguration {
return NetworkConfiguration.Builder().apply {
configProps?.getMap(PROP_RETRY_CONFIG)?.run {
if (hasKey(PROP_RETRY_MAX_RETRIES)) {
maxRetries(getInt(PROP_RETRY_MAX_RETRIES))
}
if (hasKey(PROP_RETRY_MIN_BACKOFF)) {
minimumBackOff(getDouble(PROP_RETRY_MIN_BACKOFF).toLong())
}
if (hasKey(PROP_RETRY_MAX_BACKOFF)) {
maximumBackOff(getDouble(PROP_RETRY_MAX_BACKOFF).toLong())
}
}
configProps?.run {
if (hasKey(PROP_USE_HTTPENGINE)) {
useHttpEngine(getBoolean(PROP_USE_HTTPENGINE))
}
getIntOrNull(PROP_RETRY_MAX_RETRIES)?.let { maxRetries(it) }
getDoubleOrNull(PROP_RETRY_MIN_BACKOFF)?.let { minimumBackOff(it.toLong()) }
getDoubleOrNull(PROP_RETRY_MAX_BACKOFF)?.let { maximumBackOff(it.toLong()) }
}
configProps?.getBooleanOrNull(PROP_USE_HTTPENGINE)?.let { useHttpEngine(it) }
}.build()
}

Expand All @@ -147,40 +132,26 @@ class PlayerConfigAdapter(private val configProps: ReadableMap?) {
*/
fun imaSdkSettings(): ImaSdkSettings {
return ImaSdkFactory.getInstance().createImaSdkSettings().apply {
// The partner provided player type.
setPlayerType("THEOplayer")
// The partner provided player version.
setPlayerVersion(THEOplayerGlobal.getVersion())
configProps?.getMap(PROP_ADS_CONFIGURATION)?.getMap(PROP_IMA_CONFIGURATION)?.run {
// Specifies whether VMAP and ad rules ad breaks are automatically played.
if (hasKey(PROP_AUTOPLAY_AD_BREAKS)) {
autoPlayAdBreaks = getBoolean(PROP_AUTOPLAY_AD_BREAKS)
}
getBooleanOrNull(PROP_AUTOPLAY_AD_BREAKS)?.let { autoPlayAdBreaks = it }
// Feature flags and their states. Used to control experimental features.
if (hasKey(PROP_FEATURE_FLAGS)) {
val convertedMap: MutableMap<String, String> = mutableMapOf()
getMap(PROP_FEATURE_FLAGS)?.toHashMap()?.forEach { (key, value) ->
convertedMap[key] = value as String
}
setFeatureFlags(convertedMap)
getMap(PROP_FEATURE_FLAGS)?.toHashMap()?.let { flags ->
setFeatureFlags(flags.mapValues { (_, value) -> value as String })
}
// The maximum number of VAST redirects.
if (hasKey(PROP_MAX_REDIRECTS)) {
maxRedirects = getInt(PROP_MAX_REDIRECTS)
}
// The partner provided player type.
setPlayerType("THEOplayer")
// The partner provided player version.
setPlayerVersion(THEOplayerGlobal.getVersion())
getIntOrNull(PROP_MAX_REDIRECTS)?.let { maxRedirects = it }
// The Publisher Provided Identification (PPID) sent with ads request.
if (hasKey(PROP_PPID)) {
setPpid(getString(PROP_PPID) ?: "")
}
getStringOrNull(PROP_PPID)?.let { setPpid(it) }
// The session ID to identify a single user session. This should be a UUID. It
// is used exclusively for frequency capping across the user session.
if (hasKey(PROP_SESSION_ID)) {
setSessionId(getString(PROP_PPID) ?: "")
}
getStringOrNull(PROP_SESSION_ID)?.let { setSessionId(it) }
// Toggles debug mode which will output detailed log information to the console.
if (hasKey(PROP_ENABLE_DEBUG_MODE)) {
isDebugMode = getBoolean(PROP_ENABLE_DEBUG_MODE)
}
getBooleanOrNull(PROP_ENABLE_DEBUG_MODE)?.let { isDebugMode = it }
}
}
}
Expand All @@ -191,38 +162,30 @@ class PlayerConfigAdapter(private val configProps: ReadableMap?) {
* @see <a href="https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/api/reference/com/google/ads/interactivemedia/v3/api/AdsRenderingSettings">IMA SDK for Android</a>.
*/
fun adsRenderSettings(): AdsRenderingSettings {
val adsProps = configProps?.getMap(PROP_ADS_CONFIGURATION)
return ImaSdkFactory.getInstance().createAdsRenderingSettings().apply {
configProps?.getMap(PROP_ADS_CONFIGURATION)?.run {
if (hasKey(PROP_UI_ENABLED) && !getBoolean(PROP_UI_ENABLED)) {
adsProps?.run {
if (getBooleanOrNull(PROP_UI_ENABLED) == false) {
setUiElements(emptySet())
disableUi = true
}
if (hasKey(PROP_PRELOAD)) {
val preloadTypeString = getString(PROP_PRELOAD)
enablePreloading = preloadTypeString !== "none"
getStringOrNull(PROP_PRELOAD)?.let { preloadType ->
enablePreloading = preloadType != "none"
}
if (hasKey(PROP_ALLOWED_MIMETYPES)) {
setMimeTypes(ArrayList<String>().apply {
getArray(PROP_ALLOWED_MIMETYPES)?.toArrayList()?.forEach {
add(it as String)
}
})
getArray(PROP_ALLOWED_MIMETYPES)?.toArrayList()?.let { mimeTypes ->
setMimeTypes(ArrayList(mimeTypes.filterIsInstance<String>()))
}
}
// bitrate and timeout are configured under the ima config
configProps?.getMap(PROP_ADS_CONFIGURATION)?.getMap(PROP_IMA_CONFIGURATION)?.run {
if (hasKey(PROP_BITRATE)) {
bitrateKbps = getInt(PROP_BITRATE)
}
adsProps?.getMap(PROP_IMA_CONFIGURATION)?.run {
getIntOrNull(PROP_BITRATE)?.let { bitrateKbps = it }

// The time needs to be in milliseconds on android but seconds on ios.
// we unify the prop from javascript by multiplying it by 1000 here
if (hasKey(PROP_IMA_AD_LOAD_TIMEOUT)) {
setLoadVideoTimeout(getInt(PROP_IMA_AD_LOAD_TIMEOUT) * 1000)
}
getIntOrNull(PROP_IMA_AD_LOAD_TIMEOUT)?.let { setLoadVideoTimeout(it * 1000) }

if (hasKey(PROP_IMA_FOCUS_SKIP_BUTTON_WHEN_AVAILABLE)) {
focusSkipButtonWhenAvailable = getBoolean(PROP_IMA_FOCUS_SKIP_BUTTON_WHEN_AVAILABLE)
getBooleanOrNull(PROP_IMA_FOCUS_SKIP_BUTTON_WHEN_AVAILABLE)?.let {
focusSkipButtonWhenAvailable = it
}
}
}
Expand Down Expand Up @@ -277,14 +240,24 @@ class PlayerConfigAdapter(private val configProps: ReadableMap?) {
)
}

private fun theoLiveConfig (): THEOLiveConfig {
private fun theoLiveConfig(): THEOLiveConfig {
val config = configProps?.getMap(PROP_THEOLIVE_CONFIG)
return THEOLiveConfig.Builder(
externalSessionId = config?.getString(PROP_THEOLIVE_EXTERNAL_SESSION_ID),
analyticsDisabled = if (config?.hasKey(PROP_THEOLIVE_ANALYTICS_DISABLED) == true)
config.getBoolean(PROP_THEOLIVE_ANALYTICS_DISABLED)
else false,
discoveryUrl = config?.getString(PROP_THEOLIVE_DISCOVERY_URL)
externalSessionId = config?.getStringOrNull(PROP_THEOLIVE_EXTERNAL_SESSION_ID),
analyticsDisabled = config?.getBooleanOrNull(PROP_THEOLIVE_ANALYTICS_DISABLED) ?: false,
discoveryUrl = config?.getStringOrNull(PROP_THEOLIVE_DISCOVERY_URL)
).build()
}

private fun ReadableMap.getBooleanOrNull(key: String): Boolean? =
if (hasKey(key) && !isNull(key)) getBoolean(key) else null

private fun ReadableMap.getIntOrNull(key: String): Int? =
if (hasKey(key) && !isNull(key)) getInt(key) else null

private fun ReadableMap.getDoubleOrNull(key: String): Double? =
if (hasKey(key) && !isNull(key)) getDouble(key) else null

private fun ReadableMap.getStringOrNull(key: String): String? =
if (hasKey(key) && !isNull(key)) getString(key) else null
}
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ class ReactTHEOplayerView(private val reactContext: ThemedReactContext) :

fun setSource(source: ReadableMap?) {
try {
val sourceDescription = SourceAdapter().parseSourceFromJS(source)
val sourceDescription = SourceAdapter(config?.liveOffset()).parseSourceFromJS(source)
adBridge.setSource(sourceDescription)
player?.source = sourceDescription
} catch (exception: THEOplayerException) {
Expand Down
13 changes: 10 additions & 3 deletions android/src/main/java/com/theoplayer/source/SourceAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ private const val CMCD_USER_ID = "userId"
private const val CMCD_EVENT_ENDPOINTS = "eventEndpoints"
private const val CMCD_ENDPOINT_URL = "url"

class SourceAdapter {
/**
* @param defaultLiveOffset The player-level live offset in seconds, used for sources that do not specify their own `liveOffset`.
*/
class SourceAdapter(private val defaultLiveOffset: Double? = null) {
private val gson = Gson()

companion object {
Expand Down Expand Up @@ -257,9 +260,13 @@ class SourceAdapter {
jsonTypedSource.optJSONObject(PROP_LATENCY_CONFIGURATION)?.let {
tsBuilder.latencyConfiguration(parseLatencyConfiguration(it))
}
if (jsonTypedSource.has(PROP_LIVE_OFFSET)) {
tsBuilder.liveOffset(jsonTypedSource.getDouble(PROP_LIVE_OFFSET))
// A source-level liveOffset takes precedence over the player-level default.
val liveOffset = if (jsonTypedSource.has(PROP_LIVE_OFFSET) && !jsonTypedSource.isNull(PROP_LIVE_OFFSET)) {
jsonTypedSource.getDouble(PROP_LIVE_OFFSET)
} else {
defaultLiveOffset
}
liveOffset?.let { tsBuilder.liveOffset(it) }
Comment thread
tvanlaerhoven marked this conversation as resolved.
if (jsonTypedSource.has(PROP_HLS_DATERANGE)) {
tsBuilder.hlsDateRange(jsonTypedSource.getBoolean(PROP_HLS_DATERANGE))
}
Expand Down
Loading