@fishjam-cloud/react-native-client 0.23.0-RC.0 → 0.24.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 (73) 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/FishjamRoom/index.d.ts.map +1 -1
  15. package/build/components/FishjamRoom/index.js +0 -1
  16. package/build/components/FishjamRoom/index.js.map +1 -1
  17. package/build/components/PipContainerView.d.ts +48 -0
  18. package/build/components/PipContainerView.d.ts.map +1 -0
  19. package/build/components/PipContainerView.js +34 -0
  20. package/build/components/PipContainerView.js.map +1 -0
  21. package/build/hooks/useAppScreenShare.d.ts +0 -2
  22. package/build/hooks/useAppScreenShare.d.ts.map +1 -1
  23. package/build/hooks/useAppScreenShare.js +0 -8
  24. package/build/hooks/useAppScreenShare.js.map +1 -1
  25. package/build/hooks/useCamera.d.ts +2 -2
  26. package/build/hooks/useCamera.d.ts.map +1 -1
  27. package/build/hooks/useCamera.js +1 -3
  28. package/build/hooks/useCamera.js.map +1 -1
  29. package/build/hooks/useScreenShare.d.ts +2 -3
  30. package/build/hooks/useScreenShare.d.ts.map +1 -1
  31. package/build/hooks/useScreenShare.js +0 -7
  32. package/build/hooks/useScreenShare.js.map +1 -1
  33. package/build/index.d.ts +2 -0
  34. package/build/index.d.ts.map +1 -1
  35. package/build/index.js +1 -0
  36. package/build/index.js.map +1 -1
  37. package/build/livestream/components/LivestreamStreamer.d.ts +3 -30
  38. package/build/livestream/components/LivestreamStreamer.d.ts.map +1 -1
  39. package/build/livestream/components/LivestreamStreamer.js +3 -27
  40. package/build/livestream/components/LivestreamStreamer.js.map +1 -1
  41. package/build/livestream/components/LivestreamViewer.d.ts +2 -7
  42. package/build/livestream/components/LivestreamViewer.d.ts.map +1 -1
  43. package/build/livestream/components/LivestreamViewer.js +1 -1
  44. package/build/livestream/components/LivestreamViewer.js.map +1 -1
  45. package/build/livestream/hooks/useLivestreamScreenSharingStreamer.d.ts +49 -0
  46. package/build/livestream/hooks/useLivestreamScreenSharingStreamer.d.ts.map +1 -0
  47. package/build/livestream/hooks/useLivestreamScreenSharingStreamer.js +58 -0
  48. package/build/livestream/hooks/useLivestreamScreenSharingStreamer.js.map +1 -0
  49. package/build/livestream/hooks/useLivestreamStreamer.d.ts +40 -3
  50. package/build/livestream/hooks/useLivestreamStreamer.d.ts.map +1 -1
  51. package/build/livestream/hooks/useLivestreamStreamer.js +44 -3
  52. package/build/livestream/hooks/useLivestreamStreamer.js.map +1 -1
  53. package/build/livestream/hooks/useLivestreamViewer.d.ts +7 -0
  54. package/build/livestream/hooks/useLivestreamViewer.d.ts.map +1 -1
  55. package/build/livestream/hooks/useLivestreamViewer.js +39 -11
  56. package/build/livestream/hooks/useLivestreamViewer.js.map +1 -1
  57. package/build/livestream/index.d.ts +2 -1
  58. package/build/livestream/index.d.ts.map +1 -1
  59. package/build/livestream/index.js +2 -1
  60. package/build/livestream/index.js.map +1 -1
  61. package/expo-module.config.json +4 -2
  62. package/ios/PictureInPictureManager.swift +215 -0
  63. package/ios/PipContainerView.swift +56 -0
  64. package/ios/PipContainerViewModule.swift +48 -0
  65. package/ios/RNFishjamClient.swift +1 -0
  66. package/ios/RNFishjamClientModule.swift +4 -3
  67. package/package.json +2 -2
  68. package/plugin/broadcastExtensionFiles/FishjamBroadcastSampleHandler.swift +1 -1
  69. package/plugin/broadcastExtensionFiles/Info.plist +1 -1
  70. package/plugin/build/types.d.ts +1 -0
  71. package/plugin/build/withFishjam.js +2 -2
  72. package/plugin/build/withFishjamAndroid.js +44 -5
  73. package/plugin/build/withFishjamIos.js +7 -2
@@ -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,8 @@ 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 { useLivestreamScreenSharingStreamer } from './hooks/useLivestreamScreenSharingStreamer';
6
+ export { cameras, VideoParameters } from 'react-native-whip-whep';
6
7
  export type { LivestreamStreamerProps } from './components/LivestreamStreamer';
