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,354 @@
1
+ package com.azzapp.rnskv;
2
+
3
+ import android.os.Handler;
4
+ import android.os.HandlerThread;
5
+ import android.os.Message;
6
+ import android.os.Process;
7
+ import android.os.SystemClock;
8
+ import android.util.Log;
9
+
10
+ import androidx.media3.common.util.UnstableApi;
11
+
12
+ import java.io.IOException;
13
+ import java.util.Map;
14
+ import javax.microedition.khronos.egl.EGLContext;
15
+ import javax.microedition.khronos.egl.EGL10;
16
+
17
+ /**
18
+ * A class that previews a video composition.
19
+ */
20
+ @UnstableApi
21
+ public class VideoCompositionFramesExtractor {
22
+ private static final String TAG = "VideoCompositionFramesExtractor";
23
+
24
+ private static final int PLAYBACK_PREPARE = 1;
25
+ private static final int PLAYBACK_PLAY = 2;
26
+ private static final int PLAYBACK_PAUSE = 3;
27
+ private static final int PLAYBACK_LOOP = 4;
28
+ private static final int PLAYBACK_SEEK = 5;
29
+ private static final int PLAYBACK_RELEASE = 6;
30
+
31
+ private final VideoComposition composition;
32
+
33
+ private final VideoCompositionDecoder decoder;
34
+
35
+ private AudioCompositionPlayer audioPlayer;
36
+
37
+ private final PlaybackThread playbackThread;
38
+
39
+ private final Handler handler;
40
+
41
+ private final NativeEventDispatcher eventDispatcher;
42
+
43
+ private boolean playWhenReady = false;
44
+
45
+ private boolean prepared;
46
+ private boolean releasing;
47
+ private boolean looping;
48
+
49
+
50
+ private long pendingSeek = 0;
51
+ private boolean isPlaying = false;
52
+
53
+ private long startTime = 0;
54
+ private long pausePosition = 0;
55
+ private boolean isEOS = false;
56
+ private boolean completeDispatched = false;
57
+
58
+ /**
59
+ * Create a new VideoCompositionFramesExtractor.
60
+ *
61
+ * @param composition the video composition to preview
62
+ */
63
+ public VideoCompositionFramesExtractor(VideoComposition composition, NativeEventDispatcher eventDispatcher) {
64
+ this.eventDispatcher = eventDispatcher;
65
+ this.composition = composition;
66
+ decoder = new VideoCompositionDecoder(composition, true);
67
+ // Only a decoder that cannot open, as on iOS: a codec's passing errors
68
+ // stay quiet, since the player gives up on the first error it reports.
69
+ decoder.setOnOpenErrorListener(
70
+ error -> eventDispatcher.dispatchEvent("error", messageOf(error)));
71
+ playbackThread = new PlaybackThread();
72
+ playbackThread.start();
73
+ handler = new Handler(playbackThread.getLooper(), playbackThread);
74
+ }
75
+
76
+ public void prepare() {
77
+ if (prepared) {
78
+ return;
79
+ }
80
+ try {
81
+ decoder.prepare(EGL10.EGL_NO_CONTEXT);
82
+ } catch (RuntimeException error) {
83
+ // A decoder that cannot open, as a lazy one reports it: thrown, it
84
+ // reached the UI thread's worklet uncaught and killed the app.
85
+ eventDispatcher.dispatchEvent("error", messageOf(error));
86
+ return;
87
+ }
88
+ handler.sendEmptyMessage(PLAYBACK_PREPARE);
89
+ }
90
+
91
+ /**
92
+ * Start playing the composition.
93
+ */
94
+ public void play() {
95
+ handler.sendEmptyMessage(PLAYBACK_PLAY);
96
+ }
97
+
98
+ /**
99
+ * Pause the composition.
100
+ */
101
+ public void pause() {
102
+ handler.sendEmptyMessage(PLAYBACK_PAUSE);
103
+ }
104
+
105
+ /**
106
+ * Seek to given position
107
+ */
108
+ public void seekTo(long position) {
109
+ handler.removeMessages(PLAYBACK_SEEK);
110
+ handler.obtainMessage(PLAYBACK_SEEK, position).sendToTarget();
111
+ }
112
+
113
+ /**
114
+ * Decode the next frame of each composition item according to the current position of the player.
115
+ *
116
+ * @return an independently retained snapshot map; callers close its frames
117
+ * when finished, without affecting the producer or other snapshots
118
+ */
119
+ public Map<String, VideoFrame> decodeCompositionFrames() {
120
+ return decoder.updateVideosFrames();
121
+ }
122
+
123
+ /**
124
+ * Makes the decoder's GL context current on the calling thread, the one that
125
+ * calls {@link #decodeCompositionFrames}.
126
+ */
127
+ public void makeGLContextCurrent() {
128
+ decoder.makeGLContextCurrent();
129
+ }
130
+
131
+ /**
132
+ * @return the current position of the player in microseconds
133
+ */
134
+ public long getCurrentPosition() {
135
+ return isPlaying ? microTime() - startTime : pausePosition;
136
+ }
137
+
138
+ /**
139
+ * @return the version of the frames returned by {@link #decodeCompositionFrames},
140
+ * see {@link VideoCompositionDecoder#getFramesVersion()}
141
+ */
142
+ public long getFramesVersion() {
143
+ return decoder.getFramesVersion();
144
+ }
145
+
146
+ /**
147
+ * @return whether the player is looping
148
+ */
149
+ public boolean getIsLooping() {
150
+ return looping;
151
+ }
152
+
153
+ /**
154
+ * Set whether the player should loop.
155
+ */
156
+ public void setIsLooping(boolean value) {
157
+ looping = value;
158
+ if (prepared && isEOS && value) {
159
+ play();
160
+ }
161
+ }
162
+
163
+ /**
164
+ * @return whether the player is currently playing
165
+ */
166
+ public boolean getIsPlaying() {
167
+ return isPlaying;
168
+ }
169
+
170
+ public void release() {
171
+ if (!playbackThread.isAlive()) {
172
+ decoder.release();
173
+ return;
174
+ }
175
+ isPlaying = false;
176
+ releasing = true;
177
+ handler.sendEmptyMessage(PLAYBACK_RELEASE);
178
+ }
179
+
180
+ private void prepareInternal() {
181
+ decoder.start();
182
+ if (composition.hasAudio()) {
183
+ audioPlayer = new AudioCompositionPlayer(composition);
184
+ audioPlayer.setOnErrorListener(
185
+ message -> eventDispatcher.dispatchEvent("error", message));
186
+ audioPlayer.prepare();
187
+ }
188
+ prepared = true;
189
+ eventDispatcher.dispatchEvent("ready", null);
190
+ handler.sendEmptyMessage(PLAYBACK_LOOP);
191
+ if (pendingSeek != 0) {
192
+ seekInternal(pendingSeek);
193
+ }
194
+ if (playWhenReady) {
195
+ playInternal();
196
+ }
197
+ }
198
+
199
+ private void playInternal() {
200
+ if (!prepared) {
201
+ playWhenReady = true;
202
+ return;
203
+ }
204
+ if (isEOS) {
205
+ isEOS = false;
206
+ pausePosition = 0;
207
+ seekInternal(0);
208
+ }
209
+ startTime = microTime() - pausePosition;
210
+ isPlaying = true;
211
+ pausePosition = 0;
212
+ }
213
+
214
+ private void pauseInternal() {
215
+ if (!isPlaying) {
216
+ playWhenReady = false;
217
+ return;
218
+ }
219
+ pausePosition = getCurrentPosition();
220
+ isPlaying = false;
221
+ if (audioPlayer != null) {
222
+ audioPlayer.pause();
223
+ }
224
+ }
225
+
226
+ private void loopInternal() throws IOException, InterruptedException {
227
+ long loopStartTime = SystemClock.elapsedRealtime();
228
+
229
+ long currentPosition = getCurrentPosition();
230
+
231
+ isEOS = currentPosition >= TimeHelpers.secToUs(composition.getDuration());
232
+ if (isEOS) {
233
+ if (!completeDispatched) {
234
+ completeDispatched = true;
235
+ eventDispatcher.dispatchEvent("complete", null);
236
+ }
237
+ isPlaying = false;
238
+ pausePosition = TimeHelpers.secToUs(composition.getDuration());
239
+ currentPosition = pausePosition;
240
+ } else {
241
+ completeDispatched = false;
242
+ }
243
+ decoder.updateWindow(currentPosition);
244
+ decoder.render(currentPosition);
245
+ if (isEOS && looping) {
246
+ playInternal();
247
+ }
248
+ if (audioPlayer != null) {
249
+ audioPlayer.update(getCurrentPosition(), isPlaying);
250
+ }
251
+ long delay = 10;
252
+ long duration = (SystemClock.elapsedRealtime() - loopStartTime);
253
+ delay = delay - duration;
254
+ if (delay > 0) {
255
+ handler.sendEmptyMessageDelayed(PLAYBACK_LOOP, delay);
256
+ } else {
257
+ handler.sendEmptyMessage(PLAYBACK_LOOP);
258
+ }
259
+ }
260
+
261
+ private void seekInternal(long position) {
262
+ if (!prepared) {
263
+ pendingSeek = position;
264
+ return;
265
+ }
266
+ decoder.seekTo(position);
267
+ // After the seek, so a decoder opened for this position is not sought again.
268
+ decoder.updateWindow(position);
269
+ if (audioPlayer != null) {
270
+ audioPlayer.seekTo(position);
271
+ }
272
+ if (isPlaying) {
273
+ startTime = microTime() - position;
274
+ } else {
275
+ pausePosition = position;
276
+ }
277
+ }
278
+
279
+ private void releaseInternal() {
280
+ if (audioPlayer != null) {
281
+ try {
282
+ audioPlayer.release();
283
+ } catch (Exception e) {
284
+ Log.w(TAG, "Could not release the audio players", e);
285
+ }
286
+ audioPlayer = null;
287
+ }
288
+ playbackThread.interrupt();
289
+ playbackThread.quit();
290
+ decoder.release();
291
+ }
292
+
293
+ private class PlaybackThread extends HandlerThread implements Handler.Callback {
294
+ public PlaybackThread() {
295
+ super(TAG + PlaybackThread.class.getSimpleName(), Process.THREAD_PRIORITY_VIDEO);
296
+ }
297
+
298
+ @Override
299
+ public boolean handleMessage(Message msg) {
300
+ try {
301
+ if (releasing) {
302
+ // When the releasing flag is set, just release without processing any more messages
303
+ releaseInternal();
304
+ return true;
305
+ }
306
+
307
+ switch (msg.what) {
308
+ case PLAYBACK_PREPARE -> {
309
+ prepareInternal();
310
+ return true;
311
+ }
312
+ case PLAYBACK_PLAY -> {
313
+ playInternal();
314
+ return true;
315
+ }
316
+ case PLAYBACK_PAUSE -> {
317
+ pauseInternal();
318
+ return true;
319
+ }
320
+ case PLAYBACK_LOOP -> {
321
+ loopInternal();
322
+ return true;
323
+ }
324
+ case PLAYBACK_SEEK -> {
325
+ seekInternal((Long) msg.obj);
326
+ return true;
327
+ }
328
+ case PLAYBACK_RELEASE -> {
329
+ releaseInternal();
330
+ return true;
331
+ }
332
+ default -> {
333
+ return false;
334
+ }
335
+ }
336
+ } catch (Exception error) {
337
+ eventDispatcher.dispatchEvent("error", messageOf(error));
338
+ }
339
+
340
+ // Release after an exception
341
+ releaseInternal();
342
+ return true;
343
+ }
344
+ }
345
+
346
+ // Never null: the native side reads the message as a string.
347
+ private static String messageOf(Exception error) {
348
+ return error.getMessage() != null ? error.getMessage() : error.toString();
349
+ }
350
+
351
+ private static long microTime() {
352
+ return System.nanoTime() / 1000;
353
+ }
354
+ }
@@ -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
+ }