@poolse/react-native-calls 0.2.0 → 0.4.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/dev/poolse/callcore/android/CallCore.kt +25 -8
- package/android/src/main/java/dev/poolse/callcore/android/CallVideo.kt +36 -0
- package/android/src/main/java/dev/poolse/callcore/android/SnapshotJson.kt +15 -0
- package/android/src/main/java/expo/modules/poolsecalls/PoolseCallVideoView.kt +64 -0
- package/android/src/main/java/expo/modules/poolsecalls/PoolseCallsModule.kt +10 -0
- package/android/src/main/livekit/java/dev/poolse/callcore/livekit/LiveKitMediaPort.kt +142 -14
- package/android/src/main/livekit/java/dev/poolse/callcore/livekit/LiveKitVideoSurface.kt +107 -0
- package/dist/index.cjs +34 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -2
- package/dist/index.d.ts +44 -2
- package/dist/index.js +33 -2
- package/dist/index.js.map +1 -1
- package/expo-module.config.json +6 -1
- package/ios/Core/CallCoreRuntime.swift +393 -0
- package/ios/Core/CallHttpClient.swift +210 -0
- package/ios/Core/CallMachine.swift +304 -0
- package/ios/Core/PORTS_READY.md +93 -0
- package/ios/Core/Ports.swift +193 -0
- package/ios/Core/SnapshotEncoding.swift +75 -0
- package/ios/Module/PoolseCallVideoView.swift +62 -0
- package/ios/Module/PoolseCallsAppDelegateSubscriber.swift +16 -0
- package/ios/Module/PoolseCallsModule.swift +111 -0
- package/ios/Platform/CallCore.swift +464 -0
- package/ios/Platform/CallKitController.swift +263 -0
- package/ios/Platform/LiveKitMediaPort.swift +445 -0
- package/ios/Platform/LiveKitVideoSurface.swift +67 -0
- package/ios/Platform/Ringback.swift +61 -0
- package/ios/Platform/VoipPush.swift +172 -0
- package/ios/PoolseCalls.podspec +64 -0
- package/package.json +7 -1
- package/src/expo-modules-core.d.ts +5 -0
- package/src/index.ts +85 -2
- package/src/react-shims.d.ts +22 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import CallKit
|
|
2
|
+
import Foundation
|
|
3
|
+
import PushKit
|
|
4
|
+
|
|
5
|
+
/// Native PushKit intake — the iOS twin of CallMessagingService. The
|
|
6
|
+
/// registry is created at launch (PoolseCallsAppDelegateSubscriber →
|
|
7
|
+
/// `PoolseCallsSetup.registerVoipEarly()`), long before JS, so a VoIP push that
|
|
8
|
+
/// wakes a killed app is reported to CallKit natively within PushKit's
|
|
9
|
+
/// budget.
|
|
10
|
+
///
|
|
11
|
+
/// Contract with iOS: EVERY VoIP push reports a call to CallKit
|
|
12
|
+
/// synchronously, before `completion()` — stale, duplicate, control or
|
|
13
|
+
/// unparseable ones included (reported, then ended `.failed`). Breaking
|
|
14
|
+
/// it gets the app killed and, repeated, VoIP delivery revoked.
|
|
15
|
+
final class VoipPush: NSObject, PKPushRegistryDelegate {
|
|
16
|
+
static let shared = VoipPush()
|
|
17
|
+
|
|
18
|
+
private var registry: PKPushRegistry?
|
|
19
|
+
private let lock = NSLock()
|
|
20
|
+
private var _token: String?
|
|
21
|
+
|
|
22
|
+
/// The current VoIP token (hex), readable from any thread.
|
|
23
|
+
var token: String? {
|
|
24
|
+
lock.lock(); defer { lock.unlock() }
|
|
25
|
+
return _token
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// The bridge's token subscriber (process-level, like CallCore.listener).
|
|
29
|
+
static var tokenListener: ((String) -> Void)?
|
|
30
|
+
|
|
31
|
+
/// Idempotent. Main thread.
|
|
32
|
+
func register() {
|
|
33
|
+
guard registry == nil else { return }
|
|
34
|
+
_ = CallKitController.shared // the provider must exist before any push
|
|
35
|
+
let r = PKPushRegistry(queue: .main)
|
|
36
|
+
r.delegate = self
|
|
37
|
+
r.desiredPushTypes = [.voIP]
|
|
38
|
+
registry = r
|
|
39
|
+
if let data = r.pushToken(for: .voIP) { setToken(data) }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private func setToken(_ data: Data) {
|
|
43
|
+
let hex = data.map { String(format: "%02x", $0) }.joined()
|
|
44
|
+
lock.lock(); _token = hex; lock.unlock()
|
|
45
|
+
VoipPush.tokenListener?(hex)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// MARK: - PKPushRegistryDelegate
|
|
49
|
+
|
|
50
|
+
func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) {
|
|
51
|
+
guard type == .voIP else { return }
|
|
52
|
+
setToken(pushCredentials.token)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
|
|
56
|
+
guard type == .voIP else { return }
|
|
57
|
+
lock.lock(); _token = nil; lock.unlock()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload,
|
|
61
|
+
for type: PKPushType, completion: @escaping () -> Void) {
|
|
62
|
+
if type == .voIP { handle(payload.dictionaryPayload) }
|
|
63
|
+
completion()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// MARK: - handling
|
|
67
|
+
|
|
68
|
+
private func handle(_ raw: [AnyHashable: Any]) {
|
|
69
|
+
let ck = CallKitController.shared
|
|
70
|
+
let p = VoipPayload(raw)
|
|
71
|
+
let core = CallCore.getOrNull()
|
|
72
|
+
|
|
73
|
+
switch p.event {
|
|
74
|
+
case "ring_ended", "finished":
|
|
75
|
+
// A control push on the VoIP channel still has to report a call.
|
|
76
|
+
ck.reportAndFail(callId: p.callId, title: p.title)
|
|
77
|
+
guard let core = core, let callId = p.callId else { return }
|
|
78
|
+
if p.event == "ring_ended" { core.onRingEnded(callId: callId, reason: p.reason ?? "finished") }
|
|
79
|
+
else { core.onCallFinished(callId: callId, reason: p.reason) }
|
|
80
|
+
default:
|
|
81
|
+
guard let ring = p.ringContext() else {
|
|
82
|
+
ck.reportAndFail(callId: p.callId, title: p.title)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
guard let core = core, core.isRingLive(ring) else {
|
|
86
|
+
ck.reportAndFail(callId: ring.callId, title: p.title)
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
ck.reportIncoming(ring: ring)
|
|
90
|
+
core.onRing(ring)
|
|
91
|
+
// A ring the machine did not take (busy, superseded) must not stay
|
|
92
|
+
// on the CallKit screen: reconcile once the ring is processed.
|
|
93
|
+
core.reconcileCallKitSoon()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Lenient VoIP payload reader: top-level keys, or nested under `data` /
|
|
99
|
+
/// `payload` / `call`; strings, numbers or bools for any field.
|
|
100
|
+
struct VoipPayload {
|
|
101
|
+
private let fields: [String: Any]
|
|
102
|
+
|
|
103
|
+
init(_ raw: [AnyHashable: Any]) {
|
|
104
|
+
var out: [String: Any] = [:]
|
|
105
|
+
for nest in ["data", "payload", "call"] {
|
|
106
|
+
if let inner = raw[nest] as? [AnyHashable: Any] {
|
|
107
|
+
for (k, v) in inner { if let k = k as? String { out[k] = v } }
|
|
108
|
+
} else if let s = raw[nest] as? String, let d = s.data(using: .utf8),
|
|
109
|
+
let inner = (try? JSONSerialization.jsonObject(with: d)) as? [String: Any] {
|
|
110
|
+
for (k, v) in inner { out[k] = v }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (k, v) in raw { if let k = k as? String, !(v is [AnyHashable: Any]) { out[k] = v } }
|
|
114
|
+
fields = out
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
func string(_ key: String) -> String? {
|
|
118
|
+
switch fields[key] {
|
|
119
|
+
case let s as String: return s.isEmpty ? nil : s
|
|
120
|
+
case let n as NSNumber: return n.stringValue
|
|
121
|
+
default: return nil
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func int64(_ key: String) -> Int64? {
|
|
126
|
+
switch fields[key] {
|
|
127
|
+
case let n as NSNumber: return n.int64Value
|
|
128
|
+
case let s as String: return Int64(s) ?? Double(s).map { Int64($0) }
|
|
129
|
+
default: return nil
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
func bool(_ key: String) -> Bool? {
|
|
134
|
+
switch fields[key] {
|
|
135
|
+
case let n as NSNumber: return n.boolValue
|
|
136
|
+
case let s as String: return ["1", "true", "yes"].contains(s.lowercased())
|
|
137
|
+
default: return nil
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
var callId: String? { string("call_id") }
|
|
142
|
+
var event: String? { string("event") }
|
|
143
|
+
var reason: String? { string("reason") }
|
|
144
|
+
var title: String? { string("conversation_name") ?? string("caller_name") }
|
|
145
|
+
|
|
146
|
+
var media: String {
|
|
147
|
+
if let m = string("media")?.lowercased(), m == "video" || m == "audio" { return m }
|
|
148
|
+
return bool("has_video") == true ? "video" : "audio"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Epoch ms; a seconds value (< 1e12) is scaled. Missing: sent_at (or
|
|
152
|
+
/// now) + 60 s — the server's ring_ended(timeout) is the real cut-off.
|
|
153
|
+
private var deadlineMs: Int64 {
|
|
154
|
+
func ms(_ v: Int64) -> Int64 { v < 1_000_000_000_000 ? v * 1000 : v }
|
|
155
|
+
if let d = int64("ring_deadline"), d > 0 { return ms(d) }
|
|
156
|
+
let base = int64("sent_at").map(ms) ?? Int64(Date().timeIntervalSince1970 * 1000)
|
|
157
|
+
return base + 60_000
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func ringContext() -> RingContext? {
|
|
161
|
+
guard let callId = callId, let conversationId = string("conversation_id"), let grant = string("grant") else { return nil }
|
|
162
|
+
return RingContext(
|
|
163
|
+
callId: callId,
|
|
164
|
+
conversationId: conversationId,
|
|
165
|
+
conversationName: string("conversation_name"),
|
|
166
|
+
callerName: string("caller_name"),
|
|
167
|
+
media: media,
|
|
168
|
+
grant: grant,
|
|
169
|
+
ringDeadlineEpochMs: deadlineMs
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
# LiveKit's Swift SDK (and its LiveKitUniFFI / collections dependencies)
|
|
6
|
+
# is no longer published to CocoaPods trunk: it lives in LiveKit's own
|
|
7
|
+
# spec repo. A podspec cannot declare a source, and the host's Podfile is
|
|
8
|
+
# generated by `expo prebuild`, so instead of asking every host to add
|
|
9
|
+
#
|
|
10
|
+
# source 'https://cdn.cocoapods.org/'
|
|
11
|
+
# source 'https://github.com/livekit/podspecs.git'
|
|
12
|
+
#
|
|
13
|
+
# we append LiveKit's spec repo to the resolver's sources while this
|
|
14
|
+
# podspec is loaded (autolinking evaluates it inside `pod install`).
|
|
15
|
+
# Trunk stays first, so every other pod resolves exactly as before.
|
|
16
|
+
# Opt out with POOLSE_CALLS_NO_LIVEKIT_SOURCE=1 (then add the lines above).
|
|
17
|
+
LIVEKIT_SPECS_SOURCE = 'https://github.com/livekit/podspecs.git'
|
|
18
|
+
if ENV['POOLSE_CALLS_NO_LIVEKIT_SOURCE'] != '1' &&
|
|
19
|
+
defined?(Pod::Installer::Analyzer) &&
|
|
20
|
+
!Pod::Installer::Analyzer.method_defined?(:poolse_calls_original_sources)
|
|
21
|
+
Pod::Installer::Analyzer.class_eval do
|
|
22
|
+
alias_method :poolse_calls_original_sources, :sources
|
|
23
|
+
def sources
|
|
24
|
+
@poolse_calls_sources ||= begin
|
|
25
|
+
list = poolse_calls_original_sources
|
|
26
|
+
if list.any? { |s| s.respond_to?(:url) && s.url.to_s.sub(/\.git\z/, '') == LIVEKIT_SPECS_SOURCE.sub(/\.git\z/, '') }
|
|
27
|
+
list
|
|
28
|
+
else
|
|
29
|
+
list + [Pod::Config.instance.sources_manager.find_or_create_source_with_url(LIVEKIT_SPECS_SOURCE)]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Pod::Spec.new do |s|
|
|
37
|
+
s.name = 'PoolseCalls'
|
|
38
|
+
s.version = package['version']
|
|
39
|
+
s.summary = package['description']
|
|
40
|
+
s.description = package['description']
|
|
41
|
+
s.license = package['license']
|
|
42
|
+
s.author = 'Torsten Labs DOO'
|
|
43
|
+
s.homepage = 'https://poolse.dev'
|
|
44
|
+
s.platforms = { :ios => '15.1' }
|
|
45
|
+
s.swift_version = '5.9'
|
|
46
|
+
s.source = { :git => 'https://github.com/poolse/poolse-js-sdk.git' }
|
|
47
|
+
s.static_framework = true
|
|
48
|
+
|
|
49
|
+
s.dependency 'ExpoModulesCore'
|
|
50
|
+
# 2.15.1: LiveKitWebRTC 144.7559.10 (LKRTC-prefixed symbols in its own
|
|
51
|
+
# LiveKitWebRTC.framework) — links beside react-native-webrtc's
|
|
52
|
+
# WebRTC.framework without duplicate symbols. Not 2.16+: their vendored
|
|
53
|
+
# nanopb headers use <pb.h> angled includes that do not resolve when
|
|
54
|
+
# CocoaPods builds the pod as a static library (Expo's default).
|
|
55
|
+
s.dependency 'LiveKitClient', '= 2.15.1'
|
|
56
|
+
|
|
57
|
+
s.pod_target_xcconfig = {
|
|
58
|
+
'DEFINES_MODULE' => 'YES',
|
|
59
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule',
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
s.frameworks = 'CallKit', 'PushKit', 'AVFoundation', 'UIKit'
|
|
63
|
+
s.source_files = 'Core/**/*.swift', 'Platform/**/*.swift', 'Module/**/*.swift'
|
|
64
|
+
end
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poolse/react-native-calls",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Native call core for poolse — Kotlin/Swift calling engine with a thin React Native bridge. Ring, answer and carry audio with the app killed and the phone locked.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
"android/build.gradle",
|
|
23
23
|
"android/consumer-rules.pro",
|
|
24
24
|
"android/src",
|
|
25
|
+
"ios/Core",
|
|
26
|
+
"ios/Platform",
|
|
27
|
+
"ios/Module",
|
|
28
|
+
"ios/PoolseCalls.podspec",
|
|
29
|
+
"!ios/CoreTests",
|
|
30
|
+
"!**/.build",
|
|
25
31
|
"expo-module.config.json",
|
|
26
32
|
"README.md"
|
|
27
33
|
],
|
|
@@ -8,4 +8,9 @@
|
|
|
8
8
|
// cannot collide with the app's own copy of the types.
|
|
9
9
|
declare module 'expo-modules-core' {
|
|
10
10
|
export function requireOptionalNativeModule<T>(name: string): T | null;
|
|
11
|
+
/** A native view registered by an Expo module (`View(...)` + `Name(...)`). */
|
|
12
|
+
export function requireNativeViewManager<P>(
|
|
13
|
+
moduleName: string,
|
|
14
|
+
viewName?: string,
|
|
15
|
+
): import('react').ComponentType<P>;
|
|
11
16
|
}
|
package/src/index.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
// On a platform or build without the native module (Expo Go, web,
|
|
7
7
|
// unlinked), every command is a safe no-op and the state stays idle —
|
|
8
8
|
// so a host can render against this unconditionally.
|
|
9
|
-
import {
|
|
9
|
+
import { createElement, type ComponentType, type ReactElement } from 'react';
|
|
10
|
+
import { View, type ViewProps } from 'react-native';
|
|
11
|
+
import { requireNativeViewManager, requireOptionalNativeModule } from 'expo-modules-core';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* A call's complete state. `null` means idle — no call. After a call
|
|
@@ -36,18 +38,35 @@ export interface Snapshot {
|
|
|
36
38
|
externalId: string;
|
|
37
39
|
speaking: boolean;
|
|
38
40
|
muted: boolean;
|
|
41
|
+
/** True only with a published, subscribed, unmuted camera track (show video, not avatar). */
|
|
39
42
|
cameraOn: boolean;
|
|
40
43
|
}>;
|
|
41
44
|
audioRoute: 'earpiece' | 'speaker' | 'bluetooth' | 'headset';
|
|
42
45
|
mutedSelf: boolean;
|
|
46
|
+
/** True only while the local camera track is actually published and enabled. */
|
|
43
47
|
cameraOn: boolean;
|
|
48
|
+
/** Facing of the published local camera; absent when the camera is off. */
|
|
49
|
+
cameraFacing?: 'front' | 'back';
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
export interface ConfigureOptions {
|
|
47
53
|
/** Drawable resource NAME for the call notification's small icon. */
|
|
48
54
|
smallIcon?: string;
|
|
49
|
-
/**
|
|
55
|
+
/** Ringback tone: raw resource NAME (Android); main-bundle sound name, mp3/caf/wav (iOS). */
|
|
50
56
|
ringback?: string;
|
|
57
|
+
/**
|
|
58
|
+
* iOS: the APNs environment of this build's `aps-environment`
|
|
59
|
+
* entitlement. Echoed back with the VoIP token so the server pushes to
|
|
60
|
+
* the right gateway. Ignored on Android.
|
|
61
|
+
*/
|
|
62
|
+
apnsEnvironment?: 'production' | 'sandbox';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The iOS PushKit (VoIP) token and the APNs environment it belongs to. */
|
|
66
|
+
export interface VoipToken {
|
|
67
|
+
/** Hex-encoded PushKit token. */
|
|
68
|
+
token: string;
|
|
69
|
+
environment: 'production' | 'sandbox' | string;
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
export interface PlaceOptions {
|
|
@@ -74,7 +93,9 @@ interface PoolseCallsNative {
|
|
|
74
93
|
onCallFinished(callId: string, reason: string | null): void;
|
|
75
94
|
canUseFullScreenIntent(): boolean;
|
|
76
95
|
openFullScreenIntentSettings(): void;
|
|
96
|
+
getVoipToken?(): VoipToken | null;
|
|
77
97
|
addListener(event: 'onCallState', cb: (payload: NativeSnapshot) => void): { remove(): void };
|
|
98
|
+
addListener(event: 'onVoipToken', cb: (payload: VoipToken) => void): { remove(): void };
|
|
78
99
|
}
|
|
79
100
|
|
|
80
101
|
const native = requireOptionalNativeModule<PoolseCallsNative>('PoolseCalls');
|
|
@@ -146,3 +167,65 @@ export const canUseFullScreenIntent = (): boolean => native?.canUseFullScreenInt
|
|
|
146
167
|
|
|
147
168
|
/** Open the system screen where the user grants full-screen intents (falls back to notification settings). */
|
|
148
169
|
export const openFullScreenIntentSettings = (): void => native?.openFullScreenIntentSettings();
|
|
170
|
+
|
|
171
|
+
// ── VoIP push (iOS) ───────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* iOS: the current PushKit VoIP token, registered natively at launch.
|
|
175
|
+
* `null` until iOS issues one, and always on Android (FCM carries rings).
|
|
176
|
+
*/
|
|
177
|
+
export function getVoipToken(): VoipToken | null {
|
|
178
|
+
return native?.getVoipToken?.() ?? null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** iOS: fires whenever PushKit issues (or rotates) the VoIP token. No-op on Android. */
|
|
182
|
+
export function onVoipToken(cb: (token: VoipToken) => void): Unsubscribe {
|
|
183
|
+
if (!native || !native.getVoipToken) return () => undefined;
|
|
184
|
+
const sub = native.addListener('onVoipToken', cb);
|
|
185
|
+
return () => sub.remove();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── video ─────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
export interface CallVideoViewProps extends ViewProps {
|
|
191
|
+
/** Remote participant's userId (LiveKit identity). Omit with local=true for self view. */
|
|
192
|
+
participantId?: string;
|
|
193
|
+
local?: boolean;
|
|
194
|
+
/** default: true for local front camera, false otherwise */
|
|
195
|
+
mirror?: boolean;
|
|
196
|
+
/** default 'cover' */
|
|
197
|
+
fit?: 'cover' | 'contain';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
type NativeVideoProps = CallVideoViewProps;
|
|
201
|
+
|
|
202
|
+
let nativeVideo: ComponentType<NativeVideoProps> | null | undefined;
|
|
203
|
+
|
|
204
|
+
/** Resolved lazily (first render) and only when the native module exists. */
|
|
205
|
+
function nativeVideoView(): ComponentType<NativeVideoProps> | null {
|
|
206
|
+
if (nativeVideo !== undefined) return nativeVideo;
|
|
207
|
+
nativeVideo = null;
|
|
208
|
+
if (native) {
|
|
209
|
+
try {
|
|
210
|
+
nativeVideo = requireNativeViewManager<NativeVideoProps>('PoolseCalls', 'PoolseCallVideo');
|
|
211
|
+
} catch {
|
|
212
|
+
nativeVideo = null; // an older native build without the view
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return nativeVideo;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A participant's camera (or, with `local`, your own), rendered natively
|
|
220
|
+
* by the call core. Shows nothing (stays transparent) while that camera
|
|
221
|
+
* is off — draw the avatar from the snapshot's `cameraOn` underneath.
|
|
222
|
+
* Without the native module it renders an empty View.
|
|
223
|
+
*/
|
|
224
|
+
export function CallVideoView(props: CallVideoViewProps): ReactElement | null {
|
|
225
|
+
const Native = nativeVideoView();
|
|
226
|
+
if (!Native) {
|
|
227
|
+
const { participantId: _p, local: _l, mirror: _m, fit: _f, ...rest } = props;
|
|
228
|
+
return createElement(View, rest);
|
|
229
|
+
}
|
|
230
|
+
return createElement(Native, props);
|
|
231
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Minimal ambient declarations for the React / React Native surface this
|
|
2
|
+
// package touches, so it type-checks without either installed in the
|
|
3
|
+
// monorepo (the host app always supplies both). The emitted
|
|
4
|
+
// `dist/index.d.ts` imports the REAL types by module name, so consumers
|
|
5
|
+
// type-check against their own React Native — these shims never ship.
|
|
6
|
+
declare module 'react' {
|
|
7
|
+
export interface ReactElement {
|
|
8
|
+
type: unknown;
|
|
9
|
+
props: unknown;
|
|
10
|
+
key: string | null;
|
|
11
|
+
}
|
|
12
|
+
export type ComponentType<P = object> = (props: P) => ReactElement | null;
|
|
13
|
+
export function createElement(type: unknown, props?: unknown, ...children: unknown[]): ReactElement;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare module 'react-native' {
|
|
17
|
+
export interface ViewProps {
|
|
18
|
+
style?: unknown;
|
|
19
|
+
testID?: string;
|
|
20
|
+
}
|
|
21
|
+
export const View: unknown;
|
|
22
|
+
}
|