Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: onPipChange #155

Merged
merged 2 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ 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


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"),
Expand Down Expand Up @@ -272,6 +275,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
}
Expand Down Expand Up @@ -371,6 +382,20 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte
private fun intervalHandler() {
val reactContext = context as ReactContext

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we lean on https://developer.android.com/develop/ui/views/picture-in-picture#handling_ui

override fun onPictureInPictureModeChanged(
     isInPictureInPictureMode: Boolean, newConfig: Configuration) {

instead of relying on polling the state?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe I attempted this but couldn't get it triggering so I went with polling as a last ditch effort as I needed something working at the time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However once the other PiP PR had been merged, I am happy to test it again :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Bowlerr was hoping to merge this PiP relate PR before #156 👍

Copy link
Contributor Author

@Bowlerr Bowlerr Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @dawhitla,

Last time I attempted to use this, it never triggered / claimed to override nothing.

override fun onPictureInPictureModeChanged( isInPictureInPictureMode: Boolean, newConfig: Configuration) { ...

Is there something I am missing?

Thank you

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I can pull your fork and take a look at why onPictureInPictureModeChanged does not trigger 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have cleaned this up a little replacing:

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && reactContext.packageManager .hasSystemFeature( PackageManager.FEATURE_PICTURE_IN_PICTURE))

with

if(pipEnabled) in the intervalHandler

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh I figured out why onPictureInPictureModeChanged isn't happening for the view

It's a function of a callback on the activity itself. src/main/java/com/facebook/react/ReactActivity.java

So hooking into onPictureInPictureModeChanged would require a manual setup 😮‍💨

Here is what another RN lib did
https://github.com/adkaushik/react-native-pip-android#setup

We may need to rely on the interval check for the time being for easier DX (no manual setup step)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah thank you ! @dawhitla

I believe that's where I got to before switching to the interval check. Although it isn't ideal, the interval check has been reliable in our production use case :) Check out Moar if you want to see it in action.

I am happy to look into getting this done at some point but other priorities have taken my focus for the time being. Feel free to merge as is

&& reactContext.packageManager
.hasSystemFeature(
PackageManager.FEATURE_PICTURE_IN_PICTURE))
{
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()

Expand Down Expand Up @@ -443,7 +468,6 @@ class AmazonIvsView(private val context: ThemedReactContext) : FrameLayout(conte
} else {
activity?.enterPictureInPictureMode();
}

}
}

Expand Down
8 changes: 8 additions & 0 deletions docs/ivs-player-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,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.
Expand Down
3 changes: 3 additions & 0 deletions docs/usage-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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...')
}}
Expand Down
3 changes: 3 additions & 0 deletions example/src/screens/PlaygroundExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,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) =>
Expand Down
1 change: 1 addition & 0 deletions ios/AmazonIvsManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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)
Expand Down
16 changes: 16 additions & 0 deletions ios/AmazonIvsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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? {
Expand Down Expand Up @@ -470,3 +473,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])
}
}

1 change: 1 addition & 0 deletions src/IVSPlayer.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,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,
Expand Down
11 changes: 11 additions & 0 deletions src/IVSPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,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;
Expand Down Expand Up @@ -101,6 +102,7 @@ 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;
Expand Down Expand Up @@ -147,6 +149,7 @@ const IVSPlayerContainer = React.forwardRef<IVSPlayerRef, Props>(
onPlayerStateChange,
onDurationChange,
onQualityChange,
onPipChange,
onRebuffering,
onLoadStart,
onLoad,
Expand Down Expand Up @@ -258,6 +261,13 @@ const IVSPlayerContainer = React.forwardRef<IVSPlayerRef, Props>(
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;
Expand Down Expand Up @@ -358,6 +368,7 @@ const IVSPlayerContainer = React.forwardRef<IVSPlayerRef, Props>(
onData={onDataHandler}
onSeek={onSeekHandler}
onQualityChange={onQualityChangeHandler}
onPipChange={onPipChangeHandler}
onPlayerStateChange={onPlayerStateChangeHandler}
onDurationChange={onDurationChangeHandler}
onRebuffering={onRebuffering}
Expand Down
8 changes: 8 additions & 0 deletions src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down