diff --git a/android/src/main/java/com/amazonaws/ivs/reactnative/player/AmazonIvsView.kt b/android/src/main/java/com/amazonaws/ivs/reactnative/player/AmazonIvsView.kt index c763c30..60988bd 100644 --- a/android/src/main/java/com/amazonaws/ivs/reactnative/player/AmazonIvsView.kt +++ b/android/src/main/java/com/amazonaws/ivs/reactnative/player/AmazonIvsView.kt @@ -27,15 +27,18 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte private var lastLiveLatency: Long? = null private var lastBitrate: Long? = null private var lastDuration: Long? = null + private var lastPipState: Boolean = false; private var finishedLoading: Boolean = false private var pipEnabled: Boolean = false private var isInBackground: Boolean = false + enum class Events(private val mName: String) { STATE_CHANGED("onPlayerStateChange"), DURATION_CHANGED("onDurationChange"), ERROR("onError"), QUALITY_CHANGED("onQualityChange"), + PIP_CHANGED("onPipChange"), CUE("onTextCue"), METADATA_CUE("onTextMetadataCue"), LOAD("onLoad"), @@ -281,6 +284,14 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.PROGRESS.toString(), data) } + fun onPipChange(active: Boolean) { + val reactContext = context as ReactContext + val data = Arguments.createMap() + data.putString("active", if (active) "true" else "false") + + reactContext.getJSModule(RCTEventEmitter::class.java).receiveEvent(id, Events.PIP_CHANGED.toString(), data) + } + private fun convertMilliSecondsToSeconds (milliSeconds: Long): Double { return milliSeconds / 1000.0 } @@ -380,6 +391,17 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte private fun intervalHandler() { val reactContext = context as ReactContext + if (pipEnabled) + { + val activity: Activity? = reactContext.currentActivity + val isPipActive = activity!!.isInPictureInPictureMode + if(lastPipState !== isPipActive) + { + lastPipState = isPipActive + onPipChange(isPipActive === true) + } + } + if (lastLiveLatency != player?.liveLatency) { val liveLatencyData = Arguments.createMap() diff --git a/docs/ivs-player-reference.md b/docs/ivs-player-reference.md index 884c654..603590d 100644 --- a/docs/ivs-player-reference.md +++ b/docs/ivs-player-reference.md @@ -259,6 +259,14 @@ Callback that returns new quality that is used in video/stream playback. type: [`(quality: Quality) => void`](./types.md#Quality) +### onPipChange _(optional)_ + +Callback that returns changes to the picture in picture state. +This returns false if no picture in picture is active. It returns true if picture in picture is active. +Available only for Android N+ and iOS 15+ + +type: `(isActive: boolean) => void` + ### resizeMode _(optional)_ Defines different modes for displaying video in a Player. diff --git a/docs/usage-guide.md b/docs/usage-guide.md index 0de6814..12726e0 100644 --- a/docs/usage-guide.md +++ b/docs/usage-guide.md @@ -83,6 +83,9 @@ You can find the full list of events in the [api-reference](./ivs-player-referen onQualityChange={(newQuality) => { console.log(`quality changed: ${newQuality?.name}`) }} + onPipChange={(isActive) => { + console.log(`picture in picture changed - isActive: ${isActive}`) + }} onRebuffering={() => { console.log('rebuffering...') }} diff --git a/example/src/screens/PlaygroundExample.tsx b/example/src/screens/PlaygroundExample.tsx index 6951b20..03daa18 100644 --- a/example/src/screens/PlaygroundExample.tsx +++ b/example/src/screens/PlaygroundExample.tsx @@ -191,6 +191,9 @@ export default function PlaygroundExample() { setDetectedQuality(newQuality); log(`quality changed: ${newQuality?.name}`); }} + onPipChange={(isActive) => { + log(`picture in picture changed - isActive: ${isActive}`); + }} onRebuffering={() => setBuffering(true)} onLoadStart={() => log(`load started`)} onLoad={(loadedDuration) => diff --git a/ios/AmazonIvsManager.m b/ios/AmazonIvsManager.m index f4ca182..5a0a6cf 100644 --- a/ios/AmazonIvsManager.m +++ b/ios/AmazonIvsManager.m @@ -29,6 +29,7 @@ @interface RCT_EXTERN_MODULE(AmazonIvsManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(onPlayerStateChange, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onDurationChange, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onQualityChange, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onPipChange, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onRebuffering, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoadStart, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoad, RCTDirectEventBlock) diff --git a/ios/AmazonIvsView.swift b/ios/AmazonIvsView.swift index 7b6c805..48d3ede 100644 --- a/ios/AmazonIvsView.swift +++ b/ios/AmazonIvsView.swift @@ -10,6 +10,7 @@ class AmazonIvsView: UIView, IVSPlayer.Delegate { @objc var onPlayerStateChange: RCTDirectEventBlock? @objc var onDurationChange: RCTDirectEventBlock? @objc var onQualityChange: RCTDirectEventBlock? + @objc var onPipChange: RCTDirectEventBlock? @objc var onRebuffering: RCTDirectEventBlock? @objc var onLoadStart: RCTDirectEventBlock? @objc var onLoad: RCTDirectEventBlock? @@ -37,6 +38,8 @@ class AmazonIvsView: UIView, IVSPlayer.Delegate { private var _pipController: Any? = nil + private var isPipActive: Bool = false + @available(iOS 15, *) private var pipController: AVPictureInPictureController? { @@ -498,3 +501,16 @@ class AmazonIvsView: UIView, IVSPlayer.Delegate { } } +@available(iOS 15, *) +extension AmazonIvsView: AVPictureInPictureControllerDelegate { + func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + isPipActive = true + onPipChange?(["active": isPipActive]) + } + + func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { + isPipActive = false + onPipChange?(["active": isPipActive]) + } +} + diff --git a/src/IVSPlayer.js.flow b/src/IVSPlayer.js.flow index 1cf3341..c3521f7 100644 --- a/src/IVSPlayer.js.flow +++ b/src/IVSPlayer.js.flow @@ -43,6 +43,7 @@ declare type Props = {| onPlayerStateChange?: (state: number) => void, onDurationChange?: (duration: number | null) => void, onQualityChange?: (quality: Quality | null) => void, + onPipChange?: (isActive: boolean) => void, onRebuffering?: () => void, onLoadStart?: () => void, onLoad?: (duration: number | null) => void, diff --git a/src/IVSPlayer.tsx b/src/IVSPlayer.tsx index a823b20..591ca0d 100644 --- a/src/IVSPlayer.tsx +++ b/src/IVSPlayer.tsx @@ -58,6 +58,7 @@ type IVSPlayerProps = { event: NativeSyntheticEvent<{ duration: number | null }> ): void; onQualityChange?(event: NativeSyntheticEvent<{ quality: Quality }>): void; + onPipChange?(event: NativeSyntheticEvent<{ active: boolean | string }>): void; onRebuffering?(): void; onLoadStart?(): void; onLoad?(event: NativeSyntheticEvent<{ duration: number | null }>): void; @@ -105,6 +106,7 @@ export type Props = { onPlayerStateChange?(state: PlayerState): void; onDurationChange?(duration: number | null): void; onQualityChange?(quality: Quality | null): void; + onPipChange?(isActive: boolean): void; onRebuffering?(): void; onLoadStart?(): void; onLoad?(duration: number | null): void; @@ -153,6 +155,7 @@ const IVSPlayerContainer = React.forwardRef( onPlayerStateChange, onDurationChange, onQualityChange, + onPipChange, onRebuffering, onLoadStart, onLoad, @@ -264,6 +267,13 @@ const IVSPlayerContainer = React.forwardRef( onQualityChange?.(newQuality); }; + const onPipChangeHandler = ( + event: NativeSyntheticEvent<{ active: string | boolean }> + ) => { + const { active } = event.nativeEvent; + onPipChange?.(active === true || active === 'true'); + }; + const onLoadHandler = ( event: NativeSyntheticEvent<{ duration: number | null; @@ -366,6 +376,7 @@ const IVSPlayerContainer = React.forwardRef( onData={onDataHandler} onSeek={onSeekHandler} onQualityChange={onQualityChangeHandler} + onPipChange={onPipChangeHandler} onPlayerStateChange={onPlayerStateChangeHandler} onDurationChange={onDurationChangeHandler} onRebuffering={onRebuffering} diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx index f948ce6..81391f2 100644 --- a/src/__tests__/index.test.tsx +++ b/src/__tests__/index.test.tsx @@ -96,6 +96,14 @@ test('Passing onQualityChange down works correctly', async () => { ); }); +test('Passing onPipChange down works correctly with boolean', async () => { + await testCallbackPassing('onPipChange', { active: true }, true); +}); + +test('Passing onPipChange down works correctly with string', async () => { + await testCallbackPassing('onPipChange', { active: 'true' }, true); +}); + test('Passing onRebuffering down works correctly', async () => { await testCallbackPassing('onRebuffering'); });