diff --git a/android/gradle.properties b/android/gradle.properties index acec563d..d29fa1a5 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -4,7 +4,7 @@ RNVideo_targetSdkVersion=35 RNVideo_compileSdkVersion=35 RNVideo_ndkversion=27.1.12297006 RNVideo_buildToolsVersion=35.0.0 -RNVideo_media3Version=1.4.1 +RNVideo_media3Version=1.8.0 RNVideo_useExoplayerIMA=false RNVideo_useExoplayerRtsp=false RNVideo_useExoplayerSmoothStreaming=true diff --git a/android/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java b/android/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java new file mode 100644 index 00000000..3ba01453 --- /dev/null +++ b/android/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionMediaSource.java @@ -0,0 +1,65 @@ +package androidx.media3.exoplayer.ima; + +import android.content.Context; +import android.view.View; + +import androidx.annotation.Nullable; +import androidx.media3.common.MediaItem; +import androidx.media3.common.Player; +import androidx.media3.exoplayer.drm.DrmSessionManagerProvider; +import androidx.media3.exoplayer.source.MediaSource; +import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; + +public class ImaServerSideAdInsertionMediaSource { + + public static class AdsLoader { + public void setPlayer(@Nullable Player player) { + } + + public void release() { + } + + public static class Builder { + public Builder(Context context, View playerView) { + } + + public Builder setAdEventListener(Object listener) { + return this; + } + + public Builder setAdErrorListener(Object listener) { + return this; + } + + public AdsLoader build() { + return new AdsLoader(); + } + } + } + + public static class Factory implements MediaSource.Factory { + public Factory(AdsLoader adsLoader, MediaSource.Factory mediaSourceFactory) { + } + + @Override + public MediaSource.Factory setDrmSessionManagerProvider(DrmSessionManagerProvider drmSessionManagerProvider) { + return this; + } + + @Override + public MediaSource.Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { + return this; + } + + @Override + public int[] getSupportedTypes() { + return new int[0]; + } + + @Override + public MediaSource createMediaSource(MediaItem mediaItem) { + return null; + } + } +} + diff --git a/android/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionUriBuilder.java b/android/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionUriBuilder.java new file mode 100644 index 00000000..7647d9e7 --- /dev/null +++ b/android/src/main/java/androidx/media3/exoplayer/ima/ImaServerSideAdInsertionUriBuilder.java @@ -0,0 +1,26 @@ +package androidx.media3.exoplayer.ima; + +import android.net.Uri; + +public class ImaServerSideAdInsertionUriBuilder { + public ImaServerSideAdInsertionUriBuilder setAssetKey(String assetKey) { + return this; + } + + public ImaServerSideAdInsertionUriBuilder setContentSourceId(String contentSourceId) { + return this; + } + + public ImaServerSideAdInsertionUriBuilder setVideoId(String videoId) { + return this; + } + + public ImaServerSideAdInsertionUriBuilder setFormat(int format) { + return this; + } + + public Uri build() { + return Uri.EMPTY; + } +} + diff --git a/android/src/main/java/com/brentvatne/common/api/AdsProps.kt b/android/src/main/java/com/brentvatne/common/api/AdsProps.kt index 2f5a057c..6cb5fb32 100644 --- a/android/src/main/java/com/brentvatne/common/api/AdsProps.kt +++ b/android/src/main/java/com/brentvatne/common/api/AdsProps.kt @@ -4,38 +4,98 @@ import android.net.Uri import android.text.TextUtils import com.brentvatne.common.toolbox.ReactBridgeUtils import com.facebook.react.bridge.ReadableMap +import java.util.Objects class AdsProps { + var type: String? = null + var streamType: String? = null var adTagUrl: Uri? = null var adLanguage: String? = null + var contentSourceId: String? = null + var videoId: String? = null + var assetKey: String? = null + var format: String? = null + var adTagParameters: Map? = null + var fallbackUri: String? = null + + fun isCSAI(): Boolean = type == "csai" && adTagUrl != null + fun isDAI(): Boolean = type == "ssai" + fun isDAIVod(): Boolean = type == "ssai" && streamType == "vod" + fun isDAILive(): Boolean = type == "ssai" && streamType == "live" - /** return true if this and src are equals */ override fun equals(other: Any?): Boolean { if (other == null || other !is AdsProps) return false return ( - adTagUrl == other.adTagUrl && - adLanguage == other.adLanguage + type == other.type && + streamType == other.streamType && + adTagUrl == other.adTagUrl && + adLanguage == other.adLanguage && + contentSourceId == other.contentSourceId && + videoId == other.videoId && + assetKey == other.assetKey && + format == other.format && + adTagParameters == other.adTagParameters && + fallbackUri == other.fallbackUri ) } + override fun hashCode(): Int = + Objects.hash( + type, streamType, adTagUrl, adLanguage, contentSourceId, videoId, assetKey, format, adTagParameters, fallbackUri + ) + companion object { + private const val PROP_TYPE = "type" + private const val PROP_STREAM_TYPE = "streamType" private const val PROP_AD_TAG_URL = "adTagUrl" private const val PROP_AD_LANGUAGE = "adLanguage" + private const val PROP_CONTENT_SOURCE_ID = "contentSourceId" + private const val PROP_VIDEO_ID = "videoId" + private const val PROP_ASSET_KEY = "assetKey" + private const val PROP_FORMAT = "format" + private const val PROP_AD_TAG_PARAMETERS = "adTagParameters" + private const val PROP_FALLBACK_URI = "fallbackUri" @JvmStatic fun parse(src: ReadableMap?): AdsProps { val adsProps = AdsProps() if (src != null) { + adsProps.type = ReactBridgeUtils.safeGetString(src, PROP_TYPE) + adsProps.streamType = ReactBridgeUtils.safeGetString(src, PROP_STREAM_TYPE) + val uriString = ReactBridgeUtils.safeGetString(src, PROP_AD_TAG_URL) - if (TextUtils.isEmpty(uriString)) { - adsProps.adTagUrl = null - } else { + if (!TextUtils.isEmpty(uriString)) { adsProps.adTagUrl = Uri.parse(uriString) } + val languageString = ReactBridgeUtils.safeGetString(src, PROP_AD_LANGUAGE) if (!TextUtils.isEmpty(languageString)) { adsProps.adLanguage = languageString } + + adsProps.contentSourceId = ReactBridgeUtils.safeGetString(src, PROP_CONTENT_SOURCE_ID) + adsProps.videoId = ReactBridgeUtils.safeGetString(src, PROP_VIDEO_ID) + adsProps.assetKey = ReactBridgeUtils.safeGetString(src, PROP_ASSET_KEY) + adsProps.format = ReactBridgeUtils.safeGetString(src, PROP_FORMAT) + adsProps.fallbackUri = ReactBridgeUtils.safeGetString(src, PROP_FALLBACK_URI) + + if (src.hasKey(PROP_AD_TAG_PARAMETERS)) { + val adTagParamsMap = src.getMap(PROP_AD_TAG_PARAMETERS) + if (adTagParamsMap != null) { + val params = mutableMapOf() + val iterator = adTagParamsMap.keySetIterator() + while (iterator.hasNextKey()) { + val key = iterator.nextKey() + val value = adTagParamsMap.getString(key) + if (value != null) { + params[key] = value + } + } + if (params.isNotEmpty()) { + adsProps.adTagParameters = params + } + } + } } return adsProps } diff --git a/android/src/main/java/com/brentvatne/common/api/Source.kt b/android/src/main/java/com/brentvatne/common/api/Source.kt index ef807e54..e9cf5abc 100644 --- a/android/src/main/java/com/brentvatne/common/api/Source.kt +++ b/android/src/main/java/com/brentvatne/common/api/Source.kt @@ -5,7 +5,6 @@ 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.api.DRMProps.Companion.parse import com.brentvatne.common.toolbox.DebugLog import com.brentvatne.common.toolbox.DebugLog.e @@ -90,7 +89,7 @@ class Source { */ var sideLoadedTextTracks: SideLoadedTextTrackList? = null - override fun hashCode(): Int = Objects.hash(uriString, uri, startPositionMs, cropStartMs, cropEndMs, extension, metadata, headers) + override fun hashCode(): Int = Objects.hash(uriString, uri, startPositionMs, cropStartMs, cropEndMs, extension, metadata, headers, adsProps) /** return true if this and src are equals */ override fun equals(other: Any?): Boolean { @@ -212,59 +211,53 @@ class Source { 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.isLocalAssetFile = safeGetBool(src, PROP_SRC_IS_LOCAL_ASSET_FILE, false) - source.isAsset = safeGetBool(src, PROP_SRC_IS_ASSET, false) - 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.contentStartTime = safeGetInt(src, PROP_SRC_CONTENT_START_TIME, -1) - source.extension = safeGetString(src, PROP_SRC_TYPE, null) - source.drmProps = parse(safeGetMap(src, PROP_SRC_DRM)) - source.cmcdProps = CMCDProps.parse(safeGetMap(src, PROP_SRC_CMCD)) - if (BuildConfig.USE_EXOPLAYER_IMA) { - source.adsProps = AdsProps.parse(safeGetMap(src, PROP_SRC_ADS)) - } - source.textTracksAllowChunklessPreparation = safeGetBool(src, PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION, true) - source.sideLoadedTextTracks = SideLoadedTextTrackList.parse(safeGetArray(src, PROP_SRC_TEXT_TRACKS)) - source.minLoadRetryCount = safeGetInt(src, PROP_SRC_MIN_LOAD_RETRY_COUNT, 3) - source.bufferConfig = BufferConfig.parse(safeGetMap(src, PROP_SRC_BUFFER_CONFIG)) + if (src == null) return source - 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 = current?.getString("key") - val value = current?.getString("value") - if (key != null && value != null) { - source.headers[key] = value - } + safeGetString(src, PROP_SRC_URI, null) + ?.takeIf { it.isNotBlank() } + ?.let { uriString -> + var uri = Uri.parse(uriString) + + if (!isValidScheme(uri.scheme)) { + uri = getUriFromAssetId(context, uriString) ?: return source + } + + source.uriString = uriString + source.uri = uri + } + + source.isLocalAssetFile = safeGetBool(src, PROP_SRC_IS_LOCAL_ASSET_FILE, false) + source.isAsset = safeGetBool(src, PROP_SRC_IS_ASSET, false) + 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.contentStartTime = safeGetInt(src, PROP_SRC_CONTENT_START_TIME, -1) + source.extension = safeGetString(src, PROP_SRC_TYPE, null) + source.drmProps = parse(safeGetMap(src, PROP_SRC_DRM)) + source.cmcdProps = CMCDProps.parse(safeGetMap(src, PROP_SRC_CMCD)) + if (BuildConfig.USE_EXOPLAYER_IMA) { + source.adsProps = AdsProps.parse(safeGetMap(src, PROP_SRC_ADS)) + } + source.textTracksAllowChunklessPreparation = safeGetBool(src, PROP_SRC_TEXT_TRACKS_ALLOW_CHUNKLESS_PREPARATION, true) + source.sideLoadedTextTracks = SideLoadedTextTrackList.parse(safeGetArray(src, PROP_SRC_TEXT_TRACKS)) + source.minLoadRetryCount = safeGetInt(src, PROP_SRC_MIN_LOAD_RETRY_COUNT, 3) + source.bufferConfig = BufferConfig.parse(safeGetMap(src, PROP_SRC_BUFFER_CONFIG)) + + 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 = current?.getString("key") + val value = current?.getString("value") + if (key != null && value != null) { + source.headers[key] = value } } } - source.metadata = Metadata.parse(safeGetMap(src, PROP_SRC_METADATA)) } + source.metadata = Metadata.parse(safeGetMap(src, PROP_SRC_METADATA)) + return source } diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index 539ecfd1..e16ac96d 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -55,6 +55,7 @@ import androidx.media3.common.text.CueGroup; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSpec; +import androidx.media3.datasource.DefaultDataSource; import androidx.media3.datasource.HttpDataSource; import androidx.media3.exoplayer.DefaultLoadControl; import androidx.media3.exoplayer.DefaultRenderersFactory; @@ -76,6 +77,8 @@ import androidx.media3.exoplayer.drm.HttpMediaDrmCallback; import androidx.media3.exoplayer.drm.UnsupportedDrmException; import androidx.media3.exoplayer.hls.HlsMediaSource; import androidx.media3.exoplayer.ima.ImaAdsLoader; +import androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource; +import androidx.media3.exoplayer.ima.ImaServerSideAdInsertionUriBuilder; import androidx.media3.exoplayer.mediacodec.MediaCodecInfo; import androidx.media3.exoplayer.mediacodec.MediaCodecUtil; import androidx.media3.exoplayer.rtsp.RtspMediaSource; @@ -125,9 +128,11 @@ import com.brentvatne.react.ReactNativeVideoManager; import com.brentvatne.receiver.AudioBecomingNoisyReceiver; import com.brentvatne.receiver.BecomingNoisyListener; import com.brentvatne.receiver.PictureInPictureReceiver; +import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.bridge.WritableMap; import com.facebook.react.uimanager.ThemedReactContext; import com.google.ads.interactivemedia.v3.api.AdError; import com.google.ads.interactivemedia.v3.api.AdErrorEvent; @@ -182,6 +187,7 @@ public class ReactExoplayerView extends FrameLayout implements private ExoPlayerView exoPlayerView; private FullScreenPlayerView fullScreenPlayerView; private ImaAdsLoader adsLoader; + private ImaServerSideAdInsertionMediaSource.AdsLoader daiAdsLoader; private DataSource.Factory mediaDataSourceFactory; private ExoPlayer player; @@ -622,7 +628,6 @@ public class ReactExoplayerView extends FrameLayout implements private void initializePlayer() { disableCache = ReactNativeVideoManager.Companion.getInstance().shouldDisableCache(source); - ReactExoplayerView self = this; Activity activity = themedReactContext.getCurrentActivity(); // This ensures all props have been settled, to avoid async racing conditions. @@ -632,7 +637,7 @@ public class ReactExoplayerView extends FrameLayout implements return; } try { - if (runningSource.getUri() == null) { + if (runningSource.getUri() == null && !isDaiRequest(runningSource)) { return; } @@ -731,13 +736,20 @@ public class ReactExoplayerView extends FrameLayout implements .setEnableDecoderFallback(true) .forceEnableMediaCodecAsynchronousQueueing(); - DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory); + DefaultMediaSourceFactory mediaSourceFactory; + + if (isDaiRequest(source)) { + mediaSourceFactory = createDaiMediaSourceFactory(); + } else { + mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory); + + mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView.getPlayerView()); + } + if (useCache && !disableCache) { mediaSourceFactory.setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true))); } - mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView.getPlayerView()); - player = new ExoPlayer.Builder(getContext(), renderersFactory) .setTrackSelector(self.trackSelector) .setBandwidthMeter(bandwidthMeter) @@ -831,6 +843,11 @@ public class ReactExoplayerView extends FrameLayout implements } private void initializePlayerSource(Source runningSource) { + if (isDaiRequest(runningSource)) { + initializeDaiSource(runningSource); + return; + } + if (runningSource.getUri() == null) { return; } @@ -1225,6 +1242,12 @@ public class ReactExoplayerView extends FrameLayout implements adsLoader.release(); adsLoader = null; } + + if (daiAdsLoader != null) { + daiAdsLoader.release(); + daiAdsLoader = null; + } + progressHandler.removeMessages(SHOW_PROGRESS); audioBecomingNoisyReceiver.removeListener(); pictureInPictureReceiver.removeListener(); @@ -2015,7 +2038,7 @@ public class ReactExoplayerView extends FrameLayout implements } public void setSrc(Source source) { - if (source.getUri() != null) { + if (source.getUri() != null || isDaiRequest(source)) { clearResumePosition(); boolean isSourceEqual = source.isEquals(this.source); hasDrmFailed = false; @@ -2741,10 +2764,180 @@ public class ReactExoplayerView extends FrameLayout implements "type", String.valueOf(error.getErrorType()) ); eventEmitter.onReceiveAdEvent.invoke("ERROR", errMap); + + handleDaiBackupStream(); } public void setControlsStyles(ControlsConfig controlsStyles) { controlsConfig = controlsStyles; refreshControlsStyles(); } + + /** + * Checks if the source is a DAI (Dynamic Ad Insertion) request. + * + * A DAI request is identified by either: + * - VOD: both contentSourceId and videoId are present + * - Live: assetKey is present + * + * @param source The source to check + * @return true if the source is a DAI request, false otherwise + */ + private boolean isDaiRequest(Source source) { + if (source == null || source.getAdsProps() == null) { + return false; + } + return source.getAdsProps().isDAI(); + } + + /** + * Creates and configures a server-side ad insertion (SSAI) AdsLoader for DAI. + * + * @return The configured IMA server-side ad insertion AdsLoader + */ + private ImaServerSideAdInsertionMediaSource.AdsLoader createAdsLoader() { + ImaServerSideAdInsertionMediaSource.AdsLoader.Builder adsLoaderBuilder = + new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(getContext(), exoPlayerView.getPlayerView()) + .setAdEventListener(this) + .setAdErrorListener(this); + + return adsLoaderBuilder.build(); + } + + /** + * Creates and configures a media source factory for DAI playback. + * + * @return The configured DefaultMediaSourceFactory with DAI support + */ + private DefaultMediaSourceFactory createDaiMediaSourceFactory() { + daiAdsLoader = createAdsLoader(); + + DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(getContext()); + DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(dataSourceFactory); + + ImaServerSideAdInsertionMediaSource.Factory adsMediaSourceFactory = + new ImaServerSideAdInsertionMediaSource.Factory(daiAdsLoader, mediaSourceFactory); + + mediaSourceFactory.setServerSideAdInsertionMediaSourceFactory(adsMediaSourceFactory); + + return mediaSourceFactory; + } + + /** + * Initializes the player for DAI source. + * + * Requests the DAI stream and completes player initialization. + * + * @param runningSource The source containing DAI properties + */ + private void initializeDaiSource(Source runningSource) { + if (player == null) { + DebugLog.w(TAG, "Player is null in initializeDaiSource, skipping DAI initialization"); + return; + } + + requestDaiStream(runningSource); + + player.prepare(); + playerNeedsSource = false; + + eventEmitter.onVideoLoadStart.invoke(); + loadVideoStarted = true; + + finishPlayerInitialization(); + } + + /** + * Requests a DAI stream from Google IMA using the ExoPlayer IMA extension. + * + * Builds an SSAI URI based on the provided parameters and sets it on the player. + * Supports both VOD (contentSourceId + videoId) and Live (assetKey) streams. + * + * @param runningSource The source containing DAI properties + */ + private void requestDaiStream(Source runningSource) { + if (daiAdsLoader == null) { + eventEmitter.onVideoError.invoke("DaiAdsLoader is null", null, "DAI_ADS_LOADER_NULL_ERROR"); + return; + } + + daiAdsLoader.setPlayer(player); + + AdsProps adsProps = runningSource.getAdsProps(); + int streamFormat = "dash".equalsIgnoreCase(adsProps.getFormat()) ? CONTENT_TYPE_DASH : CONTENT_TYPE_HLS; + + try { + Uri.Builder uriBuilder; + + if (adsProps.isDAILive()) { + uriBuilder = new ImaServerSideAdInsertionUriBuilder() + .setAssetKey(adsProps.getAssetKey()) + .setFormat(streamFormat) + .build() + .buildUpon(); + } else if (adsProps.isDAIVod()) { + uriBuilder = new ImaServerSideAdInsertionUriBuilder() + .setContentSourceId(adsProps.getContentSourceId()) + .setVideoId(adsProps.getVideoId()) + .setFormat(streamFormat) + .build() + .buildUpon(); + } else { + throw new IllegalArgumentException("Either assetKey (for live) or contentSourceId+videoId (for VOD) must be provided"); + } + + Map adTagParameters = adsProps.getAdTagParameters(); + if (adTagParameters != null && !adTagParameters.isEmpty()) { + for (Map.Entry entry : adTagParameters.entrySet()) { + uriBuilder.appendQueryParameter(entry.getKey(), entry.getValue()); + } + } + + Uri ssaiUri = uriBuilder.build(); + MediaItem ssaiMediaItem = MediaItem.fromUri(ssaiUri); + + player.setMediaItem(ssaiMediaItem); + } catch (Exception e) { + eventEmitter.onVideoError.invoke("DAI stream request failed: " + e.getMessage(), e, "DAI_REQUEST_ERROR"); + handleDaiBackupStream(); + } + } + + /** + * Handles fallback to backup stream when DAI stream fails. + * + * If a backup stream URI is available in the DAI properties, it cleans up DAI resources + * and switches to the backup stream. + * + * @return true if backup stream was successfully used, false otherwise + */ + private boolean handleDaiBackupStream() { + if (source == null || source.getAdsProps() == null) { + return false; + } + + String fallbackStreamUri = source.getAdsProps().getFallbackUri(); + if (fallbackStreamUri == null || fallbackStreamUri.isEmpty()) { + return false; + } + + DebugLog.d(TAG, "DAI stream error occurred, falling back to backup stream URI: " + fallbackStreamUri); + + WritableMap backupSourceMap = Arguments.createMap(); + backupSourceMap.putString("uri", fallbackStreamUri); + backupSourceMap.putBoolean("isNetwork", true); + + Source backupSource = Source.parse(backupSourceMap, themedReactContext); + if (backupSource == null || backupSource.getUri() == null) { + return false; + } + + if (daiAdsLoader != null) { + daiAdsLoader.setPlayer(null); + } + + setSrc(backupSource); + + return true; + } } diff --git a/docs/pages/component/ads.md b/docs/pages/component/ads.md index 0a9cb330..e08b035f 100644 --- a/docs/pages/component/ads.md +++ b/docs/pages/component/ads.md @@ -4,14 +4,36 @@ `react-native-video` includes built-in support for Google IMA SDK on Android and iOS. To enable it, refer to the [installation section](/installation). +The IMA SDK supports two types of ad insertion: + +1. **Client-Side Ad Insertion (CSAI)** – Ads are inserted client-side using VAST tags +2. **Server-Side Ad Insertion (SSAI)** – Server-side ad insertion where ads are stitched into the stream + +Both ad types are configured through the unified `ad` property in the source configuration, using the `type` field to specify which mode to use. + +--- + +## Client-Side Ad Insertion (CSAI) + +CSAI inserts ads client-side using VAST (Video Ad Serving Template) tags. Ads are requested and played during video playback, with the player handling ad breaks and transitions. + ### Usage -To use AVOD (Ad-Supported Video on Demand), pass the `adTagUrl` prop to the `Video` component. The `adTagUrl` should be a VAST-compliant URI. +To use CSAI, configure the `ad` property with `type: 'csai'` and provide an `adTagUrl`. The `adTagUrl` should be a VAST-compliant URI. #### Example: ```jsx -adTagUrl="https://pubads.g.doubleclick.net/gampad/ads?iu=/21775744923/external/vmap_ad_samples&sz=640x480&cust_params=sample_ar%3Dpremidpostoptimizedpodbumper&ciu_szs=300x250&gdfp_req=1&ad_rule=1&output=vmap&unviewed_position_start=1&env=vp&impl=s&cmsid=496&vid=short_onecue&correlator=" +