Merge pull request #2923 from avencat/master

Feature: Add VAST support for AVOD
This commit is contained in:
Olivier Bouillet 2022-12-09 22:16:51 +01:00 committed by GitHub
commit 346d5a1d0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 704 additions and 267 deletions

247
API.md
View File

@ -263,80 +263,82 @@ var styles = StyleSheet.create({
```
### Configurable props
| Name |Plateforms Support |
|--|--|
|[allowsExternalPlayback](#allowsexternalplayback) |iOS |
|[audioOnly](#audioonly)|All |
|[automaticallyWaitsToMinimizeStalling](#automaticallyWaitsToMinimizeStalling) | iOS|\
|[backBufferDurationMs](#backBufferDurationMs)| Android |
|[bufferConfig](#bufferconfig)|Android|
|[contentStartTime](#contentStartTime)| Android |
|[controls](#controls)|Android, iOS|
|[currentPlaybackTime](#currentPlaybackTime)|Android|
|[disableFocus](#disableFocus)|Android, iOS|
|[disableDisconnectError](#disableDisconnectError)|Android|
|[filter](#filter)|iOS|
|[filterEnabled](#filterEnabled)|iOS|
|[focusable](#focusable)|Android|
|[fullscreen](#fullscreen)|iOS|
|[fullscreenAutorotate](#fullscreenautorotate)|iOS|
|[fullscreenOrientation](#fullscreenorientation)|iOS|
|[headers](#headers)|Android|
|[hideShutterView](#hideshutterview)|Android|
|[ignoreSilentSwitch](#ignoresilentswitch)|iOS|
|[maxBitRate](#maxbitrate)|Android, iOS|
|[minLoadRetryCount](#minLoadRetryCount)|Android|
|[mixWithOthers](#mixWithOthers)|iOS|
|[muted](#muted)|All|
|[paused](#paused)|All|
|[pictureInPicture](#pictureinpicture)|iOS|
|[playInBackground](#playinbackground)|Android, iOS|
|[playWhenInactive](#playwheninactive)|iOS|
|[poster](#poster)|All|
|[posterResizeMode](#posterresizemode)|All|
|[preferredForwardBufferDuration](#preferredForwardBufferDuration)|iOS|
|[preventsDisplaySleepDuringVideoPlayback](#preventsDisplaySleepDuringVideoPlayback)|iOS, Android|
|[progressUpdateInterval](#progressupdateinterval)|All|
|[rate](#rate)|All|
|[repeat](#repeat)|All|
|[reportBandwidth](#reportbandwidth)|Android|
|[resizeMode](#resizemode)|Android, iOS, Windows UWP|
|[selectedAudioTrack](#selectedaudiotrack)|Android, iOS|
|[selectedTextTrack](#selectedtexttrack)|Android, iOS|
|[selectedVideoTrack](#selectedvideotrack)|Android|
|[source](#source)|All|
|[subtitleStyle](#subtitleStyle)|Android|
|[textTracks](#texttracks)|Android, iOS|
|[trackId](#trackId)|Android|
|[useTextureView](#usetextureview)|Android|
|[useSecureView](#useSecureView)|Android|
|[volume](#volume)|All|
|[localSourceEncryptionKeyScheme](#localSourceEncryptionKeyScheme)|All|
| Name | Platforms Support |
|-------------------------------------------------------------------------------------|---------------------------|
| [adTagUrl](#adTagUrl) | Android, iOS |
| [allowsExternalPlayback](#allowsexternalplayback) | iOS |
| [audioOnly](#audioonly) | All |
| [automaticallyWaitsToMinimizeStalling](#automaticallyWaitsToMinimizeStalling) | iOS |
| [backBufferDurationMs](#backBufferDurationMs) | Android |
| [bufferConfig](#bufferconfig) | Android |
| [contentStartTime](#contentStartTime) | Android |
| [controls](#controls) | Android, iOS |
| [currentPlaybackTime](#currentPlaybackTime) | Android |
| [disableFocus](#disableFocus) | Android, iOS |
| [disableDisconnectError](#disableDisconnectError) | Android |
| [filter](#filter) | iOS |
| [filterEnabled](#filterEnabled) | iOS |
| [focusable](#focusable) | Android |
| [fullscreen](#fullscreen) | iOS |
| [fullscreenAutorotate](#fullscreenautorotate) | iOS |
| [fullscreenOrientation](#fullscreenorientation) | iOS |
| [headers](#headers) | Android |
| [hideShutterView](#hideshutterview) | Android |
| [ignoreSilentSwitch](#ignoresilentswitch) | iOS |
| [maxBitRate](#maxbitrate) | Android, iOS |
| [minLoadRetryCount](#minLoadRetryCount) | Android |
| [mixWithOthers](#mixWithOthers) | iOS |
| [muted](#muted) | All |
| [paused](#paused) | All |
| [pictureInPicture](#pictureinpicture) | iOS |
| [playInBackground](#playinbackground) | Android, iOS |
| [playWhenInactive](#playwheninactive) | iOS |
| [poster](#poster) | All |
| [posterResizeMode](#posterresizemode) | All |
| [preferredForwardBufferDuration](#preferredForwardBufferDuration) | iOS |
| [preventsDisplaySleepDuringVideoPlayback](#preventsDisplaySleepDuringVideoPlayback) | iOS, Android |
| [progressUpdateInterval](#progressupdateinterval) | All |
| [rate](#rate) | All |
| [repeat](#repeat) | All |
| [reportBandwidth](#reportbandwidth) | Android |
| [resizeMode](#resizemode) | Android, iOS, Windows UWP |
| [selectedAudioTrack](#selectedaudiotrack) | Android, iOS |
| [selectedTextTrack](#selectedtexttrack) | Android, iOS |
| [selectedVideoTrack](#selectedvideotrack) | Android |
| [source](#source) | All |
| [subtitleStyle](#subtitleStyle) | Android |
| [textTracks](#texttracks) | Android, iOS |
| [trackId](#trackId) | Android |
| [useTextureView](#usetextureview) | Android |
| [useSecureView](#useSecureView) | Android |
| [volume](#volume) | All |
| [localSourceEncryptionKeyScheme](#localSourceEncryptionKeyScheme) | All |
### Event props
| Name |Plateforms Support |
|--|--|
|[onAudioBecomingNoisy](#onaudiobecomingnoisy)|Android, iOS|
|[onBandwidthUpdate](#onbandwidthupdate)|Android|
|[onBuffer](#onbuffer)|Android, iOS|
|[onEnd](#onend)|All|
|[onError](#onerror)|Android, iOS|
|[onExternalPlaybackChange](#onexternalplaybackchange)|iOS|
|[onFullscreenPlayerWillPresent](#onfullscreenplayerwillpresent)|Android, iOS|
|[onFullscreenPlayerDidPresent](#onfullscreenplayerdidpresent)|Android, iOS|
|[onFullscreenPlayerWillDismiss](#onfullscreenplayerwilldismiss)|Android, iOS|
|[onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss)|Android, iOS|
|[onLoad](#onload)|All|
|[onLoadStart](#onloadstart)|All|
|[onReadyForDisplay](#onreadyfordisplay)|Android, iOS, Web|
|[onPictureInPictureStatusChanged](#onpictureinpicturestatuschanged)|iOS|
|[onPlaybackRateChange](#onplaybackratechange)|All|
|[onProgress](#onprogress)|All|
|[onSeek](#onseek)|Android, iOS, Windows UWP|
|[onRestoreUserInterfaceForPictureInPictureStop](#onrestoreuserinterfaceforpictureinpicturestop)|iOS|
|[onTimedMetadata](#ontimedmetadata)|Android, iOS|
| Name | Platforms Support |
|-------------------------------------------------------------------------------------------------|---------------------------|
| [onAudioBecomingNoisy](#onaudiobecomingnoisy) | Android, iOS |
| [onBandwidthUpdate](#onbandwidthupdate) | Android |
| [onBuffer](#onbuffer) | Android, iOS |
| [onEnd](#onend) | All |
| [onError](#onerror) | Android, iOS |
| [onExternalPlaybackChange](#onexternalplaybackchange) | iOS |
| [onFullscreenPlayerWillPresent](#onfullscreenplayerwillpresent) | Android, iOS |
| [onFullscreenPlayerDidPresent](#onfullscreenplayerdidpresent) | Android, iOS |
| [onFullscreenPlayerWillDismiss](#onfullscreenplayerwilldismiss) | Android, iOS |
| [onFullscreenPlayerDidDismiss](#onfullscreenplayerdiddismiss) | Android, iOS |
| [onLoad](#onload) | All |
| [onLoadStart](#onloadstart) | All |
| [onPictureInPictureStatusChanged](#onpictureinpicturestatuschanged) | iOS |
| [onPlaybackRateChange](#onplaybackratechange) | All |
| [onProgress](#onprogress) | All |
| [onReadyForDisplay](#onreadyfordisplay) | Android, iOS, Web |
| [onReceiveAdEvent](#onReceiveAdEvent) | Android, iOS |
| [onRestoreUserInterfaceForPictureInPictureStop](#onrestoreuserinterfaceforpictureinpicturestop) | iOS |
| [onSeek](#onseek) | Android, iOS, Windows UWP |
| [onTimedMetadata](#ontimedmetadata) | Android, iOS |
### Methods
| Name |Plateforms Support |
@ -357,6 +359,16 @@ var styles = StyleSheet.create({
### Configurable props
#### adTagUrl
Sets the VAST uri to play AVOD ads.
Example:
```
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="
```
Platforms: Android, iOS
#### allowsExternalPlayback
Indicates whether the player allows switching to external playback mode such as AirPlay or HDMI.
* **true (default)** - allow switching to external playback mode
@ -1161,16 +1173,6 @@ Example:
Platforms: Android
#### onReadyForDisplay
Callback function that is called when the first video frame is ready for display. This is when the poster is removed.
Payload: none
* iOS: [readyForDisplay](https://developer.apple.com/documentation/avkit/avplayerviewcontroller/1615830-readyfordisplay?language=objc)
* Android [STATE_READY](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#STATE_READY)
Platforms: Android, iOS, Web
#### onPictureInPictureStatusChanged
Callback function that is called when picture in picture becomes active or inactive.
@ -1203,7 +1205,6 @@ Example:
Platforms: all
#### onProgress
Callback function that is called every progressUpdateInterval milliseconds with info about which position the media is currently playing.
@ -1224,6 +1225,83 @@ Example:
Platforms: all
#### onReadyForDisplay
Callback function that is called when the first video frame is ready for display. This is when the poster is removed.
Payload: none
* iOS: [readyForDisplay](https://developer.apple.com/documentation/avkit/avplayerviewcontroller/1615830-readyfordisplay?language=objc)
* Android [STATE_READY](https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#STATE_READY)
Platforms: Android, iOS, Web
#### onReceiveAdEvent
Callback function that is called when an AdEvent is received from the IMA's SDK.
Enum `AdEvent` possible values for [Android](https://developers.google.com/interactive-media-ads/docs/sdks/html5/client-side/reference/js/google.ima.AdEvent) and [iOS](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side/reference/Enums/IMAAdEventType):
| Event | Platform | Description |
|----------------------------|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `AD_BREAK_ENDED` | iOS | Fired the first time each ad break ends. Applications must reenable seeking when this occurs (only used for dynamic ad insertion). |
| `AD_BREAK_READY` | Android, iOS | Fires when an ad rule or a VMAP ad break would have played if autoPlayAdBreaks is false. |
| `AD_BREAK_STARTED` | iOS | Fired first time each ad break begins playback. If an ad break is watched subsequent times this will not be fired. Applications must disable seeking when this occurs (only used for dynamic ad insertion). |
| `AD_BUFFERING` | Android | Fires when the ad has stalled playback to buffer. |
| `AD_CAN_PLAY` | Android | Fires when the ad is ready to play without buffering, either at the beginning of the ad or after buffering completes. |
| `AD_METADATA` | Android | Fires when an ads list is loaded. |
| `AD_PERIOD_ENDED` | iOS | Fired every time the stream switches from advertising or slate to content. This will be fired even when an ad is played a second time or when seeking into an ad (only used for dynamic ad insertion). |
| `AD_PERIOD_STARTED` | iOS | Fired every time the stream switches from content to advertising or slate. This will be fired even when an ad is played a second time or when seeking into an ad (only used for dynamic ad insertion). |
| `AD_PROGRESS` | Android | Fires when the ad's current time value changes. Calling getAdData() on this event will return an AdProgressData object. |
| `ALL_ADS_COMPLETED` | Android, iOS | Fires when the ads manager is done playing all the valid ads in the ads response, or when the response doesn't return any valid ads. |
| `CLICK` | Android, iOS | Fires when the ad is clicked. |
| `COMPLETE` | Android, iOS | Fires when the ad completes playing. |
| `CONTENT_PAUSE_REQUESTED` | Android | Fires when content should be paused. This usually happens right before an ad is about to cover the content. |
| `CONTENT_RESUME_REQUESTED` | Android | Fires when content should be resumed. This usually happens when an ad finishes or collapses. |
| `CUEPOINTS_CHANGED` | iOS | Cuepoints changed for VOD stream (only used for dynamic ad insertion). |
| `DURATION_CHANGE` | Android | Fires when the ad's duration changes. |
| `FIRST_QUARTILE` | Android, iOS | Fires when the ad playhead crosses first quartile. |
| `IMPRESSION` | Android | Fires when the impression URL has been pinged. |
| `INTERACTION` | Android | Fires when an ad triggers the interaction callback. Ad interactions contain an interaction ID string in the ad data. |
| `LINEAR_CHANGED` | Android | Fires when the displayed ad changes from linear to nonlinear, or the reverse. |
| `LOADED` | Android, iOS | Fires when ad data is available. |
| `LOG` | Android, iOS | Fires when a non-fatal error is encountered. The user need not take any action since the SDK will continue with the same or next ad playback depending on the error situation. |
| `MIDPOINT` | Android, iOS | Fires when the ad playhead crosses midpoint. |
| `PAUSED` | Android, iOS | Fires when the ad is paused. |
| `RESUMED` | Android, iOS | Fires when the ad is resumed. |
| `SKIPPABLE_STATE_CHANGED` | Android | Fires when the displayed ads skippable state is changed. |
| `SKIPPED` | Android, iOS | Fires when the ad is skipped by the user. |
| `STARTED` | Android, iOS | Fires when the ad starts playing. |
| `STREAM_LOADED` | iOS | Stream request has loaded (only used for dynamic ad insertion). |
| `TAPPED` | iOS | Fires when the ad is tapped. |
| `THIRD_QUARTILE` | Android, iOS | Fires when the ad playhead crosses third quartile. |
| `UNKNOWN` | iOS | An unknown event has fired |
| `USER_CLOSE` | Android | Fires when the ad is closed by the user. |
| `VIDEO_CLICKED` | Android | Fires when the non-clickthrough portion of a video ad is clicked. |
| `VIDEO_ICON_CLICKED` | Android | Fires when a user clicks a video icon. |
| `VOLUME_CHANGED` | Android | Fires when the ad volume has changed. |
| `VOLUME_MUTED` | Android | Fires when the ad volume has been muted. |
Payload:
| Property | Type | Description |
|----------|---------|-----------------------|
| event | AdEvent | The ad event received |
Example:
```
{
"event": "LOADED"
}
```
Platforms: Android, iOS
#### onRestoreUserInterfaceForPictureInPictureStop
Callback function that corresponds to Apple's [`restoreUserInterfaceForPictureInPictureStopWithCompletionHandler`](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). Call `restoreUserInterfaceForPictureInPictureStopCompleted` inside of this function when done restoring the user interface.
Payload: none
Platforms: iOS
#### onSeek
Callback function that is called when a seek completes.
@ -1247,13 +1325,6 @@ Both the currentTime & seekTime are reported because the video player may not se
Platforms: Android, iOS, Windows UWP
#### onRestoreUserInterfaceForPictureInPictureStop
Callback function that corresponds to Apple's [`restoreUserInterfaceForPictureInPictureStopWithCompletionHandler`](https://developer.apple.com/documentation/avkit/avpictureinpicturecontrollerdelegate/1614703-pictureinpicturecontroller?language=objc). Call `restoreUserInterfaceForPictureInPictureStopCompleted` inside of this function when done restoring the user interface.
Payload: none
Platforms: iOS
#### onTimedMetadata
Callback function that is called when timed metadata becomes available

View File

@ -12,6 +12,7 @@
- Fix regression when fullscreen prop is used combined with controls [#2911](https://github.com/react-native-video/react-native-video/pull/2911)
- Fix: memory leak issue on iOS [#2907](https://github.com/react-native-video/react-native-video/pull/2907)
- Fix setting text tracks before player is initialized on iOS [#2935](https://github.com/react-native-video/react-native-video/pull/2935)
- Feature: Add VAST support for AVOD [#2923](https://github.com/react-native-video/react-native-video/pull/2923)
### Version 6.0.0-alpha.3

View File

@ -261,6 +261,13 @@ export default class Video extends Component {
}
}
}
_onReceiveAdEvent = (event) => {
if (this.props.onReceiveAdEvent) {
this.props.onReceiveAdEvent(event.nativeEvent);
}
};
getViewManagerConfig = viewManagerName => {
if (!UIManager.getViewManagerConfig) {
return UIManager[viewManagerName];
@ -343,6 +350,7 @@ export default class Video extends Component {
onGetLicense: nativeProps.drm && nativeProps.drm.getLicense && this._onGetLicense,
onPictureInPictureStatusChanged: this._onPictureInPictureStatusChanged,
onRestoreUserInterfaceForPictureInPictureStop: this._onRestoreUserInterfaceForPictureInPictureStop,
onReceiveAdEvent: this._onReceiveAdEvent,
});
const posterStyle = {
@ -527,6 +535,8 @@ Video.propTypes = {
onPictureInPictureStatusChanged: PropTypes.func,
needsToRestoreUserInterfaceForPictureInPictureStop: PropTypes.func,
onExternalPlaybackChange: PropTypes.func,
adTagUrl: PropTypes.string,
onReceiveAdEvent: PropTypes.func,
/* Required by react-native */
scaleX: PropTypes.number,

View File

@ -45,5 +45,7 @@ dependencies {
implementation('com.google.android.exoplayer:extension-okhttp:2.18.1') {
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
}
implementation 'com.google.android.exoplayer:extension-ima:2.18.1'
implementation "com.squareup.okhttp3:okhttp:" + '$OKHTTP_VERSION'
}

View File

@ -22,13 +22,15 @@ import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.Tracks;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.AdViewProvider;
import com.google.android.exoplayer2.ui.SubtitleView;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.video.VideoSize;
import java.util.List;
@TargetApi(16)
public final class ExoPlayerView extends FrameLayout {
public final class ExoPlayerView extends FrameLayout implements AdViewProvider {
private View surfaceView;
private final View shutterView;
@ -38,6 +40,7 @@ public final class ExoPlayerView extends FrameLayout {
private ExoPlayer player;
private Context context;
private ViewGroup.LayoutParams layoutParams;
private final FrameLayout adOverlayFrameLayout;
private boolean useTextureView = true;
private boolean useSecureView = false;
@ -80,8 +83,11 @@ public final class ExoPlayerView extends FrameLayout {
updateSurfaceView();
adOverlayFrameLayout = new FrameLayout(context);
layout.addView(shutterView, 1, layoutParams);
layout.addView(subtitleLayout, 2, layoutParams);
layout.addView(adOverlayFrameLayout, 3, layoutParams);
addViewInLayout(layout, 0, aspectRatioParams);
}
@ -139,6 +145,19 @@ public final class ExoPlayerView extends FrameLayout {
shutterView.setVisibility(this.hideShutterView ? View.INVISIBLE : View.VISIBLE);
}
@Override
public void requestLayout() {
super.requestLayout();
post(measureAndLayout);
}
// AdsLoader.AdViewProvider implementation.
@Override
public ViewGroup getAdViewGroup() {
return Assertions.checkNotNull(adOverlayFrameLayout, "exo_ad_overlay must be present for ad playback");
}
/**
* Set the {@link ExoPlayer} to use. The {@link ExoPlayer#addListener} method of the
* player will be called and previous

View File

@ -31,6 +31,7 @@ import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.util.RNLog;
import com.google.ads.interactivemedia.v3.api.AdEvent;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.DefaultRenderersFactory;
@ -75,11 +76,11 @@ import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters;
import com.google.android.exoplayer2.trackselection.TrackSelectionOverride;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultAllocator;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.HttpDataSource;
@ -93,6 +94,11 @@ import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
import com.google.android.exoplayer2.source.dash.manifest.Representation;
import com.google.android.exoplayer2.source.dash.manifest.Descriptor;
import com.google.android.exoplayer2.ext.ima.ImaAdsLoader;
import com.google.android.exoplayer2.source.ads.AdsMediaSource;
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory;
import com.google.common.collect.ImmutableList;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.CookiePolicy;
@ -119,7 +125,8 @@ class ReactExoplayerView extends FrameLayout implements
BandwidthMeter.EventListener,
BecomingNoisyListener,
AudioManager.OnAudioFocusChangeListener,
DrmSessionEventListener {
DrmSessionEventListener,
AdEvent.AdEventListener {
public static final double DEFAULT_MAX_HEAP_ALLOCATION_PERCENT = 1;
public static final double DEFAULT_MIN_BACK_BUFFER_MEMORY_RESERVE = 0;
@ -144,6 +151,7 @@ class ReactExoplayerView extends FrameLayout implements
private ExoPlayerView exoPlayerView;
private FullScreenPlayerView fullScreenPlayerView;
private ImaAdsLoader adsLoader;
private DataSource.Factory mediaDataSourceFactory;
private ExoPlayer player;
@ -203,6 +211,7 @@ class ReactExoplayerView extends FrameLayout implements
private String drmLicenseUrl = null;
private String[] drmLicenseHeader = null;
private boolean controls;
private Uri adTagUrl;
// \ End props
// React
@ -221,6 +230,9 @@ class ReactExoplayerView extends FrameLayout implements
switch (msg.what) {
case SHOW_PROGRESS:
if (player != null) {
if (playerControlView != null && isPlayingAd() && controls) {
playerControlView.hide();
}
long pos = player.getCurrentPosition();
long bufferedDuration = player.getBufferedPercentage() * player.getDuration() / 100;
long duration = player.getDuration();
@ -263,6 +275,9 @@ class ReactExoplayerView extends FrameLayout implements
audioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(themedReactContext);
}
private boolean isPlayingAd() {
return player != null && player.isPlayingAd();
}
@Override
public void setId(int id) {
@ -389,7 +404,9 @@ class ReactExoplayerView extends FrameLayout implements
exoPlayerView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
togglePlayerControlVisibility();
if (!isPlayingAd()) {
togglePlayerControlVisibility();
}
}
});
@ -607,13 +624,22 @@ class ReactExoplayerView extends FrameLayout implements
DefaultRenderersFactory renderersFactory =
new DefaultRenderersFactory(getContext())
.setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF);
// Create an AdsLoader.
adsLoader = new ImaAdsLoader.Builder(themedReactContext).setAdEventListener(this).build();
MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory)
.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView);
player = new ExoPlayer.Builder(getContext(), renderersFactory)
.setTrackSelector(self.trackSelector)
.setBandwidthMeter(bandwidthMeter)
.setLoadControl(loadControl)
.setMediaSourceFactory(mediaSourceFactory)
.build();
player.addListener(self);
exoPlayerView.setPlayer(player);
adsLoader.setPlayer(player);
audioBecomingNoisyReceiver.setListener(self);
bandwidthMeter.addEventListener(new Handler(), self);
setPlayWhenReady(!isPaused);
@ -643,11 +669,26 @@ class ReactExoplayerView extends FrameLayout implements
private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager) {
ArrayList<MediaSource> mediaSourceList = buildTextSources();
MediaSource videoSource = buildMediaSource(self.srcUri, self.extension, drmSessionManager);
MediaSource mediaSourceWithAds = null;
if (adTagUrl != null) {
MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory)
.setLocalAdInsertionComponents(unusedAdTagUri -> adsLoader, exoPlayerView);
DataSpec adTagDataSpec = new DataSpec(adTagUrl);
mediaSourceWithAds = new AdsMediaSource(videoSource, adTagDataSpec, ImmutableList.of(srcUri, adTagUrl), mediaSourceFactory, adsLoader, exoPlayerView);
}
MediaSource mediaSource;
if (mediaSourceList.size() == 0) {
mediaSource = videoSource;
if (mediaSourceWithAds != null) {
mediaSource = mediaSourceWithAds;
} else {
mediaSource = videoSource;
}
} else {
mediaSourceList.add(0, videoSource);
if (mediaSourceWithAds != null) {
mediaSourceList.add(0, mediaSourceWithAds);
} else {
mediaSourceList.add(0, videoSource);
}
MediaSource[] textSourceArray = mediaSourceList.toArray(
new MediaSource[mediaSourceList.size()]
);
@ -729,7 +770,17 @@ class ReactExoplayerView extends FrameLayout implements
int type = Util.inferContentType(!TextUtils.isEmpty(overrideExtension) ? "." + overrideExtension
: uri.getLastPathSegment());
config.setDisableDisconnectError(this.disableDisconnectError);
MediaItem mediaItem = new MediaItem.Builder().setUri(uri).build();
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder().setUri(uri);
if (adTagUrl != null) {
mediaItemBuilder.setAdsConfiguration(
new MediaItem.AdsConfiguration.Builder(adTagUrl).build()
);
}
MediaItem mediaItem = mediaItemBuilder.build();
DrmSessionManagerProvider drmProvider = null;
if (drmSessionManager != null) {
drmProvider = new DrmSessionManagerProvider() {
@ -813,12 +864,15 @@ class ReactExoplayerView extends FrameLayout implements
private void releasePlayer() {
if (player != null) {
adsLoader.setPlayer(null);
updateResumePosition();
player.release();
player.removeListener(this);
trackSelector = null;
player = null;
}
adsLoader.release();
adsLoader = null;
progressHandler.removeMessages(SHOW_PROGRESS);
themedReactContext.removeLifecycleEventListener(this);
audioBecomingNoisyReceiver.removeListener();
@ -1049,7 +1103,7 @@ class ReactExoplayerView extends FrameLayout implements
}
private void videoLoaded() {
if (loadVideoStarted) {
if (!player.isPlayingAd() && loadVideoStarted) {
loadVideoStarted = false;
if (audioTrackType != null) {
setSelectedAudioTrack(audioTrackType, audioTrackValue);
@ -1419,6 +1473,10 @@ class ReactExoplayerView extends FrameLayout implements
mReportBandwidth = reportBandwidth;
}
public void setAdTagUrl(final Uri uri) {
adTagUrl = uri;
}
public void setRawSrc(final Uri uri, final String extension) {
if (uri != null) {
boolean isSourceEqual = uri.equals(srcUri);
@ -1917,4 +1975,9 @@ class ReactExoplayerView extends FrameLayout implements
public void setSubtitleStyle(SubtitleStyle style) {
exoPlayerView.setSubtitleStyle(style);
}
@Override
public void onAdEvent(AdEvent adEvent) {
eventEmitter.receiveAdEvent(adEvent.getType().name());
}
}

View File

@ -31,6 +31,7 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
private static final String PROP_SRC = "src";
private static final String PROP_SRC_URI = "uri";
private static final String PROP_AD_TAG_URL = "adTagUrl";
private static final String PROP_SRC_TYPE = "type";
private static final String PROP_DRM = "drm";
private static final String PROP_DRM_TYPE = "type";
@ -189,6 +190,18 @@ public class ReactExoplayerViewManager extends ViewGroupManager<ReactExoplayerVi
}
}
@ReactProp(name = PROP_AD_TAG_URL)
public void setAdTagUrl(final ReactExoplayerView videoView, final String uriString) {
if (TextUtils.isEmpty(uriString)) {
return;
}
Uri adTagUrl = Uri.parse(uriString);
videoView.setAdTagUrl(adTagUrl);
}
@ReactProp(name = PROP_RESIZE_MODE)
public void setResizeMode(final ReactExoplayerView videoView, final String resizeModeOrdinalString) {
videoView.setResizeModeModifier(convertToIntDef(resizeModeOrdinalString));

View File

@ -50,6 +50,7 @@ class VideoEventEmitter {
private static final String EVENT_AUDIO_BECOMING_NOISY = "onVideoAudioBecomingNoisy";
private static final String EVENT_AUDIO_FOCUS_CHANGE = "onAudioFocusChanged";
private static final String EVENT_PLAYBACK_RATE_CHANGE = "onPlaybackRateChange";
private static final String EVENT_ON_RECEIVE_AD_EVENT = "onReceiveAdEvent";
static final String[] Events = {
EVENT_LOAD_START,
@ -73,6 +74,7 @@ class VideoEventEmitter {
EVENT_AUDIO_FOCUS_CHANGE,
EVENT_PLAYBACK_RATE_CHANGE,
EVENT_BANDWIDTH,
EVENT_ON_RECEIVE_AD_EVENT
};
@Retention(RetentionPolicy.SOURCE)
@ -98,6 +100,7 @@ class VideoEventEmitter {
EVENT_AUDIO_FOCUS_CHANGE,
EVENT_PLAYBACK_RATE_CHANGE,
EVENT_BANDWIDTH,
EVENT_ON_RECEIVE_AD_EVENT
})
@interface VideoEvents {
}
@ -330,6 +333,13 @@ class VideoEventEmitter {
receiveEvent(EVENT_AUDIO_BECOMING_NOISY, null);
}
void receiveAdEvent(String event) {
WritableMap map = Arguments.createMap();
map.putString("event", event);
receiveEvent(EVENT_ON_RECEIVE_AD_EVENT, map);
}
private void receiveEvent(@VideoEvents String type, WritableMap event) {
eventEmitter.receiveEvent(viewId, type, event);
}

View File

@ -237,7 +237,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 7.0;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@ -271,7 +271,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 7.0;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES;

View File

@ -0,0 +1,187 @@
import Foundation
import GoogleInteractiveMediaAds
class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate {
private var _video:RCTVideo
/* Entry point for the SDK. Used to make ad requests. */
private var adsLoader: IMAAdsLoader!
/* Main point of interaction with the SDK. Created by the SDK as the result of an ad request. */
private var adsManager: IMAAdsManager!
init(video:RCTVideo!) {
_video = video
super.init()
}
func setUpAdsLoader() {
adsLoader = IMAAdsLoader(settings: nil)
adsLoader.delegate = self
}
func requestAds() {
// Create ad display container for ad rendering.
let adDisplayContainer = IMAAdDisplayContainer(adContainer: _video, viewController: _video.reactViewController())
let adTagUrl = _video.getAdTagUrl()
let contentPlayhead = _video.getContentPlayhead()
if adTagUrl != nil && contentPlayhead != nil {
// Create an ad request with our ad tag, display container, and optional user context.
let request = IMAAdsRequest(
adTagUrl: adTagUrl!,
adDisplayContainer: adDisplayContainer,
contentPlayhead: contentPlayhead,
userContext: nil)
adsLoader.requestAds(with: request)
}
}
// MARK: - Getters
func getAdsLoader() -> IMAAdsLoader? {
return adsLoader
}
func getAdsManager() -> IMAAdsManager? {
return adsManager
}
// MARK: - IMAAdsLoaderDelegate
func adsLoader(_ loader: IMAAdsLoader, adsLoadedWith adsLoadedData: IMAAdsLoadedData) {
// 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.
let adsRenderingSettings: IMAAdsRenderingSettings = IMAAdsRenderingSettings();
adsRenderingSettings.linkOpenerPresentingController = _video.reactViewController();
adsManager.initialize(with: adsRenderingSettings)
}
func adsLoader(_ loader: IMAAdsLoader, failedWith adErrorData: IMAAdLoadingErrorData) {
if adErrorData.adError.message != nil {
print("Error loading ads: " + adErrorData.adError.message!)
}
_video.setPaused(false)
}
// MARK: - IMAAdsManagerDelegate
func adsManager(_ adsManager: IMAAdsManager, didReceive event: IMAAdEvent) {
// Play each ad once it has been loaded
if event.type == IMAAdEventType.LOADED {
adsManager.start()
}
if _video.onReceiveAdEvent != nil {
let type = convertEventToString(event: event.type)
_video.onReceiveAdEvent?([
"event": type,
"target": _video.reactTag!
]);
}
}
func adsManager(_ adsManager: IMAAdsManager, didReceive error: IMAAdError) {
if error.message != nil {
print("AdsManager error: " + error.message!)
}
// Fall back to playing content
_video.setPaused(false)
}
func adsManagerDidRequestContentPause(_ adsManager: IMAAdsManager) {
// Pause the content for the SDK to play ads.
_video.setPaused(true)
_video.setAdPlaying(true)
}
func adsManagerDidRequestContentResume(_ adsManager: IMAAdsManager) {
// Resume the content since the SDK is done playing ads (at least for now).
_video.setAdPlaying(false)
_video.setPaused(false)
}
// MARK: - Helpers
func convertEventToString(event: IMAAdEventType!) -> String {
var result = "UNKNOWN";
switch(event) {
case .AD_BREAK_READY:
result = "AD_BREAK_READY";
break;
case .AD_BREAK_ENDED:
result = "AD_BREAK_ENDED";
break;
case .AD_BREAK_STARTED:
result = "AD_BREAK_STARTED";
break;
case .AD_PERIOD_ENDED:
result = "AD_PERIOD_ENDED";
break;
case .AD_PERIOD_STARTED:
result = "AD_PERIOD_STARTED";
break;
case .ALL_ADS_COMPLETED:
result = "ALL_ADS_COMPLETED";
break;
case .CLICKED:
result = "CLICK";
break;
case .COMPLETE:
result = "COMPLETE";
break;
case .CUEPOINTS_CHANGED:
result = "CUEPOINTS_CHANGED";
break;
case .FIRST_QUARTILE:
result = "FIRST_QUARTILE";
break;
case .LOADED:
result = "LOADED";
break;
case .LOG:
result = "LOG";
break;
case .MIDPOINT:
result = "MIDPOINT";
break;
case .PAUSE:
result = "PAUSED";
break;
case .RESUME:
result = "RESUMED";
break;
case .SKIPPED:
result = "SKIPPED";
break;
case .STARTED:
result = "STARTED";
break;
case .STREAM_LOADED:
result = "STREAM_LOADED";
break;
case .TAPPED:
result = "TAPPED";
break;
case .THIRD_QUARTILE:
result = "THIRD_QUARTILE";
break;
default:
result = "UNKNOWN";
}
return result;
}
}

View File

@ -1,6 +1,7 @@
import AVFoundation
import AVKit
import Foundation
import GoogleInteractiveMediaAds
import React
import Promises
@ -60,6 +61,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
private var _filterEnabled:Bool = false
private var _presentingViewController:UIViewController?
/* IMA Ads */
private var _adTagUrl:String?
private var _imaAdsManager: RCTIMAAdsManager!
private var _didRequestAds:Bool = false
private var _adPlaying:Bool = false
/* Playhead used by the SDK to track content video progress and insert mid-rolls. */
private var _contentPlayhead: IMAAVPlayerContentPlayhead?
private var _resouceLoaderDelegate: RCTResourceLoaderDelegate?
private var _playerObserver: RCTPlayerObserver = RCTPlayerObserver()
@ -94,10 +103,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
@objc var onPictureInPictureStatusChanged: RCTDirectEventBlock?
@objc var onRestoreUserInterfaceForPictureInPictureStop: RCTDirectEventBlock?
@objc var onGetLicense: RCTDirectEventBlock?
@objc var onReceiveAdEvent: RCTDirectEventBlock?
init(eventDispatcher:RCTEventDispatcher!) {
super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
_imaAdsManager = RCTIMAAdsManager(video: self)
_eventDispatcher = eventDispatcher
NotificationCenter.default.addObserver(
@ -135,6 +147,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
_imaAdsManager = RCTIMAAdsManager(video: self)
}
deinit {
@ -203,6 +217,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
])
if currentTimeSecs >= 0 {
if !_didRequestAds && currentTimeSecs >= 0.0001 && _adTagUrl != nil {
_imaAdsManager.requestAds()
_didRequestAds = true
}
onVideoProgress?([
"currentTime": NSNumber(value: Float(currentTimeSecs)),
"playableDuration": RCTVideoUtils.calculatePlayableDuration(_player),
@ -292,6 +310,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
self.setAutomaticallyWaitsToMinimizeStalling(self._automaticallyWaitsToMinimizeStalling)
}
if self._adTagUrl != nil {
// Set up your content playhead and contentComplete callback.
self._contentPlayhead = IMAAVPlayerContentPlayhead(avPlayer: self._player!)
self._imaAdsManager.setUpAdsLoader()
}
//Perform on next run loop, otherwise onVideoLoadStart is nil
self.onVideoLoadStart?([
"src": [
@ -400,18 +425,26 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
@objc
func setPaused(_ paused:Bool) {
if paused {
_player?.pause()
_player?.rate = 0.0
if _adPlaying {
_imaAdsManager.getAdsManager()?.pause()
} else {
_player?.pause()
_player?.rate = 0.0
}
} else {
RCTPlayerOperations.configureAudio(ignoreSilentSwitch:_ignoreSilentSwitch, mixWithOthers:_mixWithOthers)
if #available(iOS 10.0, *), !_automaticallyWaitsToMinimizeStalling {
_player?.playImmediately(atRate: _rate)
if _adPlaying {
_imaAdsManager.getAdsManager()?.resume()
} else {
_player?.play()
if #available(iOS 10.0, *), !_automaticallyWaitsToMinimizeStalling {
_player?.playImmediately(atRate: _rate)
} else {
_player?.play()
_player?.rate = _rate
}
_player?.rate = _rate
}
_player?.rate = _rate
}
_paused = paused
@ -782,6 +815,25 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
_filterEnabled = filterEnabled
}
// MARK: - RCTIMAAdsManager
func getAdTagUrl() -> String? {
return _adTagUrl
}
@objc
func setAdTagUrl(_ adTagUrl:String!) {
_adTagUrl = adTagUrl
}
func getContentPlayhead() -> IMAAVPlayerContentPlayhead? {
return _contentPlayhead
}
func setAdPlaying(_ adPlaying:Bool) {
_adPlaying = adPlaying
}
// MARK: - React View Management
func insertReactSubview(view:UIView!, atIndex:Int) {
@ -1059,9 +1111,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
@objc func handlePlayerItemDidReachEnd(notification:NSNotification!) {
onVideoEnd?(["target": reactTag as Any])
if notification.object as? AVPlayerItem == _player?.currentItem {
_imaAdsManager.getAdsLoader()?.contentComplete()
}
if _repeat {
let item:AVPlayerItem! = notification.object as? AVPlayerItem
item.seek(to: CMTime.zero)
item.seek(to: CMTime.zero, completionHandler: nil)
self.applyModifiers()
} else {
self.setPaused(true);

View File

@ -5,6 +5,7 @@
RCT_EXPORT_VIEW_PROPERTY(src, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(drm, NSDictionary);
RCT_EXPORT_VIEW_PROPERTY(adTagUrl, NSString);
RCT_EXPORT_VIEW_PROPERTY(maxBitRate, float);
RCT_EXPORT_VIEW_PROPERTY(resizeMode, NSString);
RCT_EXPORT_VIEW_PROPERTY(repeat, BOOL);
@ -59,6 +60,7 @@ RCT_EXPORT_VIEW_PROPERTY(onVideoExternalPlaybackChange, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onGetLicense, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onPictureInPictureStatusChanged, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onRestoreUserInterfaceForPictureInPictureStop, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onReceiveAdEvent, RCTDirectEventBlock);
RCT_EXTERN_METHOD(save:(NSDictionary *)options
reactTag:(nonnull NSNumber *)reactTag

View File

@ -18,6 +18,9 @@ Pod::Spec.new do |s|
s.subspec "Video" do |ss|
ss.source_files = "ios/Video/**/*.{h,m,swift}"
ss.dependency "PromisesSwift"
ss.ios.dependency 'GoogleAds-IMA-iOS-SDK', '~> 3.18.1'
ss.tvos.dependency 'GoogleAds-IMA-tvOS-SDK', '~> 4.2'
end
s.subspec "VideoCaching" do |ss|