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,321 @@
1
+ package com.azzapp.rnskv;
2
+
3
+ import android.net.Uri;
4
+ import android.os.Looper;
5
+ import android.os.SystemClock;
6
+
7
+ import androidx.media3.common.C;
8
+ import androidx.media3.common.MediaItem;
9
+ import androidx.media3.common.PlaybackException;
10
+ import androidx.media3.common.PlaybackParameters;
11
+ import androidx.media3.common.Player;
12
+ import androidx.media3.common.Tracks;
13
+ import androidx.media3.common.util.UnstableApi;
14
+ import androidx.media3.exoplayer.ExoPlayer;
15
+
16
+ import java.io.File;
17
+ import java.util.ArrayList;
18
+ import java.util.List;
19
+
20
+ /**
21
+ * Plays the audio of the audio-enabled items of a video composition during
22
+ * playback. One audio only ExoPlayer is created per audio-enabled item and
23
+ * slaved to the composition clock through the {@link #update} method.
24
+ * Every method must be called on the thread that called {@link #prepare}.
25
+ */
26
+ @UnstableApi
27
+ public class AudioCompositionPlayer {
28
+
29
+ public interface OnErrorListener {
30
+ void onError(String message);
31
+ }
32
+
33
+ // Drift above which a player is hard-seeked (audible); smaller drifts are
34
+ // caught up by adjusting the playback speed (pitch preserving, inaudible).
35
+ private static final long HARD_RESYNC_US = 500000;
36
+
37
+ // Speed adjustment hysteresis: starts above 60ms of drift, stops under 20ms.
38
+ private static final long DRIFT_ADJUST_START_US = 60000;
39
+ private static final long DRIFT_ADJUST_STOP_US = 20000;
40
+
41
+ private static final float CATCH_UP_SPEED = 1.05f;
42
+ private static final float SLOW_DOWN_SPEED = 0.95f;
43
+
44
+ // Minimum delay between two hard resyncs of the same item; without it a
45
+ // player whose seek latency exceeds the threshold re-seeks on every tick.
46
+ private static final long RESYNC_COOLDOWN_MS = 1000;
47
+
48
+ // Window after a start during which the observed drift is attributed to
49
+ // the audio pipeline startup latency and learned as the seek lead.
50
+ private static final long START_LEARN_WINDOW_MS = 2000;
51
+
52
+ private static final long MAX_SEEK_LEAD_US = 1000000;
53
+
54
+ private final VideoComposition composition;
55
+
56
+ private final List<ItemPlayer> itemPlayers = new ArrayList<>();
57
+
58
+ private OnErrorListener onErrorListener;
59
+
60
+ private boolean released = false;
61
+
62
+ public AudioCompositionPlayer(VideoComposition composition) {
63
+ this.composition = composition;
64
+ }
65
+
66
+ public void setOnErrorListener(OnErrorListener onErrorListener) {
67
+ this.onErrorListener = onErrorListener;
68
+ }
69
+
70
+ public void prepare() {
71
+ if (released || !itemPlayers.isEmpty()) return;
72
+ try {
73
+ for (VideoComposition.Item item : composition.getItems()) {
74
+ if (!item.isAudioEnabled()) continue;
75
+ ItemPlayer player = new ItemPlayer(item, this::dispatchError, composition.isLazyDecoders());
76
+ // Register ownership before open(): an ExoPlayer created before a later
77
+ // prepare failure must still be released.
78
+ itemPlayers.add(player);
79
+ if (!composition.isLazyDecoders()) player.open();
80
+ }
81
+ } catch (RuntimeException | Error error) {
82
+ release();
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ private void dispatchError(String message) {
88
+ if (!released && onErrorListener != null) {
89
+ onErrorListener.onError(message);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Synchronizes the item players with the composition clock.
95
+ */
96
+ public void update(long positionUs, boolean isPlaying) {
97
+ if (released) {
98
+ return;
99
+ }
100
+ for (ItemPlayer itemPlayer : itemPlayers) {
101
+ itemPlayer.update(positionUs, isPlaying);
102
+ }
103
+ }
104
+
105
+ public void seekTo(long positionUs) {
106
+ if (released) {
107
+ return;
108
+ }
109
+ for (ItemPlayer itemPlayer : itemPlayers) {
110
+ itemPlayer.seekTo(positionUs);
111
+ }
112
+ }
113
+
114
+ public void pause() {
115
+ if (released) {
116
+ return;
117
+ }
118
+ for (ItemPlayer itemPlayer : itemPlayers) {
119
+ itemPlayer.pause();
120
+ }
121
+ }
122
+
123
+ public void release() {
124
+ if (released) {
125
+ return;
126
+ }
127
+ released = true;
128
+ for (ItemPlayer itemPlayer : itemPlayers) {
129
+ itemPlayer.release();
130
+ }
131
+ itemPlayers.clear();
132
+ }
133
+
134
+ private static class ItemPlayer {
135
+ private ExoPlayer player;
136
+ private final VideoComposition.Item item;
137
+ private final OnErrorListener onErrorListener;
138
+ private final DecoderWindow window;
139
+ private long generation;
140
+ private final long compositionStartTimeUs;
141
+ private final long durationUs;
142
+
143
+ // Learned seek/startup latency: seeking lands the player behind the
144
+ // composition clock by the time the audio pipeline takes to resume,
145
+ // leading every seek by the observed drift compensates it.
146
+ private long seekLeadUs = 0;
147
+
148
+ private long lastSyncElapsedMs = -1;
149
+
150
+ private long lastStartElapsedMs = -1;
151
+
152
+ private float currentSpeed = 1f;
153
+
154
+ ItemPlayer(VideoComposition.Item item, OnErrorListener onErrorListener, boolean lazy) {
155
+ this.item = item;
156
+ this.onErrorListener = onErrorListener;
157
+ window = DecoderWindow.of(lazy, true);
158
+ compositionStartTimeUs = TimeHelpers.secToUs(item.getCompositionStartTime());
159
+ durationUs = TimeHelpers.secToUs(item.getDuration());
160
+ }
161
+
162
+ private void open() {
163
+ if (player != null) return;
164
+ final long openedGeneration = ++generation;
165
+ player = new ExoPlayer.Builder(
166
+ ReactNativeSkiaVideoModule.currentReactApplicationContext()
167
+ ).setLooper(Looper.myLooper()).build();
168
+
169
+ player.addListener(new Player.Listener() {
170
+ private boolean audioTrackChecked = false;
171
+
172
+ @Override
173
+ public void onPlayerError(PlaybackException error) {
174
+ if (openedGeneration != generation || player == null) return;
175
+ onErrorListener.onError(error.getMessage());
176
+ }
177
+
178
+ @Override
179
+ public void onTracksChanged(Tracks tracks) {
180
+ if (openedGeneration != generation || player == null) return;
181
+ if (item.isVideo() || audioTrackChecked || tracks.getGroups().isEmpty()) {
182
+ return;
183
+ }
184
+ audioTrackChecked = true;
185
+ for (Tracks.Group group : tracks.getGroups()) {
186
+ if (group.getType() == C.TRACK_TYPE_AUDIO) {
187
+ return;
188
+ }
189
+ }
190
+ onErrorListener.onError("No audio track for path: " + item.getPath());
191
+ }
192
+ });
193
+
194
+ String path = item.getPath();
195
+ Uri uri = path.startsWith("/") ? Uri.fromFile(new File(path)) : Uri.parse(path);
196
+ long startTimeMs = Math.round(item.getStartTime() * 1000);
197
+ long endTimeMs = startTimeMs + Math.round(item.getDuration() * 1000);
198
+ MediaItem mediaItem = new MediaItem.Builder()
199
+ .setUri(uri)
200
+ .setClippingConfiguration(
201
+ new MediaItem.ClippingConfiguration.Builder()
202
+ .setStartPositionMs(startTimeMs)
203
+ .setEndPositionMs(endTimeMs)
204
+ .build()
205
+ )
206
+ .build();
207
+ player.setMediaItem(mediaItem);
208
+ // Disabling video ensures a video file used as an audio source never
209
+ // instantiates a video decoder.
210
+ player.setTrackSelectionParameters(
211
+ player.getTrackSelectionParameters().buildUpon()
212
+ .setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true)
213
+ .build()
214
+ );
215
+ player.setVolume((float) Math.max(0, Math.min(1, item.getAudioVolume())));
216
+ player.setPlayWhenReady(false);
217
+ player.prepare();
218
+ }
219
+
220
+ void update(long positionUs, boolean isPlaying) {
221
+ if (player == null && window.opens(compositionStartTimeUs,
222
+ compositionStartTimeUs + durationUs, positionUs)) {
223
+ open();
224
+ syncTo(Math.max(0, positionUs - compositionStartTimeUs));
225
+ } else if (player != null && !window.keeps(compositionStartTimeUs,
226
+ compositionStartTimeUs + durationUs, positionUs)) {
227
+ release();
228
+ }
229
+ if (player == null) return;
230
+ long localPositionUs = positionUs - compositionStartTimeUs;
231
+ boolean active =
232
+ isPlaying && localPositionUs >= 0 && localPositionUs < durationUs;
233
+ if (!active) {
234
+ if (player.getPlayWhenReady()) {
235
+ player.setPlayWhenReady(false);
236
+ }
237
+ return;
238
+ }
239
+ if (!player.getPlayWhenReady()) {
240
+ // The player is usually already primed at the target position
241
+ // (prepare, user seek, loop restart): starting it without seeking
242
+ // avoids the seek + rebuffering latency at the start of playback.
243
+ long playerPositionUs = player.getCurrentPosition() * 1000;
244
+ long targetUs = localPositionUs + seekLeadUs;
245
+ if (Math.abs(playerPositionUs - targetUs) > DRIFT_ADJUST_START_US) {
246
+ syncTo(localPositionUs);
247
+ }
248
+ setSpeed(1f);
249
+ player.setPlayWhenReady(true);
250
+ lastStartElapsedMs = SystemClock.elapsedRealtime();
251
+ lastSyncElapsedMs = lastStartElapsedMs;
252
+ return;
253
+ }
254
+ if (!player.isPlaying()) {
255
+ // Still buffering after a seek/start.
256
+ return;
257
+ }
258
+ long playerPositionUs = player.getCurrentPosition() * 1000;
259
+ long driftUs = localPositionUs - playerPositionUs;
260
+ long absDriftUs = Math.abs(driftUs);
261
+ long nowMs = SystemClock.elapsedRealtime();
262
+
263
+ if (
264
+ driftUs > 0
265
+ && lastStartElapsedMs >= 0
266
+ && nowMs - lastStartElapsedMs < START_LEARN_WINDOW_MS
267
+ ) {
268
+ seekLeadUs = Math.min(Math.max(seekLeadUs, driftUs), MAX_SEEK_LEAD_US);
269
+ }
270
+
271
+ if (absDriftUs > HARD_RESYNC_US) {
272
+ if (lastSyncElapsedMs < 0 || nowMs - lastSyncElapsedMs >= RESYNC_COOLDOWN_MS) {
273
+ syncTo(localPositionUs);
274
+ setSpeed(1f);
275
+ }
276
+ } else if (absDriftUs > DRIFT_ADJUST_START_US) {
277
+ setSpeed(driftUs > 0 ? CATCH_UP_SPEED : SLOW_DOWN_SPEED);
278
+ } else if (absDriftUs < DRIFT_ADJUST_STOP_US) {
279
+ setSpeed(1f);
280
+ }
281
+ }
282
+
283
+ private void setSpeed(float speed) {
284
+ if (currentSpeed != speed) {
285
+ currentSpeed = speed;
286
+ player.setPlaybackParameters(new PlaybackParameters(speed));
287
+ }
288
+ }
289
+
290
+ void seekTo(long positionUs) {
291
+ if (player == null && window.opens(compositionStartTimeUs,
292
+ compositionStartTimeUs + durationUs, positionUs)) open();
293
+ if (player != null && !window.keeps(compositionStartTimeUs,
294
+ compositionStartTimeUs + durationUs, positionUs)) release();
295
+ if (player == null) return;
296
+ long localPositionUs =
297
+ Math.max(0, Math.min(positionUs - compositionStartTimeUs, durationUs));
298
+ syncTo(localPositionUs);
299
+ }
300
+
301
+ private void syncTo(long localPositionUs) {
302
+ player.seekTo((localPositionUs + seekLeadUs) / 1000);
303
+ lastSyncElapsedMs = SystemClock.elapsedRealtime();
304
+ }
305
+
306
+ void pause() {
307
+ if (player != null) player.setPlayWhenReady(false);
308
+ }
309
+
310
+ void release() {
311
+ if (player != null) {
312
+ generation++;
313
+ player.release();
314
+ player = null;
315
+ currentSpeed = 1f;
316
+ seekLeadUs = 0;
317
+ lastStartElapsedMs = lastSyncElapsedMs = -1;
318
+ }
319
+ }
320
+ }
321
+ }
@@ -0,0 +1,55 @@
1
+ package com.azzapp.rnskv;
2
+
3
+ import java.lang.ref.PhantomReference;
4
+ import java.lang.ref.ReferenceQueue;
5
+ import java.util.Set;
6
+ import java.util.concurrent.ConcurrentHashMap;
7
+ import java.util.function.LongConsumer;
8
+
9
+ /** Pre-registered backing owner and exposed JNI root: no cleanup allocation is needed during GC. */
10
+ final class BufferLifetime {
11
+ private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
12
+ private final Set<Tracked> references = ConcurrentHashMap.newKeySet();
13
+ private final LongConsumer release;
14
+
15
+ BufferLifetime(LongConsumer release) { this.release = release; }
16
+
17
+ Tracked track(Object exposed, Object backing, long token) {
18
+ Tracked tracked = new Tracked(exposed, backing, token);
19
+ references.add(tracked);
20
+ return tracked;
21
+ }
22
+
23
+ void drain() {
24
+ Tracked reference;
25
+ while ((reference = (Tracked) queue.poll()) != null) process(reference);
26
+ }
27
+
28
+ void run() {
29
+ for (;;) {
30
+ try { process((Tracked) queue.remove()); }
31
+ catch (InterruptedException ignored) { /* Process-wide daemon has no application owner. */ }
32
+ }
33
+ }
34
+
35
+ private void process(Tracked reference) {
36
+ if (!references.remove(reference)) return;
37
+ reference.backing = null;
38
+ reference.clear();
39
+ if (reference.token != 0) release.accept(reference.token);
40
+ }
41
+
42
+ int trackedCount() { return references.size(); }
43
+ // Only package tests use this to enqueue a second-phase reference without relying on GC timing.
44
+ Tracked onlyTracked() { return references.iterator().next(); }
45
+
46
+ final class Tracked extends PhantomReference<Object> {
47
+ private Object backing;
48
+ private final long token;
49
+ Tracked(Object referent, Object backing, long token) {
50
+ super(referent, queue);
51
+ this.backing = backing;
52
+ this.token = token;
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,100 @@
1
+ package com.azzapp.rnskv;
2
+
3
+ import java.util.concurrent.CompletableFuture;
4
+ import java.util.concurrent.CancellationException;
5
+ import java.util.concurrent.ExecutionException;
6
+ import java.util.concurrent.TimeUnit;
7
+ import java.util.concurrent.TimeoutException;
8
+ import java.util.function.Consumer;
9
+
10
+ /** One synchronous decoder request at a time; errors also persist between requests. */
11
+ final class DecodeRequests<T> {
12
+ private CompletableFuture<T> pending;
13
+ private T completedResult;
14
+ private boolean hasCompletedResult;
15
+ private final Consumer<T> discard;
16
+ private Exception failure;
17
+ private boolean closed;
18
+
19
+ DecodeRequests() { this(result -> {}); }
20
+
21
+ DecodeRequests(Consumer<T> discard) { this.discard = discard; }
22
+
23
+ synchronized CompletableFuture<T> begin() throws Exception {
24
+ if (closed) throw new IllegalStateException("Video decoder has been released");
25
+ if (failure != null) throw failure;
26
+ if (pending != null) {
27
+ throw new IllegalStateException("A video decode request is already pending");
28
+ }
29
+ pending = new CompletableFuture<>();
30
+ return pending;
31
+ }
32
+
33
+ synchronized void fail(Exception error) {
34
+ if (closed) return;
35
+ if (failure == null) failure = error;
36
+ if (pending != null) pending.completeExceptionally(failure);
37
+ discardCompleted();
38
+ }
39
+
40
+ synchronized void complete(T result) {
41
+ if (!closed && failure == null && pending != null && !pending.isDone()) {
42
+ completedResult = result;
43
+ hasCompletedResult = true;
44
+ // Set ownership before waking waiters, including a synchronous completion
45
+ // callback which may re-enter await on this same monitor.
46
+ if (!pending.complete(result)) discardCompleted();
47
+ } else {
48
+ // Results racing cancellation, duplicate completions, or an absent
49
+ // request never reach the consumer and must release their own aliases.
50
+ discard.accept(result);
51
+ }
52
+ }
53
+
54
+ synchronized boolean isPending() {
55
+ return !closed && failure == null && pending != null && !pending.isDone();
56
+ }
57
+
58
+ synchronized boolean isClosed() { return closed; }
59
+
60
+ synchronized void close() {
61
+ closed = true;
62
+ if (pending != null) pending.cancel(true);
63
+ discardCompleted();
64
+ pending = null;
65
+ }
66
+
67
+ private void discardCompleted() {
68
+ if (!hasCompletedResult) return;
69
+ T discarded = completedResult;
70
+ completedResult = null;
71
+ hasCompletedResult = false;
72
+ discard.accept(discarded);
73
+ }
74
+
75
+ T await(CompletableFuture<T> request, long timeoutMs) throws Exception {
76
+ try {
77
+ T result = request.get(timeoutMs, TimeUnit.MILLISECONDS);
78
+ synchronized (this) {
79
+ // close/fail may run between Future.get and this ownership transfer.
80
+ // Such a result has already been discarded and must not escape.
81
+ if (closed) throw new CancellationException("Video decoder has been released");
82
+ if (failure != null) throw new ExecutionException(failure);
83
+ if (pending != request || !hasCompletedResult)
84
+ throw new IllegalStateException("Video decode result was already consumed");
85
+ completedResult = null;
86
+ hasCompletedResult = false;
87
+ pending = null;
88
+ return result;
89
+ }
90
+ } catch (TimeoutException error) {
91
+ fail(error);
92
+ close();
93
+ throw error;
94
+ } catch (InterruptedException error) {
95
+ Thread.currentThread().interrupt();
96
+ close();
97
+ throw error;
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,55 @@
1
+ package com.azzapp.rnskv;
2
+
3
+ /**
4
+ * When an item's decoder is open, for a composition with lazy decoders: from a
5
+ * little before the item starts to a little after it ends. Without lazy
6
+ * decoders every decoder is open for the life of the player.
7
+ *
8
+ * Unlike iOS, no read-ahead over a loop's wrap: the wrap seeks every decoder
9
+ * back to 0, so one opened early would only race through its item meanwhile.
10
+ */
11
+ final class DecoderWindow {
12
+
13
+ // The preview has to open a codec and decode to the first frame before the
14
+ // item is on screen; the export waits for it anyway. Chosen, not measured.
15
+ private static final long PREVIEW_LEAD_US = 1_500_000;
16
+ private static final long EXPORT_LEAD_US = 500_000;
17
+
18
+ // Kept a little past both ends, so a scrub across a cut does not reopen it.
19
+ private static final long MARGIN_US = 500_000;
20
+
21
+ private final boolean lazy;
22
+
23
+ private final long leadUs;
24
+
25
+ private DecoderWindow(boolean lazy, long leadUs) {
26
+ this.lazy = lazy;
27
+ this.leadUs = leadUs;
28
+ }
29
+
30
+ static DecoderWindow of(boolean lazy, boolean realTime) {
31
+ return new DecoderWindow(lazy, realTime ? PREVIEW_LEAD_US : EXPORT_LEAD_US);
32
+ }
33
+
34
+ boolean isLazy() {
35
+ return lazy;
36
+ }
37
+
38
+ /**
39
+ * @param startUs the item's start in the composition
40
+ * @param endUs the item's end in the composition
41
+ * @param positionUs the composition time
42
+ */
43
+ boolean opens(long startUs, long endUs, long positionUs) {
44
+ return !lazy || (positionUs >= startUs - leadUs && positionUs < endUs);
45
+ }
46
+
47
+ /**
48
+ * Wider than {@link #opens}, so an item is never closed and reopened on
49
+ * either side of the same edge.
50
+ */
51
+ boolean keeps(long startUs, long endUs, long positionUs) {
52
+ return !lazy
53
+ || (positionUs >= startUs - leadUs - MARGIN_US && positionUs < endUs + MARGIN_US);
54
+ }
55
+ }
@@ -0,0 +1,132 @@
1
+ package com.azzapp.rnskv;
2
+
3
+ import android.opengl.EGL14;
4
+ import android.opengl.EGLExt;
5
+ import android.view.Surface;
6
+
7
+ import java.util.function.Supplier;
8
+ import javax.microedition.khronos.egl.EGL10;
9
+ import javax.microedition.khronos.egl.EGLConfig;
10
+ import javax.microedition.khronos.egl.EGLContext;
11
+ import javax.microedition.khronos.egl.EGLDisplay;
12
+ import javax.microedition.khronos.egl.EGLSurface;
13
+
14
+ /** A session-owned EGL context. The display is process-owned and is never terminated here. */
15
+ public final class EGLResourcesHolder {
16
+ private final EGL10 egl;
17
+ private final EGLDisplay display;
18
+ private final EGLContext context;
19
+ private final EGLSurface surface;
20
+ private boolean released;
21
+
22
+ public static EGLResourcesHolder createWithWindowedSurface(EGLContext sharedContext, Surface surface) {
23
+ return create(sharedContext, surface);
24
+ }
25
+
26
+ public static EGLResourcesHolder createWithPBBufferSurface(EGLContext sharedContext) {
27
+ return create(sharedContext, null);
28
+ }
29
+
30
+ private static EGLResourcesHolder create(EGLContext sharedContext, Surface window) {
31
+ EGL10 egl = (EGL10) EGLContext.getEGL();
32
+ EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
33
+ if (display == EGL10.EGL_NO_DISPLAY || !egl.eglInitialize(display, new int[2])) {
34
+ throw new IllegalStateException("Could not initialize the video EGL display");
35
+ }
36
+ EGLContext context = EGL10.EGL_NO_CONTEXT;
37
+ EGLSurface surface = EGL10.EGL_NO_SURFACE;
38
+ try {
39
+ int[] attributes = window == null ? new int[]{
40
+ EGL10.EGL_RED_SIZE, 8, EGL10.EGL_GREEN_SIZE, 8, EGL10.EGL_BLUE_SIZE, 8,
41
+ EGL10.EGL_ALPHA_SIZE, 8, EGL10.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
42
+ EGL10.EGL_SURFACE_TYPE, EGL10.EGL_PBUFFER_BIT, EGL10.EGL_NONE
43
+ } : new int[]{
44
+ EGL10.EGL_RED_SIZE, 8, EGL10.EGL_GREEN_SIZE, 8, EGL10.EGL_BLUE_SIZE, 8,
45
+ EGL10.EGL_ALPHA_SIZE, 8, EGL10.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
46
+ EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT,
47
+ EGLUtils.EGL_RECORDABLE_ANDROID, 1, EGL10.EGL_NONE
48
+ };
49
+ EGLConfig[] configs = new EGLConfig[1];
50
+ int[] count = new int[1];
51
+ if (!egl.eglChooseConfig(display, attributes, configs, 1, count) || count[0] == 0) {
52
+ throw new IllegalStateException("No video EGL config found");
53
+ }
54
+ context = egl.eglCreateContext(display, configs[0], sharedContext,
55
+ new int[]{EGLUtils.EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE});
56
+ if (context == EGL10.EGL_NO_CONTEXT) {
57
+ throw new IllegalStateException("Could not create video EGL context");
58
+ }
59
+ surface = window == null
60
+ ? egl.eglCreatePbufferSurface(display, configs[0],
61
+ new int[]{EGL10.EGL_WIDTH, 1, EGL10.EGL_HEIGHT, 1, EGL10.EGL_NONE})
62
+ : egl.eglCreateWindowSurface(display, configs[0], window, new int[]{EGL10.EGL_NONE});
63
+ if (surface == EGL10.EGL_NO_SURFACE) {
64
+ throw new IllegalStateException("Could not create video EGL surface");
65
+ }
66
+ return new EGLResourcesHolder(egl, display, context, surface);
67
+ } catch (RuntimeException error) {
68
+ if (surface != EGL10.EGL_NO_SURFACE) egl.eglDestroySurface(display, surface);
69
+ if (context != EGL10.EGL_NO_CONTEXT) egl.eglDestroyContext(display, context);
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ private EGLResourcesHolder(EGL10 egl, EGLDisplay display, EGLContext context, EGLSurface surface) {
75
+ this.egl = egl;
76
+ this.display = display;
77
+ this.context = context;
78
+ this.surface = surface;
79
+ }
80
+
81
+ public boolean makeCurrent() {
82
+ if (released || !egl.eglMakeCurrent(display, surface, surface, context)) {
83
+ throw new IllegalStateException("Could not bind video EGL context");
84
+ }
85
+ return true;
86
+ }
87
+
88
+ /** Always detach our context before returning, including when the caller has no EGL context. */
89
+ public <T> T withContextCurrent(Supplier<T> action) {
90
+ android.opengl.EGLDisplay previousDisplay = EGL14.eglGetCurrentDisplay();
91
+ android.opengl.EGLContext previousContext = EGL14.eglGetCurrentContext();
92
+ android.opengl.EGLSurface previousDraw = EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW);
93
+ android.opengl.EGLSurface previousRead = EGL14.eglGetCurrentSurface(EGL14.EGL_READ);
94
+ makeCurrent();
95
+ try {
96
+ return action.get();
97
+ } finally {
98
+ if (!EGL14.EGL_NO_CONTEXT.equals(previousContext)) {
99
+ if (!EGL14.eglMakeCurrent(previousDisplay, previousDraw, previousRead, previousContext)) {
100
+ throw new IllegalStateException("Could not restore the caller's EGL context");
101
+ }
102
+ } else if (!egl.eglMakeCurrent(display, EGL10.EGL_NO_SURFACE,
103
+ EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT)) {
104
+ throw new IllegalStateException("Could not detach video EGL context");
105
+ }
106
+ }
107
+ }
108
+
109
+ public void runWithContextCurrent(Runnable action) {
110
+ withContextCurrent(() -> { action.run(); return null; });
111
+ }
112
+
113
+ public boolean swapBuffers() { return egl.eglSwapBuffers(display, surface); }
114
+
115
+ public void setPresentationTime(long nanoseconds) {
116
+ if (!EGLExt.eglPresentationTimeANDROID(EGL14.eglGetCurrentDisplay(),
117
+ EGL14.eglGetCurrentSurface(EGL14.EGL_DRAW), nanoseconds)) {
118
+ throw new IllegalStateException("Could not timestamp video encoder surface");
119
+ }
120
+ }
121
+
122
+ public void release() {
123
+ if (released) return;
124
+ if (egl.eglGetCurrentContext().equals(context)) {
125
+ egl.eglMakeCurrent(display, EGL10.EGL_NO_SURFACE,
126
+ EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
127
+ }
128
+ egl.eglDestroySurface(display, surface);
129
+ egl.eglDestroyContext(display, context);
130
+ released = true;
131
+ }
132
+ }