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:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@@ -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 won’t exceed this limit. |
|
| maxOffsetMs | number | Maximum allowed live offset. The player won’t exceed this limit. |
|
||||||
| minOffsetMs | number | Minimum allowed live offset. The player won’t go below this limit. |
|
| minOffsetMs | number | Minimum allowed live offset. The player won’t 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 | Don’t display a text track. |
|
| "disabled" | N/A | Don’t 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
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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=',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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<{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user