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,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,72 @@
1
+ package com.azzapp.rnskv;
2
+
3
+ import android.content.Context;
4
+ import android.hardware.display.DisplayManager;
5
+ import android.view.Display;
6
+
7
+ /**
8
+ * The vsync period of a display, kept current as its refresh rate changes.
9
+ *
10
+ * Taken from the display rather than from the gaps between vsyncs: those
11
+ * cannot tell a slower refresh rate from missed vsyncs, and an adaptive
12
+ * display dropping from 90 or 120 Hz to 60 Hz left such an estimate at the
13
+ * old rate.
14
+ */
15
+ final class DisplayVsyncPeriod implements DisplayManager.DisplayListener {
16
+ private final DisplayManager displayManager;
17
+ private final Display display;
18
+ private volatile long periodNanos;
19
+
20
+ private DisplayVsyncPeriod(DisplayManager displayManager, Display display) {
21
+ this.displayManager = displayManager;
22
+ this.display = display;
23
+ update();
24
+ }
25
+
26
+ /**
27
+ * Follows the display's refresh rate changes on the calling thread's looper.
28
+ *
29
+ * @param display the display to follow, null for the default one
30
+ * @return null when the display cannot be followed
31
+ */
32
+ static DisplayVsyncPeriod watch(Context context, Display display) {
33
+ DisplayManager displayManager = context.getSystemService(DisplayManager.class);
34
+ if (displayManager == null) return null;
35
+ if (display == null) display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
36
+ if (display == null) return null;
37
+ DisplayVsyncPeriod period = new DisplayVsyncPeriod(displayManager, display);
38
+ displayManager.registerDisplayListener(period, null);
39
+ return period;
40
+ }
41
+
42
+ /**
43
+ * @return the display's vsync period in nanoseconds, 0 when unknown
44
+ */
45
+ long getPeriodNanos() {
46
+ return periodNanos;
47
+ }
48
+
49
+ void release() {
50
+ displayManager.unregisterDisplayListener(this);
51
+ }
52
+
53
+ private void update() {
54
+ float rate = display.getRefreshRate();
55
+ periodNanos = rate > 0 ? (long) (1_000_000_000L / rate) : 0;
56
+ }
57
+
58
+ @Override
59
+ public void onDisplayAdded(int displayId) {
60
+ }
61
+
62
+ @Override
63
+ public void onDisplayRemoved(int displayId) {
64
+ }
65
+
66
+ @Override
67
+ public void onDisplayChanged(int displayId) {
68
+ if (displayId == display.getDisplayId()) {
69
+ update();
70
+ }
71
+ }
72
+ }