feat: implement enterPictureInPictureOnLeave prop for both platform (Android, iOS) (#3385)
* docs: enable Android PIP * chore: change comments * feat(android): implement Android PictureInPicture * refactor: minor refactor code and apply lint * fix: rewrite pip action intent code for Android14 * fix: remove redundant codes * feat: add isInPictureInPicture flag for lifecycle handling - activity provide helper method for same purpose, but this flag makes code simple * feat: add pipFullscreenPlayerView for makes PIP include video only * fix: add manifest value checker for prevent crash * docs: add pictureInPicture prop's Android guide * fix: sync controller visibility * refactor: refining variable name * fix: check multi window mode when host pause - some OS version call onPause on multi-window mode * fix: handling when onStop is called while in multi-window mode * refactor: enhance PIP util codes * fix: fix FullscreenPlayerView constructor * refactor: add enterPictureInPictureOnLeave prop and pip methods - remove pictureInPicture boolean prop - add enterPictureInPictureOnLeave boolean prop - add enterPictureInPicture method - add exitPictureInPicture method * fix: fix lint error * fix: prevent audio play in background without playInBackground prop * fix: fix onDetachedFromWindow * docs: update docs for pip * fix(android): sync pip controller with external controller state - for media session * fix(ios): fix pip active fn variable reference * refactor(ios): refactor code * refactor(android): refactor codes * fix(android): fix lint error * refactor(android): refactor android pip logics * fix(android): fix flickering issue when stop picture in picture * fix(android): fix import * fix(android): fix picture in picture with fullscreen mode * fix(ios): fix syntax error * fix(android): fix Fragment managed code * refactor(android): remove redundant override lifecycle * fix(js): add PIP type definition for codegen * fix(android): fix syntax * chore(android): fix lint error * fix(ios): fix enter background handler * refactor(ios): remove redundant code * fix(ios): fix applicationDidEnterBackground for PIP * fix(android): fix onPictureInPictureStatusChanged * fix(ios): fix RCTPictureInPicture * refactor(android): Ignore exception for some device ignore pip checker - some device ignore PIP availability check, so we need to handle exception to prevent crash * fix(android): add hideWithoutPlayer fn into Kotlin ver * refactor(android): remove redundant code * fix(android): fix pip ratio to be calculated with correct ratio value * fix(android): fix crash issue when unmounting in PIP mode * fix(android): fix lint error * Update android/src/main/java/com/brentvatne/react/VideoManagerModule.kt * fix(android): fix lint error * fix(ios): fix lint error * fix(ios): fix lint error * feat(expo): add android picture in picture config within expo plugin * fix: Replace Fragment with androidx.activity - remove code that uses Fragment, which is a tricky implementation * fix: fix lint error * fix(android): disable auto enter when player released * fix(android): fix event handler to check based on Activity it's bound to --------- Co-authored-by: jonghun <jonghun@toss.im> Co-authored-by: Olivier Bouillet <62574056+freeboub@users.noreply.github.com>
This commit is contained in:
@@ -4,16 +4,16 @@
|
||||
|
||||
class RCTIMAAdsManager: NSObject, IMAAdsLoaderDelegate, IMAAdsManagerDelegate, IMALinkOpenerDelegate {
|
||||
private weak var _video: RCTVideo?
|
||||
private var _pipEnabled: () -> Bool
|
||||
private var _isPictureInPictureActive: () -> Bool
|
||||
|
||||
/* 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!, pipEnabled: @escaping () -> Bool) {
|
||||
init(video: RCTVideo!, isPictureInPictureActive: @escaping () -> Bool) {
|
||||
_video = video
|
||||
_pipEnabled = pipEnabled
|
||||
_isPictureInPictureActive = isPictureInPictureActive
|
||||
|
||||
super.init()
|
||||
}
|
||||
@@ -103,7 +103,7 @@
|
||||
}
|
||||
// Play each ad once it has been loaded
|
||||
if event.type == IMAAdEventType.LOADED {
|
||||
if _pipEnabled() {
|
||||
if _isPictureInPictureActive() {
|
||||
return
|
||||
}
|
||||
adsManager.start()
|
||||
|
@@ -11,7 +11,9 @@ import React
|
||||
private var _onPictureInPictureExit: (() -> Void)?
|
||||
private var _onRestoreUserInterfaceForPictureInPictureStop: (() -> Void)?
|
||||
private var _restoreUserInterfaceForPIPStopCompletionHandler: ((Bool) -> Void)?
|
||||
private var _isActive = false
|
||||
private var _isPictureInPictureActive: Bool {
|
||||
return _pipController?.isPictureInPictureActive ?? false
|
||||
}
|
||||
|
||||
init(
|
||||
_ onPictureInPictureEnter: (() -> Void)? = nil,
|
||||
@@ -67,20 +69,22 @@ import React
|
||||
_pipController = nil
|
||||
}
|
||||
|
||||
func setPictureInPicture(_ isActive: Bool) {
|
||||
if _isActive == isActive {
|
||||
return
|
||||
}
|
||||
_isActive = isActive
|
||||
|
||||
func enterPictureInPicture() {
|
||||
guard let _pipController else { return }
|
||||
if !_isPictureInPictureActive {
|
||||
_pipController.startPictureInPicture()
|
||||
}
|
||||
}
|
||||
|
||||
if _isActive && !_pipController.isPictureInPictureActive {
|
||||
DispatchQueue.main.async {
|
||||
_pipController.startPictureInPicture()
|
||||
}
|
||||
} else if !_isActive && _pipController.isPictureInPictureActive {
|
||||
DispatchQueue.main.async {
|
||||
func exitPictureInPicture() {
|
||||
guard let _pipController else { return }
|
||||
if _isPictureInPictureActive {
|
||||
let state = UIApplication.shared.applicationState
|
||||
if state == .background || state == .inactive {
|
||||
deinitPipController()
|
||||
_onPictureInPictureExit?()
|
||||
_onRestoreUserInterfaceForPictureInPictureStop?()
|
||||
} else {
|
||||
_pipController.stopPictureInPicture()
|
||||
}
|
||||
}
|
||||
|
@@ -63,10 +63,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
private var _showNotificationControls = false
|
||||
// Buffer last bitrate value received. Initialized to -2 to ensure -1 (sometimes reported by AVPlayer) is not missed
|
||||
private var _lastBitrate = -2.0
|
||||
private var _pictureInPictureEnabled = false {
|
||||
private var _enterPictureInPictureOnLeave = false {
|
||||
didSet {
|
||||
#if os(iOS)
|
||||
if _pictureInPictureEnabled {
|
||||
if isPictureInPictureActive() { return }
|
||||
if _enterPictureInPictureOnLeave {
|
||||
initPictureinPicture()
|
||||
_playerViewController?.allowsPictureInPicturePlayback = true
|
||||
} else {
|
||||
@@ -166,11 +167,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
onRestoreUserInterfaceForPictureInPictureStop?([:])
|
||||
}
|
||||
|
||||
func isPipEnabled() -> Bool {
|
||||
return _pictureInPictureEnabled
|
||||
}
|
||||
|
||||
func isPipActive() -> Bool {
|
||||
func isPictureInPictureActive() -> Bool {
|
||||
#if os(iOS)
|
||||
return _pip?._pipController?.isPictureInPictureActive == true
|
||||
#else
|
||||
@@ -180,15 +177,17 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
func initPictureinPicture() {
|
||||
#if os(iOS)
|
||||
_pip = RCTPictureInPicture({ [weak self] in
|
||||
self?._onPictureInPictureEnter()
|
||||
}, { [weak self] in
|
||||
self?._onPictureInPictureExit()
|
||||
}, { [weak self] in
|
||||
self?.onRestoreUserInterfaceForPictureInPictureStop?([:])
|
||||
})
|
||||
if _pip == nil {
|
||||
_pip = RCTPictureInPicture({ [weak self] in
|
||||
self?._onPictureInPictureEnter()
|
||||
}, { [weak self] in
|
||||
self?._onPictureInPictureExit()
|
||||
}, { [weak self] in
|
||||
self?.onRestoreUserInterfaceForPictureInPictureStop?([:])
|
||||
})
|
||||
}
|
||||
|
||||
if _playerLayer != nil && !_controls {
|
||||
if _playerLayer != nil && !_controls && _pip?._pipController == nil {
|
||||
_pip?.setupPipController(_playerLayer)
|
||||
}
|
||||
#else
|
||||
@@ -200,17 +199,14 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
|
||||
ReactNativeVideoManager.shared.registerView(newInstance: self)
|
||||
#if USE_GOOGLE_IMA
|
||||
_imaAdsManager = RCTIMAAdsManager(video: self, pipEnabled: isPipEnabled)
|
||||
_imaAdsManager = RCTIMAAdsManager(video: self, isPictureInPictureActive: isPictureInPictureActive)
|
||||
#endif
|
||||
|
||||
_eventDispatcher = eventDispatcher
|
||||
|
||||
#if os(iOS)
|
||||
if _pictureInPictureEnabled {
|
||||
if _enterPictureInPictureOnLeave {
|
||||
initPictureinPicture()
|
||||
_playerViewController?.allowsPictureInPicturePlayback = true
|
||||
} else {
|
||||
_playerViewController?.allowsPictureInPicturePlayback = false
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -242,6 +238,20 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(screenWillLock),
|
||||
name: UIApplication.protectedDataWillBecomeUnavailableNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(screenDidUnlock),
|
||||
name: UIApplication.protectedDataDidBecomeAvailableNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(audioRouteChanged(notification:)),
|
||||
@@ -257,7 +267,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
#if USE_GOOGLE_IMA
|
||||
_imaAdsManager = RCTIMAAdsManager(video: self, pipEnabled: isPipEnabled)
|
||||
_imaAdsManager = RCTIMAAdsManager(video: self, isPictureInPictureActive: isPictureInPictureActive)
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -313,8 +323,12 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
@objc
|
||||
func applicationDidEnterBackground(notification _: NSNotification!) {
|
||||
if !_paused && isPictureInPictureActive() {
|
||||
_player?.play()
|
||||
_player?.rate = _rate
|
||||
}
|
||||
let isExternalPlaybackActive = getIsExternalPlaybackActive()
|
||||
if !_playInBackground || isExternalPlaybackActive || isPipActive() { return }
|
||||
if !_playInBackground || isExternalPlaybackActive || isPictureInPictureActive() { return }
|
||||
// Needed to play sound in background. See https://developer.apple.com/library/ios/qa/qa1668/_index.html
|
||||
_playerLayer?.player = nil
|
||||
_playerViewController?.player = nil
|
||||
@@ -327,6 +341,24 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
_playerViewController?.player = _player
|
||||
}
|
||||
|
||||
@objc
|
||||
func screenWillLock() {
|
||||
let isActiveBackgroundPip = isPictureInPictureActive() && UIApplication.shared.applicationState != .active
|
||||
if _playInBackground || !_isPlaying || !isActiveBackgroundPip { return }
|
||||
|
||||
_player?.pause()
|
||||
_player?.rate = 0.0
|
||||
}
|
||||
|
||||
@objc
|
||||
func screenDidUnlock() {
|
||||
let isActiveBackgroundPip = isPictureInPictureActive() && UIApplication.shared.applicationState != .active
|
||||
if _paused || !isActiveBackgroundPip { return }
|
||||
|
||||
_player?.play()
|
||||
_player?.rate = _rate
|
||||
}
|
||||
|
||||
// MARK: - Audio events
|
||||
|
||||
@objc
|
||||
@@ -710,19 +742,16 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
@objc
|
||||
func setPictureInPicture(_ pictureInPicture: Bool) {
|
||||
func setEnterPictureInPictureOnLeave(_ enterPictureInPictureOnLeave: Bool) {
|
||||
#if os(iOS)
|
||||
let audioSession = AVAudioSession.sharedInstance()
|
||||
do {
|
||||
try audioSession.setCategory(.playback)
|
||||
try audioSession.setActive(true, options: [])
|
||||
} catch {}
|
||||
if pictureInPicture {
|
||||
_pictureInPictureEnabled = true
|
||||
} else {
|
||||
_pictureInPictureEnabled = false
|
||||
if _enterPictureInPictureOnLeave != enterPictureInPictureOnLeave {
|
||||
_enterPictureInPictureOnLeave = enterPictureInPictureOnLeave
|
||||
}
|
||||
_pip?.setPictureInPicture(pictureInPicture)
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1093,8 +1122,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
viewController.view.frame = self.bounds
|
||||
viewController.player = player
|
||||
if #available(tvOS 14.0, *) {
|
||||
viewController.allowsPictureInPicturePlayback = _pictureInPictureEnabled
|
||||
viewController.allowsPictureInPicturePlayback = _enterPictureInPictureOnLeave
|
||||
}
|
||||
#if os(iOS)
|
||||
viewController.allowsPictureInPicturePlayback = _enterPictureInPictureOnLeave
|
||||
#endif
|
||||
return viewController
|
||||
}
|
||||
|
||||
@@ -1114,7 +1146,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
self.layer.needsDisplayOnBoundsChange = true
|
||||
#if os(iOS)
|
||||
if _pictureInPictureEnabled {
|
||||
if _enterPictureInPictureOnLeave {
|
||||
_pip?.setupPipController(_playerLayer)
|
||||
}
|
||||
#endif
|
||||
@@ -1685,6 +1717,31 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func enterPictureInPicture() {
|
||||
if _pip?._pipController == nil {
|
||||
initPictureinPicture()
|
||||
_playerViewController?.allowsPictureInPicturePlayback = true
|
||||
}
|
||||
_pip?.enterPictureInPicture()
|
||||
}
|
||||
|
||||
@objc
|
||||
func exitPictureInPicture() {
|
||||
guard isPictureInPictureActive() else { return }
|
||||
|
||||
_pip?.exitPictureInPicture()
|
||||
#if os(iOS)
|
||||
if _enterPictureInPictureOnLeave {
|
||||
initPictureinPicture()
|
||||
_playerViewController?.allowsPictureInPicturePlayback = true
|
||||
} else {
|
||||
_pip?.deinitPipController()
|
||||
_playerViewController?.allowsPictureInPicturePlayback = false
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Workaround for #3418 - https://github.com/TheWidlarzGroup/react-native-video/issues/3418#issuecomment-2043508862
|
||||
@objc
|
||||
func setOnClick(_: Any) {}
|
||||
|
@@ -23,7 +23,7 @@ RCT_EXPORT_VIEW_PROPERTY(playInBackground, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(preventsDisplaySleepDuringVideoPlayback, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(preferredForwardBufferDuration, float);
|
||||
RCT_EXPORT_VIEW_PROPERTY(playWhenInactive, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(pictureInPicture, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(enterPictureInPictureOnLeave, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(ignoreSilentSwitch, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(mixWithOthers, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(rate, float);
|
||||
@@ -73,6 +73,8 @@ RCT_EXTERN_METHOD(setLicenseResultErrorCmd : (nonnull NSNumber*)reactTag error :
|
||||
RCT_EXTERN_METHOD(setPlayerPauseStateCmd : (nonnull NSNumber*)reactTag paused : (nonnull BOOL)paused)
|
||||
RCT_EXTERN_METHOD(setVolumeCmd : (nonnull NSNumber*)reactTag volume : (nonnull float*)volume)
|
||||
RCT_EXTERN_METHOD(setFullScreenCmd : (nonnull NSNumber*)reactTag fullscreen : (nonnull BOOL)fullScreen)
|
||||
RCT_EXTERN_METHOD(enterPictureInPictureCmd : (nonnull NSNumber*)reactTag)
|
||||
RCT_EXTERN_METHOD(exitPictureInPictureCmd : (nonnull NSNumber*)reactTag)
|
||||
RCT_EXTERN_METHOD(setSourceCmd : (nonnull NSNumber*)reactTag source : (NSDictionary*)source)
|
||||
|
||||
RCT_EXTERN_METHOD(save
|
||||
|
@@ -72,6 +72,20 @@ class RCTVideoManager: RCTViewManager {
|
||||
})
|
||||
}
|
||||
|
||||
@objc(enterPictureInPictureCmd:)
|
||||
func enterPictureInPictureCmd(_ reactTag: NSNumber) {
|
||||
performOnVideoView(withReactTag: reactTag, callback: { videoView in
|
||||
videoView?.enterPictureInPicture()
|
||||
})
|
||||
}
|
||||
|
||||
@objc(exitPictureInPictureCmd:)
|
||||
func exitPictureInPictureCmd(_ reactTag: NSNumber) {
|
||||
performOnVideoView(withReactTag: reactTag, callback: { videoView in
|
||||
videoView?.exitPictureInPicture()
|
||||
})
|
||||
}
|
||||
|
||||
@objc(setSourceCmd:source:)
|
||||
func setSourceCmd(_ reactTag: NSNumber, source: NSDictionary) {
|
||||
performOnVideoView(withReactTag: reactTag, callback: { videoView in
|
||||
|
Reference in New Issue
Block a user