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,480 @@
1
+ #include "VideoCompositionItemDecoder.h"
2
+
3
+ #import "AVAssetTrackUtils.h"
4
+ #import "NativeResourceStats.h"
5
+ #include "RNSVMemoryBudget.h"
6
+ #import "RNSVColorSpace.h"
7
+ #import <AVFoundation/AVFoundation.h>
8
+ #import <Foundation/Foundation.h>
9
+ #include <unordered_map>
10
+ #include <mutex>
11
+
12
+ namespace RNSkiaVideo {
13
+
14
+ // Accounting follows each owned CMSampleBuffer through queue moves and swaps.
15
+ // The codec's internal allocations remain outside these counters.
16
+ static std::mutex decodedAccountingMutex;
17
+ struct DecodedAllocation { size_t bytes; uint64_t token; };
18
+ static std::unordered_map<CMSampleBufferRef, DecodedAllocation> decodedAccounting;
19
+ static void trackDecodedSample(CMSampleBufferRef sample) {
20
+ size_t bytes = pixelBufferBytes(CMSampleBufferGetImageBuffer(sample));
21
+ std::lock_guard<std::mutex> guard(decodedAccountingMutex);
22
+ auto token = MemoryBudget::instance().reserve(bytes, "iOS decoder queue");
23
+ try { decodedAccounting.emplace(sample, DecodedAllocation{bytes, token}); }
24
+ catch (...) { MemoryBudget::instance().release(token); throw; }
25
+ NativeResourceStats::decoderBuffers.add(1);
26
+ NativeResourceStats::decoderBytes.add(bytes);
27
+ }
28
+ static uint64_t takeDecodedAllocation(CMSampleBufferRef sample) {
29
+ std::lock_guard<std::mutex> guard(decodedAccountingMutex);
30
+ auto found = decodedAccounting.find(sample);
31
+ if (found == decodedAccounting.end()) return 0;
32
+ uint64_t token = found->second.token;
33
+ found->second.token = 0;
34
+ return token;
35
+ }
36
+ static void releaseDecodedSample(CMSampleBufferRef sample) {
37
+ {
38
+ std::lock_guard<std::mutex> guard(decodedAccountingMutex);
39
+ auto found = decodedAccounting.find(sample);
40
+ if (found != decodedAccounting.end()) {
41
+ NativeResourceStats::decoderBuffers.remove(1);
42
+ NativeResourceStats::decoderBytes.remove(found->second.bytes);
43
+ MemoryBudget::instance().release(found->second.token);
44
+ decodedAccounting.erase(found);
45
+ }
46
+ }
47
+ CFRelease(sample);
48
+ }
49
+
50
+ VideoCompositionItemDecoder::VideoCompositionItemDecoder(
51
+ std::shared_ptr<VideoCompositionItem> item, bool realTime,
52
+ AVURLAsset* sharedAsset, CMTime initialTime)
53
+ : frameRing(2) {
54
+ this->item = item;
55
+ this->realTime = realTime;
56
+ lock = [[NSObject alloc] init];
57
+ NSString* path =
58
+ [NSString stringWithCString:item->path.c_str()
59
+ encoding:[NSString defaultCStringEncoding]];
60
+ asset = sharedAsset
61
+ ?: [AVURLAsset URLAssetWithURL:[NSURL fileURLWithPath:path]
62
+ options:nil];
63
+ videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
64
+ if (!videoTrack) {
65
+ throw [NSError
66
+ errorWithDomain:@"com.azzapp.rnskv"
67
+ code:0
68
+ userInfo:@{
69
+ NSLocalizedDescriptionKey : [NSString
70
+ stringWithFormat:@"No video track for path: %@", path]
71
+ }];
72
+ }
73
+ width = videoTrack.naturalSize.width;
74
+ height = videoTrack.naturalSize.height;
75
+ rotation = AVAssetTrackUtils::GetTrackRotationInDegree(videoTrack);
76
+ currentFrame = nullptr;
77
+ this->setupReader(initialTime);
78
+ NativeResourceStats::decoders.add(1);
79
+ counted = true;
80
+ }
81
+
82
+ void VideoCompositionItemDecoder::setupReader(CMTime initialTime) {
83
+ readerGeneration++;
84
+ NSError* error = nil;
85
+ assetReader = [AVAssetReader assetReaderWithAsset:asset error:&error];
86
+ if (error) {
87
+ throw error;
88
+ }
89
+
90
+ auto startTime = CMTimeMakeWithSeconds(item->startTime, NSEC_PER_SEC);
91
+ // Capped at the item's end: a seek past it would otherwise ask the reader
92
+ // for a negative duration.
93
+ auto position = CMTimeMakeWithSeconds(
94
+ MIN(MAX((CMTimeGetSeconds(initialTime) - item->compositionStartTime), 0),
95
+ item->duration),
96
+ NSEC_PER_SEC);
97
+ // The start in the track's own timescale, rounded down. Given in
98
+ // nanoseconds, a start on a frame's exact time (1.2 s, frame 36 at 30 fps)
99
+ // can land a hair before that frame once AVAssetReader applies the track's
100
+ // edit offset, which is not a whole number of nanoseconds (1/30 s for a
101
+ // file with reordered frames): the reader then hands over the previous
102
+ // frame stamped with the requested time and never the one asked for.
103
+ CMTime endTime = CMTimeAdd(
104
+ startTime, CMTimeMakeWithSeconds(item->duration, NSEC_PER_SEC));
105
+ CMTime readStart = CMTimeAdd(startTime, position);
106
+ if (videoTrack.naturalTimeScale > 0) {
107
+ readStart = CMTimeMinimum(
108
+ CMTimeConvertScale(readStart, videoTrack.naturalTimeScale,
109
+ kCMTimeRoundingMethod_RoundTowardNegativeInfinity),
110
+ endTime);
111
+ }
112
+ CMTime trackEnd = CMTimeRangeGetEnd(videoTrack.timeRange);
113
+ if (CMTIME_IS_NUMERIC(trackEnd)) endTime = CMTimeMinimum(endTime, trackEnd);
114
+ if (CMTimeCompare(readStart, endTime) >= 0) {
115
+ // An empty item/range is valid, including a seek past the clip. Do not
116
+ // start AVAssetReader with an empty/negative range and treat that as a
117
+ // decoder initialization error. A later seek can create a new reader.
118
+ assetReader = nil;
119
+ return;
120
+ }
121
+ assetReader.timeRange = CMTimeRangeFromTimeToTime(readStart, endTime);
122
+
123
+ // AVAssetReaderTrackOutput takes pixel buffer attributes and video settings
124
+ // in the same dictionary, so the color properties travel with them: an HDR
125
+ // track is tone-mapped to Rec.709 by AVFoundation before we ever see it
126
+ // (see RNSVColorSpace.h).
127
+ NSDictionary* pixBuffAttributes = @{
128
+ (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
129
+ (id)kCVPixelBufferIOSurfacePropertiesKey : @{},
130
+ (id)kCVPixelBufferMetalCompatibilityKey : @YES,
131
+ AVVideoColorPropertiesKey : SDRColorProperties(),
132
+ };
133
+ CGSize resolution = item->resolution;
134
+ /*
135
+ * No explicit size asked for, but a cap on the longest side: scale the
136
+ * track's own dimensions to fit it.
137
+ *
138
+ * This is the size the frames are *decoded* at, so it is what the preview's
139
+ * four-deep frame ring costs. Left uncapped, a 4K clip is 33 MB a frame in
140
+ * BGRA — 133 MB of ring to fill a stage a few hundred points wide, which the
141
+ * OS watchdog terminates the app over.
142
+ *
143
+ * `naturalSize` is the encoded size, before the display matrix, which is why
144
+ * the cap is resolved here: a caller holding only display dimensions cannot
145
+ * tell a portrait clip from a rotated landscape one, and asking for the
146
+ * transpose would hand back a squashed picture. Even dimensions because
147
+ * hardware scalers dislike odd ones.
148
+ */
149
+ if (!(resolution.width > 0 && resolution.height > 0) && item->maxLongSide > 0) {
150
+ CGSize natural = videoTrack.naturalSize;
151
+ CGFloat longest = MAX(natural.width, natural.height);
152
+ if (longest > item->maxLongSide) {
153
+ CGFloat scale = item->maxLongSide / longest;
154
+ resolution = CGSizeMake(MAX(2, round(natural.width * scale / 2) * 2),
155
+ MAX(2, round(natural.height * scale / 2) * 2));
156
+ }
157
+ }
158
+ if (resolution.width > 0 && resolution.height > 0) {
159
+ pixBuffAttributes =
160
+ [NSMutableDictionary dictionaryWithDictionary:pixBuffAttributes];
161
+ [pixBuffAttributes setValue:@(resolution.width)
162
+ forKey:(id)kCVPixelBufferWidthKey];
163
+ [pixBuffAttributes setValue:@(resolution.height)
164
+ forKey:(id)kCVPixelBufferHeightKey];
165
+ width = resolution.width;
166
+ height = resolution.height;
167
+ }
168
+
169
+ AVAssetReaderOutput* assetReaderOutput =
170
+ [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack
171
+ outputSettings:pixBuffAttributes];
172
+ // Frames are only ever read (GPU import, read-only locks): skip the copy
173
+ // AVFoundation otherwise makes of every decoded buffer before vending it.
174
+ assetReaderOutput.alwaysCopiesSampleData = NO;
175
+ if (!assetReaderOutput || ![assetReader canAddOutput:assetReaderOutput]) {
176
+ throw [NSError errorWithDomain:@"com.azzapp.rnskv" code:0
177
+ userInfo:@{NSLocalizedDescriptionKey : @"Video reader cannot add its decoded output"}];
178
+ }
179
+ [assetReader addOutput:assetReaderOutput];
180
+ if (![assetReader startReading]) {
181
+ throw assetReader.error ?: [NSError errorWithDomain:@"com.azzapp.rnskv" code:0
182
+ userInfo:@{NSLocalizedDescriptionKey : @"Video reader cannot start decoding"}];
183
+ }
184
+ }
185
+
186
+ #define DECODER_INPUT_TIME_ADVANCE 0.1
187
+
188
+
189
+ using FrameQueue = std::list<std::pair<double, CMSampleBufferRef>>;
190
+
191
+ static constexpr size_t kMaxQueuedSamples = 8;
192
+ static constexpr size_t kMaxQueuedSampleBytes = 64ULL * 1024 * 1024;
193
+ static size_t queueBytes(const FrameQueue& queue) {
194
+ size_t bytes = 0;
195
+ for (const auto& frame : queue)
196
+ bytes += pixelBufferBytes(CMSampleBufferGetImageBuffer(frame.second));
197
+ return bytes;
198
+ }
199
+ // Keep the latest candidate at/before the requested position, plus bounded
200
+ // future samples. A large seek never stores all intermediate decoded frames.
201
+ static void prunePastSamples(FrameQueue& queue, CMTime position) {
202
+ while (queue.size() > 1) {
203
+ auto following = std::next(queue.begin());
204
+ if (CMTimeCompare(CMTimeMakeWithSeconds(following->first, NSEC_PER_SEC),
205
+ position) > 0) break;
206
+ releaseDecodedSample(queue.front().second);
207
+ queue.pop_front();
208
+ }
209
+ }
210
+ static void readSamples(AVAssetReader* reader, CMTime latest, CMTime until,
211
+ CMTime endTime, FrameQueue& out, CMTime position,
212
+ size_t maximumSamples, size_t maximumBytes,
213
+ const std::atomic<bool>& closed) {
214
+ if (!maximumSamples || !maximumBytes) return;
215
+ CMTime latestSampleTime = latest;
216
+ while (!CMTIME_IS_VALID(latestSampleTime) ||
217
+ (CMTimeCompare(latestSampleTime, until) < 0 &&
218
+ CMTimeCompare(endTime, until) >= 0)) {
219
+ bool futureQueued = !out.empty() &&
220
+ CMTimeCompare(CMTimeMakeWithSeconds(out.back().first, NSEC_PER_SEC),
221
+ position) > 0;
222
+ if (futureQueued && (out.size() >= maximumSamples ||
223
+ queueBytes(out) >= maximumBytes)) break;
224
+ if (closed.load() || !reader || reader.status != AVAssetReaderStatusReading) break;
225
+ AVAssetReaderOutput* output = [reader.outputs firstObject];
226
+ CMSampleBufferRef sample = [output copyNextSampleBuffer];
227
+ if (!sample) break;
228
+ if (CMSampleBufferGetNumSamples(sample) == 0 ||
229
+ !CMSampleBufferGetImageBuffer(sample)) {
230
+ CFRelease(sample);
231
+ continue;
232
+ }
233
+ // Already in presentation time: do not reapply track edits/slow motion.
234
+ auto timestamp = CMSampleBufferGetPresentationTimeStamp(sample);
235
+ double seconds = CMTimeGetSeconds(timestamp);
236
+ try { trackDecodedSample(sample); }
237
+ catch (...) { CFRelease(sample); throw; }
238
+ try { out.emplace_back(seconds, sample); }
239
+ catch (...) { releaseDecodedSample(sample); throw; }
240
+ prunePastSamples(out, position);
241
+ latestSampleTime = CMTimeMakeWithSeconds(seconds, NSEC_PER_SEC);
242
+ // At most one transient newly decoded sample can exceed the queue byte
243
+ // target while replacing the previous candidate; no live pixels recycled.
244
+ }
245
+ }
246
+
247
+ static CMTime lastSampleTime(const FrameQueue& queue) {
248
+ return queue.empty() ? kCMTimeInvalid
249
+ : CMTimeMakeWithSeconds(queue.back().first, NSEC_PER_SEC);
250
+ }
251
+
252
+ void VideoCompositionItemDecoder::advanceDecoder(CMTime currentTime) {
253
+ std::lock_guard<std::mutex> readerGuard(readerMutex);
254
+ AVAssetReader* reader = nil;
255
+ CMTime latest = kCMTimeInvalid;
256
+ CMTime inputPosition;
257
+ CMTime position;
258
+ size_t maximumSamples = kMaxQueuedSamples;
259
+ size_t maximumBytes = kMaxQueuedSampleBytes;
260
+ CMTime endTime;
261
+ bool intoNextLoop = false;
262
+ uint64_t batch = 0;
263
+ @synchronized(lock) {
264
+ if (released) return;
265
+ CMTime startTime = CMTimeMakeWithSeconds(item->startTime, NSEC_PER_SEC);
266
+ CMTime compositionStartTime =
267
+ CMTimeMakeWithSeconds(item->compositionStartTime, NSEC_PER_SEC);
268
+ position =
269
+ CMTimeAdd(startTime, CMTimeSubtract(currentTime, compositionStartTime));
270
+ inputPosition =
271
+ realTime
272
+ ? CMTimeAdd(position, CMTimeMakeWithSeconds(
273
+ DECODER_INPUT_TIME_ADVANCE, NSEC_PER_SEC))
274
+ : position;
275
+ CMTime duration = CMTimeMakeWithSeconds(item->duration, NSEC_PER_SEC);
276
+ endTime = CMTimeAdd(startTime, duration);
277
+
278
+ if (realTime && CMTimeCompare(endTime, inputPosition) < 0 && !hasLooped) {
279
+ // This pass is read to the item's end before the reader restarts for
280
+ // the next loop: the frames between the last position decoded and the
281
+ // end are still to be shown, and a seek into the last tenth of a second
282
+ // of the item lands on one of them. Dropping them left the frame from
283
+ // before such a seek on screen. Under the lock: it replaces the reader,
284
+ // once per loop.
285
+ prunePastSamples(decodedFrames, position);
286
+ readSamples(assetReader, lastSampleTime(decodedFrames), endTime, endTime,
287
+ decodedFrames, position, kMaxQueuedSamples,
288
+ kMaxQueuedSampleBytes, released);
289
+ setupReader(kCMTimeZero);
290
+ hasLooped = true;
291
+ }
292
+ // Once looped, the first frames of the next loop.
293
+ intoNextLoop = hasLooped;
294
+ reader = assetReader;
295
+ latest = lastSampleTime(intoNextLoop ? nextLoopFrames : decodedFrames);
296
+ batch = readerGeneration;
297
+ prunePastSamples(decodedFrames, position);
298
+ prunePastSamples(nextLoopFrames, position);
299
+ size_t queued = decodedFrames.size() + nextLoopFrames.size();
300
+ size_t bytes = queueBytes(decodedFrames) + queueBytes(nextLoopFrames);
301
+ maximumSamples = queued < kMaxQueuedSamples ? kMaxQueuedSamples - queued : 0;
302
+ maximumBytes = bytes < kMaxQueuedSampleBytes ? kMaxQueuedSampleBytes - bytes : 0;
303
+ }
304
+
305
+ // Decoded outside the lock: acquireFrameForTime takes it on the UI thread at
306
+ // every vsync, and a batch held it for the whole read (longer since frames
307
+ // are tone-mapped to Rec.709).
308
+ FrameQueue fresh;
309
+ try {
310
+ readSamples(reader, latest, inputPosition, endTime, fresh, position,
311
+ maximumSamples, maximumBytes, released);
312
+ } catch (...) {
313
+ for (const auto& frame : fresh) releaseDecodedSample(frame.second);
314
+ throw;
315
+ }
316
+
317
+ @synchronized(lock) {
318
+ if (released.load() || batch != readerGeneration || reader != assetReader) {
319
+ // A seek or a release replaced what this batch was read for.
320
+ for (const auto& frame : fresh) {
321
+ releaseDecodedSample(frame.second);
322
+ }
323
+ } else if (intoNextLoop && hasLooped) {
324
+ nextLoopFrames.splice(nextLoopFrames.end(), fresh);
325
+ } else {
326
+ // Or the loop wrapped meanwhile and the next loop's frames are current.
327
+ decodedFrames.splice(decodedFrames.end(), fresh);
328
+ }
329
+ prunePastSamples(decodedFrames, position);
330
+ }
331
+ }
332
+
333
+ std::shared_ptr<VideoFrame>
334
+ VideoCompositionItemDecoder::acquireFrameForTime(CMTime currentTime,
335
+ bool force) {
336
+ if (!frameRing.canAcquire()) return nullptr;
337
+ CMSampleBufferRef nextFrame = nil;
338
+ // advanceDecoder appends to the frame lists from a decoding thread while
339
+ // this runs on the UI thread: every access to the lists goes through the
340
+ // decoder lock. The texture upload below happens outside of it so the
341
+ // decoding thread is not held back by the GPU.
342
+ @synchronized(lock) {
343
+ if (released) return nullptr;
344
+ if (hasLooped && CMTIME_IS_VALID(lastRequestedTime) &&
345
+ CMTimeCompare(currentTime, lastRequestedTime) < 0) {
346
+ hasLooped = false;
347
+ for (const auto& frame : decodedFrames) {
348
+ releaseDecodedSample(frame.second);
349
+ }
350
+ decodedFrames = nextLoopFrames;
351
+ nextLoopFrames.clear();
352
+ }
353
+ lastRequestedTime = currentTime;
354
+
355
+ // Plus a microsecond: a time asked for at exactly a frame's timestamp
356
+ // (the export asks for i / fps) lands a fraction of a nanosecond before
357
+ // it once rounded to nanoseconds, and was given the previous frame — an
358
+ // export at the file's frame rate duplicated and skipped frames.
359
+ double offset =
360
+ MAX(CMTimeGetSeconds(currentTime) - item->compositionStartTime, 0);
361
+ CMTime start = CMTimeMakeWithSeconds(item->startTime, NSEC_PER_SEC);
362
+ CMTime position = CMTimeAdd(
363
+ CMTimeAdd(start, CMTimeMakeWithSeconds(offset, NSEC_PER_SEC)),
364
+ CMTimeMake(1, 1000000));
365
+
366
+ auto it = decodedFrames.begin();
367
+ while (it != decodedFrames.end()) {
368
+ auto timestamp = CMTimeMakeWithSeconds(it->first, NSEC_PER_SEC);
369
+ if (CMTimeCompare(timestamp, position) <= 0 ||
370
+ (force && nextFrame == nullptr)) {
371
+ if (nextFrame != nullptr) {
372
+ releaseDecodedSample(nextFrame);
373
+ }
374
+ nextFrame = it->second;
375
+ it = decodedFrames.erase(it);
376
+ } else {
377
+ break;
378
+ }
379
+ }
380
+ }
381
+ if (nextFrame) {
382
+ CVPixelBufferRef buffer = CMSampleBufferGetImageBuffer(nextFrame);
383
+ uint64_t memoryToken = takeDecodedAllocation(nextFrame);
384
+ try {
385
+ @synchronized(lock) {
386
+ if (released) {
387
+ MemoryBudget::instance().release(memoryToken);
388
+ releaseDecodedSample(nextFrame);
389
+ return nullptr;
390
+ }
391
+ auto frame = makeFrame(buffer, memoryToken);
392
+ releaseDecodedSample(nextFrame);
393
+ return frame;
394
+ }
395
+ } catch (...) {
396
+ MemoryBudget::instance().release(memoryToken);
397
+ releaseDecodedSample(nextFrame);
398
+ throw;
399
+ }
400
+ }
401
+ return nullptr;
402
+ }
403
+
404
+ std::shared_ptr<VideoFrame>
405
+ VideoCompositionItemDecoder::makeFrame(CVPixelBufferRef buffer,
406
+ uint64_t memoryToken) {
407
+ // Both texture modes now use immutable, scoped pixel buffers. The helper
408
+ // controls the copy/import and explicitly closes this lease after its barrier.
409
+ auto frame = std::make_shared<VideoFrame>(buffer, width, height, rotation,
410
+ producerId, memoryToken);
411
+ frameRing.push(frame);
412
+ return frame;
413
+ }
414
+
415
+ void VideoCompositionItemDecoder::seekTo(CMTime currentTime) {
416
+ std::lock_guard<std::mutex> readerGuard(readerMutex);
417
+ @synchronized(lock) {
418
+ if (released) return;
419
+ // A seek the reader can simply read up to: a scrub is a run of small
420
+ // forward steps, and rebuilding an AVAssetReader for each one is what
421
+ // makes dragging the playhead stutter. Reading on costs the frames in
422
+ // between, which is cheaper than a new reader for anything this short.
423
+ if (!hasLooped && seekReadsOn(assetReader &&
424
+ assetReader.status == AVAssetReaderStatusReading,
425
+ CMTIME_IS_VALID(lastRequestedTime),
426
+ CMTimeGetSeconds(lastRequestedTime),
427
+ CMTimeGetSeconds(currentTime))) {
428
+ // The reader is already ahead of the displayed frame. Keep its queued
429
+ // samples: throwing them away loses a nearby seek's target permanently.
430
+ // Keep its generation too, so an in-flight batch from this same reader
431
+ // can still publish the samples needed by the forward seek.
432
+ return;
433
+ }
434
+ // Not release(): that drops the frame ring too, and in direct mode the
435
+ // frame on screen loses its texture — the preview goes black for as long
436
+ // as the new reader takes to decode. The picture a seek replaces stays
437
+ // alive until its replacement arrives.
438
+ discardReader();
439
+ setupReader(currentTime);
440
+ }
441
+ }
442
+
443
+ void VideoCompositionItemDecoder::discardReader() {
444
+ @synchronized(lock) {
445
+ if (assetReader) {
446
+ [assetReader cancelReading];
447
+ assetReader = nullptr;
448
+ }
449
+ for (const auto& frame : decodedFrames) {
450
+ releaseDecodedSample(frame.second);
451
+ }
452
+ decodedFrames.clear();
453
+ for (const auto& frame : nextLoopFrames) {
454
+ releaseDecodedSample(frame.second);
455
+ }
456
+ nextLoopFrames.clear();
457
+ hasLooped = false;
458
+ lastRequestedTime = kCMTimeInvalid;
459
+ readerGeneration++;
460
+ }
461
+ }
462
+
463
+ void VideoCompositionItemDecoder::release() {
464
+ // Stop a long read-on batch between samples before waiting for its current
465
+ // copy to return. cancelReading only runs after that copy has completed.
466
+ if (released.exchange(true)) return;
467
+ std::lock_guard<std::mutex> readerGuard(readerMutex);
468
+ @synchronized(lock) {
469
+ discardReader();
470
+ frameRing.releaseAll();
471
+ currentFrame = nullptr;
472
+ }
473
+ }
474
+
475
+ VideoCompositionItemDecoder::~VideoCompositionItemDecoder() {
476
+ release();
477
+ if (counted) NativeResourceStats::decoders.remove(1);
478
+ }
479
+
480
+ } // namespace RNSkiaVideo
@@ -0,0 +1,83 @@
1
+ #pragma once
2
+
3
+ #import "RNSVHostObject.h"
4
+ #import "VideoComposition.h"
5
+ #import <AVFoundation/AVFoundation.h>
6
+ #import <jsi/jsi.h>
7
+ #import <map>
8
+ #include <atomic>
9
+ #include <mutex>
10
+
11
+ namespace RNSkiaVideo {
12
+ using namespace facebook;
13
+
14
+ class JSI_EXPORT VideoEncoderHostObject : public RNSVHostObject {
15
+ public:
16
+ VideoEncoderHostObject(std::string outPath, int width, int height,
17
+ int frameRate, int bitRate, std::string codec,
18
+ int audioBitRate, int audioSampleRate,
19
+ int audioChannelCount,
20
+ std::shared_ptr<VideoComposition> composition,
21
+ bool directEncoder);
22
+
23
+ /**
24
+ * Whether the device has an encoder for the given codec ("h264" or "hevc").
25
+ * H.264 is guaranteed; HEVC needs an A10 or later.
26
+ */
27
+ static bool isCodecSupported(const std::string& codec);
28
+ ~VideoEncoderHostObject() override;
29
+ jsi::Value get(jsi::Runtime&, const jsi::PropNameID& name) override;
30
+ std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime& rt) override;
31
+
32
+ private:
33
+ #if defined(RNSV_NATIVE_TESTS)
34
+ friend class NativeEncoderTestDriver;
35
+ #endif
36
+ std::string outPath;
37
+ int width;
38
+ int height;
39
+ int bitRate;
40
+ int frameRate;
41
+ std::string codec;
42
+ int audioBitRate;
43
+ int audioSampleRate;
44
+ int audioChannelCount;
45
+ std::shared_ptr<VideoComposition> composition;
46
+ // Direct native-buffer append avoids an additional CPU copy. The reference
47
+ // rgba-pixels path always fills our bounded pool, regardless of this option.
48
+ bool directEncoder = false;
49
+ std::atomic<bool> disposed{false};
50
+ std::recursive_mutex stateMutex;
51
+ bool prepared = false;
52
+ uint64_t poolMemoryToken = 0;
53
+ AVAssetWriter* assetWriter;
54
+ AVAssetWriterInput* assetWriterInput;
55
+ CVPixelBufferPoolRef pixelBufferPool = NULL;
56
+ // The pool buffer lent to JS for the GPU export path: drawn into through
57
+ // WebGPU, then appended as is. One at a time; owned until encoded or released.
58
+ CVPixelBufferRef vendedBuffer = NULL;
59
+
60
+ AVAssetWriterInput* audioWriterInput;
61
+ AVAssetReader* audioReader;
62
+ AVAssetReaderAudioMixOutput* audioMixOutput;
63
+ dispatch_queue_t audioQueue;
64
+ dispatch_semaphore_t audioCompletionSemaphore;
65
+ NSMutableArray<NSError*>* audioErrorHolder;
66
+
67
+ void prepare();
68
+ void prepareImpl();
69
+ void encodeFrame(CVPixelBufferRef source, CMTime time);
70
+ void encodePixels(const uint8_t* pixels, size_t rowBytes, CMTime time,
71
+ bool rgba = false);
72
+ CVPixelBufferRef acquireOutputBuffer();
73
+ uintptr_t acquireFrameBuffer();
74
+ void releaseFrameBuffer();
75
+ void appendBuffer(CVPixelBufferRef pixelBuffer, CMTime time);
76
+ void waitUntilReady();
77
+ void setupAudio();
78
+ void startWritingAudio();
79
+ void finish();
80
+ void release();
81
+ };
82
+
83
+ } // namespace RNSkiaVideo