react-native-skia-webgpu-video 0.0.0-stage → 0.1.0-beta.1
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/DisplayVsyncPeriod.java +72 -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 +43 -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 +114 -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 +779 -0
- package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +463 -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 +368 -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 +105 -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 +155 -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 +568 -0
- package/src/videoFrameImage.ts +205 -0
- package/src/videoPlayer.ts +318 -0
- package/src/yuvBlit.ts +331 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import { getVideoCanvas } from "./canvas";
|
|
2
|
+
import type { SkCanvas, SkImage, SkSurface } from "react-native-skia";
|
|
3
|
+
import { BlendMode, Skia } from "react-native-skia";
|
|
4
|
+
import {
|
|
5
|
+
useSharedValue,
|
|
6
|
+
useFrameCallback,
|
|
7
|
+
runOnUI,
|
|
8
|
+
type DerivedValue,
|
|
9
|
+
type SharedValue,
|
|
10
|
+
} from "react-native-reanimated";
|
|
11
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
12
|
+
import type {
|
|
13
|
+
FrameDrawer,
|
|
14
|
+
VideoFrame,
|
|
15
|
+
VideoComposition,
|
|
16
|
+
VideoCompositionFramesExtractor,
|
|
17
|
+
} from "./types";
|
|
18
|
+
import RNSkiaVideoModule from "./RNSkiaVideoModule";
|
|
19
|
+
import useEventListener from "./utils/useEventListener";
|
|
20
|
+
import { PixelRatio } from "react-native";
|
|
21
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
22
|
+
import { ownVideoFrames, releaseVideoFrameImage } from "./frameInterop";
|
|
23
|
+
import { frameByteSize, reserveVideoMemory } from "./memory";
|
|
24
|
+
import { disposePreviewResources } from "./previewLifecycle";
|
|
25
|
+
|
|
26
|
+
type UseVideoCompositionPlayerOptions<T = undefined> = {
|
|
27
|
+
/**
|
|
28
|
+
* The video composition to play.
|
|
29
|
+
* if null, the composition player won't be created.
|
|
30
|
+
*/
|
|
31
|
+
composition: VideoComposition | null;
|
|
32
|
+
/**
|
|
33
|
+
* The function used to draw the composition frames.
|
|
34
|
+
*/
|
|
35
|
+
drawFrame: FrameDrawer<T>;
|
|
36
|
+
/**
|
|
37
|
+
* A function that is called before drawing each frame.
|
|
38
|
+
* the return value will be passed to the drawFrame function as context.
|
|
39
|
+
*/
|
|
40
|
+
beforeDrawFrame?: () => T;
|
|
41
|
+
/**
|
|
42
|
+
* A function that is called after drawing each frame.
|
|
43
|
+
* the context returned by the beforeDrawFrame function will be passed to this function.
|
|
44
|
+
* This function can be used to clean up resources allocated during the drawFrame function.
|
|
45
|
+
*/
|
|
46
|
+
afterDrawFrame?: (context: T) => void;
|
|
47
|
+
/**
|
|
48
|
+
* The width of rendered frames.
|
|
49
|
+
*/
|
|
50
|
+
width: number;
|
|
51
|
+
/**
|
|
52
|
+
* The height of rendered frames.
|
|
53
|
+
*/
|
|
54
|
+
height: number;
|
|
55
|
+
/**
|
|
56
|
+
* Whether the composition should start playing automatically.
|
|
57
|
+
*/
|
|
58
|
+
autoPlay?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Weather the composition should loop.
|
|
61
|
+
*/
|
|
62
|
+
isLooping?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Whether to keep calling `drawFrame` at every vsync while playback is
|
|
65
|
+
* paused. By default a paused player only redraws when the composition time
|
|
66
|
+
* or the decoded frames change (a seek, the frame that follows it), which
|
|
67
|
+
* saves GPU time and battery. Enable it if `drawFrame` depends on values
|
|
68
|
+
* that change while paused, an overlay being dragged for instance.
|
|
69
|
+
*
|
|
70
|
+
* Or the shared values `drawFrame` reads: a paused player then redraws only
|
|
71
|
+
* when one of them is replaced, besides the time, the frames, the size or
|
|
72
|
+
* `drawFrame` itself. An idle editor stops redrawing at every vsync.
|
|
73
|
+
* @default false
|
|
74
|
+
*/
|
|
75
|
+
drawWhenPaused?: boolean | SharedValue<unknown>[];
|
|
76
|
+
/**
|
|
77
|
+
* Whether `drawFrame` can draw something different at `currentTime` than it
|
|
78
|
+
* drew at `previousTime` from the same decoded frames. By default every time
|
|
79
|
+
* change redraws, so a playing player draws at every vsync even though the
|
|
80
|
+
* video only changes at its own frame rate. A drawer whose picture depends on
|
|
81
|
+
* time only through the frames (or only at known moments, a transition say)
|
|
82
|
+
* returns false elsewhere, and the player then redraws when a new frame is
|
|
83
|
+
* decoded instead. Must be a worklet.
|
|
84
|
+
*/
|
|
85
|
+
redrawOnTime?: (previousTime: number, currentTime: number) => boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Import a newly decoded frame in one vsync and draw it in the next, instead
|
|
88
|
+
* of both in the same one. On Android the import (YUV blit and its checked
|
|
89
|
+
* GPU wait) and the draw (snapshot and its checked GPU wait) cost about as
|
|
90
|
+
* much as each other, and together they overrun a 90–120 Hz frame budget on
|
|
91
|
+
* the UI thread. The picture then trails the clock by one vsync.
|
|
92
|
+
* The first frame of a session is still drawn as soon as it is imported.
|
|
93
|
+
* @default false
|
|
94
|
+
*/
|
|
95
|
+
splitImportAndDraw?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Callback that is called when the composition is ready to play.
|
|
98
|
+
*/
|
|
99
|
+
onReadyToPlay?: () => void;
|
|
100
|
+
/**
|
|
101
|
+
* Callback that is called when the composition playback completes.
|
|
102
|
+
*/
|
|
103
|
+
onComplete?: () => void;
|
|
104
|
+
/**
|
|
105
|
+
* Callback that is called when an error occurs.
|
|
106
|
+
* @param error the error that occurred.
|
|
107
|
+
* @param retry a function that can be called to retry the operation.
|
|
108
|
+
*/
|
|
109
|
+
onError?: (error: any, retry: () => void) => void;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
type VideoCompositionPlayerController = Pick<
|
|
113
|
+
VideoCompositionFramesExtractor,
|
|
114
|
+
"currentTime" | "play" | "pause" | "seekTo" | "isPlaying"
|
|
115
|
+
>;
|
|
116
|
+
|
|
117
|
+
type UseVideoCompositionPlayerReturnType = {
|
|
118
|
+
/**
|
|
119
|
+
* The current drawn frame of the video composition.
|
|
120
|
+
*/
|
|
121
|
+
currentFrame: DerivedValue<SkImage | null>;
|
|
122
|
+
/**
|
|
123
|
+
* The video player controller.
|
|
124
|
+
*/
|
|
125
|
+
player: VideoCompositionPlayerController | null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A hook that creates a video composition player.
|
|
130
|
+
*/
|
|
131
|
+
export const useVideoComposition = <T = undefined>({
|
|
132
|
+
composition,
|
|
133
|
+
drawFrame,
|
|
134
|
+
beforeDrawFrame,
|
|
135
|
+
afterDrawFrame,
|
|
136
|
+
width,
|
|
137
|
+
height,
|
|
138
|
+
autoPlay = false,
|
|
139
|
+
isLooping = false,
|
|
140
|
+
drawWhenPaused = false,
|
|
141
|
+
redrawOnTime,
|
|
142
|
+
splitImportAndDraw = false,
|
|
143
|
+
onReadyToPlay,
|
|
144
|
+
onComplete,
|
|
145
|
+
onError,
|
|
146
|
+
}: UseVideoCompositionPlayerOptions<T>): UseVideoCompositionPlayerReturnType => {
|
|
147
|
+
const [failedComposition, setFailedComposition] =
|
|
148
|
+
useState<VideoComposition | null>(null);
|
|
149
|
+
const isErrored = composition !== null && failedComposition === composition;
|
|
150
|
+
const [session, setSession] = useState<{
|
|
151
|
+
extractor: VideoCompositionFramesExtractor;
|
|
152
|
+
generation: number;
|
|
153
|
+
composition: VideoComposition;
|
|
154
|
+
} | null>(null);
|
|
155
|
+
const framesExtractor = session?.extractor ?? null;
|
|
156
|
+
const generation = session?.generation ?? 0;
|
|
157
|
+
const nextGeneration = useRef(0);
|
|
158
|
+
const activeOnRN = useRef(0);
|
|
159
|
+
// HostObject JS wrappers are recreated across runtimes; compare numbers.
|
|
160
|
+
const activeGeneration = useSharedValue(0);
|
|
161
|
+
const frameGeneration = useSharedValue(0);
|
|
162
|
+
const sourcesGeneration = useSharedValue(0);
|
|
163
|
+
const currentFrame = useSharedValue<SkImage | null>(null);
|
|
164
|
+
const pendingSnapshot = useSharedValue<SkImage | null>(null);
|
|
165
|
+
// Reanimated interprets a function assigned to `.value` as an animation
|
|
166
|
+
// factory. Keep the teardown callback inside an ordinary value instead.
|
|
167
|
+
const pendingAfterDraw = useSharedValue<{ run: () => void } | null>(null);
|
|
168
|
+
const previousSources = useSharedValue<Record<string, VideoFrame>>({});
|
|
169
|
+
const onCreationError = useRef(onError);
|
|
170
|
+
onCreationError.current = onError;
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!composition || isErrored) {
|
|
173
|
+
setSession(null);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
let created: VideoCompositionFramesExtractor;
|
|
177
|
+
try {
|
|
178
|
+
created =
|
|
179
|
+
RNSkiaVideoModule.createVideoCompositionFramesExtractor(composition);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
setFailedComposition(composition);
|
|
182
|
+
onCreationError.current?.(error, () => setFailedComposition(null));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const createdGeneration = ++nextGeneration.current;
|
|
186
|
+
activeOnRN.current = createdGeneration;
|
|
187
|
+
activeGeneration.value = createdGeneration;
|
|
188
|
+
setSession({
|
|
189
|
+
extractor: created,
|
|
190
|
+
generation: createdGeneration,
|
|
191
|
+
composition,
|
|
192
|
+
});
|
|
193
|
+
return () => {
|
|
194
|
+
if (activeOnRN.current === createdGeneration) {
|
|
195
|
+
activeOnRN.current = 0;
|
|
196
|
+
activeGeneration.value = 0;
|
|
197
|
+
}
|
|
198
|
+
runOnUI(() => {
|
|
199
|
+
"worklet";
|
|
200
|
+
if (
|
|
201
|
+
frameGeneration.value === createdGeneration ||
|
|
202
|
+
sourcesGeneration.value === createdGeneration
|
|
203
|
+
)
|
|
204
|
+
releasePreview(created);
|
|
205
|
+
else {
|
|
206
|
+
const failure = disposePreviewResources({ native: created });
|
|
207
|
+
if (failure.failed)
|
|
208
|
+
console.warn(
|
|
209
|
+
"Video preview cleanup failed; resources remain accounted until native runtime teardown",
|
|
210
|
+
failure.error,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
})();
|
|
214
|
+
};
|
|
215
|
+
}, [
|
|
216
|
+
composition,
|
|
217
|
+
isErrored,
|
|
218
|
+
activeGeneration,
|
|
219
|
+
currentFrame,
|
|
220
|
+
previousSources,
|
|
221
|
+
frameGeneration,
|
|
222
|
+
sourcesGeneration,
|
|
223
|
+
pendingSnapshot,
|
|
224
|
+
pendingAfterDraw,
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
const retry = useCallback(() => {
|
|
228
|
+
setFailedComposition(null);
|
|
229
|
+
}, []);
|
|
230
|
+
|
|
231
|
+
const errorHandler = useCallback(
|
|
232
|
+
(error: any) => {
|
|
233
|
+
if (generation !== activeOnRN.current) return;
|
|
234
|
+
activeOnRN.current = 0;
|
|
235
|
+
activeGeneration.value = 0;
|
|
236
|
+
setFailedComposition(session!.composition);
|
|
237
|
+
onCreationError.current?.(error, retry);
|
|
238
|
+
},
|
|
239
|
+
[retry, generation, activeGeneration, session],
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
if (framesExtractor) {
|
|
244
|
+
framesExtractor.isLooping = isLooping;
|
|
245
|
+
}
|
|
246
|
+
}, [framesExtractor, isLooping]);
|
|
247
|
+
|
|
248
|
+
const isCurrentSession = useCallback(
|
|
249
|
+
() => generation !== 0 && generation === activeOnRN.current,
|
|
250
|
+
[generation],
|
|
251
|
+
);
|
|
252
|
+
useEventListener(framesExtractor, "ready", onReadyToPlay, isCurrentSession);
|
|
253
|
+
useEventListener(framesExtractor, "complete", onComplete, isCurrentSession);
|
|
254
|
+
useEventListener(framesExtractor, "error", errorHandler, isCurrentSession);
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
if (!framesExtractor) return;
|
|
257
|
+
runOnUI(() => {
|
|
258
|
+
"worklet";
|
|
259
|
+
if (activeGeneration.value !== generation) return;
|
|
260
|
+
try {
|
|
261
|
+
framesExtractor.prepare();
|
|
262
|
+
} catch (error) {
|
|
263
|
+
activeGeneration.value = 0;
|
|
264
|
+
scheduleOnRN(errorHandler, error);
|
|
265
|
+
}
|
|
266
|
+
})();
|
|
267
|
+
}, [framesExtractor, activeGeneration, generation, errorHandler]);
|
|
268
|
+
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
if (autoPlay) {
|
|
271
|
+
framesExtractor?.play();
|
|
272
|
+
}
|
|
273
|
+
}, [framesExtractor, autoPlay]);
|
|
274
|
+
|
|
275
|
+
const surfaceSharedValue = useSharedValue<SkSurface | null>(null);
|
|
276
|
+
const canvasSharedValue = useSharedValue<SkCanvas | null>(null);
|
|
277
|
+
const surfaceReservation = useSharedValue(0);
|
|
278
|
+
const surfaceWidth = useSharedValue(0);
|
|
279
|
+
const surfaceHeight = useSharedValue(0);
|
|
280
|
+
const releasePreview = (native?: { dispose: () => void }) => {
|
|
281
|
+
"worklet";
|
|
282
|
+
const resources = {
|
|
283
|
+
surface: surfaceSharedValue.value,
|
|
284
|
+
canvas: canvasSharedValue.value,
|
|
285
|
+
images: [currentFrame.value, pendingSnapshot.value],
|
|
286
|
+
frames: Object.values(previousSources.value),
|
|
287
|
+
reservation: surfaceReservation.value,
|
|
288
|
+
afterDraw: pendingAfterDraw.value?.run,
|
|
289
|
+
native,
|
|
290
|
+
};
|
|
291
|
+
// Remove consumers before attempting a drain. A thrown drain must never
|
|
292
|
+
// leave teardown callbacks able to recycle these same resources later.
|
|
293
|
+
currentFrame.value = null;
|
|
294
|
+
pendingSnapshot.value = null;
|
|
295
|
+
pendingAfterDraw.value = null;
|
|
296
|
+
frameGeneration.value = 0;
|
|
297
|
+
previousSources.value = {};
|
|
298
|
+
sourcesGeneration.value = 0;
|
|
299
|
+
surfaceSharedValue.value = null;
|
|
300
|
+
canvasSharedValue.value = null;
|
|
301
|
+
surfaceReservation.value = 0;
|
|
302
|
+
surfaceWidth.value = 0;
|
|
303
|
+
surfaceHeight.value = 0;
|
|
304
|
+
const failure = disposePreviewResources(resources);
|
|
305
|
+
if (failure.failed)
|
|
306
|
+
console.warn(
|
|
307
|
+
"Video preview cleanup failed; resources remain accounted until native runtime teardown",
|
|
308
|
+
failure.error,
|
|
309
|
+
);
|
|
310
|
+
};
|
|
311
|
+
// Frames version and composition time of the last drawn image, to skip
|
|
312
|
+
// redrawing an unchanged picture while paused.
|
|
313
|
+
const lastDrawnFramesVersion = useSharedValue(-1);
|
|
314
|
+
const lastDrawnTime = useSharedValue(-1);
|
|
315
|
+
const lastDrawnInputs = useSharedValue<unknown[] | null>(null);
|
|
316
|
+
// Frames version whose new frames the previous vsyncs already imported.
|
|
317
|
+
const lastImportedVersion = useSharedValue(-1);
|
|
318
|
+
// Whether the previous vsync imported and left its draw to this one.
|
|
319
|
+
const drawDeferred = useSharedValue(false);
|
|
320
|
+
// A new drawFrame can draw something else from the same time and frames.
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
lastDrawnTime.value = -1;
|
|
323
|
+
}, [drawFrame, lastDrawnTime]);
|
|
324
|
+
const pixelRatio = PixelRatio.get();
|
|
325
|
+
|
|
326
|
+
// Release the offscreen surface with the hook that made it. Without this a
|
|
327
|
+
// caller that mounts and unmounts the player repeatedly — a player inside a
|
|
328
|
+
// modal, say — leaks a full size texture per cycle. runOnUI because this
|
|
329
|
+
// effect is declared before the frame callback below, and so its cleanup runs
|
|
330
|
+
// before the callback is unregistered.
|
|
331
|
+
useEffect(
|
|
332
|
+
() => () => {
|
|
333
|
+
runOnUI(() => {
|
|
334
|
+
"worklet";
|
|
335
|
+
releasePreview();
|
|
336
|
+
})();
|
|
337
|
+
},
|
|
338
|
+
[
|
|
339
|
+
surfaceSharedValue,
|
|
340
|
+
canvasSharedValue,
|
|
341
|
+
surfaceReservation,
|
|
342
|
+
surfaceWidth,
|
|
343
|
+
surfaceHeight,
|
|
344
|
+
currentFrame,
|
|
345
|
+
pendingSnapshot,
|
|
346
|
+
pendingAfterDraw,
|
|
347
|
+
frameGeneration,
|
|
348
|
+
previousSources,
|
|
349
|
+
sourcesGeneration,
|
|
350
|
+
],
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
useFrameCallback(() => {
|
|
354
|
+
"worklet";
|
|
355
|
+
if (!framesExtractor || activeGeneration.value !== generation) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
const pixelWidth = Math.floor(width * pixelRatio);
|
|
360
|
+
const pixelHeight = Math.floor(height * pixelRatio);
|
|
361
|
+
|
|
362
|
+
// Layout can report zero or NaN before the player is measured. Avoid
|
|
363
|
+
// native surface allocation until the dimensions are valid.
|
|
364
|
+
if (
|
|
365
|
+
!(pixelWidth > 0) ||
|
|
366
|
+
!(pixelHeight > 0) ||
|
|
367
|
+
!isFinite(pixelWidth) ||
|
|
368
|
+
!isFinite(pixelHeight)
|
|
369
|
+
) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Pull the frames decoded since the last vsync first: it is cheap when
|
|
374
|
+
// nothing new arrived, and it is what lets a paused player pick up the
|
|
375
|
+
// frame of a seek.
|
|
376
|
+
const frames = ownVideoFrames(framesExtractor.decodeCompositionFrames());
|
|
377
|
+
for (const [key, previous] of Object.entries(previousSources.value)) {
|
|
378
|
+
if (!frames[key] || frames[key]!.producerId !== previous.producerId)
|
|
379
|
+
releaseVideoFrameImage(previous);
|
|
380
|
+
}
|
|
381
|
+
previousSources.value = frames;
|
|
382
|
+
sourcesGeneration.value = generation;
|
|
383
|
+
const currentTime = framesExtractor.currentTime;
|
|
384
|
+
const framesVersion = framesExtractor.framesVersion;
|
|
385
|
+
if (splitImportAndDraw && framesVersion !== lastImportedVersion.value) {
|
|
386
|
+
// ownVideoFrames above has just imported the new frames: leave the
|
|
387
|
+
// draw to the next vsync, which finds them in the import cache. Never
|
|
388
|
+
// twice running: a frame that lands on the vsync owed a draw would
|
|
389
|
+
// push it again, and the frame before it would never be shown.
|
|
390
|
+
lastImportedVersion.value = framesVersion;
|
|
391
|
+
if (
|
|
392
|
+
!drawDeferred.value &&
|
|
393
|
+
currentFrame.value !== null &&
|
|
394
|
+
frameGeneration.value === generation
|
|
395
|
+
) {
|
|
396
|
+
drawDeferred.value = true;
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
drawDeferred.value = false;
|
|
401
|
+
const inputs = Array.isArray(drawWhenPaused) ? drawWhenPaused : null;
|
|
402
|
+
let inputsSame = true;
|
|
403
|
+
if (inputs) {
|
|
404
|
+
const last = lastDrawnInputs.value;
|
|
405
|
+
if (!last || last.length !== inputs.length) inputsSame = false;
|
|
406
|
+
else {
|
|
407
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
408
|
+
if (inputs[i]!.value !== last[i]) {
|
|
409
|
+
inputsSame = false;
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// A negative last time means a redraw was requested (a new drawFrame).
|
|
416
|
+
const timeSame =
|
|
417
|
+
currentTime === lastDrawnTime.value ||
|
|
418
|
+
(redrawOnTime !== undefined &&
|
|
419
|
+
lastDrawnTime.value >= 0 &&
|
|
420
|
+
!redrawOnTime(lastDrawnTime.value, currentTime));
|
|
421
|
+
if (
|
|
422
|
+
drawWhenPaused !== true &&
|
|
423
|
+
inputsSame &&
|
|
424
|
+
(!framesExtractor.isPlaying || redrawOnTime !== undefined) &&
|
|
425
|
+
currentFrame.value !== null &&
|
|
426
|
+
frameGeneration.value === generation &&
|
|
427
|
+
framesVersion === lastDrawnFramesVersion.value &&
|
|
428
|
+
timeSame &&
|
|
429
|
+
surfaceWidth.value === pixelWidth &&
|
|
430
|
+
surfaceHeight.value === pixelHeight
|
|
431
|
+
) {
|
|
432
|
+
// Nothing new to show: the image on screen is still exact, and
|
|
433
|
+
// redrawing it at every vsync would only burn GPU time and battery.
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let surface: SkSurface | null = surfaceSharedValue.value;
|
|
438
|
+
|
|
439
|
+
if (
|
|
440
|
+
!surface ||
|
|
441
|
+
surfaceWidth.value !== pixelWidth ||
|
|
442
|
+
surfaceHeight.value !== pixelHeight
|
|
443
|
+
) {
|
|
444
|
+
// width and height can change while the player stays mounted — laying the
|
|
445
|
+
// stage out for a different aspect ratio calls the hook with new
|
|
446
|
+
// dimensions and the same surface. Drawing the new size into the old
|
|
447
|
+
// texture and then declaring the result to be the new size stretches the
|
|
448
|
+
// picture by the ratio between them, so the surface is re-keyed on its
|
|
449
|
+
// dimensions the way exportVideoComposition already re-keys its own.
|
|
450
|
+
const retired = {
|
|
451
|
+
images: [currentFrame.value, pendingSnapshot.value],
|
|
452
|
+
surface,
|
|
453
|
+
canvas: canvasSharedValue.value,
|
|
454
|
+
reservation: surfaceReservation.value,
|
|
455
|
+
afterDraw: pendingAfterDraw.value?.run,
|
|
456
|
+
};
|
|
457
|
+
currentFrame.value = null;
|
|
458
|
+
pendingSnapshot.value = null;
|
|
459
|
+
pendingAfterDraw.value = null;
|
|
460
|
+
canvasSharedValue.value = null;
|
|
461
|
+
surfaceSharedValue.value = null;
|
|
462
|
+
surfaceReservation.value = 0;
|
|
463
|
+
const retiredFailure = disposePreviewResources(retired);
|
|
464
|
+
if (retiredFailure.failed) throw retiredFailure.error;
|
|
465
|
+
// Surface + immutable snapshot/COW backing while replacing the preview.
|
|
466
|
+
const reservation = reserveVideoMemory(
|
|
467
|
+
frameByteSize(pixelWidth, pixelHeight) * 2,
|
|
468
|
+
"composition preview",
|
|
469
|
+
);
|
|
470
|
+
surfaceReservation.value = reservation;
|
|
471
|
+
try {
|
|
472
|
+
surface = Skia.Surface.MakeOffscreen(pixelWidth, pixelHeight);
|
|
473
|
+
surfaceSharedValue.value = surface;
|
|
474
|
+
if (!surface) throw new Error("Cannot allocate preview surface");
|
|
475
|
+
canvasSharedValue.value = getVideoCanvas(surface);
|
|
476
|
+
surfaceWidth.value = pixelWidth;
|
|
477
|
+
surfaceHeight.value = pixelHeight;
|
|
478
|
+
} catch (error) {
|
|
479
|
+
// The ordinary session cleanup owns partial allocation state too.
|
|
480
|
+
// Preserve the setup error and keep its reservation reachable.
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (!surface) {
|
|
485
|
+
console.warn("Failed to create surface");
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const canvas = canvasSharedValue.value!;
|
|
490
|
+
// Cleared as the export clears it: a drawFrame that leaves part of the
|
|
491
|
+
// canvas alone showed the previous frames there, and not in the export.
|
|
492
|
+
canvas.drawColor(Skia.Color("#00000000"), BlendMode.Clear);
|
|
493
|
+
const context = beforeDrawFrame?.() as T;
|
|
494
|
+
const drawn = {
|
|
495
|
+
submitted: false,
|
|
496
|
+
failed: false,
|
|
497
|
+
error: undefined as unknown,
|
|
498
|
+
};
|
|
499
|
+
const releaseContext = () => {
|
|
500
|
+
"worklet";
|
|
501
|
+
afterDrawFrame?.(context);
|
|
502
|
+
};
|
|
503
|
+
pendingAfterDraw.value = { run: releaseContext };
|
|
504
|
+
try {
|
|
505
|
+
drawFrame({
|
|
506
|
+
canvas,
|
|
507
|
+
context,
|
|
508
|
+
videoComposition: composition!,
|
|
509
|
+
currentTime,
|
|
510
|
+
frames,
|
|
511
|
+
width: pixelWidth,
|
|
512
|
+
height: pixelHeight,
|
|
513
|
+
});
|
|
514
|
+
const previous = currentFrame.value;
|
|
515
|
+
const next = surface.makeImageSnapshot(
|
|
516
|
+
undefined,
|
|
517
|
+
previous ?? undefined,
|
|
518
|
+
);
|
|
519
|
+
if (next !== previous) pendingSnapshot.value = next;
|
|
520
|
+
surface.flush(true);
|
|
521
|
+
drawn.submitted = true;
|
|
522
|
+
frameGeneration.value = generation;
|
|
523
|
+
if (next === previous) currentFrame.modify(undefined, true);
|
|
524
|
+
else {
|
|
525
|
+
currentFrame.value = next;
|
|
526
|
+
previous?.dispose();
|
|
527
|
+
}
|
|
528
|
+
pendingSnapshot.value = null;
|
|
529
|
+
} catch (error) {
|
|
530
|
+
drawn.failed = true;
|
|
531
|
+
drawn.error = error;
|
|
532
|
+
} finally {
|
|
533
|
+
// A drawer can throw after recording commands that sample resources
|
|
534
|
+
// from context. Submit and wait before the caller releases them.
|
|
535
|
+
if (!drawn.submitted) {
|
|
536
|
+
try {
|
|
537
|
+
surface.flush(true);
|
|
538
|
+
drawn.submitted = true;
|
|
539
|
+
} catch (error) {
|
|
540
|
+
if (!drawn.failed) drawn.error = error;
|
|
541
|
+
drawn.failed = true;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (drawn.submitted) {
|
|
545
|
+
pendingAfterDraw.value = null;
|
|
546
|
+
try {
|
|
547
|
+
releaseContext();
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (!drawn.failed) drawn.error = error;
|
|
550
|
+
drawn.failed = true;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (drawn.failed) throw drawn.error;
|
|
555
|
+
lastDrawnFramesVersion.value = framesVersion;
|
|
556
|
+
lastDrawnTime.value = currentTime;
|
|
557
|
+
if (inputs) lastDrawnInputs.value = inputs.map((input) => input.value);
|
|
558
|
+
} catch (error) {
|
|
559
|
+
activeGeneration.value = 0;
|
|
560
|
+
scheduleOnRN(errorHandler, error);
|
|
561
|
+
}
|
|
562
|
+
}, true);
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
currentFrame,
|
|
566
|
+
player: framesExtractor,
|
|
567
|
+
};
|
|
568
|
+
};
|