react-native-skia-webgpu-video 0.0.0-stage → 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) 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/DisplayVsyncPeriod.java +72 -0
  48. package/android/src/main/java/com/azzapp/rnskv/EGLResourcesHolder.java +132 -0
  49. package/android/src/main/java/com/azzapp/rnskv/EGLUtils.java +225 -0
  50. package/android/src/main/java/com/azzapp/rnskv/FrameSize.java +50 -0
  51. package/android/src/main/java/com/azzapp/rnskv/GLFrameExtractor.java +510 -0
  52. package/android/src/main/java/com/azzapp/rnskv/JNIHelpers.java +12 -0
  53. package/android/src/main/java/com/azzapp/rnskv/NativeEventDispatcher.java +25 -0
  54. package/android/src/main/java/com/azzapp/rnskv/NativeHardwareBuffer.java +62 -0
  55. package/android/src/main/java/com/azzapp/rnskv/NativeRgbaBuffer.java +43 -0
  56. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoModule.java +45 -0
  57. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoPackage.java +43 -0
  58. package/android/src/main/java/com/azzapp/rnskv/ReactNativeSkiaVideoSpec.java +9 -0
  59. package/android/src/main/java/com/azzapp/rnskv/RgbaBufferAllocator.java +114 -0
  60. package/android/src/main/java/com/azzapp/rnskv/RgbaLayout.java +33 -0
  61. package/android/src/main/java/com/azzapp/rnskv/SurfaceOutputPolicy.java +13 -0
  62. package/android/src/main/java/com/azzapp/rnskv/TextureRenderer.java +150 -0
  63. package/android/src/main/java/com/azzapp/rnskv/TimeHelpers.java +11 -0
  64. package/android/src/main/java/com/azzapp/rnskv/VideoCapabilities.java +354 -0
  65. package/android/src/main/java/com/azzapp/rnskv/VideoComposition.java +147 -0
  66. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionDecoder.java +779 -0
  67. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractor.java +463 -0
  68. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionFramesExtractorSync.java +241 -0
  69. package/android/src/main/java/com/azzapp/rnskv/VideoCompositionItemDecoder.java +492 -0
  70. package/android/src/main/java/com/azzapp/rnskv/VideoEncoder.java +639 -0
  71. package/android/src/main/java/com/azzapp/rnskv/VideoFrame.java +74 -0
  72. package/android/src/main/java/com/azzapp/rnskv/VideoPlayer.java +361 -0
  73. package/cpp/RNSVCheckedSizes.h +54 -0
  74. package/cpp/RNSVEventEmitter.cpp +199 -0
  75. package/cpp/RNSVEventEmitter.h +31 -0
  76. package/cpp/RNSVHostObject.cpp +79 -0
  77. package/cpp/RNSVHostObject.h +67 -0
  78. package/cpp/RNSVMemoryBudget.cpp +68 -0
  79. package/cpp/RNSVMemoryBudget.h +100 -0
  80. package/cpp/RNSVRuntimeLifecycleMonitor.cpp +88 -0
  81. package/cpp/RNSVRuntimeLifecycleMonitor.h +33 -0
  82. package/docs/ANDROID_DEVICE_306.md +95 -0
  83. package/docs/ANDROID_EXPORT_CACHE_306.md +48 -0
  84. package/docs/ANDROID_MEMORY_RETENTION_306.md +67 -0
  85. package/docs/BENCHMARKS.md +368 -0
  86. package/docs/IOS_DEVICE_TESTS.md +115 -0
  87. package/docs/IOS_SIMULATOR_TESTS.md +74 -0
  88. package/docs/MIGRATION.md +82 -0
  89. package/docs/NATIVE_MEMORY_INVESTIGATION_306.md +88 -0
  90. package/docs/PERFORMANCE_AB_IPHONE_303.md +196 -0
  91. package/docs/PERFORMANCE_AB_SIMULATOR.md +135 -0
  92. package/docs/PERFORMANCE_AB_SOAK_SIMULATOR.md +84 -0
  93. package/docs/PERFORMANCE_IPHONE_306_OPTIMIZATION.md +76 -0
  94. package/docs/PERFORMANCE_SIMULATOR.md +89 -0
  95. package/docs/PERFORMANCE_SIMULATOR_INITIAL.md +96 -0
  96. package/docs/PERFORMANCE_SOAK_IPHONE_303.md +41 -0
  97. package/docs/UPSTREAM_NOTES.md +188 -0
  98. package/docs/assets/banner.svg +26 -0
  99. package/examples/webgpu-overlay.ts +147 -0
  100. package/ios/AVAssetTrackUtils.h +24 -0
  101. package/ios/AudioCompositionUtils.h +27 -0
  102. package/ios/AudioCompositionUtils.mm +113 -0
  103. package/ios/DecoderWindow.h +52 -0
  104. package/ios/NativeResourceStats.h +32 -0
  105. package/ios/RNSVColorSpace.h +50 -0
  106. package/ios/RNSVJSIUtils.h +46 -0
  107. package/ios/RNSVScopedPixelBuffer.h +17 -0
  108. package/ios/RNSVVideoPlayer.h +46 -0
  109. package/ios/RNSVVideoPlayer.mm +410 -0
  110. package/ios/ReactNativeSkiaVideo.h +8 -0
  111. package/ios/ReactNativeSkiaVideo.mm +310 -0
  112. package/ios/SeekPolicy.h +27 -0
  113. package/ios/VideoComposition.h +148 -0
  114. package/ios/VideoCompositionFramesExtractorHostObject.h +105 -0
  115. package/ios/VideoCompositionFramesExtractorHostObject.mm +686 -0
  116. package/ios/VideoCompositionFramesExtractorSyncHostObject.h +37 -0
  117. package/ios/VideoCompositionFramesExtractorSyncHostObject.mm +168 -0
  118. package/ios/VideoCompositionItemDecoder.h +60 -0
  119. package/ios/VideoCompositionItemDecoder.mm +480 -0
  120. package/ios/VideoEncoderHostObject.h +83 -0
  121. package/ios/VideoEncoderHostObject.mm +706 -0
  122. package/ios/VideoFrame.h +66 -0
  123. package/ios/VideoFrame.mm +108 -0
  124. package/ios/VideoPlayerHostObject.h +52 -0
  125. package/ios/VideoPlayerHostObject.mm +332 -0
  126. package/lib/module/NativeReactNativeSkiaVideo.js +5 -0
  127. package/lib/module/NativeReactNativeSkiaVideo.js.map +1 -0
  128. package/lib/module/RNSkiaVideoModule.js +20 -0
  129. package/lib/module/RNSkiaVideoModule.js.map +1 -0
  130. package/lib/module/canvas.js +18 -0
  131. package/lib/module/canvas.js.map +1 -0
  132. package/lib/module/exportCache.js +22 -0
  133. package/lib/module/exportCache.js.map +1 -0
  134. package/lib/module/exportFrames.js +338 -0
  135. package/lib/module/exportFrames.js.map +1 -0
  136. package/lib/module/exportQueue.js +52 -0
  137. package/lib/module/exportQueue.js.map +1 -0
  138. package/lib/module/exportTarget.js +137 -0
  139. package/lib/module/exportTarget.js.map +1 -0
  140. package/lib/module/exportVideoComposition.js +492 -0
  141. package/lib/module/exportVideoComposition.js.map +1 -0
  142. package/lib/module/frameInterop.js +532 -0
  143. package/lib/module/frameInterop.js.map +1 -0
  144. package/lib/module/gpu.js +222 -0
  145. package/lib/module/gpu.js.map +1 -0
  146. package/lib/module/gpuDevice.js +54 -0
  147. package/lib/module/gpuDevice.js.map +1 -0
  148. package/lib/module/index.js +36 -0
  149. package/lib/module/index.js.map +1 -0
  150. package/lib/module/memory.js +38 -0
  151. package/lib/module/memory.js.map +1 -0
  152. package/lib/module/package.json +1 -0
  153. package/lib/module/previewLifecycle.js +52 -0
  154. package/lib/module/previewLifecycle.js.map +1 -0
  155. package/lib/module/runtimeYield.js +10 -0
  156. package/lib/module/runtimeYield.js.map +1 -0
  157. package/lib/module/types.js +4 -0
  158. package/lib/module/types.js.map +1 -0
  159. package/lib/module/utils/useEventListener.js +18 -0
  160. package/lib/module/utils/useEventListener.js.map +1 -0
  161. package/lib/module/videoCompositionPlayer.js +368 -0
  162. package/lib/module/videoCompositionPlayer.js.map +1 -0
  163. package/lib/module/videoFrameImage.js +185 -0
  164. package/lib/module/videoFrameImage.js.map +1 -0
  165. package/lib/module/videoPlayer.js +172 -0
  166. package/lib/module/videoPlayer.js.map +1 -0
  167. package/lib/module/yuvBlit.js +299 -0
  168. package/lib/module/yuvBlit.js.map +1 -0
  169. package/lib/typescript/package.json +1 -0
  170. package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts +7 -0
  171. package/lib/typescript/src/NativeReactNativeSkiaVideo.d.ts.map +1 -0
  172. package/lib/typescript/src/RNSkiaVideoModule.d.ts +4 -0
  173. package/lib/typescript/src/RNSkiaVideoModule.d.ts.map +1 -0
  174. package/lib/typescript/src/canvas.d.ts +4 -0
  175. package/lib/typescript/src/canvas.d.ts.map +1 -0
  176. package/lib/typescript/src/exportCache.d.ts +6 -0
  177. package/lib/typescript/src/exportCache.d.ts.map +1 -0
  178. package/lib/typescript/src/exportFrames.d.ts +57 -0
  179. package/lib/typescript/src/exportFrames.d.ts.map +1 -0
  180. package/lib/typescript/src/exportQueue.d.ts +5 -0
  181. package/lib/typescript/src/exportQueue.d.ts.map +1 -0
  182. package/lib/typescript/src/exportTarget.d.ts +28 -0
  183. package/lib/typescript/src/exportTarget.d.ts.map +1 -0
  184. package/lib/typescript/src/exportVideoComposition.d.ts +68 -0
  185. package/lib/typescript/src/exportVideoComposition.d.ts.map +1 -0
  186. package/lib/typescript/src/frameInterop.d.ts +17 -0
  187. package/lib/typescript/src/frameInterop.d.ts.map +1 -0
  188. package/lib/typescript/src/gpu.d.ts +39 -0
  189. package/lib/typescript/src/gpu.d.ts.map +1 -0
  190. package/lib/typescript/src/gpuDevice.d.ts +11 -0
  191. package/lib/typescript/src/gpuDevice.d.ts.map +1 -0
  192. package/lib/typescript/src/index.d.ts +21 -0
  193. package/lib/typescript/src/index.d.ts.map +1 -0
  194. package/lib/typescript/src/memory.d.ts +25 -0
  195. package/lib/typescript/src/memory.d.ts.map +1 -0
  196. package/lib/typescript/src/previewLifecycle.d.ts +22 -0
  197. package/lib/typescript/src/previewLifecycle.d.ts.map +1 -0
  198. package/lib/typescript/src/runtimeYield.d.ts +4 -0
  199. package/lib/typescript/src/runtimeYield.d.ts.map +1 -0
  200. package/lib/typescript/src/types.d.ts +631 -0
  201. package/lib/typescript/src/types.d.ts.map +1 -0
  202. package/lib/typescript/src/utils/useEventListener.d.ts +5 -0
  203. package/lib/typescript/src/utils/useEventListener.d.ts.map +1 -0
  204. package/lib/typescript/src/videoCompositionPlayer.d.ts +105 -0
  205. package/lib/typescript/src/videoCompositionPlayer.d.ts.map +1 -0
  206. package/lib/typescript/src/videoFrameImage.d.ts +60 -0
  207. package/lib/typescript/src/videoFrameImage.d.ts.map +1 -0
  208. package/lib/typescript/src/videoPlayer.d.ts +99 -0
  209. package/lib/typescript/src/videoPlayer.d.ts.map +1 -0
  210. package/lib/typescript/src/yuvBlit.d.ts +50 -0
  211. package/lib/typescript/src/yuvBlit.d.ts.map +1 -0
  212. package/package.json +155 -4
  213. package/patches/react-native-skia+3.7.0.patch +502 -0
  214. package/react-native-skia-webgpu-video.podspec +35 -0
  215. package/scripts/apply-skia-canvas-patch.mjs +48 -0
  216. package/scripts/skia-canvas-patch.json +291 -0
  217. package/src/NativeReactNativeSkiaVideo.ts +8 -0
  218. package/src/RNSkiaVideoModule.ts +27 -0
  219. package/src/canvas.ts +22 -0
  220. package/src/exportCache.ts +27 -0
  221. package/src/exportFrames.ts +421 -0
  222. package/src/exportQueue.ts +65 -0
  223. package/src/exportTarget.ts +179 -0
  224. package/src/exportVideoComposition.ts +718 -0
  225. package/src/frameInterop.ts +684 -0
  226. package/src/gpu.ts +290 -0
  227. package/src/gpuDevice.ts +80 -0
  228. package/src/index.tsx +45 -0
  229. package/src/memory.ts +48 -0
  230. package/src/previewLifecycle.ts +60 -0
  231. package/src/runtimeYield.ts +6 -0
  232. package/src/types.ts +661 -0
  233. package/src/utils/useEventListener.ts +22 -0
  234. package/src/videoCompositionPlayer.ts +568 -0
  235. package/src/videoFrameImage.ts +205 -0
  236. package/src/videoPlayer.ts +318 -0
  237. package/src/yuvBlit.ts +331 -0
