react-native-skia-webgpu-video 0.1.0-beta.3 → 0.1.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 +48 -43
- package/android/BUILD_VALIDATION.md +1 -1
- package/android/CMakeLists.txt +3 -2
- package/android/cpp/ExportHintSession.cpp +114 -0
- package/android/cpp/ExportHintSession.h +35 -0
- package/android/cpp/VideoEncoderHostObject.cpp +76 -101
- package/android/cpp/VideoEncoderHostObject.h +14 -9
- package/android/cpp/VideoFrame.cpp +3 -5
- package/android/cpp/cpp-adapter.cpp +7 -4
- package/android/src/main/java/com/azzapp/rnskv/GLFrameExtractor.java +3 -9
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +8 -11
- package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +68 -28
- package/docs/FRAME_PIXEL_FORMAT.md +17 -2
- package/docs/MIGRATION.md +1 -1
- package/docs/UPSTREAM_NOTES.md +42 -2
- package/ios/RNSVColorSpace.h +8 -3
- package/lib/module/canvas.js +3 -5
- package/lib/module/canvas.js.map +1 -1
- package/lib/module/exportFrames.js +8 -10
- package/lib/module/exportFrames.js.map +1 -1
- package/lib/module/exportTarget.js +183 -40
- package/lib/module/exportTarget.js.map +1 -1
- package/lib/module/exportVideoComposition.js +78 -35
- package/lib/module/exportVideoComposition.js.map +1 -1
- package/lib/module/frameInterop.js +323 -82
- package/lib/module/frameInterop.js.map +1 -1
- package/lib/module/gpu.js +1 -3
- package/lib/module/gpu.js.map +1 -1
- package/lib/module/gpuDevice.js +12 -0
- package/lib/module/gpuDevice.js.map +1 -1
- package/lib/module/memory.js +7 -0
- package/lib/module/memory.js.map +1 -1
- package/lib/module/previewLifecycle.js +2 -1
- package/lib/module/previewLifecycle.js.map +1 -1
- package/lib/module/videoCompositionPlayer.js +65 -14
- package/lib/module/videoCompositionPlayer.js.map +1 -1
- package/lib/module/videoFrameImage.js +1 -1
- package/lib/module/yuvBlit.js +31 -24
- package/lib/module/yuvBlit.js.map +1 -1
- package/lib/typescript/src/canvas.d.ts.map +1 -1
- package/lib/typescript/src/exportFrames.d.ts +3 -2
- package/lib/typescript/src/exportFrames.d.ts.map +1 -1
- package/lib/typescript/src/exportTarget.d.ts +53 -14
- package/lib/typescript/src/exportTarget.d.ts.map +1 -1
- package/lib/typescript/src/exportVideoComposition.d.ts +2 -1
- package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -1
- package/lib/typescript/src/frameInterop.d.ts +16 -5
- package/lib/typescript/src/frameInterop.d.ts.map +1 -1
- package/lib/typescript/src/gpu.d.ts.map +1 -1
- package/lib/typescript/src/gpuDevice.d.ts +5 -1
- package/lib/typescript/src/gpuDevice.d.ts.map +1 -1
- package/lib/typescript/src/memory.d.ts +2 -0
- package/lib/typescript/src/memory.d.ts.map +1 -1
- package/lib/typescript/src/previewLifecycle.d.ts +2 -0
- package/lib/typescript/src/previewLifecycle.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +18 -12
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/videoCompositionPlayer.d.ts +4 -5
- package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -1
- package/lib/typescript/src/videoFrameImage.d.ts +1 -1
- package/lib/typescript/src/yuvBlit.d.ts +16 -14
- package/lib/typescript/src/yuvBlit.d.ts.map +1 -1
- package/package.json +6 -6
- package/patches/react-native-skia+3.10.0.patch +150 -0
- package/scripts/skia-canvas-patch.json +22 -258
- package/src/canvas.ts +3 -7
- package/src/exportFrames.ts +10 -11
- package/src/exportTarget.ts +217 -56
- package/src/exportVideoComposition.ts +130 -85
- package/src/frameInterop.ts +389 -96
- package/src/gpu.ts +1 -3
- package/src/gpuDevice.ts +26 -1
- package/src/memory.ts +6 -0
- package/src/previewLifecycle.ts +4 -1
- package/src/types.ts +18 -12
- package/src/videoCompositionPlayer.ts +76 -23
- package/src/videoFrameImage.ts +1 -1
- package/src/yuvBlit.ts +46 -31
- package/android/cpp/EncoderTargets.cpp +0 -226
- package/android/cpp/EncoderTargets.h +0 -70
- package/patches/react-native-skia+3.7.1.patch +0 -502
package/README.md
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
6
|
<a href="https://github.com/batical/react-native-skia-webgpu-video/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/batical/react-native-skia-webgpu-video/actions/workflows/ci.yml/badge.svg?branch=main" /></a>
|
|
7
|
-
<a href="https://www.npmjs.com/package/react-native-skia-webgpu-video"><img alt="npm" src="https://img.shields.io/npm/v/react-native-skia-webgpu-video
|
|
8
|
-
<a href="#status"><img alt="Status:
|
|
9
|
-
<img alt="React Native Skia 3.
|
|
10
|
-
<img alt="React Native WebGPU 0.
|
|
7
|
+
<a href="https://www.npmjs.com/package/react-native-skia-webgpu-video"><img alt="npm" src="https://img.shields.io/npm/v/react-native-skia-webgpu-video" /></a>
|
|
8
|
+
<a href="#status"><img alt="Status: stable" src="https://img.shields.io/badge/status-stable-22c55e" /></a>
|
|
9
|
+
<img alt="React Native Skia 3.10.0" src="https://img.shields.io/badge/Skia-3.10.0-38bdf8" />
|
|
10
|
+
<img alt="React Native WebGPU 0.21.3" src="https://img.shields.io/badge/WebGPU-0.21.3-a78bfa" />
|
|
11
11
|
<a href="LICENSE"><img alt="MIT license" src="https://img.shields.io/badge/license-MIT-34d399" /></a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
@@ -27,7 +27,7 @@ A new Skia 3 / WebGPU frame-interop layer, written from scratch with explicit re
|
|
|
27
27
|
- **Audio compositions** with clip audio, separate tracks, and per-item volume.
|
|
28
28
|
- **Custom GPU processing** through an export frame processor and an optional WebGPU resource scope.
|
|
29
29
|
|
|
30
|
-
> **
|
|
30
|
+
> **0.1.0.** The Skia 3.9.1 / WebGPU 0.21.0 build has run on an iPhone 15 Pro and a Pixel 8a: export speed, preview frame accuracy, pixel and color checks, native and instrumented tests, smoke runs. Not yet qualified: memory over long sessions (last checked on 3.0.6), HDR, audible output and A/V synchronization. A few 4K cases have known issues. See [status](#status) before adopting this in production.
|
|
31
31
|
|
|
32
32
|
## Installation
|
|
33
33
|
|
|
@@ -37,8 +37,8 @@ A new Skia 3 / WebGPU frame-interop layer, written from scratch with explicit re
|
|
|
37
37
|
| --- | --- |
|
|
38
38
|
| React Native | `0.86.2` tested; declared peer range `>=0.83 <0.87` |
|
|
39
39
|
| React | `19.2.3` tested |
|
|
40
|
-
| React Native Skia | **`react-native-skia@3.
|
|
41
|
-
| React Native WebGPU | **`react-native-webgpu@0.
|
|
40
|
+
| React Native Skia | **`react-native-skia@3.10.0`** |
|
|
41
|
+
| React Native WebGPU | **`react-native-webgpu@0.21.3`** (both resolve `react-native-webgpu-dawn@158.0.0`) |
|
|
42
42
|
| Reanimated / Worklets | **`4.5.3` / `0.11.3`** |
|
|
43
43
|
| Native runtime | New Architecture and Hermes |
|
|
44
44
|
| Android | API **28+**, Vulkan 1.1 (declared as a required feature by Skia) |
|
|
@@ -49,11 +49,9 @@ On ProMotion iPhones, add `CADisableMinimumFrameDurationOnPhone` (Boolean, `YES`
|
|
|
49
49
|
|
|
50
50
|
### Install
|
|
51
51
|
|
|
52
|
-
The beta is published on npm under the `beta` tag:
|
|
53
|
-
|
|
54
52
|
```sh
|
|
55
|
-
npm install --save-exact react-native-skia@3.
|
|
56
|
-
npm install --save-exact react-native-skia-webgpu-video
|
|
53
|
+
npm install --save-exact react-native-skia@3.10.0 react-native-webgpu@0.21.3 react-native-reanimated@4.5.3 react-native-worklets@0.11.3
|
|
54
|
+
npm install --save-exact react-native-skia-webgpu-video
|
|
57
55
|
```
|
|
58
56
|
|
|
59
57
|
Each version requires exact Skia and WebGPU versions (its peer dependencies). The [GitHub releases](https://github.com/batical/react-native-skia-webgpu-video/releases) attach the same package archive, installable by URL. To build it from source instead, run `npm ci`, `npm run build` and `npm pack` in a clone, then `npm install` the resulting `.tgz`.
|
|
@@ -205,27 +203,27 @@ This budget covers **tracked video resources**, not all memory held by codecs, S
|
|
|
205
203
|
|
|
206
204
|
| Step | iOS | Android |
|
|
207
205
|
| --- | --- | --- |
|
|
208
|
-
| Decode | AVFoundation, IOSurface-backed buffers, no extra copy (`alwaysCopiesSampleData = NO`): the decoder's NV12 (`framePixelFormat: 'yuv'`, the default), or BGRA converted by VideoToolbox (`'rgba'`) | API 29+, 8-bit SDR H.264/HEVC
|
|
209
|
-
| Into Skia, preview | NV12: a YUV→RGB render pass into a
|
|
206
|
+
| Decode | AVFoundation, IOSurface-backed buffers, no extra copy (`alwaysCopiesSampleData = NO`): the decoder's NV12 (`framePixelFormat: 'yuv'`, the default), or BGRA converted by VideoToolbox (`'rgba'`) | API 29+, 8-bit SDR H.264/HEVC: `ImageReader` YUV buffers (the default), at any frame size, capped or explicit sizes included. Otherwise, or with `'rgba'`: EGL render into an RGBA AHardwareBuffer (or a CPU scoped-copy fallback) |
|
|
207
|
+
| Into Skia, preview | NV12: a YUV→RGB render pass into a texture of the frame's producer. BGRA: a WebGPU blit into it. The Skia image wraps that texture, without a copy | WebGPU blit (YUV→RGB, crop, rotation and scaling to the frame's size in one pass) into a texture of the frame's producer, which the Skia image wraps without a copy |
|
|
210
208
|
| Into Skia, export | NV12: converted into a per-clip texture, as on Android. BGRA: sampled in place through the frame's IOSurface, no copy | Same as preview |
|
|
211
|
-
| Export output | Skia draws into the encoder's CVPixelBuffer (IOSurface) | Skia draws into
|
|
209
|
+
| Export output | Skia draws into the encoder's CVPixelBuffer (IOSurface) | Skia draws into one of two textures of the library's, which a GPU copy puts into the codec's input Surface, through a WebGPU canvas context on it (Vulkan swapchain): no EGL. Before each present, the frame time is set on the window and its dataspace reset, so the codec writes Rec.709 limited range as it did with EGL |
|
|
212
210
|
| Fallback | CPU readback, with a warning | CPU readback, with a warning |
|
|
213
211
|
|
|
214
|
-
Every path
|
|
212
|
+
Every path gives source frames back only once the GPU is done with them: exports wait for checked GPU completion; the preview submits its draws and imports without waiting on the UI thread, and hands decoder buffers back when the GPU reports them done (waiting instead when the memory budget cannot hold them). Android's export waits for a frame's GPU work once it has decoded the next frame, so the GPU draws one frame while the CPU decodes the next, and it reports each frame's duration to Android's performance hints (ADPF, Android 13 and later), which raise the CPU and, from Android 15, the GPU capacity the export gets. On Android, OpenGL remains in two places: the CPU readback fallback draws into the codec Surface through EGL, and HDR and 10-bit sources (or `framePixelFormat: 'rgba'`) keep the EGL decode path.
|
|
215
213
|
|
|
216
214
|
### Frame pixel format
|
|
217
215
|
|
|
218
216
|
`framePixelFormat` chooses how a composition's frames are decoded, for its preview and its export:
|
|
219
217
|
|
|
220
218
|
```ts
|
|
221
|
-
const composition = { duration, items, framePixelFormat: 'yuv' }; // 'auto' (default) | 'yuv' | 'rgba'
|
|
219
|
+
const composition = { duration, items, framePixelFormat: 'yuv' }; // 'auto' (default) | 'yuv' | 'rgba' (deprecated)
|
|
222
220
|
```
|
|
223
221
|
|
|
224
222
|
- `yuv`: the decoder's own YUV 4:2:0 buffers, converted to RGB on the GPU when a frame is drawn. A decoded frame takes 1.5 bytes a pixel instead of 4 (12.4 MB instead of 33.2 MB in 4K), so the decoder keeps more frames ahead in the same memory.
|
|
225
|
-
- `rgba
|
|
226
|
-
- `auto`: `yuv`, on both platforms.
|
|
223
|
+
- `rgba` (**deprecated**, still supported): frames converted to RGB as they are decoded (iOS: by VideoToolbox; Android: by an OpenGL pass). It no longer has an advantage. Exports run as fast in `yuv` (a 120-clip montage on an iPhone 15 Pro: 7.7 s in `yuv`, 7.8 s in `rgba`), and `yuv` renders the same colors. A 4K60 preview keeps fewer frames on screen in `rgba` (about 235 of 360 on an iPhone 15 Pro, against 355 in `yuv`), and frames take 2.7 times the memory. Use `auto`.
|
|
224
|
+
- `auto`: `yuv`, on both platforms. Android keeps HDR and 10-bit tracks in RGBA whatever the setting.
|
|
227
225
|
|
|
228
|
-
Both convert with the frame's own YCbCr matrix and range, HDR tone mapped to Rec.709 by the platform.
|
|
226
|
+
Both convert with the frame's own YCbCr matrix and range, HDR tone mapped to Rec.709 by the platform. On iOS, both also convert files tagged Display P3 or sRGB to Rec.709, as AVFoundation does. Android takes SDR samples as Rec.709 in both formats. See the [measurements](docs/FRAME_PIXEL_FORMAT.md) before choosing.
|
|
229
227
|
|
|
230
228
|
For advanced exports, `createFrameProcessor` supports asynchronous preparation at each composition timestamp. The optional `react-native-skia-webgpu-video/gpu` entry point provides a resource scope whose asynchronous disposal waits for tracked work. See the [WebGPU cube example](examples/webgpu-overlay.ts).
|
|
231
229
|
|
|
@@ -233,33 +231,40 @@ These are integration building blocks. Three.js adapters, Core ML models, and au
|
|
|
233
231
|
|
|
234
232
|
## Benchmarks
|
|
235
233
|
|
|
236
|
-
Release builds, 3 repetitions per case, deterministic workloads from catalog `2026-10-07.1
|
|
234
|
+
Release builds, 3 repetitions per case, deterministic workloads from catalog `2026-10-07.1` (plus two 4K → 4K exports added for beta.4), median export wall time. Protocol, raw results and Perfetto analysis in the [3.7.4 verification report](migration/skia-3.7.4/verification.md). Earlier campaigns: [3.7.0](migration/skia-3.7.0/verification.md), [3.6.1](migration/skia-3.6.1/verification.md), [3.4.4](migration/skia-3.4.4/verification.md).
|
|
237
235
|
|
|
238
|
-
Skia 3.7.
|
|
236
|
+
Beta.4 (Skia 3.7.4 / WebGPU 0.19.0) exports a third faster on Android, up to 45% on 4K sources, and leaves iOS unchanged. 0.1.0 (Skia 3.9.1 / WebGPU 0.21.0) leaves export times unchanged on both phones, within ±4% ([3.9.1 verification](migration/skia-3.9.1/verification.md)).
|
|
239
237
|
|
|
240
|
-
**
|
|
238
|
+
**Pixel 8a (Android 17)**, thermal state nominal, medians of 9 repetitions for beta.3 and 6 for beta.4:
|
|
241
239
|
|
|
242
|
-
| Export case | 3.
|
|
240
|
+
| Export case | beta.3 (Skia 3.7.1) | beta.4 (Skia 3.7.4) |
|
|
243
241
|
| --- | --- | --- |
|
|
244
|
-
|
|
|
245
|
-
|
|
|
246
|
-
|
|
|
247
|
-
|
|
248
|
-
**
|
|
249
|
-
|
|
250
|
-
|
|
|
242
|
+
| 8 × 4K clips at native size, 1080 output, 360 frames | 9.8 s | **7.0 s** (−29%) |
|
|
243
|
+
| 8 × 4K clips scaled to 1280 | 12.7 s | **7.0 s** (−45%) |
|
|
244
|
+
| 3 simultaneous 4K clips, 150 frames | 5.6 s | **3.5 s** (−39%) |
|
|
245
|
+
| H.264 720p, rotated, 180 frames | 3.07 s | **1.82 s** (−41%) |
|
|
246
|
+
| HEVC 1080p, rotated, 180 frames | 3.12 s | **2.08 s** (−33%) |
|
|
247
|
+
| H.264 / HEVC 640×360, 60 frames | 1.25 / 1.27 s | **0.85 / 0.85 s** (−32 / −33%) |
|
|
248
|
+
| One 4K clip exported at 4K, H.264 / HEVC, 900 frames | 25.3 / 25.8 s | **23.0 / 22.3 s** (−9 / −13%) |
|
|
249
|
+
|
|
250
|
+
Where the time went:
|
|
251
|
+
- **Export renders straight into the codec's input Surface through WebGPU**: no EGL and no lent buffer. Exports of 1080p and below gain 10–17% on their own.
|
|
252
|
+
- **The GPU draws a frame while the CPU decodes the next one.** The export used to wait for the GPU after every frame, then leave it idle while decoding. Mali's frequency governor then kept the GPU at its lowest clock, which made the first 3.7.4 build 24% *slower* than 3.7.1 on 8 native 4K clips.
|
|
253
|
+
- **The export thread reports its frames to Android's performance hints (ADPF)**: it gets faster cores and, from Android 15, more GPU capacity. 15–29% on most cases.
|
|
254
|
+
- **`maxLongSide` and `resolution` keep the codec's YUV buffers**, scaled by the WebGPU conversion instead of an OpenGL render pass. Decoding 8 × 4K clips capped at 1280 takes 8.7 ms per frame instead of 17.
|
|
255
|
+
- A 4K → 4K export is now limited by the hardware encoder.
|
|
256
|
+
|
|
257
|
+
**iPhone 15 Pro (iOS 27.0.1)**: unchanged, since these changes are Android-only. Three alternating series, Skia 3.7.1 against 3.7.4, off the charger, with the same output bytes:
|
|
258
|
+
|
|
259
|
+
| Export case | Skia 3.7.1 | Skia 3.7.4 |
|
|
251
260
|
| --- | --- | --- |
|
|
252
|
-
| HEVC 1080p, rotated, 180 frames |
|
|
253
|
-
| H.264 640×360, 60 frames |
|
|
254
|
-
| 8 × 4K clips
|
|
255
|
-
| 3 simultaneous 4K clips, 150 frames | 5.8 s | 5.6 s |
|
|
256
|
-
| 8 × 4K clips scaled to 1280 (EGL decode) | 12.2 s | 12.5 s |
|
|
257
|
-
|
|
258
|
-
The native 4K rows compare with the 3.6.1 run of October 16. Files exported from `ImageReader`-decoded clips are up to 5.7% smaller with 3.7.0 while sampled pixels still match CPU decoding within ±1; the cause is not yet confirmed ([verification](migration/skia-3.7.0/verification.md#pixel-8a)).
|
|
261
|
+
| HEVC 1080p, rotated, 180 frames | 730 ms | 737–749 ms |
|
|
262
|
+
| H.264 640×360, 60 frames | 177 ms | 185–217 ms |
|
|
263
|
+
| 8 × 4K clips scaled to 1280, 360 frames | 2.69 s | 2.68–2.74 s |
|
|
259
264
|
|
|
260
265
|
**Smoother canvases on Android 10.** Dawn 157 asks for three swapchain images instead of two on Android. On Android 10, a canvas no longer makes the main thread wait for the previous frame's buffer: upstream measured 407 → 65 ms of `dequeueBuffer` wait per ~1.9 s trace for one opaque canvas on a Mi MIX 3. Skia 3.6.3 also keeps the render pool one frame ahead of the display: on a 120 Hz Pixel 8, an opaque `SurfaceView` canvas went from 66 to 110 presented frames per second. The swapchain change applies to every Skia and WebGPU canvas and the render pool change to Skia canvases, video previews included, with no code change. We did not measure them ourselves (no Android 10 device); see the [audit](migration/skia-3.7.0/upstream-audit.md#canvas-plus-fluide-sur-android-10).
|
|
261
266
|
|
|
262
|
-
Pass `onProfile` to an export to see where the time goes on your own compositions. On the Pixel, a native-size HEVC 1080p frame spends about 2.
|
|
267
|
+
Pass `onProfile` to an export to see where the time goes on your own compositions. On the Pixel, a native-size HEVC 1080p frame spends about 2.5 ms decoding, 1.9 ms importing, 0.8 ms waiting for the GPU and 3.7 ms encoding (beta.3: 2.8, 3.3, 1.7 and 3.8 ms); on the iPhone the whole frame takes under 4 ms.
|
|
263
268
|
|
|
264
269
|
Reproduce on your devices with the example app: `node scripts/test-ios-device-ab-run.mjs` (iPhone) and `node scripts/test-android-device-run.mjs` (Android); see the [benchmark guide](docs/BENCHMARKS.md).
|
|
265
270
|
|
|
@@ -267,12 +272,12 @@ Reproduce on your devices with the example app: `node scripts/test-ios-device-ab
|
|
|
267
272
|
|
|
268
273
|
| Area | Current evidence |
|
|
269
274
|
| --- | --- |
|
|
270
|
-
| Frame accuracy | At 60 Hz, the preview draws the frame due at 99–100% of vsyncs in 30, 24 and 60 fps on both phones, and 97–100% in 4K with `yuv` frames on the Pixel 8a. On
|
|
271
|
-
| iOS | iPhone 15 Pro: 61/61 benchmark cases pass with `yuv` frames, including the three that stopped on the video memory budget with BGRA
|
|
272
|
-
| Android | Pixel 8a: CPU vs GPU decode comparison 10/10 cases (max channel error 1, including rotation, portrait and 4K); export checks of a color pattern and of a decoded rotated clip match CPU readback
|
|
273
|
-
| JavaScript / types |
|
|
275
|
+
| Frame accuracy | At 60 Hz, the preview draws the frame due at 99–100% of vsyncs in 30, 24 and 60 fps on both phones, and 97–100% in 4K with `yuv` frames on the Pixel 8a. On an iPhone 15 Pro at 120 Hz with Skia 3.9.1: 99.7–100% of the frames read back are exact, and a 4K 60 fps preview shows 355–358 of its 360 frames at about 120 draws/s. Every exported frame holds the right source frame, in both frame pixel formats ([beta.3](docs/VERIFICATION_0.1.0-beta.3.md) and [3.9.1](migration/skia-3.9.1/verification.md) verifications). |
|
|
276
|
+
| iOS | iPhone 15 Pro: 61/61 benchmark cases pass with `yuv` frames, including the three that stopped on the video memory budget with BGRA ([beta.3 verification](docs/VERIFICATION_0.1.0-beta.3.md)). With Skia 3.9.1: 56/58 native XCTests pass, the other two being opt-in diagnostics. The smoke profile passes, and the frame-sync profile matches Skia 3.8.1's 43/58, with all 16 exports frame-exact ([3.9.1 verification](migration/skia-3.9.1/verification.md)). |
|
|
277
|
+
| Android | Pixel 8a, Skia 3.9.1: CPU vs GPU decode comparison 10/10 cases (max channel error 1, including rotation, portrait and 4K); export checks of a color pattern and of a decoded rotated clip match CPU readback, now read back asynchronously. The EGL-free export writes Rec.709, limited range ([3.7.4](migration/skia-3.7.4/verification.md) and [3.9.1](migration/skia-3.9.1/verification.md) verifications). Instrumented tests: 126/127. The failure, a 4K preview throughput test, fails the same way on Skia 3.8.1. |
|
|
278
|
+
| JavaScript / types | 247 tests in 11 suites, typecheck, package tests and 122 benchmark-harness tests pass; CI runs them on every pull request. |
|
|
274
279
|
| Image / audio fidelity | Sampled SDR pixels and an exported color pattern are checked on Android; iOS exported files have the same byte size as with 3.0.6. Whole-image fidelity, HDR, audible output and A/V synchronization remain unqualified. |
|
|
275
|
-
| Known issues | On Pixel 8a: a seek while playing a 4K sequence leaves the clock stalled, a paused seek into a 120-clip lazy montage does not show its frame, and Android 17 reports a missing file in words the harness does not recognize; all three predate beta.3.
|
|
280
|
+
| Known issues | On Pixel 8a: a seek while playing a 4K sequence leaves the clock stalled, a paused seek into a 120-clip lazy montage does not show its frame, and Android 17 reports a missing file in words the harness does not recognize; all three predate beta.3. On iPhone, a 4K 60 fps preview with the deprecated `rgba` frames shows only about 235 of 360 frames: its 33 MB buffers keep a GPU wait to stay within the memory budget. With `ImageReader`, 1:1 exports are 5–16% larger files for matching sampled pixels. |
|
|
276
281
|
| Long sessions | Not yet re-run since 3.0.6; the 3.0.6 results below are the latest. |
|
|
277
282
|
|
|
278
283
|
### Earlier results (Skia 3.0.6)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Android validation and frame ownership
|
|
2
2
|
|
|
3
|
-
The Android example in `../example/android` pins React Native 0.86.2, Skia 3.
|
|
3
|
+
The Android example in `../example/android` pins React Native 0.86.2, Skia 3.10.0, WebGPU 0.21.3, the shared `react-native-webgpu-dawn` 158.0.0 (chrome-m158) and Graphite Android 158.0.0; Gradle needs `node` to resolve that Dawn package. Since WebGPU 0.19.0, export renders straight into the codec's input Surface through a WebGPU canvas context on it (`MakeWebGPUCanvasContextFromNativeWindow`, Vulkan swapchain), with no EGL; CPU readback, drawn into the Surface by EGL, stays the fallback. On API 29+, 8-bit SDR H.264/HEVC clips go through an `ImageReader`, whose YUV buffers WebGPU converts, crops, rotates and scales to the frame's size (capped or explicit sizes included since the 3.7.4 migration); since 3.6.1 each buffer is imported once, and exports sample the converted frame in place. The 3.7.0 build was assembled and run on a Pixel 8a on 2026-10-19 ([verification](../migration/skia-3.7.0/verification.md)), after the 3.6.1 build of 2026-10-16 ([verification](../migration/skia-3.6.1/verification.md)) and the 3.4.4 build of 2026-10-14/15 ([verification](../migration/skia-3.4.4/verification.md)); the evidence below is from Skia 3.0.6 / WebGPU 0.12.1 / Graphite 154.1.0. It uses Java 17, Gradle 9.3.1, Android SDK 36 and NDK 27.1.12297006. The library requires API 28; a consuming application with a lower minimum receives an explicit Gradle configuration error. The default example ABI is arm64-v8a; `RNSKV_ANDROID_ABI` selects another ABI for the validation script.
|
|
4
4
|
|
|
5
5
|
Run `android/scripts/validate.sh build` from the repository. It assembles the example Debug and unsigned Release APKs, library AAR, standalone instrumentation APK and JVM tests. Release uses the installed `hermes-compiler` package, and both native and Metro build concurrency are bounded to two workers. The Release APK is unsigned: device installation requires signing a separate local qualification artifact. The project supplies no publishing or release signing configuration.
|
|
6
6
|
|
package/android/CMakeLists.txt
CHANGED
|
@@ -42,6 +42,8 @@ add_library(${PACKAGE_NAME}
|
|
|
42
42
|
cpp/VideoCompositionFramesExtractorSyncHostObject.cpp
|
|
43
43
|
cpp/VideoEncoderHostObject.h
|
|
44
44
|
cpp/VideoEncoderHostObject.cpp
|
|
45
|
+
cpp/ExportHintSession.h
|
|
46
|
+
cpp/ExportHintSession.cpp
|
|
45
47
|
cpp/VideoPlayerHostObject.h
|
|
46
48
|
cpp/VideoPlayerHostObject.cpp
|
|
47
49
|
cpp/VideoPlayer.h
|
|
@@ -54,8 +56,6 @@ add_library(${PACKAGE_NAME}
|
|
|
54
56
|
cpp/NativeRgbaBuffer.cpp
|
|
55
57
|
cpp/NativeHardwareBuffer.h
|
|
56
58
|
cpp/NativeHardwareBuffer.cpp
|
|
57
|
-
cpp/EncoderTargets.h
|
|
58
|
-
cpp/EncoderTargets.cpp
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
# Specifies a path to native header files.
|
|
@@ -90,6 +90,7 @@ target_link_libraries(
|
|
|
90
90
|
-lGLESv2
|
|
91
91
|
-lEGL
|
|
92
92
|
-landroid
|
|
93
|
+
-lnativewindow
|
|
93
94
|
)
|
|
94
95
|
|
|
95
96
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#include "ExportHintSession.h"
|
|
2
|
+
#include <android/performance_hint.h>
|
|
3
|
+
#include <dlfcn.h>
|
|
4
|
+
#include <time.h>
|
|
5
|
+
#include <unistd.h>
|
|
6
|
+
#include <algorithm>
|
|
7
|
+
#include <type_traits>
|
|
8
|
+
|
|
9
|
+
namespace RNSkiaVideo {
|
|
10
|
+
|
|
11
|
+
namespace {
|
|
12
|
+
int64_t nanos(clockid_t clock) {
|
|
13
|
+
timespec now{};
|
|
14
|
+
clock_gettime(clock, &now);
|
|
15
|
+
return static_cast<int64_t>(now.tv_sec) * 1'000'000'000 + now.tv_nsec;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The NDK's performance hint functions, resolved at run time: the library
|
|
20
|
+
* supports Android 9, hint sessions exist from Android 13, and work durations
|
|
21
|
+
* with a GPU part from Android 15. Null where missing.
|
|
22
|
+
*/
|
|
23
|
+
struct Hints {
|
|
24
|
+
APerformanceHintManager* (*getManager)() = nullptr;
|
|
25
|
+
APerformanceHintSession* (*createSession)(APerformanceHintManager*,
|
|
26
|
+
const int32_t*, size_t,
|
|
27
|
+
int64_t) = nullptr;
|
|
28
|
+
int (*reportActual)(APerformanceHintSession*, int64_t) = nullptr;
|
|
29
|
+
void (*closeSession)(APerformanceHintSession*) = nullptr;
|
|
30
|
+
AWorkDuration* (*createWork)() = nullptr;
|
|
31
|
+
void (*releaseWork)(AWorkDuration*) = nullptr;
|
|
32
|
+
void (*setWorkStart)(AWorkDuration*, int64_t) = nullptr;
|
|
33
|
+
void (*setWorkTotal)(AWorkDuration*, int64_t) = nullptr;
|
|
34
|
+
void (*setWorkCpu)(AWorkDuration*, int64_t) = nullptr;
|
|
35
|
+
void (*setWorkGpu)(AWorkDuration*, int64_t) = nullptr;
|
|
36
|
+
int (*reportWork)(APerformanceHintSession*, AWorkDuration*) = nullptr;
|
|
37
|
+
|
|
38
|
+
bool sessions() const {
|
|
39
|
+
return getManager && createSession && reportActual && closeSession;
|
|
40
|
+
}
|
|
41
|
+
bool workDurations() const {
|
|
42
|
+
return createWork && releaseWork && setWorkStart && setWorkTotal &&
|
|
43
|
+
setWorkCpu && setWorkGpu && reportWork;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const Hints& hints() {
|
|
48
|
+
static const Hints functions = [] {
|
|
49
|
+
Hints found;
|
|
50
|
+
void* library = dlopen("libandroid.so", RTLD_NOW);
|
|
51
|
+
if (library == nullptr) return found;
|
|
52
|
+
const auto resolve = [library](auto& function, const char* name) {
|
|
53
|
+
function = reinterpret_cast<std::remove_reference_t<decltype(function)>>(
|
|
54
|
+
dlsym(library, name));
|
|
55
|
+
};
|
|
56
|
+
resolve(found.getManager, "APerformanceHint_getManager");
|
|
57
|
+
resolve(found.createSession, "APerformanceHint_createSession");
|
|
58
|
+
resolve(found.reportActual, "APerformanceHint_reportActualWorkDuration");
|
|
59
|
+
resolve(found.closeSession, "APerformanceHint_closeSession");
|
|
60
|
+
resolve(found.createWork, "AWorkDuration_create");
|
|
61
|
+
resolve(found.releaseWork, "AWorkDuration_release");
|
|
62
|
+
resolve(found.setWorkStart, "AWorkDuration_setWorkPeriodStartTimestampNanos");
|
|
63
|
+
resolve(found.setWorkTotal, "AWorkDuration_setActualTotalDurationNanos");
|
|
64
|
+
resolve(found.setWorkCpu, "AWorkDuration_setActualCpuDurationNanos");
|
|
65
|
+
resolve(found.setWorkGpu, "AWorkDuration_setActualGpuDurationNanos");
|
|
66
|
+
resolve(found.reportWork, "APerformanceHint_reportActualWorkDuration2");
|
|
67
|
+
return found;
|
|
68
|
+
}();
|
|
69
|
+
return functions;
|
|
70
|
+
}
|
|
71
|
+
} // namespace
|
|
72
|
+
|
|
73
|
+
ExportHintSession::ExportHintSession(int frameRate)
|
|
74
|
+
: targetNanos(1'000'000'000LL / (std::max(frameRate, 1) * 4LL)) {}
|
|
75
|
+
|
|
76
|
+
ExportHintSession::~ExportHintSession() {
|
|
77
|
+
if (session != nullptr) hints().closeSession(session);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
void ExportHintSession::frameDone() {
|
|
81
|
+
const auto& functions = hints();
|
|
82
|
+
const auto now = nanos(CLOCK_MONOTONIC);
|
|
83
|
+
const auto cpu = nanos(CLOCK_THREAD_CPUTIME_ID);
|
|
84
|
+
if (session == nullptr) {
|
|
85
|
+
if (unavailable) return;
|
|
86
|
+
// The calling thread is the export's: it decodes, draws and encodes.
|
|
87
|
+
auto* manager = functions.sessions() ? functions.getManager() : nullptr;
|
|
88
|
+
const int32_t thread = gettid();
|
|
89
|
+
session = manager != nullptr
|
|
90
|
+
? functions.createSession(manager, &thread, 1, targetNanos)
|
|
91
|
+
: nullptr;
|
|
92
|
+
unavailable = session == nullptr;
|
|
93
|
+
} else if (functions.workDurations()) {
|
|
94
|
+
const auto total = std::max<int64_t>(now - lastNanos, 1);
|
|
95
|
+
const auto threadCpu = std::clamp<int64_t>(cpu - lastCpuNanos, 0, total);
|
|
96
|
+
// The thread waits out the rest of the frame, mostly for the GPU: given
|
|
97
|
+
// as GPU time, which lets Android raise the GPU clock as well.
|
|
98
|
+
auto* work = functions.createWork();
|
|
99
|
+
if (work != nullptr) {
|
|
100
|
+
functions.setWorkStart(work, lastNanos);
|
|
101
|
+
functions.setWorkTotal(work, total);
|
|
102
|
+
functions.setWorkCpu(work, threadCpu);
|
|
103
|
+
functions.setWorkGpu(work, total - threadCpu);
|
|
104
|
+
functions.reportWork(session, work);
|
|
105
|
+
functions.releaseWork(work);
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
functions.reportActual(session, std::max<int64_t>(now - lastNanos, 1));
|
|
109
|
+
}
|
|
110
|
+
lastNanos = now;
|
|
111
|
+
lastCpuNanos = cpu;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
} // namespace RNSkiaVideo
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <cstdint>
|
|
4
|
+
|
|
5
|
+
struct APerformanceHintSession;
|
|
6
|
+
|
|
7
|
+
namespace RNSkiaVideo {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Tells Android (ADPF) how long the export takes to make each frame, so that
|
|
11
|
+
* it gives the export thread, and on Android 15 and later the GPU, the
|
|
12
|
+
* capacity it needs. An export runs as fast as it can rather than at its frame
|
|
13
|
+
* rate: frames are measured against a quarter of their real-time budget.
|
|
14
|
+
* Best effort: before Android 13, or on a device without hint sessions,
|
|
15
|
+
* nothing happens.
|
|
16
|
+
*/
|
|
17
|
+
class ExportHintSession {
|
|
18
|
+
public:
|
|
19
|
+
explicit ExportHintSession(int frameRate);
|
|
20
|
+
~ExportHintSession();
|
|
21
|
+
ExportHintSession(const ExportHintSession&) = delete;
|
|
22
|
+
ExportHintSession& operator=(const ExportHintSession&) = delete;
|
|
23
|
+
|
|
24
|
+
/** On the export thread, once a frame went to the encoder. */
|
|
25
|
+
void frameDone();
|
|
26
|
+
|
|
27
|
+
private:
|
|
28
|
+
int64_t targetNanos;
|
|
29
|
+
APerformanceHintSession* session = nullptr;
|
|
30
|
+
bool unavailable = false;
|
|
31
|
+
int64_t lastNanos = 0;
|
|
32
|
+
int64_t lastCpuNanos = 0;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
} // namespace RNSkiaVideo
|
|
@@ -1,49 +1,36 @@
|
|
|
1
1
|
#include "RNSVCheckedSizes.h"
|
|
2
2
|
#include "VideoEncoderHostObject.h"
|
|
3
|
-
#include
|
|
4
|
-
#include <
|
|
3
|
+
#include <android/data_space.h>
|
|
4
|
+
#include <android/native_window_jni.h>
|
|
5
|
+
#include <dlfcn.h>
|
|
5
6
|
#include <fbjni/ByteBuffer.h>
|
|
6
7
|
#include <cmath>
|
|
7
8
|
#include <limits>
|
|
8
|
-
#include <unistd.h>
|
|
9
9
|
|
|
10
10
|
namespace RNSkiaVideo {
|
|
11
11
|
using namespace facebook::jni;
|
|
12
12
|
|
|
13
13
|
namespace {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
public:
|
|
17
|
-
explicit ScopedEncoderContext(const global_ref<VideoEncoder>& encoder)
|
|
18
|
-
: display(eglGetCurrentDisplay()), context(eglGetCurrentContext()),
|
|
19
|
-
draw(eglGetCurrentSurface(EGL_DRAW)), read(eglGetCurrentSurface(EGL_READ)) {
|
|
20
|
-
encoder->makeGLContextCurrent();
|
|
21
|
-
}
|
|
22
|
-
~ScopedEncoderContext() {
|
|
23
|
-
if (context != EGL_NO_CONTEXT) {
|
|
24
|
-
eglMakeCurrent(display, draw, read, context);
|
|
25
|
-
} else if (auto current = eglGetCurrentDisplay(); current != EGL_NO_DISPLAY) {
|
|
26
|
-
eglMakeCurrent(current, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
ScopedEncoderContext(const ScopedEncoderContext&) = delete;
|
|
30
|
-
ScopedEncoderContext& operator=(const ScopedEncoderContext&) = delete;
|
|
31
|
-
|
|
32
|
-
private:
|
|
33
|
-
EGLDisplay display;
|
|
34
|
-
EGLContext context;
|
|
35
|
-
EGLSurface draw;
|
|
36
|
-
EGLSurface read;
|
|
14
|
+
struct JSurface : JavaClass<JSurface> {
|
|
15
|
+
static constexpr auto kJavaDescriptor = "Landroid/view/Surface;";
|
|
37
16
|
};
|
|
38
17
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Sets the presentation time of the next buffer queued to a window, as
|
|
20
|
+
* eglPresentationTimeANDROID does: the codec reads it as the frame's time.
|
|
21
|
+
* libnativewindow exports it on Android 10 and later, though the NDK does not
|
|
22
|
+
* declare it (it is in the LL-NDK); null where it is missing.
|
|
23
|
+
*/
|
|
24
|
+
using SetBuffersTimestamp = int32_t (*)(ANativeWindow*, int64_t);
|
|
25
|
+
SetBuffersTimestamp setBuffersTimestamp() {
|
|
26
|
+
static const auto function = []() -> SetBuffersTimestamp {
|
|
27
|
+
void* library = dlopen("libnativewindow.so", RTLD_NOW);
|
|
28
|
+
return library == nullptr ? nullptr
|
|
29
|
+
: reinterpret_cast<SetBuffersTimestamp>(dlsym(
|
|
30
|
+
library, "ANativeWindow_setBuffersTimestamp"));
|
|
31
|
+
}();
|
|
32
|
+
return function;
|
|
33
|
+
}
|
|
47
34
|
} // namespace
|
|
48
35
|
|
|
49
36
|
local_ref<VideoEncoder>
|
|
@@ -79,12 +66,6 @@ void VideoEncoder::prepare() const {
|
|
|
79
66
|
prepareMethod(self());
|
|
80
67
|
}
|
|
81
68
|
|
|
82
|
-
void VideoEncoder::makeGLContextCurrent() const {
|
|
83
|
-
static const auto makeGLContextCurrentMethod =
|
|
84
|
-
getClass()->getMethod<void()>("makeGLContextCurrent");
|
|
85
|
-
makeGLContextCurrentMethod(self());
|
|
86
|
-
}
|
|
87
|
-
|
|
88
69
|
void VideoEncoder::encodeFrameRgba(alias_ref<JByteBuffer> pixels, jint width,
|
|
89
70
|
jint height, jint bytesPerRow, jdouble time) const {
|
|
90
71
|
static const auto method =
|
|
@@ -92,9 +73,21 @@ void VideoEncoder::encodeFrameRgba(alias_ref<JByteBuffer> pixels, jint width,
|
|
|
92
73
|
method(self(), pixels, width, height, bytesPerRow, time);
|
|
93
74
|
}
|
|
94
75
|
|
|
95
|
-
|
|
96
|
-
static const auto method =
|
|
97
|
-
|
|
76
|
+
local_ref<jobject> VideoEncoder::getInputSurface() const {
|
|
77
|
+
static const auto method =
|
|
78
|
+
getClass()->getMethod<JSurface::javaobject()>("getInputSurface");
|
|
79
|
+
auto surface = method(self());
|
|
80
|
+
return make_local(static_cast<jobject>(surface.get()));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
jlong VideoEncoder::beginWindowFrame(jdouble time) const {
|
|
84
|
+
static const auto method = getClass()->getMethod<jlong(jdouble)>("beginWindowFrame");
|
|
85
|
+
return method(self(), time);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
void VideoEncoder::endWindowFrame(jlong timeUs) const {
|
|
89
|
+
static const auto method = getClass()->getMethod<void(jlong)>("endWindowFrame");
|
|
90
|
+
method(self(), timeUs);
|
|
98
91
|
}
|
|
99
92
|
|
|
100
93
|
void VideoEncoder::release() const {
|
|
@@ -112,11 +105,11 @@ VideoEncoderHostObject::VideoEncoderHostObject(
|
|
|
112
105
|
std::string& outPath, int width, int height, int frameRate, int bitRate,
|
|
113
106
|
std::optional<std::string> encoderName, std::optional<std::string> codec,
|
|
114
107
|
alias_ref<VideoComposition> composition, int audioSampleRate,
|
|
115
|
-
int audioChannelCount, int audioBitRate)
|
|
116
|
-
: width(width), height(height) {
|
|
108
|
+
int audioChannelCount, int audioBitRate) {
|
|
117
109
|
framesExtractor = make_global(VideoEncoder::create(
|
|
118
110
|
outPath, width, height, frameRate, bitRate, encoderName, codec,
|
|
119
111
|
composition, audioSampleRate, audioChannelCount, audioBitRate));
|
|
112
|
+
hints = std::make_unique<ExportHintSession>(frameRate);
|
|
120
113
|
}
|
|
121
114
|
|
|
122
115
|
VideoEncoderHostObject::~VideoEncoderHostObject() {
|
|
@@ -128,8 +121,7 @@ VideoEncoderHostObject::getPropertyNames(jsi::Runtime& rt) {
|
|
|
128
121
|
std::vector<jsi::PropNameID> result;
|
|
129
122
|
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("prepare")));
|
|
130
123
|
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("encodeFrame")));
|
|
131
|
-
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("
|
|
132
|
-
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("releaseFrameBuffer")));
|
|
124
|
+
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("acquireInputWindow")));
|
|
133
125
|
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("finishWriting")));
|
|
134
126
|
result.push_back(jsi::PropNameID::forUtf8(rt, std::string("dispose")));
|
|
135
127
|
return result;
|
|
@@ -155,39 +147,31 @@ jsi::Value VideoEncoderHostObject::get(jsi::Runtime& runtime,
|
|
|
155
147
|
auto frame = arguments[0].asObject(runtime);
|
|
156
148
|
const auto kindValue = frame.getProperty(runtime, "kind");
|
|
157
149
|
const auto kind = kindValue.isString() ? kindValue.asString(runtime).utf8(runtime) : "";
|
|
158
|
-
if (kind == "
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (!value.isBigInt()) throw jsi::JSError(runtime, "Encoder fences must be sync-fd BigInts");
|
|
167
|
-
// WebGPU reports an already signaled fence as -1, as a uint32.
|
|
168
|
-
fences.fds.push_back(static_cast<int>(
|
|
169
|
-
static_cast<uint32_t>(value.asBigInt(runtime).getUint64(runtime))));
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const auto pointerValue = frame.getProperty(runtime, "nativeBuffer");
|
|
173
|
-
if (!pointerValue.isBigInt() || !targets)
|
|
174
|
-
throw jsi::JSError(runtime, "Encoder frame buffer is not the one lent");
|
|
175
|
-
const auto* buffer = reinterpret_cast<const AHardwareBuffer*>(
|
|
176
|
-
static_cast<uintptr_t>(pointerValue.asBigInt(runtime).getUint64(runtime)));
|
|
150
|
+
if (kind == "window") {
|
|
151
|
+
// The GPU export rendered the frame into the canvas context of the
|
|
152
|
+
// input window: `present` queues it, with the time set here.
|
|
153
|
+
if (window == nullptr)
|
|
154
|
+
throw jsi::JSError(runtime, "Encoder input window was not acquired");
|
|
155
|
+
auto present = frame.getProperty(runtime, "present");
|
|
156
|
+
if (!present.isObject() || !present.asObject(runtime).isFunction(runtime))
|
|
157
|
+
throw jsi::JSError(runtime, "A window frame needs its present function");
|
|
177
158
|
const auto time = checkedMediaSeconds(arguments[1].asNumber());
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
throw;
|
|
185
|
-
|
|
186
|
-
|
|
159
|
+
const auto timeUs = framesExtractor->beginWindowFrame(time);
|
|
160
|
+
// The swapchain tags its buffers sRGB, full range, and the codec
|
|
161
|
+
// would encode and label the file so. Unknown, as EGL leaves it,
|
|
162
|
+
// lets the codec use its default: Rec.709, limited range. Set for
|
|
163
|
+
// every frame, as a recreated swapchain tags the window again.
|
|
164
|
+
if (ANativeWindow_setBuffersDataSpace(window, ADATASPACE_UNKNOWN) != 0)
|
|
165
|
+
throw jsi::JSError(runtime, "Cannot set the encoder frame dataspace");
|
|
166
|
+
if (setBuffersTimestamp()(window, static_cast<int64_t>(timeUs) * 1000) != 0)
|
|
167
|
+
throw jsi::JSError(runtime, "Cannot set the encoder frame time");
|
|
168
|
+
present.asObject(runtime).asFunction(runtime).call(runtime);
|
|
169
|
+
framesExtractor->endWindowFrame(timeUs);
|
|
170
|
+
hints->frameDone();
|
|
187
171
|
return jsi::Value::undefined();
|
|
188
172
|
}
|
|
189
173
|
if (kind != "rgba") {
|
|
190
|
-
throw jsi::JSError(runtime, "Android encoder accepts RGBA pixels or a
|
|
174
|
+
throw jsi::JSError(runtime, "Android encoder accepts RGBA pixels or a window frame");
|
|
191
175
|
}
|
|
192
176
|
const auto checkedInteger = [&](const char* name) -> int {
|
|
193
177
|
const auto value = frame.getProperty(runtime, name).asNumber();
|
|
@@ -234,7 +218,7 @@ jsi::Value VideoEncoderHostObject::get(jsi::Runtime& runtime,
|
|
|
234
218
|
framesExtractor->encodeFrameRgba(javaPixels, width, height, stride, checkedMediaSeconds(arguments[1].asNumber()));
|
|
235
219
|
return jsi::Value::undefined();
|
|
236
220
|
});
|
|
237
|
-
} else if (propName == "
|
|
221
|
+
} else if (propName == "acquireInputWindow") {
|
|
238
222
|
return getFunction(
|
|
239
223
|
runtime, propName, 0,
|
|
240
224
|
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
@@ -243,22 +227,17 @@ jsi::Value VideoEncoderHostObject::get(jsi::Runtime& runtime,
|
|
|
243
227
|
if (released.test() || !prepared || finished) {
|
|
244
228
|
throw jsi::JSError(runtime, "Encoder not prepared");
|
|
245
229
|
}
|
|
246
|
-
//
|
|
247
|
-
if (
|
|
248
|
-
throw jsi::JSError(runtime, "
|
|
230
|
+
// Without it, every frame would carry the time it was queued at.
|
|
231
|
+
if (setBuffersTimestamp() == nullptr) {
|
|
232
|
+
throw jsi::JSError(runtime, "Encoder frame times need Android 10 or later");
|
|
249
233
|
}
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
[this](jsi::Runtime& runtime, const jsi::Value& thisValue,
|
|
258
|
-
const jsi::Value* arguments, size_t count) -> jsi::Value {
|
|
259
|
-
std::lock_guard<std::recursive_mutex> lock(encoderMutex);
|
|
260
|
-
if (targets) targets->giveBack();
|
|
261
|
-
return jsi::Value::undefined();
|
|
234
|
+
if (window == nullptr) {
|
|
235
|
+
auto surface = framesExtractor->getInputSurface();
|
|
236
|
+
window = ANativeWindow_fromSurface(Environment::current(), surface.get());
|
|
237
|
+
if (window == nullptr)
|
|
238
|
+
throw jsi::JSError(runtime, "Cannot get the encoder input window");
|
|
239
|
+
}
|
|
240
|
+
return jsi::BigInt::fromUint64(runtime, reinterpret_cast<uintptr_t>(window));
|
|
262
241
|
});
|
|
263
242
|
} else if (propName == "prepare") {
|
|
264
243
|
return getFunction(
|
|
@@ -300,16 +279,12 @@ jsi::Value VideoEncoderHostObject::get(jsi::Runtime& runtime,
|
|
|
300
279
|
void VideoEncoderHostObject::release() {
|
|
301
280
|
std::lock_guard<std::recursive_mutex> lock(encoderMutex);
|
|
302
281
|
if (!released.test_and_set()) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
ScopedEncoderContext scope(framesExtractor);
|
|
308
|
-
targets->clear();
|
|
309
|
-
} catch (...) {
|
|
310
|
-
}
|
|
282
|
+
// A canvas context presenting to the window holds its own reference.
|
|
283
|
+
if (window != nullptr) {
|
|
284
|
+
ANativeWindow_release(window);
|
|
285
|
+
window = nullptr;
|
|
311
286
|
}
|
|
312
|
-
|
|
287
|
+
hints.reset();
|
|
313
288
|
framesExtractor->release();
|
|
314
289
|
framesExtractor = nullptr;
|
|
315
290
|
}
|