crowdplaysdk 0.2.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.
- package/Examples/App.tsx +164 -0
- package/Examples/tsconfig.json +9 -0
- package/README.md +143 -0
- package/ios/CrowdPlayRNModule.swift +324 -0
- package/ios/CrowdPlayRNVideoView.swift +76 -0
- package/ios/CrowdPlayReactNative.m +42 -0
- package/ios/wire.rb +81 -0
- package/lib/ConsentScreen.d.ts +20 -0
- package/lib/ConsentScreen.js +110 -0
- package/lib/VideoView.d.ts +21 -0
- package/lib/VideoView.js +24 -0
- package/lib/index.d.ts +189 -0
- package/lib/index.js +145 -0
- package/llms.txt +224 -0
- package/package.json +44 -0
- package/src/ConsentScreen.tsx +93 -0
- package/src/VideoView.tsx +35 -0
- package/src/index.ts +294 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import CrowdPlaySDK
|
|
2
|
+
#if canImport(React)
|
|
3
|
+
import React
|
|
4
|
+
#endif
|
|
5
|
+
import UIKit
|
|
6
|
+
|
|
7
|
+
/// Native video tile hosted in the RN layout. Resolves the requested
|
|
8
|
+
/// participant's track from the shared engine on a short poll — idempotent
|
|
9
|
+
/// and reconnect-proof, and the video itself never touches JavaScript.
|
|
10
|
+
final class CrowdPlayRNVideoView: UIView {
|
|
11
|
+
private let videoView = VideoView()
|
|
12
|
+
private var resolveTimer: Timer?
|
|
13
|
+
|
|
14
|
+
@objc var participant: NSString = "" {
|
|
15
|
+
didSet { resolveTrack() }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@objc var mirror: Bool = false {
|
|
19
|
+
didSet { videoView.mirrorMode = mirror ? .mirror : .off }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override init(frame: CGRect) {
|
|
23
|
+
super.init(frame: frame)
|
|
24
|
+
videoView.layoutMode = .fill
|
|
25
|
+
addSubview(videoView)
|
|
26
|
+
resolveTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
|
|
27
|
+
self?.resolveTrack()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@available(*, unavailable)
|
|
32
|
+
required init?(coder _: NSCoder) { fatalError("not supported") }
|
|
33
|
+
|
|
34
|
+
deinit {
|
|
35
|
+
resolveTimer?.invalidate()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override func layoutSubviews() {
|
|
39
|
+
super.layoutSubviews()
|
|
40
|
+
videoView.frame = bounds
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override func willMove(toWindow newWindow: UIWindow?) {
|
|
44
|
+
super.willMove(toWindow: newWindow)
|
|
45
|
+
if newWindow == nil {
|
|
46
|
+
resolveTimer?.invalidate()
|
|
47
|
+
resolveTimer = nil
|
|
48
|
+
videoView.track = nil
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private func resolveTrack() {
|
|
53
|
+
let requested = participant as String
|
|
54
|
+
let room = CrowdPlayRNCore.shared.engine.room
|
|
55
|
+
let track: VideoTrack?
|
|
56
|
+
if requested == "local" || requested.isEmpty {
|
|
57
|
+
track = room.localParticipant.firstCameraVideoTrack
|
|
58
|
+
} else {
|
|
59
|
+
track = room.remoteParticipants.values
|
|
60
|
+
.first { $0.identity?.stringValue == requested }?
|
|
61
|
+
.firstCameraVideoTrack
|
|
62
|
+
}
|
|
63
|
+
if videoView.track?.sid != track?.sid || (videoView.track == nil) != (track == nil) {
|
|
64
|
+
videoView.track = track
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@objc(CrowdPlayVideoViewManager)
|
|
70
|
+
public final class CrowdPlayVideoViewManager: RCTViewManager {
|
|
71
|
+
public override static func requiresMainQueueSetup() -> Bool { true }
|
|
72
|
+
|
|
73
|
+
public override func view() -> UIView! {
|
|
74
|
+
CrowdPlayRNVideoView()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Objective-C registration for the CrowdPlay RN bridge. Add this file AND the
|
|
2
|
+
// two Swift files to your app target (they compile alongside your app, with
|
|
3
|
+
// CrowdPlaySDK coming from the Swift package — see the package README).
|
|
4
|
+
#import <React/RCTBridgeModule.h>
|
|
5
|
+
#import <React/RCTEventEmitter.h>
|
|
6
|
+
#import <React/RCTViewManager.h>
|
|
7
|
+
|
|
8
|
+
@interface RCT_EXTERN_MODULE (CrowdPlayReactNative, RCTEventEmitter)
|
|
9
|
+
|
|
10
|
+
RCT_EXTERN_METHOD(configure : (NSDictionary *)config)
|
|
11
|
+
RCT_EXTERN_METHOD(consentText : (RCTPromiseResolveBlock)resolve
|
|
12
|
+
rejecter : (RCTPromiseRejectBlock)reject)
|
|
13
|
+
RCT_EXTERN_METHOD(join : (NSString *)displayName
|
|
14
|
+
roomCode : (NSString *)roomCode
|
|
15
|
+
consentGrantedAtMs : (nonnull NSNumber *)consentGrantedAtMs
|
|
16
|
+
resolver : (RCTPromiseResolveBlock)resolve
|
|
17
|
+
rejecter : (RCTPromiseRejectBlock)reject)
|
|
18
|
+
RCT_EXTERN_METHOD(leave : (RCTPromiseResolveBlock)resolve
|
|
19
|
+
rejecter : (RCTPromiseRejectBlock)reject)
|
|
20
|
+
RCT_EXTERN_METHOD(setMicMuted : (BOOL)muted
|
|
21
|
+
resolver : (RCTPromiseResolveBlock)resolve
|
|
22
|
+
rejecter : (RCTPromiseRejectBlock)reject)
|
|
23
|
+
RCT_EXTERN_METHOD(setCameraEnabled : (BOOL)enabled
|
|
24
|
+
resolver : (RCTPromiseResolveBlock)resolve
|
|
25
|
+
rejecter : (RCTPromiseRejectBlock)reject)
|
|
26
|
+
RCT_EXTERN_METHOD(setAudioOutput : (NSString *)output)
|
|
27
|
+
RCT_EXTERN_METHOD(retryRecording)
|
|
28
|
+
RCT_EXTERN_METHOD(retryUploads)
|
|
29
|
+
RCT_EXTERN_METHOD(warmUp)
|
|
30
|
+
RCT_EXTERN_METHOD(snapshot : (RCTPromiseResolveBlock)resolve
|
|
31
|
+
rejecter : (RCTPromiseRejectBlock)reject)
|
|
32
|
+
RCT_EXTERN_METHOD(doctor : (RCTPromiseResolveBlock)resolve
|
|
33
|
+
rejecter : (RCTPromiseRejectBlock)reject)
|
|
34
|
+
|
|
35
|
+
@end
|
|
36
|
+
|
|
37
|
+
@interface RCT_EXTERN_MODULE (CrowdPlayVideoViewManager, RCTViewManager)
|
|
38
|
+
|
|
39
|
+
RCT_EXPORT_VIEW_PROPERTY(participant, NSString)
|
|
40
|
+
RCT_EXPORT_VIEW_PROPERTY(mirror, BOOL)
|
|
41
|
+
|
|
42
|
+
@end
|
package/ios/wire.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Wires crowdplaysdk into an iOS app project. Run from your app's
|
|
2
|
+
# ios/ directory after `pod install`:
|
|
3
|
+
#
|
|
4
|
+
# ruby node_modules/crowdplaysdk/ios/wire.rb <YourProjectName>
|
|
5
|
+
#
|
|
6
|
+
# What it does (idempotent — safe to re-run):
|
|
7
|
+
# 1. adds the CrowdPlaySDK Swift package (compiled engine) to the project
|
|
8
|
+
# 2. copies the three bridge files into your app group + target
|
|
9
|
+
# 3. creates/points the Swift bridging header at the React headers
|
|
10
|
+
#
|
|
11
|
+
# Requires the `xcodeproj` gem (ships with CocoaPods installs; otherwise
|
|
12
|
+
# `gem install xcodeproj`).
|
|
13
|
+
|
|
14
|
+
require 'fileutils'
|
|
15
|
+
require 'xcodeproj'
|
|
16
|
+
|
|
17
|
+
SDK_URL = 'https://github.com/symbiateam/crowdplaysdk'
|
|
18
|
+
SDK_VERSION = '0.2.3'
|
|
19
|
+
BRIDGE_FILES = ['CrowdPlayRNModule.swift', 'CrowdPlayRNVideoView.swift', 'CrowdPlayReactNative.m'].freeze
|
|
20
|
+
|
|
21
|
+
project_name = ARGV[0] or abort 'usage: ruby wire.rb <YourProjectName>'
|
|
22
|
+
project_path = "#{project_name}.xcodeproj"
|
|
23
|
+
abort "#{project_path} not found — run from your app's ios/ directory" unless File.exist?(project_path)
|
|
24
|
+
|
|
25
|
+
package_dir = File.expand_path(File.dirname(__FILE__))
|
|
26
|
+
project = Xcodeproj::Project.open(project_path)
|
|
27
|
+
target = project.targets.find { |t| t.name == project_name } ||
|
|
28
|
+
project.targets.find { |t| t.product_type == 'com.apple.product-type.application' }
|
|
29
|
+
abort 'no application target found' unless target
|
|
30
|
+
|
|
31
|
+
# 1. CrowdPlaySDK Swift package (skip when already present).
|
|
32
|
+
unless project.root_object.package_references.any? { |r| r.respond_to?(:repositoryURL) && r.repositoryURL == SDK_URL }
|
|
33
|
+
pkg = project.new(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference)
|
|
34
|
+
pkg.repositoryURL = SDK_URL
|
|
35
|
+
pkg.requirement = { 'kind' => 'exactVersion', 'version' => SDK_VERSION }
|
|
36
|
+
project.root_object.package_references << pkg
|
|
37
|
+
dep = project.new(Xcodeproj::Project::Object::XCSwiftPackageProductDependency)
|
|
38
|
+
dep.package = pkg
|
|
39
|
+
dep.product_name = 'CrowdPlaySDK'
|
|
40
|
+
target.package_product_dependencies << dep
|
|
41
|
+
puts "+ CrowdPlaySDK package #{SDK_VERSION}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 2. Bridge files into the app group + target.
|
|
45
|
+
group = project.main_group[project_name] || project.main_group
|
|
46
|
+
group_dir = group.real_path.to_s
|
|
47
|
+
BRIDGE_FILES.each do |name|
|
|
48
|
+
destination = File.join(group_dir, name)
|
|
49
|
+
FileUtils.cp(File.join(package_dir, name), destination)
|
|
50
|
+
next if group.files.any? { |f| f.display_name == name }
|
|
51
|
+
|
|
52
|
+
ref = group.new_reference(name)
|
|
53
|
+
target.add_file_references([ref])
|
|
54
|
+
puts "+ #{name}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# 3. Bridging header (created only when the target has none).
|
|
58
|
+
target.build_configurations.each do |config|
|
|
59
|
+
config.build_settings['SWIFT_VERSION'] ||= '5.0'
|
|
60
|
+
# CrowdPlaySDK requires iOS 17; RN templates default far lower. Only the APP
|
|
61
|
+
# target is raised — pods keep their own targets, which is fine.
|
|
62
|
+
current = config.build_settings['IPHONEOS_DEPLOYMENT_TARGET']
|
|
63
|
+
if current.nil? || current.to_f < 17.0
|
|
64
|
+
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '17.0'
|
|
65
|
+
end
|
|
66
|
+
next if config.build_settings['SWIFT_OBJC_BRIDGING_HEADER']
|
|
67
|
+
|
|
68
|
+
header_name = "#{project_name}-Bridging-Header.h"
|
|
69
|
+
header_path = File.join(group_dir, header_name)
|
|
70
|
+
unless File.exist?(header_path)
|
|
71
|
+
File.write(header_path, <<~OBJC)
|
|
72
|
+
#import <React/RCTBridgeModule.h>
|
|
73
|
+
#import <React/RCTEventEmitter.h>
|
|
74
|
+
#import <React/RCTViewManager.h>
|
|
75
|
+
OBJC
|
|
76
|
+
end
|
|
77
|
+
config.build_settings['SWIFT_OBJC_BRIDGING_HEADER'] = "#{project_name}/#{header_name}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
project.save
|
|
81
|
+
puts 'wired. Open the workspace and build, or: xcodebuild -workspace …'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drop-in consent screen. Shows CrowdPlay's locked consent wording (fetched from
|
|
3
|
+
* the native SDK so it can never drift from what the manifest records), a
|
|
4
|
+
* switch, and a continue button that only enables once the participant
|
|
5
|
+
* agrees. Yields the ConsentGrant that join() requires.
|
|
6
|
+
*
|
|
7
|
+
* <CrowdPlayConsentScreen onConsent={(grant) => joinWith(grant)} />
|
|
8
|
+
*
|
|
9
|
+
* Apps with their own consent UI can skip this component: render
|
|
10
|
+
* CrowdPlay.consentText() verbatim and construct { grantedAtMs: Date.now() } at
|
|
11
|
+
* the moment of agreement.
|
|
12
|
+
*/
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { type ConsentGrant } from './index';
|
|
15
|
+
export interface CrowdPlayConsentScreenProps {
|
|
16
|
+
onConsent: (grant: ConsentGrant) => void;
|
|
17
|
+
/** Optional style overrides for the outer container. */
|
|
18
|
+
style?: object;
|
|
19
|
+
}
|
|
20
|
+
export declare function CrowdPlayConsentScreen({ onConsent, style }: CrowdPlayConsentScreenProps): React.JSX.Element;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Drop-in consent screen. Shows CrowdPlay's locked consent wording (fetched from
|
|
4
|
+
* the native SDK so it can never drift from what the manifest records), a
|
|
5
|
+
* switch, and a continue button that only enables once the participant
|
|
6
|
+
* agrees. Yields the ConsentGrant that join() requires.
|
|
7
|
+
*
|
|
8
|
+
* <CrowdPlayConsentScreen onConsent={(grant) => joinWith(grant)} />
|
|
9
|
+
*
|
|
10
|
+
* Apps with their own consent UI can skip this component: render
|
|
11
|
+
* CrowdPlay.consentText() verbatim and construct { grantedAtMs: Date.now() } at
|
|
12
|
+
* the moment of agreement.
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
48
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
49
|
+
};
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.CrowdPlayConsentScreen = CrowdPlayConsentScreen;
|
|
52
|
+
const react_1 = __importStar(require("react"));
|
|
53
|
+
const react_native_1 = require("react-native");
|
|
54
|
+
const index_1 = __importDefault(require("./index"));
|
|
55
|
+
function CrowdPlayConsentScreen({ onConsent, style }) {
|
|
56
|
+
const [text, setText] = (0, react_1.useState)(null);
|
|
57
|
+
const [agreed, setAgreed] = (0, react_1.useState)(false);
|
|
58
|
+
const [agreedAtMs, setAgreedAtMs] = (0, react_1.useState)(null);
|
|
59
|
+
(0, react_1.useEffect)(() => {
|
|
60
|
+
let cancelled = false;
|
|
61
|
+
index_1.default.consentText()
|
|
62
|
+
.then((wording) => {
|
|
63
|
+
if (!cancelled)
|
|
64
|
+
setText(wording);
|
|
65
|
+
})
|
|
66
|
+
.catch(() => {
|
|
67
|
+
if (!cancelled)
|
|
68
|
+
setText('Consent text unavailable — check that CrowdPlay.configure() ran and the native module is installed.');
|
|
69
|
+
});
|
|
70
|
+
return () => {
|
|
71
|
+
cancelled = true;
|
|
72
|
+
};
|
|
73
|
+
}, []);
|
|
74
|
+
return (<react_native_1.View style={[styles.container, style]}>
|
|
75
|
+
<react_native_1.Text style={styles.title}>Recording consent</react_native_1.Text>
|
|
76
|
+
{text === null ? (<react_native_1.ActivityIndicator />) : (<react_native_1.Text style={styles.body}>{text}</react_native_1.Text>)}
|
|
77
|
+
<react_native_1.View style={styles.row}>
|
|
78
|
+
<react_native_1.Switch value={agreed} onValueChange={(value) => {
|
|
79
|
+
setAgreed(value);
|
|
80
|
+
setAgreedAtMs(value ? Date.now() : null);
|
|
81
|
+
}}/>
|
|
82
|
+
<react_native_1.Text style={styles.agree}>I agree</react_native_1.Text>
|
|
83
|
+
</react_native_1.View>
|
|
84
|
+
<react_native_1.Text style={styles.note}>
|
|
85
|
+
Recording starts automatically when you join, and ends when you leave.
|
|
86
|
+
</react_native_1.Text>
|
|
87
|
+
<react_native_1.Pressable accessibilityRole="button" disabled={!agreed || agreedAtMs === null} onPress={() => {
|
|
88
|
+
if (agreedAtMs !== null)
|
|
89
|
+
onConsent({ grantedAtMs: agreedAtMs });
|
|
90
|
+
}} style={({ pressed }) => [
|
|
91
|
+
styles.button,
|
|
92
|
+
(!agreed || agreedAtMs === null) && styles.buttonDisabled,
|
|
93
|
+
pressed && styles.buttonPressed,
|
|
94
|
+
]}>
|
|
95
|
+
<react_native_1.Text style={styles.buttonLabel}>Continue</react_native_1.Text>
|
|
96
|
+
</react_native_1.Pressable>
|
|
97
|
+
</react_native_1.View>);
|
|
98
|
+
}
|
|
99
|
+
const styles = react_native_1.StyleSheet.create({
|
|
100
|
+
container: { padding: 20, gap: 16 },
|
|
101
|
+
title: { fontSize: 20, fontWeight: '700' },
|
|
102
|
+
body: { fontSize: 15, lineHeight: 21 },
|
|
103
|
+
row: { flexDirection: 'row', alignItems: 'center', gap: 10 },
|
|
104
|
+
agree: { fontSize: 15 },
|
|
105
|
+
note: { fontSize: 12, opacity: 0.6 },
|
|
106
|
+
button: { backgroundColor: '#1f6feb', borderRadius: 8, paddingVertical: 12, alignItems: 'center' },
|
|
107
|
+
buttonDisabled: { opacity: 0.4 },
|
|
108
|
+
buttonPressed: { opacity: 0.8 },
|
|
109
|
+
buttonLabel: { color: 'white', fontSize: 16, fontWeight: '600' },
|
|
110
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native video tile. Renders the LOCAL camera preview or a REMOTE
|
|
3
|
+
* participant's live video (identified by the `identity` from the
|
|
4
|
+
* `participants` event). The video never touches JavaScript — this is a
|
|
5
|
+
* native LiveKit view hosted in your RN layout.
|
|
6
|
+
*
|
|
7
|
+
* <CrowdPlayVideoView participant="local" style={{ width: 120, height: 160 }} />
|
|
8
|
+
* {participants.map(p => (
|
|
9
|
+
* <CrowdPlayVideoView key={p.identity} participant={p.identity} style={styles.tile} />
|
|
10
|
+
* ))}
|
|
11
|
+
*/
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { type ViewProps } from 'react-native';
|
|
14
|
+
export interface CrowdPlayVideoViewProps extends ViewProps {
|
|
15
|
+
/** "local" for the self-view, or a remote participant's identity. */
|
|
16
|
+
participant: string;
|
|
17
|
+
/** Mirror the image (typical for the self-view). Default: true for
|
|
18
|
+
* "local", false otherwise. */
|
|
19
|
+
mirror?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function CrowdPlayVideoView(props: CrowdPlayVideoViewProps): React.JSX.Element;
|
package/lib/VideoView.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Native video tile. Renders the LOCAL camera preview or a REMOTE
|
|
4
|
+
* participant's live video (identified by the `identity` from the
|
|
5
|
+
* `participants` event). The video never touches JavaScript — this is a
|
|
6
|
+
* native LiveKit view hosted in your RN layout.
|
|
7
|
+
*
|
|
8
|
+
* <CrowdPlayVideoView participant="local" style={{ width: 120, height: 160 }} />
|
|
9
|
+
* {participants.map(p => (
|
|
10
|
+
* <CrowdPlayVideoView key={p.identity} participant={p.identity} style={styles.tile} />
|
|
11
|
+
* ))}
|
|
12
|
+
*/
|
|
13
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.CrowdPlayVideoView = CrowdPlayVideoView;
|
|
18
|
+
const react_1 = __importDefault(require("react"));
|
|
19
|
+
const react_native_1 = require("react-native");
|
|
20
|
+
const NativeCrowdPlayVideoView = (0, react_native_1.requireNativeComponent)('CrowdPlayVideoView');
|
|
21
|
+
function CrowdPlayVideoView(props) {
|
|
22
|
+
const { participant, mirror, ...rest } = props;
|
|
23
|
+
return (<NativeCrowdPlayVideoView participant={participant} mirror={mirror ?? participant === 'local'} {...rest}/>);
|
|
24
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crowdplaysdk — CrowdPlay's lossless capture SDK for React Native (iOS).
|
|
3
|
+
*
|
|
4
|
+
* The recording engine is 100% native (CrowdPlaySDK): studio-grade audio
|
|
5
|
+
* (48 kHz / 24-bit, unprocessed) + 1080p30 video captured locally during a
|
|
6
|
+
* live call, clock-synced across participants, uploaded to CrowdPlay with
|
|
7
|
+
* retries and crash recovery. **No media ever crosses the JS bridge** —
|
|
8
|
+
* only control calls and state events — so recording quality is identical
|
|
9
|
+
* to a fully native app.
|
|
10
|
+
*
|
|
11
|
+
* import CrowdPlay, { CrowdPlayConsentScreen, CrowdPlayVideoView } from 'crowdplaysdk';
|
|
12
|
+
*
|
|
13
|
+
* CrowdPlay.configure({ serverUrl: '…', appKey: 'liva_pk_…' });
|
|
14
|
+
* // Show CrowdPlayConsentScreen (or your own UI) → ConsentGrant
|
|
15
|
+
* await CrowdPlay.join({ displayName: 'Sam', roomCode: 'abc', consent });
|
|
16
|
+
* // …
|
|
17
|
+
* await CrowdPlay.leave(); // recording stops; uploads continue automatically
|
|
18
|
+
*
|
|
19
|
+
* Consent is enforced twice: join() throws here without a grant, and the
|
|
20
|
+
* native layer requires the consent record structurally — there is no code
|
|
21
|
+
* path that records without it.
|
|
22
|
+
*/
|
|
23
|
+
/** Live-call (Path A) quality. The RECORDING is always 1080p30 + 24-bit WAV
|
|
24
|
+
* regardless; raising these trades upload + thermal headroom (which the
|
|
25
|
+
* recording needs on long sessions) for a prettier live picture. Values are
|
|
26
|
+
* clamped natively to a safe envelope (≤1280×720, 10–30 fps, ≤2.5 Mbps
|
|
27
|
+
* video, 16–128 kbps audio). */
|
|
28
|
+
export interface CallQuality {
|
|
29
|
+
videoWidth?: number;
|
|
30
|
+
videoHeight?: number;
|
|
31
|
+
videoFps?: number;
|
|
32
|
+
videoBitrate?: number;
|
|
33
|
+
audioBitrate?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface CrowdPlayConfig {
|
|
36
|
+
/** CrowdPlay backend base URL (https, no trailing slash). */
|
|
37
|
+
serverUrl: string;
|
|
38
|
+
/** From the CrowdPlay dashboard. Also your data-access credential. */
|
|
39
|
+
appKey: string;
|
|
40
|
+
/** Video (~2 GB/session) rides cellular too when true. Audio + metadata
|
|
41
|
+
* always upload on any network. Default false. */
|
|
42
|
+
videoUploadsOnCellular?: boolean;
|
|
43
|
+
/** Upload segments while the call records, so the post-call wait is
|
|
44
|
+
* roughly the final segment + manifests. Mid-call transfers run one at
|
|
45
|
+
* a time and pause automatically when call quality drops — the call
|
|
46
|
+
* always wins the uplink. Default true. */
|
|
47
|
+
uploadDuringCall?: boolean;
|
|
48
|
+
callQuality?: CallQuality;
|
|
49
|
+
}
|
|
50
|
+
/** Proof the participant agreed. Produce it with CrowdPlayConsentScreen, or —
|
|
51
|
+
* if you render your own consent UI — call CrowdPlay.consentText() to show the
|
|
52
|
+
* REQUIRED wording and construct the grant at the moment of agreement. */
|
|
53
|
+
export interface ConsentGrant {
|
|
54
|
+
/** Date.now() at the moment the participant agreed. */
|
|
55
|
+
grantedAtMs: number;
|
|
56
|
+
}
|
|
57
|
+
export type Phase = 'idle' | 'connecting' | 'connected' | 'failed';
|
|
58
|
+
export interface RecordingSnapshot {
|
|
59
|
+
/** Seconds of audio safely written so far. */
|
|
60
|
+
seconds: number;
|
|
61
|
+
segmentsClosed: number;
|
|
62
|
+
droppedSamples: number;
|
|
63
|
+
/** Live input level. Speech sits around −25…−45 dBFS. */
|
|
64
|
+
inputLevelDbfs: number;
|
|
65
|
+
/** True after 10 s of silence while unmuted — a dead/disconnected mic.
|
|
66
|
+
* Surface this loudly; it is fixable mid-call. */
|
|
67
|
+
inputSilent: boolean;
|
|
68
|
+
/** Samples at digital full scale — a too-hot mic, irreversibly clipped. */
|
|
69
|
+
fullScaleSamples: number;
|
|
70
|
+
}
|
|
71
|
+
export interface UploadKindProgress {
|
|
72
|
+
total: number;
|
|
73
|
+
uploaded: number;
|
|
74
|
+
failed: number;
|
|
75
|
+
bytesTotal: number;
|
|
76
|
+
bytesUploaded: number;
|
|
77
|
+
}
|
|
78
|
+
export interface UploadProgress {
|
|
79
|
+
sessionId: string;
|
|
80
|
+
fraction: number;
|
|
81
|
+
isComplete: boolean;
|
|
82
|
+
failed: number;
|
|
83
|
+
/** Manifests + audio: upload on any network. */
|
|
84
|
+
audio: UploadKindProgress;
|
|
85
|
+
/** Video: waits for WiFi unless videoUploadsOnCellular. */
|
|
86
|
+
video: UploadKindProgress;
|
|
87
|
+
}
|
|
88
|
+
export interface Participant {
|
|
89
|
+
identity: string;
|
|
90
|
+
name: string;
|
|
91
|
+
}
|
|
92
|
+
export interface DoctorCheck {
|
|
93
|
+
id: string;
|
|
94
|
+
passed: boolean;
|
|
95
|
+
detail: string;
|
|
96
|
+
}
|
|
97
|
+
export interface CrowdPlayEvents {
|
|
98
|
+
/** Connection lifecycle. `error` is set when phase === 'failed'. */
|
|
99
|
+
phase: {
|
|
100
|
+
phase: Phase;
|
|
101
|
+
error?: string;
|
|
102
|
+
};
|
|
103
|
+
/** Recording state. `error` non-null means the session is NOT being
|
|
104
|
+
* captured — show it prominently and offer retryRecording(). */
|
|
105
|
+
recording: {
|
|
106
|
+
isRecording: boolean;
|
|
107
|
+
error?: string;
|
|
108
|
+
};
|
|
109
|
+
/** Upload progress for every session with outstanding or recent work. */
|
|
110
|
+
uploads: {
|
|
111
|
+
sessions: UploadProgress[];
|
|
112
|
+
onWifi: boolean;
|
|
113
|
+
};
|
|
114
|
+
/** Remote participants (join/leave). Render a CrowdPlayVideoView per entry. */
|
|
115
|
+
participants: {
|
|
116
|
+
participants: Participant[];
|
|
117
|
+
};
|
|
118
|
+
/** Live capture warnings: audioStalled (engine down, auto-recovering),
|
|
119
|
+
* micSilent (dead mic), clipping (too-hot mic), crossTalk (loudspeaker
|
|
120
|
+
* leaking the far end into this mic), micPolicy (unexpected input). */
|
|
121
|
+
warning: {
|
|
122
|
+
kind: 'audioStalled' | 'micSilent' | 'clipping' | 'crossTalk' | 'micPolicy';
|
|
123
|
+
active: boolean;
|
|
124
|
+
message: string;
|
|
125
|
+
};
|
|
126
|
+
/** The audio output route: current device by name ("AirPods Pro",
|
|
127
|
+
* "iPhone speaker"), connected candidates, and whether headphones are
|
|
128
|
+
* attached. Drive the call screen's output menu from this (D-079). */
|
|
129
|
+
audioRoute: {
|
|
130
|
+
currentOutputName: string;
|
|
131
|
+
detectedOutputs: string[];
|
|
132
|
+
headphonesConnected: boolean;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export type CrowdPlayEventName = keyof CrowdPlayEvents;
|
|
136
|
+
declare const CrowdPlay: {
|
|
137
|
+
/** Call once at app start, before anything else. Also resumes uploads
|
|
138
|
+
* interrupted by a crash, reboot or force-quit. */
|
|
139
|
+
configure(config: CrowdPlayConfig): void;
|
|
140
|
+
/** The REQUIRED consent wording (owned by CrowdPlay, versioned by hash). Show
|
|
141
|
+
* it verbatim if you build your own consent UI. */
|
|
142
|
+
consentText(): Promise<string>;
|
|
143
|
+
/** Pre-warms CrowdPlay's backend so joining is fast. Call from your join/lobby
|
|
144
|
+
* screen. Fire-and-forget. */
|
|
145
|
+
warmUp(): void;
|
|
146
|
+
/**
|
|
147
|
+
* Join a room. Recording starts automatically on join and stops on
|
|
148
|
+
* leave(). Rejects if the connection fails.
|
|
149
|
+
*
|
|
150
|
+
* `consent` is REQUIRED — produce it with <CrowdPlayConsentScreen> or from
|
|
151
|
+
* your own UI at the moment of agreement. There is no way to record
|
|
152
|
+
* without it, here or natively.
|
|
153
|
+
*/
|
|
154
|
+
join(options: {
|
|
155
|
+
displayName: string;
|
|
156
|
+
roomCode: string;
|
|
157
|
+
consent: ConsentGrant;
|
|
158
|
+
}): Promise<void>;
|
|
159
|
+
/** Leave the room. Recording stops; uploads proceed automatically (they
|
|
160
|
+
* survive backgrounding; force-quit pauses them until next launch). */
|
|
161
|
+
leave(): Promise<void>;
|
|
162
|
+
/** Mutes the call AND writes silence into the recording (never records
|
|
163
|
+
* someone who believes they are muted). */
|
|
164
|
+
setMicMuted(muted: boolean): Promise<void>;
|
|
165
|
+
/** Camera off sends + records black frames; the timeline stays continuous. */
|
|
166
|
+
setCameraEnabled(enabled: boolean): Promise<void>;
|
|
167
|
+
/** Retry after a `recording` event with an error (the meeting is NOT
|
|
168
|
+
* being captured until this succeeds). */
|
|
169
|
+
retryRecording(): void;
|
|
170
|
+
/** Re-attempts any uploads that settled as failed. */
|
|
171
|
+
/** Route call audio: 'automatic' = the connected device (AirPods,
|
|
172
|
+
* wired, ...), 'speaker' = force the loudspeaker. Pair with the
|
|
173
|
+
* 'audioRoute' event to build an output menu. */
|
|
174
|
+
setAudioOutput(output: "automatic" | "speaker"): void;
|
|
175
|
+
retryUploads(): void;
|
|
176
|
+
/** Live recording stats (REC timer, input level, silence/clipping). Null
|
|
177
|
+
* when not recording. Poll ~1 Hz for a status line. */
|
|
178
|
+
snapshot(): Promise<RecordingSnapshot | null>;
|
|
179
|
+
/** Integration self-check: configuration, permissions, disk, backend
|
|
180
|
+
* reachability, app-key auth. Every failing check names its fix. */
|
|
181
|
+
doctor(): Promise<DoctorCheck[]>;
|
|
182
|
+
/** Subscribe to state events. Returns a subscription; call .remove(). */
|
|
183
|
+
addListener<E extends CrowdPlayEventName>(event: E, listener: (payload: CrowdPlayEvents[E]) => void): {
|
|
184
|
+
remove(): void;
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
export default CrowdPlay;
|
|
188
|
+
export { CrowdPlayConsentScreen } from './ConsentScreen';
|
|
189
|
+
export { CrowdPlayVideoView } from './VideoView';
|