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,706 @@
1
+ #import "VideoEncoderHostObject.h"
2
+ #import "AudioCompositionUtils.h"
3
+ #import "NativeResourceStats.h"
4
+ #include "RNSVMemoryBudget.h"
5
+ #include "RNSVCheckedSizes.h"
6
+ #import "RNSVColorSpace.h"
7
+ #import "RNSVJSIUtils.h"
8
+ #import "RNSVScopedPixelBuffer.h"
9
+ // VTCopyVideoEncoderList, for asking the device which codecs it can encode
10
+ // rather than inferring it from the chip generation.
11
+ #import <VideoToolbox/VideoToolbox.h>
12
+ #import <future>
13
+ #include <chrono>
14
+ #include <cstring>
15
+ #include <stdexcept>
16
+ #include <thread>
17
+
18
+ NS_INLINE NSError* createErrorWithMessage(NSString* message) {
19
+ return [NSError errorWithDomain:@"com.azzapp.rnskv"
20
+ code:0
21
+ userInfo:@{NSLocalizedDescriptionKey : message}];
22
+ }
23
+
24
+ namespace RNSkiaVideo {
25
+
26
+ VideoEncoderHostObject::VideoEncoderHostObject(
27
+ std::string outPath, int width, int height, int frameRate, int bitRate,
28
+ std::string codec, int audioBitRate, int audioSampleRate,
29
+ int audioChannelCount, std::shared_ptr<VideoComposition> composition,
30
+ bool directEncoder) {
31
+ this->directEncoder = directEncoder;
32
+ this->outPath = outPath;
33
+ this->width = width;
34
+ this->height = height;
35
+ this->frameRate = frameRate;
36
+ this->bitRate = bitRate;
37
+ // Resolved here rather than in prepare() so that the fallback is decided
38
+ // once, before anything is allocated, and the rest of the object can treat
39
+ // this as a codec the device is known to have.
40
+ this->codec = isCodecSupported(codec) ? codec : "h264";
41
+ this->audioBitRate = audioBitRate;
42
+ this->audioSampleRate = audioSampleRate;
43
+ this->audioChannelCount = audioChannelCount;
44
+ this->composition = composition;
45
+ NativeResourceStats::encoders.add(1);
46
+ }
47
+
48
+ VideoEncoderHostObject::~VideoEncoderHostObject() {
49
+ release();
50
+ NativeResourceStats::encoders.remove(1);
51
+ }
52
+
53
+ bool VideoEncoderHostObject::isCodecSupported(const std::string& codec) {
54
+ if (codec == "h264") {
55
+ // Every device that runs this library encodes H.264.
56
+ return true;
57
+ }
58
+ if (codec != "hevc") {
59
+ return false;
60
+ }
61
+ // VTCopyVideoEncoderList is the only answer that comes from the encoders the
62
+ // device actually has, rather than from a hardcoded chip generation. It
63
+ // reports hardware and software encoders alike, which is what we want: an
64
+ // HEVC export that lands on a software encoder is slow but correct.
65
+ CFArrayRef encoders = NULL;
66
+ if (VTCopyVideoEncoderList(NULL, &encoders) != noErr || encoders == NULL) {
67
+ return false;
68
+ }
69
+ bool supported = false;
70
+ for (CFIndex i = 0, n = CFArrayGetCount(encoders); i < n && !supported; i++) {
71
+ auto encoder = (CFDictionaryRef)CFArrayGetValueAtIndex(encoders, i);
72
+ auto codecType = (CFNumberRef)CFDictionaryGetValue(
73
+ encoder, kVTVideoEncoderList_CodecType);
74
+ int32_t value = 0;
75
+ if (codecType &&
76
+ CFNumberGetValue(codecType, kCFNumberSInt32Type, &value) &&
77
+ value == kCMVideoCodecType_HEVC) {
78
+ supported = true;
79
+ }
80
+ }
81
+ CFRelease(encoders);
82
+ return supported;
83
+ }
84
+
85
+ std::vector<jsi::PropNameID>
86
+ VideoEncoderHostObject::getPropertyNames(jsi::Runtime& rt) {
87
+ std::vector<jsi::PropNameID> result;
88
+ result.push_back(jsi::PropNameID::forUtf8(rt, std::string("prepare")));
89
+ result.push_back(jsi::PropNameID::forUtf8(rt, std::string("encodeFrame")));
90
+ result.push_back(jsi::PropNameID::forUtf8(rt, std::string("acquireFrameBuffer")));
91
+ result.push_back(jsi::PropNameID::forUtf8(rt, std::string("releaseFrameBuffer")));
92
+ result.push_back(jsi::PropNameID::forUtf8(rt, std::string("finishWriting")));
93
+ result.push_back(jsi::PropNameID::forUtf8(rt, std::string("dispose")));
94
+ return result;
95
+ }
96
+
97
+ // The methods are created once per runtime (see RNSVHostObject):
98
+ // `encodeFrame` is read for every exported frame.
99
+ jsi::Value VideoEncoderHostObject::get(jsi::Runtime& runtime,
100
+ const jsi::PropNameID& propNameId) {
101
+ auto propName = propNameId.utf8(runtime);
102
+ if (propName == "prepare") {
103
+ return getFunction(
104
+ runtime, propName, 0,
105
+ [this](jsi::Runtime& runtime, const jsi::Value& thisValue,
106
+ const jsi::Value* arguments, size_t count) -> jsi::Value {
107
+ return runPooled([&] { prepare(); });
108
+ });
109
+ }
110
+ if (propName == "encodeFrame") {
111
+ return getFunction(
112
+ runtime, propName, 2,
113
+ [this](jsi::Runtime& runtime, const jsi::Value& thisValue,
114
+ const jsi::Value* arguments, size_t count) -> jsi::Value {
115
+ if (count < 2 || !arguments[0].isObject() || !arguments[1].isNumber()) {
116
+ throw jsi::JSError(runtime, "encodeFrame expects a frame and seconds");
117
+ }
118
+ auto descriptor = arguments[0].asObject(runtime);
119
+ auto time = CMTimeMakeWithSeconds(checkedMediaSeconds(arguments[1].asNumber()), NSEC_PER_SEC);
120
+ auto kindValue = descriptor.getProperty(runtime, "kind");
121
+ auto kind = kindValue.isString() ? kindValue.asString(runtime).utf8(runtime)
122
+ : std::string();
123
+ if (kind == "bgra") {
124
+ auto data = descriptor.getProperty(runtime, "data").asObject(runtime)
125
+ .getArrayBuffer(runtime);
126
+ auto rowValue = descriptor.getProperty(runtime, "bytesPerRow");
127
+ size_t rowBytes = rowValue.isNumber()
128
+ ? checkedStorageInteger(rowValue.asNumber(), "BGRA row bytes")
129
+ : checkedSizeProduct(static_cast<size_t>(width), 4, "BGRA row bytes");
130
+ size_t required = checkedPixelStorage(width, height, rowBytes);
131
+ if (descriptor.getProperty(runtime, "width").asNumber() != width ||
132
+ descriptor.getProperty(runtime, "height").asNumber() != height ||
133
+ data.size(runtime) < required) {
134
+ throw jsi::JSError(runtime, "Invalid BGRA encoder dimensions/storage");
135
+ }
136
+ return runPooled([&] { encodePixels(data.data(runtime), rowBytes, time); });
137
+ }
138
+ if (kind == "rgba-pixels") {
139
+ auto pixels = descriptor.getProperty(runtime, "pixels").asObject(runtime);
140
+ auto storage = pixels.getProperty(runtime, "buffer").asObject(runtime)
141
+ .getArrayBuffer(runtime);
142
+ size_t offset = checkedStorageInteger(
143
+ pixels.getProperty(runtime, "byteOffset").asNumber(), "RGBA byte offset");
144
+ size_t length = checkedStorageInteger(
145
+ pixels.getProperty(runtime, "byteLength").asNumber(), "RGBA byte length");
146
+ auto rowValue = descriptor.getProperty(runtime, "rowBytes");
147
+ size_t rowBytes = rowValue.isNumber()
148
+ ? checkedStorageInteger(rowValue.asNumber(), "RGBA row bytes")
149
+ : checkedSizeProduct(static_cast<size_t>(width), 4, "RGBA row bytes");
150
+ size_t required = checkedPixelStorage(width, height, rowBytes);
151
+ if (descriptor.getProperty(runtime, "width").asNumber() != width ||
152
+ descriptor.getProperty(runtime, "height").asNumber() != height ||
153
+ offset > storage.size(runtime) ||
154
+ length > storage.size(runtime) - offset ||
155
+ length < required) {
156
+ throw jsi::JSError(runtime, "Invalid RGBA encoder pixel storage");
157
+ }
158
+ return runPooled([&] {
159
+ encodePixels(storage.data(runtime) + offset, rowBytes, time, true);
160
+ });
161
+ }
162
+ if (kind != "native-buffer") {
163
+ throw jsi::JSError(runtime, "encodeFrame needs bgra, native-buffer or rgba-pixels");
164
+ }
165
+ auto pointer = descriptor.getProperty(runtime, "nativeBuffer");
166
+ if (!pointer.isBigInt()) {
167
+ throw jsi::JSError(runtime, "nativeBuffer must be a CVPixelBuffer BigInt");
168
+ }
169
+ auto buffer = reinterpret_cast<CVPixelBufferRef>(
170
+ pointer.asBigInt(runtime).asUint64(runtime));
171
+ if (!buffer) throw jsi::JSError(runtime, "Null encoder nativeBuffer");
172
+ return runPooled([&] { encodeFrame(buffer, time); });
173
+ });
174
+ }
175
+ if (propName == "acquireFrameBuffer") {
176
+ return getFunction(
177
+ runtime, propName, 0,
178
+ [this](jsi::Runtime& runtime, const jsi::Value& thisValue,
179
+ const jsi::Value* arguments, size_t count) -> jsi::Value {
180
+ uintptr_t pointer = 0;
181
+ runPooled([&] { pointer = acquireFrameBuffer(); });
182
+ return jsi::BigInt::fromUint64(runtime, pointer);
183
+ });
184
+ }
185
+ if (propName == "releaseFrameBuffer") {
186
+ return getFunction(
187
+ runtime, propName, 0,
188
+ [this](jsi::Runtime& runtime, const jsi::Value& thisValue,
189
+ const jsi::Value* arguments, size_t count) -> jsi::Value {
190
+ return runPooled([&] { releaseFrameBuffer(); });
191
+ });
192
+ }
193
+ if (propName == "finishWriting") {
194
+ return getFunction(
195
+ runtime, propName, 0,
196
+ [this](jsi::Runtime& runtime, const jsi::Value& thisValue,
197
+ const jsi::Value* arguments, size_t count) -> jsi::Value {
198
+ return runPooled([&] { finish(); });
199
+ });
200
+ } else if (propName == "dispose") {
201
+ return getFunction(
202
+ runtime, propName, 0,
203
+ [this](jsi::Runtime& runtime, const jsi::Value& thisValue,
204
+ const jsi::Value* arguments, size_t count) -> jsi::Value {
205
+ return runPooled([&] { this->release(); });
206
+ });
207
+ }
208
+ return jsi::Value::undefined();
209
+ }
210
+
211
+ void VideoEncoderHostObject::prepare() {
212
+ std::lock_guard<std::recursive_mutex> guard(stateMutex);
213
+ if (disposed) throw createErrorWithMessage(@"Encoder is disposed");
214
+ if (prepared) return;
215
+ try { prepareImpl(); }
216
+ catch (...) { release(); throw; }
217
+ }
218
+
219
+ void VideoEncoderHostObject::prepareImpl() {
220
+ if (width <= 0 || height <= 0 || frameRate <= 0 || bitRate <= 0) {
221
+ throw createErrorWithMessage(@"Invalid encoder dimensions, cadence or bitrate");
222
+ }
223
+ // Validate/reserve before starting a writer or an audio callback. A failed
224
+ // preparation rolls this reservation back immediately, even if JS retains
225
+ // the encoder host object while reporting the error.
226
+ size_t rowBytes = checkedSizeProduct(static_cast<size_t>(width), 4, "encoder stride");
227
+ auto capacity = checkedSizeProduct(checkedPixelStorage(width, height, rowBytes),
228
+ 3, "encoder pool capacity");
229
+ poolMemoryToken = MemoryBudget::instance().reserve(capacity, "iOS encoder pool capacity");
230
+ NSDictionary* attributes = @{
231
+ (NSString*)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
232
+ (NSString*)kCVPixelBufferWidthKey : @(width),
233
+ (NSString*)kCVPixelBufferHeightKey : @(height),
234
+ (NSString*)kCVPixelBufferMetalCompatibilityKey : @YES,
235
+ (NSString*)kCVPixelBufferIOSurfacePropertiesKey : @{},
236
+ };
237
+ // Allocate a fresh buffer per frame from this pool instead of reusing a
238
+ // single CVPixelBuffer. AVAssetWriter encodes appended buffers
239
+ // asynchronously, so a reused buffer could be overwritten by the next frame
240
+ // while the encoder is still reading it, producing torn frames on fast
241
+ // motion. The pool only recycles a buffer once every reference to it (the
242
+ // encoder's included) is gone.
243
+ if (pixelBufferPool) {
244
+ CVPixelBufferPoolRelease(pixelBufferPool);
245
+ pixelBufferPool = NULL;
246
+ }
247
+ CVReturn status = CVPixelBufferPoolCreate(
248
+ kCFAllocatorDefault, NULL, (__bridge CFDictionaryRef)attributes,
249
+ &pixelBufferPool);
250
+
251
+ if (status != kCVReturnSuccess) {
252
+ throw createErrorWithMessage(@"Could not create pixel buffer pool");
253
+ return;
254
+ }
255
+
256
+ CVPixelBufferRef probeBuffer = NULL;
257
+ if (CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferPool,
258
+ &probeBuffer) != kCVReturnSuccess || !probeBuffer)
259
+ throw createErrorWithMessage(@"Could not probe encoder pixel storage");
260
+ ScopedPixelBuffer probe(probeBuffer);
261
+ MemoryBudget::instance().resize(poolMemoryToken,
262
+ checkedSizeProduct(pixelBufferBytes(probe.get()), 3, "aligned encoder pool"),
263
+ "iOS aligned encoder pool capacity");
264
+
265
+ NSError* error = nil;
266
+ assetWriter = [AVAssetWriter
267
+ assetWriterWithURL:
268
+ [NSURL fileURLWithPath:
269
+ [NSString
270
+ stringWithCString:outPath.c_str()
271
+ encoding:[NSString defaultCStringEncoding]]]
272
+ fileType:AVFileTypeMPEG4
273
+ error:&error];
274
+ if (error) {
275
+ throw error;
276
+ }
277
+
278
+ bool isHEVC = codec == "hevc";
279
+
280
+ // The profile is codec specific and the H.264 constants are rejected outright
281
+ // for HEVC, so the compression properties are built per codec rather than
282
+ // shared. HEVC's Main profile is the interoperable one — Main10 would mean
283
+ // a 10 bit pixel format, which this encoder does not produce.
284
+ NSMutableDictionary* compressionProperties = [@{
285
+ AVVideoAverageBitRateKey : @(bitRate),
286
+ AVVideoMaxKeyFrameIntervalKey : @(frameRate),
287
+ } mutableCopy];
288
+ compressionProperties[AVVideoProfileLevelKey] =
289
+ isHEVC ? (id)kVTProfileLevel_HEVC_Main_AutoLevel
290
+ : (id)AVVideoProfileLevelH264HighAutoLevel;
291
+
292
+ auto videoSettings = @{
293
+ AVVideoCodecKey : isHEVC ? AVVideoCodecTypeHEVC : AVVideoCodecTypeH264,
294
+ AVVideoWidthKey : @(width),
295
+ AVVideoHeightKey : @(height),
296
+ // The frames come out of Skia in Rec.709; say so in the file rather than
297
+ // leaving every player to guess (see RNSVColorSpace.h).
298
+ AVVideoColorPropertiesKey : SDRColorProperties(),
299
+ AVVideoCompressionPropertiesKey : compressionProperties,
300
+ };
301
+
302
+ assetWriterInput =
303
+ [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo
304
+ outputSettings:videoSettings];
305
+ assetWriterInput.expectsMediaDataInRealTime = NO;
306
+ assetWriterInput.performsMultiPassEncodingIfSupported = NO;
307
+ if ([assetWriter canAddInput:assetWriterInput]) {
308
+ [assetWriter addInput:assetWriterInput];
309
+ } else {
310
+ throw assetWriter.error
311
+ ?: createErrorWithMessage(@"could not add output to asset writer");
312
+ return;
313
+ }
314
+
315
+ if (composition && composition->hasAudio()) {
316
+ setupAudio();
317
+ }
318
+
319
+ if (![assetWriter startWriting]) {
320
+ throw assetWriter.error ?: createErrorWithMessage(@"Could not start writing");
321
+ }
322
+ [assetWriter startSessionAtSourceTime:kCMTimeZero];
323
+
324
+ if (audioWriterInput) {
325
+ startWritingAudio();
326
+ }
327
+
328
+ prepared = true;
329
+ }
330
+
331
+ void VideoEncoderHostObject::waitUntilReady() {
332
+ auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
333
+ while (!assetWriterInput.isReadyForMoreMediaData) {
334
+ if (disposed || assetWriter.status != AVAssetWriterStatusWriting) {
335
+ throw assetWriter.error ?: createErrorWithMessage(@"Encoder stopped writing");
336
+ }
337
+ if (std::chrono::steady_clock::now() >= deadline) {
338
+ throw createErrorWithMessage(@"Encoder input stalled for 10 seconds");
339
+ }
340
+ std::this_thread::sleep_for(std::chrono::milliseconds(2));
341
+ }
342
+ }
343
+
344
+ CVPixelBufferRef VideoEncoderHostObject::acquireOutputBuffer() {
345
+ waitUntilReady();
346
+ // AVAssetWriter retains submitted buffers. Refuse allocation past the
347
+ // threshold and wait for the writer to release one instead of growing.
348
+ NSDictionary* auxiliary = @{(id)kCVPixelBufferPoolAllocationThresholdKey : @3};
349
+ auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
350
+ CVPixelBufferRef buffer = NULL;
351
+ while (true) {
352
+ CVReturn status = CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(
353
+ kCFAllocatorDefault, pixelBufferPool,
354
+ (__bridge CFDictionaryRef)auxiliary, &buffer);
355
+ if (status == kCVReturnSuccess && buffer) return buffer;
356
+ if (status != kCVReturnWouldExceedAllocationThreshold) {
357
+ throw createErrorWithMessage(@"Could not allocate encoder pixel buffer");
358
+ }
359
+ if (std::chrono::steady_clock::now() >= deadline || disposed ||
360
+ assetWriter.status != AVAssetWriterStatusWriting) {
361
+ throw createErrorWithMessage(@"Bounded encoder pool stalled");
362
+ }
363
+ std::this_thread::sleep_for(std::chrono::milliseconds(2));
364
+ }
365
+ }
366
+
367
+ uintptr_t VideoEncoderHostObject::acquireFrameBuffer() {
368
+ std::lock_guard<std::recursive_mutex> guard(stateMutex);
369
+ if (!prepared || disposed) throw createErrorWithMessage(@"Encoder not prepared");
370
+ if (vendedBuffer) {
371
+ throw createErrorWithMessage(@"Previous encoder frame buffer was neither encoded nor released");
372
+ }
373
+ vendedBuffer = acquireOutputBuffer();
374
+ return reinterpret_cast<uintptr_t>(vendedBuffer);
375
+ }
376
+
377
+ void VideoEncoderHostObject::releaseFrameBuffer() {
378
+ std::lock_guard<std::recursive_mutex> guard(stateMutex);
379
+ if (vendedBuffer) {
380
+ CVPixelBufferRelease(vendedBuffer);
381
+ vendedBuffer = NULL;
382
+ }
383
+ }
384
+
385
+ void VideoEncoderHostObject::encodePixels(const uint8_t* pixels,
386
+ size_t rowBytes, CMTime time,
387
+ bool rgba) {
388
+ std::lock_guard<std::recursive_mutex> guard(stateMutex);
389
+ if (!prepared || disposed) throw createErrorWithMessage(@"Encoder not prepared");
390
+ CVPixelBufferRef output = acquireOutputBuffer();
391
+ CVReturn status = CVPixelBufferLockBaseAddress(output, 0);
392
+ if (status != kCVReturnSuccess) {
393
+ CVPixelBufferRelease(output);
394
+ throw createErrorWithMessage(@"Could not lock encoder pixel buffer");
395
+ }
396
+ auto destination = static_cast<uint8_t*>(CVPixelBufferGetBaseAddress(output));
397
+ size_t destinationStride = CVPixelBufferGetBytesPerRow(output);
398
+ if (!destination) {
399
+ CVPixelBufferUnlockBaseAddress(output, 0);
400
+ CVPixelBufferRelease(output);
401
+ throw createErrorWithMessage(@"Encoder pixel storage unavailable");
402
+ }
403
+ for (int row = 0; row < height; ++row) {
404
+ auto sourceRow = pixels + row * rowBytes;
405
+ auto targetRow = destination + row * destinationStride;
406
+ if (!rgba) {
407
+ memcpy(targetRow, sourceRow, static_cast<size_t>(width) * 4);
408
+ } else {
409
+ for (int column = 0; column < width; ++column) {
410
+ targetRow[column * 4] = sourceRow[column * 4 + 2];
411
+ targetRow[column * 4 + 1] = sourceRow[column * 4 + 1];
412
+ targetRow[column * 4 + 2] = sourceRow[column * 4];
413
+ targetRow[column * 4 + 3] = sourceRow[column * 4 + 3];
414
+ }
415
+ }
416
+ }
417
+ CVPixelBufferUnlockBaseAddress(output, 0);
418
+ try { appendBuffer(output, time); }
419
+ catch (...) { CVPixelBufferRelease(output); throw; }
420
+ CVPixelBufferRelease(output);
421
+ }
422
+
423
+ void VideoEncoderHostObject::encodeFrame(CVPixelBufferRef source, CMTime time) {
424
+ std::lock_guard<std::recursive_mutex> guard(stateMutex);
425
+ if (!prepared || disposed) throw createErrorWithMessage(@"Encoder not prepared");
426
+ if (CVPixelBufferGetWidth(source) != static_cast<size_t>(width) ||
427
+ CVPixelBufferGetHeight(source) != static_cast<size_t>(height) ||
428
+ CVPixelBufferGetPixelFormatType(source) != kCVPixelFormatType_32BGRA) {
429
+ throw createErrorWithMessage(@"Encoder requires exact-sized BGRA native buffer");
430
+ }
431
+ if (source == vendedBuffer) {
432
+ // The GPU already drew into this pool buffer; take over the lent reference.
433
+ vendedBuffer = NULL;
434
+ try { appendBuffer(source, time); }
435
+ catch (...) { CVPixelBufferRelease(source); throw; }
436
+ CVPixelBufferRelease(source);
437
+ return;
438
+ }
439
+ if (directEncoder) {
440
+ CVPixelBufferRetain(source);
441
+ try { appendBuffer(source, time); }
442
+ catch (...) { CVPixelBufferRelease(source); throw; }
443
+ CVPixelBufferRelease(source);
444
+ return;
445
+ }
446
+ CVPixelBufferRef output = acquireOutputBuffer();
447
+ CVReturn sourceStatus = CVPixelBufferLockBaseAddress(source, kCVPixelBufferLock_ReadOnly);
448
+ CVReturn targetStatus = CVPixelBufferLockBaseAddress(output, 0);
449
+ if (sourceStatus != kCVReturnSuccess || targetStatus != kCVReturnSuccess) {
450
+ if (sourceStatus == kCVReturnSuccess)
451
+ CVPixelBufferUnlockBaseAddress(source, kCVPixelBufferLock_ReadOnly);
452
+ if (targetStatus == kCVReturnSuccess) CVPixelBufferUnlockBaseAddress(output, 0);
453
+ CVPixelBufferRelease(output);
454
+ throw createErrorWithMessage(@"Could not lock encoder copy buffers");
455
+ }
456
+ auto input = static_cast<const uint8_t*>(CVPixelBufferGetBaseAddress(source));
457
+ auto destination = static_cast<uint8_t*>(CVPixelBufferGetBaseAddress(output));
458
+ size_t inputStride = CVPixelBufferGetBytesPerRow(source);
459
+ size_t targetStride = CVPixelBufferGetBytesPerRow(output);
460
+ if (!input || !destination) {
461
+ CVPixelBufferUnlockBaseAddress(source, kCVPixelBufferLock_ReadOnly);
462
+ CVPixelBufferUnlockBaseAddress(output, 0);
463
+ CVPixelBufferRelease(output);
464
+ throw createErrorWithMessage(@"Could not access encoder copy buffers");
465
+ }
466
+ for (int row = 0; row < height; ++row) {
467
+ memcpy(destination + row * targetStride, input + row * inputStride, static_cast<size_t>(width) * 4);
468
+ }
469
+ CVPixelBufferUnlockBaseAddress(source, kCVPixelBufferLock_ReadOnly);
470
+ CVPixelBufferUnlockBaseAddress(output, 0);
471
+ try { appendBuffer(output, time); }
472
+ catch (...) { CVPixelBufferRelease(output); throw; }
473
+ CVPixelBufferRelease(output);
474
+ }
475
+
476
+ void VideoEncoderHostObject::appendBuffer(CVPixelBufferRef pixelBuffer,
477
+ CMTime time) {
478
+ waitUntilReady();
479
+ TagBufferAsSDR(pixelBuffer);
480
+ size_t bytes = pixelBufferBytes(pixelBuffer);
481
+ NativeResourceStats::encoderSubmittingBuffers.add(1);
482
+ NativeResourceStats::encoderSubmittingBytes.add(bytes);
483
+ CMSampleBufferRef sampleBuffer = NULL;
484
+ CMVideoFormatDescriptionRef formatDescription = NULL;
485
+ OSStatus status = CMVideoFormatDescriptionCreateForImageBuffer(
486
+ NULL, pixelBuffer, &formatDescription);
487
+ CMSampleTimingInfo timingInfo = {CMTimeMake(1, frameRate), time, kCMTimeInvalid};
488
+ NSError* error = nil;
489
+ if (status != noErr || CMSampleBufferCreateForImageBuffer(
490
+ kCFAllocatorDefault, pixelBuffer, true, NULL, NULL,
491
+ formatDescription, &timingInfo, &sampleBuffer) != noErr) {
492
+ error = createErrorWithMessage(@"Could not create sample buffer from frame");
493
+ } else if (![assetWriterInput appendSampleBuffer:sampleBuffer]) {
494
+ error = assetWriter.error ?: createErrorWithMessage(@"Could not append frame");
495
+ }
496
+ if (sampleBuffer) CFRelease(sampleBuffer);
497
+ if (formatDescription) CFRelease(formatDescription);
498
+ NativeResourceStats::encoderSubmittingBuffers.remove(1);
499
+ NativeResourceStats::encoderSubmittingBytes.remove(bytes);
500
+ if (error) throw error;
501
+ }
502
+
503
+ void VideoEncoderHostObject::setupAudio() {
504
+ auto audioComposition = buildAudioComposition(composition, nil);
505
+ if (!audioComposition.composition) {
506
+ return;
507
+ }
508
+
509
+ NSError* error = nil;
510
+ audioReader = [AVAssetReader assetReaderWithAsset:audioComposition.composition
511
+ error:&error];
512
+ if (error) {
513
+ throw error;
514
+ }
515
+ // The audio composition is padded with silence past the composition
516
+ // duration, clamp the export to the exact video duration.
517
+ audioReader.timeRange = CMTimeRangeMake(
518
+ kCMTimeZero, CMTimeMakeWithSeconds(composition->duration, NSEC_PER_SEC));
519
+
520
+ NSDictionary* pcmSettings = @{
521
+ AVFormatIDKey : @(kAudioFormatLinearPCM),
522
+ AVSampleRateKey : @(audioSampleRate),
523
+ AVNumberOfChannelsKey : @(audioChannelCount),
524
+ AVLinearPCMBitDepthKey : @(16),
525
+ AVLinearPCMIsFloatKey : @(NO),
526
+ AVLinearPCMIsBigEndianKey : @(NO),
527
+ AVLinearPCMIsNonInterleaved : @(NO)
528
+ };
529
+ audioMixOutput = [[AVAssetReaderAudioMixOutput alloc]
530
+ initWithAudioTracks:[audioComposition.composition
531
+ tracksWithMediaType:AVMediaTypeAudio]
532
+ audioSettings:pcmSettings];
533
+ if (audioComposition.audioMix) {
534
+ audioMixOutput.audioMix = audioComposition.audioMix;
535
+ }
536
+ if (![audioReader canAddOutput:audioMixOutput]) {
537
+ throw createErrorWithMessage(@"Could not read composition audio");
538
+ }
539
+ [audioReader addOutput:audioMixOutput];
540
+
541
+ NSDictionary* aacSettings = @{
542
+ AVFormatIDKey : @(kAudioFormatMPEG4AAC),
543
+ AVSampleRateKey : @(audioSampleRate),
544
+ AVNumberOfChannelsKey : @(audioChannelCount),
545
+ AVEncoderBitRateKey : @(audioBitRate)
546
+ };
547
+ audioWriterInput =
548
+ [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio
549
+ outputSettings:aacSettings];
550
+ audioWriterInput.expectsMediaDataInRealTime = NO;
551
+ if ([assetWriter canAddInput:audioWriterInput]) {
552
+ [assetWriter addInput:audioWriterInput];
553
+ } else {
554
+ audioWriterInput = nil;
555
+ throw assetWriter.error
556
+ ?: createErrorWithMessage(
557
+ @"could not add audio output to asset writer");
558
+ }
559
+ }
560
+
561
+ void VideoEncoderHostObject::startWritingAudio() {
562
+ if (![audioReader startReading]) {
563
+ throw audioReader.error
564
+ ?: createErrorWithMessage(@"Could not read composition audio");
565
+ }
566
+ audioCompletionSemaphore = dispatch_semaphore_create(0);
567
+ audioErrorHolder = [NSMutableArray array];
568
+ dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(
569
+ DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0);
570
+ audioQueue = dispatch_queue_create("RNSkiaVideoAudioEncoder", attr);
571
+
572
+ // The block only captures ObjC objects, never `this`, so it can safely
573
+ // outlive this host object.
574
+ AVAssetWriterInput* input = audioWriterInput;
575
+ AVAssetReaderAudioMixOutput* output = audioMixOutput;
576
+ AVAssetReader* reader = audioReader;
577
+ AVAssetWriter* writer = assetWriter;
578
+ dispatch_semaphore_t semaphore = audioCompletionSemaphore;
579
+ NSMutableArray<NSError*>* errorHolder = audioErrorHolder;
580
+ __block BOOL finished = NO;
581
+ [input
582
+ requestMediaDataWhenReadyOnQueue:audioQueue
583
+ usingBlock:^{
584
+ if (finished) {
585
+ return;
586
+ }
587
+ while (input.isReadyForMoreMediaData) {
588
+ CMSampleBufferRef sampleBuffer =
589
+ [output copyNextSampleBuffer];
590
+ if (!sampleBuffer) {
591
+ if (reader.status ==
592
+ AVAssetReaderStatusFailed) {
593
+ [errorHolder
594
+ addObject:reader.error
595
+ ?: createErrorWithMessage(
596
+ @"Could not read "
597
+ @"composition "
598
+ @"audio")];
599
+ }
600
+ finished = YES;
601
+ [input markAsFinished];
602
+ dispatch_semaphore_signal(semaphore);
603
+ return;
604
+ }
605
+ BOOL appended =
606
+ [input appendSampleBuffer:sampleBuffer];
607
+ CFRelease(sampleBuffer);
608
+ if (!appended) {
609
+ [errorHolder
610
+ addObject:writer.error
611
+ ?: createErrorWithMessage(
612
+ @"Could not append "
613
+ @"audio data to "
614
+ @"AVAssetWriter")];
615
+ [reader cancelReading];
616
+ finished = YES;
617
+ [input markAsFinished];
618
+ dispatch_semaphore_signal(semaphore);
619
+ return;
620
+ }
621
+ }
622
+ }];
623
+ }
624
+
625
+ void VideoEncoderHostObject::finish() {
626
+ std::lock_guard<std::recursive_mutex> guard(stateMutex);
627
+ if (!prepared || disposed) throw createErrorWithMessage(@"Encoder not prepared");
628
+ [assetWriterInput markAsFinished];
629
+ if (audioWriterInput) {
630
+ auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
631
+ while (dispatch_semaphore_wait(audioCompletionSemaphore,
632
+ dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_MSEC)) != 0) {
633
+ if (disposed) throw createErrorWithMessage(@"Encoder is disposed");
634
+ if (std::chrono::steady_clock::now() >= deadline)
635
+ throw createErrorWithMessage(@"Audio encoder finalization timed out");
636
+ }
637
+ NSError* audioError = audioErrorHolder.firstObject;
638
+ if (audioError) throw audioError;
639
+ }
640
+ // The completion may run after timeout/disposal; it owns its own state and
641
+ // never dereferences this host object or a destroyed stack promise.
642
+ struct WriterCompletion {
643
+ std::promise<void> done;
644
+ NSError* __strong error = nil;
645
+ };
646
+ auto completion = std::make_shared<WriterCompletion>();
647
+ auto future = completion->done.get_future();
648
+ AVAssetWriter* writer = assetWriter;
649
+ [writer finishWritingWithCompletionHandler:^{
650
+ completion->error = writer.status == AVAssetWriterStatusCompleted ? nil :
651
+ (writer.error ?: createErrorWithMessage(@"Failed to finalize export"));
652
+ completion->done.set_value();
653
+ }];
654
+ auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
655
+ while (future.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready) {
656
+ if (disposed || std::chrono::steady_clock::now() >= deadline) {
657
+ [writer cancelWriting];
658
+ throw createErrorWithMessage(disposed ? @"Encoder is disposed" :
659
+ @"Video encoder finalization timed out");
660
+ }
661
+ }
662
+ future.get();
663
+ NSError* error = completion->error;
664
+ if (error) throw error;
665
+ }
666
+
667
+ void VideoEncoderHostObject::release() {
668
+ // A closer announces cancellation before waiting for a synchronous encode
669
+ // or finish call. Those waits then wake promptly without freeing an in-use
670
+ // pool or writer from under the other runtime.
671
+ if (disposed.exchange(true)) return;
672
+ std::lock_guard<std::recursive_mutex> guard(stateMutex);
673
+ prepared = false;
674
+ if (audioReader && audioReader.status == AVAssetReaderStatusReading) {
675
+ [audioReader cancelReading];
676
+ }
677
+ if (audioQueue) {
678
+ AVAssetWriterInput* input = audioWriterInput;
679
+ BOOL shouldMarkFinished =
680
+ input && (assetWriter.status == AVAssetWriterStatusWriting ||
681
+ assetWriter.status == AVAssetWriterStatusFailed);
682
+ dispatch_sync(audioQueue, ^{
683
+ if (shouldMarkFinished) {
684
+ [input markAsFinished];
685
+ }
686
+ });
687
+ audioQueue = nil;
688
+ }
689
+ audioReader = nil;
690
+ audioMixOutput = nil;
691
+ audioWriterInput = nil;
692
+ if (assetWriter && assetWriter.status == AVAssetWriterStatusWriting) {
693
+ [assetWriter cancelWriting];
694
+ }
695
+ assetWriter = nil;
696
+ assetWriterInput = nil;
697
+ releaseFrameBuffer();
698
+ if (pixelBufferPool) {
699
+ CVPixelBufferPoolRelease(pixelBufferPool);
700
+ pixelBufferPool = NULL;
701
+ }
702
+ MemoryBudget::instance().release(poolMemoryToken);
703
+ poolMemoryToken = 0;
704
+ }
705
+
706
+ } // namespace RNSkiaVideo