7
8
  export type { ConnectViewerConfig } from './hooks/useLivestreamViewer';
8
9
  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,kCAAkC,EAAE,MAAM,4CAA4C,CAAC;AAChG,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,6 @@ 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 { useLivestreamScreenSharingStreamer } from './hooks/useLivestreamScreenSharingStreamer';
6
+ export { cameras, VideoParameters } from 'react-native-whip-whep';
6
7
  //# 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,kCAAkC,EAAE,MAAM,4CAA4C,CAAC;AAChG,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 { useLivestreamScreenSharingStreamer } from './hooks/useLivestreamScreenSharingStreamer';\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
+
@@ -0,0 +1,48 @@
1
+ import ExpoModulesCore
2
+
3
+ public class PipContainerViewModule: Module {
4
+ public func definition() -> ModuleDefinition {
5
+ Name("PipContainerViewModule")
6
+
7
+ View(PipContainerView.self) {
8
+ Prop("startAutomatically") { (view: PipContainerView, value: Bool) in
9
+ Task { @MainActor in
10
+ view.startAutomatically = value
11
+ }
12
+ }
13
+
14
+ Prop("stopAutomatically") { (view: PipContainerView, value: Bool) in
15
+ Task { @MainActor in
16
+ view.stopAutomatically = value
17
+ }
18
+ }
19
+
20
+ Prop("allowsCameraInBackground") { (view: PipContainerView, value: Bool) in
21
+ Task { @MainActor in
22
+ view.allowsCameraInBackground = value
23
+ }
24
+ }
25
+
26
+ Prop("primaryPlaceholderText") { (view: PipContainerView, value: String) in
27
+ Task { @MainActor in
28
+ view.primaryPlaceholderText = value
29
+ }
30
+ }
31
+
32
+ Prop("secondaryPlaceholderText") { (view: PipContainerView, value: String) in
33
+ Task { @MainActor in
34
+ view.secondaryPlaceholderText = value
35
+ }
36
+ }
37
+
38
+ AsyncFunction("startPictureInPicture") { (view: PipContainerView) in
39
+ await view.startPictureInPicture()
40
+ }
41
+
42
+ AsyncFunction("stopPictureInPicture") { (view: PipContainerView) in
43
+ await view.stopPictureInPicture()
44
+ }
45
+ }
46
+ }
47
+ }
48
+
@@ -803,6 +803,7 @@ class RNFishjamClient: FishjamClientListener {
803
803
 
804
804
  func emitEndpoints() {
805
805
  emit(event: .peersUpdate())
806
+ RNFishjamClient.tracksUpdateListenersManager.notifyListeners()
806
807
  }
807
808
 
808
809
  func onJoined(peerID: String, peersInRoom: [String: Endpoint]) {
@@ -121,6 +121,10 @@ public class RNFishjamClientModule: Module {
121
121
  return rnFishjamClient.isCameraInitialized
122
122
  }
123
123
 
124
+ Property("hasActiveCallKitSession") {
125
+ return rnFishjamClient.hasActiveCallKitSession
126
+ }
127
+
124
128
  Function("getPeers") {
125
129
  return rnFishjamClient.getPeers()
126
130
  }
@@ -281,8 +285,5 @@ public class RNFishjamClientModule: Module {
281
285
  rnFishjamClient.endCallKitSession()
282
286
  }
283
287
 
284
- Property("hasActiveCallKitSession") {
285
- return rnFishjamClient.hasActiveCallKitSession
286
- }
287
288
  }
288
289
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fishjam-cloud/react-native-client",
3
- "version": "0.23.0-RC.0",
3
+ "version": "0.24.0",
4
4
  "description": "A React Native client for Fishjam",
5
5
  "author": "Fishjam Team",
6
6
  "license": "Apache-2.0",
@@ -42,7 +42,7 @@
42
42
  "dependencies": {
43
43
  "promise-fs": "^2.1.1",
44
44
  "protobufjs": "^7.4.0",
45
- "react-native-whip-whep": "0.3.1",
45
+ "react-native-whip-whep": "0.7.1",
46
46
  "zod": "^3.25.71"
47
47
  },
48
48
  "devDependencies": {
@@ -1,4 +1,4 @@
1
- import FishjamCloudClient
1
+ import FishjamCloudBroadcastClient
2
2
  import Foundation
3
3
  import ReplayKit
4
4
  import WebRTC
@@ -3,7 +3,7 @@
3
3
  <plist version="1.0">
4
4
  <dict>
5
5
  <key>CFBundleDisplayName</key>
6
- <string>FishjamScreenBroadcast</string>
6
+ <string>{{DISPLAY_NAME}}</string>
7
7
  <key>CFBundleExecutable</key>
8
8
  <string>$(EXECUTABLE_NAME)</string>
9
9
  <key>CFBundleIdentifier</key>
@@ -11,5 +11,6 @@ export type FishjamPluginOptions = {
11
11
  appGroupContainerId?: string;
12
12
  mainTargetName?: string;
13
13
  broadcastExtensionTargetName?: string;
14
+ broadcastExtensionDisplayName?: string;
14
15
  };
15
16
  } | undefined;
@@ -6,8 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const withFishjamAndroid_1 = require("./withFishjamAndroid");
7
7
  const withFishjamIos_1 = __importDefault(require("./withFishjamIos"));
8
8
  const withFishjam = (config, options) => {
9
- (0, withFishjamAndroid_1.withFishjamAndroid)(config, options);
10
- (0, withFishjamIos_1.default)(config, options);
9
+ config = (0, withFishjamAndroid_1.withFishjamAndroid)(config, options);
10
+ config = (0, withFishjamIos_1.default)(config, options);
11
11
  return config;
12
12
  };
13
13
  exports.default = withFishjam;
@@ -9,19 +9,57 @@ const withFishjamForegroundService = (config, props) => (0, config_plugins_1.wit
9
9
  }
10
10
  const mainApplication = (0, Manifest_1.getMainApplicationOrThrow)(configuration.modResults);
11
11
  mainApplication.service = mainApplication.service || [];
12
- const newService = {
12
+ const fishjamService = {
13
13
  $: {
14
14
  'android:name': 'io.fishjam.reactnative.foregroundService.FishjamForegroundService',
15
15
  'android:foregroundServiceType': 'camera|microphone|mediaProjection',
16
16
  'android:stopWithTask': 'true',
17
17
  },
18
18
  };
19
- const existingServiceIndex = mainApplication.service.findIndex((service) => service.$['android:name'] === newService.$['android:name']);
20
- if (existingServiceIndex !== -1) {
21
- mainApplication.service[existingServiceIndex] = newService;
19
+ const whipWhepService = {
20
+ $: {
21
+ 'android:name': 'com.swmansion.reactnativeclient.foregroundService.ScreenCaptureService',
22
+ 'android:foregroundServiceType': 'mediaProjection',
23
+ 'android:stopWithTask': 'true',
24
+ },
25
+ };
26
+ // Add Fishjam service
27
+ const existingFishjamServiceIndex = mainApplication.service.findIndex((service) => service.$['android:name'] === fishjamService.$['android:name']);
28
+ if (existingFishjamServiceIndex !== -1) {
29
+ mainApplication.service[existingFishjamServiceIndex] = fishjamService;
22
30
  }
23
31
  else {
24
- mainApplication.service.push(newService);
32
+ mainApplication.service.push(fishjamService);
33
+ }
34
+ // Add WhipWhep service
35
+ const existingWhipWhepServiceIndex = mainApplication.service.findIndex((service) => service.$['android:name'] === whipWhepService.$['android:name']);
36
+ if (existingWhipWhepServiceIndex !== -1) {
37
+ mainApplication.service[existingWhipWhepServiceIndex] = whipWhepService;
38
+ }
39
+ else {
40
+ mainApplication.service.push(whipWhepService);
41
+ }
42
+ return configuration;
43
+ });
44
+ const withFishjamForegroundServicePermission = (config, props) => (0, config_plugins_1.withAndroidManifest)(config, (configuration) => {
45
+ if (!props?.android?.enableForegroundService) {
46
+ return configuration;
47
+ }
48
+ const mainApplication = configuration.modResults;
49
+ if (!mainApplication.manifest) {
50
+ return configuration;
51
+ }
52
+ if (!mainApplication.manifest['uses-permission']) {
53
+ mainApplication.manifest['uses-permission'] = [];
54
+ }
55
+ const permissions = mainApplication.manifest['uses-permission'];
56
+ const hasForegroundServicePermission = permissions.some((perm) => perm.$?.['android:name'] === 'android.permission.FOREGROUND_SERVICE');
57
+ if (!hasForegroundServicePermission) {
58
+ permissions.push({
59
+ $: {
60
+ 'android:name': 'android.permission.FOREGROUND_SERVICE',
61
+ },
62
+ });
25
63
  }
26
64
  return configuration;
27
65
  });
@@ -36,6 +74,7 @@ const withFishjamPictureInPicture = (config, props) => (0, config_plugins_1.with
36
74
  return configuration;
37
75
  });
38
76
  const withFishjamAndroid = (config, props) => {
77
+ config = withFishjamForegroundServicePermission(config, props);
39
78
  config = withFishjamForegroundService(config, props);
40
79
  config = withFishjamPictureInPicture(config, props);
41
80
  return config;