@@ -0,0 +1,463 @@
1
+ package com.azzapp.rnskv;
2
+
3
+ import android.app.Activity;
4
+ import android.os.Handler;
5
+ import android.os.HandlerThread;
6
+ import android.os.Message;
7
+ import android.os.Process;
8
+ import android.os.SystemClock;
9
+ import android.util.Log;
10
+ import android.view.Choreographer;
11
+
12
+ import androidx.media3.common.util.UnstableApi;
13
+
14
+ import com.facebook.react.bridge.ReactApplicationContext;
15
+
16
+ import java.io.IOException;
17
+ import java.util.Map;
18
+ import java.util.concurrent.atomic.AtomicBoolean;
19
+ import javax.microedition.khronos.egl.EGLContext;
20
+ import javax.microedition.khronos.egl.EGL10;
21
+
22
+ /**
23
+ * A class that previews a video composition.
24
+ */
25
+ @UnstableApi
26
+ public class VideoCompositionFramesExtractor {
27
+ private static final String TAG = "VideoCompositionFramesExtractor";
28
+
29
+ private static final int PLAYBACK_PREPARE = 1;
30
+ private static final int PLAYBACK_PLAY = 2;
31
+ private static final int PLAYBACK_PAUSE = 3;
32
+ private static final int PLAYBACK_LOOP = 4;
33
+ private static final int PLAYBACK_SEEK = 5;
34
+ private static final int PLAYBACK_RELEASE = 6;
35
+
36
+ private final VideoComposition composition;
37
+
38
+ private final VideoCompositionDecoder decoder;
39
+
40
+ private AudioCompositionPlayer audioPlayer;
41
+
42
+ private final PlaybackThread playbackThread;
43
+
44
+ private final Handler handler;
45
+
46
+ private final NativeEventDispatcher eventDispatcher;
47
+
48
+ private boolean playWhenReady = false;
49
+
50
+ private boolean prepared;
51
+ private boolean releasing;
52
+ private boolean looping;
53
+
54
+
55
+ private long pendingSeek = 0;
56
+ // Read on the thread that collects frames (see releaseForNextVsync), written on the playback thread.
57
+ private volatile boolean isPlaying = false;
58
+
59
+ private volatile long startTime = 0;
60
+ private volatile long pausePosition = 0;
61
+ private boolean isEOS = false;
62
+ private boolean completeDispatched = false;
63
+
64
+ // Vsync-aligned release, written and read on the thread that collects frames.
65
+ private long lastVsyncNanos = 0;
66
+ private long recordedVsyncNanos = 0;
67
+ private boolean recordingVsync = false;
68
+ /** Notes each vsync's timestamp on the collecting thread, for as long as the preview lives. */
69
+ private final Choreographer.FrameCallback vsyncRecorder = new Choreographer.FrameCallback() {
70
+ @Override
71
+ public void doFrame(long frameTimeNanos) {
72
+ if (releasing) {
73
+ unwatchDisplay();
74
+ return;
75
+ }
76
+ recordedVsyncNanos = frameTimeNanos;
77
+ Choreographer.getInstance().postFrameCallback(this);
78
+ }
79
+ };
80
+ // The display the preview is on; null until the first collection, or when unknown.
81
+ private DisplayVsyncPeriod vsyncPeriod;
82
+ // When that thread last released frames; the playback loop stands back meanwhile.
83
+ private volatile long lastVsyncReleaseNanos = 0;
84
+ // The release itself runs on the playback thread: the decoder's locks are held
85
+ // there for whole codec flushes on a seek, and the UI thread must never
86
+ // wait on them (measured: 65 ms frames when it did).
87
+ private volatile long vsyncReleasePosition = 0;
88
+ private final AtomicBoolean vsyncReleasePosted = new AtomicBoolean(false);
89
+ private final Runnable vsyncRelease = this::runVsyncRelease;
90
+
91
+ private void runVsyncRelease() {
92
+ vsyncReleasePosted.set(false);
93
+ if (!releasing) decoder.render(vsyncReleasePosition);
94
+ }
95
+ private static final long VSYNC_RELEASE_TIMEOUT_NANOS = 100_000_000L;
96
+
97
+ /**
98
+ * Create a new VideoCompositionFramesExtractor.
99
+ *
100
+ * @param composition the video composition to preview
101
+ */
102
+ public VideoCompositionFramesExtractor(VideoComposition composition, NativeEventDispatcher eventDispatcher) {
103
+ this.eventDispatcher = eventDispatcher;
104
+ this.composition = composition;
105
+ decoder = new VideoCompositionDecoder(composition, true);
106
+ // Only a decoder that cannot open, as on iOS: a codec's passing errors
107
+ // stay quiet, since the player gives up on the first error it reports.
108
+ decoder.setOnOpenErrorListener(
109
+ error -> eventDispatcher.dispatchEvent("error", messageOf(error)));
110
+ playbackThread = new PlaybackThread();
111
+ playbackThread.start();
112
+ handler = new Handler(playbackThread.getLooper(), playbackThread);
113
+ }
114
+
115
+ public void prepare() {
116
+ if (prepared) {
117
+ return;
118
+ }
119
+ try {
120
+ decoder.prepare(EGL10.EGL_NO_CONTEXT);
121
+ } catch (RuntimeException error) {
122
+ // A decoder that cannot open, as a lazy one reports it: thrown, it
123
+ // reached the UI thread's worklet uncaught and killed the app.
124
+ eventDispatcher.dispatchEvent("error", messageOf(error));
125
+ return;
126
+ }
127
+ handler.sendEmptyMessage(PLAYBACK_PREPARE);
128
+ }
129
+
130
+ /**
131
+ * Start playing the composition.
132
+ */
133
+ public void play() {
134
+ handler.sendEmptyMessage(PLAYBACK_PLAY);
135
+ }
136
+
137
+ /**
138
+ * Pause the composition.
139
+ */
140
+ public void pause() {
141
+ handler.sendEmptyMessage(PLAYBACK_PAUSE);
142
+ }
143
+
144
+ /**
145
+ * Seek to given position
146
+ */
147
+ public void seekTo(long position) {
148
+ handler.removeMessages(PLAYBACK_SEEK);
149
+ handler.obtainMessage(PLAYBACK_SEEK, position).sendToTarget();
150
+ }
151
+
152
+ /**
153
+ * Decode the next frame of each composition item according to the current position of the player.
154
+ *
155
+ * @return an independently retained snapshot map; callers close its frames
156
+ * when finished, without affecting the producer or other snapshots
157
+ */
158
+ public Map<String, VideoFrame> decodeCompositionFrames() {
159
+ Map<String, VideoFrame> frames = decoder.updateVideosFrames();
160
+ releaseForNextVsync();
161
+ return frames;
162
+ }
163
+
164
+ /**
165
+ * Releases the decoded frames the next vsync is due to show, from the thread
166
+ * that collects them, right after collecting.
167
+ *
168
+ * The playback loop released them on its 10 ms tick, out of phase with the
169
+ * display, so whether a frame was collected on the vsync after its time or
170
+ * the one after that was chance: 24 fps on a 60 Hz display came out in
171
+ * 17/67 ms steps among the 33/50 ms it implies. Released against the vsync's
172
+ * own timestamp, one vsync ahead, every frame is collected on the first
173
+ * vsync at or after its time. The position is decided here, against the
174
+ * vsync; the release is posted to the playback thread, a vsync ahead of the
175
+ * collection that needs it.
176
+ */
177
+ private void releaseForNextVsync() {
178
+ if (!recordingVsync) {
179
+ try {
180
+ // The collecting thread is a Choreographer thread (the UI thread).
181
+ Choreographer.getInstance().postFrameCallback(vsyncRecorder);
182
+ recordingVsync = true;
183
+ } catch (RuntimeException e) {
184
+ // No looper on this thread: leave the release to the playback loop.
185
+ return;
186
+ }
187
+ watchDisplay();
188
+ return;
189
+ }
190
+ long period = vsyncPeriod != null ? vsyncPeriod.getPeriodNanos() : 0;
191
+ if (period == 0 || recordedVsyncNanos == 0) return;
192
+ // This frame's vsync, whether the recorder has run in this frame yet or not.
193
+ long since = Math.max(0, System.nanoTime() - recordedVsyncNanos);
194
+ long vsyncNanos = recordedVsyncNanos + (since / period) * period;
195
+ if (vsyncNanos == lastVsyncNanos) return;
196
+ lastVsyncNanos = vsyncNanos;
197
+ long vsyncPeriodNanos = period;
198
+ long position = isPlaying
199
+ ? (vsyncNanos + vsyncPeriodNanos) / 1000 - startTime
200
+ : pausePosition;
201
+ lastVsyncReleaseNanos = System.nanoTime();
202
+ vsyncReleasePosition = position;
203
+ if (vsyncReleasePosted.compareAndSet(false, true)) {
204
+ handler.post(vsyncRelease);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Follows the refresh rate of the display the preview is on, from the
210
+ * collecting thread, whose looper then receives its changes. Without a React
211
+ * context (instrumented tests) the period stays unknown and the playback loop
212
+ * releases on its tick.
213
+ */
214
+ private void watchDisplay() {
215
+ ReactApplicationContext context = ReactNativeSkiaVideoModule.currentReactApplicationContext();
216
+ if (context == null) return;
217
+ Activity activity = context.getCurrentActivity();
218
+ vsyncPeriod = DisplayVsyncPeriod.watch(context,
219
+ activity != null ? activity.getWindowManager().getDefaultDisplay() : null);
220
+ }
221
+
222
+ private void unwatchDisplay() {
223
+ if (vsyncPeriod != null) {
224
+ vsyncPeriod.release();
225
+ vsyncPeriod = null;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Makes the decoder's GL context current on the calling thread, the one that
231
+ * calls {@link #decodeCompositionFrames}.
232
+ */
233
+ public void makeGLContextCurrent() {
234
+ decoder.makeGLContextCurrent();
235
+ }
236
+
237
+ /**
238
+ * @return the current position of the player in microseconds
239
+ */
240
+ public long getCurrentPosition() {
241
+ return isPlaying ? microTime() - startTime : pausePosition;
242
+ }
243
+
244
+ /**
245
+ * @return the version of the frames returned by {@link #decodeCompositionFrames},
246
+ * see {@link VideoCompositionDecoder#getFramesVersion()}
247
+ */
248
+ public long getFramesVersion() {
249
+ return decoder.getFramesVersion();
250
+ }
251
+
252
+ /**
253
+ * @return whether the player is looping
254
+ */
255
+ public boolean getIsLooping() {
256
+ return looping;
257
+ }
258
+
259
+ /**
260
+ * Set whether the player should loop.
261
+ */
262
+ public void setIsLooping(boolean value) {
263
+ looping = value;
264
+ if (prepared && isEOS && value) {
265
+ play();
266
+ }
267
+ }
268
+
269
+ /**
270
+ * @return whether the player is currently playing
271
+ */
272
+ public boolean getIsPlaying() {
273
+ return isPlaying;
274
+ }
275
+
276
+ public void release() {
277
+ if (!playbackThread.isAlive()) {
278
+ decoder.release();
279
+ return;
280
+ }
281
+ isPlaying = false;
282
+ releasing = true;
283
+ handler.sendEmptyMessage(PLAYBACK_RELEASE);
284
+ }
285
+
286
+ private void prepareInternal() {
287
+ decoder.start();
288
+ if (composition.hasAudio()) {
289
+ audioPlayer = new AudioCompositionPlayer(composition);
290
+ audioPlayer.setOnErrorListener(
291
+ message -> eventDispatcher.dispatchEvent("error", message));
292
+ audioPlayer.prepare();
293
+ }
294
+ prepared = true;
295
+ eventDispatcher.dispatchEvent("ready", null);
296
+ handler.sendEmptyMessage(PLAYBACK_LOOP);
297
+ if (pendingSeek != 0) {
298
+ seekInternal(pendingSeek);
299
+ }
300
+ if (playWhenReady) {
301
+ playInternal();
302
+ }
303
+ }
304
+
305
+ private void playInternal() {
306
+ if (!prepared) {
307
+ playWhenReady = true;
308
+ return;
309
+ }
310
+ if (isEOS) {
311
+ isEOS = false;
312
+ pausePosition = 0;
313
+ seekInternal(0);
314
+ }
315
+ startTime = microTime() - pausePosition;
316
+ isPlaying = true;
317
+ pausePosition = 0;
318
+ }
319
+
320
+ private void pauseInternal() {
321
+ if (!isPlaying) {
322
+ playWhenReady = false;
323
+ return;
324
+ }
325
+ pausePosition = getCurrentPosition();
326
+ isPlaying = false;
327
+ if (audioPlayer != null) {
328
+ audioPlayer.pause();
329
+ }
330
+ }
331
+
332
+ private void loopInternal() throws IOException, InterruptedException {
333
+ long loopStartTime = SystemClock.elapsedRealtime();
334
+
335
+ long currentPosition = getCurrentPosition();
336
+
337
+ isEOS = currentPosition >= TimeHelpers.secToUs(composition.getDuration());
338
+ if (isEOS) {
339
+ if (!completeDispatched) {
340
+ completeDispatched = true;
341
+ eventDispatcher.dispatchEvent("complete", null);
342
+ }
343
+ isPlaying = false;
344
+ pausePosition = TimeHelpers.secToUs(composition.getDuration());
345
+ currentPosition = pausePosition;
346
+ } else {
347
+ completeDispatched = false;
348
+ }
349
+ decoder.updateWindow(currentPosition);
350
+ if (System.nanoTime() - lastVsyncReleaseNanos > VSYNC_RELEASE_TIMEOUT_NANOS) {
351
+ // No one collecting at vsync (not yet, or no longer): release on the tick.
352
+ decoder.render(currentPosition);
353
+ }
354
+ if (isEOS && looping) {
355
+ playInternal();
356
+ }
357
+ if (audioPlayer != null) {
358
+ audioPlayer.update(getCurrentPosition(), isPlaying);
359
+ }
360
+ long delay = 10;
361
+ long duration = (SystemClock.elapsedRealtime() - loopStartTime);
362
+ delay = delay - duration;
363
+ if (delay > 0) {
364
+ handler.sendEmptyMessageDelayed(PLAYBACK_LOOP, delay);
365
+ } else {
366
+ handler.sendEmptyMessage(PLAYBACK_LOOP);
367
+ }
368
+ }
369
+
370
+ private void seekInternal(long position) {
371
+ if (!prepared) {
372
+ pendingSeek = position;
373
+ return;
374
+ }
375
+ decoder.seekTo(position);
376
+ // After the seek, so a decoder opened for this position is not sought again.
377
+ decoder.updateWindow(position);
378
+ if (audioPlayer != null) {
379
+ audioPlayer.seekTo(position);
380
+ }
381
+ if (isPlaying) {
382
+ startTime = microTime() - position;
383
+ } else {
384
+ pausePosition = position;
385
+ }
386
+ }
387
+
388
+ private void releaseInternal() {
389
+ if (audioPlayer != null) {
390
+ try {
391
+ audioPlayer.release();
392
+ } catch (Exception e) {
393
+ Log.w(TAG, "Could not release the audio players", e);
394
+ }
395
+ audioPlayer = null;
396
+ }
397
+ playbackThread.interrupt();
398
+ playbackThread.quit();
399
+ decoder.release();
400
+ }
401
+
402
+ private class PlaybackThread extends HandlerThread implements Handler.Callback {
403
+ public PlaybackThread() {
404
+ super(TAG + PlaybackThread.class.getSimpleName(), Process.THREAD_PRIORITY_VIDEO);
405
+ }
406
+
407
+ @Override
408
+ public boolean handleMessage(Message msg) {
409
+ try {
410
+ if (releasing) {
411
+ // When the releasing flag is set, just release without processing any more messages
412
+ releaseInternal();
413
+ return true;
414
+ }
415
+
416
+ switch (msg.what) {
417
+ case PLAYBACK_PREPARE -> {
418
+ prepareInternal();
419
+ return true;
420
+ }
421
+ case PLAYBACK_PLAY -> {
422
+ playInternal();
423
+ return true;
424
+ }
425
+ case PLAYBACK_PAUSE -> {
426
+ pauseInternal();
427
+ return true;
428
+ }
429
+ case PLAYBACK_LOOP -> {
430
+ loopInternal();
431
+ return true;
432
+ }
433
+ case PLAYBACK_SEEK -> {
434
+ seekInternal((Long) msg.obj);
435
+ return true;
436
+ }
437
+ case PLAYBACK_RELEASE -> {
438
+ releaseInternal();
439
+ return true;
440
+ }
441
+ default -> {
442
+ return false;
443
+ }
444
+ }
445
+ } catch (Exception error) {
446
+ eventDispatcher.dispatchEvent("error", messageOf(error));
447
+ }
448
+
449
+ // Release after an exception
450
+ releaseInternal();
451
+ return true;
452
+ }
453
+ }
454
+
455
+ // Never null: the native side reads the message as a string.
456
+ private static String messageOf(Exception error) {
457
+ return error.getMessage() != null ? error.getMessage() : error.toString();
458
+ }
459
+
460
+ private static long microTime() {
461
+ return System.nanoTime() / 1000;
462
+ }
463
+ }
@@ -0,0 +1,241 @@
1
+ package com.azzapp.rnskv;
2
+
3
+ import android.opengl.GLES20;
4
+ import android.os.Handler;
5
+ import android.os.HandlerThread;
6
+
7
+ import java.util.HashMap;
8
+ import java.util.HashSet;
9
+ import java.util.Map;
10
+ import java.util.Set;
11
+ import java.util.concurrent.CompletableFuture;
12
+ import java.util.concurrent.TimeUnit;
13
+
14
+ import javax.microedition.khronos.egl.EGLContext;
15
+ import javax.microedition.khronos.egl.EGL10;
16
+
17
+ public class VideoCompositionFramesExtractorSync {
18
+ private final VideoComposition composition;
19
+
20
+ private final VideoCompositionDecoder decoder;
21
+
22
+ private boolean decoding = false;
23
+ private long decodingTimeUs;
24
+
25
+ private final Map<VideoComposition.Item, Long> itemsTimes = new HashMap<>();
26
+
27
+ private final Set<VideoComposition.Item> itemsEnded = new HashSet<>();
28
+
29
+ private final Map<String, Long> renderedTimes = new HashMap<>();
30
+
31
+ private HandlerThread exportThread = null;
32
+
33
+ private Handler handler;
34
+
35
+ private static final long DECODE_TIMEOUT_MS = 30000;
36
+ private final DecodeRequests<Map<String, VideoFrame>> requests =
37
+ new DecodeRequests<>(VideoCompositionDecoder::closeFrames);
38
+ private volatile CompletableFuture<Void> startup;
39
+
40
+ public VideoCompositionFramesExtractorSync(VideoComposition composition) {
41
+ this.composition = composition;
42
+ this.decoder = new VideoCompositionDecoder(composition, false);
43
+ }
44
+
45
+ public void start() throws Exception {
46
+ final CompletableFuture<Void> initializing;
47
+ synchronized (this) {
48
+ if (requests.isClosed()) throw new IllegalStateException("Video decoder has been released");
49
+ if (startup == null) {
50
+ exportThread = new HandlerThread("ReactNativeSkiaVideo-ExportThread");
51
+ exportThread.start();
52
+ handler = new Handler(exportThread.getLooper());
53
+ startup = new CompletableFuture<>();
54
+ final CompletableFuture<Void> started = startup;
55
+ if (!handler.post(() -> {
56
+ try {
57
+ if (requests.isClosed()) throw new IllegalStateException("Video decoder has been released");
58
+ decoder.prepare(EGL10.EGL_NO_CONTEXT);
59
+ decoder.setOnErrorListener(this::handleError);
60
+ decoder.setOnFrameAvailableListener(this::onFrameAvailable);
61
+ decoder.setOnItemEndReachedListener(this::onItemEndReached);
62
+ decoder.setOnItemImageAvailableListener(this::onItemImageAvailable);
63
+ decoder.start();
64
+ started.complete(null);
65
+ } catch (Exception error) {
66
+ handleError(error);
67
+ started.completeExceptionally(error);
68
+ }
69
+ })) started.completeExceptionally(new IllegalStateException("Export looper rejected startup"));
70
+ }
71
+ initializing = startup;
72
+ }
73
+ try { initializing.get(DECODE_TIMEOUT_MS, TimeUnit.MILLISECONDS); }
74
+ catch (Exception error) { release(); throw error; }
75
+ }
76
+
77
+ /**
78
+ * Decode the next frame of each composition item according to the current position of the player.
79
+ *
80
+ * @return a map of item id to video frame
81
+ */
82
+ public Map<String, VideoFrame> decodeCompositionFrames(double time) throws Exception {
83
+ final CompletableFuture<Map<String, VideoFrame>> request;
84
+ final long targetUs = TimeHelpers.secToUs(time);
85
+ synchronized (this) {
86
+ if (handler == null || startup == null || !startup.isDone()) {
87
+ throw new IllegalStateException("Video decoder is not started");
88
+ }
89
+ request = requests.begin();
90
+ if (!handler.post(() -> {
91
+ if (requests.isClosed()) return;
92
+ try {
93
+ decodingTimeUs = targetUs;
94
+ decoder.updateWindow(decodingTimeUs);
95
+ forgetClosedItems();
96
+ decoding = true;
97
+ renderedTimes.clear();
98
+ checkIfFrameDecoded();
99
+ } catch (Exception error) { handleError(error); }
100
+ })) requests.fail(new IllegalStateException("Export looper rejected decode request"));
101
+ }
102
+ try { return requests.await(request, DECODE_TIMEOUT_MS); }
103
+ catch (Exception error) { release(); throw error; }
104
+ }
105
+
106
+ public void release() {
107
+ final Handler ownedHandler;
108
+ final HandlerThread ownedThread;
109
+ synchronized (this) {
110
+ if (requests.isClosed() && handler == null) return;
111
+ requests.close();
112
+ if (startup != null) startup.cancel(true);
113
+ ownedHandler = handler;
114
+ ownedThread = exportThread;
115
+ handler = null;
116
+ exportThread = null;
117
+ }
118
+ if (ownedHandler != null) {
119
+ // The decoder's EGL context and GL resources are bound to the export
120
+ // thread (where they are current); releasing them from another thread
121
+ // would silently fail the GL deletes and defer the EGL context
122
+ // destruction. Release on the export thread, then let the looper
123
+ // drain and quit.
124
+ if (!ownedHandler.post(decoder::release)) decoder.release();
125
+ if (ownedThread != null) ownedThread.quitSafely();
126
+ } else {
127
+ decoder.release();
128
+ if (ownedThread != null) ownedThread.quit();
129
+ }
130
+ }
131
+
132
+ /**
133
+ * With lazy decoders, an item reopened later must wait for its own frames.
134
+ */
135
+ private void forgetClosedItems() {
136
+ for (VideoComposition.Item item : composition.getItems()) {
137
+ if (item.isVideo() && !decoder.isOpen(item)) {
138
+ itemsTimes.remove(item);
139
+ itemsEnded.remove(item);
140
+ }
141
+ }
142
+ }
143
+
144
+ private void onFrameAvailable(VideoComposition.Item item, long presentationTimeUs) {
145
+ if (requests.isClosed()) return;
146
+ try {
147
+ itemsTimes.put(item, presentationTimeUs);
148
+ if (decoding) checkIfFrameDecoded();
149
+ } catch (Exception error) { handleError(error); }
150
+ }
151
+
152
+ private void onItemEndReached(VideoComposition.Item item) {
153
+ if (requests.isClosed()) return;
154
+ try {
155
+ itemsEnded.add(item);
156
+ if (decoding) checkIfFrameDecoded();
157
+ } catch (Exception error) { handleError(error); }
158
+ }
159
+
160
+ private void checkIfFrameDecoded() {
161
+ boolean allItemsReady = true;
162
+ for (VideoComposition.Item item : composition.getItems()) {
163
+ if (!item.isVideo() || !decoder.isOpen(item)) {
164
+ continue;
165
+ }
166
+ // Before the frame check: an item that ends without a single frame in
167
+ // its range (a start past the file's end) would otherwise hold the
168
+ // export forever.
169
+ if (itemsEnded.contains(item)) {
170
+ continue;
171
+ }
172
+ if (!itemsTimes.containsKey(item)) {
173
+ allItemsReady = false;
174
+ continue;
175
+ }
176
+ Long itemTime = itemsTimes.get(item);
177
+ if (itemTime == null) {
178
+ allItemsReady = false;
179
+ continue;
180
+ }
181
+ long itemCurrentTimeUs = itemTime;
182
+ long startTimeUs = TimeHelpers.secToUs(item.getStartTime());
183
+ long compositionStartTimeUs = TimeHelpers.secToUs(item.getCompositionStartTime());
184
+ if (itemCurrentTimeUs - startTimeUs < decodingTimeUs - compositionStartTimeUs) {
185
+ allItemsReady = false;
186
+ }
187
+ }
188
+ Map<String, Long> renderedTimes = decoder.render(decodingTimeUs);
189
+ renderedTimes.forEach((itemId, time) -> {
190
+ if (time != null) {
191
+ this.renderedTimes.put(itemId, time);
192
+ }
193
+ });
194
+ if (allItemsReady) {
195
+ decoding = false;
196
+ resolveIfReady();
197
+ }
198
+ }
199
+
200
+ private void onItemImageAvailable(VideoComposition.Item item) {
201
+ if (requests.isClosed()) return;
202
+ try {
203
+ if (!decoding) resolveIfReady();
204
+ } catch (Exception error) { handleError(error); }
205
+ }
206
+
207
+ private void handleError(Exception e) {
208
+ requests.fail(e);
209
+ CompletableFuture<Void> initializing = startup;
210
+ if (initializing != null) initializing.completeExceptionally(e);
211
+ }
212
+
213
+ private void resolveIfReady() {
214
+ if (!requests.isPending()) return;
215
+ Map<String, VideoFrame> videoFrames = decoder.updateInternalFrames();
216
+ for (VideoComposition.Item item : composition.getItems()) {
217
+ if (!item.isVideo() || !decoder.isOpen(item)) {
218
+ continue;
219
+ }
220
+ VideoFrame videoFrame = videoFrames.getOrDefault(item.getId(), null);
221
+ if (videoFrame == null) {
222
+ if (itemsEnded.contains(item) && !itemsTimes.containsKey(item)) {
223
+ // Ended without a frame: there will never be one to wait for.
224
+ continue;
225
+ }
226
+ return;
227
+ }
228
+ Long itemFrameTime = renderedTimes.getOrDefault(item.getId(), null);
229
+ if (itemFrameTime == null) {
230
+ continue;
231
+ }
232
+ long videoFrameTime = TimeHelpers.nsecToUs(videoFrame.getTimestampNs());
233
+ if (Math.abs(itemFrameTime - videoFrameTime) > 1000) {
234
+ return;
235
+ }
236
+ }
237
+ // Never publish the producer's owning wrappers. The completed request owns
238
+ // these aliases until await hands them over, or cancellation discards them.
239
+ requests.complete(VideoCompositionDecoder.retainFrames(videoFrames));
240
+ }
241
+ }