@everframe/react-native 0.9.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/EverframeRN.podspec +88 -0
- package/LICENSE +21 -0
- package/NOTICE +8 -0
- package/README.md +505 -0
- package/android/build.gradle.kts +209 -0
- package/android/settings.gradle.kts +8 -0
- package/android/src/main/AndroidManifest.xml +9 -0
- package/android/src/main/java/dev/everframe/rn/EverframeModule.kt +1670 -0
- package/android/src/main/java/dev/everframe/rn/EverframePackage.kt +40 -0
- package/android/src/main/java/dev/everframe/rn/EverframeSensitiveViewManager.kt +20 -0
- package/android/src/main/java/dev/everframe/rn/RnVideoPrivacyAdapter.kt +65 -0
- package/dist/NativeEverframe.d.ts +308 -0
- package/dist/NativeEverframe.js +1 -0
- package/dist/browser-ZQAY6B4M.js +1 -0
- package/dist/chunk-LPIKPZNX.js +1 -0
- package/dist/chunk-QRPINMCR.js +1 -0
- package/dist/chunk-QZRTTQRS.js +1 -0
- package/dist/chunk-XFLFA52R.js +1 -0
- package/dist/chunk-XUSV3KOD.js +1 -0
- package/dist/index.d.ts +1006 -0
- package/dist/index.js +51 -0
- package/dist/integrations/console.d.ts +8 -0
- package/dist/integrations/console.js +1 -0
- package/dist/integrations/react-native-video.d.ts +44 -0
- package/dist/integrations/react-native-video.js +1 -0
- package/dist/integrations/react-navigation.d.ts +21 -0
- package/dist/integrations/react-navigation.js +1 -0
- package/dist/integrations/theoplayer.d.ts +37 -0
- package/dist/integrations/theoplayer.js +1 -0
- package/dist/types-D5e4kWaw.d.ts +12 -0
- package/ios/Sources/EverframeBridge.swift +1043 -0
- package/ios/Sources/EverframeEventEmitter.swift +204 -0
- package/ios/Sources/EverframeModule.h +36 -0
- package/ios/Sources/EverframeModule.mm +489 -0
- package/ios/Sources/RemoteVitalsBridge.swift +99 -0
- package/package.json +121 -0
- package/src/NativeEverframe.ts +439 -0
- package/src/codegen-types.ts +11 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# SPDX-FileCopyrightText: 2026 ScriptX
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
6
|
+
|
|
7
|
+
# Derive the native-SDK range from this package's own version, so the JS half
|
|
8
|
+
# and the native half always agree. `~> X.Y.Z` means `>= X.Y.Z, < X.(Y+1).0`:
|
|
9
|
+
# it blocks cross-minor drift, and it puts a floor at this package's exact
|
|
10
|
+
# version.
|
|
11
|
+
#
|
|
12
|
+
# The floor matters. A range of `~> X.Y.0` is satisfied by X.Y.0 forever, and
|
|
13
|
+
# CocoaPods only re-resolves a pod whose `Podfile.lock` entry no longer fits its
|
|
14
|
+
# constraint — so upgrading the npm package alone would leave `pod install`
|
|
15
|
+
# returning the previously locked native SDK, with nothing in the project
|
|
16
|
+
# reporting a mismatch. The floor makes the stale entry unsatisfiable, so the
|
|
17
|
+
# ordinary `pod install` picks up the matching native version without needing
|
|
18
|
+
# `pod update`.
|
|
19
|
+
#
|
|
20
|
+
# The Android build (`android/build.gradle.kts`) carries the same floor.
|
|
21
|
+
#
|
|
22
|
+
# Every release publishes the native SDKs before the npm packages and verifies
|
|
23
|
+
# they are fetchable first, so a published `@everframe/react-native@X.Y.Z` always
|
|
24
|
+
# has a `Everframe@X.Y.Z` to resolve against.
|
|
25
|
+
#
|
|
26
|
+
# Local/CI override, mirroring `everframeNativeVersion` on the Android side
|
|
27
|
+
# (packages/sdk-react-native/android/build.gradle.kts). It exists for the window
|
|
28
|
+
# between a native minor landing and changesets publishing the JS bump that
|
|
29
|
+
# matches it — which is EVERY native minor, not an edge case: this package's
|
|
30
|
+
# version only moves at release time, so until then the derived range asks for a
|
|
31
|
+
# native pod that the workspace no longer contains, and `pod install` cannot
|
|
32
|
+
# resolve at all.
|
|
33
|
+
#
|
|
34
|
+
# Deliberately an env var rather than a file lookup: this podspec is also
|
|
35
|
+
# evaluated from inside the published npm tarball at a consumer's install, where
|
|
36
|
+
# no workspace path exists. Unset — every consumer install — the derivation
|
|
37
|
+
# below is the only source of truth, unchanged.
|
|
38
|
+
native_version_override = ENV['EVERFRAME_NATIVE_POD_VERSION'].to_s.strip
|
|
39
|
+
native_minor_range = native_version_override.empty? ? '~> 0.9.0' : native_version_override
|
|
40
|
+
|
|
41
|
+
Pod::Spec.new do |s|
|
|
42
|
+
s.name = 'EverframeRN'
|
|
43
|
+
s.version = package['version']
|
|
44
|
+
s.summary = 'React Native bridge for the Everframe iOS SDK.'
|
|
45
|
+
s.description = <<~DESC
|
|
46
|
+
iOS half of @everframe/react-native — a thin TurboModule bridge over
|
|
47
|
+
the Everframe iOS SDK. Composes capture from public primitives and delegates
|
|
48
|
+
submit to the SDK.
|
|
49
|
+
DESC
|
|
50
|
+
s.homepage = 'https://everframe.dev'
|
|
51
|
+
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
|
52
|
+
s.authors = { 'ScriptX' => 'eng@scriptx.com' }
|
|
53
|
+
s.source = { :git => 'https://github.com/scriptx-com/everframe.git', :tag => "v#{s.version}" }
|
|
54
|
+
|
|
55
|
+
s.platforms = { :ios => '15.0', :tvos => '15.0' }
|
|
56
|
+
s.swift_versions = ['5.10']
|
|
57
|
+
|
|
58
|
+
s.frameworks = ['UIKit']
|
|
59
|
+
s.requires_arc = true
|
|
60
|
+
|
|
61
|
+
s.source_files = 'ios/Sources/**/*.{h,m,mm,swift}'
|
|
62
|
+
s.public_header_files = 'ios/Sources/EverframeModule.h'
|
|
63
|
+
|
|
64
|
+
s.pod_target_xcconfig = {
|
|
65
|
+
'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17',
|
|
66
|
+
'CLANG_CXX_LIBRARY' => 'libc++',
|
|
67
|
+
'DEFINES_MODULE' => 'YES',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if defined?(install_modules_dependencies)
|
|
71
|
+
install_modules_dependencies(s)
|
|
72
|
+
else
|
|
73
|
+
s.dependency 'React-Core'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Published Everframe iOS SDK (CocoaPods Trunk). Range derived from the
|
|
77
|
+
# JS package version (see `native_minor_range` above) so JS and native
|
|
78
|
+
# always agree on the minor.
|
|
79
|
+
s.dependency 'Everframe/Core', native_minor_range
|
|
80
|
+
s.dependency 'Everframe/ReporterUI', native_minor_range
|
|
81
|
+
|
|
82
|
+
s.dependency 'React-Core'
|
|
83
|
+
|
|
84
|
+
s.test_spec 'Tests' do |t|
|
|
85
|
+
t.source_files = 'ios/Tests/**/*.swift'
|
|
86
|
+
t.frameworks = ['XCTest', 'UIKit']
|
|
87
|
+
end
|
|
88
|
+
end
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ScriptX
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: MIT -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2026 ScriptX -->
|
|
3
|
+
|
|
4
|
+
# @everframe/react-native
|
|
5
|
+
|
|
6
|
+
Everframe React Native bridge — exposes the native iOS and Android reporter
|
|
7
|
+
modal to RN host apps via a TurboModule + a thin React provider/hook API.
|
|
8
|
+
|
|
9
|
+
> Phone, Apple TV, and Android TV are all supported when the host app is
|
|
10
|
+
> built against `react-native-tvos` (see [TV Host Integration](#tv-host-integration-apple-tv--android-tv)).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add @everframe/react-native
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Peer dependencies: `react` ≥ 19, `react-native` (or `react-native-tvos` for
|
|
21
|
+
TV targets) ≥ 0.85.
|
|
22
|
+
|
|
23
|
+
Then install pods (iOS) / sync Gradle (Android) as usual:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd ios && pod install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
Wrap your app with `<EverframeProvider>` at the highest practical level
|
|
32
|
+
(above any navigator / focus engine root):
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { EverframeProvider, useEverframe } from '@everframe/react-native';
|
|
36
|
+
|
|
37
|
+
export default function App() {
|
|
38
|
+
return (
|
|
39
|
+
// `apiKey` is the only required field. The ingest endpoint is a
|
|
40
|
+
// compile-time constant in the SDK and is not configurable in v1.
|
|
41
|
+
<EverframeProvider
|
|
42
|
+
config={{
|
|
43
|
+
apiKey: 'txx_live_xxxxxxxxxxxxxxxx',
|
|
44
|
+
// Recommended in RN/Expo development: shake also opens the dev menu.
|
|
45
|
+
shakeToReport: { enabled: !__DEV__ },
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
<RootNavigator />
|
|
49
|
+
</EverframeProvider>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Trigger the reporter from any host UI:
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
function HelpButton() {
|
|
58
|
+
const { open } = useEverframe();
|
|
59
|
+
return <Button title="Report a bug" onPress={() => open()} />;
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For non-component contexts, use the top-level `open` re-export
|
|
64
|
+
(throws `EverframeNotMountedError` if the provider is not yet mounted):
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { open } from '@everframe/react-native';
|
|
68
|
+
await open();
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`open()` returns a `ReporterResult` with `{ status: 'submitted' |
|
|
72
|
+
'queued' | 'cancelled', ... }`.
|
|
73
|
+
|
|
74
|
+
For handled JavaScript exceptions, call `captureException(error)` from the
|
|
75
|
+
provider context and attach any non-sensitive context needed for diagnosis.
|
|
76
|
+
The source API requires matching rebuilt native components; published `0.7.0`
|
|
77
|
+
artifacts are not evidence of support.
|
|
78
|
+
|
|
79
|
+
## Network body capture (client veto)
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
<EverframeProvider config={{ apiKey: '…', networkBodies: { disabled: true } }}>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`networkBodies.disabled: true` is a **client veto** — it can only turn body
|
|
86
|
+
capture off locally; it can never turn it on. The server's per-app
|
|
87
|
+
`captureBodies` gate is still authoritative, and native network capture must
|
|
88
|
+
still be wired up on each platform (see below) before any body is ever
|
|
89
|
+
recorded.
|
|
90
|
+
|
|
91
|
+
**This option does not, by itself, make React Native capture network
|
|
92
|
+
bodies.** RN does not currently attach Everframe's network capture to its own
|
|
93
|
+
HTTP clients (`fetch`, Axios, etc.) — that wiring is tracked separately and
|
|
94
|
+
is not part of this SDK yet. RN apps do still inherit the native SDK's own
|
|
95
|
+
auto-capture running underneath them, so `networkBodies` controls that
|
|
96
|
+
native capture — and even that requires the native side to actually be
|
|
97
|
+
capturing network traffic in the first place:
|
|
98
|
+
|
|
99
|
+
- **Android**: bodies are captured only for OkHttpClient instances the host
|
|
100
|
+
app explicitly builds with `addEverframeInterceptor()` (see
|
|
101
|
+
`packages/sdk-android/android/README.md`). Without that interceptor, no
|
|
102
|
+
network capture — and therefore no bodies — happens regardless of this
|
|
103
|
+
option.
|
|
104
|
+
- **iOS**: equivalent native network capture wiring, if and when the host
|
|
105
|
+
app uses it.
|
|
106
|
+
|
|
107
|
+
Do not enable this option expecting RN's own network calls to show up in
|
|
108
|
+
reports — that capability doesn't exist yet.
|
|
109
|
+
|
|
110
|
+
## Navigation breadcrumbs
|
|
111
|
+
|
|
112
|
+
Mark screens with one line; the native SDK derives `from → to` from a global
|
|
113
|
+
chain shared with native auto-capture. Works with any navigation approach.
|
|
114
|
+
|
|
115
|
+
react-navigation (screens stay mounted — pass focus):
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
import { useEverframeScreen } from '@everframe/react-native';
|
|
119
|
+
import { useIsFocused, useRoute } from '@react-navigation/native';
|
|
120
|
+
|
|
121
|
+
function DetailScreen() {
|
|
122
|
+
useEverframeScreen(useRoute().name, { focused: useIsFocused() });
|
|
123
|
+
...
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
…or whole-app in one place:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
<NavigationContainer ref={navRef}
|
|
131
|
+
onStateChange={() => recordScreen(navRef.getCurrentRoute()?.name ?? '')}>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Wix react-native-navigation:
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
componentDidAppear() { recordScreen(this.props.screenName); }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Hand-rolled (conditional-render tabs, custom switchers):
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
function DeskTab() {
|
|
144
|
+
useEverframeScreen('Desk');
|
|
145
|
+
...
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Screen names should be route identifiers, never user content.
|
|
150
|
+
|
|
151
|
+
## Opt-in JS integrations
|
|
152
|
+
|
|
153
|
+
Native capture (taps, screens, lifecycle, errors) is automatic. Two things
|
|
154
|
+
live only in JS — console logs and navigator state — and are captured ONLY
|
|
155
|
+
when you opt in:
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
import { consoleIntegration } from '@everframe/react-native/integrations/console';
|
|
159
|
+
import { reactNavigationIntegration } from '@everframe/react-native/integrations/react-navigation';
|
|
160
|
+
import { createNavigationContainerRef } from '@react-navigation/native';
|
|
161
|
+
|
|
162
|
+
const navigationRef = createNavigationContainerRef();
|
|
163
|
+
const txNav = reactNavigationIntegration({ navigationRef });
|
|
164
|
+
|
|
165
|
+
<EverframeProvider config={{ apiKey, integrations: [consoleIntegration(), txNav] }}>
|
|
166
|
+
<NavigationContainer ref={navigationRef} onReady={txNav.onReady}>
|
|
167
|
+
...
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
- `consoleIntegration({ levels? })` — forwards `console.log/info/warn/error`
|
|
171
|
+
(configurable) to the breadcrumb trail with real severity. Originals always
|
|
172
|
+
run first.
|
|
173
|
+
- `reactNavigationIntegration({ navigationRef })` — records every route
|
|
174
|
+
change via `recordScreen`; also covers expo-router. Pass `txNav.onReady`
|
|
175
|
+
to the container so the initial route is recorded.
|
|
176
|
+
|
|
177
|
+
Using another navigator? Any stack is a ~5-line adapter over
|
|
178
|
+
`recordScreen` — the `useEverframeScreen` recipes above cover Wix RNN and
|
|
179
|
+
hand-rolled navigation, and a custom integration is just
|
|
180
|
+
`{ name, setup() { ...subscribe...; return unsubscribe } }`.
|
|
181
|
+
|
|
182
|
+
## Session Vitals
|
|
183
|
+
|
|
184
|
+
Native capture again: every RN app automatically gets background CPU/memory
|
|
185
|
+
sampling and a session id stamped on reports and crashes as soon as the host
|
|
186
|
+
app's project has vitals enabled on the dashboard — no JS change required.
|
|
187
|
+
|
|
188
|
+
What JS adds is **player tracking**, because in RN the player instance lives
|
|
189
|
+
inside a native view and never reaches JS.
|
|
190
|
+
|
|
191
|
+
### Config
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
<EverframeProvider config={{ apiKey, vitals: { enabled: true, sampleRate: 0.5, captureSourceQuery: false } }}>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
- `enabled` — absent follows the dashboard toggle; `false` opts out locally;
|
|
198
|
+
`true` never forces vitals on if the dashboard has them off.
|
|
199
|
+
- `sampleRate` — `[0, 1]`, `min()`'d with the server-configured rate.
|
|
200
|
+
- `captureSourceQuery` — keep the query string on `source_change.src`
|
|
201
|
+
(default `false`: signed CDN URLs carry tokens in the query).
|
|
202
|
+
|
|
203
|
+
**Reconfiguring.** A Provider remount with the *same* config does not restart
|
|
204
|
+
the SDK: both native sides compare the incoming config against the *installed*
|
|
205
|
+
one and skip the start entirely. A **changed** config does restart it — and a
|
|
206
|
+
start supersedes the running SDK, detaching every player integration it had
|
|
207
|
+
announced. **Limitation:** players tracked before that restart stay untracked
|
|
208
|
+
until their screens remount; nothing re-registers them automatically. Two React
|
|
209
|
+
instances in one process are *not* coordinated either: if a second one starts
|
|
210
|
+
the SDK under the first, the first instance's players stay untracked until its
|
|
211
|
+
own screens remount.
|
|
212
|
+
|
|
213
|
+
### react-native-video (v7)
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
import { useVideoPlayer, VideoView } from 'react-native-video';
|
|
217
|
+
import { useVideoPlayerVitals } from '@everframe/react-native/integrations/react-native-video';
|
|
218
|
+
|
|
219
|
+
function Player() {
|
|
220
|
+
const player = useVideoPlayer(source);
|
|
221
|
+
useVideoPlayerVitals(player, { name: 'main', libraryVersion: '7.0.0' });
|
|
222
|
+
return <VideoView player={player} />;
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`libraryVersion` is a host-supplied option — react-native-video does not
|
|
227
|
+
expose it at runtime.
|
|
228
|
+
|
|
229
|
+
**To measure INITIAL startup, defer the load:**
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
// Module scope: the latch belongs to the PLAYER, not to the component. A
|
|
233
|
+
// replacement instance (StrictMode's double-mount, a source change) is a
|
|
234
|
+
// different object and gets its own initialize; the same instance never
|
|
235
|
+
// gets a second one, which would restart playback under the user.
|
|
236
|
+
const initialised = new WeakSet<object>();
|
|
237
|
+
|
|
238
|
+
const player = useVideoPlayer({ uri: SOURCE, initializeOnCreation: false });
|
|
239
|
+
useVideoPlayerVitals(player, { name: 'main', libraryVersion: '7.0.0' });
|
|
240
|
+
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
if (initialised.has(player)) return;
|
|
243
|
+
initialised.add(player);
|
|
244
|
+
void player.initialize();
|
|
245
|
+
}, [player]);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Why: on Android react-native-video v7 emits `onLoadStart` synchronously from
|
|
249
|
+
inside the native player constructor — before any effect runs, so before the
|
|
250
|
+
adapter has subscribed — and `onLoadStart` is what arms the adapter's startup
|
|
251
|
+
clock, so the first source's startup latency is simply never seen. Deferring
|
|
252
|
+
the load until after the adapter subscribed puts the first `onLoadStart` back
|
|
253
|
+
where it can be observed. Without this, everything else still works; only the
|
|
254
|
+
*first* `startup` measurement is missing (later source changes are fine).
|
|
255
|
+
|
|
256
|
+
Options: `name`, `libraryVersion`, and `captureErrors` (default `false` — see
|
|
257
|
+
Limitations). `onBandwidthUpdate` is read per platform: on iOS its `bitrate`
|
|
258
|
+
is the rendition's declared bitrate and drives `bitrate_change` plus the
|
|
259
|
+
`bitrate` stat; on Android it is ExoPlayer's bandwidth estimate, reported as
|
|
260
|
+
the `bandwidthEstimate` stat (with `quality_change` for the accompanying
|
|
261
|
+
rendition size) and never as a bitrate.
|
|
262
|
+
|
|
263
|
+
`onProgress` is read per platform too. The `bufferAheadMs` stat is always the
|
|
264
|
+
media time buffered *ahead of the playhead*: on Android `bufferDuration` is
|
|
265
|
+
already that, on iOS it is the buffered range's absolute end position, so the
|
|
266
|
+
adapter subtracts `currentTime` from it.
|
|
267
|
+
|
|
268
|
+
### THEOplayer
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
import { attachTheoPlayerVitals } from '@everframe/react-native/integrations/theoplayer';
|
|
272
|
+
|
|
273
|
+
<THEOplayerView onPlayerReady={(player) => attachTheoPlayerVitals(player, { name: 'main' })} />
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Both adapters are pure translators: neither library is imported at runtime
|
|
277
|
+
or added as a dependency of this package.
|
|
278
|
+
|
|
279
|
+
### Custom log lines
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import { trackVitals } from '@everframe/react-native';
|
|
283
|
+
|
|
284
|
+
trackVitals('ad_break', { pod: 1 });
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Any other player: `trackPlayer`
|
|
288
|
+
|
|
289
|
+
Libraries without an adapter above (or a player object outside a component
|
|
290
|
+
tree) can drive the same pipeline directly:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
import { trackPlayer } from '@everframe/react-native';
|
|
294
|
+
|
|
295
|
+
const handle = trackPlayer({ library: 'my-player', libraryVersion: '1.2.0', name: 'main' });
|
|
296
|
+
handle.emit('play');
|
|
297
|
+
handle.emit('buffer_start');
|
|
298
|
+
handle.emit('buffer_end', { durationMs: 800 });
|
|
299
|
+
handle.updateStats({ bufferAheadMs: 4200, bitrate: 2_500_000 });
|
|
300
|
+
handle.track('ad_break', { pod: 1 });
|
|
301
|
+
handle.detach();
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
`useTrackPlayer(opts)` is the hook form — tracks on mount, detaches on
|
|
305
|
+
unmount, mints a fresh token on remount (Fast Refresh-safe).
|
|
306
|
+
|
|
307
|
+
Event vocabulary for `emit`: `source_change`, `drm`, `startup`, `play`,
|
|
308
|
+
`pause`, `buffer_start`, `buffer_end`, `seek`, `rate_change`,
|
|
309
|
+
`bitrate_change`, `quality_change`, `dropped_frames`, `error`, `stats`.
|
|
310
|
+
`player_attach` and `player_detach` are reserved for the native lifecycle
|
|
311
|
+
markers the registry emits itself — passing either to `emit` is silently
|
|
312
|
+
dropped, not forwarded.
|
|
313
|
+
|
|
314
|
+
### Limitations
|
|
315
|
+
|
|
316
|
+
- **JS-thread stalls pause player events.** Startup timing, span
|
|
317
|
+
derivation and stats all happen in JS, so a wedged JS thread stalls the
|
|
318
|
+
player's vitals while it is wedged. Native CPU/memory sampling keeps
|
|
319
|
+
running regardless — it does not go through JS.
|
|
320
|
+
- **Dropped frames** are reported only where the underlying library exposes
|
|
321
|
+
them; the vocabulary has the slot, adapters fill it when they can.
|
|
322
|
+
- **The ten-per-minute non-fatal error budget counts forwarding attempts,
|
|
323
|
+
including ones native drops** — e.g. before vitals start, or against a
|
|
324
|
+
token whose registration was refused. `PlayerHandle.emit` is void
|
|
325
|
+
(fire-and-forget across the bridge), so JS cannot know whether native
|
|
326
|
+
admitted an entry; an error refused over there still spends a slot.
|
|
327
|
+
- **`captureErrors` changes react-native-video's own behaviour, so it is
|
|
328
|
+
off by default.** With it on, errors carry the library's `code`
|
|
329
|
+
(`'source/invalid-uri'`, …). But v7 only throws synchronously from
|
|
330
|
+
`play()` / `pause()` / `seekBy()` / `seekTo()` / `selectTextTrack()` /
|
|
331
|
+
`getAvailableTextTracks()` when NO `onError` listener is registered — so
|
|
332
|
+
merely subscribing makes those calls stop throwing for the whole app, and
|
|
333
|
+
a host relying on `try { player.play() } catch` would silently lose its
|
|
334
|
+
errors. Off by default, fatal errors are still reported (via
|
|
335
|
+
`onStatusChange('error')`), just without a `code`.
|
|
336
|
+
- **`captureErrors` keeps swallowing after detach.** react-native-video's
|
|
337
|
+
listener removal leaves an EMPTY `onError` listener set rather than no set
|
|
338
|
+
at all, and the library's "throw synchronously when nobody is listening"
|
|
339
|
+
check only asks whether the set exists. So once `captureErrors: true` has
|
|
340
|
+
been attached even once, that player keeps suppressing the synchronous
|
|
341
|
+
`play()` / `seek*()` throws for the rest of its lifetime — detaching the
|
|
342
|
+
vitals adapter does not give them back. A library bug, not an adapter one;
|
|
343
|
+
there is nothing to fix on our side, which is the second reason
|
|
344
|
+
`captureErrors` is off by default.
|
|
345
|
+
- **`rate_change` was not observed from react-native-video v7 on Android** in
|
|
346
|
+
our Android emulator smoke — `onPlaybackRateChange` never arrived there.
|
|
347
|
+
Note that the library *does* forward ExoPlayer's
|
|
348
|
+
`onPlaybackParametersChanged`, so this is a "not seen in our smoke run"
|
|
349
|
+
result, not a proven library gap; a host that changes playback speed
|
|
350
|
+
explicitly may well see it. Nothing on the adapter side gates it either way.
|
|
351
|
+
- **react-native-video v7 does not support tvOS.** As of `7.0.0-beta.11`
|
|
352
|
+
there is no tvOS podspec/target, so it cannot be used on Apple TV at all.
|
|
353
|
+
Use THEOplayer, or a custom `trackPlayer`/`PlayerHandle` adapter, on Apple
|
|
354
|
+
TV hosts.
|
|
355
|
+
- **A native react-native-video plugin** (attaching the real
|
|
356
|
+
ExoPlayer/AVPlayer instance directly, for full fidelity including dropped
|
|
357
|
+
frames) is a follow-up once this JS-forwarding seam has proven itself in
|
|
358
|
+
the field. Every library ends on ExoPlayer/AVPlayer, but JS event
|
|
359
|
+
forwarding is what works universally today.
|
|
360
|
+
|
|
361
|
+
## TV Host Integration (Apple TV + Android TV)
|
|
362
|
+
|
|
363
|
+
`@everframe/react-native` supports Apple TV + Android TV when consumed
|
|
364
|
+
from a host app built against `react-native-tvos`. This support is developed
|
|
365
|
+
against RN-tvos `0.85.3-0`, Expo SDK `56.0.0-preview.7`, and React `19.2.5`;
|
|
366
|
+
the canonical example app below is kept building on that matrix.
|
|
367
|
+
|
|
368
|
+
### Provider placement
|
|
369
|
+
|
|
370
|
+
`<EverframeProvider>` MUST sit ABOVE the TV focus engine's root in the
|
|
371
|
+
component tree. The reporter is presented imperatively by the native side
|
|
372
|
+
(via `TXTVReporterViewController` on iOS / `:everframe-tv` `ReporterActivity`
|
|
373
|
+
on Android), so React Native's focus engine never sees it — the native VC
|
|
374
|
+
manages focus on its own UIWindow / Activity.
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
<EverframeProvider config={{ apiKey }}>
|
|
378
|
+
<NavigationContainer>{/* focus engine root */}</NavigationContainer>
|
|
379
|
+
</EverframeProvider>
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Triggers are the host app's concern
|
|
383
|
+
|
|
384
|
+
Everframe installs shake-to-report on Android and iOS phones/tablets through the
|
|
385
|
+
native SDKs. It is enabled by default, requires no permission, safely no-ops on
|
|
386
|
+
Android devices without an accelerometer, and never runs on Android TV or tvOS.
|
|
387
|
+
The dashboard is authoritative: local `enabled: true` cannot override a
|
|
388
|
+
dashboard disable.
|
|
389
|
+
|
|
390
|
+
In React Native and Expo development builds, use
|
|
391
|
+
`shakeToReport: { enabled: !__DEV__ }` because the React Native development
|
|
392
|
+
menu also uses shake. Production remains enabled by default.
|
|
393
|
+
|
|
394
|
+
All other gestures and keys are host-owned. The host app
|
|
395
|
+
calls `useEverframe().open()` (or the top-level `open()`)
|
|
396
|
+
from whatever trigger makes sense for the target form factor.
|
|
397
|
+
|
|
398
|
+
For TV, the canonical pattern uses `TVEventHandler` (exposed by
|
|
399
|
+
`react-native-tvos`).
|
|
400
|
+
|
|
401
|
+
#### API shape (react-native-tvos@0.85.3-0)
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
import { TVEventHandler } from 'react-native';
|
|
405
|
+
|
|
406
|
+
type HWEvent = {
|
|
407
|
+
eventType:
|
|
408
|
+
| 'menu' | 'playPause' | 'longPlayPause' | 'select'
|
|
409
|
+
| 'up' | 'down' | 'left' | 'right'
|
|
410
|
+
| 'longUp' | 'longDown' | 'longLeft' | 'longRight'
|
|
411
|
+
| 'pan' | string;
|
|
412
|
+
eventKeyAction?: -1 | 0 | 1 | number; // 0 = down, 1 = up, -1 = unknown
|
|
413
|
+
tag?: number;
|
|
414
|
+
body?: { state: 'Began' | 'Changed' | 'Ended'; x: number; y: number; velocityX: number; velocityY: number };
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const subscription = TVEventHandler.addListener((evt: HWEvent) => { /* ... */ });
|
|
418
|
+
subscription?.remove();
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
> If you have seen the older `new TVEventHandler(); handler.enable(cmp, cb);
|
|
422
|
+
> handler.disable()` shape elsewhere — that class-based API was removed in
|
|
423
|
+
> RN-tvos 0.85.x. Always read the version installed in your `node_modules`.
|
|
424
|
+
|
|
425
|
+
#### Apple TV: long-press Play/Pause
|
|
426
|
+
|
|
427
|
+
```tsx
|
|
428
|
+
import { Platform, TVEventHandler } from 'react-native';
|
|
429
|
+
import { useEverframe } from '@everframe/react-native';
|
|
430
|
+
import { useEffect } from 'react';
|
|
431
|
+
|
|
432
|
+
function useAppleTVReporterTrigger() {
|
|
433
|
+
const { open } = useEverframe();
|
|
434
|
+
useEffect(() => {
|
|
435
|
+
if (!(Platform.isTV && Platform.OS === 'ios')) return;
|
|
436
|
+
const sub = TVEventHandler.addListener((evt) => {
|
|
437
|
+
if (evt.eventType !== 'longPlayPause') return;
|
|
438
|
+
if (Number(evt.eventKeyAction) !== 1) return; // key-up only
|
|
439
|
+
void open();
|
|
440
|
+
});
|
|
441
|
+
return () => sub?.remove();
|
|
442
|
+
}, [open]);
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
> **Why `longPlayPause` instead of `menu`?** `.menu` is reserved by Apple as
|
|
447
|
+
> the system back-navigation gesture on the Siri Remote — apps that bind it
|
|
448
|
+
> for a non-navigation purpose are App Store-rejected. Everframe's iOS SDK
|
|
449
|
+
> enforces this via `ReservedKeysValidator`; the same constraint applies on
|
|
450
|
+
> RN. `longPlayPause` is a native long-press event emitted by the OS
|
|
451
|
+
> (no JS timing required).
|
|
452
|
+
|
|
453
|
+
#### Android TV: KEYCODE_MENU
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
import { Platform, TVEventHandler } from 'react-native';
|
|
457
|
+
import { useEverframe } from '@everframe/react-native';
|
|
458
|
+
import { useEffect } from 'react';
|
|
459
|
+
|
|
460
|
+
function useAndroidTVReporterTrigger() {
|
|
461
|
+
const { open } = useEverframe();
|
|
462
|
+
useEffect(() => {
|
|
463
|
+
if (!(Platform.isTV && Platform.OS === 'android')) return;
|
|
464
|
+
const sub = TVEventHandler.addListener((evt) => {
|
|
465
|
+
if (evt.eventType !== 'menu') return;
|
|
466
|
+
if (Number(evt.eventKeyAction) !== 1) return; // key-up only
|
|
467
|
+
void open();
|
|
468
|
+
});
|
|
469
|
+
return () => sub?.remove();
|
|
470
|
+
}, [open]);
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
`KEYCODE_MENU` is mapped to `eventType === 'menu'` by
|
|
475
|
+
`ReactAndroidHWInputDeviceHelper` (RN-tvos). It is the conventional
|
|
476
|
+
"settings / debug menu" key on Android TV remotes.
|
|
477
|
+
|
|
478
|
+
### Canonical example
|
|
479
|
+
|
|
480
|
+
The dogfood sample app at
|
|
481
|
+
[`examples/react-native/src/screens/Home.tsx`](https://github.com/scriptx-com/everframe/tree/main/examples/react-native/src/screens/Home.tsx)
|
|
482
|
+
implements both recipes side-by-side, gated by `Platform.isTV` +
|
|
483
|
+
`Platform.OS`. Cloned from the repo, build it with:
|
|
484
|
+
|
|
485
|
+
```bash
|
|
486
|
+
pnpm --filter examples-react-native ios:tv # Apple TV simulator
|
|
487
|
+
pnpm --filter examples-react-native android:tv # Android TV emulator
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
That example's own README covers prebuild details and the known pitfalls of
|
|
491
|
+
the TV targets.
|
|
492
|
+
|
|
493
|
+
### What about the floating bubble?
|
|
494
|
+
|
|
495
|
+
The iPhone floating-bubble overlay shipped in Phase 04 is NOT exposed via
|
|
496
|
+
the RN bridge today (and would not make sense on TV anyway — the focus
|
|
497
|
+
engine is the input model, not a touch-positioned overlay). On phone RN
|
|
498
|
+
hosts, add your own host-level button. On TV hosts, use the remote
|
|
499
|
+
recipes above.
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
503
|
+
## License
|
|
504
|
+
|
|
505
|
+
MIT
|