From 1f7a9818346a40efe29347071d3a76f9bcacdaed Mon Sep 17 00:00:00 2001 From: "remi.gallego@gmail.com" Date: Thu, 25 Apr 2024 09:26:46 +0200 Subject: [PATCH 1/9] chore: bump to 6.0.0-alpha.6 --- API.md | 26 +++++ CHANGELOG.md | 6 + Video.js | 31 ++---- .../exoplayer/ReactExoplayerView.java | 48 +++++--- .../exoplayer/ReactExoplayerViewManager.java | 7 +- examples/basic/ios/Podfile | 2 + ios/RCTVideo.xcodeproj/project.pbxproj | 4 - ios/Video/DataStructures/VideoSource.swift | 6 + ios/Video/Features/RCTIMAAdsManager.swift | 6 + ios/Video/Features/RCTVideoUtils.swift | 10 +- ios/Video/RCTPlayerDelegate.swift | 50 --------- ios/Video/RCTVideo.swift | 103 +++++++++++------- ios/Video/RCTVideoManager.m | 2 - ios/Video/RCTVideoPlayerViewController.swift | 29 ----- package.json | 5 +- react-native-video.podspec | 11 +- 16 files changed, 176 insertions(+), 170 deletions(-) delete mode 100644 ios/Video/RCTPlayerDelegate.swift diff --git a/API.md b/API.md index d927a3b2ce..e926452a6a 100644 --- a/API.md +++ b/API.md @@ -74,6 +74,17 @@ Video with caching ([more info](docs/caching.md)): end ``` +#### Enable custom feature in podfile file + +##### Google IMA + +Google IMA is the google SDK to support Client Side Ads Integration (CSAI), see [google documentation](https://developers.google.com/interactive-media-ads/docs/sdks/ios/client-side) for more informations. + +To enable google IMA usage define add following line in your podfile: +```podfile +$RNVideoUseGoogleIMA=true +``` + ### tvOS installation @@ -916,6 +927,21 @@ The following other types are supported on some platforms, but aren't fully docu `content://, ms-appx://, ms-appdata://, assets-library://` +##### Playing only a portion of the video (start & end time) + +Provide an optional `startTime` and/or `endTime` for the video. Value is in milliseconds. Useful when you want to play only a portion of a large video. + +Example +``` +source={{ uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', startTime: 36012, endTime: 48500 }} + +source={{ uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', startTime: 36012 }} + +source={{ uri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', endTime: 48500 }} +``` + +Platforms: iOS, Android + #### subtitleStyle Property | Description | Platforms diff --git a/CHANGELOG.md b/CHANGELOG.md index 71b54f1c63..b94ab4c3d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ ## Changelog +### Version 6.0.0-alpha.6 +- Feature: Video range support [#3030](https://github.com/react-native-video/react-native-video/pull/3030) +- iOS: remove undocumented `currentTime` property [#3064](https://github.com/react-native-video/react-native-video/pull/3064) +- iOS: make sure that the audio in ads is muted when the player is muted. [#3068](https://github.com/react-native-video/react-native-video/pull/3077) +- iOS: make IMA build optionnal + ### Version 6.0.0-alpha.5 - iOS: ensure controls are not displayed when disabled by user [#3017](https://github.com/react-native-video/react-native-video/pull/3017) diff --git a/Video.js b/Video.js index 49c4506ba2..808c0a511e 100644 --- a/Video.js +++ b/Video.js @@ -1,11 +1,11 @@ -import { ImagePropTypes, ViewPropTypes } from 'deprecated-react-native-prop-types'; -import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { findNodeHandle, Image, NativeModules, Platform, requireNativeComponent, StyleSheet, UIManager, View } from 'react-native'; +import PropTypes from 'prop-types'; +import { StyleSheet, requireNativeComponent, NativeModules, UIManager, View, Image, Platform, findNodeHandle } from 'react-native'; +import { ViewPropTypes, ImagePropTypes } from 'deprecated-react-native-prop-types'; import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; -import DRMType from './DRMType'; -import FilterType from './FilterType'; import TextTrackType from './TextTrackType'; +import FilterType from './FilterType'; +import DRMType from './DRMType'; import VideoResizeMode from './VideoResizeMode.js'; const styles = StyleSheet.create({ @@ -15,7 +15,7 @@ const styles = StyleSheet.create({ }); const { VideoDecoderProperties } = NativeModules -export { TextTrackType, FilterType, DRMType, VideoDecoderProperties }; +export { TextTrackType, FilterType, DRMType, VideoDecoderProperties } export default class Video extends Component { @@ -77,7 +77,7 @@ export default class Video extends Component { this.setNativeProps({ fullscreen: false }); }; - save = async (options) => { + save = async (options?) => { return await NativeModules.VideoManager.save(options, findNodeHandle(this._root)); } @@ -189,12 +189,6 @@ export default class Video extends Component { } }; - _onOrientationChange = (event) => { - if (this.props.onOrientationChange) { - this.props.onOrientationChange(event.nativeEvent); - } - }; - _onFullscreenPlayerDidDismiss = (event) => { if (this.props.onFullscreenPlayerDidDismiss) { this.props.onFullscreenPlayerDidDismiss(event.nativeEvent); @@ -348,6 +342,8 @@ export default class Video extends Component { mainVer: source.mainVer || 0, patchVer: source.patchVer || 0, requestHeaders: source.headers ? this.stringsOnlyObject(source.headers) : {}, + startTime: source.startTime || 0, + endTime: source.endTime }, onVideoLoadStart: this._onLoadStart, onVideoPlaybackStateChanged: this._onPlaybackStateChanged, @@ -368,7 +364,6 @@ export default class Video extends Component { onVideoFullscreenPlayerDidPresent: this._onFullscreenPlayerDidPresent, onVideoFullscreenPlayerWillDismiss: this._onFullscreenPlayerWillDismiss, onVideoFullscreenPlayerDidDismiss: this._onFullscreenPlayerDidDismiss, - onVideoPlayerOrientationChange: this._onOrientationChange, onReadyForDisplay: this._onReadyForDisplay, onPlaybackStalled: this._onPlaybackStalled, onPlaybackResume: this._onPlaybackResume, @@ -527,7 +522,6 @@ Video.propTypes = { disableBuffering: PropTypes.bool, controls: PropTypes.bool, audioOnly: PropTypes.bool, - currentTime: PropTypes.number, fullscreenAutorotate: PropTypes.bool, fullscreenOrientation: PropTypes.oneOf(['all', 'landscape', 'portrait']), progressUpdateInterval: PropTypes.number, @@ -557,7 +551,6 @@ Video.propTypes = { onFullscreenPlayerDidPresent: PropTypes.func, onFullscreenPlayerWillDismiss: PropTypes.func, onFullscreenPlayerDidDismiss: PropTypes.func, - onOrientationChange: PropTypes.func, onReadyForDisplay: PropTypes.func, onPlaybackStalled: PropTypes.func, onPlaybackResume: PropTypes.func, @@ -565,17 +558,11 @@ Video.propTypes = { onAudioFocusChanged: PropTypes.func, onAudioBecomingNoisy: PropTypes.func, onPictureInPictureStatusChanged: PropTypes.func, - needsToRestoreUserInterfaceForPictureInPictureStop: PropTypes.func, onExternalPlaybackChange: PropTypes.func, adTagUrl: PropTypes.string, onReceiveAdEvent: PropTypes.func, /* Required by react-native */ - scaleX: PropTypes.number, - scaleY: PropTypes.number, - translateX: PropTypes.number, - translateY: PropTypes.number, - rotation: PropTypes.number, ...ViewPropTypes, }; diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index ed17929968..ebecbdde0d 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -4,6 +4,7 @@ import static com.google.android.exoplayer2.C.CONTENT_TYPE_HLS; import static com.google.android.exoplayer2.C.CONTENT_TYPE_OTHER; import static com.google.android.exoplayer2.C.CONTENT_TYPE_SS; +import static com.google.android.exoplayer2.C.TIME_END_OF_SOURCE; import android.annotation.SuppressLint; import android.app.Activity; @@ -93,6 +94,7 @@ 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.android.exoplayer2.source.ClippingMediaSource; import com.google.common.collect.ImmutableList; import java.net.CookieHandler; @@ -181,6 +183,8 @@ class ReactExoplayerView extends FrameLayout implements // Props from React private int backBufferDurationMs = DefaultLoadControl.DEFAULT_BACK_BUFFER_DURATION_MS; private Uri srcUri; + private long startTimeMs = -1; + private long endTimeMs = -1; private String extension; private boolean repeat; private String audioTrackType; @@ -301,9 +305,7 @@ private void createViews() { @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - // uncommented `initializePlayer()`, because it caused video to restart when it is running - // behind another screen and the user navigated back to the screen where the video runs - // initializePlayer(); + initializePlayer(); } @Override @@ -671,7 +673,7 @@ private DrmSessionManager initializePlayerDrm(ReactExoplayerView self) { private void initializePlayerSource(ReactExoplayerView self, DrmSessionManager drmSessionManager) { ArrayList mediaSourceList = buildTextSources(); - MediaSource videoSource = buildMediaSource(self.srcUri, self.extension, drmSessionManager); + MediaSource videoSource = buildMediaSource(self.srcUri, self.extension, drmSessionManager, startTimeMs, endTimeMs); MediaSource mediaSourceWithAds = null; if (adTagUrl != null) { MediaSource.Factory mediaSourceFactory = new DefaultMediaSourceFactory(mediaDataSourceFactory) @@ -766,7 +768,7 @@ private DrmSessionManager buildDrmSessionManager(UUID uuid, String licenseUrl, S } } - private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager) { + private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessionManager drmSessionManager, long startTimeMs, long endTimeMs) { if (uri == null) { throw new IllegalStateException("Invalid video uri"); } @@ -783,7 +785,7 @@ private MediaSource buildMediaSource(Uri uri, String overrideExtension, DrmSessi } MediaItem mediaItem = mediaItemBuilder.build(); - + MediaSource mediaSource = null; DrmSessionManagerProvider drmProvider = null; if (drmSessionManager != null) { drmProvider = new DrmSessionManagerProvider() { @@ -797,39 +799,54 @@ public DrmSessionManager get(MediaItem mediaItem) { } switch (type) { case CONTENT_TYPE_SS: - return new SsMediaSource.Factory( + mediaSource = new SsMediaSource.Factory( new DefaultSsChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false) ).setDrmSessionManagerProvider(drmProvider) .setLoadErrorHandlingPolicy( config.buildLoadErrorHandlingPolicy(minLoadRetryCount) ).createMediaSource(mediaItem); + break; case CONTENT_TYPE_DASH: - return new DashMediaSource.Factory( + mediaSource = new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), buildDataSourceFactory(false) ).setDrmSessionManagerProvider(drmProvider) .setLoadErrorHandlingPolicy( config.buildLoadErrorHandlingPolicy(minLoadRetryCount) ).createMediaSource(mediaItem); + break; case CONTENT_TYPE_HLS: - return new HlsMediaSource.Factory( + mediaSource = new HlsMediaSource.Factory( mediaDataSourceFactory ).setDrmSessionManagerProvider(drmProvider) .setLoadErrorHandlingPolicy( config.buildLoadErrorHandlingPolicy(minLoadRetryCount) ).createMediaSource(mediaItem); + break; case CONTENT_TYPE_OTHER: - return new ProgressiveMediaSource.Factory( + mediaSource = new ProgressiveMediaSource.Factory( mediaDataSourceFactory ).setDrmSessionManagerProvider(drmProvider) .setLoadErrorHandlingPolicy( config.buildLoadErrorHandlingPolicy(minLoadRetryCount) ).createMediaSource(mediaItem); + break; default: { throw new IllegalStateException("Unsupported type: " + type); } } + + if (startTimeMs >= 0 && endTimeMs >= 0) + { + return new ClippingMediaSource(mediaSource, startTimeMs * 1000, endTimeMs * 1000); + } else if (startTimeMs >= 0) { + return new ClippingMediaSource(mediaSource, startTimeMs * 1000, TIME_END_OF_SOURCE); + } else if (endTimeMs >= 0) { + return new ClippingMediaSource(mediaSource, 0, endTimeMs * 1000); + } + + return mediaSource; } private ArrayList buildTextSources() { @@ -1467,11 +1484,13 @@ public void onMetadata(Metadata metadata) { // ReactExoplayerViewManager public api - public void setSrc(final Uri uri, final String extension, Map headers) { + public void setSrc(final Uri uri, final long startTimeMs, final long endTimeMs, final String extension, Map headers) { if (uri != null) { - boolean isSourceEqual = uri.equals(srcUri); + boolean isSourceEqual = uri.equals(srcUri) && startTimeMs == this.startTimeMs && endTimeMs == this.endTimeMs; hasDrmFailed = false; this.srcUri = uri; + this.startTimeMs = startTimeMs; + this.endTimeMs = endTimeMs; this.extension = extension; this.requestHeaders = headers; this.mediaDataSourceFactory = @@ -1479,7 +1498,6 @@ public void setSrc(final Uri uri, final String extension, Map he this.requestHeaders); if (!isSourceEqual) { - Log.d("PLAYER", "reload1"); reloadSource(); } } @@ -1490,6 +1508,8 @@ public void clearSrc() { player.stop(); player.clearMediaItems(); this.srcUri = null; + this.startTimeMs = -1; + this.endTimeMs = -1; this.extension = null; this.requestHeaders = null; this.mediaDataSourceFactory = null; @@ -1517,7 +1537,6 @@ public void setRawSrc(final Uri uri, final String extension) { this.mediaDataSourceFactory = buildDataSourceFactory(true); if (!isSourceEqual) { - Log.d("PLAYER", "reload2"); reloadSource(); } } @@ -1525,7 +1544,6 @@ public void setRawSrc(final Uri uri, final String extension) { public void setTextTracks(ReadableArray textTracks) { this.textTracks = textTracks; - Log.d("PLAYER", "reload3"); reloadSource(); } diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java index 8da025e74e..e0468985f2 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerViewManager.java @@ -28,9 +28,10 @@ public class ReactExoplayerViewManager extends ViewGroupManager { private static final String REACT_CLASS = "RCTVideo"; - private static final String PROP_SRC = "src"; private static final String PROP_SRC_URI = "uri"; + private static final String PROP_SRC_START_TIME = "startTime"; + private static final String PROP_SRC_END_TIME = "endTime"; private static final String PROP_AD_TAG_URL = "adTagUrl"; private static final String PROP_SRC_TYPE = "type"; private static final String PROP_DRM = "drm"; @@ -152,6 +153,8 @@ public void setDRM(final ReactExoplayerView videoView, @Nullable ReadableMap drm public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src) { Context context = videoView.getContext().getApplicationContext(); String uriString = src.hasKey(PROP_SRC_URI) ? src.getString(PROP_SRC_URI) : null; + int startTimeMs = src.hasKey(PROP_SRC_START_TIME) ? src.getInt(PROP_SRC_START_TIME) : -1; + int endTimeMs = src.hasKey(PROP_SRC_END_TIME) ? src.getInt(PROP_SRC_END_TIME) : -1; String extension = src.hasKey(PROP_SRC_TYPE) ? src.getString(PROP_SRC_TYPE) : null; Map headers = src.hasKey(PROP_SRC_HEADERS) ? toStringMap(src.getMap(PROP_SRC_HEADERS)) : null; @@ -164,7 +167,7 @@ public void setSrc(final ReactExoplayerView videoView, @Nullable ReadableMap src Uri srcUri = Uri.parse(uriString); if (srcUri != null) { - videoView.setSrc(srcUri, extension, headers); + videoView.setSrc(srcUri, startTimeMs, endTimeMs, extension, headers); } } else { int identifier = context.getResources().getIdentifier( diff --git a/examples/basic/ios/Podfile b/examples/basic/ios/Podfile index 5677fd6ca9..9438f885ea 100644 --- a/examples/basic/ios/Podfile +++ b/examples/basic/ios/Podfile @@ -7,6 +7,8 @@ install! 'cocoapods', :deterministic_uuids => false target 'videoplayer' do config = use_native_modules! + # $RNVideoUseGoogleIMA = true + # Flags change depending on the env values. flags = get_default_flags() diff --git a/ios/RCTVideo.xcodeproj/project.pbxproj b/ios/RCTVideo.xcodeproj/project.pbxproj index ae0c5309b3..1665197d03 100644 --- a/ios/RCTVideo.xcodeproj/project.pbxproj +++ b/ios/RCTVideo.xcodeproj/project.pbxproj @@ -13,7 +13,6 @@ 0177D39D27170A7A00F5BE18 /* RCTVideoPlayerViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0177D39627170A7A00F5BE18 /* RCTVideoPlayerViewControllerDelegate.swift */; }; 0177D39E27170A7A00F5BE18 /* RCTVideoManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0177D39727170A7A00F5BE18 /* RCTVideoManager.m */; }; 0177D39F27170A7A00F5BE18 /* RCTVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0177D39927170A7A00F5BE18 /* RCTVideo.swift */; }; - 832E109729C34D2100DD1D3A /* RCTPlayerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 832E109629C34D2100DD1D3A /* RCTPlayerDelegate.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -50,7 +49,6 @@ 0177D39827170A7A00F5BE18 /* RCTVideo-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "RCTVideo-Bridging-Header.h"; path = "Video/RCTVideo-Bridging-Header.h"; sourceTree = ""; }; 0177D39927170A7A00F5BE18 /* RCTVideo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RCTVideo.swift; path = Video/RCTVideo.swift; sourceTree = ""; }; 134814201AA4EA6300B7C361 /* libRCTVideo.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTVideo.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 83064F5229C350FE0060F947 /* RCTPlayerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RCTPlayerDelegate.swift; path = Video/RCTPlayerDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -89,7 +87,6 @@ 58B511D21A9E6C8500147676 = { isa = PBXGroup; children = ( - 83064F5229C350FE0060F947 /* RCTPlayerDelegate.swift */, 01489050272001A100E69940 /* DataStructures */, 01489051272001A100E69940 /* Features */, 0177D39527170A7A00F5BE18 /* RCTSwiftLog */, @@ -192,7 +189,6 @@ 0177D39B27170A7A00F5BE18 /* UIView+FindUIViewController.swift in Sources */, 0177D39F27170A7A00F5BE18 /* RCTVideo.swift in Sources */, 0177D39E27170A7A00F5BE18 /* RCTVideoManager.m in Sources */, - 832E109729C34D2100DD1D3A /* RCTPlayerDelegate.swift in Sources */, 0177D39A27170A7A00F5BE18 /* RCTVideoManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/ios/Video/DataStructures/VideoSource.swift b/ios/Video/DataStructures/VideoSource.swift index 20ab7158d4..7cb7c38bc2 100644 --- a/ios/Video/DataStructures/VideoSource.swift +++ b/ios/Video/DataStructures/VideoSource.swift @@ -6,6 +6,8 @@ struct VideoSource { let isAsset: Bool let shouldCache: Bool let requestHeaders: Dictionary? + let startTime: Int64? + let endTime: Int64? let json: NSDictionary? @@ -18,6 +20,8 @@ struct VideoSource { self.isAsset = false self.shouldCache = false self.requestHeaders = nil + self.startTime = nil + self.endTime = nil return } self.json = json @@ -27,5 +31,7 @@ struct VideoSource { self.isAsset = json["isAsset"] as? Bool ?? false self.shouldCache = json["shouldCache"] as? Bool ?? false self.requestHeaders = json["requestHeaders"] as? Dictionary + self.startTime = json["startTime"] as? Int64 + self.endTime = json["endTime"] as? Int64 } } diff --git a/ios/Video/Features/RCTIMAAdsManager.swift b/ios/Video/Features/RCTIMAAdsManager.swift index 059ec6399e..44aff4be00 100644 --- a/ios/Video/Features/RCTIMAAdsManager.swift +++ b/ios/Video/Features/RCTIMAAdsManager.swift @@ -1,3 +1,4 @@ +#if USE_GOOGLE_IMA import Foundation import GoogleInteractiveMediaAds @@ -76,6 +77,10 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate { // MARK: - IMAAdsManagerDelegate func adsManager(_ adsManager: IMAAdsManager, didReceive event: IMAAdEvent) { + // Mute ad if the main player is muted + if (_video.isMuted()) { + adsManager.volume = 0; + } // Play each ad once it has been loaded if event.type == IMAAdEventType.LOADED { adsManager.start() @@ -185,3 +190,4 @@ class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate { return result; } } +#endif diff --git a/ios/Video/Features/RCTVideoUtils.swift b/ios/Video/Features/RCTVideoUtils.swift index 3953c9d009..9916db3719 100644 --- a/ios/Video/Features/RCTVideoUtils.swift +++ b/ios/Video/Features/RCTVideoUtils.swift @@ -12,13 +12,17 @@ enum RCTVideoUtils { * * \returns The playable duration of the current player item in seconds. */ - static func calculatePlayableDuration(_ player:AVPlayer?) -> NSNumber { + static func calculatePlayableDuration(_ player:AVPlayer?, withSource source:VideoSource?) -> NSNumber { guard let player = player, let video:AVPlayerItem = player.currentItem, video.status == AVPlayerItem.Status.readyToPlay else { return 0 } + if (source?.startTime != nil && source?.endTime != nil) { + return NSNumber(value: (Float64(source?.endTime ?? 0) - Float64(source?.startTime ?? 0)) / 1000) + } + var effectiveTimeRange:CMTimeRange? for (_, value) in video.loadedTimeRanges.enumerated() { let timeRange:CMTimeRange = value.timeRangeValue @@ -31,6 +35,10 @@ enum RCTVideoUtils { if let effectiveTimeRange = effectiveTimeRange { let playableDuration:Float64 = CMTimeGetSeconds(CMTimeRangeGetEnd(effectiveTimeRange)) if playableDuration > 0 { + if (source?.startTime != nil) { + return NSNumber(value: (playableDuration - Float64(source?.startTime ?? 0) / 1000)) + } + return playableDuration as NSNumber } } diff --git a/ios/Video/RCTPlayerDelegate.swift b/ios/Video/RCTPlayerDelegate.swift deleted file mode 100644 index 20de50f2be..0000000000 --- a/ios/Video/RCTPlayerDelegate.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// RCTPlayerDelegate.swift -// -import UIKit -import AVKit -import AVFoundation - -@available(iOS 9.0, *) -class RCTPlayerDelegate: NSObject, AVPlayerViewControllerDelegate { - - private var _onPictureInPictureStatusChanged: RCTDirectEventBlock? - private var _onVideoFullscreenPlayerWillPresent: RCTDirectEventBlock? - private var _onVideoFullscreenPlayerWillDismiss: RCTDirectEventBlock? - private var _onVideoFullscreenPlayerDidDismiss: RCTDirectEventBlock? - - init(_ onPictureInPictureStatusChanged: RCTDirectEventBlock?, _ onVideoFullscreenPlayerWillPresent: RCTDirectEventBlock?, _ onVideoFullscreenPlayerWillDismiss: RCTDirectEventBlock?, _ onVideoFullscreenPlayerDidDismiss: RCTDirectEventBlock?) { - _onPictureInPictureStatusChanged = onPictureInPictureStatusChanged - _onVideoFullscreenPlayerWillPresent = onVideoFullscreenPlayerWillPresent - _onVideoFullscreenPlayerWillDismiss = onVideoFullscreenPlayerWillDismiss - _onVideoFullscreenPlayerDidDismiss = onVideoFullscreenPlayerDidDismiss - } - - func playerViewControllerDidStartPictureInPicture(_ playerViewController: AVPlayerViewController) { - - _onPictureInPictureStatusChanged!([ "isActive": NSNumber(value: true) ]) - - } - - func playerViewControllerDidStopPictureInPicture(_ playerViewController: AVPlayerViewController) { - - _onPictureInPictureStatusChanged!([ "isActive": NSNumber(value: false) ]) - - } - - func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: UIViewControllerTransitionCoordinator) { - - _onVideoFullscreenPlayerWillPresent?([:]) - - } - - func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) { - coordinator.animate(alongsideTransition: nil, completion: { _ in - self._onVideoFullscreenPlayerDidDismiss?([:]) - }) - - _onVideoFullscreenPlayerWillDismiss?([:]) - - } - -} diff --git a/ios/Video/RCTVideo.swift b/ios/Video/RCTVideo.swift index 870f21d72e..7518751ffa 100644 --- a/ios/Video/RCTVideo.swift +++ b/ios/Video/RCTVideo.swift @@ -1,7 +1,9 @@ import AVFoundation import AVKit import Foundation +#if USE_GOOGLE_IMA import GoogleInteractiveMediaAds +#endif import React import Promises @@ -64,11 +66,13 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH /* IMA Ads */ private var _adTagUrl:String? +#if USE_GOOGLE_IMA 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? +#endif + private var _didRequestAds:Bool = false + private var _adPlaying:Bool = false private var _resouceLoaderDelegate: RCTResourceLoaderDelegate? private var _playerObserver: RCTPlayerObserver = RCTPlayerObserver() @@ -96,7 +100,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH @objc var onVideoFullscreenPlayerDidPresent: RCTDirectEventBlock? @objc var onVideoFullscreenPlayerWillDismiss: RCTDirectEventBlock? @objc var onVideoFullscreenPlayerDidDismiss: RCTDirectEventBlock? - @objc var onVideoPlayerOrientationChange: RCTDirectEventBlock? @objc var onReadyForDisplay: RCTDirectEventBlock? @objc var onPlaybackStalled: RCTDirectEventBlock? @objc var onPlaybackResume: RCTDirectEventBlock? @@ -109,8 +112,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH init(eventDispatcher:RCTEventDispatcher!) { super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) - +#if USE_GOOGLE_IMA _imaAdsManager = RCTIMAAdsManager(video: self) +#endif _eventDispatcher = eventDispatcher @@ -149,8 +153,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) - +#if USE_GOOGLE_IMA _imaAdsManager = RCTIMAAdsManager(video: self) +#endif } deinit { @@ -169,6 +174,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } @objc func applicationDidEnterBackground(notification:NSNotification!) { + if _playInBackground { + // Needed to play sound in background. See https://developer.apple.com/library/ios/qa/qa1668/_index.html + _playerLayer?.player = nil + _playerViewController?.player = nil + } } @objc func applicationWillEnterForeground(notification:NSNotification!) { @@ -204,7 +214,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH return } - let currentTime = _player?.currentTime() + var currentTime = _player?.currentTime() + if (currentTime != nil && _source?.startTime != nil) { + currentTime = CMTimeSubtract(currentTime!, CMTimeMake(value: _source?.startTime ?? 0, timescale: 1000)) + } let currentPlaybackTime = _player?.currentItem?.currentDate() let duration = CMTimeGetSeconds(playerDuration) let currentTimeSecs = CMTimeGetSeconds(currentTime ?? .zero) @@ -214,13 +227,15 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH ]) if currentTimeSecs >= 0 { +#if USE_GOOGLE_IMA if !_didRequestAds && currentTimeSecs >= 0.0001 && _adTagUrl != nil { _imaAdsManager.requestAds() _didRequestAds = true } +#endif onVideoProgress?([ "currentTime": NSNumber(value: Float(currentTimeSecs)), - "playableDuration": RCTVideoUtils.calculatePlayableDuration(_player), + "playableDuration": RCTVideoUtils.calculatePlayableDuration(_player, withSource: _source), "atValue": NSNumber(value: currentTime?.value ?? .zero), "currentPlaybackTime": NSNumber(value: NSNumber(value: floor(currentPlaybackTime?.timeIntervalSince1970 ?? 0 * 1000)).int64Value), "target": reactTag, @@ -289,6 +304,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH self._playerItem = playerItem self._playerObserver.playerItem = self._playerItem self.setPreferredForwardBufferDuration(self._preferredForwardBufferDuration) + self.setPlaybackRange(playerItem, withVideoStart: self._source?.startTime, withVideoEnd: self._source?.endTime) self.setFilter(self._filterName) if let maxBitRate = self._maxBitRate { self._playerItem?.preferredPeakBitRate = Double(maxBitRate) @@ -304,13 +320,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH self.setAutomaticallyWaitsToMinimizeStalling(self._automaticallyWaitsToMinimizeStalling) } +#if USE_GOOGLE_IMA if self._adTagUrl != nil { // Set up your content playhead and contentComplete callback. self._contentPlayhead = IMAAVPlayerContentPlayhead(avPlayer: self._player!) self._imaAdsManager.setUpAdsLoader() } - +#endif //Perform on next run loop, otherwise onVideoLoadStart is nil self.onVideoLoadStart?([ "src": [ @@ -421,7 +438,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH func setPaused(_ paused:Bool) { if paused { if _adPlaying { +#if USE_GOOGLE_IMA _imaAdsManager.getAdsManager()?.pause() +#endif } else { _player?.pause() _player?.rate = 0.0 @@ -430,7 +449,9 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH RCTPlayerOperations.configureAudio(ignoreSilentSwitch:_ignoreSilentSwitch, mixWithOthers:_mixWithOthers) if _adPlaying { +#if USE_GOOGLE_IMA _imaAdsManager.getAdsManager()?.resume() +#endif } else { if #available(iOS 10.0, *), !_automaticallyWaitsToMinimizeStalling { _player?.playImmediately(atRate: _rate) @@ -445,15 +466,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _paused = paused } - @objc - func setCurrentTime(_ currentTime:Float) { - let info:NSDictionary = [ - "time": NSNumber(value: currentTime), - "tolerance": NSNumber(value: 100) - ] - setSeek(info) - } - @objc func setSeek(_ info:NSDictionary!) { let seekTime:NSNumber! = info["time"] as! NSNumber @@ -493,6 +505,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH applyModifiers() } + @objc + func isMuted() -> Bool { + return _muted + } + @objc func setMuted(_ muted:Bool) { _muted = muted @@ -530,6 +547,18 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH // Fallback on earlier versions } } + + func setPlaybackRange(_ item:AVPlayerItem!, withVideoStart videoStart:Int64?, withVideoEnd videoEnd:Int64?) { + if (videoStart != nil) { + let start = CMTimeMake(value: videoStart!, timescale: 1000) + item.reversePlaybackEndTime = start + _pendingSeekTime = Float(CMTimeGetSeconds(start)) + _pendingSeek = true + } + if (videoEnd != nil) { + item.forwardPlaybackEndTime = CMTimeMake(value: videoEnd!, timescale: 1000) + } + } func applyModifiers() { @@ -630,14 +659,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } if viewController != nil { _presentingViewController = viewController - // Moved to RCTPlayerDelegate - // self.onVideoFullscreenPlayerWillPresent?(["target": reactTag as Any]) + + self.onVideoFullscreenPlayerWillPresent?(["target": reactTag as Any]) if let playerViewController = _playerViewController { - if(_controls) { - // prevents crash https://github.com/react-native-video/react-native-video/issues/3040 - self._playerViewController?.removeFromParent() - } viewController.present(playerViewController, animated:true, completion:{ self._playerViewController?.showsPlaybackControls = self._controls self._fullscreenPlayerPresented = fullscreen @@ -698,7 +723,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH viewController.showsPlaybackControls = self._controls viewController.rctDelegate = self viewController.preferredOrientation = _fullscreenOrientation - viewController.onVideoPlayerOrientationChange = self.onVideoPlayerOrientationChange viewController.view.frame = self.bounds viewController.player = player @@ -735,10 +759,6 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH { self.removePlayerLayer() self.usePlayerViewController() - if (_playerViewController !== nil) { - _playerDelegate = RCTPlayerDelegate(self.onPictureInPictureStatusChanged, self.onVideoFullscreenPlayerWillPresent, self.onVideoFullscreenPlayerWillDismiss, self.onVideoFullscreenPlayerDidDismiss) - _playerViewController!.delegate = _playerDelegate - } } else { @@ -767,8 +787,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH func videoPlayerViewControllerWillDismiss(playerViewController:AVPlayerViewController) { if _playerViewController == playerViewController && _fullscreenPlayerPresented, let onVideoFullscreenPlayerWillDismiss = onVideoFullscreenPlayerWillDismiss { _playerObserver.removePlayerViewControllerObservers() - // Moved to RCTPlayerDelegate - // onVideoFullscreenPlayerWillDismiss(["target": reactTag as Any]) + onVideoFullscreenPlayerWillDismiss(["target": reactTag as Any]) } } @@ -780,8 +799,8 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH _playerViewController = nil _playerObserver.playerViewController = nil self.applyModifiers() - // Moved to RCTPlayerDelegate - // onVideoFullscreenPlayerDidDismiss?(["target": reactTag as Any]) + + onVideoFullscreenPlayerDidDismiss?(["target": reactTag as Any]) } } @@ -831,11 +850,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH func setAdTagUrl(_ adTagUrl:String!) { _adTagUrl = adTagUrl } - +#if USE_GOOGLE_IMA func getContentPlayhead() -> IMAAVPlayerContentPlayhead? { return _contentPlayhead } - +#endif func setAdPlaying(_ adPlaying:Bool) { _adPlaying = adPlaying } @@ -1003,7 +1022,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH } if _pendingSeek { - setCurrentTime(_pendingSeekTime) + setSeek([ + "time": NSNumber(value: _pendingSeekTime), + "tolerance": NSNumber(value: 100) + ]) _pendingSeek = false } @@ -1083,13 +1105,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH let newRect = change.newValue if !oldRect!.equalTo(newRect!) { if newRect!.equalTo(UIScreen.main.bounds) { - NSLog("in fullscreen") + RCTLog("in fullscreen") self.reactViewController().view.frame = UIScreen.main.bounds self.reactViewController().view.setNeedsLayout() - } else { - NSLog("not fullscreen") - } + } else {NSLog("not fullscreen")} } } @@ -1115,10 +1135,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH @objc func handlePlayerItemDidReachEnd(notification:NSNotification!) { onVideoEnd?(["target": reactTag as Any]) - +#if USE_GOOGLE_IMA if notification.object as? AVPlayerItem == _player?.currentItem { + _imaAdsManager.getAdsLoader()?.contentComplete() } - +#endif if _repeat { let item:AVPlayerItem! = notification.object as? AVPlayerItem item.seek(to: CMTime.zero, completionHandler: nil) diff --git a/ios/Video/RCTVideoManager.m b/ios/Video/RCTVideoManager.m index 03a93263aa..0d503d1419 100644 --- a/ios/Video/RCTVideoManager.m +++ b/ios/Video/RCTVideoManager.m @@ -27,7 +27,6 @@ @interface RCT_EXTERN_MODULE(RCTVideoManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(mixWithOthers, NSString); RCT_EXPORT_VIEW_PROPERTY(rate, float); RCT_EXPORT_VIEW_PROPERTY(seek, NSDictionary); -RCT_EXPORT_VIEW_PROPERTY(currentTime, float); RCT_EXPORT_VIEW_PROPERTY(fullscreen, BOOL); RCT_EXPORT_VIEW_PROPERTY(fullscreenAutorotate, BOOL); RCT_EXPORT_VIEW_PROPERTY(fullscreenOrientation, NSString); @@ -52,7 +51,6 @@ @interface RCT_EXTERN_MODULE(RCTVideoManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerDidPresent, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerWillDismiss, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onVideoFullscreenPlayerDidDismiss, RCTDirectEventBlock); -RCT_EXPORT_VIEW_PROPERTY(onVideoPlayerOrientationChange, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onReadyForDisplay, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackStalled, RCTDirectEventBlock); RCT_EXPORT_VIEW_PROPERTY(onPlaybackResume, RCTDirectEventBlock); diff --git a/ios/Video/RCTVideoPlayerViewController.swift b/ios/Video/RCTVideoPlayerViewController.swift index c2319dd3a3..e398e62f05 100644 --- a/ios/Video/RCTVideoPlayerViewController.swift +++ b/ios/Video/RCTVideoPlayerViewController.swift @@ -7,7 +7,6 @@ class RCTVideoPlayerViewController: AVPlayerViewController { // Optional paramters var preferredOrientation:String? var autorotate:Bool? - var onVideoPlayerOrientationChange: RCTDirectEventBlock? func shouldAutorotate() -> Bool { @@ -18,15 +17,6 @@ class RCTVideoPlayerViewController: AVPlayerViewController { return false } - override func viewDidLoad() { - super.viewDidLoad() - NotificationCenter.default.addObserver(self, selector: #selector(deviceOrientationDidChange), name: UIDevice.orientationDidChangeNotification, object: nil) - } - - deinit { - NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil) - } - override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) @@ -37,25 +27,6 @@ class RCTVideoPlayerViewController: AVPlayerViewController { } #if !TARGET_OS_TV - - @objc func deviceOrientationDidChange() { - let orientation = UIDevice.current.orientation - // Handle device orientation change - // Numbers refer to Expo Scree Orientation types: - // https://docs.expo.dev/versions/latest/sdk/screen-orientation#orientation - switch orientation { - case .portrait: - onVideoPlayerOrientationChange!([ "orientation": NSNumber(value: 1) ]) - case .portraitUpsideDown: - onVideoPlayerOrientationChange!([ "orientation": NSNumber(value: 2) ]) - case .landscapeLeft: - onVideoPlayerOrientationChange!([ "orientation": NSNumber(value: 3) ]) - case .landscapeRight: - onVideoPlayerOrientationChange!([ "orientation": NSNumber(value: 4) ]) - default: - onVideoPlayerOrientationChange!([ "orientation": NSNumber(value: 0) ]) - } - } func supportedInterfaceOrientations() -> UIInterfaceOrientationMask { return .all diff --git a/package.json b/package.json index aa671cd92f..4ffa860013 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-video", - "version": "6.0.0-alpha.5", + "version": "6.0.0-alpha.6", "description": "A