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.
Files changed (236) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +303 -2
  3. package/android/BUILD_VALIDATION.md +62 -0
  4. package/android/CMakeLists.txt +99 -0
  5. package/android/build-validation.json +262 -0
  6. package/android/build.gradle +185 -0
  7. package/android/cpp/EncoderTargets.cpp +226 -0
  8. package/android/cpp/EncoderTargets.h +70 -0
  9. package/android/cpp/JNIHelpers.cpp +15 -0
  10. package/android/cpp/JNIHelpers.h +20 -0
  11. package/android/cpp/NativeEventDispatcher.cpp +59 -0
  12. package/android/cpp/NativeEventDispatcher.h +31 -0
  13. package/android/cpp/NativeHardwareBuffer.cpp +397 -0
  14. package/android/cpp/NativeHardwareBuffer.h +34 -0
  15. package/android/cpp/NativeRgbaBuffer.cpp +45 -0
  16. package/android/cpp/NativeRgbaBuffer.h +9 -0
  17. package/android/cpp/ReturnedVideoFrames.h +37 -0
  18. package/android/cpp/RgbaFrameLease.h +53 -0
  19. package/android/cpp/VideoCapabilities.cpp +87 -0
  20. package/android/cpp/VideoCapabilities.h +44 -0
  21. package/android/cpp/VideoComposition.cpp +170 -0
  22. package/android/cpp/VideoComposition.h +36 -0
  23. package/android/cpp/VideoCompositionFramesExtractor.cpp +82 -0
  24. package/android/cpp/VideoCompositionFramesExtractor.h +48 -0
  25. package/android/cpp/VideoCompositionFramesExtractorHostObject.cpp +230 -0
  26. package/android/cpp/VideoCompositionFramesExtractorHostObject.h +42 -0
  27. package/android/cpp/VideoCompositionFramesExtractorSyncHostObject.cpp +133 -0
  28. package/android/cpp/VideoCompositionFramesExtractorSyncHostObject.h +46 -0
  29. package/android/cpp/VideoEncoderHostObject.cpp +318 -0
  30. package/android/cpp/VideoEncoderHostObject.h +76 -0
  31. package/android/cpp/VideoFrame.cpp +293 -0
  32. package/android/cpp/VideoFrame.h +27 -0
  33. package/android/cpp/VideoPlayer.cpp +96 -0
  34. package/android/cpp/VideoPlayer.h +52 -0
  35. package/android/cpp/VideoPlayerHostObject.cpp +225 -0
  36. package/android/cpp/VideoPlayerHostObject.h +43 -0
  37. package/android/cpp/cpp-adapter.cpp +316 -0
  38. package/android/gradle.properties +5 -0
  39. package/android/instrumentation-inventory.json +178 -0
  40. package/android/proguard-rules.pro +4 -0
  41. package/android/src/main/AndroidManifest.xml +2 -0
  42. package/android/src/main/java/com/azzapp/rnskv/AudioCompositionExporter.java +537 -0
  43. package/android/src/main/java/com/azzapp/rnskv/AudioCompositionPlayer.java +321 -0
  44. package/android/src/main/java/com/azzapp/rnskv/BufferLifetime.java +55 -0
  45. package/android/src/main/java/com/azzapp/rnskv/DecodeRequests.java +100 -0
  46. package/android/src/main/java/com/azzapp/rnskv/DecoderWindow.java +55 -0
  47. package/android/src/main/java/com/azzapp/rnskv/EGLResourcesHolder.java +132 -0
  48. package/android/src/main/java/com/azzapp/rnskv/EGLUtils.java +225 -0
  49. package/android/src/main/java/com/azzapp/rnskv/FrameSize.java +50 -0
  50. package/android/src/main/java/com/azzapp/rnskv/GLFrameExtractor.java +510 -0
  51. package/android/src/main/java/com/azzapp/rnskv/JNIHelpers.java +12 -0
  52. package/android/src/main/java/com/azzapp/rnskv/NativeEventDispatcher.java +25 -0
  53. package/android/src/main/java/com/azzapp/rnskv/NativeHardwareBuffer.java +62 -0
  54. package/android/src/main/java/com/azzapp/rnskv/NativeRgbaBuffer.java +35 -0
  55. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoModule.java +45 -0
  56. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoPackage.java +43 -0
  57. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoSpec.java +9 -0
  58. package/android/src/main/java/com/azzapp/rnskv/RgbaBufferAllocator.java +83 -0
  59. package/android/src/main/java/com/azzapp/rnskv/RgbaLayout.java +33 -0
  60. package/android/src/main/java/com/azzapp/rnskv/SurfaceOutputPolicy.java +13 -0
  61. package/android/src/main/java/com/azzapp/rnskv/TextureRenderer.java +150 -0
  62. package/android/src/main/java/com/azzapp/rnskv/TimeHelpers.java +11 -0
  63. package/android/src/main/java/com/azzapp/rnskv/VideoCapabilities.java +354 -0
  64. package/android/src/main/java/com/azzapp/rnskv/VideoComposition.java +147 -0
  65. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +772 -0
  66. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +354 -0
  67. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractorSync.java +241 -0
  68. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionItemDecoder.java +492 -0
  69. package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +639 -0
  70. package/android/src/main/java/com/azzapp/rnskv/VideoFrame.java +74 -0
  71. package/android/src/main/java/com/azzapp/rnskv/VideoPlayer.java +361 -0
  72. package/cpp/RNSVCheckedSizes.h +54 -0
  73. package/cpp/RNSVEventEmitter.cpp +199 -0
  74. package/cpp/RNSVEventEmitter.h +31 -0
  75. package/cpp/RNSVHostObject.cpp +79 -0
  76. package/cpp/RNSVHostObject.h +67 -0
  77. package/cpp/RNSVMemoryBudget.cpp +68 -0
  78. package/cpp/RNSVMemoryBudget.h +100 -0
  79. package/cpp/RNSVRuntimeLifecycleMonitor.cpp +88 -0
  80. package/cpp/RNSVRuntimeLifecycleMonitor.h +33 -0
  81. package/docs/ANDROID_DEVICE_306.md +95 -0
  82. package/docs/ANDROID_EXPORT_CACHE_306.md +48 -0
  83. package/docs/ANDROID_MEMORY_RETENTION_306.md +67 -0
  84. package/docs/BENCHMARKS.md +368 -0
  85. package/docs/IOS_DEVICE_TESTS.md +115 -0
  86. package/docs/IOS_SIMULATOR_TESTS.md +74 -0
  87. package/docs/MIGRATION.md +82 -0
  88. package/docs/NATIVE_MEMORY_INVESTIGATION_306.md +88 -0
  89. package/docs/PERFORMANCE_AB_IPHONE_303.md +196 -0
  90. package/docs/PERFORMANCE_AB_SIMULATOR.md +135 -0
  91. package/docs/PERFORMANCE_AB_SOAK_SIMULATOR.md +84 -0
  92. package/docs/PERFORMANCE_IPHONE_306_OPTIMIZATION.md +76 -0
  93. package/docs/PERFORMANCE_SIMULATOR.md +89 -0
  94. package/docs/PERFORMANCE_SIMULATOR_INITIAL.md +96 -0
  95. package/docs/PERFORMANCE_SOAK_IPHONE_303.md +41 -0
  96. package/docs/UPSTREAM_NOTES.md +188 -0
  97. package/docs/assets/banner.svg +26 -0
  98. package/examples/webgpu-overlay.ts +147 -0
  99. package/ios/AVAssetTrackUtils.h +24 -0
  100. package/ios/AudioCompositionUtils.h +27 -0
  101. package/ios/AudioCompositionUtils.mm +113 -0
  102. package/ios/DecoderWindow.h +52 -0
  103. package/ios/NativeResourceStats.h +32 -0
  104. package/ios/RNSVColorSpace.h +50 -0
  105. package/ios/RNSVJSIUtils.h +46 -0
  106. package/ios/RNSVScopedPixelBuffer.h +17 -0
  107. package/ios/RNSVVideoPlayer.h +46 -0
  108. package/ios/RNSVVideoPlayer.mm +410 -0
  109. package/ios/ReactNativeSkiaVideo.h +8 -0
  110. package/ios/ReactNativeSkiaVideo.mm +310 -0
  111. package/ios/SeekPolicy.h +27 -0
  112. package/ios/VideoComposition.h +148 -0
  113. package/ios/VideoCompositionFramesExtractorHostObject.h +105 -0
  114. package/ios/VideoCompositionFramesExtractorHostObject.mm +686 -0
  115. package/ios/VideoCompositionFramesExtractorSyncHostObject.h +37 -0
  116. package/ios/VideoCompositionFramesExtractorSyncHostObject.mm +168 -0
  117. package/ios/VideoCompositionItemDecoder.h +60 -0
  118. package/ios/VideoCompositionItemDecoder.mm +480 -0
  119. package/ios/VideoEncoderHostObject.h +83 -0
  120. package/ios/VideoEncoderHostObject.mm +706 -0
  121. package/ios/VideoFrame.h +66 -0
  122. package/ios/VideoFrame.mm +108 -0
  123. package/ios/VideoPlayerHostObject.h +52 -0
  124. package/ios/VideoPlayerHostObject.mm +332 -0
  125. package/lib/module/NativeReactNativeSkiaVideo.js +5 -0
  126. package/lib/module/NativeReactNativeSkiaVideo.js.map +1 -0
  127. package/lib/module/RNSkiaVideoModule.js +20 -0
  128. package/lib/module/RNSkiaVideoModule.js.map +1 -0
  129. package/lib/module/canvas.js +18 -0
  130. package/lib/module/canvas.js.map +1 -0
  131. package/lib/module/exportCache.js +22 -0
  132. package/lib/module/exportCache.js.map +1 -0
  133. package/lib/module/exportFrames.js +338 -0
  134. package/lib/module/exportFrames.js.map +1 -0
  135. package/lib/module/exportQueue.js +52 -0
  136. package/lib/module/exportQueue.js.map +1 -0
  137. package/lib/module/exportTarget.js +137 -0
  138. package/lib/module/exportTarget.js.map +1 -0
  139. package/lib/module/exportVideoComposition.js +492 -0
  140. package/lib/module/exportVideoComposition.js.map +1 -0
  141. package/lib/module/frameInterop.js +532 -0
  142. package/lib/module/frameInterop.js.map +1 -0
  143. package/lib/module/gpu.js +222 -0
  144. package/lib/module/gpu.js.map +1 -0
  145. package/lib/module/gpuDevice.js +54 -0
  146. package/lib/module/gpuDevice.js.map +1 -0
  147. package/lib/module/index.js +36 -0
  148. package/lib/module/index.js.map +1 -0
  149. package/lib/module/memory.js +38 -0
  150. package/lib/module/memory.js.map +1 -0
  151. package/lib/module/package.json +1 -0
  152. package/lib/module/previewLifecycle.js +52 -0
  153. package/lib/module/previewLifecycle.js.map +1 -0
  154. package/lib/module/runtimeYield.js +10 -0
  155. package/lib/module/runtimeYield.js.map +1 -0
  156. package/lib/module/types.js +4 -0
  157. package/lib/module/types.js.map +1 -0
  158. package/lib/module/utils/useEventListener.js +18 -0
  159. package/lib/module/utils/useEventListener.js.map +1 -0
  160. package/lib/module/videoCompositionPlayer.js +348 -0
  161. package/lib/module/videoCompositionPlayer.js.map +1 -0
  162. package/lib/module/videoFrameImage.js +185 -0
  163. package/lib/module/videoFrameImage.js.map +1 -0
  164. package/lib/module/videoPlayer.js +172 -0
  165. package/lib/module/videoPlayer.js.map +1 -0
  166. package/lib/module/yuvBlit.js +299 -0
  167. package/lib/module/yuvBlit.js.map +1 -0
  168. package/lib/typescript/package.json +1 -0
  169. package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts +7 -0
  170. package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts.map +1 -0
  171. package/lib/typescript/src/RNSkiaVideoModule.d.ts +4 -0
  172. package/lib/typescript/src/RNSkiaVideoModule.d.ts.map +1 -0
  173. package/lib/typescript/src/canvas.d.ts +4 -0
  174. package/lib/typescript/src/canvas.d.ts.map +1 -0
  175. package/lib/typescript/src/exportCache.d.ts +6 -0
  176. package/lib/typescript/src/exportCache.d.ts.map +1 -0
  177. package/lib/typescript/src/exportFrames.d.ts +57 -0
  178. package/lib/typescript/src/exportFrames.d.ts.map +1 -0
  179. package/lib/typescript/src/exportQueue.d.ts +5 -0
  180. package/lib/typescript/src/exportQueue.d.ts.map +1 -0
  181. package/lib/typescript/src/exportTarget.d.ts +28 -0
  182. package/lib/typescript/src/exportTarget.d.ts.map +1 -0
  183. package/lib/typescript/src/exportVideoComposition.d.ts +68 -0
  184. package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -0
  185. package/lib/typescript/src/frameInterop.d.ts +17 -0
  186. package/lib/typescript/src/frameInterop.d.ts.map +1 -0
  187. package/lib/typescript/src/gpu.d.ts +39 -0
  188. package/lib/typescript/src/gpu.d.ts.map +1 -0
  189. package/lib/typescript/src/gpuDevice.d.ts +11 -0
  190. package/lib/typescript/src/gpuDevice.d.ts.map +1 -0
  191. package/lib/typescript/src/index.d.ts +21 -0
  192. package/lib/typescript/src/index.d.ts.map +1 -0
  193. package/lib/typescript/src/memory.d.ts +25 -0
  194. package/lib/typescript/src/memory.d.ts.map +1 -0
  195. package/lib/typescript/src/previewLifecycle.d.ts +22 -0
  196. package/lib/typescript/src/previewLifecycle.d.ts.map +1 -0
  197. package/lib/typescript/src/runtimeYield.d.ts +4 -0
  198. package/lib/typescript/src/runtimeYield.d.ts.map +1 -0
  199. package/lib/typescript/src/types.d.ts +631 -0
  200. package/lib/typescript/src/types.d.ts.map +1 -0
  201. package/lib/typescript/src/utils/useEventListener.d.ts +5 -0
  202. package/lib/typescript/src/utils/useEventListener.d.ts.map +1 -0
  203. package/lib/typescript/src/videoCompositionPlayer.d.ts +85 -0
  204. package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -0
  205. package/lib/typescript/src/videoFrameImage.d.ts +60 -0
  206. package/lib/typescript/src/videoFrameImage.d.ts.map +1 -0
  207. package/lib/typescript/src/videoPlayer.d.ts +99 -0
  208. package/lib/typescript/src/videoPlayer.d.ts.map +1 -0
  209. package/lib/typescript/src/yuvBlit.d.ts +50 -0
  210. package/lib/typescript/src/yuvBlit.d.ts.map +1 -0
  211. package/package.json +154 -4
  212. package/patches/react-native-skia+3.7.0.patch +502 -0
  213. package/react-native-skia-webgpu-video.podspec +35 -0
  214. package/scripts/apply-skia-canvas-patch.mjs +48 -0
  215. package/scripts/skia-canvas-patch.json +291 -0
  216. package/src/NativeReactNativeSkiaVideo.ts +8 -0
  217. package/src/RNSkiaVideoModule.ts +27 -0
  218. package/src/canvas.ts +22 -0
  219. package/src/exportCache.ts +27 -0
  220. package/src/exportFrames.ts +421 -0
  221. package/src/exportQueue.ts +65 -0
  222. package/src/exportTarget.ts +179 -0
  223. package/src/exportVideoComposition.ts +718 -0
  224. package/src/frameInterop.ts +684 -0
  225. package/src/gpu.ts +290 -0
  226. package/src/gpuDevice.ts +80 -0
  227. package/src/index.tsx +45 -0
  228. package/src/memory.ts +48 -0
  229. package/src/previewLifecycle.ts +60 -0
  230. package/src/runtimeYield.ts +6 -0
  231. package/src/types.ts +661 -0
  232. package/src/utils/useEventListener.ts +22 -0
  233. package/src/videoCompositionPlayer.ts +520 -0
  234. package/src/videoFrameImage.ts +205 -0
  235. package/src/videoPlayer.ts +318 -0
  236. package/src/yuvBlit.ts +331 -0
