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() { private void clearVideoView() {
if (surfaceView instanceof TextureView) { if (surfaceView instanceof TextureView) {
player.clearVideoTextureView((TextureView) surfaceView); player.clearVideoTextureView((TextureView) surfaceView);
@ -189,7 +196,7 @@ public final class ExoPlayerView extends FrameLayout implements AdViewProvider {
surfaceView.setAlpha(0); surfaceView.setAlpha(0);
} }
private void updateShutterViewVisibility() { public void updateShutterViewVisibility() {
if (this.hideShutterView) { if (this.hideShutterView) {
hideShutterView(); hideShutterView();
} else { } else {

View File

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

View File

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

View File

@ -1,10 +1,12 @@
package com.brentvatne.react package com.brentvatne.react
import com.brentvatne.common.api.Source
import com.brentvatne.exoplayer.ReactExoplayerView import com.brentvatne.exoplayer.ReactExoplayerView
import com.facebook.react.bridge.Promise import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.UiThreadUtil import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.common.UIManagerType 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 @ReactMethod
fun getCurrentPosition(reactTag: Int, promise: Promise) { fun getCurrentPosition(reactTag: Int, promise: Promise) {
performOnPlayerView(reactTag) { 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 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. 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` ### `setFullScreen`
<PlatformsList types={['Android', 'iOS']} /> <PlatformsList types={['Android', 'iOS']} />

View File

@ -252,6 +252,10 @@ const VideoPlayer: FC<Props> = ({}) => {
cacheSizeMB: useCache ? 200 : 0, cacheSizeMB: useCache ? 200 : 0,
}; };
useEffect(() => {
videoRef.current?.setSource(currentSrc)
}, [currentSrc])
return ( return (
<View style={styles.container}> <View style={styles.container}>
<StatusBar animated={true} backgroundColor="black" hidden={false} /> <StatusBar animated={true} backgroundColor="black" hidden={false} />
@ -261,7 +265,7 @@ const VideoPlayer: FC<Props> = ({}) => {
<Video <Video
showNotificationControls={showNotificationControls} showNotificationControls={showNotificationControls}
ref={videoRef} ref={videoRef}
source={currentSrc as ReactVideoSource} // source={currentSrc as ReactVideoSource}
adTagUrl={additional?.adTagUrl} adTagUrl={additional?.adTagUrl}
drm={additional?.drm} drm={additional?.drm}
style={viewStyle} 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(setPlayerPauseStateCmd : (nonnull NSNumber*)reactTag paused : (nonnull BOOL)paused)
RCT_EXTERN_METHOD(setVolumeCmd : (nonnull NSNumber*)reactTag volume : (nonnull float*)volume) 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(setFullScreenCmd : (nonnull NSNumber*)reactTag fullscreen : (nonnull BOOL)fullScreen)
RCT_EXTERN_METHOD(setSourceCmd : (nonnull NSNumber*)reactTag source : (NSDictionary*)source)
RCT_EXTERN_METHOD(save RCT_EXTERN_METHOD(save
: (nonnull NSNumber*)reactTag options : (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:) @objc(save:options:resolve:reject:)
func save(_ reactTag: NSNumber, options: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { func save(_ reactTag: NSNumber, options: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
performOnVideoView(withReactTag: reactTag, callback: { videoView in performOnVideoView(withReactTag: reactTag, callback: { videoView in

View File

@ -53,6 +53,7 @@ import type {
OnReceiveAdEventData, OnReceiveAdEventData,
ReactVideoProps, ReactVideoProps,
CmcdData, CmcdData,
ReactVideoSource,
} from './types'; } from './types';
export interface VideoRef { export interface VideoRef {
@ -66,6 +67,7 @@ export interface VideoRef {
) => void; ) => void;
setVolume: (volume: number) => void; setVolume: (volume: number) => void;
setFullScreen: (fullScreen: boolean) => void; setFullScreen: (fullScreen: boolean) => void;
setSource: (source?: ReactVideoSource) => void;
save: (options: object) => Promise<VideoSaveData> | void; save: (options: object) => Promise<VideoSaveData> | void;
getCurrentPosition: () => Promise<number>; getCurrentPosition: () => Promise<number>;
} }
@ -157,11 +159,12 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
setRestoreUserInterfaceForPIPStopCompletionHandler, setRestoreUserInterfaceForPIPStopCompletionHandler,
] = useState<boolean | undefined>(); ] = useState<boolean | undefined>();
const src = useMemo<VideoSrc | undefined>(() => { const sourceToUnternalSource = useCallback(
if (!source) { (_source?: ReactVideoSource) => {
if (!_source) {
return undefined; return undefined;
} }
const resolvedSource = resolveAssetSourceForVideo(source); const resolvedSource = resolveAssetSourceForVideo(_source);
let uri = resolvedSource.uri || ''; let uri = resolvedSource.uri || '';
if (uri && uri.match(/^\//)) { if (uri && uri.match(/^\//)) {
uri = `file://${uri}`; uri = `file://${uri}`;
@ -177,8 +180,8 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
) )
); );
const selectedDrm = source.drm || drm; const selectedDrm = _source.drm || drm;
const _textTracks = source.textTracks || textTracks; const _textTracks = _source.textTracks || textTracks;
const _drm = !selectedDrm const _drm = !selectedDrm
? undefined ? undefined
: { : {
@ -220,7 +223,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
} }
const selectedContentStartTime = const selectedContentStartTime =
source.contentStartTime || contentStartTime; _source.contentStartTime || contentStartTime;
return { return {
uri, uri,
@ -242,13 +245,19 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
textTracksAllowChunklessPreparation: textTracksAllowChunklessPreparation:
resolvedSource.textTracksAllowChunklessPreparation, resolvedSource.textTracksAllowChunklessPreparation,
}; };
}, [ },
[
contentStartTime,
drm, drm,
localSourceEncryptionKeyScheme,
source, source,
textTracks, textTracks,
contentStartTime, ],
localSourceEncryptionKeyScheme, );
]);
const src = useMemo<VideoSrc | undefined>(() => {
return sourceToUnternalSource(source);
}, [sourceToUnternalSource, source]);
const _selectedTextTrack = useMemo(() => { const _selectedTextTrack = useMemo(() => {
if (!selectedTextTrack) { 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( const presentFullscreenPlayer = useCallback(
() => setFullScreen(true), () => setFullScreen(true),
[setFullScreen], [setFullScreen],
@ -628,6 +647,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
setVolume, setVolume,
getCurrentPosition, getCurrentPosition,
setFullScreen, setFullScreen,
setSource,
}), }),
[ [
seek, seek,
@ -640,6 +660,7 @@ const Video = forwardRef<VideoRef, ReactVideoProps>(
setVolume, setVolume,
getCurrentPosition, getCurrentPosition,
setFullScreen, setFullScreen,
setSource,
], ],
); );

View File

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