@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.
Files changed (48) hide show
  1. package/android/src/main/java/io/fishjam/reactnative/PipContainerView.kt +282 -0
  2. package/android/src/main/java/io/fishjam/reactnative/PipContainerViewModule.kt +41 -0
  3. package/android/src/main/java/io/fishjam/reactnative/PipTrackSelector.kt +98 -0
  4. package/android/src/main/java/io/fishjam/reactnative/PipViewFactory.kt +186 -0
  5. package/android/src/main/java/io/fishjam/reactnative/RNFishjamClient.kt +9 -1
  6. package/android/src/main/java/io/fishjam/reactnative/VideoPreviewView.kt +2 -0
  7. package/android/src/main/java/io/fishjam/reactnative/VideoRendererView.kt +2 -0
  8. package/android/src/main/java/io/fishjam/reactnative/VideoView.kt +152 -11
  9. package/android/src/main/java/io/fishjam/reactnative/helpers/PictureInPictureHelperFragment.kt +20 -0
  10. package/android/src/main/java/io/fishjam/reactnative/managers/ListenerManager.kt +3 -1
  11. package/build/RNFishjamClientModule.d.ts +2 -0
  12. package/build/RNFishjamClientModule.d.ts.map +1 -1
  13. package/build/RNFishjamClientModule.js.map +1 -1
  14. package/build/components/PipContainerView.d.ts +48 -0
  15. package/build/components/PipContainerView.d.ts.map +1 -0
  16. package/build/components/PipContainerView.js +34 -0
  17. package/build/components/PipContainerView.js.map +1 -0
  18. package/build/index.d.ts +2 -0
  19. package/build/index.d.ts.map +1 -1
  20. package/build/index.js +1 -0
  21. package/build/index.js.map +1 -1
  22. package/build/livestream/components/LivestreamStreamer.d.ts +3 -30
  23. package/build/livestream/components/LivestreamStreamer.d.ts.map +1 -1
  24. package/build/livestream/components/LivestreamStreamer.js +3 -27
  25. package/build/livestream/components/LivestreamStreamer.js.map +1 -1
  26. package/build/livestream/components/LivestreamViewer.d.ts +2 -7
  27. package/build/livestream/components/LivestreamViewer.d.ts.map +1 -1
  28. package/build/livestream/components/LivestreamViewer.js +1 -1
  29. package/build/livestream/components/LivestreamViewer.js.map +1 -1
  30. package/build/livestream/hooks/useLivestreamStreamer.d.ts +40 -3
  31. package/build/livestream/hooks/useLivestreamStreamer.d.ts.map +1 -1
  32. package/build/livestream/hooks/useLivestreamStreamer.js +45 -3
  33. package/build/livestream/hooks/useLivestreamStreamer.js.map +1 -1
  34. package/build/livestream/hooks/useLivestreamViewer.d.ts +7 -0
  35. package/build/livestream/hooks/useLivestreamViewer.d.ts.map +1 -1
  36. package/build/livestream/hooks/useLivestreamViewer.js +39 -11
  37. package/build/livestream/hooks/useLivestreamViewer.js.map +1 -1
  38. package/build/livestream/index.d.ts +1 -1
  39. package/build/livestream/index.d.ts.map +1 -1
  40. package/build/livestream/index.js +1 -1
  41. package/build/livestream/index.js.map +1 -1
  42. package/expo-module.config.json +4 -2
  43. package/ios/PictureInPictureManager.swift +215 -0
  44. package/ios/PipContainerView.swift +56 -0
  45. package/ios/PipContainerViewModule.swift +48 -0
  46. package/ios/RNFishjamClient.swift +1 -0
  47. package/ios/RNFishjamClientModule.swift +4 -3
  48. 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;AA4C3E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,EAC/B,KAAK,EACL,WAAW,EACX,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 * Used to override the orientation of the video (from metadata).\n * Defaults to \"portrait\".\n */\n orientation?: 'landscape' | 'portrait';\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 orientation,\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