@@ -0,0 +1,686 @@
1
+ #include "VideoCompositionFramesExtractorHostObject.h"
2
+
3
+ #import "AVAssetTrackUtils.h"
4
+ #import "AudioCompositionUtils.h"
5
+ #import "RNSVJSIUtils.h"
6
+ #include "RNSVCheckedSizes.h"
7
+ #import <AVFoundation/AVFoundation.h>
8
+ #import <Foundation/Foundation.h>
9
+
10
+ namespace RNSkiaVideo {
11
+
12
+ VideoCompositionFramesExtractorHostObject::
13
+ VideoCompositionFramesExtractorHostObject(
14
+ jsi::Runtime& runtime, std::shared_ptr<react::CallInvoker> callInvoker,
15
+ std::shared_ptr<VideoComposition> videoComposition)
16
+ : EventEmitter(runtime, callInvoker), composition(videoComposition),
17
+ window(videoComposition->lazyDecoders, true) {
18
+ lock = [[NSObject alloc] init];
19
+ }
20
+
21
+ VideoCompositionFramesExtractorHostObject::
22
+ ~VideoCompositionFramesExtractorHostObject() {
23
+ this->release();
24
+ // The queued blocks hold this, a reader being opened among them: this
25
+ // returns once they have all run.
26
+ if (decoderQueue && dispatch_get_specific(this) != this) {
27
+ dispatch_sync(decoderQueue, ^{
28
+ return;
29
+ });
30
+ }
31
+ }
32
+
33
+ std::vector<jsi::PropNameID>
34
+ VideoCompositionFramesExtractorHostObject::getPropertyNames(jsi::Runtime& rt) {
35
+ std::vector<jsi::PropNameID> result;
36
+ result.push_back(jsi::PropNameID::forUtf8(rt, std::string("prepare")));
37
+ result.push_back(jsi::PropNameID::forUtf8(rt, std::string("play")));
38
+ result.push_back(jsi::PropNameID::forUtf8(rt, std::string("pause")));
39
+ result.push_back(jsi::PropNameID::forUtf8(rt, std::string("seekTo")));
40
+ result.push_back(
41
+ jsi::PropNameID::forUtf8(rt, std::string("decodeCompositionFrames")));
42
+ result.push_back(jsi::PropNameID::forUtf8(rt, std::string("on")));
43
+ result.push_back(jsi::PropNameID::forUtf8(rt, std::string("dispose")));
44
+ result.push_back(jsi::PropNameID::forUtf8(rt, std::string("currentTime")));
45
+ result.push_back(jsi::PropNameID::forUtf8(rt, std::string("framesVersion")));
46
+ result.push_back(jsi::PropNameID::forUtf8(rt, std::string("isLooping")));
47
+ result.push_back(jsi::PropNameID::forUtf8(rt, std::string("isPlaying")));
48
+ return result;
49
+ }
50
+
51
+ // The methods are created once per runtime (see RNSVHostObject):
52
+ // `decodeCompositionFrames` is read at every vsync of the UI runtime.
53
+ jsi::Value VideoCompositionFramesExtractorHostObject::get(
54
+ jsi::Runtime& runtime, const jsi::PropNameID& propNameId) {
55
+ @synchronized(lock) {
56
+ auto propName = propNameId.utf8(runtime);
57
+ if (propName == "prepare") {
58
+ return getFunction(
59
+ runtime, propName, 0,
60
+ [this](jsi::Runtime& runtime, const jsi::Value& thisValue,
61
+ const jsi::Value* arguments, size_t count) -> jsi::Value {
62
+ @synchronized(lock) { if (!released.test()) {
63
+ prepare();
64
+ } }
65
+ return jsi::Value::undefined();
66
+ });
67
+ } else if (propName == "play") {
68
+ return getFunction(
69
+ runtime, propName, 0,
70
+ [this](jsi::Runtime& runtime, const jsi::Value& thisValue,
71
+ const jsi::Value* arguments, size_t count) -> jsi::Value {
72
+ @synchronized(lock) { if (!released.test()) {
73
+ if (initialized) {
74
+ play();
75
+ } else {
76
+ playWhenReady = true;
77
+ }
78
+ } }
79
+ return jsi::Value::undefined();
80
+ });
81
+ } else if (propName == "pause") {
82
+ return getFunction(
83
+ runtime, propName, 0,
84
+ [this](jsi::Runtime& runtime, const jsi::Value& thisValue,
85
+ const jsi::Value* arguments, size_t count) -> jsi::Value {
86
+ @synchronized(lock) { if (!released.test()) {
87
+ if (initialized) {
88
+ pause();
89
+ } else {
90
+ playWhenReady = false;
91
+ }
92
+ } }
93
+ return jsi::Value::undefined();
94
+ });
95
+ } else if (propName == "seekTo") {
96
+ return getFunction(
97
+ runtime, propName, 1,
98
+ [this](jsi::Runtime& runtime, const jsi::Value& thisValue,
99
+ const jsi::Value* arguments, size_t count) -> jsi::Value {
100
+ if (count < 1 || !arguments[0].isNumber())
101
+ throw jsi::JSError(runtime, "seekTo expects media seconds");
102
+ auto seconds = checkedMediaSeconds(arguments[0].asNumber());
103
+ if (!released.test()) {
104
+ seekTo(CMTimeMakeWithSeconds(seconds, NSEC_PER_SEC));
105
+ }
106
+ return jsi::Value::undefined();
107
+ });
108
+ } else if (propName == "decodeCompositionFrames") {
109
+ return getFunction(
110
+ runtime, propName, 0,
111
+ [this](jsi::Runtime& runtime, const jsi::Value& thisValue,
112
+ const jsi::Value* arguments, size_t count) -> jsi::Value {
113
+ scheduleDecode();
114
+ @synchronized(lock) {
115
+ if (released.test()) {
116
+ return jsi::Object(runtime);
117
+ }
118
+ if (initialized && lastAppliedSeekGeneration == seekGeneration) {
119
+ auto currentTime = getCurrentTime();
120
+ bool changed = false;
121
+ for (const auto& entry : itemDecoders) {
122
+ auto itemId = entry.first;
123
+ auto decoder = entry.second;
124
+ auto previousFrame = currentFrames[itemId];
125
+ // Forced after a seek as well as when there is nothing on
126
+ // screen: the frames the new reader hands back start at the
127
+ // keyframe before the target, so waiting for one stamped at
128
+ // or past it holds the pre-seek picture for up to a GOP —
129
+ // the scrub looks stuck, then jumps.
130
+ auto frame = decoder->acquireFrameForTime(
131
+ currentTime, !previousFrame || seekPending);
132
+ if (frame) {
133
+ currentFrames[itemId] = frame;
134
+ changed = true;
135
+ }
136
+ }
137
+ if (changed) {
138
+ framesVersion++;
139
+ seekPending = false;
140
+ }
141
+ }
142
+ // The frames object is only rebuilt when a decoder produced a
143
+ // new frame. On a 120 Hz display most calls see the same frames
144
+ // as the previous one, and rewrapping them would be one JS
145
+ // allocation per item per vsync.
146
+ return getVersionedObject(
147
+ runtime, "frames", (double)framesVersion,
148
+ [&](jsi::Object& frames) {
149
+ for (const auto& entry : currentFrames) {
150
+ if (!entry.second) {
151
+ continue;
152
+ }
153
+ frames.setProperty(runtime, entry.first.c_str(),
154
+ jsi::Object::createFromHostObject(
155
+ runtime, entry.second));
156
+ }
157
+ });
158
+ }
159
+ });
160
+ } else if (propName == "on") {
161
+ return getFunction(
162
+ runtime, propName, 2,
163
+ [this](jsi::Runtime& runtime, const jsi::Value& thisValue,
164
+ const jsi::Value* arguments, size_t count) -> jsi::Value {
165
+ @synchronized(lock) {
166
+ if (released.test()) {
167
+ // Nothing to listen to anymore: hand out a no-op unsubscribe.
168
+ return jsi::Function::createFromHostFunction(
169
+ runtime, jsi::PropNameID::forAscii(runtime, "dispose"), 0,
170
+ [](jsi::Runtime& runtime, const jsi::Value& thisValue,
171
+ const jsi::Value* arguments, size_t count) -> jsi::Value {
172
+ return jsi::Value::undefined();
173
+ });
174
+ }
175
+ if (count < 2 || !arguments[0].isString() || !arguments[1].isObject() ||
176
+ !arguments[1].asObject(runtime).isFunction(runtime))
177
+ throw jsi::JSError(runtime, "on expects an event name and listener");
178
+ auto name = arguments[0].asString(runtime).utf8(runtime);
179
+ auto handler = arguments[1].asObject(runtime).asFunction(runtime);
180
+ return this->on(name, std::move(handler));
181
+ }
182
+ });
183
+ } else if (propName == "dispose") {
184
+ return getFunction(
185
+ runtime, propName, 0,
186
+ [this](jsi::Runtime& runtime, const jsi::Value& thisValue,
187
+ const jsi::Value* arguments, size_t count) -> jsi::Value {
188
+ this->release();
189
+ return jsi::Value::undefined();
190
+ });
191
+ } else if (propName == "currentTime") {
192
+ return jsi::Value(released.test() ? 0 : CMTimeGetSeconds(getCurrentTime()));
193
+ } else if (propName == "framesVersion") {
194
+ return jsi::Value(released.test() ? 0 : (double)framesVersion);
195
+ } else if (propName == "isLooping") {
196
+ return jsi::Value(!released.test() && isLooping);
197
+ } else if (propName == "isPlaying") {
198
+ return jsi::Value(!released.test() && isPlaying);
199
+ }
200
+ return jsi::Value::undefined();
201
+ }
202
+ }
203
+
204
+ void VideoCompositionFramesExtractorHostObject::set(
205
+ jsi::Runtime& runtime, const jsi::PropNameID& propNameId,
206
+ const jsi::Value& value) {
207
+ @synchronized(lock) {
208
+ if (released.test()) {
209
+ return;
210
+ }
211
+ }
212
+ auto propName = propNameId.utf8(runtime);
213
+ if (propName == "isLooping") {
214
+ isLooping = value.asBool();
215
+ }
216
+ }
217
+
218
+ void VideoCompositionFramesExtractorHostObject::prepare() {
219
+ @synchronized(lock) {
220
+ if (released.test() || decoderQueue) return;
221
+ dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(
222
+ DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0);
223
+ decoderQueue = dispatch_queue_create("ReactNativeVideoCompositionItemDecoder", attr);
224
+ dispatch_queue_set_specific(decoderQueue, this, this, NULL);
225
+ std::weak_ptr<VideoCompositionFramesExtractorHostObject> weak =
226
+ std::static_pointer_cast<VideoCompositionFramesExtractorHostObject>(shared_from_this());
227
+ dispatch_async(decoderQueue, ^{
228
+ if (auto owner = weak.lock()) owner->init();
229
+ });
230
+ displayLink = [[RNSVDisplayLinkWrapper alloc]
231
+ initWithUpdateBlock:^(CADisplayLink* displayLink) {
232
+ if (auto owner = weak.lock()) owner->scheduleDecode();
233
+ }];
234
+ [displayLink start];
235
+ }
236
+ }
237
+ // Both display ticks and explicit seeks enter one serial decoder queue.
238
+ // A paused/occluded surface must still service a seek even when CADisplayLink
239
+ // is throttled. Coalescing bounds outstanding work to one queued tick.
240
+ void VideoCompositionFramesExtractorHostObject::scheduleDecode() {
241
+ @synchronized(lock) {
242
+ if (released.test() || !decoderQueue || decoderTickPending.exchange(true)) return;
243
+ std::weak_ptr<VideoCompositionFramesExtractorHostObject> weak =
244
+ std::static_pointer_cast<VideoCompositionFramesExtractorHostObject>(shared_from_this());
245
+ dispatch_async(decoderQueue, ^{
246
+ auto owner = weak.lock();
247
+ if (!owner) return;
248
+ uint64_t scheduledGeneration;
249
+ @synchronized(owner->lock) { scheduledGeneration = owner->seekGeneration; }
250
+ @autoreleasepool {
251
+ struct PendingGuard {
252
+ std::atomic<bool>& flag;
253
+ ~PendingGuard() { flag.store(false); }
254
+ } pendingGuard{owner->decoderTickPending};
255
+ owner->decodeTick();
256
+ }
257
+ // A seek arriving during decoding was coalesced into this task. Queue
258
+ // its latest position after releasing the pending flag, so it cannot
259
+ // be lost when the display link is idle.
260
+ @synchronized(owner->lock) {
261
+ if (!owner->released.test() &&
262
+ scheduledGeneration != owner->seekGeneration) {
263
+ owner->scheduleDecode();
264
+ }
265
+ }
266
+ });
267
+ }
268
+ }
269
+
270
+ void VideoCompositionFramesExtractorHostObject::decodeTick() {
271
+ auto* self = this;
272
+ std::vector<std::shared_ptr<VideoCompositionItemDecoder>> advancing;
273
+ std::vector<std::shared_ptr<VideoCompositionItem>> opening;
274
+ std::vector<std::shared_ptr<VideoCompositionItemDecoder>> closing;
275
+ CMTime currentTime = kCMTimeZero;
276
+ uint64_t generation = 0;
277
+ @synchronized(self->lock) {
278
+ if (self->released.test() || !self->initialized) {
279
+ return;
280
+ }
281
+ currentTime = self->getCurrentTime();
282
+ if (CMTimeGetSeconds(currentTime) >= self->composition->duration) {
283
+ if (!self->completeEmitted) {
284
+ self->completeEmitted = true;
285
+ self->emit("complete", jsi::Value::null());
286
+ }
287
+ if (self->isLooping) {
288
+ currentTime = kCMTimeZero;
289
+ self->startDate = [NSDate date];
290
+ if (self->audioPlayer) {
291
+ [self->audioPlayer seekToTime:kCMTimeZero
292
+ toleranceBefore:kCMTimeZero
293
+ toleranceAfter:kCMTimeZero
294
+ completionHandler:^(BOOL){
295
+ }];
296
+ }
297
+ } else {
298
+ // Switching to the paused clock must preserve the completed position;
299
+ // leaving its old value (usually zero) rewinds after completion.
300
+ self->pausePosition = CMTimeMakeWithSeconds(
301
+ self->composition->duration, NSEC_PER_SEC);
302
+ self->isPlaying = false;
303
+ if (self->audioPlayer) {
304
+ [self->audioPlayer pause];
305
+ }
306
+ return;
307
+ }
308
+ } else {
309
+ self->completeEmitted = false;
310
+ }
311
+ generation = self->seekGeneration;
312
+ self->updateWindow(currentTime, opening, closing);
313
+ }
314
+ // Readers are slow to open and close: not while the UI thread waits
315
+ // on the self->lock for its frames.
316
+ for (const auto& decoder : closing) {
317
+ decoder->release();
318
+ }
319
+ closing.clear();
320
+ if (!opening.empty()) {
321
+ self->openDecoders(opening, currentTime, generation);
322
+ }
323
+ @synchronized(self->lock) {
324
+ if (self->released.test() || generation != self->seekGeneration) {
325
+ return;
326
+ }
327
+ // Paused where every decoder has already been advanced to: there
328
+ // is nothing to decode until the position moves or a seek lands.
329
+ if (!self->isPlaying && opening.empty() &&
330
+ generation == self->lastAdvancedGeneration &&
331
+ CMTimeCompare(currentTime, self->lastAdvancedTime) == 0) {
332
+ return;
333
+ }
334
+ for (const auto& entry : self->itemDecoders) {
335
+ if (entry.second) {
336
+ advancing.push_back(entry.second);
337
+ }
338
+ }
339
+ }
340
+ bool applySeek = generation != self->lastAppliedSeekGeneration;
341
+ // In parallel on GCD's pool, instead of a new thread per decoder
342
+ // per vsync.
343
+ struct DecodeFailure { std::mutex mutex; std::string message; };
344
+ auto failure = std::make_shared<DecodeFailure>();
345
+ dispatch_apply(advancing.size(), DISPATCH_APPLY_AUTO, ^(size_t i) {
346
+ try {
347
+ if (applySeek) advancing[i]->seekTo(currentTime);
348
+ advancing[i]->advanceDecoder(currentTime);
349
+ }
350
+ catch (const std::exception& error) {
351
+ std::lock_guard<std::mutex> guard(failure->mutex);
352
+ if (failure->message.empty()) failure->message = error.what();
353
+ } catch (NSError* error) {
354
+ std::lock_guard<std::mutex> guard(failure->mutex);
355
+ if (failure->message.empty())
356
+ failure->message = [[error localizedDescription] UTF8String];
357
+ }
358
+ });
359
+ if (!failure->message.empty()) {
360
+ @synchronized(self->lock) { self->pause(); }
361
+ auto message = failure->message;
362
+ self->emit("error", [message](jsi::Runtime& runtime) {
363
+ auto error = jsi::Object(runtime);
364
+ error.setProperty(runtime, "message", jsi::String::createFromUtf8(runtime, message));
365
+ return jsi::Value(std::move(error));
366
+ });
367
+ }
368
+ @synchronized(lock) {
369
+ if (!released.test() && generation == seekGeneration) {
370
+ lastAppliedSeekGeneration = generation;
371
+ if (failure->message.empty()) {
372
+ lastAdvancedTime = currentTime;
373
+ lastAdvancedGeneration = generation;
374
+ }
375
+ }
376
+ }
377
+ }
378
+
379
+ void VideoCompositionFramesExtractorHostObject::init() {
380
+ @synchronized(lock) {
381
+ if (released.test()) {
382
+ return;
383
+ }
384
+ try {
385
+ // Assets are shared between the video decoders and the audio
386
+ // composition so a same file is never opened twice.
387
+ assetCache = [NSMutableDictionary dictionary];
388
+ for (const auto& item : composition->items) {
389
+ if (!item->isVideo || !opensAt(item, CMTimeGetSeconds(pausePosition))) {
390
+ continue;
391
+ }
392
+ itemDecoders[item->id] = std::make_shared<VideoCompositionItemDecoder>(
393
+ item, true, getOrCreateAsset(item->path, assetCache));
394
+ }
395
+ if (composition->hasAudio()) {
396
+ auto audioComposition = buildAudioComposition(composition, assetCache);
397
+ if (audioComposition.composition) {
398
+ AVPlayerItem* playerItem =
399
+ [AVPlayerItem playerItemWithAsset:audioComposition.composition];
400
+ if (audioComposition.audioMix) {
401
+ playerItem.audioMix = audioComposition.audioMix;
402
+ }
403
+ audioPlayer = [AVPlayer playerWithPlayerItem:playerItem];
404
+ audioPlayer.actionAtItemEnd = AVPlayerActionAtItemEndNone;
405
+ audioPlayer.automaticallyWaitsToMinimizeStalling = NO;
406
+ if (CMTimeCompare(pausePosition, kCMTimeZero) > 0) {
407
+ [audioPlayer seekToTime:pausePosition
408
+ toleranceBefore:kCMTimeZero
409
+ toleranceAfter:kCMTimeZero];
410
+ }
411
+ }
412
+ }
413
+ // A seek issued before init (a freshly created player positioned right
414
+ // away by the JS side) only reached `pausePosition` and, above, the
415
+ // audio player: the item decoders were still reading from zero and
416
+ // caught up with the clock by decoding every frame in between —
417
+ // visibly, a fast-forward from the start of the clip. Position them
418
+ // where the audio starts.
419
+ if (CMTimeCompare(pausePosition, kCMTimeZero) > 0) {
420
+ for (const auto& entry : itemDecoders) {
421
+ entry.second->seekTo(pausePosition);
422
+ }
423
+ }
424
+ } catch (const std::exception& error) {
425
+ itemDecoders.clear();
426
+ audioPlayer = nil;
427
+ assetCache = nil;
428
+ auto message = std::string(error.what());
429
+ emit("error", [message](jsi::Runtime& runtime) -> jsi::Value {
430
+ auto result = jsi::Object(runtime);
431
+ result.setProperty(runtime, "message", jsi::String::createFromUtf8(runtime, message));
432
+ return result;
433
+ });
434
+ return;
435
+ } catch (NSError* error) {
436
+ itemDecoders.clear();
437
+ audioPlayer = nil;
438
+ assetCache = nil;
439
+ emit("error", [=](jsi::Runtime& runtime) -> jsi::Value {
440
+ return RNSkiaVideo::NSErrorToJSI(runtime, error);
441
+ });
442
+ return;
443
+ }
444
+ lastAppliedSeekGeneration = seekGeneration;
445
+ initialized = true;
446
+ if (playWhenReady) {
447
+ play();
448
+ }
449
+ this->emit("ready", jsi::Value::null());
450
+ }
451
+ }
452
+
453
+ void VideoCompositionFramesExtractorHostObject::play() {
454
+ @synchronized(lock) {
455
+ if (released.test() || isPlaying) return;
456
+ // A completed paused clock stays at the end until play explicitly restarts
457
+ // it. Rewind the readers through the same serial seek generation as seekTo.
458
+ if (CMTimeGetSeconds(pausePosition) >= composition->duration) {
459
+ seekTo(kCMTimeZero);
460
+ }
461
+ if (audioPlayer) {
462
+ if (CMTimeGetSeconds(audioPlayer.currentTime) >= composition->duration) {
463
+ [audioPlayer seekToTime:kCMTimeZero
464
+ toleranceBefore:kCMTimeZero
465
+ toleranceAfter:kCMTimeZero];
466
+ }
467
+ [audioPlayer play];
468
+ }
469
+ startDate =
470
+ [NSDate dateWithTimeIntervalSinceNow:-CMTimeGetSeconds(pausePosition)];
471
+ pausePosition = kCMTimeZero;
472
+ isPlaying = true;
473
+ }
474
+ }
475
+
476
+ void VideoCompositionFramesExtractorHostObject::pause() {
477
+ @synchronized(lock) {
478
+ if (!isPlaying) {
479
+ return;
480
+ }
481
+ pausePosition = getCurrentTime();
482
+ isPlaying = false;
483
+ if (audioPlayer) {
484
+ [audioPlayer pause];
485
+ }
486
+ }
487
+ }
488
+
489
+ void VideoCompositionFramesExtractorHostObject::seekTo(CMTime time) {
490
+ @synchronized(lock) {
491
+ if (released.test()) return;
492
+ seekPending = true;
493
+ if (isPlaying) {
494
+ startDate = [NSDate dateWithTimeIntervalSinceNow:-CMTimeGetSeconds(time)];
495
+ } else {
496
+ pausePosition = time;
497
+ }
498
+ if (audioPlayer) {
499
+ [audioPlayer seekToTime:time
500
+ toleranceBefore:kCMTimeZero
501
+ toleranceAfter:kCMTimeZero];
502
+ }
503
+ seekGeneration++;
504
+ // Reader mutation is applied by decodeTick on its serial queue.
505
+ scheduleDecode();
506
+ }
507
+ }
508
+
509
+ bool VideoCompositionFramesExtractorHostObject::opensAt(
510
+ const std::shared_ptr<VideoCompositionItem>& item, double position) const {
511
+ return window.opens(item->compositionStartTime,
512
+ item->compositionStartTime + item->duration, position,
513
+ composition->duration, false);
514
+ }
515
+
516
+ // Under the lock. Closed decoders leave the maps here and are released by the
517
+ // caller; opened ones are made by openDecoders.
518
+ void VideoCompositionFramesExtractorHostObject::updateWindow(
519
+ CMTime time, std::vector<std::shared_ptr<VideoCompositionItem>>& opening,
520
+ std::vector<std::shared_ptr<VideoCompositionItemDecoder>>& closing) {
521
+ if (!window.isLazy()) {
522
+ return;
523
+ }
524
+ double position = CMTimeGetSeconds(time);
525
+ for (const auto& item : composition->items) {
526
+ if (!item->isVideo) {
527
+ continue;
528
+ }
529
+ double start = item->compositionStartTime;
530
+ double end = start + item->duration;
531
+ auto it = itemDecoders.find(item->id);
532
+ if (it == itemDecoders.end()) {
533
+ if (!failedItems.count(item->id) &&
534
+ window.opens(start, end, position, composition->duration,
535
+ isLooping)) {
536
+ #if defined(RNSV_TRACE_LIFECYCLE) && RNSV_TRACE_LIFECYCLE
537
+ NSLog(@"[rnskv] open %s at %.3fs", item->id.c_str(), position);
538
+ #endif
539
+ opening.push_back(item);
540
+ }
541
+ } else if (!window.keeps(start, end, position, composition->duration,
542
+ isLooping)) {
543
+ #if defined(RNSV_TRACE_LIFECYCLE) && RNSV_TRACE_LIFECYCLE
544
+ NSLog(@"[rnskv] close %s at %.3fs", item->id.c_str(), position);
545
+ #endif
546
+ closing.push_back(it->second);
547
+ itemDecoders.erase(it);
548
+ if (currentFrames.erase(item->id) > 0) {
549
+ framesVersion++;
550
+ }
551
+ }
552
+ }
553
+ }
554
+
555
+ // On the decoder queue, outside of the lock.
556
+ void VideoCompositionFramesExtractorHostObject::openDecoders(
557
+ const std::vector<std::shared_ptr<VideoCompositionItem>>& items,
558
+ CMTime time, uint64_t generation) {
559
+ for (const auto& item : items) {
560
+ std::shared_ptr<VideoCompositionItemDecoder> decoder;
561
+ NSError* failure = nil;
562
+ NSMutableDictionary<NSString*, AVURLAsset*>* cache;
563
+ @synchronized(lock) {
564
+ if (released.test()) return;
565
+ cache = assetCache;
566
+ }
567
+ try {
568
+ decoder = std::make_shared<VideoCompositionItemDecoder>(
569
+ item, true, getOrCreateAsset(item->path, cache), time);
570
+ } catch (const std::exception& error) {
571
+ failure = [NSError errorWithDomain:@"com.azzapp.rnskv" code:0
572
+ userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithUTF8String:error.what()]}];
573
+ } catch (NSError* error) {
574
+ failure = error;
575
+ }
576
+ if (failure) {
577
+ // Decoder queue only, as updateWindow reads it.
578
+ failedItems.insert(item->id);
579
+ emit("error", [=](jsi::Runtime& runtime) -> jsi::Value {
580
+ return RNSkiaVideo::NSErrorToJSI(runtime, failure);
581
+ });
582
+ continue;
583
+ }
584
+ @synchronized(lock) {
585
+ if (released.test()) {
586
+ decoder->release();
587
+ return;
588
+ }
589
+ if (seekGeneration != generation) {
590
+ decoder->seekTo(getCurrentTime());
591
+ }
592
+ itemDecoders[item->id] = decoder;
593
+ }
594
+ }
595
+ }
596
+
597
+ CMTime VideoCompositionFramesExtractorHostObject::getCurrentTime() {
598
+ @synchronized(lock) {
599
+ if (isPlaying) {
600
+ // When the composition has audio, the audio player is the master clock.
601
+ if (audioPlayer) {
602
+ CMTime time = audioPlayer.currentTime;
603
+ if (CMTIME_IS_NUMERIC(time)) {
604
+ return time;
605
+ }
606
+ }
607
+ NSTimeInterval elapsedTime =
608
+ [[NSDate date] timeIntervalSinceDate:startDate];
609
+ return CMTimeMakeWithSeconds(elapsedTime, NSEC_PER_SEC);
610
+ } else {
611
+ return pausePosition;
612
+ }
613
+ }
614
+ }
615
+
616
+ void VideoCompositionFramesExtractorHostObject::release() {
617
+ @synchronized(lock) {
618
+ if (released.test_and_set()) {
619
+ return;
620
+ }
621
+ try {
622
+ for (const auto& entry : itemDecoders) {
623
+ auto decoder = entry.second;
624
+ if (decoder) {
625
+ entry.second->release();
626
+ }
627
+ }
628
+ } catch (...) {
629
+ }
630
+ itemDecoders.clear();
631
+ currentFrames.clear();
632
+ assetCache = nil;
633
+ if (audioPlayer) {
634
+ [audioPlayer pause];
635
+ [audioPlayer replaceCurrentItemWithPlayerItem:nil];
636
+ audioPlayer = nil;
637
+ }
638
+ }
639
+ removeAllListeners();
640
+ if (displayLink != nullptr) {
641
+ [displayLink invalidate];
642
+ displayLink = nullptr;
643
+ }
644
+ }
645
+
646
+ } // namespace RNSkiaVideo
647
+
648
+ @implementation RNSVDisplayLinkWrapper
649
+
650
+ - (instancetype)initWithUpdateBlock:
651
+ (void (^)(CADisplayLink* displayLink))updateBlock {
652
+ self = [super init];
653
+ if (self) {
654
+ _updateBlock = [updateBlock copy];
655
+ _displayLink =
656
+ [CADisplayLink displayLinkWithTarget:self
657
+ selector:@selector(displayLinkFired:)];
658
+ }
659
+ return self;
660
+ }
661
+
662
+ - (void)displayLinkFired:(CADisplayLink*)displayLink {
663
+ if (self.updateBlock) {
664
+ self.updateBlock(displayLink);
665
+ }
666
+ }
667
+
668
+ - (void)start {
669
+ if ([NSThread isMainThread]) {
670
+ [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
671
+ } else {
672
+ dispatch_async(dispatch_get_main_queue(), ^{ [self start]; });
673
+ }
674
+ }
675
+
676
+ - (void)invalidate {
677
+ if (![NSThread isMainThread]) {
678
+ dispatch_async(dispatch_get_main_queue(), ^{ [self invalidate]; });
679
+ return;
680
+ }
681
+ [self.displayLink invalidate];
682
+ self.displayLink = nil;
683
+ self.updateBlock = nil;
684
+ }
685
+
686
+ @end