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,684 @@
|
|
|
1
|
+
import { getVideoCanvas } from "./canvas";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
import { AlphaType, BlendMode, ColorType, Skia } from "react-native-skia";
|
|
4
|
+
import type { SkCanvas, SkImage, SkSurface } from "react-native-skia";
|
|
5
|
+
import type { VideoFrame } from "./types";
|
|
6
|
+
import type { NativeVideoFrame } from "react-native-webgpu";
|
|
7
|
+
import {
|
|
8
|
+
getVideoGpuDevice,
|
|
9
|
+
createNativeVideoGpuFrame,
|
|
10
|
+
nativeVideoTextureUsage,
|
|
11
|
+
rgbaVideoTextureUsage,
|
|
12
|
+
} from "./gpuDevice";
|
|
13
|
+
import {
|
|
14
|
+
frameByteSize,
|
|
15
|
+
releaseVideoMemory,
|
|
16
|
+
reserveVideoMemory,
|
|
17
|
+
} from "./memory";
|
|
18
|
+
import { blitYuvFrame, releaseYuvImports, yuvFrameOrigin } from "./yuvBlit";
|
|
19
|
+
import type { YuvImport } from "./yuvBlit";
|
|
20
|
+
|
|
21
|
+
type FrameTexture =
|
|
22
|
+
| {
|
|
23
|
+
kind: "native-buffer";
|
|
24
|
+
nativeBuffer: bigint;
|
|
25
|
+
/** Android ImageReader frames: the codec's YUV buffer, whose picture is
|
|
26
|
+
* this crop rectangle turned clockwise by `rotation` for display. */
|
|
27
|
+
region?: { x: number; y: number; width: number; height: number };
|
|
28
|
+
rotation?: number;
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
kind: "rgba";
|
|
32
|
+
data: ArrayBuffer;
|
|
33
|
+
bytesPerRow: number;
|
|
34
|
+
copyPixelsTo?: (destination: ArrayBuffer) => void;
|
|
35
|
+
}
|
|
36
|
+
| { kind: "skia-image"; image: SkImage };
|
|
37
|
+
type Entry = {
|
|
38
|
+
id: number;
|
|
39
|
+
image: SkImage | null;
|
|
40
|
+
reservation: number;
|
|
41
|
+
releasedSources: WeakSet<VideoFrame>;
|
|
42
|
+
};
|
|
43
|
+
type PendingSource = {
|
|
44
|
+
owned: SkImage | null;
|
|
45
|
+
reservation: number;
|
|
46
|
+
frame: VideoFrame;
|
|
47
|
+
needsDrain: boolean;
|
|
48
|
+
sourceReleased: boolean;
|
|
49
|
+
nativeSource?: NativeVideoFrame | null;
|
|
50
|
+
};
|
|
51
|
+
type Cache = {
|
|
52
|
+
images: Map<number, Entry>;
|
|
53
|
+
stage: SkSurface | null;
|
|
54
|
+
canvas: SkCanvas | null;
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
format: GPUTextureFormat | null;
|
|
58
|
+
uploadTexture: GPUTexture | null;
|
|
59
|
+
uploadReservation: number;
|
|
60
|
+
cpuPixels: Uint8Array<ArrayBuffer> | null;
|
|
61
|
+
cpuReservation: number;
|
|
62
|
+
cpuBytesPerRow: number;
|
|
63
|
+
pendingSources: PendingSource[];
|
|
64
|
+
/** Android decoder buffers imported once, per producer. */
|
|
65
|
+
yuvImports: Map<number, YuvImport[]>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Capture a scalar on the RN runtime instead of importing Platform inside a
|
|
69
|
+
// worklet. iOS keeps its existing native-buffer and RGBA compatibility paths.
|
|
70
|
+
const uploadAndroidRgba = Platform.OS === "android";
|
|
71
|
+
|
|
72
|
+
const getCache = (): Cache => {
|
|
73
|
+
"worklet";
|
|
74
|
+
const runtime = globalThis as typeof globalThis & {
|
|
75
|
+
__rnskwgpuFrames?: Cache;
|
|
76
|
+
};
|
|
77
|
+
if (!runtime.__rnskwgpuFrames) {
|
|
78
|
+
runtime.__rnskwgpuFrames = {
|
|
79
|
+
images: new Map(),
|
|
80
|
+
stage: null,
|
|
81
|
+
canvas: null,
|
|
82
|
+
width: 0,
|
|
83
|
+
height: 0,
|
|
84
|
+
format: null,
|
|
85
|
+
uploadTexture: null,
|
|
86
|
+
uploadReservation: 0,
|
|
87
|
+
cpuPixels: null,
|
|
88
|
+
cpuReservation: 0,
|
|
89
|
+
cpuBytesPerRow: 0,
|
|
90
|
+
pendingSources: [],
|
|
91
|
+
yuvImports: new Map(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return runtime.__rnskwgpuFrames;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const clearEntry = (cache: Cache, producer: number) => {
|
|
98
|
+
"worklet";
|
|
99
|
+
const entry = cache.images.get(producer);
|
|
100
|
+
if (entry) {
|
|
101
|
+
entry.image?.dispose();
|
|
102
|
+
entry.image = null;
|
|
103
|
+
releaseVideoMemory(entry.reservation);
|
|
104
|
+
cache.images.delete(producer);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Release only the current Skia view, never the shared texture it wraps.
|
|
109
|
+
* The caller has either not submitted a copy or has completed its GPU fence. */
|
|
110
|
+
const clearSurfaceView = (cache: Cache) => {
|
|
111
|
+
"worklet";
|
|
112
|
+
cache.canvas?.dispose();
|
|
113
|
+
cache.canvas = null;
|
|
114
|
+
cache.stage?.dispose();
|
|
115
|
+
cache.stage = null;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const clearCpuPixels = (cache: Cache) => {
|
|
119
|
+
"worklet";
|
|
120
|
+
cache.cpuPixels = null;
|
|
121
|
+
releaseVideoMemory(cache.cpuReservation);
|
|
122
|
+
cache.cpuReservation = 0;
|
|
123
|
+
cache.cpuBytesPerRow = 0;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const clearStagingSurface = (cache: Cache) => {
|
|
127
|
+
"worklet";
|
|
128
|
+
cache.stage?.flush(true);
|
|
129
|
+
clearSurfaceView(cache);
|
|
130
|
+
cache.uploadTexture?.destroy();
|
|
131
|
+
cache.uploadTexture = null;
|
|
132
|
+
releaseVideoMemory(cache.uploadReservation);
|
|
133
|
+
cache.uploadReservation = 0;
|
|
134
|
+
clearCpuPixels(cache);
|
|
135
|
+
cache.width = cache.height = 0;
|
|
136
|
+
cache.format = null;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** Record explicit recovery work after a possibly submitted external copy.
|
|
140
|
+
* Clearing scratch cannot modify an already owned snapshot. The checked fence
|
|
141
|
+
* must succeed before either native lease or the texture can be released. */
|
|
142
|
+
const drainNativeCopies = (cache: Cache): void => {
|
|
143
|
+
"worklet";
|
|
144
|
+
if (!cache.stage || !cache.canvas)
|
|
145
|
+
throw new Error("Video import drain surface is unavailable");
|
|
146
|
+
cache.canvas.drawColor(Skia.Color("#00000000"), BlendMode.Clear);
|
|
147
|
+
cache.stage.flush(true);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** Clear a runtime's imported images after ending all its producers.
|
|
151
|
+
* Do not clear a paused producer's current frame: its native source has
|
|
152
|
+
* already been released, and the owned snapshot is its remaining pixels. */
|
|
153
|
+
export const clearVideoFrameImages = (releaseStagingSurface = false): void => {
|
|
154
|
+
"worklet";
|
|
155
|
+
const cache = getCache();
|
|
156
|
+
if (cache.pendingSources.length) {
|
|
157
|
+
// A failed drain retains leases; only a successful fence can release them.
|
|
158
|
+
if (cache.pendingSources.some((source) => source.needsDrain)) {
|
|
159
|
+
drainNativeCopies(cache);
|
|
160
|
+
for (const source of cache.pendingSources) source.needsDrain = false;
|
|
161
|
+
}
|
|
162
|
+
while (cache.pendingSources.length) {
|
|
163
|
+
const source = cache.pendingSources[0]!;
|
|
164
|
+
source.owned?.dispose();
|
|
165
|
+
source.owned = null;
|
|
166
|
+
source.nativeSource?.release();
|
|
167
|
+
source.nativeSource = null;
|
|
168
|
+
if (!source.sourceReleased) {
|
|
169
|
+
source.frame.dispose?.();
|
|
170
|
+
source.sourceReleased = true;
|
|
171
|
+
}
|
|
172
|
+
releaseVideoMemory(source.reservation);
|
|
173
|
+
cache.pendingSources.shift();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
clearSurfaceView(cache);
|
|
177
|
+
for (const producer of cache.images.keys()) clearEntry(cache, producer);
|
|
178
|
+
if (releaseStagingSurface) {
|
|
179
|
+
clearStagingSurface(cache);
|
|
180
|
+
// Every import's last frame completed: the queue drained above.
|
|
181
|
+
releaseYuvImports(cache.yuvImports);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/** Release an item which has left a composition or a producer being closed. */
|
|
186
|
+
export const releaseVideoFrameImage = (frame: VideoFrame): void => {
|
|
187
|
+
"worklet";
|
|
188
|
+
// Synthetic processor images without decoder identities belong to their
|
|
189
|
+
// GPU scope. They must not evict the fallback native producer cache.
|
|
190
|
+
if (
|
|
191
|
+
frame.producerId === undefined &&
|
|
192
|
+
frame.id === undefined &&
|
|
193
|
+
(frame.texture as FrameTexture | null)?.kind === "skia-image"
|
|
194
|
+
)
|
|
195
|
+
return;
|
|
196
|
+
const producer = frame.producerId ?? frame.id ?? -1;
|
|
197
|
+
const cache = getCache();
|
|
198
|
+
const entry = cache.images.get(producer);
|
|
199
|
+
if (entry && entry.id === (frame.id ?? -1)) clearEntry(cache, producer);
|
|
200
|
+
// Each import ends with a checked flush; after a failed one, only the
|
|
201
|
+
// drain in clearVideoFrameImages may release the decoder buffers.
|
|
202
|
+
if (!cache.pendingSources.length)
|
|
203
|
+
releaseYuvImports(cache.yuvImports, producer);
|
|
204
|
+
if (cache.images.size === 0) clearVideoFrameImages(true);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** Returns immutable, owned pixels. WebGPU uploads Android RGBA pixels or blits
|
|
208
|
+
* native buffers into one reusable texture, then snapshots a fresh Skia view.
|
|
209
|
+
* Graphite-backed snapshots bypass the raster ImageProvider's image-count LRU.
|
|
210
|
+
* Wait for completion before releasing sources or rewriting the scratch texture. */
|
|
211
|
+
export const imageFromVideoFrame = (frame: VideoFrame): SkImage | null => {
|
|
212
|
+
"worklet";
|
|
213
|
+
const cache = getCache();
|
|
214
|
+
const producer = frame.producerId ?? frame.id;
|
|
215
|
+
if (producer !== undefined && frame.id !== undefined) {
|
|
216
|
+
const existing = cache.images.get(producer);
|
|
217
|
+
if (existing?.id === frame.id && existing.image) {
|
|
218
|
+
// Android can issue a fresh lease for the same paused picture. The
|
|
219
|
+
// snapshot already owns its pixels, so close that unused native lease.
|
|
220
|
+
// Weak tracking avoids retaining raw buffers or closing one wrapper twice.
|
|
221
|
+
if (!existing.releasedSources.has(frame)) {
|
|
222
|
+
frame.dispose?.();
|
|
223
|
+
existing.releasedSources.add(frame);
|
|
224
|
+
}
|
|
225
|
+
return existing.image;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
let reservation = 0;
|
|
229
|
+
let image: SkImage | null = null;
|
|
230
|
+
let copyPending = false;
|
|
231
|
+
let ownsSurfaceView = false;
|
|
232
|
+
let nativeSource: NativeVideoFrame | null = null;
|
|
233
|
+
let sourceDrained = false;
|
|
234
|
+
let releaseSource = true;
|
|
235
|
+
let importFailed = false;
|
|
236
|
+
try {
|
|
237
|
+
if (cache.pendingSources.length) {
|
|
238
|
+
// A caller may retry the same native frame (or an alias of its stable
|
|
239
|
+
// identity). Its existing pending entry still owns both leases; do not
|
|
240
|
+
// let this rejected re-entry revoke the original decoder reservation.
|
|
241
|
+
if (
|
|
242
|
+
cache.pendingSources.some(
|
|
243
|
+
(source) =>
|
|
244
|
+
source.frame === frame ||
|
|
245
|
+
(frame.id !== undefined &&
|
|
246
|
+
source.frame.id === frame.id &&
|
|
247
|
+
source.frame.producerId === frame.producerId),
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
releaseSource = false;
|
|
251
|
+
throw new Error(
|
|
252
|
+
"Previous video import could not drain its GPU work; restart the native runtime",
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
const texture = frame.texture as FrameTexture | null | undefined;
|
|
256
|
+
if (!texture) return null;
|
|
257
|
+
if (texture.kind === "skia-image") return texture.image;
|
|
258
|
+
if (texture.kind !== "rgba" && texture.kind !== "native-buffer") {
|
|
259
|
+
throw new Error(
|
|
260
|
+
"Unsupported frame transport; use the Skia 3 video backend",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
const bytes = frameByteSize(frame.width, frame.height);
|
|
264
|
+
// Keep the published image valid until its replacement has succeeded.
|
|
265
|
+
// Account for the replacement's peak memory rather than evict paused pixels.
|
|
266
|
+
const key = producer ?? -1;
|
|
267
|
+
if (!cache.images.has(key) && cache.images.size >= 32) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
"Too many cached video producers; close unused extractors",
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
let rgbaPixels: Uint8Array<ArrayBuffer> | null = null;
|
|
273
|
+
let requiredRgbaBytes = 0;
|
|
274
|
+
let copyRgbaPixels = false;
|
|
275
|
+
if (texture.kind === "rgba") {
|
|
276
|
+
const required = texture.bytesPerRow * frame.height;
|
|
277
|
+
if (
|
|
278
|
+
!Number.isSafeInteger(texture.bytesPerRow) ||
|
|
279
|
+
!Number.isSafeInteger(required) ||
|
|
280
|
+
texture.bytesPerRow < frame.width * 4 ||
|
|
281
|
+
(uploadAndroidRgba &&
|
|
282
|
+
(texture.bytesPerRow > 0xffffffff || texture.bytesPerRow % 4 !== 0))
|
|
283
|
+
) {
|
|
284
|
+
throw new Error("Invalid RGBA video frame storage");
|
|
285
|
+
}
|
|
286
|
+
requiredRgbaBytes = required;
|
|
287
|
+
// Native Android data is lazy. Inspect the optional copy protocol before
|
|
288
|
+
// reading that getter, which would otherwise retain a Java backing per
|
|
289
|
+
// external ArrayBuffer until Hermes and ART have both collected it.
|
|
290
|
+
copyRgbaPixels =
|
|
291
|
+
uploadAndroidRgba && typeof texture.copyPixelsTo === "function";
|
|
292
|
+
if (!copyRgbaPixels) {
|
|
293
|
+
const pixels = new Uint8Array(texture.data);
|
|
294
|
+
if (pixels.byteLength < required)
|
|
295
|
+
throw new Error("Invalid RGBA video frame storage");
|
|
296
|
+
if (uploadAndroidRgba) {
|
|
297
|
+
// Exclude unused trailing storage without another pixel copy.
|
|
298
|
+
rgbaPixels = new Uint8Array(texture.data, 0, required);
|
|
299
|
+
} else {
|
|
300
|
+
// Preserve the iOS compatibility raster path.
|
|
301
|
+
reservation = reserveVideoMemory(
|
|
302
|
+
pixels.byteLength,
|
|
303
|
+
"owned video image",
|
|
304
|
+
);
|
|
305
|
+
const data = Skia.Data.fromBytes(pixels);
|
|
306
|
+
try {
|
|
307
|
+
image = Skia.Image.MakeRasterImage(
|
|
308
|
+
{
|
|
309
|
+
width: frame.width,
|
|
310
|
+
height: frame.height,
|
|
311
|
+
colorType: ColorType.RGBA_8888,
|
|
312
|
+
alphaType: AlphaType.Opaque,
|
|
313
|
+
},
|
|
314
|
+
data,
|
|
315
|
+
texture.bytesPerRow,
|
|
316
|
+
);
|
|
317
|
+
} finally {
|
|
318
|
+
data.dispose();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (texture.kind === "native-buffer" || rgbaPixels || copyRgbaPixels) {
|
|
324
|
+
const device = getVideoGpuDevice();
|
|
325
|
+
const uploadRgba = texture.kind === "rgba";
|
|
326
|
+
if (uploadRgba || uploadAndroidRgba) {
|
|
327
|
+
const extentLimit = device.limits.maxTextureDimension2D;
|
|
328
|
+
if (
|
|
329
|
+
!Number.isSafeInteger(extentLimit) ||
|
|
330
|
+
extentLimit <= 0 ||
|
|
331
|
+
frame.width > extentLimit ||
|
|
332
|
+
frame.height > extentLimit
|
|
333
|
+
)
|
|
334
|
+
throw new Error(
|
|
335
|
+
"Video frame exceeds the WebGPU texture extent limit",
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
const format =
|
|
339
|
+
uploadRgba || uploadAndroidRgba ? "rgba8unorm" : "bgra8unorm";
|
|
340
|
+
reservation = reserveVideoMemory(bytes, "owned video image");
|
|
341
|
+
if (
|
|
342
|
+
!cache.uploadTexture ||
|
|
343
|
+
cache.width !== frame.width ||
|
|
344
|
+
cache.height !== frame.height ||
|
|
345
|
+
cache.format !== format
|
|
346
|
+
) {
|
|
347
|
+
clearStagingSurface(cache);
|
|
348
|
+
cache.uploadReservation = reserveVideoMemory(
|
|
349
|
+
bytes,
|
|
350
|
+
uploadRgba
|
|
351
|
+
? "RGBA frame WebGPU upload texture"
|
|
352
|
+
: "native frame WebGPU upload texture",
|
|
353
|
+
);
|
|
354
|
+
try {
|
|
355
|
+
// One scratch texture per runtime. COPY_SRC lets Graphite snapshot
|
|
356
|
+
// it with a GPU blit rather than draw into another staging target.
|
|
357
|
+
cache.uploadTexture = device.createTexture({
|
|
358
|
+
size: { width: frame.width, height: frame.height },
|
|
359
|
+
format,
|
|
360
|
+
// Android AHB and CPU frames share the RGBA scratch. Include
|
|
361
|
+
// COPY_DST even for a first AHB frame, so a later CPU fallback
|
|
362
|
+
// cannot reuse a texture that forbids writeTexture.
|
|
363
|
+
usage:
|
|
364
|
+
uploadRgba || uploadAndroidRgba
|
|
365
|
+
? rgbaVideoTextureUsage
|
|
366
|
+
: nativeVideoTextureUsage,
|
|
367
|
+
label: uploadRgba
|
|
368
|
+
? "video RGBA frame upload"
|
|
369
|
+
: "video native frame upload",
|
|
370
|
+
});
|
|
371
|
+
cache.width = frame.width;
|
|
372
|
+
cache.height = frame.height;
|
|
373
|
+
cache.format = format;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
try {
|
|
376
|
+
clearStagingSurface(cache);
|
|
377
|
+
} catch {
|
|
378
|
+
/* unfinished cleanup/accounting stays reachable */
|
|
379
|
+
}
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (copyRgbaPixels && texture.kind === "rgba") {
|
|
384
|
+
if (
|
|
385
|
+
!cache.cpuPixels ||
|
|
386
|
+
cache.cpuPixels.byteLength !== requiredRgbaBytes ||
|
|
387
|
+
cache.cpuBytesPerRow !== texture.bytesPerRow
|
|
388
|
+
) {
|
|
389
|
+
clearCpuPixels(cache);
|
|
390
|
+
cache.cpuReservation = reserveVideoMemory(
|
|
391
|
+
requiredRgbaBytes,
|
|
392
|
+
"RGBA frame CPU upload buffer",
|
|
393
|
+
);
|
|
394
|
+
try {
|
|
395
|
+
cache.cpuPixels = new Uint8Array(
|
|
396
|
+
new ArrayBuffer(requiredRgbaBytes),
|
|
397
|
+
);
|
|
398
|
+
cache.cpuBytesPerRow = texture.bytesPerRow;
|
|
399
|
+
} catch (error) {
|
|
400
|
+
try {
|
|
401
|
+
clearCpuPixels(cache);
|
|
402
|
+
} catch {
|
|
403
|
+
/* unfinished accounting stays reachable */
|
|
404
|
+
}
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// The host function finishes memcpy synchronously. The private buffer
|
|
409
|
+
// is never exposed as texture.data and is rewritten only after the
|
|
410
|
+
// preceding import's checked fence. Saved raw aliases remain separate.
|
|
411
|
+
texture.copyPixelsTo!(cache.cpuPixels.buffer);
|
|
412
|
+
rgbaPixels = cache.cpuPixels;
|
|
413
|
+
} else {
|
|
414
|
+
// Native AHB frames and raw compatibility frames need no private CPU
|
|
415
|
+
// scratch. This also closes CPU scratch on same-format RGBA -> AHB.
|
|
416
|
+
clearCpuPixels(cache);
|
|
417
|
+
}
|
|
418
|
+
clearSurfaceView(cache);
|
|
419
|
+
// WebGPU writes do not invalidate SkSurface's cached snapshot. A fresh
|
|
420
|
+
// surface wrapper for every frame prevents stale pixels without another
|
|
421
|
+
// GPU allocation or a write through Skia that would overwrite the input.
|
|
422
|
+
cache.stage = Skia.Surface.MakeFromGPUTexture(cache.uploadTexture!);
|
|
423
|
+
if (!cache.stage) throw new Error("Cannot wrap video upload texture");
|
|
424
|
+
ownsSurfaceView = true;
|
|
425
|
+
cache.canvas = getVideoCanvas(cache.stage);
|
|
426
|
+
if (rgbaPixels && texture.kind === "rgba") {
|
|
427
|
+
// Dawn consumes this typed-array view synchronously. Keep the source
|
|
428
|
+
// alive through the checked fence, including a throw after enqueue.
|
|
429
|
+
copyPending = true;
|
|
430
|
+
device.queue.writeTexture(
|
|
431
|
+
{ texture: cache.uploadTexture! },
|
|
432
|
+
rgbaPixels,
|
|
433
|
+
{
|
|
434
|
+
offset: 0,
|
|
435
|
+
bytesPerRow: texture.bytesPerRow,
|
|
436
|
+
rowsPerImage: frame.height,
|
|
437
|
+
},
|
|
438
|
+
{ width: frame.width, height: frame.height, depthOrArrayLayers: 1 },
|
|
439
|
+
);
|
|
440
|
+
} else if (texture.kind === "native-buffer") {
|
|
441
|
+
nativeSource = createNativeVideoGpuFrame(texture.nativeBuffer);
|
|
442
|
+
const region = texture.region;
|
|
443
|
+
const rotation = texture.rotation ?? 0;
|
|
444
|
+
let origin = { x: 0, y: 0 };
|
|
445
|
+
if (region) {
|
|
446
|
+
// An Android codec buffer: padded, YUV, in encoded orientation. The
|
|
447
|
+
// blit converts, crops and turns it into the display-sized scratch,
|
|
448
|
+
// as the GL path's SurfaceTexture transform did. RN WebGPU labels
|
|
449
|
+
// driver-defined YUV formats "bgra8", yet Dawn imports them as YUV:
|
|
450
|
+
// the native side checked the format, so the label is not read.
|
|
451
|
+
origin = yuvFrameOrigin(region, rotation, nativeSource, frame);
|
|
452
|
+
} else if (
|
|
453
|
+
// RN WebGPU labels a single-plane Android RGBA AHB "bgra8". Dawn
|
|
454
|
+
// reads the actual AHB format; the string distinguishes RGB from
|
|
455
|
+
// NV12 and must not be read as Android's physical byte order.
|
|
456
|
+
nativeSource.width !== frame.width ||
|
|
457
|
+
nativeSource.height !== frame.height ||
|
|
458
|
+
nativeSource.pixelFormat !== "bgra8"
|
|
459
|
+
) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
"Native video buffer dimensions or BGRA layout mismatch",
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
// The native call can throw after submitting; retain leases until a
|
|
465
|
+
// checked queue drain even when it does not return normally.
|
|
466
|
+
copyPending = true;
|
|
467
|
+
if (region) {
|
|
468
|
+
// copyExternalImageToTexture would also convert the video's
|
|
469
|
+
// transfer function and primaries to sRGB (see yuvBlit.ts).
|
|
470
|
+
blitYuvFrame(
|
|
471
|
+
device,
|
|
472
|
+
cache.yuvImports,
|
|
473
|
+
key,
|
|
474
|
+
nativeSource,
|
|
475
|
+
rotation,
|
|
476
|
+
origin,
|
|
477
|
+
frame.width,
|
|
478
|
+
frame.height,
|
|
479
|
+
cache.uploadTexture!,
|
|
480
|
+
);
|
|
481
|
+
} else {
|
|
482
|
+
// Keep decoded orientation: drawVideoFrame applies rotation once.
|
|
483
|
+
device.queue.copyExternalImageToTexture(
|
|
484
|
+
{
|
|
485
|
+
source: nativeSource,
|
|
486
|
+
rotation: 0,
|
|
487
|
+
mirrored: false,
|
|
488
|
+
flipY: false,
|
|
489
|
+
},
|
|
490
|
+
{ texture: cache.uploadTexture!, premultipliedAlpha: false },
|
|
491
|
+
{ width: frame.width, height: frame.height },
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
// Snapshot records an independent texture copy and submits it after
|
|
496
|
+
// the external blit on the shared queue. The wrapper never draws.
|
|
497
|
+
image = cache.stage.makeImageSnapshot();
|
|
498
|
+
cache.stage.flush(true);
|
|
499
|
+
sourceDrained = true;
|
|
500
|
+
}
|
|
501
|
+
if (!image) throw new Error("Cannot create a video frame image");
|
|
502
|
+
// Complete the native handoff before replacing published pixels. A failed
|
|
503
|
+
// source close must not evict a paused image or leave an unpublished cache
|
|
504
|
+
// entry behind when the caller receives an import error.
|
|
505
|
+
if (ownsSurfaceView) {
|
|
506
|
+
clearSurfaceView(cache);
|
|
507
|
+
ownsSurfaceView = false;
|
|
508
|
+
}
|
|
509
|
+
nativeSource?.release();
|
|
510
|
+
nativeSource = null;
|
|
511
|
+
frame.dispose?.();
|
|
512
|
+
releaseSource = false;
|
|
513
|
+
clearEntry(cache, key);
|
|
514
|
+
cache.images.set(key, {
|
|
515
|
+
id: frame.id ?? -1, image, reservation,
|
|
516
|
+
releasedSources: new WeakSet([frame]),
|
|
517
|
+
});
|
|
518
|
+
return image;
|
|
519
|
+
} catch (error) {
|
|
520
|
+
importFailed = true;
|
|
521
|
+
if (copyPending && !sourceDrained) {
|
|
522
|
+
try {
|
|
523
|
+
drainNativeCopies(cache);
|
|
524
|
+
} catch {
|
|
525
|
+
cache.pendingSources.push({
|
|
526
|
+
nativeSource,
|
|
527
|
+
owned: image,
|
|
528
|
+
reservation,
|
|
529
|
+
frame,
|
|
530
|
+
needsDrain: true,
|
|
531
|
+
sourceReleased: false,
|
|
532
|
+
});
|
|
533
|
+
nativeSource = null;
|
|
534
|
+
image = null;
|
|
535
|
+
reservation = 0;
|
|
536
|
+
releaseSource = false;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
// Failed drains retain the view along with its texture and source.
|
|
541
|
+
if (releaseSource && ownsSurfaceView) {
|
|
542
|
+
clearSurfaceView(cache);
|
|
543
|
+
ownsSurfaceView = false;
|
|
544
|
+
}
|
|
545
|
+
image?.dispose();
|
|
546
|
+
image = null;
|
|
547
|
+
releaseVideoMemory(reservation);
|
|
548
|
+
} catch {
|
|
549
|
+
cache.pendingSources.push({
|
|
550
|
+
nativeSource,
|
|
551
|
+
owned: image,
|
|
552
|
+
reservation,
|
|
553
|
+
frame,
|
|
554
|
+
needsDrain: false,
|
|
555
|
+
sourceReleased: false,
|
|
556
|
+
});
|
|
557
|
+
nativeSource = null;
|
|
558
|
+
releaseSource = false;
|
|
559
|
+
}
|
|
560
|
+
throw error;
|
|
561
|
+
} finally {
|
|
562
|
+
if (releaseSource) {
|
|
563
|
+
try {
|
|
564
|
+
nativeSource?.release();
|
|
565
|
+
nativeSource = null;
|
|
566
|
+
frame.dispose?.();
|
|
567
|
+
} catch (error) {
|
|
568
|
+
cache.pendingSources.push({
|
|
569
|
+
nativeSource,
|
|
570
|
+
owned: null,
|
|
571
|
+
reservation: 0,
|
|
572
|
+
frame,
|
|
573
|
+
needsDrain: false,
|
|
574
|
+
sourceReleased: false,
|
|
575
|
+
});
|
|
576
|
+
if (!importFailed) throw error;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
/** Stage all issued frames, including items a callback currently hides.
|
|
583
|
+
* This releases decoder leases promptly instead of waiting for JS GC. */
|
|
584
|
+
export const ownVideoFrames = (
|
|
585
|
+
frames: Record<string, VideoFrame>,
|
|
586
|
+
): Record<string, VideoFrame> => {
|
|
587
|
+
"worklet";
|
|
588
|
+
const owned: Record<string, VideoFrame> = {};
|
|
589
|
+
const visited = new Set<VideoFrame>();
|
|
590
|
+
const failed = { value: false, error: undefined as unknown };
|
|
591
|
+
try {
|
|
592
|
+
// Two different pictures from one producer would invalidate the first
|
|
593
|
+
// owned image while importing the second. Reject before allocating, and
|
|
594
|
+
// permit aliases of the same stable native frame.
|
|
595
|
+
const producers = new Map<number, VideoFrame>();
|
|
596
|
+
for (const frame of Object.values(frames)) {
|
|
597
|
+
if ((frame.texture as FrameTexture | null)?.kind === "skia-image")
|
|
598
|
+
continue;
|
|
599
|
+
const producer = frame.producerId ?? frame.id ?? -1;
|
|
600
|
+
const previous = producers.get(producer);
|
|
601
|
+
if (
|
|
602
|
+
previous &&
|
|
603
|
+
previous !== frame &&
|
|
604
|
+
(frame.id === undefined ||
|
|
605
|
+
previous.id === undefined ||
|
|
606
|
+
previous.id !== frame.id)
|
|
607
|
+
) {
|
|
608
|
+
throw new Error(
|
|
609
|
+
"Composition yielded different frames for one producer; stable native frame identities are required",
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
producers.set(producer, frame);
|
|
613
|
+
}
|
|
614
|
+
for (const [key, frame] of Object.entries(frames)) {
|
|
615
|
+
visited.add(frame);
|
|
616
|
+
const image = imageFromVideoFrame(frame);
|
|
617
|
+
if (image)
|
|
618
|
+
owned[key] = {
|
|
619
|
+
width: frame.width,
|
|
620
|
+
height: frame.height,
|
|
621
|
+
rotation: frame.rotation,
|
|
622
|
+
crop: frame.crop,
|
|
623
|
+
// Synthetic processor images belong to their GPU scope. Raw anonymous
|
|
624
|
+
// input uses the fallback cache, which still needs a releasable key.
|
|
625
|
+
id:
|
|
626
|
+
frame.id ??
|
|
627
|
+
((frame.texture as FrameTexture | null)?.kind === "skia-image"
|
|
628
|
+
? undefined
|
|
629
|
+
: -1),
|
|
630
|
+
producerId:
|
|
631
|
+
frame.producerId ??
|
|
632
|
+
frame.id ??
|
|
633
|
+
((frame.texture as FrameTexture | null)?.kind === "skia-image"
|
|
634
|
+
? undefined
|
|
635
|
+
: -1),
|
|
636
|
+
texture: { kind: "skia-image", image },
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
return owned;
|
|
640
|
+
} catch (error) {
|
|
641
|
+
failed.value = true;
|
|
642
|
+
failed.error = error;
|
|
643
|
+
for (const frame of Object.values(owned)) {
|
|
644
|
+
try {
|
|
645
|
+
releaseVideoFrameImage(frame);
|
|
646
|
+
} catch {
|
|
647
|
+
/* retained/accounted by cache */
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
throw error;
|
|
651
|
+
} finally {
|
|
652
|
+
// Idempotent native dispose covers untouched frames after an import fails.
|
|
653
|
+
const pending = getCache().pendingSources;
|
|
654
|
+
for (const frame of Object.values(frames)) {
|
|
655
|
+
if (visited.has(frame)) continue;
|
|
656
|
+
// Do not undo the failed-drain retention established by the importer.
|
|
657
|
+
try {
|
|
658
|
+
if (
|
|
659
|
+
!pending.some(
|
|
660
|
+
(source) =>
|
|
661
|
+
source.frame === frame ||
|
|
662
|
+
(frame.id !== undefined &&
|
|
663
|
+
source.frame.id === frame.id &&
|
|
664
|
+
source.frame.producerId === frame.producerId),
|
|
665
|
+
)
|
|
666
|
+
)
|
|
667
|
+
frame.dispose?.();
|
|
668
|
+
} catch (error) {
|
|
669
|
+
// Keep an unsuccessful native close reachable for retry, but continue
|
|
670
|
+
// releasing every independent untouched item and preserve import error.
|
|
671
|
+
pending.push({
|
|
672
|
+
owned: null,
|
|
673
|
+
reservation: 0,
|
|
674
|
+
frame,
|
|
675
|
+
needsDrain: false,
|
|
676
|
+
sourceReleased: false,
|
|
677
|
+
});
|
|
678
|
+
if (!failed.value) failed.error = error;
|
|
679
|
+
failed.value = true;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (failed.value) throw failed.error;
|
|
683
|
+
}
|
|
684
|
+
};
|