crowdplaysdk 0.5.1 → 0.6.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/README.md +37 -8
- package/app.plugin.js +186 -0
- package/ios/wire.rb +22 -7
- package/lib/ConsentScreen.d.ts +1 -1
- package/lib/ConsentScreen.js +4 -8
- package/lib/VideoView.js +17 -0
- package/lib/VoiceView.d.ts +10 -0
- package/lib/index.d.ts +9 -1
- package/lib/index.js +5 -4
- package/llms.txt +34 -5
- package/package.json +9 -2
- package/src/ConsentScreen.tsx +4 -12
- package/src/VideoView.tsx +22 -1
- package/src/VoiceView.tsx +3 -1
- package/src/index.ts +17 -5
package/README.md
CHANGED
|
@@ -60,13 +60,39 @@ https://github.com/symbiateam/crowdplaysdk
|
|
|
60
60
|
|
|
61
61
|
## Setup (iOS)
|
|
62
62
|
|
|
63
|
-
Requirements: iOS 17+, React Native 0.71 or newer
|
|
64
|
-
|
|
63
|
+
Requirements: iOS 17+, React Native 0.71 or newer (Expo SDK 50 or newer
|
|
64
|
+
for the config plugin), a real device to test (the Simulator has no camera
|
|
65
|
+
or mic), and an **app key** from CrowdPlay.
|
|
65
66
|
iOS only for now; calls to the module on other platforms throw with a
|
|
66
67
|
clear message.
|
|
67
68
|
|
|
68
69
|
**1. Install the package and wire the engine.**
|
|
69
70
|
|
|
71
|
+
Expo (managed, prebuild or EAS Build, including app builders such as X1):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install crowdplaysdk
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
then add the plugin to `app.json` and rebuild (`npx expo prebuild`, a
|
|
78
|
+
development build, or `eas build`):
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
"plugins": ["crowdplaysdk"]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
That line is the whole native setup. The plugin adds the microphone and
|
|
85
|
+
camera usage strings and the audio background mode to Info.plist, raises
|
|
86
|
+
the iOS deployment target to 17.0, and wires the compiled engine into the
|
|
87
|
+
Xcode project at prebuild time. Options: `["crowdplaysdk", { "audioOnly":
|
|
88
|
+
true }]` skips the camera string; `microphonePermission` and
|
|
89
|
+
`cameraPermission` set your own wording. Expo Go cannot load native
|
|
90
|
+
modules, so the call, the consent text and recording only work in a
|
|
91
|
+
development build or a TestFlight build; the JavaScript side shows
|
|
92
|
+
placeholders in Expo Go instead of crashing.
|
|
93
|
+
|
|
94
|
+
Bare React Native (no Expo):
|
|
95
|
+
|
|
70
96
|
```bash
|
|
71
97
|
npm install crowdplaysdk
|
|
72
98
|
cd ios && pod install
|
|
@@ -75,11 +101,14 @@ ruby node_modules/crowdplaysdk/ios/wire.rb <YourProjectName>
|
|
|
75
101
|
```
|
|
76
102
|
|
|
77
103
|
`wire.rb` is idempotent. It adds the compiled CrowdPlaySDK Swift package,
|
|
78
|
-
copies the three bridge files into your app target,
|
|
79
|
-
bridging header at the React headers
|
|
80
|
-
|
|
104
|
+
copies the three bridge files into your app target, points the Swift
|
|
105
|
+
bridging header at the React headers, raises the app target to iOS 17 and
|
|
106
|
+
keeps explicit Swift modules on for the app target (React Native 0.8x turns
|
|
107
|
+
them off project-wide on Xcode 26, which breaks importing the compiled
|
|
108
|
+
engine). There is no CocoaPod of our own, so your Podfile stays untouched.
|
|
81
109
|
|
|
82
|
-
**2. Declare permissions in Info.plist
|
|
110
|
+
**2. Declare permissions in Info.plist** (bare React Native only; the Expo
|
|
111
|
+
plugin does this for you).
|
|
83
112
|
|
|
84
113
|
```
|
|
85
114
|
NSMicrophoneUsageDescription = "Records your microphone during sessions."
|
|
@@ -87,8 +116,8 @@ NSCameraUsageDescription = "Records your camera during sessions."
|
|
|
87
116
|
UIBackgroundModes = [audio]
|
|
88
117
|
```
|
|
89
118
|
|
|
90
|
-
**3. Let uploads finish while the app is suspended** (recommended
|
|
91
|
-
AppDelegate:
|
|
119
|
+
**3. Let uploads finish while the app is suspended** (recommended, both
|
|
120
|
+
paths). In your AppDelegate:
|
|
92
121
|
|
|
93
122
|
```objc
|
|
94
123
|
// AppDelegate.mm, with #import "<YourProjectName>-Swift.h" at the top
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// Expo config plugin for crowdplaysdk (D-121).
|
|
2
|
+
//
|
|
3
|
+
// app.json: "plugins": ["crowdplaysdk"]
|
|
4
|
+
// or "plugins": [["crowdplaysdk", { "audioOnly": true }]]
|
|
5
|
+
//
|
|
6
|
+
// That line is the whole native setup for an Expo app (managed, prebuild or
|
|
7
|
+
// EAS Build). At `expo prebuild` time it:
|
|
8
|
+
// 1. adds the microphone (and camera) usage strings and the `audio`
|
|
9
|
+
// background mode to Info.plist, only where the app has not set them;
|
|
10
|
+
// 2. raises ios.deploymentTarget to 17.0 (the engine requires iOS 17);
|
|
11
|
+
// 3. runs ios/wire.rb against the generated Xcode project, exactly as a
|
|
12
|
+
// bare React Native builder would by hand: it adds the compiled
|
|
13
|
+
// CrowdPlaySDK Swift package (pinned to the engine release the bridge
|
|
14
|
+
// was compiled against), copies the three bridge files into the app
|
|
15
|
+
// target and points the bridging header at the React headers.
|
|
16
|
+
// Step 3 runs in Expo's `finalized` mod, after every other mod has written
|
|
17
|
+
// the pbxproj and Info.plist, so nothing in config-plugins rewrites the
|
|
18
|
+
// project afterwards; `pod install` (which runs later) integrates through
|
|
19
|
+
// the same Xcodeproj gem and keeps the package reference.
|
|
20
|
+
//
|
|
21
|
+
// Props (all optional):
|
|
22
|
+
// audioOnly true: no camera usage string is added
|
|
23
|
+
// microphonePermission the NSMicrophoneUsageDescription text
|
|
24
|
+
// cameraPermission the NSCameraUsageDescription text
|
|
25
|
+
// backgroundAudio false: do not add the `audio` background mode
|
|
26
|
+
//
|
|
27
|
+
// Requires Expo SDK 50 or newer (the `finalized` mod) and a Ruby with the
|
|
28
|
+
// xcodeproj gem, which every CocoaPods install provides; EAS Build images
|
|
29
|
+
// ship both. Expo Go cannot load native modules, so the call, the consent
|
|
30
|
+
// text and recording only work in a development build or a TestFlight
|
|
31
|
+
// build; the JavaScript side degrades instead of crashing there.
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const path = require('path');
|
|
36
|
+
const { spawnSync } = require('child_process');
|
|
37
|
+
const pkg = require('./package.json');
|
|
38
|
+
|
|
39
|
+
let configPlugins;
|
|
40
|
+
try {
|
|
41
|
+
configPlugins = require('expo/config-plugins');
|
|
42
|
+
} catch (e) {
|
|
43
|
+
configPlugins = require('@expo/config-plugins');
|
|
44
|
+
}
|
|
45
|
+
const {
|
|
46
|
+
withInfoPlist, withPodfileProperties, withFinalizedMod, createRunOncePlugin, IOSConfig,
|
|
47
|
+
} = configPlugins;
|
|
48
|
+
|
|
49
|
+
const MIC_DEFAULT = 'Records your microphone during sessions.';
|
|
50
|
+
const CAMERA_DEFAULT = 'Records your camera during sessions.';
|
|
51
|
+
const MIN_IOS = 17;
|
|
52
|
+
const REACT_IMPORTS = [
|
|
53
|
+
'#import <React/RCTBridgeModule.h>',
|
|
54
|
+
'#import <React/RCTEventEmitter.h>',
|
|
55
|
+
'#import <React/RCTViewManager.h>',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
function withCrowdPlayInfoPlist(config, props) {
|
|
59
|
+
return withInfoPlist(config, (config) => {
|
|
60
|
+
const plist = config.modResults;
|
|
61
|
+
if (!plist.NSMicrophoneUsageDescription) {
|
|
62
|
+
plist.NSMicrophoneUsageDescription = props.microphonePermission || MIC_DEFAULT;
|
|
63
|
+
}
|
|
64
|
+
if (props.audioOnly !== true && !plist.NSCameraUsageDescription) {
|
|
65
|
+
plist.NSCameraUsageDescription = props.cameraPermission || CAMERA_DEFAULT;
|
|
66
|
+
}
|
|
67
|
+
if (props.backgroundAudio !== false) {
|
|
68
|
+
if (!Array.isArray(plist.UIBackgroundModes)) plist.UIBackgroundModes = [];
|
|
69
|
+
if (!plist.UIBackgroundModes.includes('audio')) plist.UIBackgroundModes.push('audio');
|
|
70
|
+
}
|
|
71
|
+
return config;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function withCrowdPlayDeploymentTarget(config) {
|
|
76
|
+
return withPodfileProperties(config, (config) => {
|
|
77
|
+
const current = parseFloat(config.modResults['ios.deploymentTarget'] || '0');
|
|
78
|
+
if (!(current >= MIN_IOS)) config.modResults['ios.deploymentTarget'] = `${MIN_IOS}.0`;
|
|
79
|
+
return config;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The Xcode project name: Expo names the .xcodeproj after the app, and
|
|
84
|
+
* modRequest.projectName carries it; fall back to a glob when it does not. */
|
|
85
|
+
function resolveProjectName(modRequest) {
|
|
86
|
+
const { projectName, projectRoot, platformProjectRoot } = modRequest;
|
|
87
|
+
if (projectName && fs.existsSync(path.join(platformProjectRoot, `${projectName}.xcodeproj`))) {
|
|
88
|
+
return projectName;
|
|
89
|
+
}
|
|
90
|
+
const found = IOSConfig.Paths.getXcodeProjectPath(projectRoot);
|
|
91
|
+
return path.basename(found, '.xcodeproj');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A Ruby that can load the xcodeproj gem: the one on PATH, the one that
|
|
95
|
+
* runs `pod`, or Bundler's when the project has a Gemfile. */
|
|
96
|
+
function findRuby(platformProjectRoot) {
|
|
97
|
+
const candidates = [{ cmd: 'ruby', args: [] }];
|
|
98
|
+
const pod = spawnSync('which', ['pod'], { encoding: 'utf8' });
|
|
99
|
+
if (pod.status === 0) {
|
|
100
|
+
try {
|
|
101
|
+
const shebang = fs.readFileSync(pod.stdout.trim(), 'utf8').split('\n')[0];
|
|
102
|
+
const m = /^#!\s*(\S+)(?:\s+(\S+))?/.exec(shebang);
|
|
103
|
+
if (m) {
|
|
104
|
+
const cmd = m[1].endsWith('/env') ? m[2] : m[1];
|
|
105
|
+
if (cmd && cmd !== 'ruby') candidates.push({ cmd, args: [] });
|
|
106
|
+
}
|
|
107
|
+
} catch (e) { /* not a script; ignore */ }
|
|
108
|
+
}
|
|
109
|
+
if (fs.existsSync(path.join(platformProjectRoot, 'Gemfile')) ||
|
|
110
|
+
fs.existsSync(path.join(platformProjectRoot, '..', 'Gemfile'))) {
|
|
111
|
+
candidates.push({ cmd: 'bundle', args: ['exec', 'ruby'] });
|
|
112
|
+
}
|
|
113
|
+
for (const c of candidates) {
|
|
114
|
+
const probe = spawnSync(c.cmd, [...c.args, '-e', "require 'xcodeproj'"], {
|
|
115
|
+
cwd: platformProjectRoot, encoding: 'utf8',
|
|
116
|
+
});
|
|
117
|
+
if (probe.status === 0) return c;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** wire.rb creates the bridging header only when the target has none. Expo's
|
|
123
|
+
* template already has one (a comment-only file), so add the React imports
|
|
124
|
+
* the bridge files need to whatever header the project points at. */
|
|
125
|
+
function ensureBridgingHeaderImports(platformProjectRoot, projectName) {
|
|
126
|
+
const pbxproj = path.join(platformProjectRoot, `${projectName}.xcodeproj`, 'project.pbxproj');
|
|
127
|
+
if (!fs.existsSync(pbxproj)) return;
|
|
128
|
+
const text = fs.readFileSync(pbxproj, 'utf8');
|
|
129
|
+
const headers = new Set();
|
|
130
|
+
const re = /SWIFT_OBJC_BRIDGING_HEADER\s*=\s*"?([^";]+)"?\s*;/g;
|
|
131
|
+
let m;
|
|
132
|
+
while ((m = re.exec(text)) !== null) headers.add(m[1].trim());
|
|
133
|
+
for (const rel of headers) {
|
|
134
|
+
const file = path.join(platformProjectRoot, rel);
|
|
135
|
+
if (!fs.existsSync(file)) continue;
|
|
136
|
+
const body = fs.readFileSync(file, 'utf8');
|
|
137
|
+
if (body.includes('React/RCTBridgeModule.h')) continue;
|
|
138
|
+
const sep = body.length === 0 || body.endsWith('\n') ? '' : '\n';
|
|
139
|
+
fs.writeFileSync(file, body + sep + REACT_IMPORTS.join('\n') + '\n');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function withCrowdPlayWire(config) {
|
|
144
|
+
return withFinalizedMod(config, ['ios', async (config) => {
|
|
145
|
+
const { introspect, platformProjectRoot } = config.modRequest;
|
|
146
|
+
if (introspect) return config;
|
|
147
|
+
const projectName = resolveProjectName(config.modRequest);
|
|
148
|
+
const ruby = findRuby(platformProjectRoot);
|
|
149
|
+
if (!ruby) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
'crowdplaysdk: no Ruby with the xcodeproj gem was found, so the CrowdPlay ' +
|
|
152
|
+
'engine could not be wired into the Xcode project. The gem ships with every ' +
|
|
153
|
+
'CocoaPods install; locally run `gem install xcodeproj --user-install` and ' +
|
|
154
|
+
'prebuild again. EAS Build images already have it.',
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const wire = path.join(__dirname, 'ios', 'wire.rb');
|
|
158
|
+
const r = spawnSync(ruby.cmd, [...ruby.args, wire, projectName], {
|
|
159
|
+
cwd: platformProjectRoot, encoding: 'utf8',
|
|
160
|
+
});
|
|
161
|
+
if (r.status !== 0) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`crowdplaysdk: ios/wire.rb failed (exit ${r.status}) while wiring the CrowdPlay ` +
|
|
164
|
+
`engine into ${projectName}.xcodeproj.\n${r.stdout || ''}${r.stderr || ''}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
for (const line of (r.stdout || '').split('\n')) {
|
|
168
|
+
if (line.startsWith('+')) console.log(`crowdplaysdk: ${line}`);
|
|
169
|
+
}
|
|
170
|
+
ensureBridgingHeaderImports(platformProjectRoot, projectName);
|
|
171
|
+
return config;
|
|
172
|
+
}]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const withCrowdPlay = (config, props) => {
|
|
176
|
+
const p = props && typeof props === 'object' ? props : {};
|
|
177
|
+
if (typeof withFinalizedMod !== 'function') {
|
|
178
|
+
throw new Error('crowdplaysdk: the Expo config plugin needs Expo SDK 50 or newer.');
|
|
179
|
+
}
|
|
180
|
+
config = withCrowdPlayInfoPlist(config, p);
|
|
181
|
+
config = withCrowdPlayDeploymentTarget(config);
|
|
182
|
+
config = withCrowdPlayWire(config);
|
|
183
|
+
return config;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
module.exports = createRunOncePlugin(withCrowdPlay, pkg.name, pkg.version);
|
package/ios/wire.rb
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
# Wires crowdplaysdk into an iOS app project.
|
|
2
|
-
#
|
|
1
|
+
# Wires crowdplaysdk into an iOS app project. Expo apps never run this by
|
|
2
|
+
# hand: the package's config plugin (app.plugin.js) runs it during
|
|
3
|
+
# `expo prebuild`. Bare React Native apps run it from ios/ after
|
|
4
|
+
# `pod install`:
|
|
3
5
|
#
|
|
4
6
|
# ruby node_modules/crowdplaysdk/ios/wire.rb <YourProjectName>
|
|
5
7
|
#
|
|
6
8
|
# What it does (idempotent; safe to re-run):
|
|
7
9
|
# 1. adds the CrowdPlaySDK Swift package (compiled engine) to the project
|
|
8
10
|
# 2. copies the three bridge files into your app group + target
|
|
9
|
-
# 3. creates/points the Swift bridging header at the React headers
|
|
11
|
+
# 3. creates/points the Swift bridging header at the React headers, raises
|
|
12
|
+
# the app target to iOS 17 and keeps explicit Swift modules ON for it
|
|
10
13
|
#
|
|
11
14
|
# Requires the `xcodeproj` gem (ships with CocoaPods installs; otherwise
|
|
12
15
|
# `gem install xcodeproj`).
|
|
@@ -22,10 +25,12 @@ rescue LoadError
|
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
SDK_URL = 'https://github.com/symbiateam/crowdplaysdk'
|
|
25
|
-
# The ENGINE release this bridge
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
|
|
28
|
+
# The ENGINE release this bridge compiles against. The npm version may run
|
|
29
|
+
# ahead of it only while the bridge uses no newer engine symbols; since
|
|
30
|
+
# D-103 the bridge calls 0.5.0 APIs, and npm 0.5.0 to 0.5.2 shipped with
|
|
31
|
+
# this still at 0.4.0, which broke every install (D-121). publish_rn.sh now
|
|
32
|
+
# compiles the bridge against exactly this release before publishing.
|
|
33
|
+
SDK_VERSION = '0.5.2'
|
|
29
34
|
BRIDGE_FILES = ['CrowdPlayRNModule.swift', 'CrowdPlayRNVideoView.swift', 'CrowdPlayReactNative.m'].freeze
|
|
30
35
|
|
|
31
36
|
project_name = ARGV[0] or abort 'usage: ruby wire.rb <YourProjectName>'
|
|
@@ -73,6 +78,16 @@ target.build_configurations.each do |config|
|
|
|
73
78
|
if current.nil? || current.to_f < 17.0
|
|
74
79
|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '17.0'
|
|
75
80
|
end
|
|
81
|
+
# React Native's pod post-install (react_native_pods.rb, RN 0.8x with the
|
|
82
|
+
# prebuilt React Native core) sets SWIFT_ENABLE_EXPLICIT_MODULES = NO at the
|
|
83
|
+
# PROJECT level for Xcode 26. With implicit modules the app target never
|
|
84
|
+
# receives the module map for _AtomicsShims, a C module of swift-atomics
|
|
85
|
+
# that the compiled engine's Swift interface imports, and every Swift file
|
|
86
|
+
# in the app fails with "missing required module '_AtomicsShims'" (measured
|
|
87
|
+
# on Expo SDK 57 / RN 0.86 / Xcode 26.6, D-121). A TARGET-level YES wins
|
|
88
|
+
# over the project-level NO, survives pod install, and only touches the app
|
|
89
|
+
# target; the same build passes with it.
|
|
90
|
+
config.build_settings['SWIFT_ENABLE_EXPLICIT_MODULES'] = 'YES'
|
|
76
91
|
next if config.build_settings['SWIFT_OBJC_BRIDGING_HEADER']
|
|
77
92
|
|
|
78
93
|
header_name = "#{project_name}-Bridging-Header.h"
|
package/lib/ConsentScreen.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Drop-in consent screen. Shows CrowdPlay's locked consent wording (fetched from
|
|
3
3
|
* the native SDK so it can never drift from what the manifest records), a link
|
|
4
|
-
* to the full CrowdPlay Terms of Use and Privacy
|
|
4
|
+
* to the full CrowdPlay Terms of Use and Privacy Policy, a switch, and a
|
|
5
5
|
* continue button that only enables once the participant agrees. Yields the
|
|
6
6
|
* ConsentGrant that join() requires.
|
|
7
7
|
*
|
package/lib/ConsentScreen.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Drop-in consent screen. Shows CrowdPlay's locked consent wording (fetched from
|
|
4
4
|
* the native SDK so it can never drift from what the manifest records), a link
|
|
5
|
-
* to the full CrowdPlay Terms of Use and Privacy
|
|
5
|
+
* to the full CrowdPlay Terms of Use and Privacy Policy, a switch, and a
|
|
6
6
|
* continue button that only enables once the participant agrees. Yields the
|
|
7
7
|
* ConsentGrant that join() requires.
|
|
8
8
|
*
|
|
@@ -70,21 +70,17 @@ function CrowdPlayConsentScreen({ onConsent, style }) {
|
|
|
70
70
|
};
|
|
71
71
|
}, []);
|
|
72
72
|
return (<react_native_1.View style={[styles.container, style]}>
|
|
73
|
-
<react_native_1.Text style={styles.title}>
|
|
74
|
-
{text === null ? (<react_native_1.ActivityIndicator />) : (<react_native_1.Text style={styles.body}>{text}</react_native_1.Text>)}
|
|
73
|
+
<react_native_1.Text style={styles.title}>Before you join</react_native_1.Text>
|
|
75
74
|
<react_native_1.Pressable accessibilityRole="link" onPress={() => void react_native_1.Linking.openURL(index_1.CROWDPLAY_TERMS_URL)}>
|
|
76
|
-
<react_native_1.Text style={styles.link}>Read the Terms of Use and Privacy
|
|
75
|
+
<react_native_1.Text style={styles.link}>Read the Terms of Use and Privacy Policy</react_native_1.Text>
|
|
77
76
|
</react_native_1.Pressable>
|
|
78
77
|
<react_native_1.View style={styles.row}>
|
|
79
78
|
<react_native_1.Switch value={agreed} onValueChange={(value) => {
|
|
80
79
|
setAgreed(value);
|
|
81
80
|
setAgreedAtMs(value ? Date.now() : null);
|
|
82
81
|
}}/>
|
|
83
|
-
<react_native_1.Text style={styles.agree}>
|
|
82
|
+
{text === null ? <react_native_1.ActivityIndicator /> : <react_native_1.Text style={styles.agree}>{text}</react_native_1.Text>}
|
|
84
83
|
</react_native_1.View>
|
|
85
|
-
<react_native_1.Text style={styles.note}>
|
|
86
|
-
Recording starts automatically when you join, and ends when you leave.
|
|
87
|
-
</react_native_1.Text>
|
|
88
84
|
<react_native_1.Pressable accessibilityRole="button" disabled={!agreed || agreedAtMs === null} onPress={() => {
|
|
89
85
|
if (agreedAtMs !== null)
|
|
90
86
|
onConsent({ grantedAtMs: agreedAtMs });
|
package/lib/VideoView.js
CHANGED
|
@@ -18,7 +18,24 @@ exports.CrowdPlayVideoView = CrowdPlayVideoView;
|
|
|
18
18
|
const react_1 = __importDefault(require("react"));
|
|
19
19
|
const react_native_1 = require("react-native");
|
|
20
20
|
const NativeCrowdPlayVideoView = (0, react_native_1.requireNativeComponent)('CrowdPlayVideoView');
|
|
21
|
+
/** The native module and the native view ship in the same bridge files, so
|
|
22
|
+
* a missing module means a build without the engine: Expo Go, or a preview
|
|
23
|
+
* that never ran the config plugin. Rendering the native view there throws
|
|
24
|
+
* at render time; a labelled placeholder keeps the rest of the screen
|
|
25
|
+
* usable and says what is going on. */
|
|
26
|
+
function nativeAvailable() {
|
|
27
|
+
return react_native_1.NativeModules.CrowdPlayReactNative != null;
|
|
28
|
+
}
|
|
21
29
|
function CrowdPlayVideoView(props) {
|
|
22
30
|
const { participant, mirror, ...rest } = props;
|
|
31
|
+
if (!nativeAvailable()) {
|
|
32
|
+
return (<react_native_1.View {...rest} style={[placeholder.box, rest.style]}>
|
|
33
|
+
<react_native_1.Text style={placeholder.label}>CrowdPlay video needs a device build</react_native_1.Text>
|
|
34
|
+
</react_native_1.View>);
|
|
35
|
+
}
|
|
23
36
|
return (<NativeCrowdPlayVideoView participant={participant} mirror={mirror ?? participant === 'local'} {...rest}/>);
|
|
24
37
|
}
|
|
38
|
+
const placeholder = react_native_1.StyleSheet.create({
|
|
39
|
+
box: { backgroundColor: '#222222', alignItems: 'center', justifyContent: 'center', padding: 8 },
|
|
40
|
+
label: { color: '#dddddd', fontSize: 12, textAlign: 'center' },
|
|
41
|
+
});
|
package/lib/VoiceView.d.ts
CHANGED
|
@@ -17,6 +17,16 @@ export interface VoiceActivity {
|
|
|
17
17
|
/** 0…1, smoothed (fast attack, slow release). */
|
|
18
18
|
energy: number;
|
|
19
19
|
}
|
|
20
|
+
/** Raw payload of the 'voiceActivity' event (the native fast stream that
|
|
21
|
+
* runs only while useVoiceActivity() or CrowdPlayVoiceView is mounted). */
|
|
22
|
+
export interface VoiceActivityEvent {
|
|
23
|
+
agentPresent: boolean;
|
|
24
|
+
agentSpeaking: boolean;
|
|
25
|
+
agentLevel: number;
|
|
26
|
+
localLevelDbfs: number | null;
|
|
27
|
+
localSpeaking: boolean;
|
|
28
|
+
connected: boolean;
|
|
29
|
+
}
|
|
20
30
|
/** Live phase + energy from the call engine. Subscribing starts the native
|
|
21
31
|
* fast stream; the last unmount stops it. */
|
|
22
32
|
export declare function useVoiceActivity(): VoiceActivity;
|
package/lib/index.d.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* native layer requires the consent record structurally; there is no code
|
|
21
21
|
* path that records without it.
|
|
22
22
|
*/
|
|
23
|
+
import type { VoiceActivityEvent, VoiceCaption } from './VoiceView';
|
|
23
24
|
/** Live-call (Path A) quality. The RECORDING is always 1080p30 + 24-bit WAV
|
|
24
25
|
* regardless; raising these trades upload + thermal headroom (which the
|
|
25
26
|
* recording needs on long sessions) for a prettier live picture. Values are
|
|
@@ -191,6 +192,13 @@ export interface CrowdPlayEvents {
|
|
|
191
192
|
name: string;
|
|
192
193
|
argumentsJSON: string;
|
|
193
194
|
};
|
|
195
|
+
/** Voice-AI activity (agent present/speaking, local level). Flows ONLY
|
|
196
|
+
* while the native fast stream is running, which useVoiceActivity() and
|
|
197
|
+
* CrowdPlayVoiceView start on mount and stop on unmount; prefer those. */
|
|
198
|
+
voiceActivity: VoiceActivityEvent;
|
|
199
|
+
/** The latest live transcript line from the voice agent; see
|
|
200
|
+
* useLatestCaption(). */
|
|
201
|
+
caption: VoiceCaption;
|
|
194
202
|
}
|
|
195
203
|
export type CrowdPlayEventName = keyof CrowdPlayEvents;
|
|
196
204
|
declare const CrowdPlay: {
|
|
@@ -262,4 +270,4 @@ export default CrowdPlay;
|
|
|
262
270
|
export { CrowdPlayConsentScreen } from './ConsentScreen';
|
|
263
271
|
export { CrowdPlayVideoView } from './VideoView';
|
|
264
272
|
export { CrowdPlayVoiceView, VoiceReactive, useVoiceActivity, useLatestCaption } from './VoiceView';
|
|
265
|
-
export type { CrowdPlayVoiceViewProps, VoiceActivity, VoicePhase, VoiceCaption } from './VoiceView';
|
|
273
|
+
export type { CrowdPlayVoiceViewProps, VoiceActivity, VoiceActivityEvent, VoicePhase, VoiceCaption } from './VoiceView';
|
package/lib/index.js
CHANGED
|
@@ -30,10 +30,11 @@ exports.CROWDPLAY_TERMS_URL = 'https://dashboard.crowdplay.ai/legal/terms.pdf';
|
|
|
30
30
|
function native() {
|
|
31
31
|
const module = react_native_1.NativeModules.CrowdPlayReactNative;
|
|
32
32
|
if (!module) {
|
|
33
|
-
throw new Error("crowdplaysdk: native module not found.
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
33
|
+
throw new Error("crowdplaysdk: native module not found. Expo: add \"crowdplaysdk\" to " +
|
|
34
|
+
"\"plugins\" in app.json and rebuild (npx expo prebuild, a development " +
|
|
35
|
+
"build, or eas build); Expo Go cannot load native modules. Bare React " +
|
|
36
|
+
"Native: run ruby node_modules/crowdplaysdk/ios/wire.rb <YourProjectName> " +
|
|
37
|
+
"from ios/ after pod install; see the package README. " +
|
|
37
38
|
(react_native_1.Platform.OS !== 'ios' ? `(platform '${react_native_1.Platform.OS}' is not supported yet; iOS only.)` : ''));
|
|
38
39
|
}
|
|
39
40
|
return module;
|
package/llms.txt
CHANGED
|
@@ -50,8 +50,8 @@ its video calls invisibly; recording is infrastructure, not identity:
|
|
|
50
50
|
|
|
51
51
|
1. NEVER add, pin, import, or update LiveKit, WebRTC, or any
|
|
52
52
|
react-native-webrtc package. The correct engine ships as a compiled
|
|
53
|
-
binary that ios/wire.rb installs. Adding
|
|
54
|
-
symbols or silently breaks capture.
|
|
53
|
+
binary that the Expo config plugin (or ios/wire.rb) installs. Adding
|
|
54
|
+
your own causes duplicate symbols or silently breaks capture.
|
|
55
55
|
2. NEVER write AVAudioSession / AVAudioEngine / AVCaptureSession code (or
|
|
56
56
|
add audio-session-touching RN packages like react-native-incall-manager)
|
|
57
57
|
in an app using this SDK. The engine owns the microphone and camera.
|
|
@@ -67,6 +67,27 @@ its video calls invisibly; recording is infrastructure, not identity:
|
|
|
67
67
|
|
|
68
68
|
## Integration procedure
|
|
69
69
|
|
|
70
|
+
Steps 1 to 4 depend on the project type. Everything from step 5 on is the
|
|
71
|
+
same for both.
|
|
72
|
+
|
|
73
|
+
EXPO (managed, prebuild or EAS Build; app builders such as X1):
|
|
74
|
+
1. npm install crowdplaysdk
|
|
75
|
+
2. app.json: add "crowdplaysdk" to the "plugins" array, or
|
|
76
|
+
["crowdplaysdk", { "audioOnly": true }] for an audio-only app.
|
|
77
|
+
That is the WHOLE native setup: at prebuild the plugin adds the
|
|
78
|
+
microphone and camera usage strings and the audio background mode to
|
|
79
|
+
Info.plist (only where the app has not set them), raises the iOS
|
|
80
|
+
deployment target to 17.0, and wires the compiled engine into the
|
|
81
|
+
Xcode project. Do not add those Info.plist keys or any native code
|
|
82
|
+
yourself. Expo SDK 50 or newer.
|
|
83
|
+
3. Rebuild: npx expo prebuild, a development build, or eas build.
|
|
84
|
+
4. Expo Go and any preview that never ran the plugin cannot load native
|
|
85
|
+
modules: there the consent text, the call and recording fail with a
|
|
86
|
+
clear error and the video view shows a placeholder. That is EXPECTED;
|
|
87
|
+
do not debug it and never remove the package or the plugin to make a
|
|
88
|
+
preview pass. Everything else works in preview.
|
|
89
|
+
|
|
90
|
+
BARE REACT NATIVE (no Expo):
|
|
70
91
|
1. npm install crowdplaysdk
|
|
71
92
|
2. cd ios && pod install (installs RN's own pods; CrowdPlay has none)
|
|
72
93
|
3. gem install xcodeproj --user-install (one-time; wire.rb needs it
|
|
@@ -74,9 +95,12 @@ its video calls invisibly; recording is infrastructure, not identity:
|
|
|
74
95
|
ruby node_modules/crowdplaysdk/ios/wire.rb <YourProjectName>
|
|
75
96
|
Idempotent. It: adds the CrowdPlaySDK binary Swift package to the Xcode
|
|
76
97
|
project, copies the three native bridge files into the app target,
|
|
77
|
-
creates/points the Swift bridging header at the React headers,
|
|
78
|
-
|
|
79
|
-
|
|
98
|
+
creates/points the Swift bridging header at the React headers, raises
|
|
99
|
+
the iOS deployment target to 17.0 if the RN template left it lower, and
|
|
100
|
+
sets SWIFT_ENABLE_EXPLICIT_MODULES = YES on the app target (React Native
|
|
101
|
+
0.8x sets it to NO project-wide on Xcode 26, and with implicit modules
|
|
102
|
+
the compiled engine's interface cannot be imported). Rerun it any time;
|
|
103
|
+
it never duplicates. Never undo that setting to fix an unrelated error.
|
|
80
104
|
4. Info.plist: NSMicrophoneUsageDescription, NSCameraUsageDescription,
|
|
81
105
|
UIBackgroundModes = [audio]. A missing usage string makes iOS kill
|
|
82
106
|
the app at first mic/camera access (uncatchable).
|
|
@@ -117,6 +141,11 @@ its video calls invisibly; recording is infrastructure, not identity:
|
|
|
117
141
|
detail names its own fix. NOTE: on a fresh install the "permissions"
|
|
118
142
|
check reports "not requested yet (NORMAL)"; iOS prompts at first join.
|
|
119
143
|
That is not an integration bug; only a DENIED state needs action.
|
|
144
|
+
KNOWN ENGINE BUG: on engine 0.5.1 and 0.5.2 the "consent_terms" check
|
|
145
|
+
fails on every device (the engine's own hash bookkeeping, fixed in
|
|
146
|
+
engine 0.5.3 / crowdplaysdk 0.6.1). Nothing in your app causes it and
|
|
147
|
+
nothing in your app fixes it; the consent screen and the recorded
|
|
148
|
+
consent are correct. Every other check in that run is valid.
|
|
120
149
|
11. Hand off (REQUIRED final message): the Simulator has no camera or
|
|
121
150
|
mic, so nothing is proven until the app runs on a real iPhone. Do
|
|
122
151
|
not end with "the implementation is complete"; your FINAL message
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "crowdplaysdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "CrowdPlay lossless conversation capture for React Native (iOS). Studio-grade per-participant recording during live video calls, delivered to CrowdPlay automatically.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
|
+
"app.plugin.js",
|
|
8
9
|
"lib",
|
|
9
10
|
"src",
|
|
10
11
|
"ios",
|
|
@@ -29,12 +30,18 @@
|
|
|
29
30
|
"recording",
|
|
30
31
|
"video-call",
|
|
31
32
|
"lossless",
|
|
32
|
-
"crowdplay"
|
|
33
|
+
"crowdplay",
|
|
34
|
+
"expo"
|
|
33
35
|
],
|
|
34
36
|
"peerDependencies": {
|
|
35
37
|
"react": "*",
|
|
36
38
|
"react-native": "*"
|
|
37
39
|
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"expo": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
38
45
|
"devDependencies": {
|
|
39
46
|
"@types/react": "^18.2.0",
|
|
40
47
|
"react": "18.2.0",
|
package/src/ConsentScreen.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Drop-in consent screen. Shows CrowdPlay's locked consent wording (fetched from
|
|
3
3
|
* the native SDK so it can never drift from what the manifest records), a link
|
|
4
|
-
* to the full CrowdPlay Terms of Use and Privacy
|
|
4
|
+
* to the full CrowdPlay Terms of Use and Privacy Policy, a switch, and a
|
|
5
5
|
* continue button that only enables once the participant agrees. Yields the
|
|
6
6
|
* ConsentGrant that join() requires.
|
|
7
7
|
*
|
|
@@ -43,14 +43,9 @@ export function CrowdPlayConsentScreen({ onConsent, style }: CrowdPlayConsentScr
|
|
|
43
43
|
|
|
44
44
|
return (
|
|
45
45
|
<View style={[styles.container, style]}>
|
|
46
|
-
<Text style={styles.title}>
|
|
47
|
-
{text === null ? (
|
|
48
|
-
<ActivityIndicator />
|
|
49
|
-
) : (
|
|
50
|
-
<Text style={styles.body}>{text}</Text>
|
|
51
|
-
)}
|
|
46
|
+
<Text style={styles.title}>Before you join</Text>
|
|
52
47
|
<Pressable accessibilityRole="link" onPress={() => void Linking.openURL(CROWDPLAY_TERMS_URL)}>
|
|
53
|
-
<Text style={styles.link}>Read the Terms of Use and Privacy
|
|
48
|
+
<Text style={styles.link}>Read the Terms of Use and Privacy Policy</Text>
|
|
54
49
|
</Pressable>
|
|
55
50
|
<View style={styles.row}>
|
|
56
51
|
<Switch
|
|
@@ -60,11 +55,8 @@ export function CrowdPlayConsentScreen({ onConsent, style }: CrowdPlayConsentScr
|
|
|
60
55
|
setAgreedAtMs(value ? Date.now() : null);
|
|
61
56
|
}}
|
|
62
57
|
/>
|
|
63
|
-
<Text style={styles.agree}>
|
|
58
|
+
{text === null ? <ActivityIndicator /> : <Text style={styles.agree}>{text}</Text>}
|
|
64
59
|
</View>
|
|
65
|
-
<Text style={styles.note}>
|
|
66
|
-
Recording starts automatically when you join, and ends when you leave.
|
|
67
|
-
</Text>
|
|
68
60
|
<Pressable
|
|
69
61
|
accessibilityRole="button"
|
|
70
62
|
disabled={!agreed || agreedAtMs === null}
|
package/src/VideoView.tsx
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import React from 'react';
|
|
14
|
-
import { requireNativeComponent, type ViewProps } from 'react-native';
|
|
14
|
+
import { NativeModules, requireNativeComponent, StyleSheet, Text, View, type ViewProps } from 'react-native';
|
|
15
15
|
|
|
16
16
|
export interface CrowdPlayVideoViewProps extends ViewProps {
|
|
17
17
|
/** "local" for the self-view, or a remote participant's identity. */
|
|
@@ -23,8 +23,24 @@ export interface CrowdPlayVideoViewProps extends ViewProps {
|
|
|
23
23
|
|
|
24
24
|
const NativeCrowdPlayVideoView = requireNativeComponent<CrowdPlayVideoViewProps>('CrowdPlayVideoView');
|
|
25
25
|
|
|
26
|
+
/** The native module and the native view ship in the same bridge files, so
|
|
27
|
+
* a missing module means a build without the engine: Expo Go, or a preview
|
|
28
|
+
* that never ran the config plugin. Rendering the native view there throws
|
|
29
|
+
* at render time; a labelled placeholder keeps the rest of the screen
|
|
30
|
+
* usable and says what is going on. */
|
|
31
|
+
function nativeAvailable(): boolean {
|
|
32
|
+
return NativeModules.CrowdPlayReactNative != null;
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
export function CrowdPlayVideoView(props: CrowdPlayVideoViewProps): React.JSX.Element {
|
|
27
36
|
const { participant, mirror, ...rest } = props;
|
|
37
|
+
if (!nativeAvailable()) {
|
|
38
|
+
return (
|
|
39
|
+
<View {...rest} style={[placeholder.box, rest.style]}>
|
|
40
|
+
<Text style={placeholder.label}>CrowdPlay video needs a device build</Text>
|
|
41
|
+
</View>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
28
44
|
return (
|
|
29
45
|
<NativeCrowdPlayVideoView
|
|
30
46
|
participant={participant}
|
|
@@ -33,3 +49,8 @@ export function CrowdPlayVideoView(props: CrowdPlayVideoViewProps): React.JSX.El
|
|
|
33
49
|
/>
|
|
34
50
|
);
|
|
35
51
|
}
|
|
52
|
+
|
|
53
|
+
const placeholder = StyleSheet.create({
|
|
54
|
+
box: { backgroundColor: '#222222', alignItems: 'center', justifyContent: 'center', padding: 8 },
|
|
55
|
+
label: { color: '#dddddd', fontSize: 12, textAlign: 'center' },
|
|
56
|
+
});
|
package/src/VoiceView.tsx
CHANGED
|
@@ -22,7 +22,9 @@ export interface VoiceActivity {
|
|
|
22
22
|
energy: number;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
/** Raw payload of the 'voiceActivity' event (the native fast stream that
|
|
26
|
+
* runs only while useVoiceActivity() or CrowdPlayVoiceView is mounted). */
|
|
27
|
+
export interface VoiceActivityEvent {
|
|
26
28
|
agentPresent: boolean;
|
|
27
29
|
agentSpeaking: boolean;
|
|
28
30
|
agentLevel: number;
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
|
|
25
|
+
import type { VoiceActivityEvent, VoiceCaption } from './VoiceView';
|
|
25
26
|
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
28
|
// Types
|
|
@@ -186,6 +187,13 @@ export interface CrowdPlayEvents {
|
|
|
186
187
|
/** The voice AI asked the app to run a tool (D-103). Prefer
|
|
187
188
|
* CrowdPlay.onAgentToolCall(), which answers for you. */
|
|
188
189
|
agentToolCall: { callId: string; name: string; argumentsJSON: string };
|
|
190
|
+
/** Voice-AI activity (agent present/speaking, local level). Flows ONLY
|
|
191
|
+
* while the native fast stream is running, which useVoiceActivity() and
|
|
192
|
+
* CrowdPlayVoiceView start on mount and stop on unmount; prefer those. */
|
|
193
|
+
voiceActivity: VoiceActivityEvent;
|
|
194
|
+
/** The latest live transcript line from the voice agent; see
|
|
195
|
+
* useLatestCaption(). */
|
|
196
|
+
caption: VoiceCaption;
|
|
189
197
|
}
|
|
190
198
|
|
|
191
199
|
export type CrowdPlayEventName = keyof CrowdPlayEvents;
|
|
@@ -210,6 +218,9 @@ interface NativeCrowdPlay {
|
|
|
210
218
|
setAudioOutput(output: string): void;
|
|
211
219
|
snapshot(): Promise<RecordingSnapshot | null>;
|
|
212
220
|
doctor(): Promise<DoctorCheck[]>;
|
|
221
|
+
/** Start/stop the 'voiceActivity' fast stream (useVoiceActivity owns this). */
|
|
222
|
+
startVoiceActivityUpdates(): void;
|
|
223
|
+
stopVoiceActivityUpdates(): void;
|
|
213
224
|
addListener(eventName: string): void;
|
|
214
225
|
removeListeners(count: number): void;
|
|
215
226
|
}
|
|
@@ -218,10 +229,11 @@ function native(): NativeCrowdPlay {
|
|
|
218
229
|
const module = NativeModules.CrowdPlayReactNative as NativeCrowdPlay | undefined;
|
|
219
230
|
if (!module) {
|
|
220
231
|
throw new Error(
|
|
221
|
-
"crowdplaysdk: native module not found.
|
|
222
|
-
"
|
|
223
|
-
"
|
|
224
|
-
"
|
|
232
|
+
"crowdplaysdk: native module not found. Expo: add \"crowdplaysdk\" to " +
|
|
233
|
+
"\"plugins\" in app.json and rebuild (npx expo prebuild, a development " +
|
|
234
|
+
"build, or eas build); Expo Go cannot load native modules. Bare React " +
|
|
235
|
+
"Native: run ruby node_modules/crowdplaysdk/ios/wire.rb <YourProjectName> " +
|
|
236
|
+
"from ios/ after pod install; see the package README. " +
|
|
225
237
|
(Platform.OS !== 'ios' ? `(platform '${Platform.OS}' is not supported yet; iOS only.)` : '')
|
|
226
238
|
);
|
|
227
239
|
}
|
|
@@ -408,4 +420,4 @@ export default CrowdPlay;
|
|
|
408
420
|
export { CrowdPlayConsentScreen } from './ConsentScreen';
|
|
409
421
|
export { CrowdPlayVideoView } from './VideoView';
|
|
410
422
|
export { CrowdPlayVoiceView, VoiceReactive, useVoiceActivity, useLatestCaption } from './VoiceView';
|
|
411
|
-
export type { CrowdPlayVoiceViewProps, VoiceActivity, VoicePhase, VoiceCaption } from './VoiceView';
|
|
423
|
+
export type { CrowdPlayVoiceViewProps, VoiceActivity, VoiceActivityEvent, VoicePhase, VoiceCaption } from './VoiceView';
|