feat: add setSource API function fix ads playback (#4185)

* feat: add setSource API function fix ads playback
This commit is contained in:
Olivier Bouillet 2024-10-10 22:59:41 +02:00 committed by GitHub
parent 4c9db2845b
commit 9a3fcda3b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 189 additions and 133 deletions

View File

@ -93,6 +93,13 @@ public final class ExoPlayerView extends FrameLayout implements AdViewProvider {
}
}
public void showAds() {
adOverlayFrameLayout.setVisibility(View.GONE);
}
public void hideAds() {
adOverlayFrameLayout.setVisibility(View.VISIBLE);
}
private void clearVideoView() {
if (surfaceView instanceof TextureView) {
player.clearVideoTextureView((TextureView) surfaceView);
@ -189,7 +196,7 @@ public final class ExoPlayerView extends FrameLayout implements AdViewProvider {
surfaceView.setAlpha(0);
}
private void updateShutterViewVisibility() {
public void updateShutterViewVisibility() {
if (this.hideShutterView) {
hideShutterView();
} else {

View File

@ -735,22 +735,28 @@ public class ReactExoplayerView extends FrameLayout implements
ReactExoplayerView self = this;
Activity activity = themedReactContext.getCurrentActivity();
// This ensures all props have been settled, to avoid async racing conditions.
Source runningSource = source;
mainRunnable = () -> {
if (viewHasDropped) {
if (viewHasDropped && runningSource == source) {
return;
}
try {
if (runningSource.getUri() == null) {
return;
}
if (player == null) {
// Initialize core configuration and listeners
initializePlayerCore(self);
}
if (playerNeedsSource && source.getUri() != null) {
if (playerNeedsSource) {
// Will force display of shutter view if needed
exoPlayerView.updateShutterViewVisibility();
exoPlayerView.invalidateAspectRatio();
// DRM session manager creation must be done on a different thread to prevent crashes so we start a new thread
ExecutorService es = Executors.newSingleThreadExecutor();
es.execute(() -> {
// DRM initialization must run on a different thread
if (viewHasDropped) {
if (viewHasDropped && runningSource == source) {
return;
}
if (activity == null) {
@ -761,12 +767,12 @@ public class ReactExoplayerView extends FrameLayout implements
// Initialize handler to run on the main thread
activity.runOnUiThread(() -> {
if (viewHasDropped) {
if (viewHasDropped && runningSource == source) {
return;
}
try {
// Source initialization must run on the main thread
initializePlayerSource();
initializePlayerSource(runningSource);
} catch (Exception ex) {
self.playerNeedsSource = true;
DebugLog.e(TAG, "Failed to initialize Player! 1");
@ -776,8 +782,8 @@ public class ReactExoplayerView extends FrameLayout implements
}
});
});
} else if (source.getUri() != null) {
initializePlayerSource();
} else if (runningSource == source) {
initializePlayerSource(runningSource);
}
} catch (Exception ex) {
self.playerNeedsSource = true;
@ -816,6 +822,11 @@ public class ReactExoplayerView extends FrameLayout implements
.setEnableDecoderFallback(true)
.forceEnableMediaCodecAsynchronousQueueing();
DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory);
if (useCache) {
mediaSourceFactory.setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true)));
}
ImaSdkSettings imaSdkSettings = ImaSdkFactory.getInstance().createImaSdkSettings();
imaSdkSettings.setLanguage(adLanguage);
@ -826,14 +837,7 @@ public class ReactExoplayerView extends FrameLayout implements
.setAdEventListener(this)
.setAdErrorListener(this)
.build();
DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory);
if (useCache) {
mediaSourceFactory.setDataSourceFactory(RNVSimpleCache.INSTANCE.getCacheFactory(buildHttpDataSourceFactory(true)));
}
if (adsLoader != null) {
mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView);
}
mediaSourceFactory.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView);
player = new ExoPlayer.Builder(getContext(), renderersFactory)
.setTrackSelector(self.trackSelector)
@ -846,6 +850,7 @@ public class ReactExoplayerView extends FrameLayout implements
player.addListener(self);
player.setVolume(muted ? 0.f : audioVolume * 1);
exoPlayerView.setPlayer(player);
if (adsLoader != null) {
adsLoader.setPlayer(player);
}
@ -884,31 +889,28 @@ public class ReactExoplayerView extends FrameLayout implements
return drmSessionManager;
}
private void initializePlayerSource() {
if (source.getUri() == null) {
private void initializePlayerSource(Source runningSource) {
if (runningSource.getUri() == null) {
return;
}
/// init DRM
DrmSessionManager drmSessionManager = initializePlayerDrm();
if (drmSessionManager == null && source.getDrmProps() != null && source.getDrmProps().getDrmType() != null) {
if (drmSessionManager == null && runningSource.getDrmProps() != null && runningSource.getDrmProps().getDrmType() != null) {
// Failed to initialize DRM session manager - cannot continue
DebugLog.e(TAG, "Failed to initialize DRM Session Manager Framework!");
return;
}
// init source to manage ads and external text tracks
ArrayList<MediaSource> mediaSourceList = buildTextSources();
MediaSource videoSource = buildMediaSource(source.getUri(), source.getExtension(), drmSessionManager, source.getCropStartMs(), source.getCropEndMs());
MediaSource videoSource = buildMediaSource(runningSource.getUri(), runningSource.getExtension(), drmSessionManager, runningSource.getCropStartMs(), runningSource.getCropEndMs());
MediaSource mediaSourceWithAds = null;
if (adTagUrl != null && adsLoader != null) {
if (adTagUrl != null && BuildConfig.USE_EXOPLAYER_IMA) {
DefaultMediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory)
.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView);
DataSpec adTagDataSpec = new DataSpec(adTagUrl);
mediaSourceWithAds = new AdsMediaSource(videoSource, adTagDataSpec, ImmutableList.of(source.getUri(), adTagUrl), mediaSourceFactory, adsLoader, exoPlayerView);
} else {
if (adTagUrl == null && adsLoader != null) {
adsLoader.release();
adsLoader = null;
}
DebugLog.w(TAG, "ads " + adTagUrl);
mediaSourceWithAds = new AdsMediaSource(videoSource, adTagDataSpec, ImmutableList.of(runningSource.getUri(), adTagUrl), mediaSourceFactory, adsLoader, exoPlayerView);
exoPlayerView.showAds();
}
MediaSource mediaSource;
if (mediaSourceList.isEmpty()) {
@ -943,8 +945,8 @@ public class ReactExoplayerView extends FrameLayout implements
if (haveResumePosition) {
player.seekTo(resumeWindow, resumePosition);
player.setMediaSource(mediaSource, false);
} else if (source.getStartPositionMs() > 0) {
player.setMediaSource(mediaSource, source.getStartPositionMs());
} else if (runningSource.getStartPositionMs() > 0) {
player.setMediaSource(mediaSource, runningSource.getStartPositionMs());
} else {
player.setMediaSource(mediaSource, true);
}
@ -1243,10 +1245,6 @@ public class ReactExoplayerView extends FrameLayout implements
private void releasePlayer() {
if (player != null) {
if (adsLoader != null) {
adsLoader.setPlayer(null);
}
if(playbackServiceBinder != null) {
playbackServiceBinder.getService().unregisterPlayer(player);
themedReactContext.unbindService(playbackServiceConnection);
@ -1903,19 +1901,21 @@ public class ReactExoplayerView extends FrameLayout implements
if (!isSourceEqual) {
reloadSource();
}
} else {
clearSrc();
}
}
public void clearSrc() {
if (source.getUri() != null) {
if (player != null) {
player.stop();
player.clearMediaItems();
}
this.source = new Source();
this.mediaDataSourceFactory = null;
clearResumePosition();
}
exoPlayerView.hideAds();
this.source = new Source();
this.mediaDataSourceFactory = null;
clearResumePosition();
}
public void setProgressUpdateInterval(final float progressUpdateInterval) {
@ -1927,6 +1927,7 @@ public class ReactExoplayerView extends FrameLayout implements
}
public void setAdTagUrl(final Uri uri) {
DebugLog.w(TAG, "setAdTagUrl" + uri);
adTagUrl = uri;
}

View File

@ -89,12 +89,7 @@ class ReactExoplayerViewManager(private val config: ReactExoplayerConfig) : View
@ReactProp(name = PROP_SRC)
fun setSrc(videoView: ReactExoplayerView, src: ReadableMap?) {
val context = videoView.context.applicationContext
val source = Source.parse(src, context)
if (source.uri == null) {
videoView.clearSrc()
} else {
videoView.setSrc(source)
}
videoView.setSrc(Source.parse(src, context))
}
@ReactProp(name = PROP_AD_TAG_URL)

View File

@ -1,10 +1,12 @@
package com.brentvatne.react
import com.brentvatne.common.api.Source
import com.brentvatne.exoplayer.ReactExoplayerView
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.common.UIManagerType
@ -63,6 +65,13 @@ class VideoManagerModule(reactContext: ReactApplicationContext?) : ReactContextB
}
}
@ReactMethod
fun setSourceCmd(reactTag: Int, source: ReadableMap?) {
performOnPlayerView(reactTag) {
it?.setSrc(Source.parse(source, reactApplicationContext))
}
}
@ReactMethod
fun getCurrentPosition(reactTag: Int, promise: Promise) {
performOnPlayerView(reactTag) {

View File

@ -115,6 +115,16 @@ This function will change the volume exactly like [volume](./props#volume) prope
This function retrieves and returns the precise current position of the video playback, measured in seconds.
This function will throw an error if player is not initialized.
### `setSource`
<PlatformsList types={['Android', 'iOS']} />
`setSource(source: ReactVideoSource): Promise<void>`
This function will change the source exactly like [source](./props#source) property.
Changing source with this function will overide source provided as props.
### `setFullScreen`
<PlatformsList types={['Android', 'iOS']} />

View File

@ -252,6 +252,10 @@ const VideoPlayer: FC<Props> = ({}) => {
cacheSizeMB: useCache ? 200 : 0,
};
useEffect(() => {
videoRef.current?.setSource(currentSrc)
}, [currentSrc])
return (
<View style={styles.container}>
<StatusBar animated={true} backgroundColor="black" hidden={false} />
@ -261,7 +265,7 @@ const VideoPlayer: FC<Props> = ({}) => {
<Video
showNotificationControls={showNotificationControls}
ref={videoRef}
source={currentSrc as ReactVideoSource}
// source={currentSrc as ReactVideoSource}
adTagUrl={additional?.adTagUrl}
drm={additional?.drm}
style={viewStyle}

View File

@ -75,6 +75,7 @@ RCT_EXTERN_METHOD(setLicenseResultErrorCmd : (nonnull NSNumber*)reactTag error :
RCT_EXTERN_METHOD(setPlayerPauseStateCmd : (nonnull NSNumber*)reactTag paused : (nonnull BOOL)paused)
RCT_EXTERN_METHOD(setVolumeCmd : (nonnull NSNumber*)reactTag volume : (nonnull float*)volume)
RCT_EXTERN_METHOD(setFullScreenCmd : (nonnull NSNumber*)reactTag fullscreen : (nonnull BOOL)fullScreen)
RCT_EXTERN_METHOD(setSourceCmd : (nonnull NSNumber*)reactTag source : (NSDictionary*)source)
RCT_EXTERN_METHOD(save
: (nonnull NSNumber*)reactTag options

View File

@ -72,6 +72,13 @@ class RCTVideoManager: RCTViewManager {
})
}
@objc(setSourceCmd:source:)
func setSourceCmd(_ reactTag: NSNumber, source: NSDictionary) {
performOnVideoView(withReactTag: reactTag, callback: { videoView in
videoView?.setSrc(source)
})
}
@objc(save:options:resolve:reject:)
func save(_ reactTag: NSNumber, options: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
performOnVideoView(withReactTag: reactTag, callback: { videoView in

View File

@ -53,6 +53,7 @@ import type {
OnReceiveAdEventData,
ReactVideoProps,
CmcdData,
ReactVideoSource,
} from './types';
export interface VideoRef {
@ -66,6 +67,7 @@ export interface VideoRef {
) => void;
setVolume: (volume: number) => void;
setFullScreen: (fullScreen: boolean) => void;
setSource: (source?: ReactVideoSource) => void;
save: (options: object) => Promise<VideoSaveData> | void;
getCurrentPosition: () => Promise<number>;
}
@ -157,98 +159,105 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
setRestoreUserInterfaceForPIPStopCompletionHandler,
] = useState<boolean | undefined>();
const src = useMemo<VideoSrc | undefined>(() => {
if (!source) {
return undefined;
}
const resolvedSource = resolveAssetSourceForVideo(source);
let uri = resolvedSource.uri || '';
if (uri && uri.match(/^\//)) {
uri = `file://${uri}`;
}
if (!uri) {
console.log('Trying to load empty source');
}
const isNetwork = !!(uri && uri.match(/^(rtp|rtsp|http|https):/));
const isAsset = !!(
uri &&
uri.match(
/^(assets-library|ipod-library|file|content|ms-appx|ms-appdata):/,
)
);
const selectedDrm = source.drm || drm;
const _textTracks = source.textTracks || textTracks;
const _drm = !selectedDrm
? undefined
: {
type: selectedDrm.type,
licenseServer: selectedDrm.licenseServer,
headers: generateHeaderForNative(selectedDrm.headers),
contentId: selectedDrm.contentId,
certificateUrl: selectedDrm.certificateUrl,
base64Certificate: selectedDrm.base64Certificate,
useExternalGetLicense: !!selectedDrm.getLicense,
multiDrm: selectedDrm.multiDrm,
localSourceEncryptionKeyScheme:
selectedDrm.localSourceEncryptionKeyScheme ||
localSourceEncryptionKeyScheme,
};
let _cmcd: NativeCmcdConfiguration | undefined;
if (Platform.OS === 'android' && source?.cmcd) {
const cmcd = source.cmcd;
if (typeof cmcd === 'boolean') {
_cmcd = cmcd ? {mode: CmcdMode.MODE_QUERY_PARAMETER} : undefined;
} else if (typeof cmcd === 'object' && !Array.isArray(cmcd)) {
const createCmcdHeader = (property?: CmcdData) =>
property ? generateHeaderForNative(property) : undefined;
_cmcd = {
mode: cmcd.mode ?? CmcdMode.MODE_QUERY_PARAMETER,
request: createCmcdHeader(cmcd.request),
session: createCmcdHeader(cmcd.session),
object: createCmcdHeader(cmcd.object),
status: createCmcdHeader(cmcd.status),
};
} else {
throw new Error(
'Invalid CMCD configuration: Expected a boolean or an object.',
);
const sourceToUnternalSource = useCallback(
(_source?: ReactVideoSource) => {
if (!_source) {
return undefined;
}
}
const resolvedSource = resolveAssetSourceForVideo(_source);
let uri = resolvedSource.uri || '';
if (uri && uri.match(/^\//)) {
uri = `file://${uri}`;
}
if (!uri) {
console.log('Trying to load empty source');
}
const isNetwork = !!(uri && uri.match(/^(rtp|rtsp|http|https):/));
const isAsset = !!(
uri &&
uri.match(
/^(assets-library|ipod-library|file|content|ms-appx|ms-appdata):/,
)
);
const selectedContentStartTime =
source.contentStartTime || contentStartTime;
const selectedDrm = _source.drm || drm;
const _textTracks = _source.textTracks || textTracks;
const _drm = !selectedDrm
? undefined
: {
type: selectedDrm.type,
licenseServer: selectedDrm.licenseServer,
headers: generateHeaderForNative(selectedDrm.headers),
contentId: selectedDrm.contentId,
certificateUrl: selectedDrm.certificateUrl,
base64Certificate: selectedDrm.base64Certificate,
useExternalGetLicense: !!selectedDrm.getLicense,
multiDrm: selectedDrm.multiDrm,
localSourceEncryptionKeyScheme:
selectedDrm.localSourceEncryptionKeyScheme ||
localSourceEncryptionKeyScheme,
};
return {
uri,
isNetwork,
isAsset,
shouldCache: resolvedSource.shouldCache || false,
type: resolvedSource.type || '',
mainVer: resolvedSource.mainVer || 0,
patchVer: resolvedSource.patchVer || 0,
requestHeaders: generateHeaderForNative(resolvedSource.headers),
startPosition: resolvedSource.startPosition ?? -1,
cropStart: resolvedSource.cropStart || 0,
cropEnd: resolvedSource.cropEnd,
contentStartTime: selectedContentStartTime,
metadata: resolvedSource.metadata,
drm: _drm,
cmcd: _cmcd,
textTracks: _textTracks,
textTracksAllowChunklessPreparation:
resolvedSource.textTracksAllowChunklessPreparation,
};
}, [
drm,
source,
textTracks,
contentStartTime,
localSourceEncryptionKeyScheme,
]);
let _cmcd: NativeCmcdConfiguration | undefined;
if (Platform.OS === 'android' && source?.cmcd) {
const cmcd = source.cmcd;
if (typeof cmcd === 'boolean') {
_cmcd = cmcd ? {mode: CmcdMode.MODE_QUERY_PARAMETER} : undefined;
} else if (typeof cmcd === 'object' && !Array.isArray(cmcd)) {
const createCmcdHeader = (property?: CmcdData) =>
property ? generateHeaderForNative(property) : undefined;
_cmcd = {
mode: cmcd.mode ?? CmcdMode.MODE_QUERY_PARAMETER,
request: createCmcdHeader(cmcd.request),
session: createCmcdHeader(cmcd.session),
object: createCmcdHeader(cmcd.object),
status: createCmcdHeader(cmcd.status),
};
} else {
throw new Error(
'Invalid CMCD configuration: Expected a boolean or an object.',
);
}
}
const selectedContentStartTime =
_source.contentStartTime || contentStartTime;
return {
uri,
isNetwork,
isAsset,
shouldCache: resolvedSource.shouldCache || false,
type: resolvedSource.type || '',
mainVer: resolvedSource.mainVer || 0,
patchVer: resolvedSource.patchVer || 0,
requestHeaders: generateHeaderForNative(resolvedSource.headers),
startPosition: resolvedSource.startPosition ?? -1,
cropStart: resolvedSource.cropStart || 0,
cropEnd: resolvedSource.cropEnd,
contentStartTime: selectedContentStartTime,
metadata: resolvedSource.metadata,
drm: _drm,
cmcd: _cmcd,
textTracks: _textTracks,
textTracksAllowChunklessPreparation:
resolvedSource.textTracksAllowChunklessPreparation,
};
},
[
contentStartTime,
drm,
localSourceEncryptionKeyScheme,
source,
textTracks,
],
);
const src = useMemo<VideoSrc | undefined>(() => {
return sourceToUnternalSource(source);
}, [sourceToUnternalSource, source]);
const _selectedTextTrack = useMemo(() => {
if (!selectedTextTrack) {
@ -370,6 +379,16 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
);
}, []);
const setSource = useCallback(
(_source?: ReactVideoSource) => {
return NativeVideoManager.setSourceCmd(
getReactTag(nativeRef),
sourceToUnternalSource(_source),
);
},
[sourceToUnternalSource],
);
const presentFullscreenPlayer = useCallback(
() => setFullScreen(true),
[setFullScreen],
@ -628,6 +647,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
setVolume,
getCurrentPosition,
setFullScreen,
setSource,
}),
[
seek,
@ -640,6 +660,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
setVolume,
getCurrentPosition,
setFullScreen,
setSource,
],
);

View File

@ -24,6 +24,7 @@ export interface VideoManagerType {
licenseUrl: string,
) => Promise<void>;
setFullScreenCmd: (reactTag: Int32, fullScreen: boolean) => Promise<void>;
setSourceCmd: (reactTag: Int32, source?: UnsafeObject) => Promise<void>;
setVolumeCmd: (reactTag: Int32, volume: number) => Promise<void>;
save: (reactTag: Int32, option: UnsafeObject) => Promise<VideoSaveData>;
getCurrentPosition: (reactTag: Int32) => Promise<Int32>;