@fishjam-cloud/react-native-client 0.22.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/io/fishjam/reactnative/PipContainerView.kt +282 -0
- package/android/src/main/java/io/fishjam/reactnative/PipContainerViewModule.kt +41 -0
- package/android/src/main/java/io/fishjam/reactnative/PipTrackSelector.kt +98 -0
- package/android/src/main/java/io/fishjam/reactnative/PipViewFactory.kt +186 -0
- package/android/src/main/java/io/fishjam/reactnative/RNFishjamClient.kt +9 -1
- package/android/src/main/java/io/fishjam/reactnative/VideoPreviewView.kt +2 -0
- package/android/src/main/java/io/fishjam/reactnative/VideoRendererView.kt +2 -0
- package/android/src/main/java/io/fishjam/reactnative/VideoView.kt +152 -11
- package/android/src/main/java/io/fishjam/reactnative/helpers/PictureInPictureHelperFragment.kt +20 -0
- package/android/src/main/java/io/fishjam/reactnative/managers/ListenerManager.kt +3 -1
- package/build/RNFishjamClientModule.d.ts +2 -0
- package/build/RNFishjamClientModule.d.ts.map +1 -1
- package/build/RNFishjamClientModule.js.map +1 -1
- package/build/components/PipContainerView.d.ts +48 -0
- package/build/components/PipContainerView.d.ts.map +1 -0
- package/build/components/PipContainerView.js +34 -0
- package/build/components/PipContainerView.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/livestream/components/LivestreamStreamer.d.ts +3 -30
- package/build/livestream/components/LivestreamStreamer.d.ts.map +1 -1
- package/build/livestream/components/LivestreamStreamer.js +3 -27
- package/build/livestream/components/LivestreamStreamer.js.map +1 -1
- package/build/livestream/components/LivestreamViewer.d.ts +2 -7
- package/build/livestream/components/LivestreamViewer.d.ts.map +1 -1
- package/build/livestream/components/LivestreamViewer.js +1 -1
- package/build/livestream/components/LivestreamViewer.js.map +1 -1
- package/build/livestream/hooks/useLivestreamStreamer.d.ts +40 -3
- package/build/livestream/hooks/useLivestreamStreamer.d.ts.map +1 -1
- package/build/livestream/hooks/useLivestreamStreamer.js +45 -3
- package/build/livestream/hooks/useLivestreamStreamer.js.map +1 -1
- package/build/livestream/hooks/useLivestreamViewer.d.ts +7 -0
- package/build/livestream/hooks/useLivestreamViewer.d.ts.map +1 -1
- package/build/livestream/hooks/useLivestreamViewer.js +39 -11
- package/build/livestream/hooks/useLivestreamViewer.js.map +1 -1
- package/build/livestream/index.d.ts +1 -1
- package/build/livestream/index.d.ts.map +1 -1
- package/build/livestream/index.js +1 -1
- package/build/livestream/index.js.map +1 -1
- package/expo-module.config.json +4 -2
- package/ios/PictureInPictureManager.swift +215 -0
- package/ios/PipContainerView.swift +56 -0
- package/ios/PipContainerViewModule.swift +48 -0
- package/ios/RNFishjamClient.swift +1 -0
- package/ios/RNFishjamClientModule.swift +4 -3
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LivestreamViewer.js","sourceRoot":"","sources":["../../../src/livestream/components/LivestreamViewer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAqB,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"LivestreamViewer.js","sourceRoot":"","sources":["../../../src/livestream/components/LivestreamViewer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAqB,MAAM,wBAAwB,CAAC;AAuC3E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,EAC/B,KAAK,EACL,UAAU,EACV,YAAY,EACZ,WAAW,EACX,OAAO,EACP,GAAG,GACmB,EAAE,EAAE,CAAC,CAC3B,CAAC,cAAc,CACb,GAAG,CAAC,CAAC,GAAG,CAAC,CACT,KAAK,CAAC,CAAC,KAAsB,CAAC,CAC9B,UAAU,CAAC,CAAC,UAAU,CAAC,CACvB,YAAY,CAAC,CAAC,YAAY,CAAC,CAC3B,WAAW,CAAC,CAAC,WAAW,CAAC,CACzB,OAAO,CAAC,CAAC,OAAO,CAAC,EACjB,CACH,CAAC","sourcesContent":["import { CSSProperties, Ref } from 'react';\nimport { StyleProp, ViewStyle } from 'react-native';\nimport { WhepClientView, WhepClientViewRef } from 'react-native-whip-whep';\n\nexport type LivestreamViewerRef = WhepClientViewRef;\n\n/**\n * Props of the LivestreamView component\n */\nexport type LivestreamViewerProps = {\n /**\n * Styles of the LivestreamView component\n */\n style?: StyleProp<ViewStyle>;\n /**\n * A variable deciding whether the Picture-in-Picture is enabled.\n * Defaults to true.\n */\n pipEnabled?: boolean;\n /**\n * A variable deciding whether the Picture-in-Picture mode should be started automatically after the app is backgrounded.\n * Defaults to false.\n */\n autoStartPip?: boolean;\n /**\n * A variable deciding whether the Picture-in-Picture mode should be stopped automatically on iOS after the app is foregrounded.\n * Always enabled on Android as PiP is not supported in foreground.\n * Defaults to false.\n */\n autoStopPip?: boolean;\n /**\n * A variable deciding the size of the Picture-in-Picture mode.\n */\n pipSize?: {\n width: number;\n height: number;\n };\n\n ref: Ref<LivestreamViewerRef>;\n};\n\n/**\n * Renders a video player playing the livestream set up with {@link useLivestreamViewer} hook.\n *\n * @category Components\n * @param {object} props\n * @param {object} props.style\n */\nexport const LivestreamViewer = ({\n style,\n pipEnabled,\n autoStartPip,\n autoStopPip,\n pipSize,\n ref,\n}: LivestreamViewerProps) => (\n <WhepClientView\n ref={ref}\n style={style as CSSProperties}\n pipEnabled={pipEnabled}\n autoStartPip={autoStartPip}\n autoStopPip={autoStopPip}\n pipSize={pipSize}\n />\n);\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Camera, SenderAudioCodecName, SenderVideoCodecName, VideoParameters, WhipClientViewRef } from 'react-native-whip-whep';
|
|
2
2
|
/**
|
|
3
3
|
* @category Livestream
|
|
4
4
|
*/
|
|
@@ -12,17 +12,54 @@ export interface useLivestreamStreamerResult {
|
|
|
12
12
|
connect: (token: string, urlOverride?: string) => Promise<void>;
|
|
13
13
|
/** Callback to stop publishing anything previously published with {@link connect} */
|
|
14
14
|
disconnect: () => Promise<void>;
|
|
15
|
+
/** Callback to flip camera */
|
|
16
|
+
flipCamera: () => Promise<void>;
|
|
17
|
+
/** Callback to switch camera to the one passed as arguement */
|
|
18
|
+
switchCamera: (deviceId: string) => Promise<void>;
|
|
19
|
+
/** Callback to get the id of camera which is used for streaming */
|
|
20
|
+
currentCameraDeviceId: () => Promise<string | undefined>;
|
|
15
21
|
/** Utility flag which indicates the current connection status */
|
|
16
22
|
isConnected: boolean;
|
|
17
23
|
/**
|
|
18
24
|
* Reference to the WhipClient instance. Needs to be passed to the {@link LivestreamStreamer} component.
|
|
19
25
|
*/
|
|
20
|
-
whipClientRef: React.RefObject<
|
|
26
|
+
whipClientRef: React.RefObject<WhipClientViewRef | null>;
|
|
21
27
|
}
|
|
28
|
+
export type UseLivestreamStreamerParams = {
|
|
29
|
+
/**
|
|
30
|
+
* If video track should be enabled.
|
|
31
|
+
* Defaults to true.
|
|
32
|
+
*/
|
|
33
|
+
videoEnabled?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* If audio track should be enabled.
|
|
36
|
+
* Defaults to true.
|
|
37
|
+
*/
|
|
38
|
+
audioEnabled?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Camera to use for the livestream.
|
|
41
|
+
* Use {@link cameras} to get the list of supported cameras.
|
|
42
|
+
*/
|
|
43
|
+
camera?: Camera;
|
|
44
|
+
/**
|
|
45
|
+
* Set video parameters for the camera
|
|
46
|
+
*/
|
|
47
|
+
videoParameters?: VideoParameters;
|
|
48
|
+
/**
|
|
49
|
+
* Set the preferred video codecs for sending the video.
|
|
50
|
+
* Use {@link WhipClient.getSupportedVideoCodecs} to get the list of supported video codecs.
|
|
51
|
+
*/
|
|
52
|
+
preferredVideoCodecs?: SenderVideoCodecName[];
|
|
53
|
+
/**
|
|
54
|
+
* Set the preferred audio codecs for sending the audio.
|
|
55
|
+
* Use {@link WhipClient.getSupportedAudioCodecs} to get the list of supported audio codecs.
|
|
56
|
+
*/
|
|
57
|
+
preferredAudioCodecs?: SenderAudioCodecName[];
|
|
58
|
+
};
|
|
22
59
|
/**
|
|
23
60
|
* Hook for publishing a livestream, which can be then received with {@link useLivestreamViewer}
|
|
24
61
|
* @category Livestream
|
|
25
62
|
* @group Hooks
|
|
26
63
|
*/
|
|
27
|
-
export declare const useLivestreamStreamer: () => useLivestreamStreamerResult;
|
|
64
|
+
export declare const useLivestreamStreamer: ({ videoEnabled, audioEnabled, camera, videoParameters, preferredVideoCodecs, preferredAudioCodecs, }?: UseLivestreamStreamerParams) => useLivestreamStreamerResult;
|
|
28
65
|
//# sourceMappingURL=useLivestreamStreamer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLivestreamStreamer.d.ts","sourceRoot":"","sources":["../../../src/livestream/hooks/useLivestreamStreamer.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"useLivestreamStreamer.d.ts","sourceRoot":"","sources":["../../../src/livestream/hooks/useLivestreamStreamer.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EAEN,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EACf,iBAAiB,EAElB,MAAM,wBAAwB,CAAC;AAGhC;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C;;;;;OAKG;IACH,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,qFAAqF;IACrF,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,8BAA8B;IAC9B,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,+DAA+D;IAC/D,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,oEAAoE;IACpE,qBAAqB,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACzD,iEAAiE;IACjE,WAAW,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,aAAa,EAAE,KAAK,CAAC,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC;;;OAGG;IACH,oBAAoB,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAC9C;;;OAGG;IACH,oBAAoB,CAAC,EAAE,oBAAoB,EAAE,CAAC;CAC/C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAAI,uGAOnC,2BAAgC,KAAG,2BAqErC,CAAC"}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { useCallback, useRef } from 'react';
|
|
2
|
-
import { useWhipConnectionState } from 'react-native-whip-whep';
|
|
1
|
+
import { useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
import { cameras, useWhipConnectionState, } from 'react-native-whip-whep';
|
|
3
3
|
import { FISHJAM_WHIP_URL } from '../../consts';
|
|
4
4
|
/**
|
|
5
5
|
* Hook for publishing a livestream, which can be then received with {@link useLivestreamViewer}
|
|
6
6
|
* @category Livestream
|
|
7
7
|
* @group Hooks
|
|
8
8
|
*/
|
|
9
|
-
export const useLivestreamStreamer = () => {
|
|
9
|
+
export const useLivestreamStreamer = ({ videoEnabled, audioEnabled, camera, videoParameters, preferredVideoCodecs, preferredAudioCodecs, } = {}) => {
|
|
10
10
|
const state = useWhipConnectionState();
|
|
11
11
|
const isConnected = state === 'connected';
|
|
12
12
|
const whipClientRef = useRef(null);
|
|
@@ -20,9 +20,51 @@ export const useLivestreamStreamer = () => {
|
|
|
20
20
|
const disconnect = useCallback(async () => {
|
|
21
21
|
await whipClientRef.current?.disconnect();
|
|
22
22
|
}, []);
|
|
23
|
+
const flipCamera = useCallback(async () => {
|
|
24
|
+
await whipClientRef.current?.flipCamera();
|
|
25
|
+
}, []);
|
|
26
|
+
const switchCamera = useCallback(async (deviceId) => {
|
|
27
|
+
return await whipClientRef.current?.switchCamera(deviceId);
|
|
28
|
+
}, []);
|
|
29
|
+
const currentCameraDeviceId = useCallback(async () => {
|
|
30
|
+
return await whipClientRef.current?.currentCameraDeviceId();
|
|
31
|
+
}, []);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
try {
|
|
34
|
+
const initializeCamera = async () => {
|
|
35
|
+
await whipClientRef.current?.initializeCamera({
|
|
36
|
+
audioEnabled: audioEnabled ?? true,
|
|
37
|
+
videoEnabled: videoEnabled ?? true,
|
|
38
|
+
videoDeviceId: camera?.id ?? cameras[0].id,
|
|
39
|
+
videoParameters: videoParameters,
|
|
40
|
+
preferredVideoCodecs: preferredVideoCodecs,
|
|
41
|
+
preferredAudioCodecs: preferredAudioCodecs,
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
initializeCamera();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
console.error('Failed to initialize camera');
|
|
48
|
+
}
|
|
49
|
+
const ref = whipClientRef.current;
|
|
50
|
+
return () => {
|
|
51
|
+
ref?.disconnect();
|
|
52
|
+
};
|
|
53
|
+
}, [
|
|
54
|
+
camera,
|
|
55
|
+
videoParameters,
|
|
56
|
+
videoEnabled,
|
|
57
|
+
audioEnabled,
|
|
58
|
+
preferredVideoCodecs,
|
|
59
|
+
preferredAudioCodecs,
|
|
60
|
+
whipClientRef,
|
|
61
|
+
]);
|
|
23
62
|
return {
|
|
24
63
|
connect,
|
|
25
64
|
disconnect,
|
|
65
|
+
flipCamera,
|
|
66
|
+
switchCamera,
|
|
67
|
+
currentCameraDeviceId,
|
|
26
68
|
isConnected,
|
|
27
69
|
whipClientRef,
|
|
28
70
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLivestreamStreamer.js","sourceRoot":"","sources":["../../../src/livestream/hooks/useLivestreamStreamer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useLivestreamStreamer.js","sourceRoot":"","sources":["../../../src/livestream/hooks/useLivestreamStreamer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,EAEL,OAAO,EAKP,sBAAsB,GACvB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AA6DhD;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,EACpC,YAAY,EACZ,YAAY,EACZ,MAAM,EACN,eAAe,EACf,oBAAoB,EACpB,oBAAoB,MACW,EAAE,EAA+B,EAAE;IAClE,MAAM,KAAK,GAAG,sBAAsB,EAAE,CAAC;IACvC,MAAM,WAAW,GAAG,KAAK,KAAK,WAAW,CAAC;IAE1C,MAAM,aAAa,GAAG,MAAM,CAA2B,IAAI,CAAC,CAAC;IAE7D,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,EAAE,KAAa,EAAE,WAAoB,EAAE,EAAE;QACxE,MAAM,WAAW,GAAG,WAAW,IAAI,gBAAgB,CAAC;QACpD,MAAM,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC;YACnC,SAAS,EAAE,KAAK;YAChB,SAAS,EAAE,WAAW;SACvB,CAAC,CAAC;IACL,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACxC,MAAM,aAAa,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;IAC5C,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACxC,MAAM,aAAa,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;IAC5C,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,YAAY,GAAG,WAAW,CAAC,KAAK,EAAE,QAAgB,EAAE,EAAE;QAC1D,OAAO,MAAM,aAAa,CAAC,OAAO,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC7D,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,qBAAqB,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACnD,OAAO,MAAM,aAAa,CAAC,OAAO,EAAE,qBAAqB,EAAE,CAAC;IAC9D,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC;YACH,MAAM,gBAAgB,GAAG,KAAK,IAAI,EAAE;gBAClC,MAAM,aAAa,CAAC,OAAO,EAAE,gBAAgB,CAAC;oBAC5C,YAAY,EAAE,YAAY,IAAI,IAAI;oBAClC,YAAY,EAAE,YAAY,IAAI,IAAI;oBAClC,aAAa,EAAE,MAAM,EAAE,EAAE,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE;oBAC1C,eAAe,EAAE,eAAe;oBAChC,oBAAoB,EAAE,oBAAoB;oBAC1C,oBAAoB,EAAE,oBAAoB;iBAC3C,CAAC,CAAC;YACL,CAAC,CAAC;YACF,gBAAgB,EAAE,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;QAC/C,CAAC;QACD,MAAM,GAAG,GAAG,aAAa,CAAC,OAAO,CAAC;QAClC,OAAO,GAAG,EAAE;YACV,GAAG,EAAE,UAAU,EAAE,CAAC;QACpB,CAAC,CAAC;IACJ,CAAC,EAAE;QACD,MAAM;QACN,eAAe;QACf,YAAY;QACZ,YAAY;QACZ,oBAAoB;QACpB,oBAAoB;QACpB,aAAa;KACd,CAAC,CAAC;IAEH,OAAO;QACL,OAAO;QACP,UAAU;QACV,UAAU;QACV,YAAY;QACZ,qBAAqB;QACrB,WAAW;QACX,aAAa;KACd,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import { useCallback, useRef, useEffect } from 'react';\nimport {\n Camera,\n cameras,\n SenderAudioCodecName,\n SenderVideoCodecName,\n VideoParameters,\n WhipClientViewRef,\n useWhipConnectionState,\n} from 'react-native-whip-whep';\nimport { FISHJAM_WHIP_URL } from '../../consts';\n\n/**\n * @category Livestream\n */\nexport interface useLivestreamStreamerResult {\n /**\n * Callback used to start publishing the selected audio and video media streams.\n *\n * @remarks\n * Calling {@link connect} multiple times will have the effect of only publishing the **last** specified inputs.\n */\n connect: (token: string, urlOverride?: string) => Promise<void>;\n /** Callback to stop publishing anything previously published with {@link connect} */\n disconnect: () => Promise<void>;\n /** Callback to flip camera */\n flipCamera: () => Promise<void>;\n /** Callback to switch camera to the one passed as arguement */\n switchCamera: (deviceId: string) => Promise<void>;\n /** Callback to get the id of camera which is used for streaming */\n currentCameraDeviceId: () => Promise<string | undefined>;\n /** Utility flag which indicates the current connection status */\n isConnected: boolean;\n /**\n * Reference to the WhipClient instance. Needs to be passed to the {@link LivestreamStreamer} component.\n */\n whipClientRef: React.RefObject<WhipClientViewRef | null>;\n}\n\nexport type UseLivestreamStreamerParams = {\n /**\n * If video track should be enabled.\n * Defaults to true.\n */\n videoEnabled?: boolean;\n /**\n * If audio track should be enabled.\n * Defaults to true.\n */\n audioEnabled?: boolean;\n /**\n * Camera to use for the livestream.\n * Use {@link cameras} to get the list of supported cameras.\n */\n camera?: Camera;\n /**\n * Set video parameters for the camera\n */\n videoParameters?: VideoParameters;\n /**\n * Set the preferred video codecs for sending the video.\n * Use {@link WhipClient.getSupportedVideoCodecs} to get the list of supported video codecs.\n */\n preferredVideoCodecs?: SenderVideoCodecName[];\n /**\n * Set the preferred audio codecs for sending the audio.\n * Use {@link WhipClient.getSupportedAudioCodecs} to get the list of supported audio codecs.\n */\n preferredAudioCodecs?: SenderAudioCodecName[];\n};\n\n/**\n * Hook for publishing a livestream, which can be then received with {@link useLivestreamViewer}\n * @category Livestream\n * @group Hooks\n */\nexport const useLivestreamStreamer = ({\n videoEnabled,\n audioEnabled,\n camera,\n videoParameters,\n preferredVideoCodecs,\n preferredAudioCodecs,\n}: UseLivestreamStreamerParams = {}): useLivestreamStreamerResult => {\n const state = useWhipConnectionState();\n const isConnected = state === 'connected';\n\n const whipClientRef = useRef<WhipClientViewRef | null>(null);\n\n const connect = useCallback(async (token: string, urlOverride?: string) => {\n const resolvedUrl = urlOverride ?? FISHJAM_WHIP_URL;\n await whipClientRef.current?.connect({\n authToken: token,\n serverUrl: resolvedUrl,\n });\n }, []);\n\n const disconnect = useCallback(async () => {\n await whipClientRef.current?.disconnect();\n }, []);\n\n const flipCamera = useCallback(async () => {\n await whipClientRef.current?.flipCamera();\n }, []);\n\n const switchCamera = useCallback(async (deviceId: string) => {\n return await whipClientRef.current?.switchCamera(deviceId);\n }, []);\n\n const currentCameraDeviceId = useCallback(async () => {\n return await whipClientRef.current?.currentCameraDeviceId();\n }, []);\n\n useEffect(() => {\n try {\n const initializeCamera = async () => {\n await whipClientRef.current?.initializeCamera({\n audioEnabled: audioEnabled ?? true,\n videoEnabled: videoEnabled ?? true,\n videoDeviceId: camera?.id ?? cameras[0].id,\n videoParameters: videoParameters,\n preferredVideoCodecs: preferredVideoCodecs,\n preferredAudioCodecs: preferredAudioCodecs,\n });\n };\n initializeCamera();\n } catch {\n console.error('Failed to initialize camera');\n }\n const ref = whipClientRef.current;\n return () => {\n ref?.disconnect();\n };\n }, [\n camera,\n videoParameters,\n videoEnabled,\n audioEnabled,\n preferredVideoCodecs,\n preferredAudioCodecs,\n whipClientRef,\n ]);\n\n return {\n connect,\n disconnect,\n flipCamera,\n switchCamera,\n currentCameraDeviceId,\n isConnected,\n whipClientRef,\n };\n};\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { WhepClientViewRef } from 'react-native-whip-whep';
|
|
1
2
|
export type ConnectViewerConfig = {
|
|
2
3
|
token: string;
|
|
3
4
|
streamId?: never;
|
|
@@ -19,6 +20,12 @@ export interface useLivestreamViewerResult {
|
|
|
19
20
|
disconnect: () => Promise<void>;
|
|
20
21
|
/** Utility flag which indicates the current connection status */
|
|
21
22
|
isConnected: boolean;
|
|
23
|
+
/** Utility flag which indicates if whep client is connecting */
|
|
24
|
+
isLoading: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Reference to the WhepClient instance. Needs to be passed to the {@link LivestreamViewer} component.
|
|
27
|
+
*/
|
|
28
|
+
whepClientRef: React.RefObject<WhepClientViewRef | null>;
|
|
22
29
|
}
|
|
23
30
|
/**
|
|
24
31
|
* Hook for receiving a published livestream.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLivestreamViewer.d.ts","sourceRoot":"","sources":["../../../src/livestream/hooks/useLivestreamViewer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useLivestreamViewer.d.ts","sourceRoot":"","sources":["../../../src/livestream/hooks/useLivestreamViewer.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AAGhC,MAAM,MAAM,mBAAmB,GAC3B;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,KAAK,CAAA;CAAE,GACnC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAOxC;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,OAAO,EAAE,CAAC,MAAM,EAAE,mBAAmB,EAAE,GAAG,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,4EAA4E;IAC5E,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,iEAAiE;IACjE,WAAW,EAAE,OAAO,CAAC;IACrB,gEAAgE;IAChE,SAAS,EAAE,OAAO,CAAC;IACnB;;OAEG;IACH,aAAa,EAAE,KAAK,CAAC,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;CAC1D;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,QAAO,yBAqEtC,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useWhepConnectionState, } from 'react-native-whip-whep';
|
|
3
3
|
import { FISHJAM_WHEP_URL } from '../../consts';
|
|
4
4
|
const urlFromConfig = (config) => {
|
|
5
5
|
if (config.streamId)
|
|
@@ -12,34 +12,62 @@ const urlFromConfig = (config) => {
|
|
|
12
12
|
* @group Hooks
|
|
13
13
|
*/
|
|
14
14
|
export const useLivestreamViewer = () => {
|
|
15
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
16
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
15
17
|
const state = useWhepConnectionState();
|
|
16
18
|
const isConnected = state === 'connected';
|
|
17
|
-
const
|
|
19
|
+
const whepClientRef = useRef(null);
|
|
20
|
+
const isConnectingRef = useRef(false);
|
|
18
21
|
useEffect(() => {
|
|
19
22
|
const createClient = () => {
|
|
20
|
-
|
|
23
|
+
whepClientRef.current?.createWhepClient({
|
|
21
24
|
audioEnabled: true,
|
|
22
25
|
videoEnabled: true,
|
|
23
26
|
});
|
|
24
27
|
};
|
|
25
28
|
createClient();
|
|
29
|
+
const ref = whepClientRef.current;
|
|
26
30
|
return () => {
|
|
27
|
-
|
|
31
|
+
ref?.disconnect();
|
|
28
32
|
};
|
|
29
33
|
}, []);
|
|
30
34
|
const connect = useCallback(async (config, url) => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
if (isConnectingRef.current) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
isConnectingRef.current = true;
|
|
39
|
+
setIsLoading(true);
|
|
40
|
+
try {
|
|
41
|
+
if (!isInitialized) {
|
|
42
|
+
await whepClientRef.current?.createWhepClient({
|
|
43
|
+
audioEnabled: true,
|
|
44
|
+
videoEnabled: true,
|
|
45
|
+
});
|
|
46
|
+
setIsInitialized(true);
|
|
47
|
+
}
|
|
48
|
+
await whepClientRef.current?.connect({
|
|
49
|
+
serverUrl: url ?? urlFromConfig(config),
|
|
50
|
+
authToken: config.token,
|
|
51
|
+
});
|
|
52
|
+
setIsLoading(false);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error('Failed to connect to WHEP Client', error);
|
|
56
|
+
setIsLoading(false);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
isConnectingRef.current = false;
|
|
60
|
+
}
|
|
61
|
+
}, [isInitialized]);
|
|
36
62
|
const disconnect = useCallback(async () => {
|
|
37
|
-
await
|
|
63
|
+
await whepClientRef.current?.disconnect();
|
|
38
64
|
}, []);
|
|
39
65
|
return {
|
|
40
66
|
connect,
|
|
41
67
|
disconnect,
|
|
68
|
+
whepClientRef,
|
|
42
69
|
isConnected,
|
|
70
|
+
isLoading,
|
|
43
71
|
};
|
|
44
72
|
};
|
|
45
73
|
//# sourceMappingURL=useLivestreamViewer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLivestreamViewer.js","sourceRoot":"","sources":["../../../src/livestream/hooks/useLivestreamViewer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useLivestreamViewer.js","sourceRoot":"","sources":["../../../src/livestream/hooks/useLivestreamViewer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACjE,OAAO,EACL,sBAAsB,GAEvB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMhD,MAAM,aAAa,GAAG,CAAC,MAA2B,EAAE,EAAE;IACpD,IAAI,MAAM,CAAC,QAAQ;QAAE,OAAO,GAAG,gBAAgB,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;IACrE,OAAO,gBAAgB,CAAC;AAC1B,CAAC,CAAC;AAwBF;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAA8B,EAAE;IACjE,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClD,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC1D,MAAM,KAAK,GAAG,sBAAsB,EAAE,CAAC;IACvC,MAAM,WAAW,GAAG,KAAK,KAAK,WAAW,CAAC;IAE1C,MAAM,aAAa,GAAG,MAAM,CAA2B,IAAI,CAAC,CAAC;IAC7D,MAAM,eAAe,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAEtC,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,YAAY,GAAG,GAAG,EAAE;YACxB,aAAa,CAAC,OAAO,EAAE,gBAAgB,CAAC;gBACtC,YAAY,EAAE,IAAI;gBAClB,YAAY,EAAE,IAAI;aACnB,CAAC,CAAC;QACL,CAAC,CAAC;QACF,YAAY,EAAE,CAAC;QAEf,MAAM,GAAG,GAAG,aAAa,CAAC,OAAO,CAAC;QAClC,OAAO,GAAG,EAAE;YACV,GAAG,EAAE,UAAU,EAAE,CAAC;QACpB,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,OAAO,GAAG,WAAW,CACzB,KAAK,EAAE,MAA2B,EAAE,GAAY,EAAE,EAAE;QAClD,IAAI,eAAe,CAAC,OAAO,EAAE,CAAC;YAC5B,OAAO;QACT,CAAC;QAED,eAAe,CAAC,OAAO,GAAG,IAAI,CAAC;QAC/B,YAAY,CAAC,IAAI,CAAC,CAAC;QAEnB,IAAI,CAAC;YACH,IAAI,CAAC,aAAa,EAAE,CAAC;gBACnB,MAAM,aAAa,CAAC,OAAO,EAAE,gBAAgB,CAAC;oBAC5C,YAAY,EAAE,IAAI;oBAClB,YAAY,EAAE,IAAI;iBACnB,CAAC,CAAC;gBACH,gBAAgB,CAAC,IAAI,CAAC,CAAC;YACzB,CAAC;YAED,MAAM,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC;gBACnC,SAAS,EAAE,GAAG,IAAI,aAAa,CAAC,MAAM,CAAC;gBACvC,SAAS,EAAE,MAAM,CAAC,KAAK;aACxB,CAAC,CAAC;YAEH,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,kCAAkC,EAAE,KAAK,CAAC,CAAC;YACzD,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;gBAAS,CAAC;YACT,eAAe,CAAC,OAAO,GAAG,KAAK,CAAC;QAClC,CAAC;IACH,CAAC,EACD,CAAC,aAAa,CAAC,CAChB,CAAC;IAEF,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACxC,MAAM,aAAa,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;IAC5C,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO;QACL,OAAO;QACP,UAAU;QACV,aAAa;QACb,WAAW;QACX,SAAS;KACV,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n useWhepConnectionState,\n WhepClientViewRef,\n} from 'react-native-whip-whep';\nimport { FISHJAM_WHEP_URL } from '../../consts';\n\nexport type ConnectViewerConfig =\n | { token: string; streamId?: never }\n | { streamId: string; token?: never };\n\nconst urlFromConfig = (config: ConnectViewerConfig) => {\n if (config.streamId) return `${FISHJAM_WHEP_URL}/${config.streamId}`;\n return FISHJAM_WHEP_URL;\n};\n\n/**\n * @category Livestream\n */\nexport interface useLivestreamViewerResult {\n /**\n * Callback to start receiving a livestream.\n * If the livestream is private, provide `token`.\n * If the livestream is public, provide `streamId`.\n */\n connect: (config: ConnectViewerConfig, url?: string) => Promise<void>;\n /** Disconnect from a stream previously connected to with {@link connect} */\n disconnect: () => Promise<void>;\n /** Utility flag which indicates the current connection status */\n isConnected: boolean;\n /** Utility flag which indicates if whep client is connecting */\n isLoading: boolean;\n /**\n * Reference to the WhepClient instance. Needs to be passed to the {@link LivestreamViewer} component.\n */\n whepClientRef: React.RefObject<WhepClientViewRef | null>;\n}\n\n/**\n * Hook for receiving a published livestream.\n * @category Livestream\n * @group Hooks\n */\nexport const useLivestreamViewer = (): useLivestreamViewerResult => {\n const [isLoading, setIsLoading] = useState(false);\n const [isInitialized, setIsInitialized] = useState(false);\n const state = useWhepConnectionState();\n const isConnected = state === 'connected';\n\n const whepClientRef = useRef<WhepClientViewRef | null>(null);\n const isConnectingRef = useRef(false);\n\n useEffect(() => {\n const createClient = () => {\n whepClientRef.current?.createWhepClient({\n audioEnabled: true,\n videoEnabled: true,\n });\n };\n createClient();\n\n const ref = whepClientRef.current;\n return () => {\n ref?.disconnect();\n };\n }, []);\n\n const connect = useCallback(\n async (config: ConnectViewerConfig, url?: string) => {\n if (isConnectingRef.current) {\n return;\n }\n\n isConnectingRef.current = true;\n setIsLoading(true);\n\n try {\n if (!isInitialized) {\n await whepClientRef.current?.createWhepClient({\n audioEnabled: true,\n videoEnabled: true,\n });\n setIsInitialized(true);\n }\n\n await whepClientRef.current?.connect({\n serverUrl: url ?? urlFromConfig(config),\n authToken: config.token,\n });\n\n setIsLoading(false);\n } catch (error) {\n console.error('Failed to connect to WHEP Client', error);\n setIsLoading(false);\n } finally {\n isConnectingRef.current = false;\n }\n },\n [isInitialized],\n );\n\n const disconnect = useCallback(async () => {\n await whepClientRef.current?.disconnect();\n }, []);\n\n return {\n connect,\n disconnect,\n whepClientRef,\n isConnected,\n isLoading,\n };\n};\n"]}
|
|
@@ -2,7 +2,7 @@ export { LivestreamStreamer } from './components/LivestreamStreamer';
|
|
|
2
2
|
export { LivestreamViewer } from './components/LivestreamViewer';
|
|
3
3
|
export { useLivestreamViewer } from './hooks/useLivestreamViewer';
|
|
4
4
|
export { useLivestreamStreamer } from './hooks/useLivestreamStreamer';
|
|
5
|
-
export { cameras, VideoParameters
|
|
5
|
+
export { cameras, VideoParameters } from 'react-native-whip-whep';
|
|
6
6
|
export type { LivestreamStreamerProps } from './components/LivestreamStreamer';
|
|
7
7
|
export type { ConnectViewerConfig } from './hooks/useLivestreamViewer';
|
|
8
8
|
export type { useLivestreamViewerResult } from './hooks/useLivestreamViewer';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/livestream/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/livestream/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAClE,YAAY,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAC/E,YAAY,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,YAAY,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AAC7E,YAAY,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AACjF,YAAY,EACV,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,+BAA+B,CAAC"}
|
|
@@ -2,5 +2,5 @@ export { LivestreamStreamer } from './components/LivestreamStreamer';
|
|
|
2
2
|
export { LivestreamViewer } from './components/LivestreamViewer';
|
|
3
3
|
export { useLivestreamViewer } from './hooks/useLivestreamViewer';
|
|
4
4
|
export { useLivestreamStreamer } from './hooks/useLivestreamStreamer';
|
|
5
|
-
export { cameras, VideoParameters
|
|
5
|
+
export { cameras, VideoParameters } from 'react-native-whip-whep';
|
|
6
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/livestream/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/livestream/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC","sourcesContent":["export { LivestreamStreamer } from './components/LivestreamStreamer';\nexport { LivestreamViewer } from './components/LivestreamViewer';\nexport { useLivestreamViewer } from './hooks/useLivestreamViewer';\nexport { useLivestreamStreamer } from './hooks/useLivestreamStreamer';\nexport { cameras, VideoParameters } from 'react-native-whip-whep';\nexport type { LivestreamStreamerProps } from './components/LivestreamStreamer';\nexport type { ConnectViewerConfig } from './hooks/useLivestreamViewer';\nexport type { useLivestreamViewerResult } from './hooks/useLivestreamViewer';\nexport type { useLivestreamStreamerResult } from './hooks/useLivestreamStreamer';\nexport type {\n LivestreamViewerProps,\n LivestreamViewerRef,\n} from './components/LivestreamViewer';\n"]}
|
package/expo-module.config.json
CHANGED
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
"modules": [
|
|
5
5
|
"RNFishjamClientModule",
|
|
6
6
|
"VideoPreviewViewModule",
|
|
7
|
-
"VideoRendererViewModule"
|
|
7
|
+
"VideoRendererViewModule",
|
|
8
|
+
"PipContainerViewModule"
|
|
8
9
|
]
|
|
9
10
|
},
|
|
10
11
|
"android": {
|
|
11
12
|
"modules": [
|
|
12
13
|
"io.fishjam.reactnative.RNFishjamClientModule",
|
|
13
14
|
"io.fishjam.reactnative.VideoPreviewViewModule",
|
|
14
|
-
"io.fishjam.reactnative.VideoRendererViewModule"
|
|
15
|
+
"io.fishjam.reactnative.VideoRendererViewModule",
|
|
16
|
+
"io.fishjam.reactnative.PipContainerViewModule"
|
|
15
17
|
]
|
|
16
18
|
}
|
|
17
19
|
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import AVKit
|
|
2
|
+
import FishjamCloudClient
|
|
3
|
+
import WebRTC
|
|
4
|
+
|
|
5
|
+
@MainActor
|
|
6
|
+
class PictureInPictureManager {
|
|
7
|
+
private let sourceView: UIView
|
|
8
|
+
private let eventEmitter: (EmitableEvent) -> Void
|
|
9
|
+
private weak var fishjamClient: FishjamClient?
|
|
10
|
+
private var lastActiveVadInfo: RemoteTrackInfo? = nil
|
|
11
|
+
|
|
12
|
+
private var controller: PictureInPictureController?
|
|
13
|
+
|
|
14
|
+
private var startAutomatically: Bool {
|
|
15
|
+
didSet {
|
|
16
|
+
controller?.startAutomatically = startAutomatically
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private var stopAutomatically: Bool {
|
|
21
|
+
didSet {
|
|
22
|
+
controller?.stopAutomatically = stopAutomatically
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var primaryPlaceholderText: String = "No camera" {
|
|
27
|
+
didSet {
|
|
28
|
+
controller?.primaryPlaceholderText = primaryPlaceholderText
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var secondaryPlaceholderText: String = "No active speaker" {
|
|
33
|
+
didSet {
|
|
34
|
+
controller?.secondaryPlaceholderText = secondaryPlaceholderText
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
init(
|
|
39
|
+
sourceView: UIView,
|
|
40
|
+
eventEmitter: @escaping (EmitableEvent) -> Void,
|
|
41
|
+
fishjamClient: FishjamClient?
|
|
42
|
+
) {
|
|
43
|
+
self.sourceView = sourceView
|
|
44
|
+
self.eventEmitter = eventEmitter
|
|
45
|
+
self.fishjamClient = fishjamClient
|
|
46
|
+
self.startAutomatically = true
|
|
47
|
+
self.stopAutomatically = true
|
|
48
|
+
|
|
49
|
+
RNFishjamClient.tracksUpdateListenersManager.add(self)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
deinit {
|
|
53
|
+
RNFishjamClient.tracksUpdateListenersManager.remove(self)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@discardableResult
|
|
57
|
+
private func ensureController() -> PictureInPictureController? {
|
|
58
|
+
if controller == nil {
|
|
59
|
+
let newController = PictureInPictureController(
|
|
60
|
+
sourceView: sourceView,
|
|
61
|
+
primaryPlaceholder: primaryPlaceholderText,
|
|
62
|
+
secondaryPlaceholder: secondaryPlaceholderText
|
|
63
|
+
)
|
|
64
|
+
newController.startAutomatically = startAutomatically
|
|
65
|
+
newController.stopAutomatically = stopAutomatically
|
|
66
|
+
controller = newController
|
|
67
|
+
|
|
68
|
+
updateTracks()
|
|
69
|
+
}
|
|
70
|
+
return controller
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private func findLocalCameraTrack() -> RTCVideoTrack? {
|
|
74
|
+
guard let fishjamClient = fishjamClient else { return nil }
|
|
75
|
+
|
|
76
|
+
let localEndpoint = fishjamClient.getLocalEndpoint()
|
|
77
|
+
|
|
78
|
+
for (_, track) in localEndpoint.tracks {
|
|
79
|
+
if let videoTrack = track as? LocalVideoTrack,
|
|
80
|
+
track.metadata.toDict()["type"] as? String == "camera" {
|
|
81
|
+
return videoTrack.mediaTrack as? RTCVideoTrack
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return nil
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private func findRemoteVadActiveTrack() -> RemoteTrackInfo? {
|
|
89
|
+
guard let fishjamClient else { return nil }
|
|
90
|
+
|
|
91
|
+
let remoteEndpoints = fishjamClient.getRemoteEndpoints()
|
|
92
|
+
|
|
93
|
+
// First pass: look for active VAD
|
|
94
|
+
for endpoint in remoteEndpoints {
|
|
95
|
+
// Check if this endpoint has an active VAD audio track
|
|
96
|
+
let hasActiveVad = endpoint.tracks.values.contains { track in
|
|
97
|
+
if let audioTrack = track as? RemoteAudioTrack {
|
|
98
|
+
return audioTrack.vadStatus == .speech
|
|
99
|
+
}
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if hasActiveVad {
|
|
104
|
+
let videoTrack = endpoint.tracks.values.first { track in
|
|
105
|
+
track is RemoteVideoTrack
|
|
106
|
+
} as? RemoteVideoTrack
|
|
107
|
+
lastActiveVadInfo = trackToTrackInfo(videoTrack: videoTrack, endpoint: endpoint)
|
|
108
|
+
return lastActiveVadInfo
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if lastActiveVadInfo != nil {
|
|
112
|
+
return lastActiveVadInfo
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Fallback: return first available remote peer with video track
|
|
116
|
+
for endpoint in remoteEndpoints {
|
|
117
|
+
if let videoTrack = endpoint.tracks.values.first(where: { $0 is RemoteVideoTrack }) as? RemoteVideoTrack {
|
|
118
|
+
return trackToTrackInfo(videoTrack: videoTrack, endpoint: endpoint)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return nil
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private func trackToTrackInfo(videoTrack: RemoteVideoTrack?, endpoint: Endpoint) -> RemoteTrackInfo {
|
|
126
|
+
let metadataDict = endpoint.metadata.toDict()
|
|
127
|
+
let peerMetadata = metadataDict["peer"] as? [String: String]
|
|
128
|
+
let displayName = peerMetadata?["displayName"] ?? peerMetadata?["name"]
|
|
129
|
+
|
|
130
|
+
var isVideoTrackActive: Bool? = nil
|
|
131
|
+
if videoTrack?.metadata["active"] as? Bool == false { isVideoTrackActive = false }
|
|
132
|
+
if videoTrack?.metadata["paused"] as? Bool == true { isVideoTrackActive = false }
|
|
133
|
+
|
|
134
|
+
return RemoteTrackInfo(
|
|
135
|
+
videoTrack: videoTrack?.mediaTrack as? RTCVideoTrack,
|
|
136
|
+
displayName: displayName,
|
|
137
|
+
hasVideoTrack: videoTrack != nil,
|
|
138
|
+
isVideoActive: isVideoTrackActive ?? true
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private func updateTracks() {
|
|
143
|
+
guard let pipController = controller else { return }
|
|
144
|
+
|
|
145
|
+
let localCameraTrack = findLocalCameraTrack()
|
|
146
|
+
let remoteTrackInfo = findRemoteVadActiveTrack()
|
|
147
|
+
|
|
148
|
+
pipController.primaryVideoTrack = localCameraTrack
|
|
149
|
+
pipController.updateSecondaryTrack(trackInfo: remoteTrackInfo)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
private func emitWarning(_ message: String) {
|
|
155
|
+
eventEmitter(.warning(message: message))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
func start() {
|
|
160
|
+
_ = ensureController()
|
|
161
|
+
controller?.startPictureInPicture()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
func stop() {
|
|
165
|
+
controller?.stopPictureInPicture()
|
|
166
|
+
lastActiveVadInfo = nil
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func setAllowsCameraWhileInPictureInPicture(_ enabled: Bool) {
|
|
170
|
+
guard let fishjamClient = fishjamClient else {
|
|
171
|
+
emitWarning("PictureInPicture: Fishjam client not available")
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let localEndpoint = fishjamClient.getLocalEndpoint()
|
|
176
|
+
guard let cameraTrack = localEndpoint.tracks.compactMap({ $0.value as? LocalCameraTrack }).first else {
|
|
177
|
+
emitWarning("PictureInPicture: Unable to configure background camera - camera not initialized")
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
guard cameraTrack.isMultitaskingCameraAccessSupported() else {
|
|
182
|
+
emitWarning("PictureInPicture: Background camera is not supported on this device.")
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
cameraTrack.setMultitaskingCameraAccessEnabled(enabled)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func setStartAutomatically(_ enabled: Bool) {
|
|
190
|
+
if enabled {
|
|
191
|
+
ensureController()
|
|
192
|
+
}
|
|
193
|
+
startAutomatically = enabled
|
|
194
|
+
controller?.startAutomatically = enabled
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func setStopAutomatically(_ enabled: Bool) {
|
|
198
|
+
stopAutomatically = enabled
|
|
199
|
+
controller?.stopAutomatically = enabled
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
func cleanup() {
|
|
203
|
+
controller?.stopPictureInPicture()
|
|
204
|
+
controller = nil
|
|
205
|
+
lastActiveVadInfo = nil
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
extension PictureInPictureManager: TrackUpdateListener {
|
|
210
|
+
nonisolated func onTracksUpdate() {
|
|
211
|
+
Task { @MainActor in
|
|
212
|
+
updateTracks()
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
@MainActor
|
|
4
|
+
class PipContainerView: ExpoView {
|
|
5
|
+
private var pipManager: PictureInPictureManager!
|
|
6
|
+
|
|
7
|
+
required init(appContext: AppContext? = nil) {
|
|
8
|
+
super.init(appContext: appContext)
|
|
9
|
+
pipManager = PictureInPictureManager(
|
|
10
|
+
sourceView: self,
|
|
11
|
+
eventEmitter: { event in
|
|
12
|
+
RNFishjamClient.sendEvent?(event)
|
|
13
|
+
},
|
|
14
|
+
fishjamClient: RNFishjamClient.fishjamClient
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
var startAutomatically: Bool = true {
|
|
19
|
+
didSet {
|
|
20
|
+
pipManager.setStartAutomatically(startAutomatically)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var stopAutomatically: Bool = true {
|
|
25
|
+
didSet {
|
|
26
|
+
pipManager.setStopAutomatically(stopAutomatically)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var allowsCameraInBackground: Bool = false {
|
|
31
|
+
didSet {
|
|
32
|
+
pipManager.setAllowsCameraWhileInPictureInPicture(allowsCameraInBackground)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var primaryPlaceholderText: String = "No camera" {
|
|
37
|
+
didSet {
|
|
38
|
+
pipManager.primaryPlaceholderText = primaryPlaceholderText
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var secondaryPlaceholderText: String = "No active speaker" {
|
|
43
|
+
didSet {
|
|
44
|
+
pipManager.secondaryPlaceholderText = secondaryPlaceholderText
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func startPictureInPicture() {
|
|
49
|
+
pipManager.start()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func stopPictureInPicture() {
|
|
53
|
+
pipManager.stop()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|