react-native-skia-webgpu-video 0.1.0-beta.2 → 0.1.0-beta.4
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/LICENSE +0 -1
- package/README.md +62 -34
- 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/VideoComposition.cpp +12 -0
- package/android/cpp/VideoCompositionFramesExtractor.cpp +6 -0
- package/android/cpp/VideoCompositionFramesExtractor.h +1 -0
- package/android/cpp/VideoCompositionFramesExtractorHostObject.cpp +1 -1
- 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/PreviewFrameRateVote.java +104 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoComposition.java +4 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +8 -9
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +32 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +68 -28
- package/docs/FRAME_PIXEL_FORMAT.md +124 -0
- package/docs/MIGRATION.md +1 -1
- package/docs/UPSTREAM_NOTES.md +15 -2
- package/docs/VERIFICATION_0.1.0-beta.3.md +147 -0
- package/ios/NativeResourceStats.h +9 -0
- package/ios/RNSVColorSpace.h +35 -4
- package/ios/ReactNativeSkiaVideo.mm +4 -0
- package/ios/VideoComposition.h +20 -0
- package/ios/VideoCompositionFramesExtractorHostObject.h +5 -0
- package/ios/VideoCompositionFramesExtractorHostObject.mm +31 -2
- package/ios/VideoCompositionItemDecoder.mm +21 -8
- package/ios/VideoFrame.mm +6 -0
- package/lib/module/canvas.js +3 -5
- package/lib/module/canvas.js.map +1 -1
- package/lib/module/exportFrames.js +25 -20
- 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 +53 -12
- package/lib/module/exportVideoComposition.js.map +1 -1
- package/lib/module/frameInterop.js +15 -12
- package/lib/module/frameInterop.js.map +1 -1
- package/lib/module/gpu.js +0 -2
- 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/previewLifecycle.js +0 -1
- package/lib/module/previewLifecycle.js.map +1 -1
- package/lib/module/videoCompositionPlayer.js +13 -5
- package/lib/module/videoCompositionPlayer.js.map +1 -1
- package/lib/module/yuvBlit.js +98 -21
- 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 +11 -10
- 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.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/previewLifecycle.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +36 -5
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -1
- package/lib/typescript/src/yuvBlit.d.ts +26 -14
- package/lib/typescript/src/yuvBlit.d.ts.map +1 -1
- package/package.json +6 -6
- package/patches/react-native-skia+3.7.4.patch +146 -0
- package/scripts/skia-canvas-patch.json +22 -258
- package/src/canvas.ts +3 -7
- package/src/exportFrames.ts +53 -36
- package/src/exportTarget.ts +217 -56
- package/src/exportVideoComposition.ts +87 -26
- package/src/frameInterop.ts +26 -11
- package/src/gpu.ts +0 -2
- package/src/gpuDevice.ts +26 -1
- package/src/previewLifecycle.ts +0 -1
- package/src/types.ts +37 -5
- package/src/videoCompositionPlayer.ts +14 -4
- package/src/yuvBlit.ts +108 -27
- 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
|
@@ -109,6 +109,9 @@ public class VideoEncoder {
|
|
|
109
109
|
|
|
110
110
|
private Surface inputSurface;
|
|
111
111
|
|
|
112
|
+
// Only for CPU readback frames, created on the first one: the GPU export
|
|
113
|
+
// presents to inputSurface through WebGPU instead, and a window has one
|
|
114
|
+
// producer at a time.
|
|
112
115
|
private EGLResourcesHolder eglResourcesHolder;
|
|
113
116
|
|
|
114
117
|
private TextureRenderer textureRenderer;
|
|
@@ -120,6 +123,7 @@ public class VideoEncoder {
|
|
|
120
123
|
private long lastPresentationTimeUs = -1;
|
|
121
124
|
private long submittedFrames;
|
|
122
125
|
private long writtenFrames;
|
|
126
|
+
private long windowFrames;
|
|
123
127
|
private static final int MAX_IN_FLIGHT_FRAMES = 4;
|
|
124
128
|
private static final long MAX_PENDING_SAMPLE_BYTES = 16L * 1024 * 1024;
|
|
125
129
|
private long pendingSampleBytes;
|
|
@@ -211,14 +215,6 @@ public class VideoEncoder {
|
|
|
211
215
|
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
|
212
216
|
|
|
213
217
|
inputSurface = encoder.createInputSurface();
|
|
214
|
-
eglResourcesHolder = EGLResourcesHolder.createWithWindowedSurface(EGL10.EGL_NO_CONTEXT, inputSurface);
|
|
215
|
-
eglResourcesHolder.runWithContextCurrent(() -> {
|
|
216
|
-
textureRenderer = new TextureRenderer();
|
|
217
|
-
int[] textures = new int[1];
|
|
218
|
-
GLES20.glGenTextures(1, textures, 0);
|
|
219
|
-
uploadTexture = textures[0];
|
|
220
|
-
EGLUtils.configureTexture(GLES20.GL_TEXTURE_2D, uploadTexture);
|
|
221
|
-
});
|
|
222
218
|
encoder.start();
|
|
223
219
|
|
|
224
220
|
try {
|
|
@@ -264,8 +260,59 @@ public class VideoEncoder {
|
|
|
264
260
|
}
|
|
265
261
|
}
|
|
266
262
|
|
|
267
|
-
|
|
268
|
-
|
|
263
|
+
/** The codec's input Surface, for the GPU export to present to. */
|
|
264
|
+
public Surface getInputSurface() {
|
|
265
|
+
if (inputSurface == null) {
|
|
266
|
+
throw new IllegalStateException("Encoder has not been prepared");
|
|
267
|
+
}
|
|
268
|
+
return inputSurface;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Before the GPU export presents a frame to the input Surface: waits for
|
|
273
|
+
* room in the bounded queue and returns the frame's presentation time in
|
|
274
|
+
* microseconds, which the caller sets on the window for the buffer it
|
|
275
|
+
* queues next.
|
|
276
|
+
*/
|
|
277
|
+
public long beginWindowFrame(double time) {
|
|
278
|
+
if (!Double.isFinite(time) || time < 0) {
|
|
279
|
+
throw new IllegalArgumentException("Invalid encoder timestamp");
|
|
280
|
+
}
|
|
281
|
+
if (encoder == null) {
|
|
282
|
+
throw new IllegalStateException("Encoder has not been prepared");
|
|
283
|
+
}
|
|
284
|
+
if (eglResourcesHolder != null) {
|
|
285
|
+
throw new IllegalStateException("The encoder's input Surface is already drawn by EGL");
|
|
286
|
+
}
|
|
287
|
+
awaitEncoderCapacity();
|
|
288
|
+
if (audioException != null) {
|
|
289
|
+
throw new RuntimeException("Could not encode composition audio", audioException);
|
|
290
|
+
}
|
|
291
|
+
long timeUS = TimeHelpers.secToUs(time);
|
|
292
|
+
if (timeUS <= lastPresentationTimeUs) {
|
|
293
|
+
throw new IllegalArgumentException("Encoder presentation timestamps must increase");
|
|
294
|
+
}
|
|
295
|
+
return timeUS;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Once the frame of beginWindowFrame was presented. */
|
|
299
|
+
public void endWindowFrame(long timeUS) {
|
|
300
|
+
lastPresentationTimeUs = timeUS;
|
|
301
|
+
submittedFrames++;
|
|
302
|
+
windowFrames++;
|
|
303
|
+
drainEncoder(false);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private void ensureGLContext() {
|
|
307
|
+
if (eglResourcesHolder != null) return;
|
|
308
|
+
eglResourcesHolder = EGLResourcesHolder.createWithWindowedSurface(EGL10.EGL_NO_CONTEXT, inputSurface);
|
|
309
|
+
eglResourcesHolder.runWithContextCurrent(() -> {
|
|
310
|
+
textureRenderer = new TextureRenderer();
|
|
311
|
+
int[] textures = new int[1];
|
|
312
|
+
GLES20.glGenTextures(1, textures, 0);
|
|
313
|
+
uploadTexture = textures[0];
|
|
314
|
+
EGLUtils.configureTexture(GLES20.GL_TEXTURE_2D, uploadTexture);
|
|
315
|
+
});
|
|
269
316
|
}
|
|
270
317
|
|
|
271
318
|
/** Uploads owned pixels into this encoder's private context, without retaining the input buffer. */
|
|
@@ -275,9 +322,13 @@ public class VideoEncoder {
|
|
|
275
322
|
if (rgba == null || rgba.remaining() < required || !Double.isFinite(time) || time < 0) {
|
|
276
323
|
throw new IllegalArgumentException("Invalid encoder RGBA frame or timestamp");
|
|
277
324
|
}
|
|
278
|
-
if (encoder == null
|
|
325
|
+
if (encoder == null) {
|
|
279
326
|
throw new IllegalStateException("Encoder has not been prepared");
|
|
280
327
|
}
|
|
328
|
+
if (windowFrames > 0) {
|
|
329
|
+
throw new IllegalStateException("The encoder's input Surface is already drawn by WebGPU");
|
|
330
|
+
}
|
|
331
|
+
ensureGLContext();
|
|
281
332
|
awaitEncoderCapacity();
|
|
282
333
|
ByteBuffer source = rgba.duplicate();
|
|
283
334
|
if (bytesPerRow != RgbaLayout.bytesPerRow(sourceWidth)) {
|
|
@@ -314,23 +365,6 @@ public class VideoEncoder {
|
|
|
314
365
|
});
|
|
315
366
|
}
|
|
316
367
|
|
|
317
|
-
/**
|
|
318
|
-
* Encodes a GL_TEXTURE_2D of this encoder's context: a hardware buffer the
|
|
319
|
-
* export rendered into through WebGPU, imported as an EGLImage natively.
|
|
320
|
-
* The caller waited for the renderer's fence and keeps the buffer until the
|
|
321
|
-
* reads this draw queues are complete.
|
|
322
|
-
*/
|
|
323
|
-
public void encodeTexture(int texture, double time) {
|
|
324
|
-
if (texture <= 0 || !Double.isFinite(time) || time < 0) {
|
|
325
|
-
throw new IllegalArgumentException("Invalid encoder texture or timestamp");
|
|
326
|
-
}
|
|
327
|
-
if (encoder == null || eglResourcesHolder == null) {
|
|
328
|
-
throw new IllegalStateException("Encoder has not been prepared");
|
|
329
|
-
}
|
|
330
|
-
awaitEncoderCapacity();
|
|
331
|
-
eglResourcesHolder.runWithContextCurrent(() -> encodeTextureCurrent(texture, time));
|
|
332
|
-
}
|
|
333
|
-
|
|
334
368
|
private void awaitEncoderCapacity() {
|
|
335
369
|
drainEncoder(false);
|
|
336
370
|
long deadline = SystemClock.uptimeMillis() + 15_000;
|
|
@@ -370,6 +404,12 @@ public class VideoEncoder {
|
|
|
370
404
|
|
|
371
405
|
public void finishWriting() {
|
|
372
406
|
drainEncoder(true);
|
|
407
|
+
// A canvas context drops a frame it could not acquire a window buffer
|
|
408
|
+
// for, without an error: every presented frame must have come out.
|
|
409
|
+
if (windowFrames > 0 && writtenFrames != submittedFrames) {
|
|
410
|
+
throw new IllegalStateException("The encoder wrote " + writtenFrames + " of the "
|
|
411
|
+
+ submittedFrames + " frames presented to its input Surface");
|
|
412
|
+
}
|
|
373
413
|
if (audioThread != null) {
|
|
374
414
|
try {
|
|
375
415
|
audioThread.join(30_000);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# YUV ou RGBA : le format des frames décodées
|
|
2
|
+
|
|
3
|
+
Mesures du 21 octobre 2026 sur **iPhone 15 Pro** (iOS 27.0.1) et **Pixel 8a** (Android 17), builds Release, Skia 3.7.1 / WebGPU 0.17.0. Ce document explique l'option `framePixelFormat`, ce que chaque format coûte et rend, et les pièges trouvés en la mesurant. Le détail des runs est dans le [rapport de vérification beta.3](VERIFICATION_0.1.0-beta.3.md).
|
|
4
|
+
|
|
5
|
+
## En bref
|
|
6
|
+
|
|
7
|
+
- **`auto` vaut `yuv` sur les deux plateformes** ; `rgba` reste disponible.
|
|
8
|
+
- **Android** décode en YUV depuis le début (`ImageReader`), et c'est nettement le meilleur choix. Forcer `rgba` (la passe OpenGL) divise par deux à trois la vitesse des exports 4K, et fait perdre près des deux tiers des frames d'une preview 4K 60 fps.
|
|
9
|
+
- **iOS** décodait jusqu'ici en BGRA (VideoToolbox convertit chaque frame). En NV12, une frame 4K pèse **12,4 Mo au lieu de 33,2 Mo**. Le décodeur garde cinq frames d'avance au lieu d'une ou deux, la preview 4K perd moins de frames, et les cas qui dépassaient le budget mémoire passent. Seul coût mesuré : **environ 2 à 4 ms de plus par frame à l'export**, visible sur les exports longs ou à petite sortie.
|
|
10
|
+
- **Le rendu est le même** pour la vidéo de caméra (Rec.709 ou HLG). Un fichier étiqueté Display P3 ou avec la fonction de transfert sRGB est converti en Rec.709 par `rgba`, pas par `yuv`.
|
|
11
|
+
|
|
12
|
+
## L'option
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
const composition = { duration, items, framePixelFormat: 'yuv' }; // 'auto' | 'yuv' | 'rgba'
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| | iOS | Android |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| `yuv` | AVAssetReader sort le NV12 du décodeur. La preview l'importe comme texture externe à chaque frame et le convertit en RGB dans une passe de rendu ; l'export le convertit dans une texture par clip | `ImageReader` (pistes 8 bits SDR, à toute taille de frame, plafonnée ou explicite comprise ; les autres restent en `rgba`), converti en RGB et mis à la taille de la frame par la même passe |
|
|
21
|
+
| `rgba` | VideoToolbox convertit en BGRA ; l'export GPU échantillonne l'IOSurface en place, sans copie | Passe OpenGL vers un `AHardwareBuffer` RGBA |
|
|
22
|
+
| `auto` (défaut) | `yuv` | `yuv` |
|
|
23
|
+
|
|
24
|
+
L'option ne concerne que les compositions (preview et export) : le lecteur d'un clip seul, `useVideoPlayer`, reste en BGRA sur iOS.
|
|
25
|
+
|
|
26
|
+
La conversion YUV → RGB applique la matrice YCbCr et la plage de chaque buffer, sans convertir la fonction de transfert ni les primaires : c'est ce qui évite d'éclaircir les tons moyens à chaque export (voir `yuvBlit.ts`). Le HDR est d'abord ramené en SDR Rec.709 par la plateforme, dans les deux formats.
|
|
27
|
+
|
|
28
|
+
## Mesures
|
|
29
|
+
|
|
30
|
+
### Décodeur seul (iOS)
|
|
31
|
+
|
|
32
|
+
XCTest `DecodeThroughputTests` : AVAssetReader lit les 360 frames d'un clip HEVC 4K 60 fps, sans React Native ni GPU, en alternant les formats sur trois tours. Téléphone en état thermique *serious*.
|
|
33
|
+
|
|
34
|
+
| Format | Débit | CPU par frame | Taille d'une frame |
|
|
35
|
+
| --- | --- | --- | --- |
|
|
36
|
+
| BGRA + propriétés Rec.709 | 105–127 fps | 0,42–0,47 ms | 33,2 Mo |
|
|
37
|
+
| NV12 + propriétés Rec.709 | 93–139 fps | 0,45–0,55 ms | 12,4 Mo |
|
|
38
|
+
| NV12 brut | 120–137 fps | 0,47–0,62 ms | 12,4 Mo |
|
|
39
|
+
|
|
40
|
+
La conversion BGRA est faite par le matériel et ne coûte presque rien en débit. La différence est la **mémoire**, et ce qu'elle permet : la file du décodeur de la preview est plafonnée à 64 Mo, soit une ou deux frames d'avance en BGRA 4K, cinq en NV12.
|
|
41
|
+
|
|
42
|
+
### Preview
|
|
43
|
+
|
|
44
|
+
Profil `sync` : chaque frame porte son numéro, lu à chaque vsync. « Exact » : part des vsyncs qui montrent la frame due ; « frames » : frames distinctes affichées.
|
|
45
|
+
|
|
46
|
+
**Pixel 8a**, 60 Hz, état thermique *nominal* :
|
|
47
|
+
|
|
48
|
+
| Cas | `yuv` | `rgba` |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| HEVC 4K 60 fps | **96,9 % exact**, 344/360 frames | 18,4 %, 132/360 |
|
|
51
|
+
| HEVC 4K 30 fps | **100 %**, 179/180 | 38,1 %, 123/180 |
|
|
52
|
+
| H.264 4K 30 fps | **98,8 %**, 177/180 | 38,4 %, 128/180 |
|
|
53
|
+
| Vraie vidéo 4K 60 fps (fleurs, 60 Mbit/s) | **335/360** | 145/360 |
|
|
54
|
+
| HEVC 1080p 60 fps | 99,4 % | 92,7 % |
|
|
55
|
+
| 30 fps, 24 fps, coupe | 99,7–100 % | 99,7–100 % |
|
|
56
|
+
|
|
57
|
+
**iPhone 15 Pro**, 120 Hz, quatre runs alternés `rgba` / `yuv` / `rgba` / `yuv`, tous en état *serious* (le téléphone n'a pas refroidi entre les runs ; l'alternance répartit cette chaleur sur les deux formats). Deux valeurs par format :
|
|
58
|
+
|
|
59
|
+
| Cas | `yuv` | `rgba` |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| HEVC 4K 60 fps (frames, sans lecture du numéro) | **311, 294** /360 | 241, 261 |
|
|
62
|
+
| HEVC 4K 60 fps (exact, une frame sur quatre lue) | **77 %, 90 %** | 46 %, 48 % |
|
|
63
|
+
| Vraie vidéo 4K 60 fps (frames) | **270, 327** /360 | 234, 284 |
|
|
64
|
+
| HEVC 4K 30 fps du benchmark (frames) | **235, 239** /240 | 223, 224 |
|
|
65
|
+
| Vraie vidéo 4K 30 fps (frames) | 173, 178 /180 | 173, 169 |
|
|
66
|
+
| 30, 60 fps et HEVC 1080p 60 fps (exact) | 73–95 % | 71–91 % |
|
|
67
|
+
|
|
68
|
+
En 1080p et en 30 fps, les écarts entre formats restent dans la dispersion d'un run à l'autre, très forte sur un téléphone chaud.
|
|
69
|
+
|
|
70
|
+
### Export
|
|
71
|
+
|
|
72
|
+
Chaque frame exportée a été relue : la bonne frame source dans tous les cas, dans les deux formats, sur les deux téléphones.
|
|
73
|
+
|
|
74
|
+
| Cas | Pixel `yuv` | Pixel `rgba` | iPhone `yuv` | iPhone `rgba` |
|
|
75
|
+
| --- | --- | --- | --- | --- |
|
|
76
|
+
| 4K 60 → 4K 60 | **5,0 s** | 15,6 s | 4,2–4,3 s | 4,1–4,5 s |
|
|
77
|
+
| 4K 30 → 4K 30 | **3,2 s** | 8,7 s | 2,3 s | 2,4–2,7 s |
|
|
78
|
+
| 4K 60 → 1080p 30 | **2,4–2,9 s** | 8,0–8,2 s | 1,4–2,2 s | 1,7 s |
|
|
79
|
+
| HEVC 1080p 60 → 640×360 | 4,1 s | 8,9 s | 0,9–1,4 s | **0,5 s** |
|
|
80
|
+
| Montage de 120 clips (suite `full`) | **63,5 s** | 95,9 s | 18,1 s | **14,0 s** |
|
|
81
|
+
|
|
82
|
+
Sur iOS, l'export en `yuv` importe chaque frame NV12 et la convertit dans une texture, là où le BGRA est lu en place. Ce surcoût d'environ 2 à 4 ms par frame est invisible en 4K, où le décodage domine, mais pèse sur les sorties petites ou les exports longs. Pour un export iOS où la vitesse prime sur la mémoire, `framePixelFormat: 'rgba'` reste le plus rapide.
|
|
83
|
+
|
|
84
|
+
### Mémoire
|
|
85
|
+
|
|
86
|
+
Pics de la suite `full` (une répétition par format). Pixel : PSS ; iPhone : empreinte physique.
|
|
87
|
+
|
|
88
|
+
| Cas | Pixel `yuv` | Pixel `rgba` | iPhone `yuv` | iPhone `rgba` |
|
|
89
|
+
| --- | --- | --- | --- | --- |
|
|
90
|
+
| Lecture continue 4K | **438–458 Mio** | 554–602 | **336–343 Mio** | 400–405 |
|
|
91
|
+
| Clip seul 4K | 698–707 | 795–830 | **601–639** | 657–677 |
|
|
92
|
+
| 8 clips 4K, paresseux | **766–785** | 1 020–1 037 | **571–573** | 670–683 |
|
|
93
|
+
| 3 clips 4K simultanés | 1 507 | 1 314 | **734, passe** | arrêté par le budget |
|
|
94
|
+
| 8 clips 4K, eager | 1 562, passe | arrêté par le budget | 1 089, passe | arrêté par le budget |
|
|
95
|
+
|
|
96
|
+
Les cas « eager » qui s'arrêtaient sur le budget en `rgba` vont jusqu'au bout en `yuv`, d'où une mémoire plus haute sur ceux-là.
|
|
97
|
+
|
|
98
|
+
## Rendu
|
|
99
|
+
|
|
100
|
+
Trois contrôles, la même frame décodée dans chaque format :
|
|
101
|
+
|
|
102
|
+
- **Mire SMPTE** (H.264 et HEVC 1080p, étiquetées Rec.709) comparée à ffmpeg : écart maximal de **0 niveau en `yuv`, 1 en `rgba`**, sur les deux téléphones.
|
|
103
|
+
- **Vraie vidéo** : trois clips tirés de photos d'appareil (panoramique, grain, débits de téléphone, étiquetés Rec.709) comparés à ffmpeg sur 24 points à trois instants. Écart moyen de **0,07 à 0,16 en `yuv`** sur les deux téléphones (max 1). En `rgba` : 0,15 à 0,48 sur le Pixel, 0,5 à 0,8 sur l'iPhone (max 5).
|
|
104
|
+
- **Fixtures du benchmark** (rotations, HDR HLG 10 bits, portrait, 638×358, slow motion) : `yuv` contre `rgba`, écart moyen de 0 à 0,8 niveau. Les maxima de 5 à 17 niveaux sont ponctuels, sur des bords nets entre aplats saturés, où VideoToolbox (ou la passe OpenGL) et la passe GPU n'interpolent pas la chrominance de la même façon.
|
|
105
|
+
|
|
106
|
+
## Pièges trouvés
|
|
107
|
+
|
|
108
|
+
**VideoToolbox réétiquette sans convertir.** Avec une sortie NV12, demander les propriétés Rec.709 fait étiqueter le buffer en BT.709 sans convertir les échantillons. Pour une vidéo SD sans étiquette, codée en BT.601, le GPU appliquait alors la mauvaise matrice : le bleu d'un jaune décalé de 11 niveaux, 4,7 en moyenne sur un clip 320×240. Le XCTest `ColorMatrixDiagnosticsTests` le montre : le BGRA de VideoToolbox correspond au NV12 converti en BT.601 (écart 0,4), pas en BT.709 (6,3). Correction : en NV12, seules les pistes HDR ou BT.2020 reçoivent ces propriétés (pour le tone mapping) ; les autres gardent l'étiquette réelle du décodeur, que le GPU applique. Écart après correction : max 1 (638×358) et 3 (320×240), moyenne 0,15 à 0,32.
|
|
109
|
+
|
|
110
|
+
**Les étiquettes Display P3 et sRGB.** `rgba` convertit vers Rec.709 ce que le fichier déclare (primaires P3, fonction de transfert sRGB) : VideoToolbox sur iOS, la passe OpenGL sur Android. `yuv` n'applique que la matrice et la plage. Sur un clip étiqueté P3 (fixture `tagged-p3-flowers`), `yuv` reste à 0,2 niveau de ffmpeg (qui ignore aussi les primaires), `rgba` s'en écarte de 5,8 en moyenne sur l'iPhone (max 26) et de 1,3 sur le Pixel (max 16). La vidéo de caméra n'est pas concernée ; certains enregistrements d'écran ou exports d'éditeurs le sont.
|
|
111
|
+
|
|
112
|
+
**Les fixtures héritaient des étiquettes des photos.** ffmpeg recopiait dans les clips de test les primaires P3, la fonction de transfert sRGB ou le profil ICC des photos sources, malgré `-color_primaries bt709`. Le script force désormais les étiquettes avec `setparams` et retire le profil ICC, puis les vérifie avec `ffprobe`.
|
|
113
|
+
|
|
114
|
+
**La file de décodage iOS tournait en priorité *utility*.** Sur un iPhone chaud, iOS la reléguait sur les cœurs basse consommation : des gels de 300 à 500 ms. Elle tourne désormais en *user-interactive* ; le même run passe de 56 à 244 frames sur 360 en 4K 60 fps.
|
|
115
|
+
|
|
116
|
+
## Recommandation
|
|
117
|
+
|
|
118
|
+
Garder `auto` (`yuv`). Choisir `rgba` sur iOS seulement pour :
|
|
119
|
+
- un export où la vitesse compte plus que la mémoire ;
|
|
120
|
+
- des fichiers étiquetés Display P3 ou sRGB dont on veut la conversion colorimétrique.
|
|
121
|
+
|
|
122
|
+
Pistes pour la suite :
|
|
123
|
+
- réduire le coût de l'import NV12 à l'export sur iOS, par exemple en réutilisant les imports d'un même IOSurface ;
|
|
124
|
+
- mesurer à froid, sur plusieurs répétitions, les écarts 1080p de l'iPhone qui restent dans le bruit.
|
package/docs/MIGRATION.md
CHANGED
|
@@ -67,7 +67,7 @@ Audio is implemented on both native platforms, but the device runs so far (3.0.6
|
|
|
67
67
|
|
|
68
68
|
Set `configureVideoMemory({ maxBytes })` before allocating sessions. The budget covers tracked resources only; codecs and drivers can retain additional memory. A budget rejection does not silently reduce export dimensions.
|
|
69
69
|
|
|
70
|
-
On Android with the AHardwareBuffer interop (API 29+, 8-bit SDR H.264/HEVC
|
|
70
|
+
On Android with the AHardwareBuffer interop (API 29+, 8-bit SDR H.264/HEVC), the codec writes into an `ImageReader`; WebGPU converts, crops, rotates and scales each YUV buffer, imported once, to the frame's size (a `maxLongSide` cap or an explicit `resolution` included) in an independent Graphite snapshot after a checked GPU fence. Exports skip that snapshot: each clip's frame is converted into a texture of its own and sampled in place, as on iOS. Other sources render through the decoder's private EGL context into an RGBA AHardwareBuffer, or, without the interop, read owned RGBA pixels on the CPU and upload them. All of these avoid Skia's image-count raster-upload cache. CPU Java/JSI backing buffers remain accounted until their aliases and ART/Hermes owners are collected. See the [upstream cache notes](UPSTREAM_NOTES.md#android--éviter-le-cache-dimages-raster-du-provider) and the [3.6.1 verification](../migration/skia-3.6.1/verification.md).
|
|
71
71
|
|
|
72
72
|
The optional `createFrameProcessor` export factory creates a processor once per export. Its asynchronous `prepareFrame` runs at the requested composition timestamp and its `dispose` must finish all frame readers. Drive animated effects from that timestamp, not an independent display animation loop.
|
|
73
73
|
|
package/docs/UPSTREAM_NOTES.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Points amont à revoir lors d'une mise à jour Skia
|
|
2
2
|
|
|
3
|
+
**Mise à jour du 23 octobre 2026 :** **`react-native-skia@3.7.4`** et **`react-native-webgpu@0.19.0`**, toujours sur Dawn/Graphite 157.0.0. Le patch local passe de 7 fichiers et 62 éditions à 3 fichiers et 8 éditions. `canvas.dispose()` disparaît : en amont, la surface possède son canvas ([#4278](https://github.com/wcandillon/react-native-skia/pull/4278)) et `surface.dispose()` le détache. Le correctif d'`ImageProvider::Make()` est entré en amont par [#4277](https://github.com/wcandillon/react-native-skia/pull/4277). Restent les soumissions vérifiées, réécrites sur le nouveau `DawnContext::submitRecording(recorder, …)`, et `__rnskvTrimRecorderCache`, réduit à un appel de `trimThreadRecorders`. L'export Android rend désormais directement dans la `Surface` d'entrée du codec par `MakeWebGPUCanvasContextFromNativeWindow` ([WebGPU #588](https://github.com/wcandillon/react-native-webgpu/pull/588)), sans EGL. Voir l'[audit](../migration/skia-3.7.4/upstream-audit.md) et la [vérification](../migration/skia-3.7.4/verification.md).
|
|
4
|
+
|
|
3
5
|
**Mise à jour du 20 octobre 2026 :** **`react-native-skia@3.7.1`**, toujours avec `react-native-webgpu@0.17.0` et Dawn/Graphite 157.0.0. La 3.7.1 ne contient qu'une correction iOS ([#4273](https://github.com/wcandillon/react-native-skia/pull/4273)) : `SkiaView.mm` présente désormais à la pleine fréquence de l'écran sur les appareils ProMotion. Les sept fichiers modifiés par le patch local sont identiques octet pour octet à ceux de la 3.7.0 : le patch s'applique tel quel, seuls sa version et son nom changent. Nos PR amont [#4274](https://github.com/wcandillon/react-native-skia/pull/4274), [#4275](https://github.com/wcandillon/react-native-skia/pull/4275) et [#4276](https://github.com/wcandillon/react-native-skia/pull/4276) sont encore ouvertes : le patch ne rétrécit pas. Vérifié le 20 octobre sur appareils, en Release : iPhone 15 Pro, tests natifs 50/50 et profil smoke 15/15 (l'app déclare Skia 3.7.1, écran à 120 Hz) ; Pixel 8a, contrôle `interop` 10/10 (erreur maximale par canal de 1, comme en 3.7.0, chemins GPU et readback d'accord à l'export) et smoke 15/15. Les mesures d'export de la 3.7.0 restent la référence : le code d'export ne change pas.
|
|
4
6
|
|
|
5
7
|
**Migration du 19 octobre 2026 :** le code et les dépendances déclarées ciblent désormais **`react-native-skia@3.7.0` / `react-native-webgpu@0.17.0`**, sur Graphite **157.0.0** et **`react-native-webgpu-dawn@157.0.0`** (Skia `chrome/m157`). Les deux paquets doivent monter ensemble : Gradle et le podspec refusent deux Dawn différents. Le patch local garde 61 de ses 62 éditions ; seule tombe l'include `<stdexcept>`, désormais présent en amont. Le Dawn 157 demande trois images FIFO à la swapchain Android, ce qui supprime l'attente du thread principal dans `dequeueBuffer` sur Android 10 ; il garde aussi la robustesse des shaders sur Android, nos WGSL compris. L'[audit](../migration/skia-3.7.0/upstream-audit.md) détaille ces changements ; la [vérification](../migration/skia-3.7.0/verification.md) donne les mesures sur iPhone 15 Pro et Pixel 8a. Après une mise à jour, supprimer le bundle JS généré par Gradle avant de construire un APK de qualification : sa tâche ne voit pas le `node_modules` racine.
|
|
@@ -97,6 +99,8 @@ Cette architecture pourrait éviter la lecture CPU de la **sortie composée**. E
|
|
|
97
99
|
|
|
98
100
|
## Patch distinct : durée de vie et soumissions vérifiées
|
|
99
101
|
|
|
102
|
+
**Depuis 3.7.4**, seules les soumissions vérifiées et la réduction du cache du runtime d'export restent dans le patch : la durée de vie du canvas et la factory d'`ImageProvider` sont corrigées en amont (voir la mise à jour du 23 octobre). La suite décrit l'historique du patch.
|
|
103
|
+
|
|
100
104
|
Ces quatre protections restent nécessaires en **3.4.4**, puis en **3.6.1**, dont les fichiers patchés sont identiques à ceux de 3.4.4 : l'amont n'a ajouté ni `dispose()` de canvas, ni contrôle d'`insertRecording`, ni garde du snapshot nul, et sa factory d'`ImageProvider` n'a pas changé. Le `dispose()` rebasé remet la surface au `Dispatcher` de son thread créateur, comme le fait désormais le destructeur amont du canvas. Le reste de cette section décrit le patch d'origine.
|
|
101
105
|
|
|
102
106
|
Dans Skia 3.0.3 et encore dans les sources 3.0.6, le wrapper `JsiSkCanvas` peut garder une référence propriétaire à la surface dont il provient. Disposer uniquement le wrapper `SkSurface` ne ferme donc pas cette référence tant que le canvas est encore détenu ; attendre le GC pour les surfaces temporaires est insuffisant pour une fermeture déterministe.
|
|
@@ -113,7 +117,7 @@ La [source Skia de ce build](https://chromium.googlesource.com/skia/+/2466dcf393
|
|
|
113
117
|
|
|
114
118
|
Fichiers inspectables :
|
|
115
119
|
|
|
116
|
-
- [`patches/react-native-skia+3.
|
|
120
|
+
- [`patches/react-native-skia+3.7.4.patch`](../patches/react-native-skia+3.7.4.patch) : diff du correctif courant ; ceux des versions précédentes sont conservés comme historique.
|
|
117
121
|
- [`scripts/skia-canvas-patch.json`](../scripts/skia-canvas-patch.json) : version, empreintes avant/après et substitutions attendues.
|
|
118
122
|
- [`scripts/apply-skia-canvas-patch.mjs`](../scripts/apply-skia-canvas-patch.mjs) : vérifie tout avant toute écriture, accepte un patch déjà appliqué, refuse un paquet différent.
|
|
119
123
|
|
|
@@ -179,6 +183,15 @@ Le patch ajoute également une méthode privée `__rnskvTrimRecorderCache` : ell
|
|
|
179
183
|
|
|
180
184
|
Lors d'une mise à jour amont, vérifier séparément la correction de la factory, l'affinité de thread du Recorder, les erreurs de soumission et une éventuelle API officielle de nettoyage. Ne pas remplacer cette politique limitée par une purge globale à chaque frame.
|
|
181
185
|
|
|
186
|
+
## Piste testée : Graphite échantillonne le YUV du décodeur — 24 octobre 2026
|
|
187
|
+
|
|
188
|
+
Sur Android, l'export convertit chaque buffer YUV de l'`ImageReader` en RGBA (une passe WebGPU par frame), puis Skia échantillonne cette texture. Le Skia Graphite précompilé sait échantillonner un `OpaqueYCbCrAndroid` directement, à travers un sampler YCbCr Vulkan (`DawnTextureInfo::fYcbcrVkDescriptor`, `DawnDescriptorToImmutableSamplerInfo`). Un prototype l'a fait marcher sur le Pixel 8a :
|
|
189
|
+
- RN Skia demande `YCbCrVulkanSamplers` et `StaticSamplers` au device, en plus d'`OpaqueYCbCrAndroidForExternalTexture` ;
|
|
190
|
+
- une méthode `Skia.Image.MakeImageFromYCbCrTexture(texture, hardwareBuffer)` lit le descripteur avec `wgpu::Device::GetAHardwareBufferProperties` et enveloppe la texture de `importSharedTextureMemory` ;
|
|
191
|
+
- l'export ouvre l'accès du buffer à l'import et le ferme quand la frame est libérée ; la frame porte le recadrage et la rotation du buffer, comme une frame `direct`.
|
|
192
|
+
|
|
193
|
+
La passe de conversion disparaît (attente GPU d'une frame 8×4K → 1080 : 2,7 → 1,2 ms), mais la durée des exports ne change pas : une fois l'attente GPU différée et les indices ADPF en place, le bloc codec (décodage 4K et encodage) limite le débit. Les octets exportés changent aussi (reconstruction de la chrominance par le sampler), ce qui demanderait une qualification d'image. Le prototype n'a donc pas été gardé ; il reste à reprendre si un cas devient limité par le GPU (plusieurs couches 4K, GPU plus modeste). Mesures : [vérification 3.7.4](../migration/skia-3.7.4/verification.md#améliorations-du-24-octobre).
|
|
194
|
+
|
|
182
195
|
## Référence fournie : `enzomanuelmangano/demos`
|
|
183
196
|
|
|
184
197
|
Le [dépôt de démos](https://github.com/enzomanuelmangano/demos) est utile pour choisir des scènes visuelles : transitions, reflets de shader et interactions pilotées par des valeurs partagées. Le [manifest consulté](https://github.com/enzomanuelmangano/demos/blob/main/package.json) fixe encore Skia 2.6.2 et WebGPU 0.5.14 ; ses patches de compatibilité ne sont donc pas une preuve de compatibilité avec le couple désormais ciblé 3.0.6/0.12.1, ni avec le couple mesuré 3.0.3/0.11.0.
|
|
@@ -187,4 +200,4 @@ Les idées retenues pour les futurs adaptateurs sont une transition à progressi
|
|
|
187
200
|
|
|
188
201
|
## Procédure de mise à jour
|
|
189
202
|
|
|
190
|
-
Pour chaque nouvelle version Skia : relever le couple Dawn/WebGPU et les versions RN/Reanimated/Worklets compatibles, relire les imports/sorties de buffers, les soumissions vérifiées, la fermeture canvas/snapshot et les budgets/caches Graphite, revoir les empreintes, compiler les deux plateformes, exécuter les tests de pixels/timestamps puis les **
|
|
203
|
+
Pour chaque nouvelle version Skia : relever le couple Dawn/WebGPU et les versions RN/Reanimated/Worklets compatibles, relire les imports/sorties de buffers, les soumissions vérifiées, la fermeture canvas/snapshot et les budgets/caches Graphite, revoir les empreintes, compiler les deux plateformes, exécuter les tests de pixels/timestamps puis les **67 workloads du catalogue `2026-10-07.1`** (59 historiques, trois lectures continues `copy` et leurs trois équivalents `direct`, plus les deux exports 4K → 4K ajoutés le 24 octobre 2026 sans changer les autres) et les sessions longues. La configuration iPhone déjà compilée pour la référence 3.0.3 utilise RN 0.86.2, Reanimated 4.5.3 et Worklets 0.11.3 ; ces versions sont conservées pour la migration 3.0.6. Ne pas laisser une plage ouverte sélectionner une autre version sans refaire l'installation native. Documenter la version et le transport effectif dans les résultats. La présence d'un cas dans le catalogue ne signifie pas qu'il a été exécuté sur toutes les cibles. Une correction amont ou des tests de l'hôte verts seuls ne justifient pas de supprimer un garde-fou mémoire.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Vérification de la 0.1.0-beta.3 — 21 octobre 2026
|
|
2
|
+
|
|
3
|
+
Ce document liste ce qui a **réellement** été exécuté pour cette version :
|
|
4
|
+
- sur l'hôte ;
|
|
5
|
+
- sur **iPhone 15 Pro** (iOS 27.0.1) ;
|
|
6
|
+
- sur **Pixel 8a** (Android 17).
|
|
7
|
+
|
|
8
|
+
Tous les builds sont en Release, sur Skia 3.7.1 / WebGPU 0.17.0, inchangés depuis la beta.2. L'analyse du format des frames est dans [FRAME_PIXEL_FORMAT.md](FRAME_PIXEL_FORMAT.md).
|
|
9
|
+
|
|
10
|
+
## Ce qui change
|
|
11
|
+
|
|
12
|
+
- **Horloge de `drawFrame`** (iOS, Android) : `currentTime` vaut l'heure à laquelle les frames dessinées ont été choisies. Lue plus tard, elle avançait jusqu'à une frame sur la vidéo à 60 fps, et les overlays calculés d'après elle étaient décalés d'autant.
|
|
13
|
+
- **`splitImportAndDraw`** ne perd plus une frame sur deux quand une nouvelle frame arrive à chaque vsync.
|
|
14
|
+
- **Android 15+** : une preview en lecture demande à l'écran sa fréquence maximale. Le Pixel ne descend plus à 30 Hz, et monte à 120 Hz quand le réglage de l'utilisateur le permet.
|
|
15
|
+
- **iOS** : la file de décodage de la preview tourne en priorité *user-interactive* au lieu d'*utility*.
|
|
16
|
+
- **`framePixelFormat`** (`'auto' | 'yuv' | 'rgba'`) : `auto` vaut `yuv` sur les deux plateformes. Sur iOS, le décodeur sort désormais du NV12 (12,4 Mo par frame 4K au lieu de 33,2 Mo de BGRA). `rgba` reste disponible.
|
|
17
|
+
- **Diagnostic** : `getResourceStats()` donne sur iOS les refus de l'anneau de frames, les files de décodage vides et les ticks de décodage lents.
|
|
18
|
+
- **App de test** : profil `sync` (exactitude des frames, seeks, exports, couleurs, rendu de vrais médias), option `--frame-pixel-format` des lanceurs, `--case` pour iOS, clé `CADisableMinimumFrameDurationOnPhone` (120 Hz sur ProMotion).
|
|
19
|
+
|
|
20
|
+
## Hôte
|
|
21
|
+
|
|
22
|
+
| Contrôle | Résultat |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| Jest | **237/237 tests, 11 suites** (dont les chemins NV12 de la preview et de l'export) |
|
|
25
|
+
| `tsc --noEmit`, `test:package`, `benchmark:test` | OK, 5/5, 122/122 |
|
|
26
|
+
| `test:native` (C++ hôte, validation native, syntaxe des 50 assertions iOS historiques) | OK |
|
|
27
|
+
| Tests JVM | 27/27 |
|
|
28
|
+
| `test-skia-patch-syntax.mjs`, `test-skia-provider-ownership.mjs` | OK |
|
|
29
|
+
| `npm run build`, `check-package` (publint strict) | OK |
|
|
30
|
+
|
|
31
|
+
## Exactitude de la lecture (profil `sync`)
|
|
32
|
+
|
|
33
|
+
Chaque frame des fixtures porte son numéro en code-barres, lu à chaque vsync dans `drawFrame` et comparé à l'horloge du player. « Exact » : part des vsyncs qui montrent la frame due. « Irr » : part des changements de frame décalés de plus de 3/4 de vsync. Écran à 60 Hz.
|
|
34
|
+
|
|
35
|
+
| Cas | Pixel, avant | Pixel, beta.3 | iPhone, avant | iPhone, beta.3 |
|
|
36
|
+
| --- | --- | --- | --- | --- |
|
|
37
|
+
| 30 fps | 93 % · irr 1,8 % | **99,4 %** · irr 1,8 % | 100 % | 100 % |
|
|
38
|
+
| 30 fps, `splitImportAndDraw` | 79 % · irr 81 % · 75 frames sautées | **99 %** · irr 0 % · 2 | 100 % | 100 % |
|
|
39
|
+
| 24 fps | 85 % · irr 21 % | **100 %** · irr 4,6 % | 100 % | 100 % |
|
|
40
|
+
| 60 fps | 51 % · 166 sautées | **99,4 %** · 2 | 80 % · 9 sautées | **100 %** |
|
|
41
|
+
| HEVC 1080p 60 fps | 66 % | **98,8 %** | 99,7 % (0,3 % au premier run) | **100 %** |
|
|
42
|
+
|
|
43
|
+
« Avant » : le code de `main` avec le seul profil `sync` ajouté. Les 2 à 9 frames sautées restantes tombent au démarrage d'une lecture.
|
|
44
|
+
|
|
45
|
+
**4K, `yuv` (défaut) contre `rgba`** :
|
|
46
|
+
|
|
47
|
+
| Cas | Pixel `rgba` | Pixel `yuv` | iPhone `rgba` | iPhone `yuv` |
|
|
48
|
+
| --- | --- | --- | --- | --- |
|
|
49
|
+
| HEVC 4K 60 fps | 18 % · irr 65 % · 205 sautées | **97 % · irr 0,3 %** · 9 | 66 % · irr 6,5 % · 64 | **97 % · irr 1,3 %** · 9 |
|
|
50
|
+
| HEVC 4K 30 fps | 38 % · irr 52 % | **100 %** · irr 0 % | 97 % · irr 1,9 % | **100 %** · irr 0 % |
|
|
51
|
+
| H.264 4K 30 fps | 38 % · irr 48 % | **99 %** · irr 0,6 % | 96 % · irr 6,9 % | **100 %** · irr 1,2 % |
|
|
52
|
+
| Vraie vidéo 4K 60 fps, 60 Mbit/s (frames distinctes) | 145/360 | **335/360** | 268/360 | 267/360 |
|
|
53
|
+
|
|
54
|
+
Le Pixel est resté à l'état thermique *nominal*. Les deux runs iPhone de ce tableau ont tourné téléphone chaud, *fair* pour `rgba` et *serious* pour `yuv`. Quatre runs alternés `rgba` / `yuv` / `rgba` / `yuv`, tous en *serious*, confirment l'avantage du `yuv` en 4K : en HEVC 4K 60 fps, 294–311 frames sur 360 contre 241–261, et 77–90 % d'exact contre 46–48 % en lisant une frame sur quatre. En 1080p et en 30 fps, les écarts restent dans la dispersion entre runs (73 à 95 % d'exact dans les deux formats). Détail dans [FRAME_PIXEL_FORMAT.md](FRAME_PIXEL_FORMAT.md#preview).
|
|
55
|
+
|
|
56
|
+
Les **seeks** tombent juste sur les deux plateformes : 22/22 sur chaque téléphone, y compris à ±0,5 ms d'une limite de frame. Sur Android, un seek en pause montre les frames intermédiaires décodées depuis la keyframe avant la bonne ; l'iPhone garde l'image précédente. Ce comportement n'a pas changé.
|
|
57
|
+
|
|
58
|
+
## Exports
|
|
59
|
+
|
|
60
|
+
Chaque frame exportée porte son numéro de sortie sous le code-barres source, et est relue après l'export. **Tous les exports sont exacts sur les deux téléphones et dans les deux formats** (30→30, 60→30, 30→60, 24→30, HEVC 60→60, coupe, 4K→4K en 30 et 60 fps, 4K→1080p) : la bonne frame source, aucune frame perdue ou dupliquée, et le décodeur du système compte le bon nombre de frames dans chaque fichier.
|
|
61
|
+
|
|
62
|
+
| Pixel 8a | `yuv` | `rgba` |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| 4K 60 → 4K 60 | **5,0 s** | 15,6 s |
|
|
65
|
+
| 4K 30 → 4K 30 | **3,2 s** | 8,7 s |
|
|
66
|
+
| 4K 60 → 1080p 30 | **2,4–2,9 s** | 8,0–8,2 s |
|
|
67
|
+
|
|
68
|
+
| iPhone 15 Pro, runs alternés en *serious* | `yuv` | `rgba` |
|
|
69
|
+
| --- | --- | --- |
|
|
70
|
+
| 4K 60 → 4K 60 | 4,2–4,3 s | 4,1–4,5 s |
|
|
71
|
+
| 4K 30 → 4K 30 | 2,3 s | 2,4–2,7 s |
|
|
72
|
+
| HEVC 1080p 60 → 640×360 | 0,9–1,4 s | **0,5 s** |
|
|
73
|
+
| Montage de 120 clips (suite `full`) | 18,1 s | **14,0 s** |
|
|
74
|
+
|
|
75
|
+
Sur iOS, l'export `yuv` importe et convertit chaque frame NV12 au lieu de lire le BGRA en place : environ 2 à 4 ms de plus par frame, sensibles sur les exports longs ou à petite sortie.
|
|
76
|
+
|
|
77
|
+
## Rendu et couleurs
|
|
78
|
+
|
|
79
|
+
- **Mire SMPTE** (H.264 et HEVC 1080p, Rec.709) comparée à ffmpeg : écart maximal de 0 niveau en `yuv`, 1 en `rgba`, sur les deux téléphones.
|
|
80
|
+
- **Vraie vidéo** (trois clips tirés de photos d'appareil, étiquetés Rec.709) comparée à ffmpeg sur 24 points à trois instants : écart moyen de 0,07 à 0,16 en `yuv` sur les deux téléphones (max 1). En `rgba` : 0,15 à 0,48 sur le Pixel, 0,5 à 0,8 sur l'iPhone (max 5).
|
|
81
|
+
- **Clip étiqueté Display P3** : `yuv` reste à 0,2 de ffmpeg ; `rgba` convertit le gamut (iPhone : moyenne 5,8, max 26 ; Pixel : 1,3, max 16).
|
|
82
|
+
- **Fixtures du benchmark** (rotations, HDR HLG, portrait, SD, slow motion), `yuv` contre `rgba` : écart moyen de 0 à 0,8 niveau. Les maxima ponctuels de 5 à 17 sont sur des bords nets.
|
|
83
|
+
- **Pixel, `interop`** : décodage GPU contre CPU **10/10**, écarts maximaux 1,1,0,0,1,1,0,0,1,1, identiques à la 3.7.0. Export GPU contre readback : écart 0 sur la mire et sur la vidéo.
|
|
84
|
+
|
|
85
|
+
Correction faite pendant la vérification : en NV12, VideoToolbox réétiquetait en BT.709 une vidéo SD codée en BT.601 sans convertir ses échantillons (le bleu d'un jaune décalé de 11 niveaux). Seules les pistes HDR ou BT.2020 reçoivent désormais les propriétés de conversion. Voir [FRAME_PIXEL_FORMAT.md](FRAME_PIXEL_FORMAT.md#pièges-trouvés).
|
|
86
|
+
|
|
87
|
+
## Suite `full` (65 cas, 1 répétition par format)
|
|
88
|
+
|
|
89
|
+
| | Réussis | Échecs | Arrêtés par le budget | Non exécutés |
|
|
90
|
+
| --- | --- | --- | --- | --- |
|
|
91
|
+
| Pixel `rgba` (26 min, nominal) | 57 | 4 | — | 4 |
|
|
92
|
+
| Pixel `yuv` (24 min, nominal) | **58** | 3 | — | 4 |
|
|
93
|
+
| iPhone `rgba` (17 min, *fair* puis *serious*) | 58 | — | 3 | 4 |
|
|
94
|
+
| iPhone `yuv` (20 min, *nominal* à *serious*) | **61** | — | — | 4 |
|
|
95
|
+
|
|
96
|
+
Les 4 cas non exécutés sont les extensions LUT, compute, Three.js et Core ML, non branchées dans l'app.
|
|
97
|
+
|
|
98
|
+
**Pixel.** Les trois échecs communs aux deux formats existaient déjà dans la beta.2 : le build de `main` (`1eeaa3b`) échoue aux mêmes cas, 2 répétitions sur 2, avec les mêmes symptômes.
|
|
99
|
+
|
|
100
|
+
| Cas | Symptôme, `main` comme beta.3 |
|
|
101
|
+
| --- | --- |
|
|
102
|
+
| `seek-while-playing` | Après un seek en lecture dans une séquence 4K, l'horloge reste vers 4,0 s pendant 4 s |
|
|
103
|
+
| `long-montage-120-clips` | Après un seek en pause dans un montage paresseux de 120 clips, la frame du clip 83 n'arrive pas |
|
|
104
|
+
| `missing-file` | Android 17 répond « Failed to instantiate extractor », que le harness ne reconnaît pas comme une erreur de fichier manquant |
|
|
105
|
+
|
|
106
|
+
Le quatrième échec en `rgba`, `4k-x8-eager`, dépasse le budget mémoire ; il passe en `yuv`.
|
|
107
|
+
|
|
108
|
+
| Pixel, `rgba` → `yuv` | Export | Mémoire max (PSS) |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| 8 clips 4K, paresseux | 29,0 → **12,9 s** | 1 020 → **766 Mio** |
|
|
111
|
+
| Montage en fondu, eager | 27,3 → **14,4 s** | 1 468 → **1 029 Mio** |
|
|
112
|
+
| 3 clips 4K simultanés | 23,2 → **7,3 s** | 1 314 → 1 507 Mio |
|
|
113
|
+
| Exports simples (`encode-*`) | 2,4–2,6 → **1,7–1,9 s** | ≈ égal |
|
|
114
|
+
| Lecture continue 4K | — | 554–602 → **438–458 Mio** |
|
|
115
|
+
|
|
116
|
+
**iPhone.** En `rgba`, trois cas s'arrêtent sur le budget de la file du décodeur : `montage-all-crossfade-eager`, `4k-x8-eager` et `4k-x3-simultaneous` (problème connu des versions précédentes). **En `yuv`, les trois passent**, et aucun cas n'échoue.
|
|
117
|
+
|
|
118
|
+
| iPhone, `rgba` → `yuv` | Mémoire physique max | Export |
|
|
119
|
+
| --- | --- | --- |
|
|
120
|
+
| Lecture continue 4K | 400–405 → **336–343 Mio** | — |
|
|
121
|
+
| 8 clips 4K, paresseux | 670–683 → **571–573 Mio** | 4,4–4,5 → 4,5–4,9 s |
|
|
122
|
+
| 3 clips 4K simultanés | budget dépassé → **734 Mio, passe** | — |
|
|
123
|
+
| Montage en fondu, paresseux | 650 → **585 Mio** | 4,3 → 3,6 s |
|
|
124
|
+
| 20 clips courts | ≈ | 3,3 → **2,5 s** |
|
|
125
|
+
| Montage de 120 clips | ≈ | **14,0** → 18,1 s |
|
|
126
|
+
|
|
127
|
+
## Tests natifs
|
|
128
|
+
|
|
129
|
+
- **iPhone, XCTest** : **56/58**, les deux autres étant des diagnostics qui ne tournent qu'à la demande. Six nouveaux tests : chaque frame d'un clip et d'un montage lues en NV12, le format annoncé par chaque frame, et la lecture de `framePixelFormat` (`auto` et `yuv` en NV12, `rgba` en BGRA, valeur inconnue refusée). Les deux diagnostics ont été lancés à part (débit du décodeur par format, étiquettes de couleur des clips sans étiquette).
|
|
130
|
+
- **Pixel, tests instrumentés** : **126/127** dans la suite complète. L'échec, `BigMontageTest.aLazyPreviewOfEight4KClipsPlaysThrough`, figure dans la référence connue (il échouait déjà sur `main` dans la suite complète). Lancée seule juste après, sur un Pixel aux cœurs à 85 °C après des heures de 4K, la classe échoue 2 fois sur 5 : le seuil de 150 frames à l'heure en 4K, et un `OutOfMemoryError` du tas Java après l'export eager de 8 clips 4K. Ces tests pilotent le décodeur Java sans `Looper`, chemin que cette version ne modifie pas. Ils n'ont pas été relancés à froid avant la release : l'attribution à la chaleur n'est pas confirmée.
|
|
131
|
+
|
|
132
|
+
## Runs écartés
|
|
133
|
+
|
|
134
|
+
- `iphone-full-rgba-interrupted-call` : appel téléphonique pendant le run.
|
|
135
|
+
- `iphone-full-yuv-interrupted-phone-in-use` : téléphone utilisé pendant le run.
|
|
136
|
+
- Les runs `sync` iPhone antérieurs à la correction des couleurs SD et à l'étiquetage forcé des clips photo ne servent pas pour le rendu.
|
|
137
|
+
|
|
138
|
+
Les résultats sont dans `benchmark/results/*.local.json`, fichiers locaux non versionnés.
|
|
139
|
+
|
|
140
|
+
## Toujours non qualifié
|
|
141
|
+
|
|
142
|
+
- Mémoire sur de longues sessions (dernière mesure en 3.0.6).
|
|
143
|
+
- HDR au-delà de la conversion HLG → SDR, son audible et synchronisation audio/vidéo.
|
|
144
|
+
- Les trois bugs Android de la suite `full` ci-dessus, et le seek dans une composition 4K paresseuse sur Pixel.
|
|
145
|
+
- Vraie vidéo 4K 60 fps à 60 Mbit/s sur iPhone chaud : 65 à 91 % des frames affichées selon les runs.
|
|
146
|
+
- Export iOS en `yuv` : environ 2 à 4 ms de plus par frame qu'en `rgba` (à optimiser).
|
|
147
|
+
- Aucune mesure iPhone à l'état thermique *nominal* pour la comparaison des formats : le téléphone est resté chaud toute la journée.
|
|
@@ -24,6 +24,15 @@ struct NativeResourceStats {
|
|
|
24
24
|
inline static ResourceCounter encoderSubmittingBuffers;
|
|
25
25
|
inline static ResourceCounter encoderSubmittingBytes;
|
|
26
26
|
};
|
|
27
|
+
// Composition preview diagnostics, counted since launch: an acquire refused
|
|
28
|
+
// while the frame ring is full, an acquire that found no decoded frame queued
|
|
29
|
+
// (the decoder not ahead), the decode ticks run, and those over 16 ms.
|
|
30
|
+
struct PreviewStats {
|
|
31
|
+
inline static std::atomic<uint64_t> ringRefusals{0};
|
|
32
|
+
inline static std::atomic<uint64_t> emptyQueues{0};
|
|
33
|
+
inline static std::atomic<uint64_t> decodeTicks{0};
|
|
34
|
+
inline static std::atomic<uint64_t> slowDecodeTicks{0};
|
|
35
|
+
};
|
|
27
36
|
inline size_t pixelBufferBytes(CVPixelBufferRef buffer) {
|
|
28
37
|
size_t bytes = CVPixelBufferGetDataSize(buffer);
|
|
29
38
|
return bytes ? bytes : CVPixelBufferGetBytesPerRow(buffer) *
|
package/ios/RNSVColorSpace.h
CHANGED
|
@@ -16,10 +16,10 @@ namespace RNSkiaVideo {
|
|
|
16
16
|
* contrast. Asking AVFoundation for these properties makes it tone-map to
|
|
17
17
|
* SDR on the way out, on its own fast path.
|
|
18
18
|
*
|
|
19
|
-
* Applied
|
|
20
|
-
* in Rec.709 costs nothing, and a BT.601 one gets the conversion it
|
|
21
|
-
* needed.
|
|
22
|
-
*
|
|
19
|
+
* Applied to every BGRA output rather than only to HDR sources: a source
|
|
20
|
+
* already in Rec.709 costs nothing, and a BT.601 one gets the conversion it
|
|
21
|
+
* always needed. NV12 output takes them only for HDR, see
|
|
22
|
+
* TrackNeedsSDRConversion.
|
|
23
23
|
*/
|
|
24
24
|
static inline NSDictionary* SDRColorProperties() {
|
|
25
25
|
return @{
|
|
@@ -29,6 +29,37 @@ static inline NSDictionary* SDRColorProperties() {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Whether a track needs AVFoundation's conversion to Rec.709 even when its
|
|
34
|
+
* frames are decoded to NV12: an HDR transfer function (HLG, PQ) or BT.2020
|
|
35
|
+
* primaries, which only a tone mapping brings to SDR.
|
|
36
|
+
*
|
|
37
|
+
* An SDR track decoded to NV12 is left as it is: its buffers carry their own
|
|
38
|
+
* YCbCr matrix, which the GPU applies when it converts them. Asked for Rec.709
|
|
39
|
+
* properties, VideoToolbox relabels an SD clip's BT.601 samples as BT.709
|
|
40
|
+
* without converting them, and its saturated colors shift (a yellow's blue
|
|
41
|
+
* by 11 levels), where its BGRA output does convert.
|
|
42
|
+
*/
|
|
43
|
+
static inline bool TrackNeedsSDRConversion(AVAssetTrack* track) {
|
|
44
|
+
#pragma clang diagnostic push
|
|
45
|
+
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
|
46
|
+
NSArray* descriptions = track.formatDescriptions;
|
|
47
|
+
#pragma clang diagnostic pop
|
|
48
|
+
for (id description in descriptions) {
|
|
49
|
+
auto format = (__bridge CMFormatDescriptionRef)description;
|
|
50
|
+
CFTypeRef transfer = CMFormatDescriptionGetExtension(
|
|
51
|
+
format, kCMFormatDescriptionExtension_TransferFunction);
|
|
52
|
+
CFTypeRef primaries = CMFormatDescriptionGetExtension(
|
|
53
|
+
format, kCMFormatDescriptionExtension_ColorPrimaries);
|
|
54
|
+
if (transfer && (CFEqual(transfer, kCVImageBufferTransferFunction_ITU_R_2100_HLG) ||
|
|
55
|
+
CFEqual(transfer, kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ)))
|
|
56
|
+
return true;
|
|
57
|
+
if (primaries && CFEqual(primaries, kCVImageBufferColorPrimaries_ITU_R_2020))
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
32
63
|
/**
|
|
33
64
|
* Stamps Rec.709 onto a buffer handed to AVAssetWriter. The writer's own
|
|
34
65
|
* output settings already declare it, but a buffer carrying no color
|
|
@@ -276,6 +276,10 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) {
|
|
|
276
276
|
add("encoders", NativeResourceStats::encoders);
|
|
277
277
|
add("encoderSubmittingBuffers", NativeResourceStats::encoderSubmittingBuffers);
|
|
278
278
|
add("encoderSubmittingBytes", NativeResourceStats::encoderSubmittingBytes);
|
|
279
|
+
result.setProperty(rt, "previewRingRefusals", static_cast<double>(PreviewStats::ringRefusals.load()));
|
|
280
|
+
result.setProperty(rt, "previewEmptyQueues", static_cast<double>(PreviewStats::emptyQueues.load()));
|
|
281
|
+
result.setProperty(rt, "previewDecodeTicks", static_cast<double>(PreviewStats::decodeTicks.load()));
|
|
282
|
+
result.setProperty(rt, "previewSlowDecodeTicks", static_cast<double>(PreviewStats::slowDecodeTicks.load()));
|
|
279
283
|
return jsi::Value(std::move(result));
|
|
280
284
|
}));
|
|
281
285
|
RNSVModule.setProperty(runtime, "getBackendInfo",
|
package/ios/VideoComposition.h
CHANGED
|
@@ -31,6 +31,11 @@ public:
|
|
|
31
31
|
* default) does. iOS only.
|
|
32
32
|
*/
|
|
33
33
|
bool directTexture = false;
|
|
34
|
+
/**
|
|
35
|
+
* The composition's `framePixelFormat: 'yuv'`: frames decoded to NV12, the
|
|
36
|
+
* decoder's own format, rather than BGRA. See setupReader.
|
|
37
|
+
*/
|
|
38
|
+
bool yuvFrames = false;
|
|
34
39
|
bool isVideo = true;
|
|
35
40
|
bool audioEnabled = false;
|
|
36
41
|
double audioVolume = 1.0;
|
|
@@ -61,6 +66,20 @@ public:
|
|
|
61
66
|
auto lazyProp = jsComposition.getProperty(runtime, "lazyDecoders");
|
|
62
67
|
composition->lazyDecoders = lazyProp.isBool() && lazyProp.getBool();
|
|
63
68
|
}
|
|
69
|
+
// 'auto' is NV12 on iOS, as on Android: a third of BGRA's memory, and the
|
|
70
|
+
// same picture for camera footage (docs/FRAME_PIXEL_FORMAT.md).
|
|
71
|
+
bool yuvFrames = true;
|
|
72
|
+
if (jsComposition.hasProperty(runtime, "framePixelFormat")) {
|
|
73
|
+
auto formatProp = jsComposition.getProperty(runtime, "framePixelFormat");
|
|
74
|
+
if (formatProp.isString()) {
|
|
75
|
+
auto format = formatProp.asString(runtime).utf8(runtime);
|
|
76
|
+
if (format != "auto" && format != "yuv" && format != "rgba")
|
|
77
|
+
throw jsi::JSError(runtime, "framePixelFormat must be 'auto', 'yuv' or 'rgba'");
|
|
78
|
+
yuvFrames = format != "rgba";
|
|
79
|
+
} else if (!formatProp.isUndefined()) {
|
|
80
|
+
throw jsi::JSError(runtime, "framePixelFormat must be 'auto', 'yuv' or 'rgba'");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
64
83
|
auto jsItems = jsComposition.getProperty(runtime, "items")
|
|
65
84
|
.asObject(runtime)
|
|
66
85
|
.asArray(runtime);
|
|
@@ -81,6 +100,7 @@ public:
|
|
|
81
100
|
item->duration = checkedMediaSeconds(jsItem.getProperty(runtime, "duration").asNumber());
|
|
82
101
|
checkedMediaSeconds(item->startTime + item->duration);
|
|
83
102
|
checkedMediaSeconds(item->compositionStartTime + item->duration);
|
|
103
|
+
item->yuvFrames = yuvFrames;
|
|
84
104
|
item->resolution = CGSize();
|
|
85
105
|
if (jsItem.hasProperty(runtime, "resolution")) {
|
|
86
106
|
auto resProp = jsItem.getProperty(runtime, "resolution");
|
|
@@ -71,6 +71,10 @@ private:
|
|
|
71
71
|
RNSVDisplayLinkWrapper* displayLink;
|
|
72
72
|
NSDate* startDate;
|
|
73
73
|
CMTime pausePosition = kCMTimeZero;
|
|
74
|
+
// The clock decodeCompositionFrames last picked the frames at, and when
|
|
75
|
+
// (CACurrentMediaTime); invalid once play, pause or a seek moves the clock.
|
|
76
|
+
CMTime framesTime = kCMTimeInvalid;
|
|
77
|
+
CFTimeInterval framesTimeHost = 0;
|
|
74
78
|
bool playWhenReady = false;
|
|
75
79
|
bool isPlaying = false;
|
|
76
80
|
bool isLooping = false;
|
|
@@ -91,6 +95,7 @@ private:
|
|
|
91
95
|
void pause();
|
|
92
96
|
void seekTo(CMTime time);
|
|
93
97
|
CMTime getCurrentTime();
|
|
98
|
+
CMTime getPresentedTime();
|
|
94
99
|
void release();
|
|
95
100
|
bool opensAt(const std::shared_ptr<VideoCompositionItem>& item,
|
|
96
101
|
double position) const;
|