crowdplaysdk 0.3.1 → 0.3.3

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.
@@ -122,6 +122,38 @@ final class CrowdPlayRNCore {
122
122
  message: engine.micPolicyViolation ?? "")
123
123
  }
124
124
 
125
+ // MARK: - Voice-agent activity (M8 voice UI)
126
+
127
+ private var voiceTimer: Timer?
128
+
129
+ /// Fast (0.15 s) voice-activity stream for the animated voice UI —
130
+ /// runs only while some JS component is subscribed, so the bridge
131
+ /// stays quiet for apps that never render it.
132
+ func startVoiceActivity() {
133
+ guard voiceTimer == nil else { return }
134
+ voiceTimer = Timer.scheduledTimer(withTimeInterval: 0.15, repeats: true) { _ in
135
+ Task { @MainActor in CrowdPlayRNCore.shared.voiceTick() }
136
+ }
137
+ }
138
+
139
+ func stopVoiceActivity() {
140
+ voiceTimer?.invalidate()
141
+ voiceTimer = nil
142
+ }
143
+
144
+ private func voiceTick() {
145
+ guard emitter != nil else { return }
146
+ let activity = engine.voiceActivity()
147
+ send("crowdplay:voiceActivity", [
148
+ "agentPresent": activity.agentPresent,
149
+ "agentSpeaking": activity.agentSpeaking,
150
+ "agentLevel": activity.agentLevel,
151
+ "localLevelDbfs": activity.localLevelDbfs as Any,
152
+ "localSpeaking": activity.localSpeaking,
153
+ "connected": engine.phase == .connected,
154
+ ])
155
+ }
156
+
125
157
  private func updateWarning(_ kind: String, active: Bool, message: String) {
126
158
  let was = activeWarnings.contains(kind)
127
159
  guard was != active else { return }
@@ -178,7 +210,15 @@ public final class CrowdPlayRNModule: RCTEventEmitter {
178
210
  public override static func requiresMainQueueSetup() -> Bool { true }
179
211
 
180
212
  public override func supportedEvents() -> [String]! {
181
- ["crowdplay:phase", "crowdplay:recording", "crowdplay:uploads", "crowdplay:participants", "crowdplay:warning", "crowdplay:audioRoute"]
213
+ ["crowdplay:phase", "crowdplay:recording", "crowdplay:uploads", "crowdplay:participants", "crowdplay:warning", "crowdplay:audioRoute", "crowdplay:voiceActivity"]
214
+ }
215
+
216
+ @objc public func startVoiceActivityUpdates() {
217
+ Task { @MainActor in CrowdPlayRNCore.shared.startVoiceActivity() }
218
+ }
219
+
220
+ @objc public func stopVoiceActivityUpdates() {
221
+ Task { @MainActor in CrowdPlayRNCore.shared.stopVoiceActivity() }
182
222
  }
183
223
 
184
224
  public override func startObserving() {
@@ -27,6 +27,8 @@ RCT_EXTERN_METHOD(setAudioOutput : (NSString *)output)
27
27
  RCT_EXTERN_METHOD(retryRecording)
28
28
  RCT_EXTERN_METHOD(retryUploads)
29
29
  RCT_EXTERN_METHOD(warmUp)
30
+ RCT_EXTERN_METHOD(startVoiceActivityUpdates)
31
+ RCT_EXTERN_METHOD(stopVoiceActivityUpdates)
30
32
  RCT_EXTERN_METHOD(snapshot : (RCTPromiseResolveBlock)resolve
31
33
  rejecter : (RCTPromiseRejectBlock)reject)
32
34
  RCT_EXTERN_METHOD(doctor : (RCTPromiseResolveBlock)resolve
package/ios/wire.rb CHANGED
@@ -25,7 +25,7 @@ SDK_URL = 'https://github.com/symbiateam/crowdplaysdk'
25
25
  # The ENGINE release this bridge was tested against. The npm package
26
26
  # version may be AHEAD of this (bridge/docs fixes ship without a new
27
27
  # engine binary); that is deliberate, not drift.
28
- SDK_VERSION = '0.3.1'
28
+ SDK_VERSION = '0.3.3'
29
29
  BRIDGE_FILES = ['CrowdPlayRNModule.swift', 'CrowdPlayRNVideoView.swift', 'CrowdPlayReactNative.m'].freeze
30
30
 
31
31
  project_name = ARGV[0] or abort 'usage: ruby wire.rb <YourProjectName>'
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The standard voice-AI visual (founders, 2026-08-22): a breathing orb in
3
+ * the style of realtime assistants — calm while idle, a cool ring while the
4
+ * user talks, an energetic pulse while the AI speaks. The React Native
5
+ * counterpart of the native SDK's CrowdPlayVoiceView, driven by the same
6
+ * engine signal over a fast bridge event that runs only while mounted.
7
+ *
8
+ * Two levels of customization:
9
+ * 1. Props on <CrowdPlayVoiceView> — colors and size of the built-in orb.
10
+ * 2. useVoiceActivity() — the phase + smoothed energy underneath, for apps
11
+ * that want to draw something entirely their own.
12
+ */
13
+ import React from 'react';
14
+ export type VoicePhase = 'connecting' | 'idle' | 'listening' | 'speaking';
15
+ export interface VoiceActivity {
16
+ phase: VoicePhase;
17
+ /** 0…1, smoothed (fast attack, slow release). */
18
+ energy: number;
19
+ }
20
+ /** Live phase + energy from the call engine. Subscribing starts the native
21
+ * fast stream; the last unmount stops it. */
22
+ export declare function useVoiceActivity(): VoiceActivity;
23
+ export interface CrowdPlayVoiceViewProps {
24
+ /** Orb color while the AI is speaking. */
25
+ agentColor?: string;
26
+ /** Ring color while the user is speaking. */
27
+ listeningColor?: string;
28
+ /** Resting color (dimmed while connecting). */
29
+ idleColor?: string;
30
+ /** Diameter in points. */
31
+ size?: number;
32
+ }
33
+ export declare function CrowdPlayVoiceView({ agentColor, listeningColor, idleColor, size, }: CrowdPlayVoiceViewProps): React.JSX.Element;
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ /**
3
+ * The standard voice-AI visual (founders, 2026-08-22): a breathing orb in
4
+ * the style of realtime assistants — calm while idle, a cool ring while the
5
+ * user talks, an energetic pulse while the AI speaks. The React Native
6
+ * counterpart of the native SDK's CrowdPlayVoiceView, driven by the same
7
+ * engine signal over a fast bridge event that runs only while mounted.
8
+ *
9
+ * Two levels of customization:
10
+ * 1. Props on <CrowdPlayVoiceView> — colors and size of the built-in orb.
11
+ * 2. useVoiceActivity() — the phase + smoothed energy underneath, for apps
12
+ * that want to draw something entirely their own.
13
+ */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || (function () {
31
+ var ownKeys = function(o) {
32
+ ownKeys = Object.getOwnPropertyNames || function (o) {
33
+ var ar = [];
34
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
+ return ar;
36
+ };
37
+ return ownKeys(o);
38
+ };
39
+ return function (mod) {
40
+ if (mod && mod.__esModule) return mod;
41
+ var result = {};
42
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
+ __setModuleDefault(result, mod);
44
+ return result;
45
+ };
46
+ })();
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.useVoiceActivity = useVoiceActivity;
49
+ exports.CrowdPlayVoiceView = CrowdPlayVoiceView;
50
+ const react_1 = __importStar(require("react"));
51
+ const react_native_1 = require("react-native");
52
+ /** Live phase + energy from the call engine. Subscribing starts the native
53
+ * fast stream; the last unmount stops it. */
54
+ function useVoiceActivity() {
55
+ const [activity, setActivity] = (0, react_1.useState)({ phase: 'connecting', energy: 0 });
56
+ const energyRef = (0, react_1.useRef)(0);
57
+ (0, react_1.useEffect)(() => {
58
+ const module = react_native_1.NativeModules.CrowdPlayReactNative;
59
+ if (!module)
60
+ return;
61
+ const emitter = new react_native_1.NativeEventEmitter(module);
62
+ const sub = emitter.addListener('crowdplay:voiceActivity', (e) => {
63
+ let phase;
64
+ let target;
65
+ if (!e.connected || !e.agentPresent) {
66
+ phase = 'connecting';
67
+ target = 0;
68
+ }
69
+ else if (e.agentSpeaking) {
70
+ phase = 'speaking';
71
+ target = 0.55 + Math.min(0.45, e.agentLevel * 3.0);
72
+ }
73
+ else if (e.localSpeaking) {
74
+ phase = 'listening';
75
+ const db = e.localLevelDbfs ?? -50;
76
+ target = Math.max(0.2, Math.min(1.0, (db + 50) / 40));
77
+ }
78
+ else {
79
+ phase = 'idle';
80
+ target = 0;
81
+ }
82
+ // Fast attack, slow release — speech onsets snap, tails breathe out.
83
+ const rate = target > energyRef.current ? 0.55 : 0.12;
84
+ energyRef.current += (target - energyRef.current) * rate;
85
+ setActivity({ phase, energy: energyRef.current });
86
+ });
87
+ module.startVoiceActivityUpdates?.();
88
+ return () => {
89
+ sub.remove();
90
+ module.stopVoiceActivityUpdates?.();
91
+ };
92
+ }, []);
93
+ return activity;
94
+ }
95
+ function CrowdPlayVoiceView({ agentColor = '#598CFF', listeningColor = '#4DD9B3', idleColor = '#BFBFBF', size = 180, }) {
96
+ const { phase, energy } = useVoiceActivity();
97
+ const breathe = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
98
+ const scale = (0, react_1.useRef)(new react_native_1.Animated.Value(0.62)).current;
99
+ const halo = (0, react_1.useRef)(new react_native_1.Animated.Value(0.85)).current;
100
+ // Continuous idle breathing, independent of the event stream.
101
+ (0, react_1.useEffect)(() => {
102
+ const loop = react_native_1.Animated.loop(react_native_1.Animated.sequence([
103
+ react_native_1.Animated.timing(breathe, {
104
+ toValue: 1, duration: 2200, easing: react_native_1.Easing.inOut(react_native_1.Easing.sin), useNativeDriver: true,
105
+ }),
106
+ react_native_1.Animated.timing(breathe, {
107
+ toValue: 0, duration: 2200, easing: react_native_1.Easing.inOut(react_native_1.Easing.sin), useNativeDriver: true,
108
+ }),
109
+ ]));
110
+ loop.start();
111
+ return () => loop.stop();
112
+ }, [breathe]);
113
+ // Energy drives the core + halo scale; spring gives the organic wobble
114
+ // that the native version synthesizes with sines.
115
+ (0, react_1.useEffect)(() => {
116
+ react_native_1.Animated.spring(scale, {
117
+ toValue: 0.62 + 0.22 * energy, useNativeDriver: true,
118
+ speed: 20, bounciness: 12,
119
+ }).start();
120
+ react_native_1.Animated.spring(halo, {
121
+ toValue: 0.85 + 0.35 * energy, useNativeDriver: true,
122
+ speed: 14, bounciness: 8,
123
+ }).start();
124
+ }, [energy, scale, halo]);
125
+ const color = phase === 'speaking' ? agentColor
126
+ : phase === 'listening' ? listeningColor
127
+ : idleColor;
128
+ const dim = phase === 'connecting' ? 0.45 : 1;
129
+ const breatheScale = breathe.interpolate({ inputRange: [0, 1], outputRange: [1, 1.05] });
130
+ return (<react_native_1.View style={[styles.container, { width: size, height: size }]} accessibilityLabel={phase === 'speaking' ? 'Assistant is speaking'
131
+ : phase === 'listening' ? 'Listening'
132
+ : phase === 'idle' ? 'Assistant is ready'
133
+ : 'Connecting to assistant'}>
134
+ <react_native_1.Animated.View style={[styles.circle, {
135
+ width: size, height: size, borderRadius: size / 2,
136
+ backgroundColor: color,
137
+ opacity: 0.18 * dim + 0.25 * energy,
138
+ transform: [{ scale: react_native_1.Animated.multiply(halo, breatheScale) }],
139
+ }]}/>
140
+ {phase === 'listening' && (<react_native_1.View style={[styles.circle, {
141
+ width: size * 0.8, height: size * 0.8, borderRadius: size * 0.4,
142
+ borderWidth: Math.max(2, size * 0.015), borderColor: color,
143
+ backgroundColor: 'transparent',
144
+ }]}/>)}
145
+ <react_native_1.Animated.View style={[styles.circle, {
146
+ width: size, height: size, borderRadius: size / 2,
147
+ backgroundColor: color,
148
+ opacity: dim,
149
+ transform: [{ scale: react_native_1.Animated.multiply(scale, breatheScale) }],
150
+ }]}/>
151
+ </react_native_1.View>);
152
+ }
153
+ const styles = react_native_1.StyleSheet.create({
154
+ container: { alignItems: 'center', justifyContent: 'center' },
155
+ circle: { position: 'absolute' },
156
+ });
package/lib/index.d.ts CHANGED
@@ -193,3 +193,5 @@ declare const CrowdPlay: {
193
193
  export default CrowdPlay;
194
194
  export { CrowdPlayConsentScreen } from './ConsentScreen';
195
195
  export { CrowdPlayVideoView } from './VideoView';
196
+ export { CrowdPlayVoiceView, useVoiceActivity } from './VoiceView';
197
+ export type { CrowdPlayVoiceViewProps, VoiceActivity, VoicePhase } from './VoiceView';
package/lib/index.js CHANGED
@@ -22,7 +22,7 @@
22
22
  * path that records without it.
23
23
  */
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.CrowdPlayVideoView = exports.CrowdPlayConsentScreen = void 0;
25
+ exports.useVoiceActivity = exports.CrowdPlayVoiceView = exports.CrowdPlayVideoView = exports.CrowdPlayConsentScreen = void 0;
26
26
  const react_native_1 = require("react-native");
27
27
  function native() {
28
28
  const module = react_native_1.NativeModules.CrowdPlayReactNative;
@@ -143,3 +143,6 @@ var ConsentScreen_1 = require("./ConsentScreen");
143
143
  Object.defineProperty(exports, "CrowdPlayConsentScreen", { enumerable: true, get: function () { return ConsentScreen_1.CrowdPlayConsentScreen; } });
144
144
  var VideoView_1 = require("./VideoView");
145
145
  Object.defineProperty(exports, "CrowdPlayVideoView", { enumerable: true, get: function () { return VideoView_1.CrowdPlayVideoView; } });
146
+ var VoiceView_1 = require("./VoiceView");
147
+ Object.defineProperty(exports, "CrowdPlayVoiceView", { enumerable: true, get: function () { return VoiceView_1.CrowdPlayVoiceView; } });
148
+ Object.defineProperty(exports, "useVoiceActivity", { enumerable: true, get: function () { return VoiceView_1.useVoiceActivity; } });
package/llms.txt CHANGED
@@ -147,7 +147,17 @@ Default export `CrowdPlay`:
147
147
  handler: (payload: CrowdPlayEvents[E]) => void): { remove(): void }
148
148
 
149
149
  Named exports: CrowdPlayConsentScreen (props: onConsent(grant)), CrowdPlayVideoView
150
- (props: participant: string | 'local', plus standard View style).
150
+ (props: participant: string | 'local', plus standard View style),
151
+ CrowdPlayVoiceView (props: agentColor?, listeningColor?, idleColor?, size?),
152
+ useVoiceActivity() -> { phase: 'connecting'|'idle'|'listening'|'speaking', energy: 0…1 }.
153
+
154
+ Voice-AI apps: when the app includes the CrowdPlay voice AI, render the AI
155
+ participant with <CrowdPlayVoiceView /> — the standard animated orb that
156
+ breathes when idle, rings while the user talks, and pulses while the AI
157
+ talks. The AI publishes NO video: never show it as a camera tile or an
158
+ empty box. Detect it in the participants event by identity prefix
159
+ "agent-" or name "agent". For a fully custom visual, drive your own
160
+ component from useVoiceActivity().
151
161
 
152
162
  Types:
153
163
  CrowdPlayConfig { serverUrl; appKey; audioOnly?=false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "crowdplaysdk",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
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",
@@ -0,0 +1,175 @@
1
+ /**
2
+ * The standard voice-AI visual (founders, 2026-08-22): a breathing orb in
3
+ * the style of realtime assistants — calm while idle, a cool ring while the
4
+ * user talks, an energetic pulse while the AI speaks. The React Native
5
+ * counterpart of the native SDK's CrowdPlayVoiceView, driven by the same
6
+ * engine signal over a fast bridge event that runs only while mounted.
7
+ *
8
+ * Two levels of customization:
9
+ * 1. Props on <CrowdPlayVoiceView> — colors and size of the built-in orb.
10
+ * 2. useVoiceActivity() — the phase + smoothed energy underneath, for apps
11
+ * that want to draw something entirely their own.
12
+ */
13
+
14
+ import React, { useEffect, useRef, useState } from 'react';
15
+ import { Animated, Easing, NativeEventEmitter, NativeModules, StyleSheet, View } from 'react-native';
16
+
17
+ export type VoicePhase = 'connecting' | 'idle' | 'listening' | 'speaking';
18
+
19
+ export interface VoiceActivity {
20
+ phase: VoicePhase;
21
+ /** 0…1, smoothed (fast attack, slow release). */
22
+ energy: number;
23
+ }
24
+
25
+ interface VoiceActivityEvent {
26
+ agentPresent: boolean;
27
+ agentSpeaking: boolean;
28
+ agentLevel: number;
29
+ localLevelDbfs: number | null;
30
+ localSpeaking: boolean;
31
+ connected: boolean;
32
+ }
33
+
34
+ /** Live phase + energy from the call engine. Subscribing starts the native
35
+ * fast stream; the last unmount stops it. */
36
+ export function useVoiceActivity(): VoiceActivity {
37
+ const [activity, setActivity] = useState<VoiceActivity>({ phase: 'connecting', energy: 0 });
38
+ const energyRef = useRef(0);
39
+
40
+ useEffect(() => {
41
+ const module = NativeModules.CrowdPlayReactNative;
42
+ if (!module) return;
43
+ const emitter = new NativeEventEmitter(module);
44
+ const sub = emitter.addListener('crowdplay:voiceActivity', (e: VoiceActivityEvent) => {
45
+ let phase: VoicePhase;
46
+ let target: number;
47
+ if (!e.connected || !e.agentPresent) {
48
+ phase = 'connecting';
49
+ target = 0;
50
+ } else if (e.agentSpeaking) {
51
+ phase = 'speaking';
52
+ target = 0.55 + Math.min(0.45, e.agentLevel * 3.0);
53
+ } else if (e.localSpeaking) {
54
+ phase = 'listening';
55
+ const db = e.localLevelDbfs ?? -50;
56
+ target = Math.max(0.2, Math.min(1.0, (db + 50) / 40));
57
+ } else {
58
+ phase = 'idle';
59
+ target = 0;
60
+ }
61
+ // Fast attack, slow release — speech onsets snap, tails breathe out.
62
+ const rate = target > energyRef.current ? 0.55 : 0.12;
63
+ energyRef.current += (target - energyRef.current) * rate;
64
+ setActivity({ phase, energy: energyRef.current });
65
+ });
66
+ module.startVoiceActivityUpdates?.();
67
+ return () => {
68
+ sub.remove();
69
+ module.stopVoiceActivityUpdates?.();
70
+ };
71
+ }, []);
72
+
73
+ return activity;
74
+ }
75
+
76
+ export interface CrowdPlayVoiceViewProps {
77
+ /** Orb color while the AI is speaking. */
78
+ agentColor?: string;
79
+ /** Ring color while the user is speaking. */
80
+ listeningColor?: string;
81
+ /** Resting color (dimmed while connecting). */
82
+ idleColor?: string;
83
+ /** Diameter in points. */
84
+ size?: number;
85
+ }
86
+
87
+ export function CrowdPlayVoiceView({
88
+ agentColor = '#598CFF',
89
+ listeningColor = '#4DD9B3',
90
+ idleColor = '#BFBFBF',
91
+ size = 180,
92
+ }: CrowdPlayVoiceViewProps): React.JSX.Element {
93
+ const { phase, energy } = useVoiceActivity();
94
+ const breathe = useRef(new Animated.Value(0)).current;
95
+ const scale = useRef(new Animated.Value(0.62)).current;
96
+ const halo = useRef(new Animated.Value(0.85)).current;
97
+
98
+ // Continuous idle breathing, independent of the event stream.
99
+ useEffect(() => {
100
+ const loop = Animated.loop(
101
+ Animated.sequence([
102
+ Animated.timing(breathe, {
103
+ toValue: 1, duration: 2200, easing: Easing.inOut(Easing.sin), useNativeDriver: true,
104
+ }),
105
+ Animated.timing(breathe, {
106
+ toValue: 0, duration: 2200, easing: Easing.inOut(Easing.sin), useNativeDriver: true,
107
+ }),
108
+ ]),
109
+ );
110
+ loop.start();
111
+ return () => loop.stop();
112
+ }, [breathe]);
113
+
114
+ // Energy drives the core + halo scale; spring gives the organic wobble
115
+ // that the native version synthesizes with sines.
116
+ useEffect(() => {
117
+ Animated.spring(scale, {
118
+ toValue: 0.62 + 0.22 * energy, useNativeDriver: true,
119
+ speed: 20, bounciness: 12,
120
+ }).start();
121
+ Animated.spring(halo, {
122
+ toValue: 0.85 + 0.35 * energy, useNativeDriver: true,
123
+ speed: 14, bounciness: 8,
124
+ }).start();
125
+ }, [energy, scale, halo]);
126
+
127
+ const color = phase === 'speaking' ? agentColor
128
+ : phase === 'listening' ? listeningColor
129
+ : idleColor;
130
+ const dim = phase === 'connecting' ? 0.45 : 1;
131
+ const breatheScale = breathe.interpolate({ inputRange: [0, 1], outputRange: [1, 1.05] });
132
+
133
+ return (
134
+ <View
135
+ style={[styles.container, { width: size, height: size }]}
136
+ accessibilityLabel={
137
+ phase === 'speaking' ? 'Assistant is speaking'
138
+ : phase === 'listening' ? 'Listening'
139
+ : phase === 'idle' ? 'Assistant is ready'
140
+ : 'Connecting to assistant'
141
+ }
142
+ >
143
+ <Animated.View
144
+ style={[styles.circle, {
145
+ width: size, height: size, borderRadius: size / 2,
146
+ backgroundColor: color,
147
+ opacity: 0.18 * dim + 0.25 * energy,
148
+ transform: [{ scale: Animated.multiply(halo, breatheScale) }],
149
+ }]}
150
+ />
151
+ {phase === 'listening' && (
152
+ <View
153
+ style={[styles.circle, {
154
+ width: size * 0.8, height: size * 0.8, borderRadius: size * 0.4,
155
+ borderWidth: Math.max(2, size * 0.015), borderColor: color,
156
+ backgroundColor: 'transparent',
157
+ }]}
158
+ />
159
+ )}
160
+ <Animated.View
161
+ style={[styles.circle, {
162
+ width: size, height: size, borderRadius: size / 2,
163
+ backgroundColor: color,
164
+ opacity: dim,
165
+ transform: [{ scale: Animated.multiply(scale, breatheScale) }],
166
+ }]}
167
+ />
168
+ </View>
169
+ );
170
+ }
171
+
172
+ const styles = StyleSheet.create({
173
+ container: { alignItems: 'center', justifyContent: 'center' },
174
+ circle: { position: 'absolute' },
175
+ });
package/src/index.ts CHANGED
@@ -297,3 +297,5 @@ const CrowdPlay = {
297
297
  export default CrowdPlay;
298
298
  export { CrowdPlayConsentScreen } from './ConsentScreen';
299
299
  export { CrowdPlayVideoView } from './VideoView';
300
+ export { CrowdPlayVoiceView, useVoiceActivity } from './VoiceView';
301
+ export type { CrowdPlayVoiceViewProps, VoiceActivity, VoicePhase } from './VoiceView';