@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.
- 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/FishjamRoom/index.d.ts.map +1 -1
- package/build/components/FishjamRoom/index.js +0 -1
- package/build/components/FishjamRoom/index.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/hooks/useAppScreenShare.d.ts +0 -2
- package/build/hooks/useAppScreenShare.d.ts.map +1 -1
- package/build/hooks/useAppScreenShare.js +0 -8
- package/build/hooks/useAppScreenShare.js.map +1 -1
- package/build/hooks/useCamera.d.ts +2 -2
- package/build/hooks/useCamera.d.ts.map +1 -1
- package/build/hooks/useCamera.js +1 -3
- package/build/hooks/useCamera.js.map +1 -1
- package/build/hooks/useScreenShare.d.ts +2 -3
- package/build/hooks/useScreenShare.d.ts.map +1 -1
- package/build/hooks/useScreenShare.js +0 -7
- package/build/hooks/useScreenShare.js.map +1 -1
- 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/useLivestreamScreenSharingStreamer.d.ts +49 -0
- package/build/livestream/hooks/useLivestreamScreenSharingStreamer.d.ts.map +1 -0
- package/build/livestream/hooks/useLivestreamScreenSharingStreamer.js +58 -0
- package/build/livestream/hooks/useLivestreamScreenSharingStreamer.js.map +1 -0
- 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 +44 -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 +2 -1
- package/build/livestream/index.d.ts.map +1 -1
- package/build/livestream/index.js +2 -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
- package/plugin/broadcastExtensionFiles/FishjamBroadcastSampleHandler.swift +1 -1
- package/plugin/broadcastExtensionFiles/Info.plist +1 -1
- package/plugin/build/types.d.ts +1 -0
- package/plugin/build/withFishjam.js +2 -2
- package/plugin/build/withFishjamAndroid.js +44 -5
- package/plugin/build/withFishjamIos.js +7 -2
|
@@ -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,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 {
|
|
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,
|
|
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 {
|
|
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,
|
|
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"]}
|
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
|
+
|
|
@@ -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.
|
|
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.
|
|
45
|
+
"react-native-whip-whep": "0.7.1",
|
|
46
46
|
"zod": "^3.25.71"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
package/plugin/build/types.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
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(
|
|
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;
|