@xorgate/react-native 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/CHANGELOG.md +47 -0
- package/LICENSE +21 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/live-video.d.ts +46 -0
- package/dist/live-video.d.ts.map +1 -0
- package/dist/live-video.js +83 -0
- package/dist/live-video.js.map +1 -0
- package/dist/platform.d.ts +32 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +23 -0
- package/dist/platform.js.map +1 -0
- package/dist/polyfills.d.ts +3 -0
- package/dist/polyfills.d.ts.map +1 -0
- package/dist/polyfills.js +56 -0
- package/dist/polyfills.js.map +1 -0
- package/dist/provider.d.ts +19 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +28 -0
- package/dist/provider.js.map +1 -0
- package/dist/use-live-video.d.ts +38 -0
- package/dist/use-live-video.d.ts.map +1 -0
- package/dist/use-live-video.js +49 -0
- package/dist/use-live-video.js.map +1 -0
- package/dist/wake.d.ts +25 -0
- package/dist/wake.d.ts.map +1 -0
- package/dist/wake.js +38 -0
- package/dist/wake.js.map +1 -0
- package/dist/webcrypto.d.ts +45 -0
- package/dist/webcrypto.d.ts.map +1 -0
- package/dist/webcrypto.js +62 -0
- package/dist/webcrypto.js.map +1 -0
- package/dist/webrtc-platform.d.ts +21 -0
- package/dist/webrtc-platform.d.ts.map +1 -0
- package/dist/webrtc-platform.js +61 -0
- package/dist/webrtc-platform.js.map +1 -0
- package/package.json +95 -0
- package/readme.md +312 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wake.d.ts","sourceRoot":"","sources":["../src/wake.ts"],"names":[],"mappings":"AAEA,kFAAkF;AAClF,MAAM,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAC;AAE5D;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAK3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,GAAG,OAAO,EAAE,UAAU,EAAE,GAAG,UAAU,CAOhE"}
|
package/dist/wake.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { AppState } from "react-native";
|
|
2
|
+
/**
|
|
3
|
+
* The foreground edge. This is the wake source a phone actually needs: iOS
|
|
4
|
+
* suspends the app's sockets within seconds of it leaving the screen, so both
|
|
5
|
+
* the MQTT connection and the peer connection are usually dead by the time the
|
|
6
|
+
* user comes back — and the live plane would otherwise sit out a backoff of up
|
|
7
|
+
* to 30 s in front of them. A browser tab has the same problem in miniature;
|
|
8
|
+
* `visibilitychange` is the web's version of this listener.
|
|
9
|
+
*
|
|
10
|
+
* Only the `active` edge counts. "inactive" is the app switcher or a system
|
|
11
|
+
* sheet over the app, where nothing has been torn down yet.
|
|
12
|
+
*/
|
|
13
|
+
export function appStateWake(onWake) {
|
|
14
|
+
const subscription = AppState.addEventListener("change", (state) => {
|
|
15
|
+
if (state === "active")
|
|
16
|
+
onWake();
|
|
17
|
+
});
|
|
18
|
+
return () => subscription.remove();
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Fan one `onWake` out over several sources. Used to add "the network came
|
|
22
|
+
* back" to the foreground edge; see the README for the expo-network and
|
|
23
|
+
* NetInfo recipes, which are four lines each and stay in the consumer so this
|
|
24
|
+
* package needs neither as a dependency.
|
|
25
|
+
*
|
|
26
|
+
* A wake is a hint, never a command: the feeds and the video session ignore
|
|
27
|
+
* one while they are connected or mid-connect, so firing twice costs nothing.
|
|
28
|
+
*/
|
|
29
|
+
export function composeWake(...sources) {
|
|
30
|
+
return (onWake) => {
|
|
31
|
+
const unsubscribes = sources.map((source) => source(onWake));
|
|
32
|
+
return () => {
|
|
33
|
+
for (const unsubscribe of unsubscribes)
|
|
34
|
+
unsubscribe();
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=wake.js.map
|
package/dist/wake.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wake.js","sourceRoot":"","sources":["../src/wake.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAKxC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,MAAkB;IAC7C,MAAM,YAAY,GAAG,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;QACjE,IAAI,KAAK,KAAK,QAAQ;YAAE,MAAM,EAAE,CAAC;IACnC,CAAC,CAAC,CAAC;IACH,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC;AACrC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,GAAG,OAAqB;IAClD,OAAO,CAAC,MAAM,EAAE,EAAE;QAChB,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;QAC7D,OAAO,GAAG,EAAE;YACV,KAAK,MAAM,WAAW,IAAI,YAAY;gBAAE,WAAW,EAAE,CAAC;QACxD,CAAC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
type Bytes = ArrayBuffer | ArrayBufferView;
|
|
2
|
+
export interface HmacKey {
|
|
3
|
+
type: "secret";
|
|
4
|
+
algorithm: {
|
|
5
|
+
name: "HMAC";
|
|
6
|
+
hash: {
|
|
7
|
+
name: "SHA-256";
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
extractable: boolean;
|
|
11
|
+
usages: string[];
|
|
12
|
+
raw: Uint8Array;
|
|
13
|
+
}
|
|
14
|
+
export declare const subtle: {
|
|
15
|
+
digest(algorithm: {
|
|
16
|
+
name: string;
|
|
17
|
+
} | string, data: Bytes): Promise<ArrayBuffer>;
|
|
18
|
+
importKey(format: string, keyData: Bytes, algorithm: {
|
|
19
|
+
name: string;
|
|
20
|
+
hash?: {
|
|
21
|
+
name: string;
|
|
22
|
+
} | string;
|
|
23
|
+
}, extractable: boolean, usages: string[]): Promise<HmacKey>;
|
|
24
|
+
sign(_algorithm: {
|
|
25
|
+
name: string;
|
|
26
|
+
}, key: HmacKey, data: Bytes): Promise<ArrayBuffer>;
|
|
27
|
+
};
|
|
28
|
+
declare const webcrypto: {
|
|
29
|
+
subtle: {
|
|
30
|
+
digest(algorithm: {
|
|
31
|
+
name: string;
|
|
32
|
+
} | string, data: Bytes): Promise<ArrayBuffer>;
|
|
33
|
+
importKey(format: string, keyData: Bytes, algorithm: {
|
|
34
|
+
name: string;
|
|
35
|
+
hash?: {
|
|
36
|
+
name: string;
|
|
37
|
+
} | string;
|
|
38
|
+
}, extractable: boolean, usages: string[]): Promise<HmacKey>;
|
|
39
|
+
sign(_algorithm: {
|
|
40
|
+
name: string;
|
|
41
|
+
}, key: HmacKey, data: Bytes): Promise<ArrayBuffer>;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export default webcrypto;
|
|
45
|
+
//# sourceMappingURL=webcrypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webcrypto.d.ts","sourceRoot":"","sources":["../src/webcrypto.ts"],"names":[],"mappings":"AAuBA,KAAK,KAAK,GAAG,WAAW,GAAG,eAAe,CAAC;AAc3C,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,QAAQ,CAAC;IACf,SAAS,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE;YAAE,IAAI,EAAE,SAAS,CAAA;SAAE,CAAA;KAAE,CAAC;IACvD,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,GAAG,EAAE,UAAU,CAAC;CACjB;AAED,eAAO,MAAM,MAAM;sBACO;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,QAAQ,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC;sBAW3E,MAAM,WACL,KAAK,aACH;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,GAAG,MAAM,CAAA;KAAE,eAChD,OAAO,UACZ,MAAM,EAAE,GACf,OAAO,CAAC,OAAO,CAAC;qBAgBI;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,OAAO,OAAO,QAAQ,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC;CAK1F,CAAC;AAEF,QAAA,MAAM,SAAS;;0BAvCW;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,GAAG,MAAM,QAAQ,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC;0BAW3E,MAAM,WACL,KAAK,aACH;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE;gBAAE,IAAI,EAAE,MAAM,CAAA;aAAE,GAAG,MAAM,CAAA;SAAE,eAChD,OAAO,UACZ,MAAM,EAAE,GACf,OAAO,CAAC,OAAO,CAAC;yBAgBI;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,OAAO,OAAO,QAAQ,KAAK,GAAG,OAAO,CAAC,WAAW,CAAC;;CAO/D,CAAC;AAC7B,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@xorgate/react-native/webcrypto` — the WebCrypto subset the KVS signaling
|
|
3
|
+
* client's SigV4 signer needs, for Metro to alias `isomorphic-webcrypto` at.
|
|
4
|
+
* See README "Metro setup"; it is one line of config and is not imported by
|
|
5
|
+
* anything in this package.
|
|
6
|
+
*
|
|
7
|
+
* Why an alias is needed at all: `amazon-kinesis-video-streams-webrtc` signs
|
|
8
|
+
* its WebSocket URL through `isomorphic-webcrypto`, whose React Native build
|
|
9
|
+
* evaluates to `undefined` on Hermes — its export is assembled behind an
|
|
10
|
+
* msrcrypto PRNG promise, and it also wants `expo-random`, removed from the
|
|
11
|
+
* Expo SDK after 51. The symptom is "Cannot read property 'subtle' of
|
|
12
|
+
* undefined" the first time a viewer connects.
|
|
13
|
+
*
|
|
14
|
+
* Why this is a complete answer rather than a stopgap: the signer makes
|
|
15
|
+
* exactly three calls — `digest(SHA-256)`, `importKey("raw", HMAC)` and
|
|
16
|
+
* `sign(HMAC)` — and all three are deterministic hashing over
|
|
17
|
+
* `@aws-crypto/sha256-js`, which is already in the tree because
|
|
18
|
+
* `presignIotWssUrl` signs with it. The entropy msrcrypto was providing is
|
|
19
|
+
* not used on this path. Anything outside those three calls throws by name
|
|
20
|
+
* instead of returning a wrong answer quietly.
|
|
21
|
+
*/
|
|
22
|
+
import { Sha256 } from "@aws-crypto/sha256-js";
|
|
23
|
+
function toU8(data) {
|
|
24
|
+
if (data instanceof ArrayBuffer)
|
|
25
|
+
return new Uint8Array(data);
|
|
26
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
27
|
+
}
|
|
28
|
+
function toArrayBuffer(bytes) {
|
|
29
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
30
|
+
}
|
|
31
|
+
export const subtle = {
|
|
32
|
+
async digest(algorithm, data) {
|
|
33
|
+
const name = typeof algorithm === "string" ? algorithm : algorithm.name;
|
|
34
|
+
if (name.toUpperCase() !== "SHA-256") {
|
|
35
|
+
throw new Error(`@xorgate/react-native/webcrypto: unsupported digest ${name}`);
|
|
36
|
+
}
|
|
37
|
+
const hash = new Sha256();
|
|
38
|
+
hash.update(toU8(data));
|
|
39
|
+
return toArrayBuffer(await hash.digest());
|
|
40
|
+
},
|
|
41
|
+
async importKey(format, keyData, algorithm, extractable, usages) {
|
|
42
|
+
if (format !== "raw" || algorithm.name.toUpperCase() !== "HMAC") {
|
|
43
|
+
throw new Error(`@xorgate/react-native/webcrypto: unsupported importKey ${format}/${algorithm.name}`);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
type: "secret",
|
|
47
|
+
algorithm: { name: "HMAC", hash: { name: "SHA-256" } },
|
|
48
|
+
extractable,
|
|
49
|
+
usages,
|
|
50
|
+
// Copied: the caller may reuse its buffer for the next signing step.
|
|
51
|
+
raw: toU8(keyData).slice(),
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
async sign(_algorithm, key, data) {
|
|
55
|
+
const hash = new Sha256(key.raw);
|
|
56
|
+
hash.update(toU8(data));
|
|
57
|
+
return toArrayBuffer(await hash.digest());
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const webcrypto = { subtle };
|
|
61
|
+
export default webcrypto;
|
|
62
|
+
//# sourceMappingURL=webcrypto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webcrypto.js","sourceRoot":"","sources":["../src/webcrypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAI/C,SAAS,IAAI,CAAC,IAAW;IACvB,IAAI,IAAI,YAAY,WAAW;QAAE,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC;IAC7D,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,aAAa,CAAC,KAAiB;IACtC,OAAO,KAAK,CAAC,MAAM,CAAC,KAAK,CACvB,KAAK,CAAC,UAAU,EAChB,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CACrB,CAAC;AACnB,CAAC;AAUD,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,KAAK,CAAC,MAAM,CAAC,SAAoC,EAAE,IAAW;QAC5D,MAAM,IAAI,GAAG,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC;QACxE,IAAI,IAAI,CAAC,WAAW,EAAE,KAAK,SAAS,EAAE,CAAC;YACrC,MAAM,IAAI,KAAK,CAAC,uDAAuD,IAAI,EAAE,CAAC,CAAC;QACjF,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,MAAM,EAAE,CAAC;QAC1B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACxB,OAAO,aAAa,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,SAAS,CACb,MAAc,EACd,OAAc,EACd,SAA6D,EAC7D,WAAoB,EACpB,MAAgB;QAEhB,IAAI,MAAM,KAAK,KAAK,IAAI,SAAS,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,MAAM,EAAE,CAAC;YAChE,MAAM,IAAI,KAAK,CACb,0DAA0D,MAAM,IAAI,SAAS,CAAC,IAAI,EAAE,CACrF,CAAC;QACJ,CAAC;QACD,OAAO;YACL,IAAI,EAAE,QAAQ;YACd,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE;YACtD,WAAW;YACX,MAAM;YACN,qEAAqE;YACrE,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE;SAC3B,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,UAA4B,EAAE,GAAY,EAAE,IAAW;QAChE,MAAM,IAAI,GAAG,IAAI,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACxB,OAAO,aAAa,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5C,CAAC;CACF,CAAC;AAEF,MAAM,SAAS,GAAG,EAAE,MAAM,EAAE,CAAC;AAC7B,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { WebRtcPlatform } from "@xorgate/react";
|
|
2
|
+
/**
|
|
3
|
+
* The KVS viewer's platform on React Native.
|
|
4
|
+
*
|
|
5
|
+
* Only two of the five members differ from the browser: the peer connection
|
|
6
|
+
* (react-native-webrtc's, which is a real native PeerConnection with hardware
|
|
7
|
+
* H.264 decode behind it) and the id entropy (Hermes has no `crypto` global).
|
|
8
|
+
* The three AWS bodies — `getViewerEndpoints`, `getIceServers` and the lazy
|
|
9
|
+
* `createSignaling` facade — are `@xorgate/react`'s own: the AWS SDK v3
|
|
10
|
+
* clients and `amazon-kinesis-video-streams-webrtc` both run on Hermes once
|
|
11
|
+
* Metro resolves them (see README "Metro setup"), so copying them here would
|
|
12
|
+
* only mean two implementations of the hardened signaling path. The one
|
|
13
|
+
* addition is the missing-`crypto` guard below, in front of the first of
|
|
14
|
+
* them.
|
|
15
|
+
*
|
|
16
|
+
* `iceTransportPolicy: "all"` matters on a phone: a viewer on a carrier NAT
|
|
17
|
+
* frequently has no working host or srflx path to the device and lands on the
|
|
18
|
+
* TURN relay AWS hands out with the ICE server list.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createNativeWebRtcPlatform(randomId: () => string): WebRtcPlatform;
|
|
21
|
+
//# sourceMappingURL=webrtc-platform.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webrtc-platform.d.ts","sourceRoot":"","sources":["../src/webrtc-platform.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAsB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAGzE;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,MAAM,GAAG,cAAc,CAiBjF"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createWebRtcPlatform } from "@xorgate/react";
|
|
2
|
+
import { RTCPeerConnection } from "react-native-webrtc";
|
|
3
|
+
/**
|
|
4
|
+
* The KVS viewer's platform on React Native.
|
|
5
|
+
*
|
|
6
|
+
* Only two of the five members differ from the browser: the peer connection
|
|
7
|
+
* (react-native-webrtc's, which is a real native PeerConnection with hardware
|
|
8
|
+
* H.264 decode behind it) and the id entropy (Hermes has no `crypto` global).
|
|
9
|
+
* The three AWS bodies — `getViewerEndpoints`, `getIceServers` and the lazy
|
|
10
|
+
* `createSignaling` facade — are `@xorgate/react`'s own: the AWS SDK v3
|
|
11
|
+
* clients and `amazon-kinesis-video-streams-webrtc` both run on Hermes once
|
|
12
|
+
* Metro resolves them (see README "Metro setup"), so copying them here would
|
|
13
|
+
* only mean two implementations of the hardened signaling path. The one
|
|
14
|
+
* addition is the missing-`crypto` guard below, in front of the first of
|
|
15
|
+
* them.
|
|
16
|
+
*
|
|
17
|
+
* `iceTransportPolicy: "all"` matters on a phone: a viewer on a carrier NAT
|
|
18
|
+
* frequently has no working host or srflx path to the device and lands on the
|
|
19
|
+
* TURN relay AWS hands out with the ICE server list.
|
|
20
|
+
*/
|
|
21
|
+
export function createNativeWebRtcPlatform(randomId) {
|
|
22
|
+
const platform = createWebRtcPlatform({
|
|
23
|
+
randomId,
|
|
24
|
+
createPeerConnection: (iceServers) => new RTCPeerConnection({
|
|
25
|
+
iceServers: iceServers,
|
|
26
|
+
iceTransportPolicy: "all",
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
...platform,
|
|
31
|
+
async getViewerEndpoints(channelRef, region, credentials) {
|
|
32
|
+
assertCryptoGlobal();
|
|
33
|
+
return platform.getViewerEndpoints(channelRef, region, credentials);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The AWS SDK needs a `crypto` global, and Hermes has none.
|
|
39
|
+
*
|
|
40
|
+
* `@smithy/core`'s React Native serde bundle ends its UUID v4 path in
|
|
41
|
+
* `const _getRandomValues = (array) => crypto.getRandomValues(array)` — a BARE
|
|
42
|
+
* `crypto` — and that is the fallback `bindV4` selects precisely when
|
|
43
|
+
* `typeof crypto === "undefined"`, so the guard hands control to the one line
|
|
44
|
+
* that cannot survive the case it guards against. Without the polyfill the
|
|
45
|
+
* first signaling call throws `Property 'crypto' doesn't exist`, which the
|
|
46
|
+
* viewer session reads as a network failure and retries forever.
|
|
47
|
+
*
|
|
48
|
+
* Checked here, at the first AWS call, rather than at platform construction:
|
|
49
|
+
* throwing while the provider mounts would take the whole app down over a
|
|
50
|
+
* feature the user may not have opened. This way the message lands in the
|
|
51
|
+
* session's `error` and says what to do.
|
|
52
|
+
*/
|
|
53
|
+
function assertCryptoGlobal() {
|
|
54
|
+
const c = globalThis.crypto;
|
|
55
|
+
if (c && typeof c.getRandomValues === "function")
|
|
56
|
+
return;
|
|
57
|
+
throw new Error("Live video needs a `crypto` global, which React Native does not have: the AWS SDK " +
|
|
58
|
+
'calls crypto.getRandomValues(). Add `import "@xorgate/react-native/polyfills"` to the ' +
|
|
59
|
+
"top of your entry file. (Live telemetry does not need it.)");
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=webrtc-platform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webrtc-platform.js","sourceRoot":"","sources":["../src/webrtc-platform.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,0BAA0B,CAAC,QAAsB;IAC/D,MAAM,QAAQ,GAAG,oBAAoB,CAAC;QACpC,QAAQ;QACR,oBAAoB,EAAE,CAAC,UAAU,EAAsB,EAAE,CACvD,IAAI,iBAAiB,CAAC;YACpB,UAAU,EAAE,UAAmB;YAC/B,kBAAkB,EAAE,KAAK;SAC1B,CAAkC;KACtC,CAAC,CAAC;IAEH,OAAO;QACL,GAAG,QAAQ;QACX,KAAK,CAAC,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW;YACtD,kBAAkB,EAAE,CAAC;YACrB,OAAO,QAAQ,CAAC,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC;QACtE,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,SAAS,kBAAkB;IACzB,MAAM,CAAC,GAAI,UAAyD,CAAC,MAAM,CAAC;IAC5E,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,eAAe,KAAK,UAAU;QAAE,OAAO;IACzD,MAAM,IAAI,KAAK,CACb,oFAAoF;QAClF,wFAAwF;QACxF,4DAA4D,CAC/D,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xorgate/react-native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React Native adapters for @xorgate/react: live telemetry over MQTT, live video over WebRTC (react-native-webrtc / RTCView), and the recorded-media replay player over expo-video.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Epye Labs",
|
|
7
|
+
"homepage": "https://docs.xorgate.io/docs/frontend-sdk",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/epyelabs/xorgate-react-native.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/epyelabs/xorgate-react-native/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"xorgate",
|
|
17
|
+
"react-native",
|
|
18
|
+
"expo",
|
|
19
|
+
"hooks",
|
|
20
|
+
"iot",
|
|
21
|
+
"telemetry",
|
|
22
|
+
"webrtc",
|
|
23
|
+
"video",
|
|
24
|
+
"replay"
|
|
25
|
+
],
|
|
26
|
+
"type": "module",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"sideEffects": [
|
|
31
|
+
"./dist/polyfills.js"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/index.js",
|
|
40
|
+
"default": "./dist/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./polyfills": {
|
|
43
|
+
"types": "./dist/polyfills.d.ts",
|
|
44
|
+
"import": "./dist/polyfills.js",
|
|
45
|
+
"default": "./dist/polyfills.js"
|
|
46
|
+
},
|
|
47
|
+
"./package.json": "./package.json",
|
|
48
|
+
"./webcrypto": {
|
|
49
|
+
"types": "./dist/webcrypto.d.ts",
|
|
50
|
+
"import": "./dist/webcrypto.js",
|
|
51
|
+
"default": "./dist/webcrypto.js"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"module": "./dist/index.js",
|
|
55
|
+
"types": "./dist/index.d.ts",
|
|
56
|
+
"files": [
|
|
57
|
+
"dist",
|
|
58
|
+
"README.md",
|
|
59
|
+
"CHANGELOG.md",
|
|
60
|
+
"LICENSE"
|
|
61
|
+
],
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
64
|
+
"ci": "npm run typecheck && npm test && npm run build && npm run verify:tarball",
|
|
65
|
+
"clean": "rm -rf dist .tmp",
|
|
66
|
+
"prepack": "npm run build",
|
|
67
|
+
"test": "vitest run",
|
|
68
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
69
|
+
"verify:tarball": "node scripts/verify-tarball.mjs"
|
|
70
|
+
},
|
|
71
|
+
"peerDependencies": {
|
|
72
|
+
"expo-crypto": ">=13.0.0",
|
|
73
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
74
|
+
"react-native": ">=0.76.0",
|
|
75
|
+
"react-native-webrtc": ">=124.0.0"
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"@aws-crypto/sha256-js": "^5.2.0",
|
|
79
|
+
"@xorgate/react": "^0.2.0-next.1"
|
|
80
|
+
},
|
|
81
|
+
"devDependencies": {
|
|
82
|
+
"@testing-library/react": "^16.3.3",
|
|
83
|
+
"@types/node": "^22.10.2",
|
|
84
|
+
"@types/react": "^19.0.0",
|
|
85
|
+
"@types/react-dom": "^19.3.0",
|
|
86
|
+
"expo-crypto": "~57.0.3",
|
|
87
|
+
"jsdom": "^25.0.1",
|
|
88
|
+
"react": "^19.2.6",
|
|
89
|
+
"react-dom": "^19.3.0",
|
|
90
|
+
"react-native": "^0.86.3",
|
|
91
|
+
"react-native-webrtc": "^124.0.8",
|
|
92
|
+
"typescript": "^5.7.2",
|
|
93
|
+
"vitest": "^3.0.0"
|
|
94
|
+
}
|
|
95
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# @xorgate/react-native
|
|
2
|
+
|
|
3
|
+
React Native adapters for [`@xorgate/react`](https://www.npmjs.com/package/@xorgate/react).
|
|
4
|
+
Live telemetry over MQTT-over-WebSocket and live video over WebRTC, rendered by
|
|
5
|
+
`react-native-webrtc`'s `RTCView` — a real native video view with hardware
|
|
6
|
+
H.264 decode behind it, not a web player in a shell.
|
|
7
|
+
|
|
8
|
+
Everything pure or platform-neutral — the KVS session hardening, the telemetry
|
|
9
|
+
feed, the timeline, lanes, the replay clock, the telemetry math, the data hooks
|
|
10
|
+
— lives in `@xorgate/react` and is **re-exported from here**, so your app
|
|
11
|
+
imports from one package. What this package adds is the runtime underneath
|
|
12
|
+
(`nativePlatform`, `XorgateNativeProvider`) and the two things that cannot be
|
|
13
|
+
platform-neutral because they are pixels.
|
|
14
|
+
|
|
15
|
+
**0.1.0 ships telemetry and live video.** Replay over `expo-video` is 0.2.0.
|
|
16
|
+
|
|
17
|
+
**Full reference: [docs.xorgate.io/docs/frontend-sdk](https://docs.xorgate.io/docs/frontend-sdk)**
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx expo install @xorgate/react-native react-native-webrtc @config-plugins/react-native-webrtc expo-crypto
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Peers: `react`, `react-native`, `react-native-webrtc`, `expo-crypto`.
|
|
26
|
+
`@xorgate/react` is a dependency, not a peer — one resolved copy is what makes
|
|
27
|
+
`instanceof XorgateError` hold across the boundary.
|
|
28
|
+
|
|
29
|
+
`expo-crypto` is required, not optional: Hermes ships **no `crypto` global at
|
|
30
|
+
all** — not `randomUUID`, not `getRandomValues`, not `subtle` — and every MQTT
|
|
31
|
+
and signaling client id needs entropy, so there is nothing else to default to.
|
|
32
|
+
|
|
33
|
+
Add the config plugin to `app.json` / `app.config.ts`:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{ "plugins": ["@config-plugins/react-native-webrtc"] }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**A development build is required.** `react-native-webrtc` is a native module,
|
|
40
|
+
so Expo Go cannot run it.
|
|
41
|
+
|
|
42
|
+
The plugin writes both an `NSCameraUsageDescription` and an
|
|
43
|
+
`NSMicrophoneUsageDescription`, and on Android adds `CAMERA`, `RECORD_AUDIO`,
|
|
44
|
+
`MODIFY_AUDIO_SETTINGS` and Bluetooth permissions. A **viewer** app uses none
|
|
45
|
+
of them, but Apple's upload check (ITMS-90683) still wants a microphone string
|
|
46
|
+
from any binary that links the microphone APIs, which WebRTC does. Set an
|
|
47
|
+
honest one rather than shipping the plugin's placeholder, and strip the Android
|
|
48
|
+
permissions you do not use:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
plugins: [["@config-plugins/react-native-webrtc", {
|
|
52
|
+
cameraPermission: "…",
|
|
53
|
+
microphonePermission: "This app views live video only and does not record audio.",
|
|
54
|
+
}]],
|
|
55
|
+
android: { blockedPermissions: ["android.permission.RECORD_AUDIO", "android.permission.MODIFY_AUDIO_SETTINGS"] },
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Metro setup
|
|
59
|
+
|
|
60
|
+
`@xorgate/react`'s AWS / KVS / mqtt tree bundles under Metro with four
|
|
61
|
+
resolver rules. They are mechanical and package-agnostic — they read each
|
|
62
|
+
package's own `package.json` — so paste this into `metro.config.js` once:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
const fs = require("node:fs")
|
|
66
|
+
const path = require("node:path")
|
|
67
|
+
const { getDefaultConfig } = require("expo/metro-config")
|
|
68
|
+
|
|
69
|
+
const config = getDefaultConfig(__dirname)
|
|
70
|
+
|
|
71
|
+
const AWS_SCOPES = ["@aws-sdk/", "@smithy/"]
|
|
72
|
+
const esEntry = new Map()
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* (1) `@aws-sdk/*` and `@smithy/*` packages that ship NO `exports` map fall
|
|
76
|
+
* through to `main` = `dist-cjs`, which requires `node:https` / `node:stream`.
|
|
77
|
+
* Their `react-native` and `browser` fields are sub-path maps, not entry
|
|
78
|
+
* points, so Metro cannot use them: redirect the bare request to the `module`
|
|
79
|
+
* entry (`dist-es`). Packages that DO have `exports` (`@smithy/core`,
|
|
80
|
+
* `@aws-sdk/core`, `@aws-sdk/nested-clients`) resolve correctly and must be
|
|
81
|
+
* left alone — `@smithy/core` actively refuses `./dist-es/index.js`.
|
|
82
|
+
*/
|
|
83
|
+
function awsEsEntry(name) {
|
|
84
|
+
if (!AWS_SCOPES.some((s) => name.startsWith(s))) return null
|
|
85
|
+
if (name.split("/").length !== 2) return null // sub-path imports are fine
|
|
86
|
+
if (esEntry.has(name)) return esEntry.get(name)
|
|
87
|
+
let result = null
|
|
88
|
+
try {
|
|
89
|
+
const dir = path.dirname(require.resolve(`${name}/package.json`, { paths: [__dirname] }))
|
|
90
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"))
|
|
91
|
+
if (!pkg.exports && typeof pkg.module === "string") result = path.join(dir, pkg.module)
|
|
92
|
+
} catch {
|
|
93
|
+
result = null
|
|
94
|
+
}
|
|
95
|
+
esEntry.set(name, result)
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const previous = config.resolver.resolveRequest
|
|
100
|
+
|
|
101
|
+
config.resolver.resolveRequest = (context, moduleName, platform) => {
|
|
102
|
+
const es = awsEsEntry(moduleName)
|
|
103
|
+
if (es) return { type: "sourceFile", filePath: es }
|
|
104
|
+
|
|
105
|
+
// (2) `./runtimeConfig` from inside a `dist-es` directory -> the
|
|
106
|
+
// `runtimeConfig.native.js` sibling, which is what the package's
|
|
107
|
+
// `react-native` sub-path map would have chosen. Without it the AWS clients
|
|
108
|
+
// come up with the Node runtime config instead of `FetchHttpHandler`.
|
|
109
|
+
if (moduleName === "./runtimeConfig" && context.originModulePath.includes(`${path.sep}dist-es${path.sep}`)) {
|
|
110
|
+
const native = path.join(path.dirname(context.originModulePath), "runtimeConfig.native.js")
|
|
111
|
+
if (fs.existsSync(native)) return { type: "sourceFile", filePath: native }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// (3) The KVS signaling client does `new (WebSocket || require("ws"))(url)`.
|
|
115
|
+
// React Native has a global `WebSocket`, so the Node implementation only has
|
|
116
|
+
// to RESOLVE; it never runs.
|
|
117
|
+
if (moduleName === "ws") {
|
|
118
|
+
return { type: "sourceFile", filePath: require.resolve("ws/browser.js", { paths: [__dirname] }) }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// (4) `isomorphic-webcrypto` -> this package's WebCrypto subset. The real
|
|
122
|
+
// package's React Native build evaluates to `undefined` on Hermes (its
|
|
123
|
+
// export is assembled behind an msrcrypto PRNG promise, and it wants
|
|
124
|
+
// `expo-random`, gone from the Expo SDK after 51), so the KVS signaling
|
|
125
|
+
// client dies with "Cannot read property 'subtle' of undefined" the first
|
|
126
|
+
// time a viewer connects.
|
|
127
|
+
if (moduleName === "isomorphic-webcrypto") {
|
|
128
|
+
return {
|
|
129
|
+
type: "sourceFile",
|
|
130
|
+
filePath: require.resolve("@xorgate/react-native/webcrypto", { paths: [__dirname] }),
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return (previous ?? context.resolveRequest)(context, moduleName, platform)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = config
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`@xorgate/react-native/webcrypto` covers exactly the three calls the SigV4
|
|
141
|
+
signer makes — `digest(SHA-256)`, `importKey("raw", HMAC)`, `sign(HMAC)` —
|
|
142
|
+
over `@aws-crypto/sha256-js`, which is already in the tree. All three are
|
|
143
|
+
deterministic hashing, so the entropy msrcrypto was adding is not used on this
|
|
144
|
+
path; anything outside those three throws by name rather than answering wrong.
|
|
145
|
+
|
|
146
|
+
Nothing else was needed on Expo SDK 57 / Hermes: no `buffer`, `process`,
|
|
147
|
+
`events`, `url`, stream or URL polyfill, measured rather than assumed.
|
|
148
|
+
|
|
149
|
+
A global `resolverMainFields: ["module", …]` is **not** a shortcut for rule 1:
|
|
150
|
+
it breaks `react-native-safe-area-context`'s codegen spec.
|
|
151
|
+
|
|
152
|
+
## Polyfill
|
|
153
|
+
|
|
154
|
+
Add one line at the very top of your entry file, before anything that reaches
|
|
155
|
+
the SDK:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import "@xorgate/react-native/polyfills"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
It installs `getRandomValues` and `randomUUID` from `expo-crypto` onto
|
|
162
|
+
`globalThis.crypto`, additively (an existing `crypto` keeps everything it
|
|
163
|
+
already has).
|
|
164
|
+
|
|
165
|
+
**Live video does not work without it.** Not because of this package —
|
|
166
|
+
`nativePlatform()` covers the SDK's own entropy through the `randomId` slot,
|
|
167
|
+
and live telemetry runs with no polyfill at all — but because of the AWS SDK
|
|
168
|
+
underneath the KVS viewer. `@smithy/core`'s React Native serde bundle ends its
|
|
169
|
+
UUID v4 path in
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
const _getRandomValues = (array) => crypto.getRandomValues(array)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
with a **bare** `crypto`, and that is the fallback `bindV4` selects precisely
|
|
176
|
+
when `typeof crypto === "undefined"` — so the guard hands control to the one
|
|
177
|
+
line that cannot survive the case it guards against. On Hermes the first
|
|
178
|
+
`GetSignalingChannelEndpoint` call then throws `Property 'crypto' doesn't
|
|
179
|
+
exist`, the session treats it as a network error, and the viewer retries
|
|
180
|
+
forever on a backoff. (Measured on Expo SDK 57 / Hermes, `@smithy/core` 3.34.1,
|
|
181
|
+
2026-09-15. The same bug was fixed for the Node build in
|
|
182
|
+
smithy-typescript#2022; the browser and React Native builds were not changed —
|
|
183
|
+
smithy-typescript#2276.)
|
|
184
|
+
|
|
185
|
+
## Use
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
import { XorgateNativeProvider } from "@xorgate/react-native"
|
|
189
|
+
|
|
190
|
+
<XorgateNativeProvider
|
|
191
|
+
key={organizationId ?? "none"} // remounting is how you switch tenants
|
|
192
|
+
auth={{ getLiveCredentials }}
|
|
193
|
+
organizationId={organizationId}
|
|
194
|
+
>
|
|
195
|
+
{children}
|
|
196
|
+
</XorgateNativeProvider>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Same props as `@xorgate/react`'s `XorgateProvider`, plus the native slots
|
|
200
|
+
(`randomId`, `wake`, `mqttConnect`, and a `platform` escape hatch).
|
|
201
|
+
|
|
202
|
+
### Live telemetry
|
|
203
|
+
|
|
204
|
+
Unchanged from the web package — `mqtt.js` 5 runs on React Native's global
|
|
205
|
+
`WebSocket` as it is:
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
const { latest, history, status, receivedAt } = useLiveTelemetry(deviceId)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Read freshness off `receivedAt`, not `status`. A phone that loses signal keeps
|
|
212
|
+
a socket that reads `connected` until the OS surfaces the close, which can take
|
|
213
|
+
20 s or more; the age of the last frame is the honest signal, and the web app
|
|
214
|
+
treats it the same way.
|
|
215
|
+
|
|
216
|
+
### Live video
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
import { LiveVideo, useVideoChannels } from "@xorgate/react-native"
|
|
220
|
+
|
|
221
|
+
const { data } = useVideoChannels(deviceId)
|
|
222
|
+
const cam0 = data?.find((c) => c.streamKey === "cam0") ?? null
|
|
223
|
+
|
|
224
|
+
<LiveVideo channel={cam0} style={{ aspectRatio: 16 / 9 }} />
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`useVideoChannels` is a REST read, so it needs an auth mode that HAS a REST
|
|
228
|
+
credential. An app on `auth={{ getLiveCredentials }}` — the vended-direct mode,
|
|
229
|
+
where your own backend mints the credential and your users have no xorgate
|
|
230
|
+
identity — gets `INVALID_CONFIG` from it by design, and should fetch the
|
|
231
|
+
channel list from its own API and hand the `VideoChannel` to `LiveVideo`
|
|
232
|
+
directly. The live and replay planes are the parts that mode reaches.
|
|
233
|
+
|
|
234
|
+
`LiveVideo` is the view and nothing else: no card, no spinner, no status text,
|
|
235
|
+
no aspect ratio of its own. Take `onStatusChange`, `onStats`,
|
|
236
|
+
`onVideoDimensions` and `onDisplayedStreamChange` for chrome of your own, or
|
|
237
|
+
drop to the hook:
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
const { streamURL, status, stats, error } = useLiveVideo(cam0)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
One behaviour `LiveVideo` adds over a bare `RTCView`: **the reconnect hold**.
|
|
244
|
+
Every reconnect produces a new `MediaStream` with a new `streamURL`, and an
|
|
245
|
+
`RTCView` pointed at a stream that has not decoded a frame is black — so a
|
|
246
|
+
short blip would blank the picture, where a browser's `<video>` simply keeps
|
|
247
|
+
its last frame. `LiveVideo` mounts the new stream underneath at zero opacity
|
|
248
|
+
and promotes it only once the renderer reports its dimensions, falling back to
|
|
249
|
+
the swap after 3 s so a dead stream cannot pin a stale frame. Render `RTCView`
|
|
250
|
+
yourself and you own that.
|
|
251
|
+
|
|
252
|
+
### Waking the live plane
|
|
253
|
+
|
|
254
|
+
The foreground edge (`AppState` → `active`) is wired for you, and on a phone it
|
|
255
|
+
matters more than anything else in this package: iOS suspends the app's sockets
|
|
256
|
+
within seconds of it leaving the screen, so both the MQTT connection and the
|
|
257
|
+
peer connection are usually dead when the user comes back. Without the nudge
|
|
258
|
+
the live plane sits out a backoff of up to 30 s in front of them.
|
|
259
|
+
|
|
260
|
+
Add "the network came back" — four lines, and it stays in your app so this
|
|
261
|
+
package depends on neither network library:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import * as Network from "expo-network"
|
|
265
|
+
|
|
266
|
+
const networkWake = (onWake: () => void) => {
|
|
267
|
+
const sub = Network.addNetworkStateListener((s) => {
|
|
268
|
+
if (s.isInternetReachable ?? s.isConnected) onWake()
|
|
269
|
+
})
|
|
270
|
+
return () => sub.remove()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
<XorgateNativeProvider wake={[networkWake]} …>
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
With `@react-native-community/netinfo`:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
const networkWake = (onWake: () => void) =>
|
|
280
|
+
NetInfo.addEventListener((s) => { if (s.isConnected) onWake() })
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
A wake is a hint, never a command: a feed that is connected or mid-connect
|
|
284
|
+
ignores one, so firing twice costs nothing.
|
|
285
|
+
|
|
286
|
+
## What is not here
|
|
287
|
+
|
|
288
|
+
`@xorgate/react`'s browser-only exports — its `useLiveVideo`, `LiveVideo`,
|
|
289
|
+
`useReplayPlayer`, `ReplayVideo` and the `browser*` platform helpers — are
|
|
290
|
+
deliberately not re-exported: they need `HTMLVideoElement`, `window` or Media
|
|
291
|
+
Source Extensions. `useReplayPlayerCore`, the timeline, the lanes and the clock
|
|
292
|
+
**are** re-exported; the `expo-video` engine that binds them to a player
|
|
293
|
+
arrives in 0.2.0.
|
|
294
|
+
|
|
295
|
+
## Scripts
|
|
296
|
+
|
|
297
|
+
`npm run ci` = `typecheck` → `test` → `build` → `verify:tarball` (pack, install
|
|
298
|
+
into an empty project, import it as a consumer with stub peers, check the
|
|
299
|
+
export surface, scan for secret material).
|
|
300
|
+
|
|
301
|
+
The suite runs under jsdom against doubles for `react-native` and
|
|
302
|
+
`react-native-webrtc`, because neither has a Node build. It proves the wiring —
|
|
303
|
+
which listener is attached, which prop the view gets, what happens across a
|
|
304
|
+
reconnect — and nothing about pixels or hardware decode, which are measured by
|
|
305
|
+
hand on a simulator and a phone.
|
|
306
|
+
|
|
307
|
+
Publishing is manual (`npm publish --tag next` for pre-releases, `npm publish`
|
|
308
|
+
for a final); CI has no publish job on purpose.
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
MIT © Epye Labs
|