feat!: add DAI support (#4816)

* feat: add DAI support

* refactor!: unify CSAI and DAI configuration into single ad prop

* fix(android): remove broken wait() in DAI initialization

* feat(android): support HLS/DASH format in DAI

* fix: lint formatting issues

* refactor(android): simplify URI parsing

* refactor(android): rename backupStreamUri to fallbackStreamUri for consistency

* refactor: restore deleted comment

* refactor: remove expo-dai example app

* feat: add streamType field

* refactor: simplify DAI config normalization

* refactor: rename type value from dai to ssai

* docs: replace DAI terminology with SSAI

* docs: change ssai desc

Co-authored-by: Kamil Moskała <91079590+moskalakamil@users.noreply.github.com>

* fix(ios): conditional background playback for DAI based on PiP settings

* fix: remove unused import

---------

Co-authored-by: Maciej Budzinski <maciej.budzinski.93@gmail.com>
Co-authored-by: Kamil Moskała <91079590+moskalakamil@users.noreply.github.com>
This commit is contained in:
Filip Wnęk
2026-01-15 21:47:14 +01:00
committed by GitHub
parent f387177785
commit 88ac1ae1dc
16 changed files with 1324 additions and 265 deletions

View File

@@ -4,7 +4,7 @@ RNVideo_targetSdkVersion=35
RNVideo_compileSdkVersion=35 RNVideo_compileSdkVersion=35
RNVideo_ndkversion=27.1.12297006 RNVideo_ndkversion=27.1.12297006
RNVideo_buildToolsVersion=35.0.0 RNVideo_buildToolsVersion=35.0.0
RNVideo_media3Version=1.4.1 RNVideo_media3Version=1.8.0
RNVideo_useExoplayerIMA=false RNVideo_useExoplayerIMA=false
RNVideo_useExoplayerRtsp=false RNVideo_useExoplayerRtsp=false
RNVideo_useExoplayerSmoothStreaming=true RNVideo_useExoplayerSmoothStreaming=true

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -4,38 +4,98 @@ import android.net.Uri
import android.text.TextUtils import android.text.TextUtils
import com.brentvatne.common.toolbox.ReactBridgeUtils import com.brentvatne.common.toolbox.ReactBridgeUtils
import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.ReadableMap
import java.util.Objects
class AdsProps { class AdsProps {
var type: String? = null
var streamType: String? = null
var adTagUrl: Uri? = null var adTagUrl: Uri? = null
var adLanguage: String? = null var adLanguage: String? = null
var contentSourceId: String? = null
var videoId: String? = null
var assetKey: String? = null
var format: String? = null
var adTagParameters: Map<String, String>? = 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 { override fun equals(other: Any?): Boolean {
if (other == null || other !is AdsProps) return false if (other == null || other !is AdsProps) return false
return ( return (
adTagUrl == other.adTagUrl && type == other.type &&
adLanguage == other.adLanguage 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 { 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_TAG_URL = "adTagUrl"
private const val PROP_AD_LANGUAGE = "adLanguage" 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 @JvmStatic
fun parse(src: ReadableMap?): AdsProps { fun parse(src: ReadableMap?): AdsProps {
val adsProps = AdsProps() val adsProps = AdsProps()
if (src != null) { 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) val uriString = ReactBridgeUtils.safeGetString(src, PROP_AD_TAG_URL)
if (TextUtils.isEmpty(uriString)) { if (!TextUtils.isEmpty(uriString)) {
adsProps.adTagUrl = null
} else {
adsProps.adTagUrl = Uri.parse(uriString) adsProps.adTagUrl = Uri.parse(uriString)
} }
val languageString = ReactBridgeUtils.safeGetString(src, PROP_AD_LANGUAGE) val languageString = ReactBridgeUtils.safeGetString(src, PROP_AD_LANGUAGE)
if (!TextUtils.isEmpty(languageString)) { if (!TextUtils.isEmpty(languageString)) {
adsProps.adLanguage = 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<String, String>()
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 return adsProps
} }

View File

@@ -5,7 +5,6 @@ import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.net.Uri import android.net.Uri
import android.text.TextUtils
import com.brentvatne.common.api.DRMProps.Companion.parse import com.brentvatne.common.api.DRMProps.Companion.parse
import com.brentvatne.common.toolbox.DebugLog import com.brentvatne.common.toolbox.DebugLog
import com.brentvatne.common.toolbox.DebugLog.e import com.brentvatne.common.toolbox.DebugLog.e
@@ -90,7 +89,7 @@ class Source {
*/ */
var sideLoadedTextTracks: SideLoadedTextTrackList? = null 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 */ /** return true if this and src are equals */
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@@ -212,59 +211,53 @@ class Source {
fun parse(src: ReadableMap?, context: Context): Source { fun parse(src: ReadableMap?, context: Context): Source {
val source = Source() val source = Source()
if (src != null) { if (src == null) return source
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))
val propSrcHeadersArray = safeGetArray(src, PROP_SRC_HEADERS) safeGetString(src, PROP_SRC_URI, null)
if (propSrcHeadersArray != null) { ?.takeIf { it.isNotBlank() }
if (propSrcHeadersArray.size() > 0) { ?.let { uriString ->
for (i in 0 until propSrcHeadersArray.size()) { var uri = Uri.parse(uriString)
val current = propSrcHeadersArray.getMap(i)
val key = current?.getString("key") if (!isValidScheme(uri.scheme)) {
val value = current?.getString("value") uri = getUriFromAssetId(context, uriString) ?: return source
if (key != null && value != null) { }
source.headers[key] = value
} 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 return source
} }

View File

@@ -55,6 +55,7 @@ import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSpec; import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.datasource.HttpDataSource; import androidx.media3.datasource.HttpDataSource;
import androidx.media3.exoplayer.DefaultLoadControl; import androidx.media3.exoplayer.DefaultLoadControl;
import androidx.media3.exoplayer.DefaultRenderersFactory; 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.drm.UnsupportedDrmException;
import androidx.media3.exoplayer.hls.HlsMediaSource; import androidx.media3.exoplayer.hls.HlsMediaSource;
import androidx.media3.exoplayer.ima.ImaAdsLoader; 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.MediaCodecInfo;
import androidx.media3.exoplayer.mediacodec.MediaCodecUtil; import androidx.media3.exoplayer.mediacodec.MediaCodecUtil;
import androidx.media3.exoplayer.rtsp.RtspMediaSource; import androidx.media3.exoplayer.rtsp.RtspMediaSource;
@@ -125,9 +128,11 @@ import com.brentvatne.react.ReactNativeVideoManager;
import com.brentvatne.receiver.AudioBecomingNoisyReceiver; import com.brentvatne.receiver.AudioBecomingNoisyReceiver;
import com.brentvatne.receiver.BecomingNoisyListener; import com.brentvatne.receiver.BecomingNoisyListener;
import com.brentvatne.receiver.PictureInPictureReceiver; import com.brentvatne.receiver.PictureInPictureReceiver;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.UiThreadUtil;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.ThemedReactContext;
import com.google.ads.interactivemedia.v3.api.AdError; import com.google.ads.interactivemedia.v3.api.AdError;
import com.google.ads.interactivemedia.v3.api.AdErrorEvent; import com.google.ads.interactivemedia.v3.api.AdErrorEvent;
@@ -182,6 +187,7 @@ public class ReactExoplayerView extends FrameLayout implements
private ExoPlayerView exoPlayerView; private ExoPlayerView exoPlayerView;
private FullScreenPlayerView fullScreenPlayerView; private FullScreenPlayerView fullScreenPlayerView;
private ImaAdsLoader adsLoader; private ImaAdsLoader adsLoader;
private ImaServerSideAdInsertionMediaSource.AdsLoader daiAdsLoader;
private DataSource.Factory mediaDataSourceFactory; private DataSource.Factory mediaDataSourceFactory;
private ExoPlayer player; private ExoPlayer player;
@@ -622,7 +628,6 @@ public class ReactExoplayerView extends FrameLayout implements
private void initializePlayer() { private void initializePlayer() {
disableCache = ReactNativeVideoManager.Companion.getInstance().shouldDisableCache(source); disableCache = ReactNativeVideoManager.Companion.getInstance().shouldDisableCache(source);
ReactExoplayerView self = this; ReactExoplayerView self = this;
Activity activity = themedReactContext.getCurrentActivity(); Activity activity = themedReactContext.getCurrentActivity();
// This ensures all props have been settled, to avoid async racing conditions. // This ensures all props have been settled, to avoid async racing conditions.
@@ -632,7 +637,7 @@ public class ReactExoplayerView extends FrameLayout implements
return; return;
} }
try { try {
if (runningSource.getUri() == null) { if (runningSource.getUri() == null && !isDaiRequest(runningSource)) {
return; return;
} }
@@ -731,13 +736,20 @@ public class ReactExoplayerView extends FrameLayout implements
.setEnableDecoderFallback(true) .setEnableDecoderFallback(true)
.forceEnableMediaCodecAsynchronousQueueing(); .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) { if (useCache && !disableCache) {
mediaSourceFactory.setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true))); mediaSourceFactory.setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true)));
} }
mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView.getPlayerView());
player = new ExoPlayer.Builder(getContext(), renderersFactory) player = new ExoPlayer.Builder(getContext(), renderersFactory)
.setTrackSelector(self.trackSelector) .setTrackSelector(self.trackSelector)
.setBandwidthMeter(bandwidthMeter) .setBandwidthMeter(bandwidthMeter)
@@ -831,6 +843,11 @@ public class ReactExoplayerView extends FrameLayout implements
} }
private void initializePlayerSource(Source runningSource) { private void initializePlayerSource(Source runningSource) {
if (isDaiRequest(runningSource)) {
initializeDaiSource(runningSource);
return;
}
if (runningSource.getUri() == null) { if (runningSource.getUri() == null) {
return; return;
} }
@@ -1225,6 +1242,12 @@ public class ReactExoplayerView extends FrameLayout implements
adsLoader.release(); adsLoader.release();
adsLoader = null; adsLoader = null;
} }
if (daiAdsLoader != null) {
daiAdsLoader.release();
daiAdsLoader = null;
}
progressHandler.removeMessages(SHOW_PROGRESS); progressHandler.removeMessages(SHOW_PROGRESS);
audioBecomingNoisyReceiver.removeListener(); audioBecomingNoisyReceiver.removeListener();
pictureInPictureReceiver.removeListener(); pictureInPictureReceiver.removeListener();
@@ -2015,7 +2038,7 @@ public class ReactExoplayerView extends FrameLayout implements
} }
public void setSrc(Source source) { public void setSrc(Source source) {
if (source.getUri() != null) { if (source.getUri() != null || isDaiRequest(source)) {
clearResumePosition(); clearResumePosition();
boolean isSourceEqual = source.isEquals(this.source); boolean isSourceEqual = source.isEquals(this.source);
hasDrmFailed = false; hasDrmFailed = false;
@@ -2741,10 +2764,180 @@ public class ReactExoplayerView extends FrameLayout implements
"type", String.valueOf(error.getErrorType()) "type", String.valueOf(error.getErrorType())
); );
eventEmitter.onReceiveAdEvent.invoke("ERROR", errMap); eventEmitter.onReceiveAdEvent.invoke("ERROR", errMap);
handleDaiBackupStream();
} }
public void setControlsStyles(ControlsConfig controlsStyles) { public void setControlsStyles(ControlsConfig controlsStyles) {
controlsConfig = controlsStyles; controlsConfig = controlsStyles;
refreshControlsStyles(); 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<String, String> adTagParameters = adsProps.getAdTagParameters();
if (adTagParameters != null && !adTagParameters.isEmpty()) {
for (Map.Entry<String, String> 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;
}
} }

View File

@@ -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). `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 ### 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: #### Example:
```jsx ```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=" <Video
source={{
uri: 'https://example.com/video.mp4',
ad: {
type: 'csai',
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=',
},
}}
/>
``` ```
> **Note:** Video ads cannot start when Picture-in-Picture (PiP) mode is active on iOS. More details are available in the [Google IMA SDK Docs](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/picture_in_picture?hl=en#starting_ads). If you are using custom controls, hide the PiP button when receiving the `STARTED` event from `onReceiveAdEvent` and show it again when receiving the `ALL_ADS_COMPLETED` event. > **Note:** Video ads cannot start when Picture-in-Picture (PiP) mode is active on iOS. More details are available in the [Google IMA SDK Docs](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/picture_in_picture?hl=en#starting_ads). If you are using custom controls, hide the PiP button when receiving the `STARTED` event from `onReceiveAdEvent` and show it again when receiving the `ALL_ADS_COMPLETED` event.
@@ -23,21 +45,146 @@ To receive events from the IMA SDK, pass the `onReceiveAdEvent` prop to the `Vid
#### Example: #### Example:
```jsx ```jsx
... <Video
onReceiveAdEvent={event => console.log(event)} onReceiveAdEvent={(event) => console.log(event)}
... // ... other props
/>
``` ```
### Localization ### Localization
To change the language of the IMA SDK, pass the `adLanguage` prop to the `Video` component. The list of supported languages is available [here](https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/localization#locale-codes). To change the language of the IMA SDK, pass the `adLanguage` prop within the `ad` configuration. The list of supported languages is available [here](https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side/localization#locale-codes).
- By default, **iOS** uses the system language, and **Android** defaults to `en` (English). - By default, **iOS** uses the system language, and **Android** defaults to `en` (English).
#### Example: #### Example:
```jsx ```jsx
... <Video
adLanguage="fr" source={{
... uri: 'https://example.com/video.mp4',
ad: {
type: 'csai',
adTagUrl: 'https://example.com/adtag',
adLanguage: 'fr',
},
}}
/>
``` ```
---
## Server-Side Ad Insertion (SSAI)
SSAI (Server-Side Ad Insertion) is a server-side ad insertion solution where ads are stitched into the video stream before it reaches the player. This provides a seamless viewing experience with no playback interruptions, as the stream appears as a single continuous video.
Currently, we support **Google IMA DAI**
SSAI is ideal for:
- Live streaming with ad breaks
- VOD content with dynamic ad insertion
- Scenarios where you want a seamless, uninterrupted viewing experience
### Usage
To use SSAI, configure the `ad` property with `type: 'ssai'` within the `source` prop. SSAI supports both Video On Demand (VOD) and Live streaming.
#### VOD Example:
```jsx
<Video
source={{
ad: {
type: 'ssai',
streamType: 'vod',
contentSourceId: '2548831',
videoId: 'tears-of-steel',
adTagParameters: {
custom_param: 'value',
},
fallbackUri: 'https://example.com/backup-stream.m3u8',
},
}}
/>
```
#### Live Example:
```jsx
<Video
source={{
ad: {
type: 'ssai',
streamType: 'live',
assetKey: 'c-rArva4ShKVIAkNfy6HUQ',
adTagParameters: {
custom_param: 'value',
},
fallbackUri: 'https://example.com/backup-stream.m3u8',
},
}}
/>
```
### Configuration
For VOD streams, you must provide:
- `contentSourceId` The content source ID
- `videoId` The video ID
For Live streams, you must provide:
- `assetKey` The asset key for the live stream
Optional properties:
- `format` Stream format: `'hls'` (default) or `'dash'`. Android only - iOS automatically detects the format.
- `adTagParameters` Custom key-value pairs to pass as ad tag parameters to the IMA SDK. For a list of supported Ad Manager ad tag parameters, see the [Google Ad Manager documentation](https://support.google.com/admanager/answer/7320899?hl=en#npa).
- `fallbackUri` Fallback stream URI. If the SSAI stream fails to load, the player will automatically fall back to this URI
> **Note:** The `streamType` field (`'vod'` or `'live'`) is required to specify the type of SSAI stream.
### Events
SSAI uses the same `onReceiveAdEvent` prop as CSAI to report ad-related events. The full list of supported events is available [here](https://github.com/TheWidlarzGroup/react-native-video/blob/master/src/types/Ads.ts).
#### Example:
```jsx
<Video
source={{
ad: {
type: 'ssai',
streamType: 'vod',
contentSourceId: '2548831',
videoId: 'tears-of-steel',
},
}}
onReceiveAdEvent={(event) => console.log(event)}
// ... other props
/>
```
For more details on ad configuration properties, see the [props documentation](/component/props#ad).
### Fallback Stream
If the SSAI stream fails to load and a `fallbackUri` is provided, the player will automatically fall back to the fallback stream. This ensures playback continuity even when SSAI services are unavailable.
### Example App
For testing and experimenting with SSAI, you can use the `expo-dai` example app located in the `examples/expo-dai` directory. This example app demonstrates SSAI functionality for both VOD and Live streaming scenarios.
### Differences from CSAI
| Feature | CSAI | SSAI |
| ---------------------- | ------------------------------------- | ------------------------------------------ |
| Ad insertion | Client-side | Server-side |
| Playback interruptions | Possible during ad breaks | Seamless, no interruptions |
| Stream format | Original video + separate ad requests | Single unified stream with ads |
| Use case | VOD with pre-defined ad breaks | Live and VOD with server-side ad insertion |
| Configuration | `source.ad` with `type: 'csai'` | `source.ad` with `type: 'ssai'` |
---

View File

@@ -18,7 +18,8 @@ Sets the VAST URI to play AVOD ads.
**Example:** **Example:**
```javascript ```javascript
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=" 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=';
``` ```
> **Note:** You need to enable IMA SDK in the Gradle or Podfile see [Enable Client-Side Ads Insertion](/installation). > **Note:** You need to enable IMA SDK in the Gradle or Podfile see [Enable Client-Side Ads Insertion](/installation).
@@ -67,28 +68,28 @@ Indicates whether the player should automatically delay playback to minimize sta
Adjusts the buffer settings. This prop takes an object with one or more of the following properties: Adjusts the buffer settings. This prop takes an object with one or more of the following properties:
| Property | Type | Description | | Property | Type | Description |
|----------------------------------|--------|-----------------------------------------------------------------------------------------------| | --------------------------------- | ------ | --------------------------------------------------------------------------------- |
| minBufferMs | number | Minimum duration (ms) the player will attempt to keep buffered. | | minBufferMs | number | Minimum duration (ms) the player will attempt to keep buffered. |
| maxBufferMs | number | Maximum duration (ms) the player will attempt to buffer. | | maxBufferMs | number | Maximum duration (ms) the player will attempt to buffer. |
| bufferForPlaybackMs | number | Duration (ms) that must be buffered before playback starts or resumes. | | bufferForPlaybackMs | number | Duration (ms) that must be buffered before playback starts or resumes. |
| bufferForPlaybackAfterRebufferMs | number | Duration (ms) that must be buffered after a rebuffer before playback resumes. | | bufferForPlaybackAfterRebufferMs | number | Duration (ms) that must be buffered after a rebuffer before playback resumes. |
| backBufferDurationMs | number | Duration (ms) of buffer to keep before the current position (allows rewinding). | | backBufferDurationMs | number | Duration (ms) of buffer to keep before the current position (allows rewinding). |
| maxHeapAllocationPercent | number | Percentage of available heap the video can use to buffer (0 to 1). | | maxHeapAllocationPercent | number | Percentage of available heap the video can use to buffer (0 to 1). |
| minBackBufferMemoryReservePercent| number | Percentage of available app memory before the back buffer is disabled (0 to 1). | | minBackBufferMemoryReservePercent | number | Percentage of available app memory before the back buffer is disabled (0 to 1). |
| minBufferMemoryReservePercent | number | Percentage of available app memory reserved for preventing buffer usage (0 to 1). | | minBufferMemoryReservePercent | number | Percentage of available app memory reserved for preventing buffer usage (0 to 1). |
| cacheSizeMB | number | Cache size in MB. Set to `0` to disable caching (Android only). | | cacheSizeMB | number | Cache size in MB. Set to `0` to disable caching (Android only). |
| live | object | Object containing configuration for live playback. See below. | | live | object | Object containing configuration for live playback. See below. |
#### Live Buffer Configurations #### Live Buffer Configurations
| Property | Type | Description | | Property | Type | Description |
|-----------------|--------|-----------------------------------------------------------------------------| | ---------------- | ------ | ------------------------------------------------------------------ |
| maxPlaybackSpeed| number | Maximum playback speed for catching up to target live offset. | | maxPlaybackSpeed | number | Maximum playback speed for catching up to target live offset. |
| minPlaybackSpeed| number | Minimum playback speed for falling back to target live offset. | | minPlaybackSpeed | number | Minimum playback speed for falling back to target live offset. |
| maxOffsetMs | number | Maximum allowed live offset. The player wont exceed this limit. | | maxOffsetMs | number | Maximum allowed live offset. The player wont exceed this limit. |
| minOffsetMs | number | Minimum allowed live offset. The player wont go below this limit. | | minOffsetMs | number | Minimum allowed live offset. The player wont go below this limit. |
| targetOffsetMs | number | The target live offset the player will aim to maintain. | | targetOffsetMs | number | The target live offset the player will aim to maintain. |
For more details on Android live streaming, see [ExoPlayer Live Streaming](https://developer.android.com/media/media3/exoplayer/live-streaming?hl=en). For more details on Android live streaming, see [ExoPlayer Live Streaming](https://developer.android.com/media/media3/exoplayer/live-streaming?hl=en).
@@ -130,12 +131,12 @@ Configures the buffering and data loading strategy.
Provides a custom chapter source for tvOS. This prop takes an array of objects with the following properties: Provides a custom chapter source for tvOS. This prop takes an array of objects with the following properties:
| Property | Type | Description | | Property | Type | Description |
|----------|--------|-----------------------------------------------------------------------------| | --------- | ------- | ------------------------------------------------------------------------------ |
| title | string | The title of the chapter. | | title | string | The title of the chapter. |
| startTime| number | The start time of the chapter (seconds). | | startTime | number | The start time of the chapter (seconds). |
| endTime | number | The end time of the chapter (seconds). | | endTime | number | The end time of the chapter (seconds). |
| uri | string? | Optional image override URL (HTTP or Base64). Some media auto-generate images. | | uri | string? | Optional image override URL (HTTP or Base64). Some media auto-generate images. |
--- ---
@@ -171,22 +172,22 @@ See [Useful Side Projects](/projects).
Adjust the control styles. This prop is needed only if `controls={true}` and is an object. See the supported properties below. Adjust the control styles. This prop is needed only if `controls={true}` and is an object. See the supported properties below.
| Property | Type | Description | | Property | Type | Description |
|-------------------------------------|---------|---------------------------------------------------------------------------------------------| | ----------------------------------- | ------- | ------------------------------------------------------------------- |
| hidePosition | boolean | Hides the position indicator. Default is `false`. | | hidePosition | boolean | Hides the position indicator. Default is `false`. |
| hidePlayPause | boolean | Hides the play/pause button. Default is `false`. | | hidePlayPause | boolean | Hides the play/pause button. Default is `false`. |
| hideForward | boolean | Hides the forward button. Default is `false`. | | hideForward | boolean | Hides the forward button. Default is `false`. |
| hideRewind | boolean | Hides the rewind button. Default is `false`. | | hideRewind | boolean | Hides the rewind button. Default is `false`. |
| hideNext | boolean | Hides the next button. Default is `false`. | | hideNext | boolean | Hides the next button. Default is `false`. |
| hidePrevious | boolean | Hides the previous button. Default is `false`. | | hidePrevious | boolean | Hides the previous button. Default is `false`. |
| hideFullscreen | boolean | Hides the fullscreen button. Default is `false`. | | hideFullscreen | boolean | Hides the fullscreen button. Default is `false`. |
| hideSeekBar | boolean | Hides the seek bar, useful for live broadcasts. Default is `false`. | | hideSeekBar | boolean | Hides the seek bar, useful for live broadcasts. Default is `false`. |
| hideDuration | boolean | Hides the duration display. Default is `false`. | | hideDuration | boolean | Hides the duration display. Default is `false`. |
| hideNavigationBarOnFullScreenMode | boolean | Hides the navigation bar in fullscreen mode. Default is `true`. | | hideNavigationBarOnFullScreenMode | boolean | Hides the navigation bar in fullscreen mode. Default is `true`. |
| hideNotificationBarOnFullScreenMode | boolean | Hides the notification bar in fullscreen mode. Default is `true`. | | hideNotificationBarOnFullScreenMode | boolean | Hides the notification bar in fullscreen mode. Default is `true`. |
| hideSettingButton | boolean | Hides the settings button. Default is `true`. | | hideSettingButton | boolean | Hides the settings button. Default is `true`. |
| seekIncrementMS | number | Defines the seek increment in milliseconds. Default is `10000`. | | seekIncrementMS | number | Defines the seek increment in milliseconds. Default is `10000`. |
| liveLabel | string | Sets a label for live video. | | liveLabel | string | Sets a label for live video. |
**Example with default values:** **Example with default values:**
@@ -231,10 +232,10 @@ Enables detailed logging.
> [!WARNING] > [!WARNING]
> Do not use this in production builds. > Do not use this in production builds.
| Property | Type | Description | | Property | Type | Description |
| -------- | ------- | -------------------------------------------- | | -------- | ------- | ----------------------------------------- |
| `enable` | boolean | Enables verbose logs. Default is `false`. | | `enable` | boolean | Enables verbose logs. Default is `false`. |
| `thread` | boolean | Displays logs with thread information. | | `thread` | boolean | Displays logs with thread information. |
**Example:** **Example:**
@@ -338,26 +339,27 @@ Determines whether to enter Picture-in-Picture (PiP) mode when the user leaves t
Applies a video filter. Applies a video filter.
| FilterType | Description | | FilterType | Description |
|-----------------------------|-------------------------| | ------------------ | --------------------- |
| `NONE (default)` | No filter | | `NONE (default)` | No filter |
| `INVERT` | CIColorInvert | | `INVERT` | CIColorInvert |
| `MONOCHROME` | CIColorMonochrome | | `MONOCHROME` | CIColorMonochrome |
| `POSTERIZE` | CIColorPosterize | | `POSTERIZE` | CIColorPosterize |
| `FALSE` | CIFalseColor | | `FALSE` | CIFalseColor |
| `MAXIMUMCOMPONENT` | CIMaximumComponent | | `MAXIMUMCOMPONENT` | CIMaximumComponent |
| `MINIMUMCOMPONENT` | CIMinimumComponent | | `MINIMUMCOMPONENT` | CIMinimumComponent |
| `CHROME` | CIPhotoEffectChrome | | `CHROME` | CIPhotoEffectChrome |
| `FADE` | CIPhotoEffectFade | | `FADE` | CIPhotoEffectFade |
| `INSTANT` | CIPhotoEffectInstant | | `INSTANT` | CIPhotoEffectInstant |
| `MONO` | CIPhotoEffectMono | | `MONO` | CIPhotoEffectMono |
| `NOIR` | CIPhotoEffectNoir | | `NOIR` | CIPhotoEffectNoir |
| `PROCESS` | CIPhotoEffectProcess | | `PROCESS` | CIPhotoEffectProcess |
| `TONAL` | CIPhotoEffectTonal | | `TONAL` | CIPhotoEffectTonal |
| `TRANSFER` | CIPhotoEffectTransfer | | `TRANSFER` | CIPhotoEffectTransfer |
| `SEPIA` | CISepiaTone | | `SEPIA` | CISepiaTone |
> **Notes:** > **Notes:**
>
> 1. Using a filter may increase CPU usage. > 1. Using a filter may increase CPU usage.
> 2. Saving a filtered video and reloading it is a workaround for performance issues. > 2. Saving a filtered video and reloading it is a workaround for performance issues.
> 3. Filters are not supported on HLS playlists. > 3. Filters are not supported on HLS playlists.
@@ -551,9 +553,10 @@ An image to display while the video is loading.
```javascript ```javascript
<Video> <Video>
poster={{ poster=
source: { uri: "https://baconmockup.com/300/200/" }, {{
resizeMode: "cover", source: {uri: 'https://baconmockup.com/300/200/'},
resizeMode: 'cover',
}} }}
</Video> </Video>
``` ```
@@ -646,7 +649,8 @@ interface ReactVideoRenderLoaderProps {
```javascript ```javascript
<Video> <Video>
renderLoader={() => ( renderLoader=
{() => (
<View> <View>
<Text>Custom Loader</Text> <Text>Custom Loader</Text>
</View> </View>
@@ -708,8 +712,8 @@ selectedAudioTrack={{
| ------------------ | ------ | ------------------------------------------------------------------------------------------- | | ------------------ | ------ | ------------------------------------------------------------------------------------------- |
| "system" (default) | N/A | Play the audio track that matches the system language. If none match, play the first track. | | "system" (default) | N/A | Play the audio track that matches the system language. If none match, play the first track. |
| "disabled" | N/A | Turn off audio. | | "disabled" | N/A | Turn off audio. |
| "title" | string | Play the audio track with the specified title, e.g., "French". | | "title" | string | Play the audio track with the specified title, e.g., "French". |
| "language" | string | Play the audio track with the specified language, e.g., "fr". | | "language" | string | Play the audio track with the specified language, e.g., "fr". |
| "index" | number | Play the audio track with the specified index, e.g., 0. | | "index" | number | Play the audio track with the specified index, e.g., 0. |
If no matching track is found, the first available track will be played. If multiple tracks match, the first match will be used. If no matching track is found, the first available track will be played. If multiple tracks match, the first match will be used.
@@ -729,13 +733,13 @@ selectedTextTrack={{
}} }}
``` ```
| Type | Value | Description | | Type | Value | Description |
| ------------------ | ------ | ----------------------------------------------------------------------------- | | ------------------ | ------ | ----------------------------------------------------------------------- |
| "system" (default) | N/A | Display captions only if the system preference for captions is enabled. | | "system" (default) | N/A | Display captions only if the system preference for captions is enabled. |
| "disabled" | N/A | Dont display a text track. | | "disabled" | N/A | Dont display a text track. |
| "title" | string | Display the text track with the specified title, e.g., "French 1". | | "title" | string | Display the text track with the specified title, e.g., "French 1". |
| "language" | string | Display the text track with the specified language, e.g., "fr". | | "language" | string | Display the text track with the specified language, e.g., "fr". |
| "index" | number | Display the text track with the specified index, e.g., 0. | | "index" | number | Display the text track with the specified index, e.g., 0. |
If no matching track is found, no subtitles will be displayed. If multiple tracks match, the first match will be used. If no matching track is found, no subtitles will be displayed. If multiple tracks match, the first match will be used.
@@ -754,12 +758,12 @@ selectedVideoTrack={{
}} }}
``` ```
| Type | Value | Description | | Type | Value | Description |
| ---------------- | ------ | ---------------------------------------------------------------------------- | | ---------------- | ------ | ------------------------------------------------------------------------------ |
| "auto" (default) | N/A | Let the player determine the best track using ABR. | | "auto" (default) | N/A | Let the player determine the best track using ABR. |
| "disabled" | N/A | Turn off video. | | "disabled" | N/A | Turn off video. |
| "resolution" | number | Play the video track with the specified height, e.g., 480 for the 480p stream. | | "resolution" | number | Play the video track with the specified height, e.g., 480 for the 480p stream. |
| "index" | number | Play the video track with the specified index, e.g., 0. | | "index" | number | Play the video track with the specified index, e.g., 0. |
If no matching track is found, ABR will be used. If no matching track is found, ABR will be used.
@@ -803,7 +807,7 @@ Pass the asset directly (deprecated):
```javascript ```javascript
const sintel = require('./sintel.mp4'); const sintel = require('./sintel.mp4');
source = { sintel }; source = {sintel};
``` ```
Or by using a URI (starting from `6.0.0-beta.6`): Or by using a URI (starting from `6.0.0-beta.6`):
@@ -954,20 +958,82 @@ source={{
<PlatformsList types={['Android', 'iOS']} /> <PlatformsList types={['Android', 'iOS']} />
Sets the ad configuration. Sets the ad configuration. Supports both Client-Side Ad Insertion (CSAI) and Server-Side Ad Insertion (SSAI) through the unified `type` field.
**Example:** **CSAI (Client-Side Ad Insertion)**
For client-side ads using VAST tags:
| Property | Type | Required | Description |
| ------------ | ------ | -------- | ---------------------------------------------------- |
| `type` | string | Yes | Must be `'csai'` |
| `adTagUrl` | string | Yes | VAST-compliant ad tag URL |
| `adLanguage` | string | Optional | Language code for the IMA SDK (e.g., `'en'`, `'fr'`) |
**CSAI Example:**
```javascript ```javascript
ad: { source={{
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=" uri: 'https://example.com/video.mp4',
adLanguage="fr" ad: {
} type: 'csai',
adTagUrl: 'https://pubads.g.doubleclick.net/gampad/ads?...',
adLanguage: 'fr'
}
}}
```
**SSAI (Server-Side Ad Insertion)**
For server-side ad insertion. Supports both VOD and Live streaming.
| Property | Type | Required | Description |
| ----------------- | ------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type` | string | Yes | Must be `'ssai'` |
| `streamType` | string | Yes | Must be `'vod'` for VOD streams or `'live'` for live streams |
| `contentSourceId` | string | VOD only | The content source ID for VOD streams. Required for VOD, must not be used with `assetKey`. |
| `videoId` | string | VOD only | The video ID for VOD streams. Required for VOD, must not be used with `assetKey`. |
| `assetKey` | string | Live only | The asset key for live streams. Required for Live, must not be used with `contentSourceId` or `videoId`. |
| `adTagParameters` | object | Optional | Custom key-value pairs to pass as ad tag parameters to the IMA SDK. For a list of supported Ad Manager ad tag parameters, see the [Google Ad Manager documentation](https://support.google.com/admanager/answer/7320899). |
| `fallbackUri` | string | Optional | Fallback stream URI. If the SSAI stream fails to load, the player will automatically fall back to this URI. |
| `adLanguage` | string | Optional | Language code for the IMA SDK (e.g., `'en'`, `'fr'`) |
**SSAI VOD Example:**
```javascript
source={{
ad: {
type: 'ssai',
streamType: 'vod',
contentSourceId: '2548831',
videoId: 'tears-of-steel',
adTagParameters: {
'custom_param': 'value'
},
fallbackUri: 'https://example.com/backup-stream.m3u8'
}
}}
```
**SSAI Live Example:**
```javascript
source={{
ad: {
type: 'ssai',
streamType: 'live',
assetKey: 'c-rArva4ShKVIAkNfy6HUQ',
adTagParameters: {
'custom_param': 'value'
},
fallbackUri: 'https://example.com/backup-stream.m3u8'
}
}}
``` ```
See: [ads](./ads.md) for more information. See: [ads](./ads.md) for more information.
Note: You need to enable IMA SDK in the Gradle or Pod file - [enable client-side ads insertion](/installation). Note: You need to enable IMA SDK in the Gradle or Podfile see [Enable Client-Side Ads Insertion](/installation).
#### `contentStartTime` #### `contentStartTime`
@@ -999,19 +1065,19 @@ source={{
Adjust the buffer settings. This prop takes an object with one or more of the properties listed below. Adjust the buffer settings. This prop takes an object with one or more of the properties listed below.
| Property | Type | Description | | Property | Type | Description |
|----------------------------------|--------|------------------------------------------------------------------------------------------------------------------------------------------------| | --------------------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| minBufferMs | number | Minimum duration of media that the player will attempt to buffer at all times, in milliseconds. | | minBufferMs | number | Minimum duration of media that the player will attempt to buffer at all times, in milliseconds. |
| maxBufferMs | number | Maximum duration of media that the player will attempt to buffer, in milliseconds. | | maxBufferMs | number | Maximum duration of media that the player will attempt to buffer, in milliseconds. |
| bufferForPlaybackMs | number | Duration of media that must be buffered for playback to start or resume following a user action, in milliseconds. | | bufferForPlaybackMs | number | Duration of media that must be buffered for playback to start or resume following a user action, in milliseconds. |
| bufferForPlaybackAfterRebufferMs | number | Duration of media that must be buffered for playback to resume after a rebuffer, in milliseconds. | | bufferForPlaybackAfterRebufferMs | number | Duration of media that must be buffered for playback to resume after a rebuffer, in milliseconds. |
| backBufferDurationMs | number | Duration of buffer to keep before the current position, allowing rewinding without rebuffering. | | backBufferDurationMs | number | Duration of buffer to keep before the current position, allowing rewinding without rebuffering. |
| maxHeapAllocationPercent | number | Percentage of available heap that the video can use to buffer, between 0 and 1. | | maxHeapAllocationPercent | number | Percentage of available heap that the video can use to buffer, between 0 and 1. |
| minBackBufferMemoryReservePercent| number | Percentage of available app memory at which during startup the back buffer will be disabled, between 0 and 1. | | minBackBufferMemoryReservePercent | number | Percentage of available app memory at which during startup the back buffer will be disabled, between 0 and 1. |
| minBufferMemoryReservePercent | number | Percentage of available app memory to keep in reserve, preventing buffer usage, between 0 and 1. | | minBufferMemoryReservePercent | number | Percentage of available app memory to keep in reserve, preventing buffer usage, between 0 and 1. |
| initialBitrate | number | Initial bitrate in bits per second (Android only). Defaults to 1_000_000. Used only at start, then ABR (Adaptive Bitrate Streaming) takes over.| | initialBitrate | number | Initial bitrate in bits per second (Android only). Defaults to 1_000_000. Used only at start, then ABR (Adaptive Bitrate Streaming) takes over. |
| cacheSizeMB | number | Cache size in MB, preventing new src requests and saving bandwidth while repeating videos, or 0 to disable. Android only. | | cacheSizeMB | number | Cache size in MB, preventing new src requests and saving bandwidth while repeating videos, or 0 to disable. Android only. |
| live | object | Object containing another config set for live playback configuration. | | live | object | Object containing another config set for live playback configuration. |
#### `minLoadRetryCount` #### `minLoadRetryCount`
@@ -1038,10 +1104,10 @@ Load one or more "sidecar" text tracks. This takes an array of objects represent
> ⚠️ This feature does not work with HLS playlists (e.g., m3u8) on iOS. > ⚠️ This feature does not work with HLS playlists (e.g., m3u8) on iOS.
| Property | Description | | Property | Description |
|----------|---------------------------------------------------------------------------------------------------------------| | -------- | ----------------------------------------------------------------------------------------------------------- |
| title | Descriptive name for the track. | | title | Descriptive name for the track. |
| language | 2-letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) representing the language. | | language | 2-letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) representing the language. |
| type | Mime type of the track. Supports `TextTrackType.SUBRIP`, `TextTrackType.TTML`, `TextTrackType.VTT`. | | type | Mime type of the track. Supports `TextTrackType.SUBRIP`, `TextTrackType.TTML`, `TextTrackType.VTT`. |
| uri | URL for the text track. Only tracks hosted on a web server are supported. | | uri | URL for the text track. Only tracks hosted on a web server are supported. |
@@ -1050,22 +1116,22 @@ Note: Due to iOS limitations, sidecar text tracks are not compatible with AirPla
**Example:** **Example:**
```javascript ```javascript
import { TextTrackType } from 'react-native-video'; import {TextTrackType} from 'react-native-video';
textTracks=[ textTracks = [
{ {
title: "English CC", title: 'English CC',
language: "en", language: 'en',
type: TextTrackType.VTT, // "text/vtt" type: TextTrackType.VTT, // "text/vtt"
uri: "https://bitdash-a.akamaihd.net/content/sintel/subtitles/subtitles_en.vtt" uri: 'https://bitdash-a.akamaihd.net/content/sintel/subtitles/subtitles_en.vtt',
}, },
{ {
title: "Spanish Subtitles", title: 'Spanish Subtitles',
language: "es", language: 'es',
type: TextTrackType.SUBRIP, // "application/x-subrip" type: TextTrackType.SUBRIP, // "application/x-subrip"
uri: "https://durian.blender.org/wp-content/content/subtitles/sintel_es.srt" uri: 'https://durian.blender.org/wp-content/content/subtitles/sintel_es.srt',
} },
] ];
``` ```
--- ---
@@ -1074,15 +1140,15 @@ textTracks=[
<PlatformsList types={['Android', 'iOS']} /> <PlatformsList types={['Android', 'iOS']} />
| Property | Platform | Description | Platforms | | Property | Platform | Description | Platforms |
| ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | | -------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
| fontSize | Android | Adjust the font size of the subtitles. Default: font size of the device | Android | | fontSize | Android | Adjust the font size of the subtitles. Default: font size of the device | Android |
| paddingTop | Android | Adjust the top padding of the subtitles. Default: 0 | Android | | paddingTop | Android | Adjust the top padding of the subtitles. Default: 0 | Android |
| paddingBottom | Android | Adjust the bottom padding of the subtitles. Default: 0 | Android | | paddingBottom | Android | Adjust the bottom padding of the subtitles. Default: 0 | Android |
| paddingLeft | Android | Adjust the left padding of the subtitles. Default: 0 | Android | | paddingLeft | Android | Adjust the left padding of the subtitles. Default: 0 | Android |
| paddingRight | Android | Adjust the right padding of the subtitles. Default: 0 | Android | | paddingRight | Android | Adjust the right padding of the subtitles. Default: 0 | Android |
| opacity | Android, iOS | Adjust the visibility of subtitles with 0 hiding and 1 fully showing them. Android supports float values between 0 and 1 for varying opacity levels, whereas iOS supports only 0 or 1. Default: 1. | Android, iOS | | opacity | Android, iOS | Adjust the visibility of subtitles with 0 hiding and 1 fully showing them. Android supports float values between 0 and 1 for varying opacity levels, whereas iOS supports only 0 or 1. Default: 1. | Android, iOS |
| subtitlesFollowVideo | Android | Boolean to adjust position of subtitles. Default: true | | subtitlesFollowVideo | Android | Boolean to adjust position of subtitles. Default: true |
**Example:** **Example:**
@@ -1101,10 +1167,10 @@ So there is a second view, the video view.
Subtitles are managed in a third view. Subtitles are managed in a third view.
* When `subtitlesFollowVideo` is set to true, the subtitle view will adapt to the video view. - When `subtitlesFollowVideo` is set to true, the subtitle view will adapt to the video view.
If the video is displayed out of screen, the subtitles may also be displayed out of screen. If the video is displayed out of screen, the subtitles may also be displayed out of screen.
* When `subtitlesFollowVideo` is set to false, the subtitle view will adapt to the main view. - When `subtitlesFollowVideo` is set to false, the subtitle view will adapt to the main view.
If the video is displayed out of screen, the subtitles may still remain visible within the main view. If the video is displayed out of screen, the subtitles may still remain visible within the main view.
This prop can be changed at runtime. This prop can be changed at runtime.
@@ -1121,12 +1187,12 @@ Load one or more "sidecar" text tracks. This takes an array of objects represent
> ⚠️ This feature does not work with HLS playlists (e.g., m3u8) on iOS. > ⚠️ This feature does not work with HLS playlists (e.g., m3u8) on iOS.
| Property | Description | | Property | Description |
|----------|-------------| | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| title | Descriptive name for the track | | title | Descriptive name for the track |
| language | 2-letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) representing the language | | language | 2-letter [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) representing the language |
| type | Mime type of the track (TextTrackType.SUBRIP - SubRip (.srt), TextTrackType.TTML - TTML (.ttml), TextTrackType.VTT - WebVTT (.vtt)). iOS only supports VTT, Android supports all 3. | | type | Mime type of the track (TextTrackType.SUBRIP - SubRip (.srt), TextTrackType.TTML - TTML (.ttml), TextTrackType.VTT - WebVTT (.vtt)). iOS only supports VTT, Android supports all 3. |
| uri | URL for the text track. Currently, only tracks hosted on a web server are supported. | | uri | URL for the text track. Currently, only tracks hosted on a web server are supported. |
On iOS, sidecar text tracks are only supported for individual files, not HLS playlists. For HLS, you should include the text tracks as part of the playlist. On iOS, sidecar text tracks are only supported for individual files, not HLS playlists. For HLS, you should include the text tracks as part of the playlist.
@@ -1240,8 +1306,9 @@ useTextureView can only be set at the same time you're setting the source.
Allows explicitly specifying the view type. Allows explicitly specifying the view type.
This flag replaces `useSecureView` and `useTextureView` fields. This flag replaces `useSecureView` and `useTextureView` fields.
There are 3 available values: There are 3 available values:
- 'textureView': The video is rendered in a texture view. It allows mapping the view on a texture (useful for 3D). - 'textureView': The video is rendered in a texture view. It allows mapping the view on a texture (useful for 3D).
DRM playback is not supported on textureView. If the DRM prop is provided, the surface will be transformed into a SurfaceView. DRM playback is not supported on textureView. If the DRM prop is provided, the surface will be transformed into a SurfaceView.
- 'surfaceView' (default): The video is rendered in a surface, taking fewer resources to render. - 'surfaceView' (default): The video is rendered in a surface, taking fewer resources to render.
- 'secureView': The video is rendered in a surface that prevents screenshots from being taken. - 'secureView': The video is rendered in a surface that prevents screenshots from being taken.
@@ -1269,22 +1336,25 @@ For detailed information about CMCD, please refer to the [CTA-5004 Final Specifi
When providing an object, you can configure the following properties: When providing an object, you can configure the following properties:
| Property | Type | Description | | Property | Type | Description |
|------------|------------|------------------------------------------------| | --------- | ---------- | ------------------------------------------------- |
| `mode` | `CmcdMode` | The mode for sending CMCD data | | `mode` | `CmcdMode` | The mode for sending CMCD data |
| `request` | `CmcdData` | Custom key-value pairs for the request object | | `request` | `CmcdData` | Custom key-value pairs for the request object |
| `session` | `CmcdData` | Custom key-value pairs for the session object | | `session` | `CmcdData` | Custom key-value pairs for the session object |
| `object` | `CmcdData` | Custom key-value pairs for the object metadata | | `object` | `CmcdData` | Custom key-value pairs for the object metadata |
| `status` | `CmcdData` | Custom key-value pairs for the status information | | `status` | `CmcdData` | Custom key-value pairs for the status information |
Note: The `mode` property defaults to `CmcdMode.MODE_QUERY_PARAMETER` if not specified. Note: The `mode` property defaults to `CmcdMode.MODE_QUERY_PARAMETER` if not specified.
#### `CmcdMode` #### `CmcdMode`
CmcdMode is an enum that defines how CMCD data should be sent: CmcdMode is an enum that defines how CMCD data should be sent:
- `CmcdMode.MODE_REQUEST_HEADER` (0) - Send CMCD data in the HTTP request headers. - `CmcdMode.MODE_REQUEST_HEADER` (0) - Send CMCD data in the HTTP request headers.
- `CmcdMode.MODE_QUERY_PARAMETER` (1) - Send CMCD data as query parameters in the URL. - `CmcdMode.MODE_QUERY_PARAMETER` (1) - Send CMCD data as query parameters in the URL.
#### `CmcdData` #### `CmcdData`
CmcdData is a type representing custom key-value pairs for CMCD data. It's defined as: CmcdData is a type representing custom key-value pairs for CMCD data. It's defined as:
```typescript ```typescript
@@ -1302,19 +1372,19 @@ Custom key names MUST include a hyphenated prefix to prevent namespace collision
cmcd: { cmcd: {
mode: CmcdMode.MODE_QUERY_PARAMETER, mode: CmcdMode.MODE_QUERY_PARAMETER,
request: { request: {
'com-custom-key': 'custom-value' 'com-custom-key': 'custom-value',
}, },
session: { session: {
sid: 'session-id' sid: 'session-id',
}, },
object: { object: {
br: '3000', br: '3000',
d: '4000' d: '4000',
}, },
status: { status: {
rtp: '1200' rtp: '1200',
} },
} },
}} }}
// or other video props // or other video props
/> />

View File

@@ -118,6 +118,7 @@ export const srcAllPlatformList = [
{ {
description: '(mp4) big buck bunny With Ads', description: '(mp4) big buck bunny With Ads',
ad: { ad: {
type: 'csai',
adTagUrl: 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=', '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=',
}, },

View File

@@ -1,18 +1,60 @@
public struct AdParams { public struct AdParams {
let type: String?
let streamType: String?
let adTagUrl: String? let adTagUrl: String?
let adLanguage: String? let adLanguage: String?
let contentSourceId: String?
let videoId: String?
let assetKey: String?
let format: String?
let adTagParameters: [String: String]?
let fallbackUri: String?
let json: NSDictionary? let json: NSDictionary?
var isCSAI: Bool { type == "csai" && adTagUrl != nil }
var isDAI: Bool { type == "ssai" }
var isDAIVod: Bool { type == "ssai" && streamType == "vod" }
var isDAILive: Bool { type == "ssai" && streamType == "live" }
init(_ json: NSDictionary!) { init(_ json: NSDictionary!) {
guard json != nil else { guard json != nil else {
self.json = nil self.json = nil
type = nil
streamType = nil
adTagUrl = nil adTagUrl = nil
adLanguage = nil adLanguage = nil
contentSourceId = nil
videoId = nil
assetKey = nil
format = nil
adTagParameters = nil
fallbackUri = nil
return return
} }
self.json = json self.json = json
type = json["type"] as? String
streamType = json["streamType"] as? String
adTagUrl = json["adTagUrl"] as? String adTagUrl = json["adTagUrl"] as? String
adLanguage = json["adLanguage"] as? String adLanguage = json["adLanguage"] as? String
contentSourceId = json["contentSourceId"] as? String
videoId = json["videoId"] as? String
assetKey = json["assetKey"] as? String
format = json["format"] as? String
fallbackUri = json["fallbackUri"] as? String
if let adTagParamsDict = json["adTagParameters"] as? [String: String] {
adTagParameters = adTagParamsDict
} else if let adTagParamsDict = json["adTagParameters"] as? NSDictionary {
var params: [String: String] = [:]
adTagParamsDict.enumerateKeysAndObjects { key, value, _ in
if let keyString = key as? String, let valueString = value as? String {
params[keyString] = valueString
}
}
adTagParameters = params.isEmpty ? nil : params
} else {
adTagParameters = nil
}
} }
} }

View File

@@ -2,7 +2,7 @@
import Foundation import Foundation
import GoogleInteractiveMediaAds import GoogleInteractiveMediaAds
class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, IMALinkOpenerDelegate { class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, IMALinkOpenerDelegate, IMAStreamManagerDelegate {
private weak var _video: RCTVideo? private weak var _video: RCTVideo?
private var _isPictureInPictureActive: () -> Bool private var _isPictureInPictureActive: () -> Bool
@@ -10,6 +10,12 @@
private var adsLoader: IMAAdsLoader! private var adsLoader: IMAAdsLoader!
/* Main point of interaction with the SDK. Created by the SDK as the result of an ad request. */ /* Main point of interaction with the SDK. Created by the SDK as the result of an ad request. */
private var adsManager: IMAAdsManager! private var adsManager: IMAAdsManager!
/* References the stream manager from the IMA DAI SDK after successfully loading the DAI stream. */
private var streamManager: IMAStreamManager?
/* Ad container view for DAI - stored to ensure proper z-ordering */
private var daiAdContainerView: UIView?
/* Picture-in-Picture proxy for DAI - stored to ensure proper Picture-in-Picture support */
private var pipProxy: IMAPictureInPictureProxy?
init(video: RCTVideo!, isPictureInPictureActive: @escaping () -> Bool) { init(video: RCTVideo!, isPictureInPictureActive: @escaping () -> Bool) {
_video = video _video = video
@@ -28,6 +34,18 @@
adsLoader.delegate = self adsLoader.delegate = self
} }
func setupDaiLoader() {
guard let _video else { return }
let settings = IMASettings()
// Enable background playback only if PiP or playInBackground is enabled
settings.enableBackgroundPlayback = _video.shouldEnableBackgroundPlayback()
if let adLanguage = _video.getAdLanguage() {
settings.language = adLanguage
}
adsLoader = IMAAdsLoader(settings: settings)
adsLoader.delegate = self
}
func requestAds() { func requestAds() {
guard let _video else { return } guard let _video else { return }
// fixes RCTVideo --> RCTIMAAdsManager --> IMAAdsLoader --> IMAAdDisplayContainer --> RCTVideo memory leak. // fixes RCTVideo --> RCTIMAAdsManager --> IMAAdsLoader --> IMAAdDisplayContainer --> RCTVideo memory leak.
@@ -54,14 +72,86 @@
} }
} }
func requestDaiStream() {
guard let _video else { return }
// fixes RCTVideo --> RCTIMAAdsManager --> IMAAdsLoader --> IMAAdDisplayContainer --> RCTVideo memory leak.
let adContainerView = UIView(frame: _video.bounds)
adContainerView.backgroundColor = .clear
_video.addSubview(adContainerView)
// Store reference for later z-ordering management. DAI requires ad container to stay on top for proper ad UI visibility
daiAdContainerView = adContainerView
// Create ad display container for ad rendering.
let adDisplayContainer = IMAAdDisplayContainer(adContainer: adContainerView, viewController: _video.reactViewController())
let contentSourceID = _video.getContentSourceId()
let videoID = _video.getVideoId()
let imaVideoDisplay = _video.getIMAVideoDisplay()
let assetKey = _video.getAssetKey()
let adTagParameters = _video.getAdTagParameters()
if let pip = _video.getPip() {
pipProxy = IMAPictureInPictureProxy(avPictureInPictureControllerDelegate: pip)
} else {
pipProxy = nil
}
// Request DAI stream for VOD (Video On Demand)
// Requires both contentSourceId (CMS ID) and videoId to identify the content
if _video.isDAIVod() {
let request = IMAVODStreamRequest(
contentSourceID: contentSourceID!,
videoID: videoID!,
adDisplayContainer: adDisplayContainer,
videoDisplay: imaVideoDisplay!,
pictureInPictureProxy: pipProxy,
userContext: nil
)
// Apply adTagParameters if provided
if let adTagParams = adTagParameters {
request.adTagParameters = adTagParams
}
adsLoader.requestStream(with: request)
// Request DAI stream for live content
// Uses assetKey to identify the live stream
} else if _video.isDAILive() {
let request = IMALiveStreamRequest(
assetKey: assetKey!,
adDisplayContainer: adDisplayContainer,
videoDisplay: imaVideoDisplay!,
pictureInPictureProxy: pipProxy,
userContext: nil
)
// Apply adTagParameters if provided
if let adTagParams = adTagParameters {
request.adTagParameters = adTagParams
}
adsLoader.requestStream(with: request)
}
}
func releaseAds() { func releaseAds() {
guard let adsManager else { return } // CSAI
// Destroy AdsManager may be delayed for a few milliseconds if let adsManager {
// But what we want is it stopped producing sound immediately // Destroy AdsManager may be delayed for a few milliseconds
// Issue found on tvOS 17, or iOS if view detach & STARTED event happen at the same moment // But what we want is it stopped producing sound immediately
adsManager.volume = 0 // Issue found on tvOS 17, or iOS if view detach & STARTED event happen at the same moment
adsManager.pause() adsManager.volume = 0
adsManager.destroy() adsManager.pause()
adsManager.destroy()
}
// DAI
if let streamManager {
streamManager.destroy()
self.streamManager = nil
daiAdContainerView = nil
}
} }
// MARK: - Getters // MARK: - Getters
@@ -78,24 +168,71 @@
func adsLoader(_: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) { func adsLoader(_: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) {
guard let _video else { return } guard let _video else { return }
// Grab the instance of the IMAAdsManager and set yourself as the delegate.
adsManager = adsLoadedData.adsManager
adsManager?.delegate = self
// Create ads rendering settings and tell the SDK to use the in-app browser. // Check if this is a stream manager (DAI) or ads manager (CSAI)
let adsRenderingSettings = IMAAdsRenderingSettings() // The adsLoadedData will contain either streamManager (for DAI) or adsManager (for CSAI)
adsRenderingSettings.linkOpenerDelegate = self if let streamMgr = adsLoadedData.streamManager {
adsRenderingSettings.linkOpenerPresentingController = _video.reactViewController() streamManager = streamMgr
streamManager?.delegate = self
adsManager.initialize(with: adsRenderingSettings) // Ensure ad container stays on top when stream initializes
// This is critical for DAI as ad overlays need to be visible
if let adContainerView = daiAdContainerView {
_video.bringSubviewToFront(adContainerView)
}
// For DAI, the stream manager + IMAVideoDisplay automatically load content
// No need to extract a URL - the stream manager handles playback directly
// Just initialize and the player will start automatically
self.streamManager?.initialize(with: nil)
} else {
// CSAI: Client-side ad insertion - ads are inserted by the client app
self.adsManager = adsLoadedData.adsManager
self.adsManager?.delegate = self
// Create ads rendering settings and tell the SDK to use the in-app browser.
let adsRenderingSettings = IMAAdsRenderingSettings()
adsRenderingSettings.linkOpenerDelegate = self
adsRenderingSettings.linkOpenerPresentingController = _video.reactViewController()
self.adsManager?.initialize(with: adsRenderingSettings)
}
} }
func adsLoader(_: IMAAdsLoader, failedWith adErrorData: IMAAdLoadingErrorData) { func adsLoader(_: IMAAdsLoader, failedWith adErrorData: IMAAdLoadingErrorData) {
guard let _video else { return }
if adErrorData.adError.message != nil { if adErrorData.adError.message != nil {
print("Error loading ads: " + adErrorData.adError.message!) print("Error loading ads: " + adErrorData.adError.message!)
} }
_video?.setPaused(false) // CSAI
if adsManager != nil {
_video.setPaused(false)
}
// DAI
if streamManager != nil {
_video.isSetSourceOngoing = false
_video.applyNextSource()
// Handle DAI error by falling back to backup content if available
// This provides resilience when DAI stream fails or is unavailable
if let backupStreamUri = _video.getBackupStreamUri() {
print("DAI stream error occurred, falling back to backup stream URI: \(backupStreamUri)")
// Clean up DAI resources before switching to backup
releaseAds()
// Switch to backup stream - create a simple source dictionary with the URI
// The backup stream typically contains the content without ads
let backupSource: NSDictionary = [
"uri": backupStreamUri,
"isNetwork": true,
]
DispatchQueue.main.async {
_video.setSrc(backupSource)
}
}
}
} }
// MARK: - IMAAdsManagerDelegate // MARK: - IMAAdsManagerDelegate
@@ -167,6 +304,49 @@
_video?.setPaused(false) _video?.setPaused(false)
} }
// MARK: - IMAStreamManagerDelegate
func streamManager(_: IMAStreamManager, didReceive event: IMAAdEvent) {
guard let _video else { return }
if _video.onReceiveAdEvent != nil {
let type = convertEventToString(event: event.type)
if event.adData != nil {
_video.onReceiveAdEvent?([
"event": type,
"data": event.adData ?? [String](),
"target": _video.reactTag!,
])
} else {
_video.onReceiveAdEvent?([
"event": type,
"target": _video.reactTag!,
])
}
}
}
func streamManager(_: IMAStreamManager, didReceive error: IMAAdError) {
if error.message != nil {
print("AdsManager error: " + error.message!)
}
guard let _video else { return }
if _video.onReceiveAdEvent != nil {
_video.onReceiveAdEvent?([
"event": "ERROR",
"data": [
"message": error.message ?? "",
"code": error.code,
"type": error.type,
],
"target": _video.reactTag!,
])
}
}
// MARK: - IMALinkOpenerDelegate // MARK: - IMALinkOpenerDelegate
func linkOpenerDidClose(inAppLink _: NSObject) { func linkOpenerDidClose(inAppLink _: NSObject) {

View File

@@ -87,6 +87,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
private var _imaAdsManager: RCTIMAAdsManager! private var _imaAdsManager: RCTIMAAdsManager!
/* Playhead used by the SDK to track content video progress and insert mid-rolls. */ /* Playhead used by the SDK to track content video progress and insert mid-rolls. */
private var _contentPlayhead: IMAAVPlayerContentPlayhead? private var _contentPlayhead: IMAAVPlayerContentPlayhead?
/* The reference of your video player for the IMA DAI SDK to monitor playback and handle timed metadata */
private var _imaVideoDisplay: IMAAVPlayerVideoDisplay?
#endif #endif
private var _didRequestAds = false private var _didRequestAds = false
private var _adPlaying = false private var _adPlaying = false
@@ -438,7 +440,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
if currentTimeSecs >= 0 { if currentTimeSecs >= 0 {
#if USE_GOOGLE_IMA #if USE_GOOGLE_IMA
if !_didRequestAds && currentTimeSecs >= 0.0001 && _source?.adParams.adTagUrl != nil { if !_didRequestAds && currentTimeSecs >= 0.0001 && _source?.adParams.isCSAI == true {
_imaAdsManager.requestAds() _imaAdsManager.requestAds()
_didRequestAds = true _didRequestAds = true
} }
@@ -513,7 +515,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
applyNextSource() applyNextSource()
throw NSError(domain: "", code: 0, userInfo: nil) throw NSError(domain: "", code: 0, userInfo: nil)
} }
if let startPosition = _source?.startPosition { if let startPosition = _source?.startPosition {
_startPosition = startPosition / 1000 _startPosition = startPosition / 1000
} }
@@ -621,10 +622,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
} }
#if USE_GOOGLE_IMA #if USE_GOOGLE_IMA
if _source?.adParams.adTagUrl != nil { if _source?.adParams.isCSAI == true {
// Set up your content playhead and contentComplete callback.
_contentPlayhead = IMAAVPlayerContentPlayhead(avPlayer: _player!) _contentPlayhead = IMAAVPlayerContentPlayhead(avPlayer: _player!)
_imaAdsManager.setUpAdsLoader() _imaAdsManager.setUpAdsLoader()
} }
#endif #endif
@@ -644,6 +643,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
let initializeSource = { let initializeSource = {
self._source = VideoSource(source) self._source = VideoSource(source)
#if USE_GOOGLE_IMA
if self.isDaiSource() {
self.handleDaiSource()
return
}
#endif
if self._source?.uri == nil || self._source?.uri == "" { if self._source?.uri == nil || self._source?.uri == "" {
self._player?.replaceCurrentItem(with: nil) self._player?.replaceCurrentItem(with: nil)
self.isSetSourceOngoing = false self.isSetSourceOngoing = false
@@ -1162,10 +1168,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
} }
func usePlayerViewController() { func usePlayerViewController() {
guard let _player, let _playerItem else { return } guard let _player else { return }
if _playerViewController == nil { if _playerViewController == nil {
_playerViewController = createPlayerViewController(player: _player, withPlayerItem: _playerItem) _playerViewController = createPlayerViewController(player: _player)
} }
// to prevent video from being animated when resizeMode is 'cover' // to prevent video from being animated when resizeMode is 'cover'
// resize mode must be set before subview is added // resize mode must be set before subview is added
@@ -1189,7 +1195,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
_playerObserver.playerViewController = _playerViewController _playerObserver.playerViewController = _playerViewController
} }
func createPlayerViewController(player: AVPlayer, withPlayerItem _: AVPlayerItem) -> RCTVideoPlayerViewController { func createPlayerViewController(player: AVPlayer) -> RCTVideoPlayerViewController {
let viewController = RCTVideoPlayerViewController() let viewController = RCTVideoPlayerViewController()
viewController.showsPlaybackControls = self._controls viewController.showsPlaybackControls = self._controls
#if !os(tvOS) #if !os(tvOS)
@@ -1354,6 +1360,17 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
return _source?.adParams.adTagUrl return _source?.adParams.adTagUrl
} }
func getPip() -> RCTPictureInPicture? {
initPictureinPicture()
return _pip
}
/// Returns whether background playback should be enabled for IMA DAI SDK.
/// Used to configure `IMASettings.enableBackgroundPlayback` which is required for DAI streams
func shouldEnableBackgroundPlayback() -> Bool {
return _playInBackground || _enterPictureInPictureOnLeave
}
#if USE_GOOGLE_IMA #if USE_GOOGLE_IMA
func getContentPlayhead() -> IMAAVPlayerContentPlayhead? { func getContentPlayhead() -> IMAAVPlayerContentPlayhead? {
return _contentPlayhead return _contentPlayhead
@@ -1618,7 +1635,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
"" : (_playerItem.error! as NSError).localizedFailureReason) ?? "", "" : (_playerItem.error! as NSError).localizedFailureReason) ?? "",
"localizedRecoverySuggestion": ((_playerItem.error! as NSError).localizedRecoverySuggestion == nil ? "localizedRecoverySuggestion": ((_playerItem.error! as NSError).localizedRecoverySuggestion == nil ?
"" : (_playerItem.error! as NSError).localizedRecoverySuggestion) ?? "", "" : (_playerItem.error! as NSError).localizedRecoverySuggestion) ?? "",
"domain": (_playerItem.error as! NSError).domain, "domain": (_playerItem.error as NSError?)?.domain ?? "",
], ],
"target": reactTag as Any, "target": reactTag as Any,
] ]
@@ -1864,7 +1881,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
@objc @objc
func exitPictureInPicture() { func exitPictureInPicture() {
guard isPictureInPictureActive() else { return } guard isPictureInPictureActive() else { return }
_pip?.exitPictureInPicture() _pip?.exitPictureInPicture()
if _enterPictureInPictureOnLeave { if _enterPictureInPictureOnLeave {
initPictureinPicture() initPictureinPicture()
@@ -1877,3 +1893,186 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
@objc @objc
func setOnClick(_: Any) {} func setOnClick(_: Any) {}
} }
// MARK: - DAI Support
#if USE_GOOGLE_IMA
extension RCTVideo: IMAAVPlayerVideoDisplayDelegate {
/// Checks if the current source is a DAI (Dynamic Ad Insertion) request.
///
/// Returns `true` if either:
/// - VOD request: both `contentSourceId` and `videoId` are present
func isDaiSource() -> Bool {
return _source?.adParams.isDAI ?? false
}
func isDAIVod() -> Bool {
return _source?.adParams.isDAIVod ?? false
}
func isDAILive() -> Bool {
return _source?.adParams.isDAILive ?? false
}
func getContentSourceId() -> String? {
return _source?.adParams.contentSourceId
}
func getAssetKey() -> String? {
return _source?.adParams.assetKey
}
func getVideoId() -> String? {
return _source?.adParams.videoId
}
func getAdTagParameters() -> [String: String]? {
return _source?.adParams.adTagParameters
}
func getBackupStreamUri() -> String? {
return _source?.adParams.fallbackUri
}
/// Returns the IMA video display instance used for DAI playback.
func getIMAVideoDisplay() -> IMAVideoDisplay? {
return _imaVideoDisplay
}
/// Sets up DAI (Dynamic Ad Insertion) by preparing the player, setting up the DAI loader, and requesting the stream.
/// This method must be called on the main thread as it performs UI operations.
func handleDaiSource() {
DispatchQueue.main.sync {
removePlayerLayer()
_playerObserver.player = nil
_playerObserver.playerItem = nil
_drmManager = nil
preparePlayerForDai()
_imaVideoDisplay = IMAAVPlayerVideoDisplay(avPlayer: _player!)
_imaVideoDisplay?.playerVideoDisplayDelegate = self
if _controls {
usePlayerViewController()
} else {
usePlayerLayer()
}
_imaAdsManager.setupDaiLoader()
_imaAdsManager.requestDaiStream()
_videoLoadStarted = true
}
}
/// Prepares the AVPlayer for DAI playback by initializing or resetting the player configuration.
func preparePlayerForDai() {
if !isSetSourceOngoing {
DebugLog("setSrc has been canceled last step")
return
}
if _player == nil {
_player = AVPlayer()
ReactNativeVideoManager.shared.onInstanceCreated(id: instanceId, player: _player as Any)
}
_player!.pause()
_player!.replaceCurrentItem(with: nil)
_player!.actionAtItemEnd = .none
if #available(iOS 10.0, *) {
_player!.automaticallyWaitsToMinimizeStalling = _automaticallyWaitsToMinimizeStalling
}
if #available(iOS 15.0, *) {
if _playInBackground {
_player!.audiovisualBackgroundPlaybackPolicy = .continuesIfPossible
} else {
_player!.audiovisualBackgroundPlaybackPolicy = .automatic
}
}
_playerObserver.player = _player
}
/// Sets up the player item with all item-specific configurations for DAI playback.
///
/// This method should be called after `preparePlayerForDai()` when a player item becomes available.
///
/// - Parameter playerItem: The AVPlayerItem to configure and set on the player
func setupDaiPlayerItem(_ playerItem: AVPlayerItem) async throws {
if !isSetSourceOngoing {
DebugLog("setSrc has been canceled last step")
return
}
guard let _player else {
throw NSError(domain: "RCTVideo", code: -1, userInfo: [NSLocalizedDescriptionKey: "Player not initialized. Call preparePlayerForDai() first."])
}
_playerItem = playerItem
_playerObserver.playerItem = _playerItem
setPreferredForwardBufferDuration(_preferredForwardBufferDuration)
setPlaybackRange(playerItem, withCropStart: _source?.cropStart, withCropEnd: _source?.cropEnd)
setFilter(_filterName)
if let maxBitRate = _maxBitRate {
_playerItem?.preferredPeakBitRate = Double(maxBitRate)
}
_player.replaceCurrentItem(with: playerItem)
#if !os(tvOS) && !os(visionOS)
if #available(iOS 16.0, macCatalyst 18.0, *) {
self._playerViewController?.allowsVideoFrameAnalysis = false
self._playerViewController?.allowsVideoFrameAnalysis = true
}
#endif
if _showNotificationControls {
NowPlayingInfoCenterManager.shared.registerPlayer(player: _player)
} else {
NowPlayingInfoCenterManager.shared.updateNowPlayingInfo()
}
applyModifiers()
isSetSourceOngoing = false
applyNextSource()
}
/// Called when the IMA video display loads a player item for DAI playback.
///
/// This delegate method is invoked by the IMA SDK when the DAI stream player item is ready.
/// It sets up the player item and applies all necessary modifiers.
///
/// - Parameters:
/// - playerVideoDisplay: The IMA video display instance
/// - playerItem: The AVPlayerItem loaded by the IMA SDK
func playerVideoDisplay(_: IMAAVPlayerVideoDisplay,
didLoad playerItem: AVPlayerItem) {
RCTVideoUtils.delay { [weak self] in
do {
guard let self else { throw NSError(domain: "", code: 0, userInfo: nil) }
try await self.setupDaiPlayerItem(playerItem)
} catch {
DebugLog("An error occurred: \(error.localizedDescription)")
if let self {
self.onVideoError?(["error": error.localizedDescription])
self.isSetSourceOngoing = false
self.applyNextSource()
if let player = self._player {
NowPlayingInfoCenterManager.shared.removePlayer(player: player)
}
}
}
}
}
}
#endif

View File

@@ -78,14 +78,9 @@ RCT_EXTERN_METHOD(enterPictureInPictureCmd : (nonnull NSNumber*)reactTag)
RCT_EXTERN_METHOD(exitPictureInPictureCmd : (nonnull NSNumber*)reactTag) RCT_EXTERN_METHOD(exitPictureInPictureCmd : (nonnull NSNumber*)reactTag)
RCT_EXTERN_METHOD(setSourceCmd : (nonnull NSNumber*)reactTag source : (NSDictionary*)source) RCT_EXTERN_METHOD(setSourceCmd : (nonnull NSNumber*)reactTag source : (NSDictionary*)source)
RCT_EXTERN_METHOD(save RCT_EXTERN_METHOD(save : (nonnull NSNumber*)reactTag options : (NSDictionary*)options resolve : (RCTPromiseResolveBlock)
: (nonnull NSNumber*)reactTag options resolve reject : (RCTPromiseRejectBlock)reject)
: (NSDictionary*)options resolve RCT_EXTERN_METHOD(getCurrentPosition : (nonnull NSNumber*)reactTag resolve : (RCTPromiseResolveBlock)
: (RCTPromiseResolveBlock)resolve reject resolve reject : (RCTPromiseRejectBlock)reject)
: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(getCurrentPosition
: (nonnull NSNumber*)reactTag resolve
: (RCTPromiseResolveBlock)resolve reject
: (RCTPromiseRejectBlock)reject)
@end @end

View File

@@ -53,7 +53,57 @@ import type {
ReactVideoProps, ReactVideoProps,
CmcdData, CmcdData,
ReactVideoSource, ReactVideoSource,
AdConfig,
AdConfigDAIVod,
AdConfigDAILive,
} from './types'; } from './types';
import type {ISO639_1} from './types/language';
import type {AdsConfig} from './specs/VideoNativeComponent';
function normalizeAdConfig(
ad: AdConfig | undefined,
legacyAdTagUrl: string | undefined,
legacyAdLanguage: ISO639_1 | undefined,
): AdsConfig | undefined {
if (ad) {
// Default to 'csai' for backward compatibility with old API (ad without type)
const adType = 'type' in ad ? ad.type : 'csai';
if (adType === 'ssai') {
const daiAd = ad as AdConfigDAIVod | AdConfigDAILive;
return {
type: 'ssai',
streamType: daiAd.streamType,
adLanguage: daiAd.adLanguage,
contentSourceId:
'contentSourceId' in daiAd ? daiAd.contentSourceId : undefined,
videoId: 'videoId' in daiAd ? daiAd.videoId : undefined,
assetKey: 'assetKey' in daiAd ? daiAd.assetKey : undefined,
format: daiAd.format,
adTagParameters: daiAd.adTagParameters,
fallbackUri: daiAd.fallbackUri,
};
}
// CSAI (explicit or default for backward compatibility)
return {
type: 'csai',
adTagUrl: 'adTagUrl' in ad ? ad.adTagUrl : undefined,
adLanguage: ad.adLanguage,
};
}
// Legacy props at <Video> component level
if (legacyAdTagUrl || legacyAdLanguage) {
return {
type: 'csai',
adTagUrl: legacyAdTagUrl,
adLanguage: legacyAdLanguage,
};
}
return undefined;
}
const Video = forwardRef<VideoRef, ReactVideoProps>( const Video = forwardRef<VideoRef, ReactVideoProps>(
( (
@@ -166,7 +216,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
if (uri && uri.match(/^\//)) { if (uri && uri.match(/^\//)) {
uri = `file://${uri}`; uri = `file://${uri}`;
} }
if (!uri) { if (!uri && _source.ad?.type !== 'ssai') {
console.log('Trying to load empty source'); console.log('Trying to load empty source');
} }
const isNetwork = !!(uri && uri.match(/^(rtp|rtsp|http|https):/)); const isNetwork = !!(uri && uri.match(/^(rtp|rtsp|http|https):/));
@@ -222,11 +272,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
const selectedContentStartTime = const selectedContentStartTime =
_source.contentStartTime || contentStartTime; _source.contentStartTime || contentStartTime;
const _ad = const _ad = normalizeAdConfig(_source.ad, adTagUrl, adLanguage);
_source.ad ||
(adTagUrl || adLanguage
? {adTagUrl: adTagUrl, adLanguage: adLanguage}
: undefined);
const _minLoadRetryCount = const _minLoadRetryCount =
_source.minLoadRetryCount || minLoadRetryCount; _source.minLoadRetryCount || minLoadRetryCount;

View File

@@ -27,8 +27,16 @@ type VideoMetadata = Readonly<{
}>; }>;
export type AdsConfig = Readonly<{ export type AdsConfig = Readonly<{
type?: string;
streamType?: string;
adTagUrl?: string; adTagUrl?: string;
adLanguage?: string; adLanguage?: string;
contentSourceId?: string;
videoId?: string;
assetKey?: string;
format?: string;
adTagParameters?: Record<string, string>;
fallbackUri?: string;
}>; }>;
export type VideoSrc = Readonly<{ export type VideoSrc = Readonly<{

View File

@@ -77,11 +77,45 @@ export enum DRMType {
FAIRPLAY = 'fairplay', FAIRPLAY = 'fairplay',
} }
export type AdConfig = Readonly<{ export type DaiFormat = 'hls' | 'dash';
adTagUrl?: string; export type DaiStreamType = 'vod' | 'live';
type AdConfigBase = Readonly<{
adLanguage?: ISO639_1; adLanguage?: ISO639_1;
}>; }>;
export type AdConfigCSAI = AdConfigBase &
Readonly<{
type: 'csai';
adTagUrl: string;
}>;
type AdConfigDAIBase = AdConfigBase &
Readonly<{
type: 'ssai';
streamType: DaiStreamType;
format?: DaiFormat;
adTagParameters?: Record<string, string>;
fallbackUri?: string;
}>;
export type AdConfigDAIVod = AdConfigDAIBase &
Readonly<{
streamType: 'vod';
contentSourceId: string;
videoId: string;
}>;
export type AdConfigDAILive = AdConfigDAIBase &
Readonly<{
streamType: 'live';
assetKey: string;
}>;
export type AdConfigDAI = AdConfigDAIVod | AdConfigDAILive;
export type AdConfig = AdConfigCSAI | AdConfigDAI;
export type Drm = Readonly<{ export type Drm = Readonly<{
type?: DRMType; type?: DRMType;
licenseServer?: string; licenseServer?: string;