payfri-video-sdk 0.1.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/LICENSE +21 -0
- package/README.md +289 -0
- package/app.plugin.cjs +8 -0
- package/dist/client/index.cjs +99 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.d.cts +53 -0
- package/dist/client/index.d.ts +53 -0
- package/dist/client/index.js +97 -0
- package/dist/client/index.js.map +1 -0
- package/dist/native/index.cjs +151 -0
- package/dist/native/index.cjs.map +1 -0
- package/dist/native/index.d.cts +84 -0
- package/dist/native/index.d.ts +84 -0
- package/dist/native/index.js +148 -0
- package/dist/native/index.js.map +1 -0
- package/dist/plugin/withVideoSdk.cjs +25 -0
- package/dist/plugin/withVideoSdk.cjs.map +1 -0
- package/dist/plugin/withVideoSdk.d.cts +23 -0
- package/dist/plugin/withVideoSdk.d.ts +23 -0
- package/dist/plugin/withVideoSdk.js +23 -0
- package/dist/plugin/withVideoSdk.js.map +1 -0
- package/dist/server/index.cjs +61 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +79 -0
- package/dist/server/index.d.ts +79 -0
- package/dist/server/index.js +58 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var reactNative = require('react-native');
|
|
5
|
+
var reactNativeWebview = require('react-native-webview');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
|
|
8
|
+
// src/native/VideoCall.tsx
|
|
9
|
+
|
|
10
|
+
// src/native/buildRoomUrl.ts
|
|
11
|
+
function buildRoomUrl({
|
|
12
|
+
embedBaseUrl,
|
|
13
|
+
token,
|
|
14
|
+
livekitUrl,
|
|
15
|
+
theme
|
|
16
|
+
}) {
|
|
17
|
+
if (!embedBaseUrl) {
|
|
18
|
+
throw new Error("buildRoomUrl: embedBaseUrl es requerido");
|
|
19
|
+
}
|
|
20
|
+
if (!token) {
|
|
21
|
+
throw new Error("buildRoomUrl: token es requerido");
|
|
22
|
+
}
|
|
23
|
+
if (!livekitUrl) {
|
|
24
|
+
throw new Error("buildRoomUrl: livekitUrl es requerido");
|
|
25
|
+
}
|
|
26
|
+
const base = embedBaseUrl.replace(/\/+$/, "");
|
|
27
|
+
const params = new URLSearchParams({ token, livekitUrl });
|
|
28
|
+
if (theme) {
|
|
29
|
+
params.set("theme", theme);
|
|
30
|
+
}
|
|
31
|
+
return `${base}/room#${params.toString()}`;
|
|
32
|
+
}
|
|
33
|
+
async function ensureAndroidPermissions() {
|
|
34
|
+
if (reactNative.Platform.OS !== "android") {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
const result = await reactNative.PermissionsAndroid.requestMultiple([
|
|
38
|
+
reactNative.PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
39
|
+
reactNative.PermissionsAndroid.PERMISSIONS.RECORD_AUDIO
|
|
40
|
+
]);
|
|
41
|
+
return result[reactNative.PermissionsAndroid.PERMISSIONS.CAMERA] === reactNative.PermissionsAndroid.RESULTS.GRANTED && result[reactNative.PermissionsAndroid.PERMISSIONS.RECORD_AUDIO] === reactNative.PermissionsAndroid.RESULTS.GRANTED;
|
|
42
|
+
}
|
|
43
|
+
function parseBridgeEvent(raw) {
|
|
44
|
+
try {
|
|
45
|
+
const data = JSON.parse(raw);
|
|
46
|
+
return typeof data?.type === "string" ? data : null;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function VideoCall(props) {
|
|
52
|
+
for (const forbidden of ["apiKey", "apiSecret", "secret"]) {
|
|
53
|
+
if (forbidden in props) {
|
|
54
|
+
throw new Error(`VideoCall no acepta "${forbidden}": jam\xE1s pases credenciales al cliente`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const {
|
|
58
|
+
token,
|
|
59
|
+
livekitUrl,
|
|
60
|
+
embedBaseUrl,
|
|
61
|
+
theme,
|
|
62
|
+
onConnected,
|
|
63
|
+
onDisconnected,
|
|
64
|
+
onLeave,
|
|
65
|
+
onParticipantJoined,
|
|
66
|
+
onParticipantLeft,
|
|
67
|
+
onError,
|
|
68
|
+
style
|
|
69
|
+
} = props;
|
|
70
|
+
const [permission, setPermission] = react.useState("pending");
|
|
71
|
+
react.useEffect(() => {
|
|
72
|
+
let cancelled = false;
|
|
73
|
+
ensureAndroidPermissions().then((ok) => {
|
|
74
|
+
if (cancelled) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
setPermission(ok ? "granted" : "denied");
|
|
78
|
+
if (!ok) {
|
|
79
|
+
onError?.(new Error("Permisos de c\xE1mara/micr\xF3fono denegados"));
|
|
80
|
+
}
|
|
81
|
+
}).catch((err) => {
|
|
82
|
+
if (cancelled) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
setPermission("denied");
|
|
86
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
87
|
+
});
|
|
88
|
+
return () => {
|
|
89
|
+
cancelled = true;
|
|
90
|
+
};
|
|
91
|
+
}, [onError]);
|
|
92
|
+
const uri = react.useMemo(
|
|
93
|
+
() => buildRoomUrl({ embedBaseUrl, token, livekitUrl, theme }),
|
|
94
|
+
[embedBaseUrl, token, livekitUrl, theme]
|
|
95
|
+
);
|
|
96
|
+
const handleMessage = react.useCallback(
|
|
97
|
+
(event) => {
|
|
98
|
+
const parsed = parseBridgeEvent(event.nativeEvent.data);
|
|
99
|
+
if (!parsed) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
switch (parsed.type) {
|
|
103
|
+
case "connected":
|
|
104
|
+
onConnected?.();
|
|
105
|
+
break;
|
|
106
|
+
case "disconnected":
|
|
107
|
+
onDisconnected?.();
|
|
108
|
+
break;
|
|
109
|
+
case "leave":
|
|
110
|
+
onLeave?.();
|
|
111
|
+
break;
|
|
112
|
+
case "participantJoined":
|
|
113
|
+
onParticipantJoined?.({ identity: parsed.identity });
|
|
114
|
+
break;
|
|
115
|
+
case "participantLeft":
|
|
116
|
+
onParticipantLeft?.({ identity: parsed.identity });
|
|
117
|
+
break;
|
|
118
|
+
case "error":
|
|
119
|
+
onError?.(new Error(parsed.message));
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
[onConnected, onDisconnected, onLeave, onParticipantJoined, onParticipantLeft, onError]
|
|
124
|
+
);
|
|
125
|
+
if (permission !== "granted") {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
129
|
+
reactNativeWebview.WebView,
|
|
130
|
+
{
|
|
131
|
+
source: { uri },
|
|
132
|
+
style: [styles.fill, style],
|
|
133
|
+
onMessage: handleMessage,
|
|
134
|
+
originWhitelist: ["https://*", "http://*"],
|
|
135
|
+
javaScriptEnabled: true,
|
|
136
|
+
domStorageEnabled: true,
|
|
137
|
+
allowsInlineMediaPlayback: true,
|
|
138
|
+
mediaPlaybackRequiresUserAction: false,
|
|
139
|
+
mediaCapturePermissionGrantType: "grant",
|
|
140
|
+
onError: (e) => onError?.(new Error(e.nativeEvent.description))
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
var styles = reactNative.StyleSheet.create({
|
|
145
|
+
fill: { flex: 1 }
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
exports.VideoCall = VideoCall;
|
|
149
|
+
exports.buildRoomUrl = buildRoomUrl;
|
|
150
|
+
//# sourceMappingURL=index.cjs.map
|
|
151
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/native/buildRoomUrl.ts","../../src/native/VideoCall.tsx"],"names":["Platform","PermissionsAndroid","useState","useEffect","useMemo","useCallback","jsx","WebView","StyleSheet"],"mappings":";;;;;;;;;;AAiBO,SAAS,YAAA,CAAa;AAAA,EAC3B,YAAA;AAAA,EACA,KAAA;AAAA,EACA,UAAA;AAAA,EACA;AACF,CAAA,EAA8B;AAC5B,EAAA,IAAI,CAAC,YAAA,EAAc;AACjB,IAAA,MAAM,IAAI,MAAM,yCAAyC,CAAA;AAAA,EAC3D;AACA,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,MAAM,IAAI,MAAM,kCAAkC,CAAA;AAAA,EACpD;AACA,EAAA,IAAI,CAAC,UAAA,EAAY;AACf,IAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AAAA,EACzD;AAEA,EAAA,MAAM,IAAA,GAAO,YAAA,CAAa,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AAC5C,EAAA,MAAM,SAAS,IAAI,eAAA,CAAgB,EAAE,KAAA,EAAO,YAAY,CAAA;AACxD,EAAA,IAAI,KAAA,EAAO;AACT,IAAA,MAAA,CAAO,GAAA,CAAI,SAAS,KAAK,CAAA;AAAA,EAC3B;AACA,EAAA,OAAO,CAAA,EAAG,IAAI,CAAA,MAAA,EAAS,MAAA,CAAO,UAAU,CAAA,CAAA;AAC1C;ACzBA,eAAe,wBAAA,GAA6C;AAC1D,EAAA,IAAIA,oBAAA,CAAS,OAAO,SAAA,EAAW;AAC7B,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,MAAM,MAAA,GAAS,MAAMC,8BAAA,CAAmB,eAAA,CAAgB;AAAA,IACtDA,+BAAmB,WAAA,CAAY,MAAA;AAAA,IAC/BA,+BAAmB,WAAA,CAAY;AAAA,GAChC,CAAA;AACD,EAAA,OACE,MAAA,CAAOA,8BAAA,CAAmB,WAAA,CAAY,MAAM,MAAMA,8BAAA,CAAmB,OAAA,CAAQ,OAAA,IAC7E,MAAA,CAAOA,8BAAA,CAAmB,WAAA,CAAY,YAAY,CAAA,KAAMA,+BAAmB,OAAA,CAAQ,OAAA;AAEvF;AAEA,SAAS,iBAAiB,GAAA,EAAiC;AACzD,EAAA,IAAI;AACF,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAC3B,IAAA,OAAO,OAAO,IAAA,EAAM,IAAA,KAAS,QAAA,GAAY,IAAA,GAAuB,IAAA;AAAA,EAClE,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AAOO,SAAS,UAAU,KAAA,EAA4C;AAGpE,EAAA,KAAA,MAAW,SAAA,IAAa,CAAC,QAAA,EAAU,WAAA,EAAa,QAAQ,CAAA,EAAG;AACzD,IAAA,IAAI,aAAc,KAAA,EAA8C;AAC9D,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,qBAAA,EAAwB,SAAS,CAAA,yCAAA,CAAwC,CAAA;AAAA,IAC3F;AAAA,EACF;AAEA,EAAA,MAAM;AAAA,IACJ,KAAA;AAAA,IACA,UAAA;AAAA,IACA,YAAA;AAAA,IACA,KAAA;AAAA,IACA,WAAA;AAAA,IACA,cAAA;AAAA,IACA,OAAA;AAAA,IACA,mBAAA;AAAA,IACA,iBAAA;AAAA,IACA,OAAA;AAAA,IACA;AAAA,GACF,GAAI,KAAA;AAEJ,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAIC,eAA0B,SAAS,CAAA;AAEvE,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,IAAI,SAAA,GAAY,KAAA;AAChB,IAAA,wBAAA,EAAyB,CACtB,IAAA,CAAK,CAAC,EAAA,KAAO;AACZ,MAAA,IAAI,SAAA,EAAW;AACb,QAAA;AAAA,MACF;AACA,MAAA,aAAA,CAAc,EAAA,GAAK,YAAY,QAAQ,CAAA;AACvC,MAAA,IAAI,CAAC,EAAA,EAAI;AACP,QAAA,OAAA,GAAU,IAAI,KAAA,CAAM,8CAAwC,CAAC,CAAA;AAAA,MAC/D;AAAA,IACF,CAAC,CAAA,CACA,KAAA,CAAM,CAAC,GAAA,KAAiB;AACvB,MAAA,IAAI,SAAA,EAAW;AACb,QAAA;AAAA,MACF;AACA,MAAA,aAAA,CAAc,QAAQ,CAAA;AACtB,MAAA,OAAA,GAAU,GAAA,YAAe,QAAQ,GAAA,GAAM,IAAI,MAAM,MAAA,CAAO,GAAG,CAAC,CAAC,CAAA;AAAA,IAC/D,CAAC,CAAA;AACH,IAAA,OAAO,MAAM;AACX,MAAA,SAAA,GAAY,IAAA;AAAA,IACd,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAA,MAAM,GAAA,GAAMC,aAAA;AAAA,IACV,MAAM,YAAA,CAAa,EAAE,cAAc,KAAA,EAAO,UAAA,EAAY,OAAO,CAAA;AAAA,IAC7D,CAAC,YAAA,EAAc,KAAA,EAAO,UAAA,EAAY,KAAK;AAAA,GACzC;AAEA,EAAA,MAAM,aAAA,GAAgBC,iBAAA;AAAA,IACpB,CAAC,KAAA,KAA+B;AAC9B,MAAA,MAAM,MAAA,GAAS,gBAAA,CAAiB,KAAA,CAAM,WAAA,CAAY,IAAI,CAAA;AACtD,MAAA,IAAI,CAAC,MAAA,EAAQ;AACX,QAAA;AAAA,MACF;AACA,MAAA,QAAQ,OAAO,IAAA;AAAM,QACnB,KAAK,WAAA;AACH,UAAA,WAAA,IAAc;AACd,UAAA;AAAA,QACF,KAAK,cAAA;AACH,UAAA,cAAA,IAAiB;AACjB,UAAA;AAAA,QACF,KAAK,OAAA;AACH,UAAA,OAAA,IAAU;AACV,UAAA;AAAA,QACF,KAAK,mBAAA;AACH,UAAA,mBAAA,GAAsB,EAAE,QAAA,EAAU,MAAA,CAAO,QAAA,EAAU,CAAA;AACnD,UAAA;AAAA,QACF,KAAK,iBAAA;AACH,UAAA,iBAAA,GAAoB,EAAE,QAAA,EAAU,MAAA,CAAO,QAAA,EAAU,CAAA;AACjD,UAAA;AAAA,QACF,KAAK,OAAA;AACH,UAAA,OAAA,GAAU,IAAI,KAAA,CAAM,MAAA,CAAO,OAAO,CAAC,CAAA;AACnC,UAAA;AAAA;AACJ,IACF,CAAA;AAAA,IACA,CAAC,WAAA,EAAa,cAAA,EAAgB,OAAA,EAAS,mBAAA,EAAqB,mBAAmB,OAAO;AAAA,GACxF;AAEA,EAAA,IAAI,eAAe,SAAA,EAAW;AAC5B,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,uBACEC,cAAA;AAAA,IAACC,0BAAA;AAAA,IAAA;AAAA,MACC,MAAA,EAAQ,EAAE,GAAA,EAAI;AAAA,MACd,KAAA,EAAO,CAAC,MAAA,CAAO,IAAA,EAAM,KAAK,CAAA;AAAA,MAC1B,SAAA,EAAW,aAAA;AAAA,MACX,eAAA,EAAiB,CAAC,WAAA,EAAa,UAAU,CAAA;AAAA,MACzC,iBAAA,EAAiB,IAAA;AAAA,MACjB,iBAAA,EAAiB,IAAA;AAAA,MAEjB,yBAAA,EAAyB,IAAA;AAAA,MACzB,+BAAA,EAAiC,KAAA;AAAA,MACjC,+BAAA,EAAgC,OAAA;AAAA,MAChC,OAAA,EAAS,CAAC,CAAA,KAAM,OAAA,GAAU,IAAI,KAAA,CAAM,CAAA,CAAE,WAAA,CAAY,WAAW,CAAC;AAAA;AAAA,GAChE;AAEJ;AAEA,IAAM,MAAA,GAASC,uBAAW,MAAA,CAAO;AAAA,EAC/B,IAAA,EAAM,EAAE,IAAA,EAAM,CAAA;AAChB,CAAC,CAAA","file":"index.cjs","sourcesContent":["/**\n * Arma la URL de la página embebible con los parámetros en el **hash** (no en la\n * query string, para que el token no termine en logs de servidor). El formato\n * coincide exactamente con lo que parsea `apps/embed/src/lib/roomParams.ts`:\n *\n * https://embed.host/room#token=JWT&livekitUrl=wss://...&theme=%23ff0000\n *\n * Se usa `URLSearchParams` en ambos extremos (aquí y en el embed), así el\n * escapado/desescapado hace round-trip sin sorpresas.\n */\nexport interface BuildRoomUrlInput {\n embedBaseUrl: string;\n token: string;\n livekitUrl: string;\n theme?: string;\n}\n\nexport function buildRoomUrl({\n embedBaseUrl,\n token,\n livekitUrl,\n theme,\n}: BuildRoomUrlInput): string {\n if (!embedBaseUrl) {\n throw new Error(\"buildRoomUrl: embedBaseUrl es requerido\");\n }\n if (!token) {\n throw new Error(\"buildRoomUrl: token es requerido\");\n }\n if (!livekitUrl) {\n throw new Error(\"buildRoomUrl: livekitUrl es requerido\");\n }\n\n const base = embedBaseUrl.replace(/\\/+$/, \"\");\n const params = new URLSearchParams({ token, livekitUrl });\n if (theme) {\n params.set(\"theme\", theme);\n }\n return `${base}/room#${params.toString()}`;\n}\n","import { useCallback, useEffect, useMemo, useState, type ReactElement } from \"react\";\nimport { PermissionsAndroid, Platform, StyleSheet } from \"react-native\";\nimport { WebView, type WebViewMessageEvent } from \"react-native-webview\";\nimport { buildRoomUrl } from \"./buildRoomUrl\";\nimport type { BridgeEvent, VideoCallProps } from \"./types\";\n\ntype PermissionState = \"pending\" | \"granted\" | \"denied\";\n\n/**\n * Pide permisos de cámara y micrófono en Android antes de montar el WebView.\n * En iOS el prompt lo dispara el propio WebView al llamar a getUserMedia\n * (requiere NSCameraUsageDescription / NSMicrophoneUsageDescription en el\n * Info.plist — los inyecta el config plugin de la Fase 2).\n */\nasync function ensureAndroidPermissions(): Promise<boolean> {\n if (Platform.OS !== \"android\") {\n return true;\n }\n const result = await PermissionsAndroid.requestMultiple([\n PermissionsAndroid.PERMISSIONS.CAMERA,\n PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,\n ]);\n return (\n result[PermissionsAndroid.PERMISSIONS.CAMERA] === PermissionsAndroid.RESULTS.GRANTED &&\n result[PermissionsAndroid.PERMISSIONS.RECORD_AUDIO] === PermissionsAndroid.RESULTS.GRANTED\n );\n}\n\nfunction parseBridgeEvent(raw: string): BridgeEvent | null {\n try {\n const data = JSON.parse(raw) as Partial<BridgeEvent>;\n return typeof data?.type === \"string\" ? (data as BridgeEvent) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Incrusta una videollamada de LiveKit en una app React Native envolviendo la\n * página embebible (`apps/embed`) en un WebView. Solo recibe un `token`; nunca\n * una API key (el tipo y este guard lo hacen imposible).\n */\nexport function VideoCall(props: VideoCallProps): ReactElement | null {\n // Guard defensivo: el tipo ya lo impide, pero rechazamos cualquier intento de\n // colar credenciales de servidor al dispositivo.\n for (const forbidden of [\"apiKey\", \"apiSecret\", \"secret\"]) {\n if (forbidden in (props as unknown as Record<string, unknown>)) {\n throw new Error(`VideoCall no acepta \"${forbidden}\": jamás pases credenciales al cliente`);\n }\n }\n\n const {\n token,\n livekitUrl,\n embedBaseUrl,\n theme,\n onConnected,\n onDisconnected,\n onLeave,\n onParticipantJoined,\n onParticipantLeft,\n onError,\n style,\n } = props;\n\n const [permission, setPermission] = useState<PermissionState>(\"pending\");\n\n useEffect(() => {\n let cancelled = false;\n ensureAndroidPermissions()\n .then((ok) => {\n if (cancelled) {\n return;\n }\n setPermission(ok ? \"granted\" : \"denied\");\n if (!ok) {\n onError?.(new Error(\"Permisos de cámara/micrófono denegados\"));\n }\n })\n .catch((err: unknown) => {\n if (cancelled) {\n return;\n }\n setPermission(\"denied\");\n onError?.(err instanceof Error ? err : new Error(String(err)));\n });\n return () => {\n cancelled = true;\n };\n }, [onError]);\n\n const uri = useMemo(\n () => buildRoomUrl({ embedBaseUrl, token, livekitUrl, theme }),\n [embedBaseUrl, token, livekitUrl, theme],\n );\n\n const handleMessage = useCallback(\n (event: WebViewMessageEvent) => {\n const parsed = parseBridgeEvent(event.nativeEvent.data);\n if (!parsed) {\n return;\n }\n switch (parsed.type) {\n case \"connected\":\n onConnected?.();\n break;\n case \"disconnected\":\n onDisconnected?.();\n break;\n case \"leave\":\n onLeave?.();\n break;\n case \"participantJoined\":\n onParticipantJoined?.({ identity: parsed.identity });\n break;\n case \"participantLeft\":\n onParticipantLeft?.({ identity: parsed.identity });\n break;\n case \"error\":\n onError?.(new Error(parsed.message));\n break;\n }\n },\n [onConnected, onDisconnected, onLeave, onParticipantJoined, onParticipantLeft, onError],\n );\n\n if (permission !== \"granted\") {\n return null;\n }\n\n return (\n <WebView\n source={{ uri }}\n style={[styles.fill, style]}\n onMessage={handleMessage}\n originWhitelist={[\"https://*\", \"http://*\"]}\n javaScriptEnabled\n domStorageEnabled\n // iOS: permite reproducir video inline y conceder getUserMedia sin gesto.\n allowsInlineMediaPlayback\n mediaPlaybackRequiresUserAction={false}\n mediaCapturePermissionGrantType=\"grant\"\n onError={(e) => onError?.(new Error(e.nativeEvent.description))}\n />\n );\n}\n\nconst styles = StyleSheet.create({\n fill: { flex: 1 },\n});\n"]}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
import { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Props del componente `<VideoCall>`. Igual que `payfri-video-sdk/client`, NO existe
|
|
6
|
+
* un campo para la API key: el tipo hace imposible filtrar credenciales de
|
|
7
|
+
* servidor al dispositivo. El cliente solo pasa un `token` de LiveKit.
|
|
8
|
+
*/
|
|
9
|
+
interface VideoCallProps {
|
|
10
|
+
/** Token de acceso de LiveKit (corta duración) emitido por tu backend. */
|
|
11
|
+
token: string;
|
|
12
|
+
/** URL ws/wss del servidor LiveKit (la devuelve `POST /v1/tokens`). */
|
|
13
|
+
livekitUrl: string;
|
|
14
|
+
/** Base de la página embebible, ej. `https://embed.tudominio.com`. */
|
|
15
|
+
embedBaseUrl: string;
|
|
16
|
+
/** Color de acento opcional para branding del tenant (lo aplica el embed). */
|
|
17
|
+
theme?: string;
|
|
18
|
+
/** La videollamada conectó. */
|
|
19
|
+
onConnected?: () => void;
|
|
20
|
+
/** El usuario se desconectó o cerró la sala. */
|
|
21
|
+
onDisconnected?: () => void;
|
|
22
|
+
/** El usuario pulsó "salir" en la UI del embed. */
|
|
23
|
+
onLeave?: () => void;
|
|
24
|
+
/** Entró un participante remoto. */
|
|
25
|
+
onParticipantJoined?: (participant: {
|
|
26
|
+
identity: string;
|
|
27
|
+
}) => void;
|
|
28
|
+
/** Salió un participante remoto. */
|
|
29
|
+
onParticipantLeft?: (participant: {
|
|
30
|
+
identity: string;
|
|
31
|
+
}) => void;
|
|
32
|
+
/** Error (permisos denegados, token inválido, fallo de la sala). */
|
|
33
|
+
onError?: (error: Error) => void;
|
|
34
|
+
/** Estilo del contenedor. Por defecto ocupa todo el espacio disponible. */
|
|
35
|
+
style?: StyleProp<ViewStyle>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Eventos que el embed publica hacia la app nativa vía
|
|
39
|
+
* `window.ReactNativeWebView.postMessage`. El contrato lo implementa
|
|
40
|
+
* `apps/embed/src/lib/rnBridge.ts` (Fase 3).
|
|
41
|
+
*/
|
|
42
|
+
type BridgeEvent = {
|
|
43
|
+
type: "connected";
|
|
44
|
+
} | {
|
|
45
|
+
type: "disconnected";
|
|
46
|
+
} | {
|
|
47
|
+
type: "leave";
|
|
48
|
+
} | {
|
|
49
|
+
type: "participantJoined";
|
|
50
|
+
identity: string;
|
|
51
|
+
} | {
|
|
52
|
+
type: "participantLeft";
|
|
53
|
+
identity: string;
|
|
54
|
+
} | {
|
|
55
|
+
type: "error";
|
|
56
|
+
message: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Incrusta una videollamada de LiveKit en una app React Native envolviendo la
|
|
61
|
+
* página embebible (`apps/embed`) en un WebView. Solo recibe un `token`; nunca
|
|
62
|
+
* una API key (el tipo y este guard lo hacen imposible).
|
|
63
|
+
*/
|
|
64
|
+
declare function VideoCall(props: VideoCallProps): ReactElement | null;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Arma la URL de la página embebible con los parámetros en el **hash** (no en la
|
|
68
|
+
* query string, para que el token no termine en logs de servidor). El formato
|
|
69
|
+
* coincide exactamente con lo que parsea `apps/embed/src/lib/roomParams.ts`:
|
|
70
|
+
*
|
|
71
|
+
* https://embed.host/room#token=JWT&livekitUrl=wss://...&theme=%23ff0000
|
|
72
|
+
*
|
|
73
|
+
* Se usa `URLSearchParams` en ambos extremos (aquí y en el embed), así el
|
|
74
|
+
* escapado/desescapado hace round-trip sin sorpresas.
|
|
75
|
+
*/
|
|
76
|
+
interface BuildRoomUrlInput {
|
|
77
|
+
embedBaseUrl: string;
|
|
78
|
+
token: string;
|
|
79
|
+
livekitUrl: string;
|
|
80
|
+
theme?: string;
|
|
81
|
+
}
|
|
82
|
+
declare function buildRoomUrl({ embedBaseUrl, token, livekitUrl, theme, }: BuildRoomUrlInput): string;
|
|
83
|
+
|
|
84
|
+
export { type BridgeEvent, type BuildRoomUrlInput, VideoCall, type VideoCallProps, buildRoomUrl };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
import { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Props del componente `<VideoCall>`. Igual que `payfri-video-sdk/client`, NO existe
|
|
6
|
+
* un campo para la API key: el tipo hace imposible filtrar credenciales de
|
|
7
|
+
* servidor al dispositivo. El cliente solo pasa un `token` de LiveKit.
|
|
8
|
+
*/
|
|
9
|
+
interface VideoCallProps {
|
|
10
|
+
/** Token de acceso de LiveKit (corta duración) emitido por tu backend. */
|
|
11
|
+
token: string;
|
|
12
|
+
/** URL ws/wss del servidor LiveKit (la devuelve `POST /v1/tokens`). */
|
|
13
|
+
livekitUrl: string;
|
|
14
|
+
/** Base de la página embebible, ej. `https://embed.tudominio.com`. */
|
|
15
|
+
embedBaseUrl: string;
|
|
16
|
+
/** Color de acento opcional para branding del tenant (lo aplica el embed). */
|
|
17
|
+
theme?: string;
|
|
18
|
+
/** La videollamada conectó. */
|
|
19
|
+
onConnected?: () => void;
|
|
20
|
+
/** El usuario se desconectó o cerró la sala. */
|
|
21
|
+
onDisconnected?: () => void;
|
|
22
|
+
/** El usuario pulsó "salir" en la UI del embed. */
|
|
23
|
+
onLeave?: () => void;
|
|
24
|
+
/** Entró un participante remoto. */
|
|
25
|
+
onParticipantJoined?: (participant: {
|
|
26
|
+
identity: string;
|
|
27
|
+
}) => void;
|
|
28
|
+
/** Salió un participante remoto. */
|
|
29
|
+
onParticipantLeft?: (participant: {
|
|
30
|
+
identity: string;
|
|
31
|
+
}) => void;
|
|
32
|
+
/** Error (permisos denegados, token inválido, fallo de la sala). */
|
|
33
|
+
onError?: (error: Error) => void;
|
|
34
|
+
/** Estilo del contenedor. Por defecto ocupa todo el espacio disponible. */
|
|
35
|
+
style?: StyleProp<ViewStyle>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Eventos que el embed publica hacia la app nativa vía
|
|
39
|
+
* `window.ReactNativeWebView.postMessage`. El contrato lo implementa
|
|
40
|
+
* `apps/embed/src/lib/rnBridge.ts` (Fase 3).
|
|
41
|
+
*/
|
|
42
|
+
type BridgeEvent = {
|
|
43
|
+
type: "connected";
|
|
44
|
+
} | {
|
|
45
|
+
type: "disconnected";
|
|
46
|
+
} | {
|
|
47
|
+
type: "leave";
|
|
48
|
+
} | {
|
|
49
|
+
type: "participantJoined";
|
|
50
|
+
identity: string;
|
|
51
|
+
} | {
|
|
52
|
+
type: "participantLeft";
|
|
53
|
+
identity: string;
|
|
54
|
+
} | {
|
|
55
|
+
type: "error";
|
|
56
|
+
message: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Incrusta una videollamada de LiveKit en una app React Native envolviendo la
|
|
61
|
+
* página embebible (`apps/embed`) en un WebView. Solo recibe un `token`; nunca
|
|
62
|
+
* una API key (el tipo y este guard lo hacen imposible).
|
|
63
|
+
*/
|
|
64
|
+
declare function VideoCall(props: VideoCallProps): ReactElement | null;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Arma la URL de la página embebible con los parámetros en el **hash** (no en la
|
|
68
|
+
* query string, para que el token no termine en logs de servidor). El formato
|
|
69
|
+
* coincide exactamente con lo que parsea `apps/embed/src/lib/roomParams.ts`:
|
|
70
|
+
*
|
|
71
|
+
* https://embed.host/room#token=JWT&livekitUrl=wss://...&theme=%23ff0000
|
|
72
|
+
*
|
|
73
|
+
* Se usa `URLSearchParams` en ambos extremos (aquí y en el embed), así el
|
|
74
|
+
* escapado/desescapado hace round-trip sin sorpresas.
|
|
75
|
+
*/
|
|
76
|
+
interface BuildRoomUrlInput {
|
|
77
|
+
embedBaseUrl: string;
|
|
78
|
+
token: string;
|
|
79
|
+
livekitUrl: string;
|
|
80
|
+
theme?: string;
|
|
81
|
+
}
|
|
82
|
+
declare function buildRoomUrl({ embedBaseUrl, token, livekitUrl, theme, }: BuildRoomUrlInput): string;
|
|
83
|
+
|
|
84
|
+
export { type BridgeEvent, type BuildRoomUrlInput, VideoCall, type VideoCallProps, buildRoomUrl };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
2
|
+
import { StyleSheet, Platform, PermissionsAndroid } from 'react-native';
|
|
3
|
+
import { WebView } from 'react-native-webview';
|
|
4
|
+
import { jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/native/VideoCall.tsx
|
|
7
|
+
|
|
8
|
+
// src/native/buildRoomUrl.ts
|
|
9
|
+
function buildRoomUrl({
|
|
10
|
+
embedBaseUrl,
|
|
11
|
+
token,
|
|
12
|
+
livekitUrl,
|
|
13
|
+
theme
|
|
14
|
+
}) {
|
|
15
|
+
if (!embedBaseUrl) {
|
|
16
|
+
throw new Error("buildRoomUrl: embedBaseUrl es requerido");
|
|
17
|
+
}
|
|
18
|
+
if (!token) {
|
|
19
|
+
throw new Error("buildRoomUrl: token es requerido");
|
|
20
|
+
}
|
|
21
|
+
if (!livekitUrl) {
|
|
22
|
+
throw new Error("buildRoomUrl: livekitUrl es requerido");
|
|
23
|
+
}
|
|
24
|
+
const base = embedBaseUrl.replace(/\/+$/, "");
|
|
25
|
+
const params = new URLSearchParams({ token, livekitUrl });
|
|
26
|
+
if (theme) {
|
|
27
|
+
params.set("theme", theme);
|
|
28
|
+
}
|
|
29
|
+
return `${base}/room#${params.toString()}`;
|
|
30
|
+
}
|
|
31
|
+
async function ensureAndroidPermissions() {
|
|
32
|
+
if (Platform.OS !== "android") {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
const result = await PermissionsAndroid.requestMultiple([
|
|
36
|
+
PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
37
|
+
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO
|
|
38
|
+
]);
|
|
39
|
+
return result[PermissionsAndroid.PERMISSIONS.CAMERA] === PermissionsAndroid.RESULTS.GRANTED && result[PermissionsAndroid.PERMISSIONS.RECORD_AUDIO] === PermissionsAndroid.RESULTS.GRANTED;
|
|
40
|
+
}
|
|
41
|
+
function parseBridgeEvent(raw) {
|
|
42
|
+
try {
|
|
43
|
+
const data = JSON.parse(raw);
|
|
44
|
+
return typeof data?.type === "string" ? data : null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function VideoCall(props) {
|
|
50
|
+
for (const forbidden of ["apiKey", "apiSecret", "secret"]) {
|
|
51
|
+
if (forbidden in props) {
|
|
52
|
+
throw new Error(`VideoCall no acepta "${forbidden}": jam\xE1s pases credenciales al cliente`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const {
|
|
56
|
+
token,
|
|
57
|
+
livekitUrl,
|
|
58
|
+
embedBaseUrl,
|
|
59
|
+
theme,
|
|
60
|
+
onConnected,
|
|
61
|
+
onDisconnected,
|
|
62
|
+
onLeave,
|
|
63
|
+
onParticipantJoined,
|
|
64
|
+
onParticipantLeft,
|
|
65
|
+
onError,
|
|
66
|
+
style
|
|
67
|
+
} = props;
|
|
68
|
+
const [permission, setPermission] = useState("pending");
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
let cancelled = false;
|
|
71
|
+
ensureAndroidPermissions().then((ok) => {
|
|
72
|
+
if (cancelled) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
setPermission(ok ? "granted" : "denied");
|
|
76
|
+
if (!ok) {
|
|
77
|
+
onError?.(new Error("Permisos de c\xE1mara/micr\xF3fono denegados"));
|
|
78
|
+
}
|
|
79
|
+
}).catch((err) => {
|
|
80
|
+
if (cancelled) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
setPermission("denied");
|
|
84
|
+
onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
85
|
+
});
|
|
86
|
+
return () => {
|
|
87
|
+
cancelled = true;
|
|
88
|
+
};
|
|
89
|
+
}, [onError]);
|
|
90
|
+
const uri = useMemo(
|
|
91
|
+
() => buildRoomUrl({ embedBaseUrl, token, livekitUrl, theme }),
|
|
92
|
+
[embedBaseUrl, token, livekitUrl, theme]
|
|
93
|
+
);
|
|
94
|
+
const handleMessage = useCallback(
|
|
95
|
+
(event) => {
|
|
96
|
+
const parsed = parseBridgeEvent(event.nativeEvent.data);
|
|
97
|
+
if (!parsed) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
switch (parsed.type) {
|
|
101
|
+
case "connected":
|
|
102
|
+
onConnected?.();
|
|
103
|
+
break;
|
|
104
|
+
case "disconnected":
|
|
105
|
+
onDisconnected?.();
|
|
106
|
+
break;
|
|
107
|
+
case "leave":
|
|
108
|
+
onLeave?.();
|
|
109
|
+
break;
|
|
110
|
+
case "participantJoined":
|
|
111
|
+
onParticipantJoined?.({ identity: parsed.identity });
|
|
112
|
+
break;
|
|
113
|
+
case "participantLeft":
|
|
114
|
+
onParticipantLeft?.({ identity: parsed.identity });
|
|
115
|
+
break;
|
|
116
|
+
case "error":
|
|
117
|
+
onError?.(new Error(parsed.message));
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
[onConnected, onDisconnected, onLeave, onParticipantJoined, onParticipantLeft, onError]
|
|
122
|
+
);
|
|
123
|
+
if (permission !== "granted") {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return /* @__PURE__ */ jsx(
|
|
127
|
+
WebView,
|
|
128
|
+
{
|
|
129
|
+
source: { uri },
|
|
130
|
+
style: [styles.fill, style],
|
|
131
|
+
onMessage: handleMessage,
|
|
132
|
+
originWhitelist: ["https://*", "http://*"],
|
|
133
|
+
javaScriptEnabled: true,
|
|
134
|
+
domStorageEnabled: true,
|
|
135
|
+
allowsInlineMediaPlayback: true,
|
|
136
|
+
mediaPlaybackRequiresUserAction: false,
|
|
137
|
+
mediaCapturePermissionGrantType: "grant",
|
|
138
|
+
onError: (e) => onError?.(new Error(e.nativeEvent.description))
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
var styles = StyleSheet.create({
|
|
143
|
+
fill: { flex: 1 }
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
export { VideoCall, buildRoomUrl };
|
|
147
|
+
//# sourceMappingURL=index.js.map
|
|
148
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/native/buildRoomUrl.ts","../../src/native/VideoCall.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Arma la URL de la página embebible con los parámetros en el **hash** (no en la\n * query string, para que el token no termine en logs de servidor). El formato\n * coincide exactamente con lo que parsea `apps/embed/src/lib/roomParams.ts`:\n *\n * https://embed.host/room#token=JWT&livekitUrl=wss://...&theme=%23ff0000\n *\n * Se usa `URLSearchParams` en ambos extremos (aquí y en el embed), así el\n * escapado/desescapado hace round-trip sin sorpresas.\n */\nexport interface BuildRoomUrlInput {\n embedBaseUrl: string;\n token: string;\n livekitUrl: string;\n theme?: string;\n}\n\nexport function buildRoomUrl({\n embedBaseUrl,\n token,\n livekitUrl,\n theme,\n}: BuildRoomUrlInput): string {\n if (!embedBaseUrl) {\n throw new Error(\"buildRoomUrl: embedBaseUrl es requerido\");\n }\n if (!token) {\n throw new Error(\"buildRoomUrl: token es requerido\");\n }\n if (!livekitUrl) {\n throw new Error(\"buildRoomUrl: livekitUrl es requerido\");\n }\n\n const base = embedBaseUrl.replace(/\\/+$/, \"\");\n const params = new URLSearchParams({ token, livekitUrl });\n if (theme) {\n params.set(\"theme\", theme);\n }\n return `${base}/room#${params.toString()}`;\n}\n","import { useCallback, useEffect, useMemo, useState, type ReactElement } from \"react\";\nimport { PermissionsAndroid, Platform, StyleSheet } from \"react-native\";\nimport { WebView, type WebViewMessageEvent } from \"react-native-webview\";\nimport { buildRoomUrl } from \"./buildRoomUrl\";\nimport type { BridgeEvent, VideoCallProps } from \"./types\";\n\ntype PermissionState = \"pending\" | \"granted\" | \"denied\";\n\n/**\n * Pide permisos de cámara y micrófono en Android antes de montar el WebView.\n * En iOS el prompt lo dispara el propio WebView al llamar a getUserMedia\n * (requiere NSCameraUsageDescription / NSMicrophoneUsageDescription en el\n * Info.plist — los inyecta el config plugin de la Fase 2).\n */\nasync function ensureAndroidPermissions(): Promise<boolean> {\n if (Platform.OS !== \"android\") {\n return true;\n }\n const result = await PermissionsAndroid.requestMultiple([\n PermissionsAndroid.PERMISSIONS.CAMERA,\n PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,\n ]);\n return (\n result[PermissionsAndroid.PERMISSIONS.CAMERA] === PermissionsAndroid.RESULTS.GRANTED &&\n result[PermissionsAndroid.PERMISSIONS.RECORD_AUDIO] === PermissionsAndroid.RESULTS.GRANTED\n );\n}\n\nfunction parseBridgeEvent(raw: string): BridgeEvent | null {\n try {\n const data = JSON.parse(raw) as Partial<BridgeEvent>;\n return typeof data?.type === \"string\" ? (data as BridgeEvent) : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Incrusta una videollamada de LiveKit en una app React Native envolviendo la\n * página embebible (`apps/embed`) en un WebView. Solo recibe un `token`; nunca\n * una API key (el tipo y este guard lo hacen imposible).\n */\nexport function VideoCall(props: VideoCallProps): ReactElement | null {\n // Guard defensivo: el tipo ya lo impide, pero rechazamos cualquier intento de\n // colar credenciales de servidor al dispositivo.\n for (const forbidden of [\"apiKey\", \"apiSecret\", \"secret\"]) {\n if (forbidden in (props as unknown as Record<string, unknown>)) {\n throw new Error(`VideoCall no acepta \"${forbidden}\": jamás pases credenciales al cliente`);\n }\n }\n\n const {\n token,\n livekitUrl,\n embedBaseUrl,\n theme,\n onConnected,\n onDisconnected,\n onLeave,\n onParticipantJoined,\n onParticipantLeft,\n onError,\n style,\n } = props;\n\n const [permission, setPermission] = useState<PermissionState>(\"pending\");\n\n useEffect(() => {\n let cancelled = false;\n ensureAndroidPermissions()\n .then((ok) => {\n if (cancelled) {\n return;\n }\n setPermission(ok ? \"granted\" : \"denied\");\n if (!ok) {\n onError?.(new Error(\"Permisos de cámara/micrófono denegados\"));\n }\n })\n .catch((err: unknown) => {\n if (cancelled) {\n return;\n }\n setPermission(\"denied\");\n onError?.(err instanceof Error ? err : new Error(String(err)));\n });\n return () => {\n cancelled = true;\n };\n }, [onError]);\n\n const uri = useMemo(\n () => buildRoomUrl({ embedBaseUrl, token, livekitUrl, theme }),\n [embedBaseUrl, token, livekitUrl, theme],\n );\n\n const handleMessage = useCallback(\n (event: WebViewMessageEvent) => {\n const parsed = parseBridgeEvent(event.nativeEvent.data);\n if (!parsed) {\n return;\n }\n switch (parsed.type) {\n case \"connected\":\n onConnected?.();\n break;\n case \"disconnected\":\n onDisconnected?.();\n break;\n case \"leave\":\n onLeave?.();\n break;\n case \"participantJoined\":\n onParticipantJoined?.({ identity: parsed.identity });\n break;\n case \"participantLeft\":\n onParticipantLeft?.({ identity: parsed.identity });\n break;\n case \"error\":\n onError?.(new Error(parsed.message));\n break;\n }\n },\n [onConnected, onDisconnected, onLeave, onParticipantJoined, onParticipantLeft, onError],\n );\n\n if (permission !== \"granted\") {\n return null;\n }\n\n return (\n <WebView\n source={{ uri }}\n style={[styles.fill, style]}\n onMessage={handleMessage}\n originWhitelist={[\"https://*\", \"http://*\"]}\n javaScriptEnabled\n domStorageEnabled\n // iOS: permite reproducir video inline y conceder getUserMedia sin gesto.\n allowsInlineMediaPlayback\n mediaPlaybackRequiresUserAction={false}\n mediaCapturePermissionGrantType=\"grant\"\n onError={(e) => onError?.(new Error(e.nativeEvent.description))}\n />\n );\n}\n\nconst styles = StyleSheet.create({\n fill: { flex: 1 },\n});\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var configPlugins = require('@expo/config-plugins');
|
|
4
|
+
|
|
5
|
+
// src/plugin/withVideoSdk.ts
|
|
6
|
+
var DEFAULT_CAMERA_TEXT = "Esta app usa la c\xE1mara para las videollamadas.";
|
|
7
|
+
var DEFAULT_MICROPHONE_TEXT = "Esta app usa el micr\xF3fono para las videollamadas.";
|
|
8
|
+
var withVideoSdk = (config, props) => {
|
|
9
|
+
const cameraText = props?.cameraText ?? DEFAULT_CAMERA_TEXT;
|
|
10
|
+
const microphoneText = props?.microphoneText ?? DEFAULT_MICROPHONE_TEXT;
|
|
11
|
+
const withIos = configPlugins.withInfoPlist(config, (cfg) => {
|
|
12
|
+
cfg.modResults.NSCameraUsageDescription = cfg.modResults.NSCameraUsageDescription ?? cameraText;
|
|
13
|
+
cfg.modResults.NSMicrophoneUsageDescription = cfg.modResults.NSMicrophoneUsageDescription ?? microphoneText;
|
|
14
|
+
return cfg;
|
|
15
|
+
});
|
|
16
|
+
return configPlugins.AndroidConfig.Permissions.withPermissions(withIos, [
|
|
17
|
+
"android.permission.CAMERA",
|
|
18
|
+
"android.permission.RECORD_AUDIO"
|
|
19
|
+
]);
|
|
20
|
+
};
|
|
21
|
+
var withVideoSdk_default = withVideoSdk;
|
|
22
|
+
|
|
23
|
+
module.exports = withVideoSdk_default;
|
|
24
|
+
//# sourceMappingURL=withVideoSdk.cjs.map
|
|
25
|
+
//# sourceMappingURL=withVideoSdk.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/plugin/withVideoSdk.ts"],"names":["withInfoPlist","AndroidConfig"],"mappings":";;;;;AAyBA,IAAM,mBAAA,GAAsB,mDAAA;AAC5B,IAAM,uBAAA,GAA0B,sDAAA;AAEhC,IAAM,YAAA,GAAyD,CAAC,MAAA,EAAQ,KAAA,KAAU;AAChF,EAAA,MAAM,UAAA,GAAa,OAAO,UAAA,IAAc,mBAAA;AACxC,EAAA,MAAM,cAAA,GAAiB,OAAO,cAAA,IAAkB,uBAAA;AAGhD,EAAA,MAAM,OAAA,GAAUA,2BAAA,CAAc,MAAA,EAAQ,CAAC,GAAA,KAAQ;AAC7C,IAAA,GAAA,CAAI,UAAA,CAAW,wBAAA,GACb,GAAA,CAAI,UAAA,CAAW,wBAAA,IAA4B,UAAA;AAC7C,IAAA,GAAA,CAAI,UAAA,CAAW,4BAAA,GACb,GAAA,CAAI,UAAA,CAAW,4BAAA,IAAgC,cAAA;AACjD,IAAA,OAAO,GAAA;AAAA,EACT,CAAC,CAAA;AAGD,EAAA,OAAOC,2BAAA,CAAc,WAAA,CAAY,eAAA,CAAgB,OAAA,EAAS;AAAA,IACxD,2BAAA;AAAA,IACA;AAAA,GACD,CAAA;AACH,CAAA;AAEA,IAAO,oBAAA,GAAQ","file":"withVideoSdk.cjs","sourcesContent":["import {\n AndroidConfig,\n withInfoPlist,\n type ConfigPlugin,\n} from \"@expo/config-plugins\";\n\n/**\n * Config plugin de Expo para `payfri-video-sdk`. Inyecta en el prebuild los permisos\n * de cámara y micrófono que exige el sistema operativo para usar getUserMedia\n * dentro del WebView de `<VideoCall>`:\n *\n * - iOS: NSCameraUsageDescription, NSMicrophoneUsageDescription (Info.plist)\n * - Android: CAMERA, RECORD_AUDIO (AndroidManifest)\n *\n * El tenant lo activa en su `app.json` y no toca ningún archivo nativo:\n *\n * { \"plugins\": [[\"payfri-video-sdk\", { \"cameraText\": \"Para videollamadas\" }]] }\n */\nexport interface VideoSdkPluginProps {\n /** Texto del permiso de cámara mostrado por iOS. */\n cameraText?: string;\n /** Texto del permiso de micrófono mostrado por iOS. */\n microphoneText?: string;\n}\n\nconst DEFAULT_CAMERA_TEXT = \"Esta app usa la cámara para las videollamadas.\";\nconst DEFAULT_MICROPHONE_TEXT = \"Esta app usa el micrófono para las videollamadas.\";\n\nconst withVideoSdk: ConfigPlugin<VideoSdkPluginProps | void> = (config, props) => {\n const cameraText = props?.cameraText ?? DEFAULT_CAMERA_TEXT;\n const microphoneText = props?.microphoneText ?? DEFAULT_MICROPHONE_TEXT;\n\n // iOS: no pisar valores que el proyecto ya haya definido.\n const withIos = withInfoPlist(config, (cfg) => {\n cfg.modResults.NSCameraUsageDescription =\n cfg.modResults.NSCameraUsageDescription ?? cameraText;\n cfg.modResults.NSMicrophoneUsageDescription =\n cfg.modResults.NSMicrophoneUsageDescription ?? microphoneText;\n return cfg;\n });\n\n // Android: withPermissions deduplica, así que es seguro re-declararlos.\n return AndroidConfig.Permissions.withPermissions(withIos, [\n \"android.permission.CAMERA\",\n \"android.permission.RECORD_AUDIO\",\n ]);\n};\n\nexport default withVideoSdk;\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ConfigPlugin } from '@expo/config-plugins';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Config plugin de Expo para `payfri-video-sdk`. Inyecta en el prebuild los permisos
|
|
5
|
+
* de cámara y micrófono que exige el sistema operativo para usar getUserMedia
|
|
6
|
+
* dentro del WebView de `<VideoCall>`:
|
|
7
|
+
*
|
|
8
|
+
* - iOS: NSCameraUsageDescription, NSMicrophoneUsageDescription (Info.plist)
|
|
9
|
+
* - Android: CAMERA, RECORD_AUDIO (AndroidManifest)
|
|
10
|
+
*
|
|
11
|
+
* El tenant lo activa en su `app.json` y no toca ningún archivo nativo:
|
|
12
|
+
*
|
|
13
|
+
* { "plugins": [["payfri-video-sdk", { "cameraText": "Para videollamadas" }]] }
|
|
14
|
+
*/
|
|
15
|
+
interface VideoSdkPluginProps {
|
|
16
|
+
/** Texto del permiso de cámara mostrado por iOS. */
|
|
17
|
+
cameraText?: string;
|
|
18
|
+
/** Texto del permiso de micrófono mostrado por iOS. */
|
|
19
|
+
microphoneText?: string;
|
|
20
|
+
}
|
|
21
|
+
declare const withVideoSdk: ConfigPlugin<VideoSdkPluginProps | void>;
|
|
22
|
+
|
|
23
|
+
export { type VideoSdkPluginProps, withVideoSdk as default };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ConfigPlugin } from '@expo/config-plugins';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Config plugin de Expo para `payfri-video-sdk`. Inyecta en el prebuild los permisos
|
|
5
|
+
* de cámara y micrófono que exige el sistema operativo para usar getUserMedia
|
|
6
|
+
* dentro del WebView de `<VideoCall>`:
|
|
7
|
+
*
|
|
8
|
+
* - iOS: NSCameraUsageDescription, NSMicrophoneUsageDescription (Info.plist)
|
|
9
|
+
* - Android: CAMERA, RECORD_AUDIO (AndroidManifest)
|
|
10
|
+
*
|
|
11
|
+
* El tenant lo activa en su `app.json` y no toca ningún archivo nativo:
|
|
12
|
+
*
|
|
13
|
+
* { "plugins": [["payfri-video-sdk", { "cameraText": "Para videollamadas" }]] }
|
|
14
|
+
*/
|
|
15
|
+
interface VideoSdkPluginProps {
|
|
16
|
+
/** Texto del permiso de cámara mostrado por iOS. */
|
|
17
|
+
cameraText?: string;
|
|
18
|
+
/** Texto del permiso de micrófono mostrado por iOS. */
|
|
19
|
+
microphoneText?: string;
|
|
20
|
+
}
|
|
21
|
+
declare const withVideoSdk: ConfigPlugin<VideoSdkPluginProps | void>;
|
|
22
|
+
|
|
23
|
+
export { type VideoSdkPluginProps, withVideoSdk as default };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { withInfoPlist, AndroidConfig } from '@expo/config-plugins';
|
|
2
|
+
|
|
3
|
+
// src/plugin/withVideoSdk.ts
|
|
4
|
+
var DEFAULT_CAMERA_TEXT = "Esta app usa la c\xE1mara para las videollamadas.";
|
|
5
|
+
var DEFAULT_MICROPHONE_TEXT = "Esta app usa el micr\xF3fono para las videollamadas.";
|
|
6
|
+
var withVideoSdk = (config, props) => {
|
|
7
|
+
const cameraText = props?.cameraText ?? DEFAULT_CAMERA_TEXT;
|
|
8
|
+
const microphoneText = props?.microphoneText ?? DEFAULT_MICROPHONE_TEXT;
|
|
9
|
+
const withIos = withInfoPlist(config, (cfg) => {
|
|
10
|
+
cfg.modResults.NSCameraUsageDescription = cfg.modResults.NSCameraUsageDescription ?? cameraText;
|
|
11
|
+
cfg.modResults.NSMicrophoneUsageDescription = cfg.modResults.NSMicrophoneUsageDescription ?? microphoneText;
|
|
12
|
+
return cfg;
|
|
13
|
+
});
|
|
14
|
+
return AndroidConfig.Permissions.withPermissions(withIos, [
|
|
15
|
+
"android.permission.CAMERA",
|
|
16
|
+
"android.permission.RECORD_AUDIO"
|
|
17
|
+
]);
|
|
18
|
+
};
|
|
19
|
+
var withVideoSdk_default = withVideoSdk;
|
|
20
|
+
|
|
21
|
+
export { withVideoSdk_default as default };
|
|
22
|
+
//# sourceMappingURL=withVideoSdk.js.map
|
|
23
|
+
//# sourceMappingURL=withVideoSdk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/plugin/withVideoSdk.ts"],"names":[],"mappings":";;;AAyBA,IAAM,mBAAA,GAAsB,mDAAA;AAC5B,IAAM,uBAAA,GAA0B,sDAAA;AAEhC,IAAM,YAAA,GAAyD,CAAC,MAAA,EAAQ,KAAA,KAAU;AAChF,EAAA,MAAM,UAAA,GAAa,OAAO,UAAA,IAAc,mBAAA;AACxC,EAAA,MAAM,cAAA,GAAiB,OAAO,cAAA,IAAkB,uBAAA;AAGhD,EAAA,MAAM,OAAA,GAAU,aAAA,CAAc,MAAA,EAAQ,CAAC,GAAA,KAAQ;AAC7C,IAAA,GAAA,CAAI,UAAA,CAAW,wBAAA,GACb,GAAA,CAAI,UAAA,CAAW,wBAAA,IAA4B,UAAA;AAC7C,IAAA,GAAA,CAAI,UAAA,CAAW,4BAAA,GACb,GAAA,CAAI,UAAA,CAAW,4BAAA,IAAgC,cAAA;AACjD,IAAA,OAAO,GAAA;AAAA,EACT,CAAC,CAAA;AAGD,EAAA,OAAO,aAAA,CAAc,WAAA,CAAY,eAAA,CAAgB,OAAA,EAAS;AAAA,IACxD,2BAAA;AAAA,IACA;AAAA,GACD,CAAA;AACH,CAAA;AAEA,IAAO,oBAAA,GAAQ","file":"withVideoSdk.js","sourcesContent":["import {\n AndroidConfig,\n withInfoPlist,\n type ConfigPlugin,\n} from \"@expo/config-plugins\";\n\n/**\n * Config plugin de Expo para `payfri-video-sdk`. Inyecta en el prebuild los permisos\n * de cámara y micrófono que exige el sistema operativo para usar getUserMedia\n * dentro del WebView de `<VideoCall>`:\n *\n * - iOS: NSCameraUsageDescription, NSMicrophoneUsageDescription (Info.plist)\n * - Android: CAMERA, RECORD_AUDIO (AndroidManifest)\n *\n * El tenant lo activa en su `app.json` y no toca ningún archivo nativo:\n *\n * { \"plugins\": [[\"payfri-video-sdk\", { \"cameraText\": \"Para videollamadas\" }]] }\n */\nexport interface VideoSdkPluginProps {\n /** Texto del permiso de cámara mostrado por iOS. */\n cameraText?: string;\n /** Texto del permiso de micrófono mostrado por iOS. */\n microphoneText?: string;\n}\n\nconst DEFAULT_CAMERA_TEXT = \"Esta app usa la cámara para las videollamadas.\";\nconst DEFAULT_MICROPHONE_TEXT = \"Esta app usa el micrófono para las videollamadas.\";\n\nconst withVideoSdk: ConfigPlugin<VideoSdkPluginProps | void> = (config, props) => {\n const cameraText = props?.cameraText ?? DEFAULT_CAMERA_TEXT;\n const microphoneText = props?.microphoneText ?? DEFAULT_MICROPHONE_TEXT;\n\n // iOS: no pisar valores que el proyecto ya haya definido.\n const withIos = withInfoPlist(config, (cfg) => {\n cfg.modResults.NSCameraUsageDescription =\n cfg.modResults.NSCameraUsageDescription ?? cameraText;\n cfg.modResults.NSMicrophoneUsageDescription =\n cfg.modResults.NSMicrophoneUsageDescription ?? microphoneText;\n return cfg;\n });\n\n // Android: withPermissions deduplica, así que es seguro re-declararlos.\n return AndroidConfig.Permissions.withPermissions(withIos, [\n \"android.permission.CAMERA\",\n \"android.permission.RECORD_AUDIO\",\n ]);\n};\n\nexport default withVideoSdk;\n"]}
|