Skip to content

Commit

Permalink
Merge pull request #334 from THEOplayer/feature/ssai_custom_integration
Browse files Browse the repository at this point in the history
Feature/ssai custom integration
  • Loading branch information
tvanlaerhoven authored Jun 18, 2024
2 parents 95dce09 + 1efb5a0 commit c178488
Show file tree
Hide file tree
Showing 19 changed files with 133 additions and 396 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Fixed

- Fixed an issue where the Cast API wouldn't be initialized yet when in the `onPlayerReady` callback.
- Added support for THEOplayer Android v7.6.0.

## [7.4.0] - 24-06-11

Expand Down
9 changes: 5 additions & 4 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,14 @@ dependencies {
implementation "androidx.appcompat:appcompat:${safeExtGet('appcompatVersion', '1.6.1')}"
implementation "androidx.core:core-ktx:${safeExtGet('corektxVersion', '1.10.1')}"

// The minimum supported THEOplayer version is 7.0.0
def theoplayer_sdk_version = safeExtGet('THEOplayer_sdk', '[7.0.0, 8.0.0)')
// The minimum supported THEOplayer version is 7.6.0
def theoplayer_sdk_version = safeExtGet('THEOplayer_sdk', '[7.6.0, 8.0.0)')
def theoplayer_mediasession_version = safeExtGet('THEOplayer_mediasession', '[7.5.0, 8.0.0)')

println("Using THEOplayer (${versionString(theoplayer_sdk_version)})")
implementation "com.theoplayer.theoplayer-sdk-android:core:${theoplayer_sdk_version}"
implementation "com.theoplayer.theoplayer-sdk-android:ads-wrapper:7.0.0"
implementation "com.theoplayer.android-connector:mediasession:${theoplayer_sdk_version}"
implementation "com.theoplayer.theoplayer-sdk-android:ads-wrapper:7.6.0"
implementation "com.theoplayer.android-connector:mediasession:${theoplayer_mediasession_version}"

if (enabledGoogleIMA) {
println('Enable THEOplayer IMA extension.')
Expand Down
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>7.0.0</modelVersion>
<modelVersion>7.6.0</modelVersion>
<groupId>com.theoplayer.theoplayer-sdk-android</groupId>
<artifactId>ads-wrapper</artifactId>
<version>7.0.0</version>
<version>7.6.0</version>
<packaging>aar</packaging>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
<groupId>com.theoplayer.theoplayer-sdk-android</groupId>
<artifactId>ads-wrapper</artifactId>
<versioning>
<latest>7.0.0</latest>
<release>7.0.0</release>
<latest>7.6.0</latest>
<release>7.6.0</release>
<versions>
<version>7.0.0</version>
<version>7.6.0</version>
</versions>
<lastUpdated>20240425153844</lastUpdated>
<lastUpdated>20240618230000</lastUpdated>
</versioning>
</metadata>
9 changes: 9 additions & 0 deletions android/src/main/java/com/theoplayer/ads/AdAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import java.lang.Exception

private const val PROP_AD_SYSTEM = "adSystem"
private const val PROP_AD_INTEGRATION = "integration"
private const val PROP_AD_CUSTOM_INTEGRATION = "customIntegration"
private const val PROP_AD_TYPE = "type"
private const val PROP_AD_ID = "id"
private const val PROP_AD_BREAK = "adBreak"
Expand Down Expand Up @@ -208,6 +209,10 @@ object AdAdapter {
return AdIntegrationKind.from(ad.getString(PROP_AD_INTEGRATION))
}

override fun getCustomIntegration(): String? {
return ad.getString(PROP_AD_CUSTOM_INTEGRATION)
}

override fun getImaAd(): com.google.ads.interactivemedia.v3.api.Ad {
return parseImaAd(ad)
}
Expand Down Expand Up @@ -279,6 +284,10 @@ object AdAdapter {
override fun getIntegration(): AdIntegrationKind {
return AdIntegrationKind.from(adBreak.getString(PROP_ADBREAK_INTEGRATION))
}

override fun getCustomIntegration(): String? {
return adBreak.getString(PROP_AD_CUSTOM_INTEGRATION)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.theoplayer.source

import com.google.gson.Gson
import com.theoplayer.BuildConfig
import com.theoplayer.android.api.error.ErrorCode
import com.theoplayer.android.api.error.THEOplayerException
import com.theoplayer.android.api.source.GoogleDaiTypedSource
import com.theoplayer.android.api.source.TypedSource
import com.theoplayer.android.api.source.ssai.dai.GoogleDaiLiveConfiguration
import com.theoplayer.android.api.source.ssai.dai.GoogleDaiVodConfiguration
import org.json.JSONObject

private const val PROP_AVAILABILITY_TYPE = "availabilityType"
private const val AVAILABILITY_TYPE_VOD = "vod"
private const val ERROR_DAI_NOT_ENABLED = "Google DAI support not enabled."

@Throws(THEOplayerException::class)
fun googleDaiBuilderFromJson(builder: TypedSource.Builder, json: JSONObject): TypedSource.Builder {
// Check whether the integration was enabled
if (!BuildConfig.EXTENSION_GOOGLE_DAI) {
throw THEOplayerException(ErrorCode.AD_ERROR, ERROR_DAI_NOT_ENABLED)
}
// We need to create a new builder as the player SDK checks for:
// typedSource is GoogleDaiTypedSource
return if (json.optString(PROP_AVAILABILITY_TYPE) == AVAILABILITY_TYPE_VOD) {
GoogleDaiTypedSource.Builder(
Gson().fromJson(json.toString(), GoogleDaiVodConfiguration::class.java)
)
} else {
GoogleDaiTypedSource.Builder(
Gson().fromJson(json.toString(), GoogleDaiLiveConfiguration::class.java)
)
}
}
51 changes: 51 additions & 0 deletions android/src/main/java/com/theoplayer/source/SSAIAdapterRegistry.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.theoplayer.source

import android.text.TextUtils
import com.theoplayer.android.api.error.ErrorCode
import com.theoplayer.android.api.error.THEOplayerException
import com.theoplayer.android.api.source.SourceType
import com.theoplayer.android.api.source.TypedSource
import org.json.JSONObject

typealias CustomSSAIAdapter = (json: JSONObject, currentBuilder: TypedSource.Builder) -> TypedSource.Builder

private const val ERROR_UNSUPPORTED_SSAI_INTEGRATION = "Unsupported SSAI integration"
private const val ERROR_MISSING_SSAI_INTEGRATION = "Missing SSAI integration"
private const val PROP_INTEGRATION = "integration"

object SSAIAdapterRegistry {
private val _adapters: MutableMap<String, CustomSSAIAdapter> = HashMap()

fun register(integration: String, adapter: CustomSSAIAdapter) {
_adapters[integration] = adapter
}

fun hasIntegration(integration: String): Boolean {
return _adapters[integration] != null
}

fun typedSourceBuilderFromJson(json: JSONObject, currentBuilder: TypedSource.Builder, sourceType: SourceType?): TypedSource.Builder {
// Check for valid SsaiIntegration
val ssaiIntegrationStr = json.optString(PROP_INTEGRATION)

// Check for valid `integration` property, which is mandatory.
if (TextUtils.isEmpty(ssaiIntegrationStr)) {
throw THEOplayerException(ErrorCode.AD_ERROR, ERROR_MISSING_SSAI_INTEGRATION)
}

// Check for known SsaiIntegration
if (!hasIntegration(ssaiIntegrationStr)) {
throw THEOplayerException(
ErrorCode.AD_ERROR,
"$ERROR_UNSUPPORTED_SSAI_INTEGRATION: $ssaiIntegrationStr"
)
}

// Prefer DASH if SSAI type not specified
if (sourceType == null) {
currentBuilder.type(SourceType.DASH)
}

return _adapters[ssaiIntegrationStr]?.invoke(json, currentBuilder) ?: currentBuilder
}
}
81 changes: 23 additions & 58 deletions android/src/main/java/com/theoplayer/source/SourceAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,16 @@ package com.theoplayer.source

import android.text.TextUtils
import android.util.Log
import com.facebook.react.bridge.Arguments
import com.google.gson.Gson
import com.theoplayer.android.api.error.THEOplayerException
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.WritableMap
import com.theoplayer.android.api.source.SourceDescription
import com.theoplayer.android.api.source.TypedSource
import com.theoplayer.android.api.source.metadata.MetadataDescription
import com.theoplayer.android.api.source.addescription.AdDescription
import com.theoplayer.android.api.source.TextTrackDescription
import com.theoplayer.android.api.source.SourceType
import com.theoplayer.android.api.source.ssai.SsaiIntegration
import com.theoplayer.android.api.source.GoogleDaiTypedSource
import com.theoplayer.android.api.source.ssai.dai.GoogleDaiVodConfiguration
import com.theoplayer.android.api.source.ssai.dai.GoogleDaiLiveConfiguration
import com.theoplayer.android.api.source.hls.HlsPlaybackConfiguration
import com.theoplayer.android.api.event.ads.AdIntegrationKind
import com.theoplayer.android.api.source.addescription.GoogleImaAdDescription
Expand Down Expand Up @@ -55,19 +49,26 @@ private const val PROP_INTEGRATION = "integration"
private const val PROP_TEXT_TRACKS = "textTracks"
private const val PROP_POSTER = "poster"
private const val PROP_ADS = "ads"
private const val PROP_AVAILABILITY_TYPE = "availabilityType"
private const val PROP_DASH = "dash"
private const val PROP_DASH_IGNORE_AVAILABILITYWINDOW = "ignoreAvailabilityWindow"
private const val ERROR_DAI_NOT_ENABLED = "Google DAI support not enabled."
private const val ERROR_UNSUPPORTED_SSAI_INTEGRATION = "Unsupported SSAI integration"
private const val ERROR_MISSING_SSAI_INTEGRATION = "Missing SSAI integration"
private const val ERROR_IMA_NOT_ENABLED = "Google IMA support not enabled."
private const val ERROR_UNSUPPORTED_CSAI_INTEGRATION = "Unsupported CSAI integration"
private const val ERROR_MISSING_CSAI_INTEGRATION = "Missing CSAI integration"

private const val PROP_SSAI_INTEGRATION_GOOGLE_DAI = "google-dai"

class SourceAdapter {
private val gson = Gson()

companion object {
init {
// Register default SSAI adapter for Google DAI.
SSAIAdapterRegistry.register(PROP_SSAI_INTEGRATION_GOOGLE_DAI) { json, currentBuilder ->
googleDaiBuilderFromJson(currentBuilder, json)
}
}
}

@Throws(THEOplayerException::class)
fun parseSourceFromJS(source: ReadableMap?): SourceDescription? {
if (source == null) {
Expand All @@ -83,17 +84,11 @@ class SourceAdapter {
val jsonSources = jsonSourceObject.optJSONArray(PROP_SOURCES)
if (jsonSources != null) {
for (i in 0 until jsonSources.length()) {
val typedSource = parseTypedSource(jsonSources[i] as JSONObject)
if (typedSource != null) {
typedSources.add(typedSource)
}
typedSources.add(parseTypedSource(jsonSources[i] as JSONObject))
}
} else {
val jsonSource = jsonSourceObject.optJSONObject(PROP_SOURCES) ?: return null
val typedSource = parseTypedSource(jsonSource)
if (typedSource != null) {
typedSources.add(typedSource)
}
typedSources.add(parseTypedSource(jsonSource))
}

// poster
Expand Down Expand Up @@ -142,48 +137,13 @@ class SourceAdapter {
}

@Throws(THEOplayerException::class)
private fun parseTypedSource(jsonTypedSource: JSONObject): TypedSource? {
private fun parseTypedSource(jsonTypedSource: JSONObject): TypedSource {
try {
var tsBuilder = TypedSource.Builder(jsonTypedSource.optString(PROP_SRC))
val sourceType = parseSourceType(jsonTypedSource)
if (jsonTypedSource.has(PROP_SSAI)) {
val ssaiJson = jsonTypedSource.getJSONObject(PROP_SSAI)

// Check for valid SsaiIntegration
val ssaiIntegrationStr = ssaiJson.optString(PROP_INTEGRATION)
if (!TextUtils.isEmpty(ssaiIntegrationStr)) {
val ssaiIntegration = SsaiIntegration.from(ssaiIntegrationStr)
?: throw THEOplayerException(
ErrorCode.AD_ERROR,
"$ERROR_UNSUPPORTED_SSAI_INTEGRATION: $ssaiIntegrationStr"
)
when (ssaiIntegration) {
SsaiIntegration.GOOGLE_DAI -> {
if (!BuildConfig.EXTENSION_GOOGLE_DAI) {
throw THEOplayerException(ErrorCode.AD_ERROR, ERROR_DAI_NOT_ENABLED)
}
tsBuilder = if (ssaiJson.optString(PROP_AVAILABILITY_TYPE) == "vod") {
GoogleDaiTypedSource.Builder(
gson.fromJson(ssaiJson.toString(), GoogleDaiVodConfiguration::class.java)
)
} else {
GoogleDaiTypedSource.Builder(
gson.fromJson(ssaiJson.toString(), GoogleDaiLiveConfiguration::class.java)
)
}
// Prefer DASH if not SSAI type specified
if (sourceType == null) {
tsBuilder.type(SourceType.DASH)
}
}
else -> throw THEOplayerException(
ErrorCode.AD_ERROR,
"$ERROR_UNSUPPORTED_SSAI_INTEGRATION: $ssaiIntegrationStr"
)
}
} else {
throw THEOplayerException(ErrorCode.AD_ERROR, ERROR_MISSING_SSAI_INTEGRATION)
}
tsBuilder = SSAIAdapterRegistry.typedSourceBuilderFromJson(ssaiJson, tsBuilder, sourceType)
}
if (sourceType != null) {
tsBuilder.type(sourceType)
Expand Down Expand Up @@ -216,10 +176,13 @@ class SourceAdapter {
}
}
return tsBuilder.build()
} catch (e: JSONException) {
e.printStackTrace()
} catch (e: THEOplayerException) {
// Rethrow THEOplayerException
throw e
} catch (e: Exception) {
// Wrap exception
throw THEOplayerException(ErrorCode.SOURCE_INVALID, "Invalid source: ${e.message}")
}
return null
}

@Throws(THEOplayerException::class)
Expand Down Expand Up @@ -281,12 +244,14 @@ class SourceAdapter {
AdIntegrationKind.GOOGLE_IMA -> parseImaAdFromJS(
jsonAdDescription
)

AdIntegrationKind.DEFAULT -> {
throw THEOplayerException(
ErrorCode.AD_ERROR,
"$ERROR_UNSUPPORTED_CSAI_INTEGRATION: $integrationKindStr"
)
}

else -> {
throw THEOplayerException(
ErrorCode.AD_ERROR,
Expand Down
2 changes: 1 addition & 1 deletion example/android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ newArchEnabled=false
hermesEnabled=true

# Version of the THEOplayer SDK, if not specified, the latest available version within bounds is set.
#THEOplayer_sdk=[7.0.0, 8.0.0)
#THEOplayer_sdk=[7.6.0, 8.0.0)

# Override Android sdk versions
#THEOplayer_compileSdkVersion = 34
Expand Down
Loading

0 comments on commit c178488

Please sign in to comment.