+ {"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 { WhipClient } from 'react-native-whip-whep';
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<WhipClient | null>;
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,EAA0B,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAG5E;;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,iEAAiE;IACjE,WAAW,EAAE,OAAO,CAAC;IACrB;;OAEG;IACH,aAAa,EAAE,KAAK,CAAC,SAAS,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;CACnD;AAED;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,QAAO,2BAwBxC,CAAC"}
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;AAC5C,OAAO,EAAE,sBAAsB,EAAc,MAAM,wBAAwB,CAAC;AAC5E,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAuBhD;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAgC,EAAE;IACrE,MAAM,KAAK,GAAG,sBAAsB,EAAE,CAAC;IACvC,MAAM,WAAW,GAAG,KAAK,KAAK,WAAW,CAAC;IAE1C,MAAM,aAAa,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IAEtD,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,OAAO;QACL,OAAO;QACP,UAAU;QACV,WAAW;QACX,aAAa;KACd,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import { useCallback, useRef } from 'react';\nimport { useWhipConnectionState, WhipClient } 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 /** 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<WhipClient | null>;\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 = (): useLivestreamStreamerResult => {\n const state = useWhipConnectionState();\n const isConnected = state === 'connected';\n\n const whipClientRef = useRef<WhipClient | 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 return {\n connect,\n disconnect,\n isConnected,\n whipClientRef,\n };\n};\n"]}
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":"AAIA,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;CACtB;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,QAAO,yBAuCtC,CAAC"}
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 { WhepClient, useWhepConnectionState } from 'react-native-whip-whep';
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 whepClient = useRef(null);
19
+ const whepClientRef = useRef(null);
20
+ const isConnectingRef = useRef(false);
18
21
  useEffect(() => {
19
22
  const createClient = () => {
20
- whepClient.current = new WhepClient({
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
- whepClient.current?.disconnect();
31
+ ref?.disconnect();
28
32
  };
29
33
  }, []);
30
34
  const connect = useCallback(async (config, url) => {
31
- await whepClient.current?.connect({
32
- serverUrl: url ?? urlFromConfig(config),
33
- authToken: config.token,
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 whepClient.current?.disconnect();
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;AACvD,OAAO,EAAE,UAAU,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAC5E,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;AAkBF;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAA8B,EAAE;IACjE,MAAM,KAAK,GAAG,sBAAsB,EAAE,CAAC;IACvC,MAAM,WAAW,GAAG,KAAK,KAAK,WAAW,CAAC;IAE1C,MAAM,UAAU,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IAEnD,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,YAAY,GAAG,GAAG,EAAE;YACxB,UAAU,CAAC,OAAO,GAAG,IAAI,UAAU,CAAC;gBAClC,YAAY,EAAE,IAAI;gBAClB,YAAY,EAAE,IAAI;aACnB,CAAC,CAAC;QACL,CAAC,CAAC;QACF,YAAY,EAAE,CAAC;QAEf,OAAO,GAAG,EAAE;YACV,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;QACnC,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,OAAO,GAAG,WAAW,CACzB,KAAK,EAAE,MAA2B,EAAE,GAAY,EAAE,EAAE;QAClD,MAAM,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC;YAChC,SAAS,EAAE,GAAG,IAAI,aAAa,CAAC,MAAM,CAAC;YACvC,SAAS,EAAE,MAAM,CAAC,KAAK;SACxB,CAAC,CAAC;IACL,CAAC,EACD,EAAE,CACH,CAAC;IAEF,MAAM,UAAU,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACxC,MAAM,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;IACzC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO;QACL,OAAO;QACP,UAAU;QACV,WAAW;KACZ,CAAC;AACJ,CAAC,CAAC","sourcesContent":["import { useCallback, useEffect, useRef } from 'react';\nimport { WhepClient, useWhepConnectionState } 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}\n\n/**\n * Hook for receiving a published livestream.\n * @category Livestream\n * @group Hooks\n */\nexport const useLivestreamViewer = (): useLivestreamViewerResult => {\n const state = useWhepConnectionState();\n const isConnected = state === 'connected';\n\n const whepClient = useRef<WhepClient | null>(null);\n\n useEffect(() => {\n const createClient = () => {\n whepClient.current = new WhepClient({\n audioEnabled: true,\n videoEnabled: true,\n });\n };\n createClient();\n\n return () => {\n whepClient.current?.disconnect();\n };\n }, []);\n\n const connect = useCallback(\n async (config: ConnectViewerConfig, url?: string) => {\n await whepClient.current?.connect({\n serverUrl: url ?? urlFromConfig(config),\n authToken: config.token,\n });\n },\n [],\n );\n\n const disconnect = useCallback(async () => {\n await whepClient.current?.disconnect();\n }, []);\n\n return {\n connect,\n disconnect,\n isConnected,\n };\n};\n"]}
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, WhipClient, WhepClient, } from 'react-native-whip-whep';
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,EACL,OAAO,EACP,eAAe,EACf,UAAU,EACV,UAAU,GACX,MAAM,wBAAwB,CAAC;AAChC,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"}
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, WhipClient, WhepClient, } from 'react-native-whip-whep';
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,EACL,OAAO,EACP,eAAe,EACf,UAAU,EACV,UAAU,GACX,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 {\n cameras,\n VideoParameters,\n WhipClient,\n WhepClient,\n} 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"]}
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"]}
@@ -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
+