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,32 @@
1
+ #pragma once
2
+ #import <CoreVideo/CoreVideo.h>
3
+ #include <atomic>
4
+ #include <cstdint>
5
+ namespace RNSkiaVideo {
6
+ struct ResourceCounter {
7
+ std::atomic<uint64_t> current{0};
8
+ std::atomic<uint64_t> peak{0};
9
+ void add(uint64_t amount) {
10
+ uint64_t value = current.fetch_add(amount) + amount;
11
+ uint64_t previous = peak.load();
12
+ while (value > previous && !peak.compare_exchange_weak(previous, value)) {}
13
+ }
14
+ void remove(uint64_t amount) { current.fetch_sub(amount); }
15
+ };
16
+ struct NativeResourceStats {
17
+ inline static ResourceCounter frameBuffers;
18
+ inline static ResourceCounter frameBytes;
19
+ inline static ResourceCounter decoderBuffers;
20
+ inline static ResourceCounter decoderBytes;
21
+ inline static ResourceCounter decoders;
22
+ inline static ResourceCounter encoders;
23
+ // Codec-owned references after append require process/Instruments measures.
24
+ inline static ResourceCounter encoderSubmittingBuffers;
25
+ inline static ResourceCounter encoderSubmittingBytes;
26
+ };
27
+ inline size_t pixelBufferBytes(CVPixelBufferRef buffer) {
28
+ size_t bytes = CVPixelBufferGetDataSize(buffer);
29
+ return bytes ? bytes : CVPixelBufferGetBytesPerRow(buffer) *
30
+ CVPixelBufferGetHeight(buffer);
31
+ }
32
+ } // namespace RNSkiaVideo
@@ -0,0 +1,50 @@
1
+ #pragma once
2
+
3
+ #import <AVFoundation/AVFoundation.h>
4
+ #import <CoreVideo/CoreVideo.h>
5
+
6
+ namespace RNSkiaVideo {
7
+
8
+ /**
9
+ * Rec.709 — the one color space this library decodes into, draws in and
10
+ * encodes out.
11
+ *
12
+ * Skia is handed plain BGRA textures and does no color management, so every
13
+ * frame has to reach it already in the space the canvas assumes. Since the
14
+ * iPhone 12 the camera records HLG HDR by default, and an HLG buffer decoded
15
+ * as-is into BGRA and drawn as sRGB comes out washed out, with crushed
16
+ * contrast. Asking AVFoundation for these properties makes it tone-map to
17
+ * SDR on the way out, on its own fast path.
18
+ *
19
+ * Applied unconditionally rather than only to HDR sources: a source already
20
+ * in Rec.709 costs nothing, and a BT.601 one gets the conversion it always
21
+ * needed. It also spares the decoders an inspection of the track's transfer
22
+ * function before its format descriptions are even loaded.
23
+ */
24
+ static inline NSDictionary* SDRColorProperties() {
25
+ return @{
26
+ AVVideoColorPrimariesKey : AVVideoColorPrimaries_ITU_R_709_2,
27
+ AVVideoTransferFunctionKey : AVVideoTransferFunction_ITU_R_709_2,
28
+ AVVideoYCbCrMatrixKey : AVVideoYCbCrMatrix_ITU_R_709_2,
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Stamps Rec.709 onto a buffer handed to AVAssetWriter. The writer's own
34
+ * output settings already declare it, but a buffer carrying no color
35
+ * attachment at all leaves some muxers to guess, and a file with no
36
+ * colorimetry is read differently by every player.
37
+ */
38
+ static inline void TagBufferAsSDR(CVPixelBufferRef buffer) {
39
+ CVBufferSetAttachment(buffer, kCVImageBufferColorPrimariesKey,
40
+ kCVImageBufferColorPrimaries_ITU_R_709_2,
41
+ kCVAttachmentMode_ShouldPropagate);
42
+ CVBufferSetAttachment(buffer, kCVImageBufferTransferFunctionKey,
43
+ kCVImageBufferTransferFunction_ITU_R_709_2,
44
+ kCVAttachmentMode_ShouldPropagate);
45
+ CVBufferSetAttachment(buffer, kCVImageBufferYCbCrMatrixKey,
46
+ kCVImageBufferYCbCrMatrix_ITU_R_709_2,
47
+ kCVAttachmentMode_ShouldPropagate);
48
+ }
49
+
50
+ } // namespace RNSkiaVideo
@@ -0,0 +1,46 @@
1
+ #pragma once
2
+ #import <Foundation/Foundation.h>
3
+ #import <jsi/jsi.h>
4
+ #include <stdexcept>
5
+ #include <type_traits>
6
+
7
+ namespace RNSkiaVideo {
8
+
9
+ using namespace facebook;
10
+
11
+ // The worklet runtime threads that drive video exports never drain their
12
+ // autorelease pool, so every autoreleased object created by a host function
13
+ // (Metal command buffers, CoreMedia wrappers, AVFoundation internals…) would
14
+ // accumulate for the lifetime of the app. JSI entry points doing ObjC work
15
+ // run inside this helper. Convert NSError into an owning C++ exception after
16
+ // the pool drains, avoiding a leaked CF retain on every failed export.
17
+ template <typename F>
18
+ static jsi::Value runPooled(F&& body) {
19
+ NSError* pendingError = nil;
20
+ jsi::Value result = jsi::Value::undefined();
21
+ @autoreleasepool {
22
+ try {
23
+ if constexpr (std::is_void_v<std::invoke_result_t<F>>) body();
24
+ else result = body();
25
+ } catch (NSError* error) {
26
+ pendingError = error;
27
+ }
28
+ }
29
+ if (pendingError) {
30
+ throw std::runtime_error([[pendingError localizedDescription] UTF8String]);
31
+ }
32
+ return result;
33
+ }
34
+
35
+ static jsi::Value NSErrorToJSI(jsi::Runtime& runtime, NSError* error) {
36
+ auto jsError = jsi::Object(runtime);
37
+ auto message = error == nil ? @"Unknown error" : [error description];
38
+ jsError.setProperty(
39
+ runtime, "message",
40
+ jsi::String::createFromUtf8(runtime, [message UTF8String]));
41
+ jsError.setProperty(runtime, "code",
42
+ error != nil ? jsi::Value((double)[error code])
43
+ : jsi::Value::null());
44
+ return jsError;
45
+ }
46
+ } // namespace RNSkiaVideo
@@ -0,0 +1,17 @@
1
+ #pragma once
2
+ #import <CoreVideo/CoreVideo.h>
3
+
4
+ namespace RNSkiaVideo {
5
+ // Adopts the +1 returned by CoreVideo/AVPlayer copy APIs. The owner always
6
+ // releases it, including when reserving a frame lease throws.
7
+ class ScopedPixelBuffer {
8
+ public:
9
+ explicit ScopedPixelBuffer(CVPixelBufferRef buffer) : buffer(buffer) {}
10
+ ~ScopedPixelBuffer() { if (buffer) CVPixelBufferRelease(buffer); }
11
+ ScopedPixelBuffer(const ScopedPixelBuffer&) = delete;
12
+ ScopedPixelBuffer& operator=(const ScopedPixelBuffer&) = delete;
13
+ CVPixelBufferRef get() const { return buffer; }
14
+ private:
15
+ CVPixelBufferRef buffer;
16
+ };
17
+ }
@@ -0,0 +1,46 @@
1
+ #import <AVFoundation/AVFoundation.h>
2
+ #import <Foundation/Foundation.h>
3
+ #import <Metal/Metal.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ @protocol RNSVVideoPlayerDelegate <NSObject>
8
+
9
+ - (void)readyToPlay:(NSDictionary*)assetInfos;
10
+ - (void)frameAvailable:(CMTime)time;
11
+ - (void)bufferingStart;
12
+ - (void)bufferingEnd;
13
+ - (void)bufferingUpdate:(NSArray<NSValue*>*)loadedTimeRanges;
14
+ - (void)videoError:(nullable NSError*)error;
15
+ - (void)complete;
16
+ - (void)isPlaying:(BOOL)playing;
17
+
18
+ @end
19
+
20
+ @interface RNSVVideoPlayer : NSObject
21
+
22
+ @property(nonatomic, readonly) CMTime currentTime;
23
+ @property(nonatomic, readonly) CMTime duration;
24
+ @property(nonatomic) float volume;
25
+ @property(nonatomic) float playbackSpeed;
26
+ @property(nonatomic, readonly) BOOL disposed;
27
+ @property(nonatomic, readonly) BOOL isInitialized;
28
+ @property(nonatomic, readonly) BOOL isPlaying;
29
+ @property(nonatomic) BOOL isLooping;
30
+ @property(nonatomic) CGSize resolution;
31
+
32
+ - (instancetype)initWithURL:(NSURL*)url
33
+ delegate:(id<RNSVVideoPlayerDelegate>)delegate
34
+ resolution:(CGSize)resolution;
35
+ /**
36
+ * Returns the pixel buffer for the given item time, or NULL if none is
37
+ * available. Ownership is transferred to the caller (CVPixelBufferRelease).
38
+ */
39
+ - (nullable CVPixelBufferRef)copyPixelBufferForTime:(CMTime)time;
40
+ - (void)pause;
41
+ - (void)play;
42
+ - (void)seekTo:(CMTime)time completionHandler:(void (^)(BOOL))completionHandle;
43
+ - (void)dispose;
44
+ @end
45
+
46
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,410 @@
1
+ //
2
+ // Port from flutter video player, see:
3
+ // https://github.com/flutter/packages/blob/41f5a16e13f4c9327e75d431d338c33f2fb35870/packages/video_player/video_player_avfoundation/darwin/Classes/FVPVideoPlayerPlugin.m
4
+ //
5
+
6
+ #import "RNSVVideoPlayer.h"
7
+ #import "AVAssetTrackUtils.h"
8
+ #import "RNSVColorSpace.h"
9
+ #import <QuartzCore/QuartzCore.h>
10
+ #include <atomic>
11
+
12
+ static void* timeRangeContext = &timeRangeContext;
13
+ static void* statusContext = &statusContext;
14
+ static void* presentationSizeContext = &presentationSizeContext;
15
+ static void* durationContext = &durationContext;
16
+ static void* playbackLikelyToKeepUpContext = &playbackLikelyToKeepUpContext;
17
+ static void* rateContext = &rateContext;
18
+
19
+ @implementation RNSVVideoPlayer {
20
+ AVPlayer* _player;
21
+ AVPlayerItemVideoOutput* _videoOutput;
22
+ CADisplayLink* _displayLink;
23
+ id<RNSVVideoPlayerDelegate> _delegate;
24
+ Boolean _complete;
25
+ Boolean _waitingForFrame;
26
+ float _playbackSpeed;
27
+ std::atomic<bool> _disposed;
28
+ std::atomic<bool> _tracksLoading;
29
+ }
30
+
31
+ - (instancetype)initWithURL:(NSURL*)url
32
+ delegate:(id<RNSVVideoPlayerDelegate>)delegate
33
+ resolution:(CGSize)resolution {
34
+ self = [super init];
35
+ _disposed = false;
36
+ _tracksLoading = false;
37
+ _delegate = delegate;
38
+ _complete = NO;
39
+ _waitingForFrame = YES;
40
+ _playbackSpeed = 1.0f;
41
+
42
+ AVAsset* asset = [AVAsset assetWithURL:url];
43
+ self.resolution = resolution;
44
+ // IOSurface-backed: the frames are wrapped into Metal texture views without
45
+ // a copy, and their recycling is gated on the surface's use count.
46
+ // Output settings rather than plain pixel buffer attributes: the dictionary
47
+ // takes both, and only this form carries the color properties that make
48
+ // AVFoundation tone-map an HDR source to Rec.709 (see RNSVColorSpace.h).
49
+ NSMutableDictionary* outputSettings = [@{
50
+ (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
51
+ (id)kCVPixelBufferIOSurfacePropertiesKey : @{},
52
+ (id)kCVPixelBufferMetalCompatibilityKey : @YES,
53
+ AVVideoColorPropertiesKey : RNSkiaVideo::SDRColorProperties(),
54
+ } mutableCopy];
55
+ if (!CGSizeEqualToSize(CGSizeZero, resolution)) {
56
+ [outputSettings setValue:@(resolution.width)
57
+ forKey:(id)kCVPixelBufferWidthKey];
58
+ [outputSettings setValue:@(resolution.height)
59
+ forKey:(id)kCVPixelBufferHeightKey];
60
+ }
61
+
62
+ _videoOutput =
63
+ [[AVPlayerItemVideoOutput alloc] initWithOutputSettings:outputSettings];
64
+
65
+ _displayLink =
66
+ [CADisplayLink displayLinkWithTarget:self
67
+ selector:@selector(displayLinkFired:)];
68
+ [_displayLink addToRunLoop:[NSRunLoop currentRunLoop]
69
+ forMode:NSRunLoopCommonModes];
70
+
71
+ AVPlayerItem* item = [AVPlayerItem playerItemWithAsset:asset];
72
+ _player = [AVPlayer playerWithPlayerItem:item];
73
+ _player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
74
+ [self addObserversForItem:item player:_player];
75
+
76
+ return self;
77
+ }
78
+
79
+ - (BOOL)disposed { return _disposed.load(); }
80
+
81
+ - (CMTime)currentTime {
82
+ return _player.currentTime;
83
+ }
84
+
85
+ - (CMTime)duration {
86
+ return _player.currentItem.duration;
87
+ }
88
+
89
+ - (void)pause {
90
+ _isPlaying = NO;
91
+ [self updatePlayingState];
92
+ }
93
+
94
+ - (void)play {
95
+ _isPlaying = YES;
96
+ [self updatePlayingState];
97
+ }
98
+
99
+ - (void)setIsLooping:(BOOL)isLooping {
100
+ _isLooping = isLooping;
101
+ _player.actionAtItemEnd =
102
+ isLooping ? AVPlayerActionAtItemEndNone : AVPlayerActionAtItemEndPause;
103
+ // restart only if video as ended
104
+ if (_complete) {
105
+ _complete = NO;
106
+ [_player.currentItem seekToTime:kCMTimeZero completionHandler:nil];
107
+ }
108
+ }
109
+
110
+ - (float)volume {
111
+ return _player.volume;
112
+ }
113
+
114
+ - (void)setVolume:(float)volume {
115
+ _player.volume =
116
+ (float)((volume < 0.0) ? 0.0 : ((volume > 1.0) ? 1.0 : volume));
117
+ }
118
+
119
+ - (float)playbackSpeed {
120
+ return _playbackSpeed;
121
+ }
122
+
123
+ - (void)setPlaybackSpeed:(float)playbackSpeed {
124
+ _playbackSpeed = MAX(0.1f, playbackSpeed);
125
+ if (_isPlaying) {
126
+ _player.rate = _playbackSpeed;
127
+ }
128
+ }
129
+
130
+ - (nullable CVPixelBufferRef)copyPixelBufferForTime:(CMTime)time {
131
+ if ([_videoOutput hasNewPixelBufferForItemTime:time]) {
132
+ // The frame retains this immutable buffer until its explicit disposal.
133
+ return [_videoOutput copyPixelBufferForItemTime:time
134
+ itemTimeForDisplay:nil];
135
+ }
136
+ return NULL;
137
+ }
138
+
139
+ - (void)seekTo:(CMTime)time
140
+ completionHandler:(void (^)(BOOL))completionHandler {
141
+ CMTime previousCMTime = _player.currentTime;
142
+ CMTime duration = _player.currentItem.asset.duration;
143
+ CMTime tolerance =
144
+ duration.value == time.value ? CMTimeMake(1, 1000) : kCMTimeZero;
145
+ __weak RNSVVideoPlayer* weakSelf = self;
146
+ [_player seekToTime:time
147
+ toleranceBefore:tolerance
148
+ toleranceAfter:tolerance
149
+ completionHandler:^(BOOL completed) {
150
+ RNSVVideoPlayer* owner = weakSelf;
151
+ if (!owner || owner->_disposed) return;
152
+ if (CMTimeCompare(owner->_player.currentTime, previousCMTime) != 0) {
153
+ // Ensure that a frame is drawn once available, even if currently
154
+ // paused. In theory a race is possible here where the new frame has
155
+ // already drawn by the time this code runs, and the display link
156
+ // stays on indefinitely, but that should be relatively harmless. This
157
+ // must use the display link rather than just informing the engine
158
+ // that a new frame is available because the seek completing doesn't
159
+ // guarantee that the pixel buffer is already available.
160
+ [owner expectFrame];
161
+ }
162
+
163
+ if (completionHandler) {
164
+ completionHandler(completed);
165
+ }
166
+ }];
167
+ }
168
+
169
+ - (void)addObserversForItem:(AVPlayerItem*)item player:(AVPlayer*)player {
170
+ [item addObserver:self
171
+ forKeyPath:@"loadedTimeRanges"
172
+ options:NSKeyValueObservingOptionInitial |
173
+ NSKeyValueObservingOptionNew
174
+ context:timeRangeContext];
175
+ [item addObserver:self
176
+ forKeyPath:@"status"
177
+ options:NSKeyValueObservingOptionInitial |
178
+ NSKeyValueObservingOptionNew
179
+ context:statusContext];
180
+ [item addObserver:self
181
+ forKeyPath:@"presentationSize"
182
+ options:NSKeyValueObservingOptionInitial |
183
+ NSKeyValueObservingOptionNew
184
+ context:presentationSizeContext];
185
+ [item addObserver:self
186
+ forKeyPath:@"duration"
187
+ options:NSKeyValueObservingOptionInitial |
188
+ NSKeyValueObservingOptionNew
189
+ context:durationContext];
190
+ [item addObserver:self
191
+ forKeyPath:@"playbackLikelyToKeepUp"
192
+ options:NSKeyValueObservingOptionInitial |
193
+ NSKeyValueObservingOptionNew
194
+ context:playbackLikelyToKeepUpContext];
195
+
196
+ // Add observer to AVPlayer instead of AVPlayerItem since the AVPlayerItem
197
+ // does not have a "rate" property
198
+ [player addObserver:self
199
+ forKeyPath:@"rate"
200
+ options:NSKeyValueObservingOptionInitial |
201
+ NSKeyValueObservingOptionNew
202
+ context:rateContext];
203
+
204
+ [[NSNotificationCenter defaultCenter]
205
+ addObserver:self
206
+ selector:@selector(itemDidPlayToEndTime:)
207
+ name:AVPlayerItemDidPlayToEndTimeNotification
208
+ object:item];
209
+ }
210
+
211
+ - (void)displayLinkFired:(CADisplayLink*)sender {
212
+ if (_disposed) return;
213
+ CMTime outputItemTime =
214
+ [_videoOutput itemTimeForHostTime:CACurrentMediaTime()];
215
+ if ([_videoOutput hasNewPixelBufferForItemTime:outputItemTime]) {
216
+ [_delegate frameAvailable:outputItemTime];
217
+ if (_waitingForFrame) {
218
+ _waitingForFrame = NO;
219
+ // If the display link was only running temporarily to pick up a new frame
220
+ // while the video was paused, stop it again.
221
+ if (!self.isPlaying) {
222
+ _displayLink.paused = YES;
223
+ }
224
+ }
225
+ }
226
+ }
227
+
228
+ - (void)itemDidPlayToEndTime:(NSNotification*)notification {
229
+ if (_disposed) return;
230
+ if (notification.object == _player.currentItem && _isLooping) {
231
+ AVPlayerItem* item = [notification object];
232
+ // [item seekToTime:kCMTimeZero completionHandler:nil];
233
+ __weak RNSVVideoPlayer* weakSelf = self;
234
+ [item seekToTime:kCMTimeZero
235
+ toleranceBefore:kCMTimeZero
236
+ toleranceAfter:kCMTimeZero
237
+ completionHandler:^(BOOL finished) {
238
+ __strong RNSVVideoPlayer* strongSelf = weakSelf;
239
+ if (!strongSelf || strongSelf->_disposed)
240
+ return;
241
+ if (finished) {
242
+ [strongSelf->_delegate frameAvailable:kCMTimeZero];
243
+ }
244
+ }];
245
+ } else {
246
+ [_delegate complete];
247
+ _complete = YES;
248
+ }
249
+ }
250
+
251
+ - (void)observeValueForKeyPath:(NSString*)path
252
+ ofObject:(id)object
253
+ change:(NSDictionary*)change
254
+ context:(void*)context {
255
+ if (_disposed) return;
256
+ if (context == timeRangeContext) {
257
+ NSArray<NSValue*>* timeRanges = [object loadedTimeRanges];
258
+ if (timeRanges != nil) {
259
+ [_delegate bufferingUpdate:[object loadedTimeRanges]];
260
+ }
261
+ } else if (context == statusContext) {
262
+ AVPlayerItem* item = (AVPlayerItem*)object;
263
+ switch (item.status) {
264
+ case AVPlayerItemStatusFailed:
265
+ [_delegate videoError:item.error];
266
+ break;
267
+ case AVPlayerItemStatusUnknown:
268
+ break;
269
+ case AVPlayerItemStatusReadyToPlay:
270
+ [item addOutput:_videoOutput];
271
+ [self dispatchReadyToPlay];
272
+ [self updatePlayingState];
273
+ break;
274
+ }
275
+ } else if (context == presentationSizeContext || context == durationContext) {
276
+ AVPlayerItem* item = (AVPlayerItem*)object;
277
+ if (item.status == AVPlayerItemStatusReadyToPlay) {
278
+ // Due to an apparent bug, when the player item is ready, it still may not
279
+ // have determined its presentation size or duration. When these
280
+ // properties are finally set, re-check if all required properties and
281
+ // instantiate the event sink if it is not already set up.
282
+ [self dispatchReadyToPlay];
283
+ [self updatePlayingState];
284
+ }
285
+ } else if (context == playbackLikelyToKeepUpContext) {
286
+ [self updatePlayingState];
287
+ if ([[_player currentItem] isPlaybackLikelyToKeepUp]) {
288
+ [_delegate bufferingEnd];
289
+ } else {
290
+ [_delegate bufferingStart];
291
+ }
292
+ } else if (context == rateContext) {
293
+ AVPlayer* player = (AVPlayer*)object;
294
+ [_delegate isPlaying:player.rate > 0];
295
+ }
296
+ }
297
+
298
+ - (void)dispatchReadyToPlay {
299
+ if (_disposed) return;
300
+ if (!_isInitialized) {
301
+ AVPlayerItem* currentItem = _player.currentItem;
302
+ CGSize resolution = CGSizeEqualToSize(self.resolution, CGSizeZero)
303
+ ? currentItem.presentationSize
304
+ : self.resolution;
305
+ CGFloat width = resolution.width;
306
+ CGFloat height = resolution.height;
307
+
308
+ // Wait until tracks are loaded to check duration or if there are any
309
+ // videos.
310
+ AVAsset* asset = currentItem.asset;
311
+ if ([asset statusOfValueForKey:@"tracks"
312
+ error:nil] != AVKeyValueStatusLoaded) {
313
+ if (_tracksLoading.exchange(true)) return;
314
+ __weak RNSVVideoPlayer* weakSelf = self;
315
+ __weak AVAsset* weakAsset = asset;
316
+ void (^trackCompletionHandler)(void) = ^{
317
+ RNSVVideoPlayer* owner = weakSelf;
318
+ if (!owner) return;
319
+ owner->_tracksLoading = false;
320
+ AVAsset* loadedAsset = weakAsset;
321
+ if (owner->_disposed || !loadedAsset || [loadedAsset statusOfValueForKey:@"tracks"
322
+ error:nil] != AVKeyValueStatusLoaded) {
323
+ // Cancelled, or something failed.
324
+ return;
325
+ }
326
+ dispatch_async(dispatch_get_main_queue(), ^{
327
+ [weakSelf dispatchReadyToPlay];
328
+ });
329
+ };
330
+ [asset loadValuesAsynchronouslyForKeys:@[ @"tracks" ]
331
+ completionHandler:trackCompletionHandler];
332
+ return;
333
+ }
334
+
335
+ NSArray* videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
336
+ NSArray* allTracks = asset.tracks;
337
+ BOOL hasVideoTracks = videoTracks.count != 0;
338
+ BOOL hasNoTracks = allTracks.count == 0;
339
+
340
+ // The player has not yet initialized when it has no size, unless it is an
341
+ // audio-only track. HLS m3u8 video files never load any tracks, and are
342
+ // also not yet initialized until they have a size.
343
+ if ((hasVideoTracks || hasNoTracks) && height == CGSizeZero.height &&
344
+ width == CGSizeZero.width) {
345
+ return;
346
+ }
347
+ AVAssetTrack* track = [videoTracks firstObject];
348
+ int rotation =
349
+ RNSkiaVideo::AVAssetTrackUtils::GetTrackRotationInDegree(track);
350
+
351
+ // The player may be initialized but still needs to determine the duration.
352
+ CMTime duration = currentItem.duration;
353
+ if (CMTIME_IS_INDEFINITE(duration) || duration.timescale == 0 ||
354
+ duration.value == 0) {
355
+ return;
356
+ }
357
+
358
+ _isInitialized = YES;
359
+ [_delegate readyToPlay:@{
360
+ @"width" : @(width),
361
+ @"height" : @(height),
362
+ @"rotation" : @(rotation)
363
+ }];
364
+ }
365
+ }
366
+
367
+ - (void)updatePlayingState {
368
+ if (!_isInitialized) {
369
+ return;
370
+ }
371
+ if (_isPlaying && _player.rate == 0) {
372
+ _player.rate = _playbackSpeed;
373
+ } else if (!_isPlaying && _player.rate != 0) {
374
+ [_player pause];
375
+ }
376
+ // If the texture is still waiting for an expected frame, the display link
377
+ // needs to keep running until it arrives regardless of the play/pause state.
378
+ _displayLink.paused = !_isPlaying && !_waitingForFrame;
379
+ }
380
+
381
+ - (void)expectFrame {
382
+ _waitingForFrame = YES;
383
+ _displayLink.paused = NO;
384
+ }
385
+
386
+ - (void)removeKeyValueObservers {
387
+ AVPlayerItem* currentItem = _player.currentItem;
388
+ [currentItem removeObserver:self forKeyPath:@"status"];
389
+ [currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
390
+ [currentItem removeObserver:self forKeyPath:@"presentationSize"];
391
+ [currentItem removeObserver:self forKeyPath:@"duration"];
392
+ [currentItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
393
+ [_player removeObserver:self forKeyPath:@"rate"];
394
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
395
+ }
396
+
397
+ - (void)dispose {
398
+ if (_disposed.exchange(true)) {
399
+ return;
400
+ }
401
+ [_player.currentItem cancelPendingSeeks];
402
+ [_player.currentItem.asset cancelLoading];
403
+ [self removeKeyValueObservers];
404
+ [_displayLink invalidate];
405
+ [_player replaceCurrentItemWithPlayerItem:nil];
406
+ _player = nil;
407
+ _displayLink = nil;
408
+ }
409
+
410
+ @end
@@ -0,0 +1,8 @@
1
+ #import <React/RCTCallInvokerModule.h>
2
+
3
+ #import "RNReactNativeSkiaVideoSpec.h"
4
+
5
+ @interface ReactNativeSkiaVideo
6
+ : NSObject <NativeReactNativeSkiaVideoSpec, RCTCallInvokerModule>
7
+
8
+ @end