react-native-skia-webgpu-video 0.0.0-stage → 0.1.0-beta.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/LICENSE +22 -0
- package/README.md +303 -2
- package/android/BUILD_VALIDATION.md +62 -0
- package/android/CMakeLists.txt +99 -0
- package/android/build-validation.json +262 -0
- package/android/build.gradle +185 -0
- package/android/cpp/EncoderTargets.cpp +226 -0
- package/android/cpp/EncoderTargets.h +70 -0
- package/android/cpp/JNIHelpers.cpp +15 -0
- package/android/cpp/JNIHelpers.h +20 -0
- package/android/cpp/NativeEventDispatcher.cpp +59 -0
- package/android/cpp/NativeEventDispatcher.h +31 -0
- package/android/cpp/NativeHardwareBuffer.cpp +397 -0
- package/android/cpp/NativeHardwareBuffer.h +34 -0
- package/android/cpp/NativeRgbaBuffer.cpp +45 -0
- package/android/cpp/NativeRgbaBuffer.h +9 -0
- package/android/cpp/ReturnedVideoFrames.h +37 -0
- package/android/cpp/RgbaFrameLease.h +53 -0
- package/android/cpp/VideoCapabilities.cpp +87 -0
- package/android/cpp/VideoCapabilities.h +44 -0
- package/android/cpp/VideoComposition.cpp +170 -0
- package/android/cpp/VideoComposition.h +36 -0
- package/android/cpp/VideoCompositionFramesExtractor.cpp +82 -0
- package/android/cpp/VideoCompositionFramesExtractor.h +48 -0
- package/android/cpp/VideoCompositionFramesExtractorHostObject.cpp +230 -0
- package/android/cpp/VideoCompositionFramesExtractorHostObject.h +42 -0
- package/android/cpp/VideoCompositionFramesExtractorSyncHostObject.cpp +133 -0
- package/android/cpp/VideoCompositionFramesExtractorSyncHostObject.h +46 -0
- package/android/cpp/VideoEncoderHostObject.cpp +318 -0
- package/android/cpp/VideoEncoderHostObject.h +76 -0
- package/android/cpp/VideoFrame.cpp +293 -0
- package/android/cpp/VideoFrame.h +27 -0
- package/android/cpp/VideoPlayer.cpp +96 -0
- package/android/cpp/VideoPlayer.h +52 -0
- package/android/cpp/VideoPlayerHostObject.cpp +225 -0
- package/android/cpp/VideoPlayerHostObject.h +43 -0
- package/android/cpp/cpp-adapter.cpp +316 -0
- package/android/gradle.properties +5 -0
- package/android/instrumentation-inventory.json +178 -0
- package/android/proguard-rules.pro +4 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/azzapp/rnskv/AudioCompositionExporter.java +537 -0
- package/android/src/main/java/com/azzapp/rnskv/AudioCompositionPlayer.java +321 -0
- package/android/src/main/java/com/azzapp/rnskv/BufferLifetime.java +55 -0
- package/android/src/main/java/com/azzapp/rnskv/DecodeRequests.java +100 -0
- package/android/src/main/java/com/azzapp/rnskv/DecoderWindow.java +55 -0
- package/android/src/main/java/com/azzapp/rnskv/EGLResourcesHolder.java +132 -0
- package/android/src/main/java/com/azzapp/rnskv/EGLUtils.java +225 -0
- package/android/src/main/java/com/azzapp/rnskv/FrameSize.java +50 -0
- package/android/src/main/java/com/azzapp/rnskv/GLFrameExtractor.java +510 -0
- package/android/src/main/java/com/azzapp/rnskv/JNIHelpers.java +12 -0
- package/android/src/main/java/com/azzapp/rnskv/NativeEventDispatcher.java +25 -0
- package/android/src/main/java/com/azzapp/rnskv/NativeHardwareBuffer.java +62 -0
- package/android/src/main/java/com/azzapp/rnskv/NativeRgbaBuffer.java +35 -0
- package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoModule.java +45 -0
- package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoPackage.java +43 -0
- package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoSpec.java +9 -0
- package/android/src/main/java/com/azzapp/rnskv/RgbaBufferAllocator.java +83 -0
- package/android/src/main/java/com/azzapp/rnskv/RgbaLayout.java +33 -0
- package/android/src/main/java/com/azzapp/rnskv/SurfaceOutputPolicy.java +13 -0
- package/android/src/main/java/com/azzapp/rnskv/TextureRenderer.java +150 -0
- package/android/src/main/java/com/azzapp/rnskv/TimeHelpers.java +11 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCapabilities.java +354 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoComposition.java +147 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +772 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +354 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractorSync.java +241 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionItemDecoder.java +492 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +639 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoFrame.java +74 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoPlayer.java +361 -0
- package/cpp/RNSVCheckedSizes.h +54 -0
- package/cpp/RNSVEventEmitter.cpp +199 -0
- package/cpp/RNSVEventEmitter.h +31 -0
- package/cpp/RNSVHostObject.cpp +79 -0
- package/cpp/RNSVHostObject.h +67 -0
- package/cpp/RNSVMemoryBudget.cpp +68 -0
- package/cpp/RNSVMemoryBudget.h +100 -0
- package/cpp/RNSVRuntimeLifecycleMonitor.cpp +88 -0
- package/cpp/RNSVRuntimeLifecycleMonitor.h +33 -0
- package/docs/ANDROID_DEVICE_306.md +95 -0
- package/docs/ANDROID_EXPORT_CACHE_306.md +48 -0
- package/docs/ANDROID_MEMORY_RETENTION_306.md +67 -0
- package/docs/BENCHMARKS.md +368 -0
- package/docs/IOS_DEVICE_TESTS.md +115 -0
- package/docs/IOS_SIMULATOR_TESTS.md +74 -0
- package/docs/MIGRATION.md +82 -0
- package/docs/NATIVE_MEMORY_INVESTIGATION_306.md +88 -0
- package/docs/PERFORMANCE_AB_IPHONE_303.md +196 -0
- package/docs/PERFORMANCE_AB_SIMULATOR.md +135 -0
- package/docs/PERFORMANCE_AB_SOAK_SIMULATOR.md +84 -0
- package/docs/PERFORMANCE_IPHONE_306_OPTIMIZATION.md +76 -0
- package/docs/PERFORMANCE_SIMULATOR.md +89 -0
- package/docs/PERFORMANCE_SIMULATOR_INITIAL.md +96 -0
- package/docs/PERFORMANCE_SOAK_IPHONE_303.md +41 -0
- package/docs/UPSTREAM_NOTES.md +188 -0
- package/docs/assets/banner.svg +26 -0
- package/examples/webgpu-overlay.ts +147 -0
- package/ios/AVAssetTrackUtils.h +24 -0
- package/ios/AudioCompositionUtils.h +27 -0
- package/ios/AudioCompositionUtils.mm +113 -0
- package/ios/DecoderWindow.h +52 -0
- package/ios/NativeResourceStats.h +32 -0
- package/ios/RNSVColorSpace.h +50 -0
- package/ios/RNSVJSIUtils.h +46 -0
- package/ios/RNSVScopedPixelBuffer.h +17 -0
- package/ios/RNSVVideoPlayer.h +46 -0
- package/ios/RNSVVideoPlayer.mm +410 -0
- package/ios/ReactNativeSkiaVideo.h +8 -0
- package/ios/ReactNativeSkiaVideo.mm +310 -0
- package/ios/SeekPolicy.h +27 -0
- package/ios/VideoComposition.h +148 -0
- package/ios/VideoCompositionFramesExtractorHostObject.h +105 -0
- package/ios/VideoCompositionFramesExtractorHostObject.mm +686 -0
- package/ios/VideoCompositionFramesExtractorSyncHostObject.h +37 -0
- package/ios/VideoCompositionFramesExtractorSyncHostObject.mm +168 -0
- package/ios/VideoCompositionItemDecoder.h +60 -0
- package/ios/VideoCompositionItemDecoder.mm +480 -0
- package/ios/VideoEncoderHostObject.h +83 -0
- package/ios/VideoEncoderHostObject.mm +706 -0
- package/ios/VideoFrame.h +66 -0
- package/ios/VideoFrame.mm +108 -0
- package/ios/VideoPlayerHostObject.h +52 -0
- package/ios/VideoPlayerHostObject.mm +332 -0
- package/lib/module/NativeReactNativeSkiaVideo.js +5 -0
- package/lib/module/NativeReactNativeSkiaVideo.js.map +1 -0
- package/lib/module/RNSkiaVideoModule.js +20 -0
- package/lib/module/RNSkiaVideoModule.js.map +1 -0
- package/lib/module/canvas.js +18 -0
- package/lib/module/canvas.js.map +1 -0
- package/lib/module/exportCache.js +22 -0
- package/lib/module/exportCache.js.map +1 -0
- package/lib/module/exportFrames.js +338 -0
- package/lib/module/exportFrames.js.map +1 -0
- package/lib/module/exportQueue.js +52 -0
- package/lib/module/exportQueue.js.map +1 -0
- package/lib/module/exportTarget.js +137 -0
- package/lib/module/exportTarget.js.map +1 -0
- package/lib/module/exportVideoComposition.js +492 -0
- package/lib/module/exportVideoComposition.js.map +1 -0
- package/lib/module/frameInterop.js +532 -0
- package/lib/module/frameInterop.js.map +1 -0
- package/lib/module/gpu.js +222 -0
- package/lib/module/gpu.js.map +1 -0
- package/lib/module/gpuDevice.js +54 -0
- package/lib/module/gpuDevice.js.map +1 -0
- package/lib/module/index.js +36 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/memory.js +38 -0
- package/lib/module/memory.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/previewLifecycle.js +52 -0
- package/lib/module/previewLifecycle.js.map +1 -0
- package/lib/module/runtimeYield.js +10 -0
- package/lib/module/runtimeYield.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/useEventListener.js +18 -0
- package/lib/module/utils/useEventListener.js.map +1 -0
- package/lib/module/videoCompositionPlayer.js +348 -0
- package/lib/module/videoCompositionPlayer.js.map +1 -0
- package/lib/module/videoFrameImage.js +185 -0
- package/lib/module/videoFrameImage.js.map +1 -0
- package/lib/module/videoPlayer.js +172 -0
- package/lib/module/videoPlayer.js.map +1 -0
- package/lib/module/yuvBlit.js +299 -0
- package/lib/module/yuvBlit.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts +7 -0
- package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts.map +1 -0
- package/lib/typescript/src/RNSkiaVideoModule.d.ts +4 -0
- package/lib/typescript/src/RNSkiaVideoModule.d.ts.map +1 -0
- package/lib/typescript/src/canvas.d.ts +4 -0
- package/lib/typescript/src/canvas.d.ts.map +1 -0
- package/lib/typescript/src/exportCache.d.ts +6 -0
- package/lib/typescript/src/exportCache.d.ts.map +1 -0
- package/lib/typescript/src/exportFrames.d.ts +57 -0
- package/lib/typescript/src/exportFrames.d.ts.map +1 -0
- package/lib/typescript/src/exportQueue.d.ts +5 -0
- package/lib/typescript/src/exportQueue.d.ts.map +1 -0
- package/lib/typescript/src/exportTarget.d.ts +28 -0
- package/lib/typescript/src/exportTarget.d.ts.map +1 -0
- package/lib/typescript/src/exportVideoComposition.d.ts +68 -0
- package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -0
- package/lib/typescript/src/frameInterop.d.ts +17 -0
- package/lib/typescript/src/frameInterop.d.ts.map +1 -0
- package/lib/typescript/src/gpu.d.ts +39 -0
- package/lib/typescript/src/gpu.d.ts.map +1 -0
- package/lib/typescript/src/gpuDevice.d.ts +11 -0
- package/lib/typescript/src/gpuDevice.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +21 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/memory.d.ts +25 -0
- package/lib/typescript/src/memory.d.ts.map +1 -0
- package/lib/typescript/src/previewLifecycle.d.ts +22 -0
- package/lib/typescript/src/previewLifecycle.d.ts.map +1 -0
- package/lib/typescript/src/runtimeYield.d.ts +4 -0
- package/lib/typescript/src/runtimeYield.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +631 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/utils/useEventListener.d.ts +5 -0
- package/lib/typescript/src/utils/useEventListener.d.ts.map +1 -0
- package/lib/typescript/src/videoCompositionPlayer.d.ts +85 -0
- package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -0
- package/lib/typescript/src/videoFrameImage.d.ts +60 -0
- package/lib/typescript/src/videoFrameImage.d.ts.map +1 -0
- package/lib/typescript/src/videoPlayer.d.ts +99 -0
- package/lib/typescript/src/videoPlayer.d.ts.map +1 -0
- package/lib/typescript/src/yuvBlit.d.ts +50 -0
- package/lib/typescript/src/yuvBlit.d.ts.map +1 -0
- package/package.json +154 -4
- package/patches/react-native-skia+3.7.0.patch +502 -0
- package/react-native-skia-webgpu-video.podspec +35 -0
- package/scripts/apply-skia-canvas-patch.mjs +48 -0
- package/scripts/skia-canvas-patch.json +291 -0
- package/src/NativeReactNativeSkiaVideo.ts +8 -0
- package/src/RNSkiaVideoModule.ts +27 -0
- package/src/canvas.ts +22 -0
- package/src/exportCache.ts +27 -0
- package/src/exportFrames.ts +421 -0
- package/src/exportQueue.ts +65 -0
- package/src/exportTarget.ts +179 -0
- package/src/exportVideoComposition.ts +718 -0
- package/src/frameInterop.ts +684 -0
- package/src/gpu.ts +290 -0
- package/src/gpuDevice.ts +80 -0
- package/src/index.tsx +45 -0
- package/src/memory.ts +48 -0
- package/src/previewLifecycle.ts +60 -0
- package/src/runtimeYield.ts +6 -0
- package/src/types.ts +661 -0
- package/src/utils/useEventListener.ts +22 -0
- package/src/videoCompositionPlayer.ts +520 -0
- package/src/videoFrameImage.ts +205 -0
- package/src/videoPlayer.ts +318 -0
- package/src/yuvBlit.ts +331 -0
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertExportRecorderCacheSupport,
|
|
3
|
+
trimExportRecorderCache,
|
|
4
|
+
} from "./exportCache";
|
|
5
|
+
import { getVideoCanvas } from "./canvas";
|
|
6
|
+
import {
|
|
7
|
+
createWorkletRuntime,
|
|
8
|
+
createSynchronizable,
|
|
9
|
+
runOnRuntime,
|
|
10
|
+
scheduleOnRN,
|
|
11
|
+
type WorkletRuntime,
|
|
12
|
+
} from "react-native-worklets";
|
|
13
|
+
import { AlphaType, BlendMode, ColorType, Skia } from "react-native-skia";
|
|
14
|
+
import type { ImageInfo, SkCanvas, SkSurface } from "react-native-skia";
|
|
15
|
+
import { Platform } from "react-native";
|
|
16
|
+
import type {
|
|
17
|
+
ExportOptions,
|
|
18
|
+
FrameDrawer,
|
|
19
|
+
VideoComposition,
|
|
20
|
+
VideoEncoder,
|
|
21
|
+
VideoCompositionFramesExtractorSync,
|
|
22
|
+
VideoFrame,
|
|
23
|
+
} from "./types";
|
|
24
|
+
import RNSkiaVideoModule from "./RNSkiaVideoModule";
|
|
25
|
+
import {
|
|
26
|
+
clearVideoFrameImages,
|
|
27
|
+
ownVideoFrames,
|
|
28
|
+
releaseVideoFrameImage,
|
|
29
|
+
} from "./frameInterop";
|
|
30
|
+
import {
|
|
31
|
+
frameByteSize,
|
|
32
|
+
releaseVideoMemory,
|
|
33
|
+
reserveVideoMemory,
|
|
34
|
+
} from "./memory";
|
|
35
|
+
import { enqueueExport, exportAbortError } from "./exportQueue";
|
|
36
|
+
import {
|
|
37
|
+
acquireEncoderTarget,
|
|
38
|
+
closeEncoderTarget,
|
|
39
|
+
probeEncoderTarget,
|
|
40
|
+
supportsEncoderTargets,
|
|
41
|
+
type EncoderTarget,
|
|
42
|
+
} from "./exportTarget";
|
|
43
|
+
import { yieldToRuntime } from "./runtimeYield";
|
|
44
|
+
import {
|
|
45
|
+
createExportFrameImports,
|
|
46
|
+
ownExportFrames,
|
|
47
|
+
releaseExportFrames,
|
|
48
|
+
type ExportFrameImports,
|
|
49
|
+
} from "./exportFrames";
|
|
50
|
+
|
|
51
|
+
export type VideoFrameProcessingContext = {
|
|
52
|
+
frames: Record<string, VideoFrame>;
|
|
53
|
+
currentTime: number;
|
|
54
|
+
width: number;
|
|
55
|
+
height: number;
|
|
56
|
+
videoComposition: VideoComposition;
|
|
57
|
+
isCancelled: () => boolean;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Optional per-export GPU, 3D or ML adapter. Construct resources on the
|
|
61
|
+
* export runtime; the legacy draw callback stays synchronous. */
|
|
62
|
+
export type VideoFrameProcessor = {
|
|
63
|
+
prepareFrame: (context: VideoFrameProcessingContext) => void | Promise<void>;
|
|
64
|
+
/** Must finish every GPU/ML read before settling. A rejected cleanup is
|
|
65
|
+
* conservatively retained and prevents another export on that runtime. */
|
|
66
|
+
dispose: () => void | Promise<void>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Cumulative wall-clock milliseconds per export stage, measured on the
|
|
70
|
+
* export runtime. Stages are sequential, so they add up to `totalMs`. */
|
|
71
|
+
export type ExportProfile = {
|
|
72
|
+
frames: number;
|
|
73
|
+
totalMs: number;
|
|
74
|
+
/** Native decoding up to each frame time. */
|
|
75
|
+
decodeMs: number;
|
|
76
|
+
/** Wrapping or copying decoded frames for Skia, and closing replaced ones. */
|
|
77
|
+
importMs: number;
|
|
78
|
+
/** createFrameProcessor's prepareFrame. */
|
|
79
|
+
prepareMs: number;
|
|
80
|
+
/** GPU export: lending and wrapping an encoder buffer, including waits
|
|
81
|
+
* for the bounded pool to return one. */
|
|
82
|
+
targetMs: number;
|
|
83
|
+
/** drawFrame, before/afterDrawFrame and progress scheduling. */
|
|
84
|
+
drawMs: number;
|
|
85
|
+
/** The checked flush(true): waiting for the GPU to complete the frame. */
|
|
86
|
+
gpuMs: number;
|
|
87
|
+
/** Readback when used, submission to the encoder including its
|
|
88
|
+
* backpressure, and the final file write. */
|
|
89
|
+
encodeMs: number;
|
|
90
|
+
/** Between frames, on the export runtime's event loop. */
|
|
91
|
+
yieldMs: number;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type VideoExportArguments<T = undefined> = ExportOptions & {
|
|
95
|
+
videoComposition: VideoComposition;
|
|
96
|
+
drawFrame: FrameDrawer<T>;
|
|
97
|
+
beforeDrawFrame?: () => T;
|
|
98
|
+
afterDrawFrame?: (context: T) => void;
|
|
99
|
+
onProgress?: (progress: {
|
|
100
|
+
framesCompleted: number;
|
|
101
|
+
nbFrames: number;
|
|
102
|
+
}) => void;
|
|
103
|
+
abortSignal?: AbortSignal;
|
|
104
|
+
/** A worklet factory, initialized once. prepareFrame completes before
|
|
105
|
+
* drawing; dispose completes before export resolves or rejects. */
|
|
106
|
+
createFrameProcessor?: (context: {
|
|
107
|
+
width: number;
|
|
108
|
+
height: number;
|
|
109
|
+
videoComposition: VideoComposition;
|
|
110
|
+
isCancelled: () => boolean;
|
|
111
|
+
}) => VideoFrameProcessor | Promise<VideoFrameProcessor>;
|
|
112
|
+
/** Called once, after the last frame is written and before the export
|
|
113
|
+
* resolves. Measuring adds a few clock reads per frame; omit to skip it. */
|
|
114
|
+
onProfile?: (profile: ExportProfile) => void;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Native Skia 3.4.4 accepts destination/stride in readPixels; its
|
|
118
|
+
// published Canvas type currently omits these arguments. Use only a private,
|
|
119
|
+
// full-length, zero-offset Uint8Array (the native implementation ignores offsets).
|
|
120
|
+
type ReadbackCanvas = {
|
|
121
|
+
readPixels: (
|
|
122
|
+
x: number,
|
|
123
|
+
y: number,
|
|
124
|
+
info: ImageInfo,
|
|
125
|
+
destination: Uint8Array,
|
|
126
|
+
bytesPerRow: number,
|
|
127
|
+
) => Uint8Array | Float32Array | null;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
type ExportState = {
|
|
131
|
+
encoder: VideoEncoder | null;
|
|
132
|
+
extractor: VideoCompositionFramesExtractorSync | null;
|
|
133
|
+
surface: SkSurface | null;
|
|
134
|
+
canvas: SkCanvas | null;
|
|
135
|
+
readback: Uint8Array | null;
|
|
136
|
+
/** Draw into a lent encoder buffer instead of reading pixels back. */
|
|
137
|
+
gpu: boolean;
|
|
138
|
+
target: EncoderTarget | null;
|
|
139
|
+
/** iOS GPU export: decoded frames sampled in place, by producer. */
|
|
140
|
+
imports: ExportFrameImports | null;
|
|
141
|
+
frames: Record<string, VideoFrame>;
|
|
142
|
+
processor: VideoFrameProcessor | null;
|
|
143
|
+
pendingAfterDraw: (() => void) | null;
|
|
144
|
+
reservations: number[];
|
|
145
|
+
index: number;
|
|
146
|
+
setupError?: unknown;
|
|
147
|
+
poisoned?: boolean;
|
|
148
|
+
hasRecorder?: boolean;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
let exportRuntime: WorkletRuntime | null = null;
|
|
152
|
+
const getExportRuntime = () => {
|
|
153
|
+
if (!exportRuntime)
|
|
154
|
+
exportRuntime = createWorkletRuntime({ name: "RNSkiaWebGPUVideoExport" });
|
|
155
|
+
return exportRuntime;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// The persistent worklet thread does not drain Apple autoreleased objects.
|
|
159
|
+
// Scope synchronous native/Skia work only: an async callback would return its
|
|
160
|
+
// Promise before that work finishes, so its later continuation needs its own
|
|
161
|
+
// synchronous scope.
|
|
162
|
+
const runWithVideoAutoreleasePool = <T>(fn: () => T): T => {
|
|
163
|
+
"worklet";
|
|
164
|
+
return (
|
|
165
|
+
RNSkiaVideoModule.runWithAutoreleasePool ?? ((body: () => T) => body())
|
|
166
|
+
)(fn);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/** Public drawing/timing contract preserved; one step is submitted at a time.
|
|
170
|
+
* Native input and output queues, the shared budget and serialized exports
|
|
171
|
+
* prevent slow GPU/codec/ML consumers from growing an unbounded frame backlog. */
|
|
172
|
+
export const exportVideoComposition = <T = undefined>(
|
|
173
|
+
args: VideoExportArguments<T>,
|
|
174
|
+
): Promise<void> => enqueueExport(() => executeExport(args), args.abortSignal);
|
|
175
|
+
|
|
176
|
+
const executeExport = <T>(args: VideoExportArguments<T>): Promise<void> =>
|
|
177
|
+
new Promise((resolve, reject) => {
|
|
178
|
+
const {
|
|
179
|
+
videoComposition,
|
|
180
|
+
drawFrame,
|
|
181
|
+
beforeDrawFrame,
|
|
182
|
+
afterDrawFrame,
|
|
183
|
+
onProgress,
|
|
184
|
+
abortSignal,
|
|
185
|
+
createFrameProcessor,
|
|
186
|
+
onProfile,
|
|
187
|
+
...options
|
|
188
|
+
} = args;
|
|
189
|
+
if (abortSignal?.aborted) {
|
|
190
|
+
reject(exportAbortError(abortSignal));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
let bytes: number;
|
|
194
|
+
let nbFrames: number;
|
|
195
|
+
try {
|
|
196
|
+
bytes = frameByteSize(options.width, options.height);
|
|
197
|
+
if (
|
|
198
|
+
!Number.isFinite(options.frameRate) ||
|
|
199
|
+
options.frameRate <= 0 ||
|
|
200
|
+
!Number.isFinite(videoComposition.duration) ||
|
|
201
|
+
videoComposition.duration <= 0 ||
|
|
202
|
+
!Number.isFinite(options.bitRate) ||
|
|
203
|
+
options.bitRate <= 0 ||
|
|
204
|
+
!options.outPath
|
|
205
|
+
) {
|
|
206
|
+
throw new Error("Invalid video export options");
|
|
207
|
+
}
|
|
208
|
+
nbFrames = Math.ceil(
|
|
209
|
+
videoComposition.duration * options.frameRate - 1e-6,
|
|
210
|
+
);
|
|
211
|
+
if (!Number.isSafeInteger(nbFrames) || nbFrames <= 0)
|
|
212
|
+
throw new Error("Invalid export frame count");
|
|
213
|
+
} catch (error) {
|
|
214
|
+
reject(error);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const cancelled = createSynchronizable(false);
|
|
218
|
+
const onAbort = () => cancelled.setBlocking(true);
|
|
219
|
+
abortSignal?.addEventListener("abort", onAbort);
|
|
220
|
+
// Cancellation is observed in the worklet. Rejection happens after draining
|
|
221
|
+
// and cleanup, so the caller may immediately retry/reuse its output path.
|
|
222
|
+
const settle = (
|
|
223
|
+
error: unknown,
|
|
224
|
+
aborted: boolean,
|
|
225
|
+
failed = error !== null,
|
|
226
|
+
) => {
|
|
227
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
228
|
+
if (aborted) reject(exportAbortError(abortSignal));
|
|
229
|
+
else if (failed) reject(error);
|
|
230
|
+
else resolve();
|
|
231
|
+
};
|
|
232
|
+
let runtime: WorkletRuntime;
|
|
233
|
+
try {
|
|
234
|
+
runtime = getExportRuntime();
|
|
235
|
+
} catch (error) {
|
|
236
|
+
settle(error, cancelled.getBlocking(), true);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const bgra = Platform.OS === "ios";
|
|
240
|
+
// iOS always; Android when its AHardwareBuffer interop is negotiated.
|
|
241
|
+
const gpuExport =
|
|
242
|
+
RNSkiaVideoModule.getBackendInfo?.()?.gpuDirectExport === true;
|
|
243
|
+
// Keep the export closure identity stable across frames. Recreating this
|
|
244
|
+
// worklet per image makes Worklets reconstruct its entire captured graph
|
|
245
|
+
// on the persistent runtime, including nested drawing/native functions.
|
|
246
|
+
const step = () => {
|
|
247
|
+
try {
|
|
248
|
+
runOnRuntime(runtime, frameWorklet)();
|
|
249
|
+
} catch (error) {
|
|
250
|
+
settle(error, cancelled.getBlocking(), true);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const frameWorklet = () => {
|
|
254
|
+
"worklet";
|
|
255
|
+
const storage = globalThis as typeof globalThis & {
|
|
256
|
+
__rnskwgpuExport?: ExportState;
|
|
257
|
+
};
|
|
258
|
+
const state = storage.__rnskwgpuExport;
|
|
259
|
+
if (!state) {
|
|
260
|
+
scheduleOnRN(settle, new Error("Export state unavailable"), false);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const isCancelled = () => {
|
|
264
|
+
"worklet";
|
|
265
|
+
return cancelled.getBlocking();
|
|
266
|
+
};
|
|
267
|
+
// Per session: this worklet runs once and loops over every frame.
|
|
268
|
+
const profile = onProfile
|
|
269
|
+
? {
|
|
270
|
+
frames: 0,
|
|
271
|
+
totalMs: 0,
|
|
272
|
+
decodeMs: 0,
|
|
273
|
+
importMs: 0,
|
|
274
|
+
prepareMs: 0,
|
|
275
|
+
targetMs: 0,
|
|
276
|
+
drawMs: 0,
|
|
277
|
+
gpuMs: 0,
|
|
278
|
+
encodeMs: 0,
|
|
279
|
+
yieldMs: 0,
|
|
280
|
+
}
|
|
281
|
+
: null;
|
|
282
|
+
const clock = { last: -1 };
|
|
283
|
+
/** Charges the time since the previous lap to one stage. */
|
|
284
|
+
const lap = (
|
|
285
|
+
stage: Exclude<keyof ExportProfile, "frames" | "totalMs">,
|
|
286
|
+
) => {
|
|
287
|
+
"worklet";
|
|
288
|
+
if (!profile) return;
|
|
289
|
+
const now = performance.now();
|
|
290
|
+
if (clock.last >= 0) profile[stage] += now - clock.last;
|
|
291
|
+
clock.last = now;
|
|
292
|
+
};
|
|
293
|
+
const finish = async (
|
|
294
|
+
error: unknown,
|
|
295
|
+
aborted = false,
|
|
296
|
+
failed = false,
|
|
297
|
+
) => {
|
|
298
|
+
"worklet";
|
|
299
|
+
const cleanup = { failure: error, failed, safeToRelease: true };
|
|
300
|
+
// A rejected adapter drain cannot prove that it stopped reading
|
|
301
|
+
// our pixels. Retain/account that session instead of recycling them.
|
|
302
|
+
const clean = (fn: () => void) => {
|
|
303
|
+
"worklet";
|
|
304
|
+
try {
|
|
305
|
+
runWithVideoAutoreleasePool(fn);
|
|
306
|
+
return true;
|
|
307
|
+
} catch (e) {
|
|
308
|
+
if (!cleanup.failed) cleanup.failure = e;
|
|
309
|
+
cleanup.failed = true;
|
|
310
|
+
cleanup.safeToRelease = false;
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
// Submit any recording left by a drawing exception before an
|
|
315
|
+
// adapter is allowed to destroy textures referenced by that canvas.
|
|
316
|
+
clean(() => state.surface?.flush(true));
|
|
317
|
+
clean(() => state.target?.surface?.flush(true));
|
|
318
|
+
if (cleanup.safeToRelease && state.pendingAfterDraw) {
|
|
319
|
+
if (clean(state.pendingAfterDraw)) state.pendingAfterDraw = null;
|
|
320
|
+
}
|
|
321
|
+
if (cleanup.safeToRelease) {
|
|
322
|
+
try {
|
|
323
|
+
await state.processor?.dispose();
|
|
324
|
+
state.processor = null;
|
|
325
|
+
} catch (e) {
|
|
326
|
+
if (!cleanup.failed) cleanup.failure = e;
|
|
327
|
+
cleanup.failed = true;
|
|
328
|
+
cleanup.safeToRelease = false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (clean(() => state.extractor?.dispose())) state.extractor = null;
|
|
332
|
+
if (clean(() => state.encoder?.dispose())) state.encoder = null;
|
|
333
|
+
if (cleanup.safeToRelease) {
|
|
334
|
+
clean(() => state.surface?.flush(true));
|
|
335
|
+
clean(() => state.target?.surface?.flush(true));
|
|
336
|
+
if (cleanup.safeToRelease) {
|
|
337
|
+
for (const frame of Object.values(state.frames))
|
|
338
|
+
clean(() => frame.dispose?.());
|
|
339
|
+
const imports = state.imports;
|
|
340
|
+
if (imports && clean(() => releaseExportFrames(imports)))
|
|
341
|
+
state.imports = null;
|
|
342
|
+
clean(() => clearVideoFrameImages(true));
|
|
343
|
+
}
|
|
344
|
+
if (cleanup.safeToRelease && clean(() => state.canvas?.dispose()))
|
|
345
|
+
state.canvas = null;
|
|
346
|
+
if (cleanup.safeToRelease && clean(() => state.surface?.dispose()))
|
|
347
|
+
state.surface = null;
|
|
348
|
+
const target = state.target;
|
|
349
|
+
if (cleanup.safeToRelease && target) {
|
|
350
|
+
if (clean(() => closeEncoderTarget(target))) state.target = null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// The export runtime is private and serialized. Its unused cache
|
|
354
|
+
// need not survive a completed session; leave the shared Context
|
|
355
|
+
// and the UI/runtime caches alone. A failed drain stays quarantined.
|
|
356
|
+
if (cleanup.safeToRelease && state.hasRecorder) {
|
|
357
|
+
if (clean(trimExportRecorderCache)) state.hasRecorder = false;
|
|
358
|
+
}
|
|
359
|
+
if (cleanup.safeToRelease) {
|
|
360
|
+
for (let i = state.reservations.length - 1; i >= 0; i--) {
|
|
361
|
+
if (clean(() => releaseVideoMemory(state.reservations[i]!)))
|
|
362
|
+
state.reservations.splice(i, 1);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (cleanup.safeToRelease) {
|
|
366
|
+
state.frames = {};
|
|
367
|
+
state.readback = null;
|
|
368
|
+
delete storage.__rnskwgpuExport;
|
|
369
|
+
} else {
|
|
370
|
+
// One retained session at most; subsequent exports fail before
|
|
371
|
+
// allocating anything. Runtime teardown is the recovery boundary.
|
|
372
|
+
state.poisoned = true;
|
|
373
|
+
state.setupError = cleanup.failure;
|
|
374
|
+
}
|
|
375
|
+
scheduleOnRN(
|
|
376
|
+
settle,
|
|
377
|
+
cleanup.failure,
|
|
378
|
+
aborted || isCancelled(),
|
|
379
|
+
cleanup.failed,
|
|
380
|
+
);
|
|
381
|
+
};
|
|
382
|
+
// One frame per call; true when another frame follows.
|
|
383
|
+
const work = async (): Promise<boolean> => {
|
|
384
|
+
"worklet";
|
|
385
|
+
try {
|
|
386
|
+
if ("setupError" in state) throw state.setupError;
|
|
387
|
+
lap("yieldMs");
|
|
388
|
+
if (isCancelled()) {
|
|
389
|
+
await finish(null, true);
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
const time = state.index / options.frameRate;
|
|
393
|
+
// Purge ended/closed producer images, including lazily opened clips.
|
|
394
|
+
const owned = runWithVideoAutoreleasePool(() => {
|
|
395
|
+
const decoded = state.extractor!.decodeCompositionFrames(time);
|
|
396
|
+
lap("decodeMs");
|
|
397
|
+
const imports = state.imports;
|
|
398
|
+
// In place, a producer's frame is released by its next import.
|
|
399
|
+
const imported = imports
|
|
400
|
+
? ownExportFrames(imports, decoded, ownVideoFrames)
|
|
401
|
+
: ownVideoFrames(decoded);
|
|
402
|
+
for (const [key, previous] of Object.entries(state.frames)) {
|
|
403
|
+
if (imports?.producers.has(previous.producerId ?? -1)) continue;
|
|
404
|
+
if (
|
|
405
|
+
!imported[key] ||
|
|
406
|
+
imported[key]!.producerId !== previous.producerId
|
|
407
|
+
)
|
|
408
|
+
releaseVideoFrameImage(previous);
|
|
409
|
+
}
|
|
410
|
+
return imported;
|
|
411
|
+
});
|
|
412
|
+
lap("importMs");
|
|
413
|
+
state.frames = owned;
|
|
414
|
+
await state.processor?.prepareFrame({
|
|
415
|
+
frames: owned,
|
|
416
|
+
currentTime: time,
|
|
417
|
+
videoComposition,
|
|
418
|
+
width: options.width,
|
|
419
|
+
height: options.height,
|
|
420
|
+
isCancelled,
|
|
421
|
+
});
|
|
422
|
+
lap("prepareMs");
|
|
423
|
+
if (isCancelled()) {
|
|
424
|
+
await finish(null, true);
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
const draw = () => {
|
|
428
|
+
"worklet";
|
|
429
|
+
if (state.gpu)
|
|
430
|
+
state.target = acquireEncoderTarget(
|
|
431
|
+
state.encoder!,
|
|
432
|
+
options.width,
|
|
433
|
+
options.height,
|
|
434
|
+
);
|
|
435
|
+
lap("targetMs");
|
|
436
|
+
const target = state.target;
|
|
437
|
+
const surface = target ? target.surface! : state.surface!;
|
|
438
|
+
const canvas = target ? target.canvas! : state.canvas!;
|
|
439
|
+
canvas.drawColor(Skia.Color("#00000000"), BlendMode.Clear);
|
|
440
|
+
const context = beforeDrawFrame?.() as T;
|
|
441
|
+
const drawn = {
|
|
442
|
+
failed: false,
|
|
443
|
+
failure: undefined as unknown,
|
|
444
|
+
drained: false,
|
|
445
|
+
};
|
|
446
|
+
try {
|
|
447
|
+
drawFrame({
|
|
448
|
+
canvas,
|
|
449
|
+
context,
|
|
450
|
+
videoComposition,
|
|
451
|
+
currentTime: time,
|
|
452
|
+
frames: owned,
|
|
453
|
+
width: options.width,
|
|
454
|
+
height: options.height,
|
|
455
|
+
});
|
|
456
|
+
lap("drawMs");
|
|
457
|
+
surface.flush(true);
|
|
458
|
+
drawn.drained = true;
|
|
459
|
+
lap("gpuMs");
|
|
460
|
+
if (target) {
|
|
461
|
+
closeEncoderTarget(target, true);
|
|
462
|
+
state.target = null;
|
|
463
|
+
state.encoder!.encodeFrame(
|
|
464
|
+
{
|
|
465
|
+
kind: "native-buffer",
|
|
466
|
+
nativeBuffer: target.pointer,
|
|
467
|
+
fences: target.fences,
|
|
468
|
+
},
|
|
469
|
+
time,
|
|
470
|
+
);
|
|
471
|
+
} else {
|
|
472
|
+
const readbackCanvas = canvas as unknown as ReadbackCanvas;
|
|
473
|
+
const pixels = readbackCanvas.readPixels(
|
|
474
|
+
0,
|
|
475
|
+
0,
|
|
476
|
+
{
|
|
477
|
+
width: options.width,
|
|
478
|
+
height: options.height,
|
|
479
|
+
colorType: bgra ? ColorType.BGRA_8888 : ColorType.RGBA_8888,
|
|
480
|
+
alphaType: AlphaType.Premul,
|
|
481
|
+
},
|
|
482
|
+
state.readback!,
|
|
483
|
+
options.width * 4,
|
|
484
|
+
);
|
|
485
|
+
if (
|
|
486
|
+
!(pixels instanceof Uint8Array) ||
|
|
487
|
+
pixels.byteLength !== bytes ||
|
|
488
|
+
pixels.byteOffset !== 0 ||
|
|
489
|
+
pixels.buffer !== state.readback!.buffer
|
|
490
|
+
) {
|
|
491
|
+
throw new Error("Video export pixel readback failed");
|
|
492
|
+
}
|
|
493
|
+
// Fallback: consumed synchronously by the bounded native pool.
|
|
494
|
+
const storage =
|
|
495
|
+
pixels.byteOffset === 0 &&
|
|
496
|
+
pixels.buffer.byteLength === pixels.byteLength
|
|
497
|
+
? pixels.buffer
|
|
498
|
+
: pixels.buffer.slice(
|
|
499
|
+
pixels.byteOffset,
|
|
500
|
+
pixels.byteOffset + pixels.byteLength,
|
|
501
|
+
);
|
|
502
|
+
state.encoder!.encodeFrame(
|
|
503
|
+
{
|
|
504
|
+
kind: bgra ? "bgra" : "rgba",
|
|
505
|
+
data: storage,
|
|
506
|
+
width: options.width,
|
|
507
|
+
height: options.height,
|
|
508
|
+
bytesPerRow: options.width * 4,
|
|
509
|
+
},
|
|
510
|
+
time,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
lap("encodeMs");
|
|
514
|
+
} catch (error) {
|
|
515
|
+
drawn.failed = true;
|
|
516
|
+
drawn.failure = error;
|
|
517
|
+
} finally {
|
|
518
|
+
if (afterDrawFrame) {
|
|
519
|
+
// The context may own a texture referenced by a recording
|
|
520
|
+
// left behind by a thrown draw. Submit it before its owner
|
|
521
|
+
// can destroy it; keep the callback/context if draining fails.
|
|
522
|
+
if (!drawn.drained) {
|
|
523
|
+
try {
|
|
524
|
+
surface.flush(true);
|
|
525
|
+
drawn.drained = true;
|
|
526
|
+
} catch (error) {
|
|
527
|
+
if (!drawn.failed) drawn.failure = error;
|
|
528
|
+
drawn.failed = true;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const releaseContext = () => {
|
|
532
|
+
"worklet";
|
|
533
|
+
afterDrawFrame(context);
|
|
534
|
+
};
|
|
535
|
+
if (drawn.drained) {
|
|
536
|
+
try {
|
|
537
|
+
releaseContext();
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (!drawn.failed) drawn.failure = error;
|
|
540
|
+
drawn.failed = true;
|
|
541
|
+
}
|
|
542
|
+
} else state.pendingAfterDraw = releaseContext;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (drawn.failed) throw drawn.failure;
|
|
546
|
+
};
|
|
547
|
+
runWithVideoAutoreleasePool(draw);
|
|
548
|
+
state.index++;
|
|
549
|
+
if (onProgress)
|
|
550
|
+
scheduleOnRN(onProgress, {
|
|
551
|
+
framesCompleted: state.index,
|
|
552
|
+
nbFrames,
|
|
553
|
+
});
|
|
554
|
+
lap("drawMs");
|
|
555
|
+
if (isCancelled()) {
|
|
556
|
+
await finish(null, true);
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
if (state.index === nbFrames) {
|
|
560
|
+
runWithVideoAutoreleasePool(() => state.encoder!.finishWriting());
|
|
561
|
+
lap("encodeMs");
|
|
562
|
+
if (profile && onProfile) {
|
|
563
|
+
profile.frames = state.index;
|
|
564
|
+
profile.totalMs =
|
|
565
|
+
profile.decodeMs +
|
|
566
|
+
profile.importMs +
|
|
567
|
+
profile.prepareMs +
|
|
568
|
+
profile.targetMs +
|
|
569
|
+
profile.drawMs +
|
|
570
|
+
profile.gpuMs +
|
|
571
|
+
profile.encodeMs +
|
|
572
|
+
profile.yieldMs;
|
|
573
|
+
// Scheduled before the settlement, so it runs first.
|
|
574
|
+
scheduleOnRN(onProfile, { ...profile });
|
|
575
|
+
}
|
|
576
|
+
await finish(null);
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
return true;
|
|
580
|
+
} catch (error) {
|
|
581
|
+
await finish(error, false, true);
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
const run = async () => {
|
|
586
|
+
"worklet";
|
|
587
|
+
// Yield on this runtime between frames: GPU/ML promises still settle,
|
|
588
|
+
// and a busy RN thread no longer paces the export.
|
|
589
|
+
while (await work())
|
|
590
|
+
await new Promise<void>((resolve) => yieldToRuntime(resolve));
|
|
591
|
+
};
|
|
592
|
+
void run();
|
|
593
|
+
};
|
|
594
|
+
try {
|
|
595
|
+
runOnRuntime(runtime, () => {
|
|
596
|
+
"worklet";
|
|
597
|
+
const storage = globalThis as typeof globalThis & {
|
|
598
|
+
__rnskwgpuExport?: ExportState;
|
|
599
|
+
};
|
|
600
|
+
if (storage.__rnskwgpuExport) {
|
|
601
|
+
scheduleOnRN(
|
|
602
|
+
settle,
|
|
603
|
+
new Error(
|
|
604
|
+
"Previous export cleanup failed; resources remain accounted. Restart the native runtime before exporting again.",
|
|
605
|
+
),
|
|
606
|
+
false,
|
|
607
|
+
);
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const state: ExportState = {
|
|
611
|
+
encoder: null,
|
|
612
|
+
extractor: null,
|
|
613
|
+
surface: null,
|
|
614
|
+
canvas: null,
|
|
615
|
+
readback: null,
|
|
616
|
+
gpu: false,
|
|
617
|
+
target: null,
|
|
618
|
+
imports: null,
|
|
619
|
+
frames: {},
|
|
620
|
+
processor: null,
|
|
621
|
+
pendingAfterDraw: null,
|
|
622
|
+
reservations: [],
|
|
623
|
+
index: 0,
|
|
624
|
+
};
|
|
625
|
+
storage.__rnskwgpuExport = state;
|
|
626
|
+
const setup = async () => {
|
|
627
|
+
"worklet";
|
|
628
|
+
try {
|
|
629
|
+
if (cancelled.getBlocking()) {
|
|
630
|
+
scheduleOnRN(step);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
runWithVideoAutoreleasePool(() => {
|
|
634
|
+
assertExportRecorderCacheSupport();
|
|
635
|
+
const allocateReadback = () => {
|
|
636
|
+
// Render target + transient full-frame CPU readback. Native decoder
|
|
637
|
+
// and encoder reservations are additional entries in the same budget.
|
|
638
|
+
state.reservations.push(
|
|
639
|
+
reserveVideoMemory(bytes, "export render target"),
|
|
640
|
+
);
|
|
641
|
+
state.reservations.push(
|
|
642
|
+
reserveVideoMemory(
|
|
643
|
+
bytes * 2,
|
|
644
|
+
"export reusable readback and transient raster",
|
|
645
|
+
),
|
|
646
|
+
);
|
|
647
|
+
state.readback = new Uint8Array(bytes);
|
|
648
|
+
state.hasRecorder = true;
|
|
649
|
+
state.surface = Skia.Surface.MakeOffscreen(
|
|
650
|
+
options.width,
|
|
651
|
+
options.height,
|
|
652
|
+
);
|
|
653
|
+
if (!state.surface)
|
|
654
|
+
throw new Error("Cannot allocate export surface");
|
|
655
|
+
state.canvas = getVideoCanvas(state.surface);
|
|
656
|
+
};
|
|
657
|
+
if (!gpuExport) allocateReadback();
|
|
658
|
+
state.encoder = RNSkiaVideoModule.createVideoEncoder(
|
|
659
|
+
{
|
|
660
|
+
...options,
|
|
661
|
+
audioBitRate: options.audioBitRate ?? 128000,
|
|
662
|
+
audioSampleRate: options.audioSampleRate ?? 44100,
|
|
663
|
+
audioChannelCount: options.audioChannelCount ?? 2,
|
|
664
|
+
},
|
|
665
|
+
videoComposition,
|
|
666
|
+
);
|
|
667
|
+
state.encoder.prepare();
|
|
668
|
+
if (gpuExport) {
|
|
669
|
+
// Skia draws straight into the encoder's buffers; the probe
|
|
670
|
+
// keeps devices without IOSurface interop on the readback path.
|
|
671
|
+
state.hasRecorder = true;
|
|
672
|
+
const failure = supportsEncoderTargets(state.encoder)
|
|
673
|
+
? probeEncoderTarget(
|
|
674
|
+
state.encoder,
|
|
675
|
+
options.width,
|
|
676
|
+
options.height,
|
|
677
|
+
)
|
|
678
|
+
: new Error("Encoder cannot lend frame buffers");
|
|
679
|
+
state.gpu = failure === null;
|
|
680
|
+
// Decoded frames are sampled in place too: on iOS through the
|
|
681
|
+
// same IOSurface import, on Android once converted from YUV.
|
|
682
|
+
if (state.gpu) state.imports = createExportFrameImports();
|
|
683
|
+
if (!state.gpu) {
|
|
684
|
+
console.warn(
|
|
685
|
+
"[react-native-skia-webgpu-video] GPU export unavailable, using CPU readback:",
|
|
686
|
+
String(failure),
|
|
687
|
+
);
|
|
688
|
+
allocateReadback();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
state.extractor =
|
|
692
|
+
RNSkiaVideoModule.createVideoCompositionFramesExtractorSync(
|
|
693
|
+
videoComposition,
|
|
694
|
+
);
|
|
695
|
+
state.extractor.start();
|
|
696
|
+
});
|
|
697
|
+
state.processor =
|
|
698
|
+
(await createFrameProcessor?.({
|
|
699
|
+
width: options.width,
|
|
700
|
+
height: options.height,
|
|
701
|
+
videoComposition,
|
|
702
|
+
isCancelled: () => {
|
|
703
|
+
"worklet";
|
|
704
|
+
return cancelled.getBlocking();
|
|
705
|
+
},
|
|
706
|
+
})) ?? null;
|
|
707
|
+
} catch (error) {
|
|
708
|
+
// Reuse the ordinary cleanup step, retaining the original error.
|
|
709
|
+
state.setupError = error;
|
|
710
|
+
}
|
|
711
|
+
scheduleOnRN(step);
|
|
712
|
+
};
|
|
713
|
+
void setup();
|
|
714
|
+
})();
|
|
715
|
+
} catch (error) {
|
|
716
|
+
settle(error, cancelled.getBlocking(), true);
|
|
717
|
+
}
|
|
718
|
+
});
|