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,779 @@
1
+ package com.azzapp.rnskv;
2
+
3
+ import android.os.Handler;
4
+ import android.os.HandlerThread;
5
+ import android.util.Log;
6
+
7
+ import java.io.IOException;
8
+ import java.util.HashMap;
9
+ import java.util.Iterator;
10
+ import java.util.Map;
11
+ import java.util.concurrent.ConcurrentHashMap;
12
+ import java.util.concurrent.ConcurrentLinkedQueue;
13
+
14
+ import javax.microedition.khronos.egl.EGLContext;
15
+ import javax.microedition.khronos.egl.EGL10;
16
+
17
+ /**
18
+ * A class to decode a video composition and extract frames from the video items.
19
+ *
20
+ * With lazy decoders an item has a decoder only around its own time (see
21
+ * {@link DecoderWindow}); otherwise every item has one from prepare to release.
22
+ */
23
+ public class VideoCompositionDecoder {
24
+
25
+ private static final String TAG = "VideoCompositionDecoder";
26
+
27
+ private final VideoComposition composition;
28
+
29
+ private final boolean realTime;
30
+
31
+ private final DecoderWindow window;
32
+
33
+ /**
34
+ * The items that have a decoder, or one on its way. Written under this
35
+ * object's lock, read from the GL thread without it.
36
+ */
37
+ private final ConcurrentHashMap<VideoComposition.Item, Slot> slots = new ConcurrentHashMap<>();
38
+
39
+ /**
40
+ * Items out of their window. Their GL objects go once their codec has, since
41
+ * it draws into them until then.
42
+ */
43
+ private final ConcurrentLinkedQueue<Slot> retired = new ConcurrentLinkedQueue<>();
44
+
45
+ private EGLResourcesHolder eglResourcesHolder;
46
+
47
+ private final Object glLock = new Object();
48
+
49
+ private final HashMap<String, VideoFrame> videoFrames = new HashMap<>();
50
+
51
+ private long framesVersion = 0;
52
+
53
+ private boolean started = false;
54
+
55
+ private volatile boolean released = false;
56
+
57
+ private long seekGeneration = 0;
58
+
59
+ private long seekPositionUs = 0;
60
+
61
+ private HandlerThread openerThread;
62
+
63
+ private Handler opener;
64
+
65
+ private HandlerThread callbackThread;
66
+
67
+ private Handler callbacks;
68
+
69
+ private OnItemImageAvailableListener onItemImageAvailableListener;
70
+
71
+ private OnFrameAvailableListener onFrameAvailableListener;
72
+
73
+ private OnErrorListener onErrorListener;
74
+
75
+ private OnErrorListener onOpenErrorListener;
76
+
77
+ private OnItemEndReachedListener onItemEndReachedListener;
78
+
79
+ /**
80
+ * Creates a new video composition decoder.
81
+ *
82
+ * @param composition The video composition to decode.
83
+ * @param realTime true for the preview, whose GL work happens on the thread
84
+ * asking for frames and its playback on another; false for
85
+ * the export, which does everything on one thread
86
+ */
87
+ public VideoCompositionDecoder(VideoComposition composition, boolean realTime) {
88
+ this.composition = composition;
89
+ this.realTime = realTime;
90
+ this.window = DecoderWindow.of(composition.isLazyDecoders(), realTime);
91
+ }
92
+
93
+ /**
94
+ * Prepares the items decoders and the image readers.
95
+ */
96
+ public void prepare(EGLContext sharedContext) {
97
+ prepare(sharedContext, 0);
98
+ }
99
+
100
+ /**
101
+ * Prepares the decoders of the items open at the given position. The others
102
+ * are opened by {@link #updateWindow} as the position moves.
103
+ */
104
+ public void prepare(EGLContext sharedContext, long positionUs) {
105
+ // Historical callers may pass a Ganesh context. Graphite needs no EGL
106
+ // sharing: the decoder owns its context and exports immutable CPU pixels.
107
+ eglResourcesHolder = EGLResourcesHolder.createWithPBBufferSurface(EGL10.EGL_NO_CONTEXT);
108
+ try {
109
+ eglResourcesHolder.runWithContextCurrent(() -> prepareCurrent(positionUs));
110
+ } catch (RuntimeException error) {
111
+ release();
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ private void prepareCurrent(long positionUs) {
117
+ if (realTime) {
118
+ // The codecs call back on their own thread, lazy or not. Left to default
119
+ // they call back on the looper of the thread that configures them, and
120
+ // the preview prepares from the UI thread: every buffer of every item,
121
+ // and the burst after each seek (a loop's wrap, a seam), then queued on
122
+ // the main looper ahead of the frame that draws them.
123
+ callbackThread = new HandlerThread("ReactNativeSkiaVideo-Codecs");
124
+ callbackThread.start();
125
+ callbacks = new Handler(callbackThread.getLooper());
126
+ }
127
+ if (realTime && window.isLazy()) {
128
+ // Opening a codec takes tens of milliseconds: not on the thread that
129
+ // draws, nor the one that plays, nor the one the playing codec calls back on.
130
+ openerThread = new HandlerThread("ReactNativeSkiaVideo-Opener");
131
+ openerThread.start();
132
+ opener = new Handler(openerThread.getLooper());
133
+ }
134
+ for (VideoComposition.Item item : composition.getItems()) {
135
+ if (!item.isVideo() || !opens(item, positionUs)) {
136
+ continue;
137
+ }
138
+ Slot slot = new Slot(item, positionUs, seekGeneration);
139
+ slots.put(item, slot);
140
+ try {
141
+ slot.extractor = newExtractor(item);
142
+ slot.decoder = openCodec(slot);
143
+ } catch (Exception e) {
144
+ throw new RuntimeException(e);
145
+ }
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Starts the decoders.
151
+ */
152
+ public synchronized void start() {
153
+ started = true;
154
+ for (Slot slot : slots.values()) {
155
+ VideoCompositionItemDecoder decoder = slot.decoder;
156
+ if (decoder != null) {
157
+ decoder.start();
158
+ }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Opens the decoders of the items coming up at the given position and closes
164
+ * those of the items left behind. Does nothing without lazy decoders.
165
+ *
166
+ * The export calls it from its GL thread, where decoders open on the spot.
167
+ * The preview calls it from its playback thread: the GL thread makes the
168
+ * surface on its next {@link #updateVideosFrames}, then the opener the codec.
169
+ *
170
+ * @param positionUs the composition time
171
+ */
172
+ public synchronized void updateWindow(long positionUs) {
173
+ if (released || !window.isLazy()) {
174
+ return;
175
+ }
176
+ for (VideoComposition.Item item : composition.getItems()) {
177
+ if (!item.isVideo()) {
178
+ continue;
179
+ }
180
+ Slot slot = slots.get(item);
181
+ if (slot == null && opens(item, positionUs)) {
182
+ Log.d(TAG, "open " + item.getId() + " at " + positionUs + "us");
183
+ slot = new Slot(item, positionUs, seekGeneration);
184
+ slots.put(item, slot);
185
+ if (!realTime) {
186
+ openNow(slot);
187
+ }
188
+ } else if (slot != null && !keeps(item, positionUs)) {
189
+ Log.d(TAG, "close " + item.getId() + " at " + positionUs + "us");
190
+ slots.remove(item);
191
+ retire(slot);
192
+ }
193
+ }
194
+ }
195
+
196
+ /**
197
+ * @return whether the item has a decoder, which is always the case without
198
+ * lazy decoders once prepared
199
+ */
200
+ public boolean isOpen(VideoComposition.Item item) {
201
+ Slot slot = slots.get(item);
202
+ return slot != null && slot.decoder != null;
203
+ }
204
+
205
+ /**
206
+ * Sets the listener to be called when an image is available.
207
+ *
208
+ * @param onItemImageAvailableListener The listener to be called.
209
+ */
210
+ public void setOnItemImageAvailableListener(OnItemImageAvailableListener onItemImageAvailableListener) {
211
+ this.onItemImageAvailableListener = onItemImageAvailableListener;
212
+ }
213
+
214
+ /**
215
+ * Sets the listener to be called when an error occurs.
216
+ *
217
+ * @param onErrorListener The listener to be called.
218
+ */
219
+ public void setOnErrorListener(OnErrorListener onErrorListener) {
220
+ this.onErrorListener = onErrorListener;
221
+ }
222
+
223
+ /**
224
+ * Sets the listener for a decoder that could not be opened lazily; falls
225
+ * back to the error listener. Unlike a codec's passing errors, the item
226
+ * will show no frame.
227
+ */
228
+ public void setOnOpenErrorListener(OnErrorListener onOpenErrorListener) {
229
+ this.onOpenErrorListener = onOpenErrorListener;
230
+ }
231
+
232
+ /**
233
+ * Sets the listener to be called when a frame has been decoded.
234
+ *
235
+ * @param onFrameAvailableListener The listener to be called.
236
+ */
237
+ public void setOnFrameAvailableListener(OnFrameAvailableListener onFrameAvailableListener) {
238
+ this.onFrameAvailableListener = onFrameAvailableListener;
239
+ }
240
+
241
+ /**
242
+ * Sets the listener to be called when an item reaches its end.
243
+ *
244
+ * @param onItemEndReachedListener The listener to be called.
245
+ */
246
+ public void setOnItemEndReachedListener(OnItemEndReachedListener onItemEndReachedListener) {
247
+ this.onItemEndReachedListener = onItemEndReachedListener;
248
+ }
249
+
250
+ /**
251
+ * Renders the video composition at the given position.
252
+ *
253
+ * @param currentPositionUs The current position in microseconds.
254
+ * @return A map with the rendered times for each item.
255
+ */
256
+ public synchronized Map<String, Long> render(long currentPositionUs) {
257
+ Map<String, Long> renderedTimes = new HashMap<>();
258
+ slots.forEach((item, slot) -> {
259
+ VideoCompositionItemDecoder decoder = slot.decoder;
260
+ if (decoder != null) {
261
+ renderedTimes.put(item.getId(), decoder.render(currentPositionUs));
262
+ }
263
+ });
264
+ return renderedTimes;
265
+ }
266
+
267
+ /**
268
+ * Updates the composition and returns independently retained frame snapshots.
269
+ * Callers own these wrappers and may keep them after this decoder is released.
270
+ *
271
+ * @return A map with the updated video frames.
272
+ */
273
+ public Map<String, VideoFrame> updateVideosFrames() {
274
+ synchronized (glLock) {
275
+ updateInternalFrames();
276
+ return retainFrames(videoFrames);
277
+ }
278
+ }
279
+
280
+ /** Export readiness checks borrow the producer map on the export thread.
281
+ * They must not publish it or close its frames. Only a completed request
282
+ * creates retained aliases, so polling never accumulates native leases. */
283
+ Map<String, VideoFrame> updateInternalFrames() {
284
+ synchronized (glLock) {
285
+ if (eglResourcesHolder != null && !released)
286
+ eglResourcesHolder.withContextCurrent(this::updateVideosFramesCurrent);
287
+ return videoFrames;
288
+ }
289
+ }
290
+
291
+ static Map<String, VideoFrame> retainFrames(Map<String, VideoFrame> frames) {
292
+ Map<String, VideoFrame> retained = new HashMap<>();
293
+ try {
294
+ for (Map.Entry<String, VideoFrame> entry : frames.entrySet()) {
295
+ VideoFrame alias = entry.getValue().retain();
296
+ try { retained.put(entry.getKey(), alias); }
297
+ catch (RuntimeException | Error error) {
298
+ try { alias.close(); }
299
+ catch (RuntimeException | Error cleanup) { error.addSuppressed(cleanup); }
300
+ throw error;
301
+ }
302
+ }
303
+ return retained;
304
+ } catch (RuntimeException | Error error) {
305
+ try { closeFrames(retained); }
306
+ catch (RuntimeException | Error cleanup) { error.addSuppressed(cleanup); }
307
+ throw error;
308
+ }
309
+ }
310
+
311
+ static void closeFrames(Map<String, VideoFrame> frames) {
312
+ Throwable failure = null;
313
+ for (VideoFrame frame : frames.values()) {
314
+ try { frame.close(); }
315
+ catch (RuntimeException | Error error) {
316
+ if (failure == null) failure = error;
317
+ else failure.addSuppressed(error);
318
+ }
319
+ }
320
+ if (failure instanceof RuntimeException) throw (RuntimeException) failure;
321
+ if (failure instanceof Error) throw (Error) failure;
322
+ }
323
+
324
+ private Map<String, VideoFrame> updateVideosFramesCurrent() {
325
+ boolean current = false;
326
+ for (Iterator<Slot> it = retired.iterator(); it.hasNext(); ) {
327
+ Slot slot = it.next();
328
+ if (!slot.closed) {
329
+ continue;
330
+ }
331
+ it.remove();
332
+ GLFrameExtractor extractor;
333
+ synchronized (slot) {
334
+ extractor = slot.extractor;
335
+ slot.extractor = null;
336
+ }
337
+ if (extractor != null) {
338
+ if (!current) {
339
+ eglResourcesHolder.makeCurrent();
340
+ current = true;
341
+ }
342
+ extractor.release();
343
+ }
344
+ // The item may have been reopened since, and drawn its own frame.
345
+ String id = slot.item.getId();
346
+ if (slot.frame != null && videoFrames.get(id) == slot.frame) {
347
+ videoFrames.remove(id);
348
+ framesVersion++;
349
+ }
350
+ // This is the producer's wrapper. Independently retained snapshots stay valid.
351
+ if (slot.frame != null) {
352
+ slot.frame.close();
353
+ slot.frame = null;
354
+ }
355
+ }
356
+ for (VideoComposition.Item item : composition.getItems()) {
357
+ if (!item.isVideo()) {
358
+ continue;
359
+ }
360
+ Slot slot = slots.get(item);
361
+ if (slot == null) {
362
+ continue;
363
+ }
364
+ if (!current) {
365
+ eglResourcesHolder.makeCurrent();
366
+ current = true;
367
+ }
368
+ GLFrameExtractor glFrameExtractor = slot.extractor;
369
+ if (glFrameExtractor == null) {
370
+ makeSurface(slot);
371
+ continue;
372
+ }
373
+ VideoCompositionItemDecoder decoder = slot.decoder;
374
+ if (decoder == null) {
375
+ continue;
376
+ }
377
+ // The decoder still decodes the whole picture; the cap sizes the texture
378
+ // it is drawn into, which is what an item holds for the life of the player.
379
+ int[] size = FrameSize.of(
380
+ item.getWidth(),
381
+ item.getHeight(),
382
+ item.getMaxLongSide(),
383
+ decoder.getVideoWidth(),
384
+ decoder.getVideoHeight(),
385
+ decoder.getRotation()
386
+ );
387
+ int frameWidth = size[0];
388
+ int frameHeight = size[1];
389
+ if (!glFrameExtractor.decodeNextFrame(frameWidth, frameHeight)) {
390
+ continue;
391
+ }
392
+ VideoFrame nextFrame = glFrameExtractor.snapshotFrame(0);
393
+ VideoFrame previous = slot.frame;
394
+ final VideoFrame displaced;
395
+ try { displaced = videoFrames.put(item.getId(), nextFrame); }
396
+ catch (RuntimeException | Error error) {
397
+ try { nextFrame.close(); }
398
+ catch (RuntimeException | Error cleanup) { error.addSuppressed(cleanup); }
399
+ throw error;
400
+ }
401
+ slot.frame = nextFrame;
402
+ framesVersion++;
403
+ if (previous != null) previous.close();
404
+ if (displaced != null && displaced != previous) displaced.close();
405
+ }
406
+ return videoFrames;
407
+ }
408
+
409
+ /**
410
+ * @return a counter incremented every time {@link #updateVideosFrames}
411
+ * produced a new frame for an item. Comparing two values tells an unchanged
412
+ * frame set from a fresh one without inspecting the frames.
413
+ */
414
+ public long getFramesVersion() {
415
+ return framesVersion;
416
+ }
417
+
418
+ /**
419
+ * Makes the decoder's GL context current on the calling thread, where its
420
+ * frames are decoded.
421
+ */
422
+ public void makeGLContextCurrent() {
423
+ if (eglResourcesHolder != null) {
424
+ eglResourcesHolder.makeCurrent();
425
+ }
426
+ }
427
+
428
+ /**
429
+ * Seeks to the given position.
430
+ *
431
+ * @param position The position to seek to in microseconds.
432
+ */
433
+ synchronized public void seekTo(long position) {
434
+ seekPositionUs = position;
435
+ seekGeneration++;
436
+ for (Slot slot : slots.values()) {
437
+ VideoCompositionItemDecoder decoder = slot.decoder;
438
+ if (decoder != null) {
439
+ decoder.seekTo(position);
440
+ }
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Releases the resources.
446
+ */
447
+ public void release() {
448
+ synchronized (this) {
449
+ if (released) return;
450
+ released = true;
451
+ }
452
+ // A codec being opened draws into a surface released below. Waited for
453
+ // outside of the lock, which the opener takes to hand its codec over.
454
+ if (openerThread != null) {
455
+ openerThread.quit();
456
+ boolean interrupted = Thread.interrupted();
457
+ // Reclaiming its output surface before a codec configuration has
458
+ // returned is unsafe. Cleanup waits on its background owner; never
459
+ // force-release a surface after a fixed one-second delay.
460
+ while (openerThread.isAlive()) {
461
+ try {
462
+ openerThread.join(1000);
463
+ } catch (InterruptedException e) {
464
+ interrupted = true;
465
+ }
466
+ }
467
+ if (interrupted) {
468
+ Thread.currentThread().interrupt();
469
+ }
470
+ }
471
+ synchronized (this) {
472
+ for (Slot slot : slots.values()) {
473
+ releaseCodec(slot);
474
+ }
475
+ for (Slot slot : retired) {
476
+ releaseCodec(slot);
477
+ }
478
+ }
479
+ synchronized (glLock) {
480
+ // Internal wrappers own one lease each. Public snapshots have independent
481
+ // leases and remain readable after teardown, including from Java tests.
482
+ try {
483
+ closeFrames(videoFrames);
484
+ } finally {
485
+ videoFrames.clear();
486
+ // A slot absent from the map was closed when its replacement was
487
+ // installed. Drop references without revoking any public snapshot.
488
+ for (Slot slot : slots.values()) slot.frame = null;
489
+ for (Slot slot : retired) slot.frame = null;
490
+ try {
491
+ if (eglResourcesHolder != null) {
492
+ eglResourcesHolder.runWithContextCurrent(() -> {
493
+ for (Slot slot : slots.values()) releaseExtractor(slot);
494
+ for (Slot slot : retired) releaseExtractor(slot);
495
+ });
496
+ }
497
+ } finally {
498
+ if (eglResourcesHolder != null) {
499
+ eglResourcesHolder.release();
500
+ eglResourcesHolder = null;
501
+ }
502
+ slots.clear();
503
+ retired.clear();
504
+ }
505
+ }
506
+ }
507
+ if (callbackThread != null) {
508
+ callbackThread.quit();
509
+ }
510
+ }
511
+
512
+ private boolean opens(VideoComposition.Item item, long positionUs) {
513
+ long startUs = TimeHelpers.secToUs(item.getCompositionStartTime());
514
+ return window.opens(startUs, startUs + TimeHelpers.secToUs(item.getDuration()), positionUs);
515
+ }
516
+
517
+ private boolean keeps(VideoComposition.Item item, long positionUs) {
518
+ long startUs = TimeHelpers.secToUs(item.getCompositionStartTime());
519
+ return window.keeps(startUs, startUs + TimeHelpers.secToUs(item.getDuration()), positionUs);
520
+ }
521
+
522
+ /**
523
+ * The export's path: this thread has the GL context and the codec callbacks.
524
+ */
525
+ private void openNow(Slot slot) {
526
+ try {
527
+ eglResourcesHolder.runWithContextCurrent(() -> slot.extractor = newExtractor(slot.item));
528
+ VideoCompositionItemDecoder decoder = openCodec(slot);
529
+ slot.decoder = decoder;
530
+ if (started) {
531
+ decoder.start();
532
+ }
533
+ } catch (Exception e) {
534
+ releaseCodec(slot);
535
+ reportOpenError(e);
536
+ }
537
+ }
538
+
539
+ /**
540
+ * The preview's path, on the GL thread: the surface here, then the codec on
541
+ * the opener. Tried once; a failure leaves the item without frames.
542
+ */
543
+ private void makeSurface(Slot slot) {
544
+ if (opener == null) {
545
+ return;
546
+ }
547
+ synchronized (slot) {
548
+ // released is read under the slot lock, which release() takes after
549
+ // setting it: a surface made here is one release() will see.
550
+ if (released || slot.retired || slot.failed || slot.extractor != null) {
551
+ return;
552
+ }
553
+ try {
554
+ slot.extractor = newExtractor(slot.item);
555
+ } catch (Exception e) {
556
+ slot.failed = true;
557
+ reportOpenError(e);
558
+ return;
559
+ }
560
+ }
561
+ if (!opener.post(() -> openLater(slot))) {
562
+ releaseExtractor(slot);
563
+ }
564
+ }
565
+
566
+ private void openLater(Slot slot) {
567
+ synchronized (slot) {
568
+ if (slot.retired) {
569
+ return;
570
+ }
571
+ }
572
+ VideoCompositionItemDecoder decoder;
573
+ try {
574
+ decoder = openCodec(slot);
575
+ } catch (Exception e) {
576
+ reportOpenError(e);
577
+ return;
578
+ }
579
+ synchronized (this) {
580
+ boolean dropped;
581
+ synchronized (slot) {
582
+ dropped = released || slot.retired;
583
+ if (!dropped) {
584
+ slot.decoder = decoder;
585
+ }
586
+ }
587
+ if (dropped) {
588
+ decoder.release();
589
+ return;
590
+ }
591
+ if (started) {
592
+ try {
593
+ decoder.start();
594
+ if (seekGeneration != slot.seekGeneration) {
595
+ decoder.seekTo(seekPositionUs);
596
+ }
597
+ } catch (RuntimeException e) {
598
+ // Uncaught, this would end the app from the opener thread.
599
+ releaseCodec(slot);
600
+ reportOpenError(e);
601
+ }
602
+ }
603
+ }
604
+ }
605
+
606
+ private void retire(Slot slot) {
607
+ synchronized (slot) {
608
+ slot.retired = true;
609
+ }
610
+ retired.add(slot);
611
+ if (opener == null) {
612
+ close(slot);
613
+ } else if (!opener.post(() -> close(slot))) {
614
+ Log.w(TAG, "The opener has quit, the decoder goes with release()");
615
+ }
616
+ }
617
+
618
+ private void close(Slot slot) {
619
+ try {
620
+ releaseCodec(slot);
621
+ } catch (RuntimeException e) {
622
+ Log.w(TAG, "Could not release a decoder", e);
623
+ }
624
+ slot.closed = true;
625
+ }
626
+
627
+ private static void releaseCodec(Slot slot) {
628
+ VideoCompositionItemDecoder decoder;
629
+ synchronized (slot) {
630
+ decoder = slot.decoder;
631
+ slot.decoder = null;
632
+ }
633
+ if (decoder != null) {
634
+ decoder.release();
635
+ }
636
+ }
637
+
638
+ private static void releaseExtractor(Slot slot) {
639
+ GLFrameExtractor extractor;
640
+ synchronized (slot) {
641
+ extractor = slot.extractor;
642
+ slot.extractor = null;
643
+ }
644
+ if (extractor != null) {
645
+ extractor.release();
646
+ }
647
+ }
648
+
649
+ private GLFrameExtractor newExtractor(VideoComposition.Item item) {
650
+ GLFrameExtractor glFrameExtractor = new GLFrameExtractor(
651
+ item.isDirectTexture(), composition.isHardwareBufferEnabled());
652
+ glFrameExtractor.setOnFrameAvailableListener(() -> {
653
+ if (onItemImageAvailableListener != null) {
654
+ onItemImageAvailableListener.onItemImageAvailable(item);
655
+ }
656
+ });
657
+ return glFrameExtractor;
658
+ }
659
+
660
+ private VideoCompositionItemDecoder openCodec(Slot slot) throws IOException {
661
+ VideoComposition.Item item = slot.item;
662
+ VideoCompositionItemDecoder decoder = new VideoCompositionItemDecoder(item);
663
+ decoder.setOnErrorListener(this::reportError);
664
+ decoder.setOnFrameAvailableListener(presentationTimeUs -> {
665
+ if (onFrameAvailableListener != null) {
666
+ onFrameAvailableListener.onFrameAvailable(item, presentationTimeUs);
667
+ }
668
+ });
669
+ decoder.setOnEndReachedListener(() -> {
670
+ if (onItemEndReachedListener != null) {
671
+ onItemEndReachedListener.onItemEndReached(item);
672
+ }
673
+ });
674
+ decoder.setInitialPosition(slot.openAtUs);
675
+ decoder.setCallbackHandler(callbacks);
676
+ try {
677
+ decoder.prepare();
678
+ decoder.setSurface(slot.extractor.surfaceFor(decoder.getVideoWidth(),
679
+ decoder.getVideoHeight(), decoder.getRotation(), wantsNativeImages(item, decoder)));
680
+ } catch (IOException | RuntimeException e) {
681
+ decoder.release();
682
+ throw e;
683
+ }
684
+ return decoder;
685
+ }
686
+
687
+ /**
688
+ * Hardware frames at the encoded size from an 8-bit SDR track skip the GL
689
+ * render pass: the codec writes into ImageReader buffers WebGPU samples.
690
+ * A capped or explicit size keeps the GL path, which scales while drawing.
691
+ */
692
+ private boolean wantsNativeImages(VideoComposition.Item item, VideoCompositionItemDecoder decoder) {
693
+ if (!composition.isHardwareBufferEnabled() || !decoder.isEightBitSdr()) return false;
694
+ int[] size = FrameSize.of(item.getWidth(), item.getHeight(), item.getMaxLongSide(),
695
+ decoder.getVideoWidth(), decoder.getVideoHeight(), 0);
696
+ return size[0] == decoder.getVideoWidth() && size[1] == decoder.getVideoHeight();
697
+ }
698
+
699
+ private void reportOpenError(Exception e) {
700
+ OnErrorListener listener = onOpenErrorListener;
701
+ if (listener != null) {
702
+ listener.onError(e);
703
+ } else {
704
+ reportError(e);
705
+ }
706
+ }
707
+
708
+ private void reportError(Exception e) {
709
+ OnErrorListener listener = onErrorListener;
710
+ if (listener != null) {
711
+ listener.onError(e);
712
+ } else {
713
+ Log.w(TAG, "Decoder error", e);
714
+ }
715
+ }
716
+
717
+ /**
718
+ * An item's decoder and the surface it draws into.
719
+ */
720
+ private static final class Slot {
721
+ final VideoComposition.Item item;
722
+
723
+ /** The composition time the slot was made at, where its codec starts. */
724
+ final long openAtUs;
725
+
726
+ /** Tells whether a seek happened while the codec was being opened. */
727
+ final long seekGeneration;
728
+
729
+ volatile GLFrameExtractor extractor;
730
+
731
+ volatile VideoCompositionItemDecoder decoder;
732
+
733
+ /** The last frame this slot put in the frames map. GL thread only. */
734
+ VideoFrame frame;
735
+
736
+ /** Guarded by the slot. */
737
+ boolean retired;
738
+
739
+ /** Guarded by the slot. */
740
+ boolean failed;
741
+
742
+ /** The codec is released, so the surface can go. */
743
+ volatile boolean closed;
744
+
745
+ Slot(VideoComposition.Item item, long openAtUs, long seekGeneration) {
746
+ this.item = item;
747
+ this.openAtUs = openAtUs;
748
+ this.seekGeneration = seekGeneration;
749
+ }
750
+ }
751
+
752
+ /**
753
+ * Listener to be called when an image is available.
754
+ */
755
+ public interface OnItemImageAvailableListener {
756
+ void onItemImageAvailable(VideoComposition.Item item);
757
+ }
758
+
759
+ /**
760
+ * Listener to be called when an item reaches its end.
761
+ */
762
+ public interface OnItemEndReachedListener {
763
+ void onItemEndReached(VideoComposition.Item item);
764
+ }
765
+
766
+ /**
767
+ * Listener to be called when a frame is available.
768
+ */
769
+ public interface OnFrameAvailableListener {
770
+ void onFrameAvailable(VideoComposition.Item item, long presentationTimeUs);
771
+ }
772
+
773
+ /**
774
+ * Listener to be called when an error occurs.
775
+ */
776
+ public interface OnErrorListener {
777
+ void onError(Exception e);
778
+ }
779
+ }