react-native-webgpu 0.10.5 → 0.11.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 (61) hide show
  1. package/android/cpp/AndroidPlatformContext.h +100 -9
  2. package/android/cpp/AndroidVideoPlayer.h +307 -0
  3. package/android/cpp/cpp-adapter.cpp +66 -6
  4. package/android/src/main/java/com/webgpu/ViewSnapshot.java +162 -0
  5. package/android/src/main/java/com/webgpu/WebGPUModule.java +78 -0
  6. package/android/src/main/java/com/webgpu/WebGPUVideoPlayer.java +420 -0
  7. package/apple/ApplePlatformContext.h +14 -1
  8. package/apple/ApplePlatformContext.mm +176 -1
  9. package/apple/AppleVideoPlayer.mm +157 -47
  10. package/apple/WebGPUModule.mm +20 -1
  11. package/cpp/rnwgpu/PlatformContext.h +76 -1
  12. package/cpp/rnwgpu/RNWebGPUManager.cpp +7 -0
  13. package/cpp/rnwgpu/api/GPUDevice.cpp +2 -1
  14. package/cpp/rnwgpu/api/GPUDevice.h +6 -0
  15. package/cpp/rnwgpu/api/GPUQueue.cpp +261 -4
  16. package/cpp/rnwgpu/api/GPUQueue.h +29 -2
  17. package/cpp/rnwgpu/api/VideoFrameBlit.h +355 -0
  18. package/cpp/rnwgpu/api/VideoPlayer.h +50 -3
  19. package/cpp/rnwgpu/api/descriptors/GPUDrawElementImageDestination.h +60 -0
  20. package/cpp/rnwgpu/api/descriptors/GPUDrawElementImageSource.h +106 -0
  21. package/cpp/rnwgpu/api/descriptors/GPUImageCopyExternalImage.h +30 -3
  22. package/lib/commonjs/importDevice.js +2 -2
  23. package/lib/commonjs/index.js.map +1 -1
  24. package/lib/commonjs/main/index.js +45 -0
  25. package/lib/commonjs/main/index.js.map +1 -1
  26. package/lib/commonjs/main/index.web.js +11 -0
  27. package/lib/commonjs/main/index.web.js.map +1 -1
  28. package/lib/commonjs/video.js +58 -0
  29. package/lib/commonjs/video.js.map +1 -0
  30. package/lib/module/importDevice.js +2 -2
  31. package/lib/module/index.js.map +1 -1
  32. package/lib/module/main/index.js +34 -0
  33. package/lib/module/main/index.js.map +1 -1
  34. package/lib/module/main/index.web.js +1 -0
  35. package/lib/module/main/index.web.js.map +1 -1
  36. package/lib/module/video.js +49 -0
  37. package/lib/module/video.js.map +1 -0
  38. package/lib/typescript/lib/commonjs/video.d.ts +30 -0
  39. package/lib/typescript/lib/commonjs/video.d.ts.map +1 -0
  40. package/lib/typescript/lib/module/main/index.d.ts +1 -0
  41. package/lib/typescript/lib/module/main/index.web.d.ts +1 -0
  42. package/lib/typescript/lib/module/video.d.ts +4 -0
  43. package/lib/typescript/lib/module/video.d.ts.map +1 -0
  44. package/lib/typescript/src/importDevice.d.ts +2 -2
  45. package/lib/typescript/src/index.d.ts +8 -3
  46. package/lib/typescript/src/index.d.ts.map +1 -1
  47. package/lib/typescript/src/main/index.d.ts +1 -0
  48. package/lib/typescript/src/main/index.d.ts.map +1 -1
  49. package/lib/typescript/src/main/index.web.d.ts +1 -0
  50. package/lib/typescript/src/main/index.web.d.ts.map +1 -1
  51. package/lib/typescript/src/types.d.ts +32 -0
  52. package/lib/typescript/src/types.d.ts.map +1 -1
  53. package/lib/typescript/src/video.d.ts +41 -0
  54. package/lib/typescript/src/video.d.ts.map +1 -0
  55. package/package.json +1 -1
  56. package/src/importDevice.ts +2 -2
  57. package/src/index.tsx +57 -3
  58. package/src/main/index.tsx +50 -0
  59. package/src/main/index.web.tsx +1 -0
  60. package/src/types.ts +89 -7
  61. package/src/video.ts +69 -0
@@ -0,0 +1,162 @@
1
+ package com.webgpu;
2
+
3
+ import android.graphics.Bitmap;
4
+ import android.graphics.Canvas;
5
+ import android.graphics.ColorSpace;
6
+ import android.graphics.HardwareRenderer;
7
+ import android.graphics.PixelFormat;
8
+ import android.graphics.RecordingCanvas;
9
+ import android.graphics.RenderNode;
10
+ import android.hardware.HardwareBuffer;
11
+ import android.media.Image;
12
+ import android.media.ImageReader;
13
+ import android.os.Build;
14
+ import android.util.Log;
15
+ import android.view.View;
16
+
17
+ import androidx.annotation.NonNull;
18
+ import androidx.annotation.RequiresApi;
19
+
20
+ /**
21
+ * Rasterizes a native view for GPUQueue.drawElementImageToTexture. Must be
22
+ * called on the UI thread.
23
+ *
24
+ * On API 29+ the view is rendered through {@link HardwareRenderer}: the
25
+ * recording reuses the children's existing display lists, so the result is
26
+ * exactly what the window's own renderer draws (shadows, hardware layers,
27
+ * TextureView content). Older devices, or a failed hardware render, fall back
28
+ * to drawing the view into a software canvas, which misses anything that is
29
+ * only drawn hardware-accelerated. SurfaceView content is composed outside
30
+ * the view hierarchy and is captured by neither path.
31
+ */
32
+ final class ViewSnapshot {
33
+ private static final String TAG = "WebGPUViewSnapshot";
34
+
35
+ private ViewSnapshot() {}
36
+
37
+ /**
38
+ * @param sourceX/Y/Width/Height the source rectangle in the view's own
39
+ * coordinate space, in pixels (already scaled by the display density)
40
+ * @param width/height the output size in pixels
41
+ * @return a software ARGB_8888 (premultiplied) bitmap of exactly
42
+ * width x height pixels
43
+ */
44
+ @NonNull
45
+ static Bitmap snapshot(
46
+ @NonNull View view,
47
+ float sourceX,
48
+ float sourceY,
49
+ float sourceWidth,
50
+ float sourceHeight,
51
+ int width,
52
+ int height) {
53
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && view.isHardwareAccelerated()) {
54
+ try {
55
+ return snapshotHardware(view, sourceX, sourceY, sourceWidth, sourceHeight, width, height);
56
+ } catch (Throwable t) {
57
+ Log.w(TAG, "HardwareRenderer snapshot failed, falling back to software rendering", t);
58
+ }
59
+ }
60
+ return snapshotSoftware(view, sourceX, sourceY, sourceWidth, sourceHeight, width, height);
61
+ }
62
+
63
+ @NonNull
64
+ private static Bitmap snapshotSoftware(
65
+ @NonNull View view,
66
+ float sourceX,
67
+ float sourceY,
68
+ float sourceWidth,
69
+ float sourceHeight,
70
+ int width,
71
+ int height) {
72
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
73
+ Canvas canvas = new Canvas(bitmap);
74
+ canvas.scale(width / sourceWidth, height / sourceHeight);
75
+ canvas.translate(-sourceX, -sourceY);
76
+ view.draw(canvas);
77
+ return bitmap;
78
+ }
79
+
80
+ @RequiresApi(Build.VERSION_CODES.Q)
81
+ @NonNull
82
+ private static Bitmap snapshotHardware(
83
+ @NonNull View view,
84
+ float sourceX,
85
+ float sourceY,
86
+ float sourceWidth,
87
+ float sourceHeight,
88
+ int width,
89
+ int height) {
90
+ // The renderer draws into an ImageReader whose buffers are GPU-sampleable
91
+ // (a later zero-copy path can import them directly); for now the frame is
92
+ // read back into a software bitmap.
93
+ ImageReader reader =
94
+ ImageReader.newInstance(
95
+ width,
96
+ height,
97
+ PixelFormat.RGBA_8888,
98
+ 1,
99
+ HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE | HardwareBuffer.USAGE_GPU_COLOR_OUTPUT);
100
+ HardwareRenderer renderer = new HardwareRenderer();
101
+ Image image = null;
102
+ try {
103
+ renderer.setSurface(reader.getSurface());
104
+ // Match the framework's default light so elevation shadows come out
105
+ // like on screen (ThreadedRenderer reads these from the theme; the
106
+ // Material defaults are used here).
107
+ float density = view.getResources().getDisplayMetrics().density;
108
+ renderer.setLightSourceGeometry(
109
+ width / 2f, -400f * density, 500f * density, 800f * density);
110
+ renderer.setLightSourceAlpha(0.039f, 0.19f);
111
+
112
+ RenderNode root = new RenderNode("rnwgpu-view-snapshot");
113
+ root.setPosition(0, 0, width, height);
114
+ RecordingCanvas canvas = root.beginRecording(width, height);
115
+ try {
116
+ canvas.scale(width / sourceWidth, height / sourceHeight);
117
+ canvas.translate(-sourceX, -sourceY);
118
+ view.draw(canvas);
119
+ } finally {
120
+ root.endRecording();
121
+ }
122
+ renderer.setContentRoot(root);
123
+
124
+ HardwareRenderer.FrameRenderRequest request = renderer.createRenderRequest();
125
+ request.setWaitForPresent(true);
126
+ int status = request.syncAndDraw();
127
+ int failed =
128
+ HardwareRenderer.SYNC_LOST_SURFACE_REWARD_IF_FOUND
129
+ | HardwareRenderer.SYNC_CONTEXT_IS_STOPPED
130
+ | HardwareRenderer.SYNC_FRAME_DROPPED;
131
+ if ((status & failed) != 0) {
132
+ throw new IllegalStateException("HardwareRenderer.syncAndDraw failed with status " + status);
133
+ }
134
+ image = reader.acquireNextImage();
135
+ if (image == null) {
136
+ throw new IllegalStateException("HardwareRenderer produced no image");
137
+ }
138
+ HardwareBuffer buffer = image.getHardwareBuffer();
139
+ if (buffer == null) {
140
+ throw new IllegalStateException("HardwareRenderer image has no HardwareBuffer");
141
+ }
142
+ Bitmap hardware =
143
+ Bitmap.wrapHardwareBuffer(buffer, ColorSpace.get(ColorSpace.Named.SRGB));
144
+ buffer.close();
145
+ if (hardware == null) {
146
+ throw new IllegalStateException("Bitmap.wrapHardwareBuffer failed");
147
+ }
148
+ Bitmap software = hardware.copy(Bitmap.Config.ARGB_8888, false);
149
+ hardware.recycle();
150
+ if (software == null) {
151
+ throw new IllegalStateException("could not read back the hardware bitmap");
152
+ }
153
+ return software;
154
+ } finally {
155
+ if (image != null) {
156
+ image.close();
157
+ }
158
+ renderer.destroy();
159
+ reader.close();
160
+ }
161
+ }
162
+ }
@@ -1,6 +1,8 @@
1
1
  package com.webgpu;
2
2
 
3
+ import android.graphics.Bitmap;
3
4
  import android.util.Log;
5
+ import android.view.View;
4
6
 
5
7
  import androidx.annotation.OptIn;
6
8
 
@@ -11,7 +13,10 @@ import com.facebook.proguard.annotations.DoNotStrip;
11
13
  import com.facebook.react.bridge.ReactApplicationContext;
12
14
  import com.facebook.react.bridge.JavaScriptContextHolder;
13
15
  import com.facebook.react.bridge.ReactMethod;
16
+ import com.facebook.react.bridge.UIManager;
17
+ import com.facebook.react.bridge.UiThreadUtil;
14
18
  import com.facebook.react.common.annotations.FrameworkAPI;
19
+ import com.facebook.react.uimanager.UIManagerHelper;
15
20
  import com.facebook.react.module.annotations.ReactModule;
16
21
  import com.facebook.react.modules.blob.BlobModule;
17
22
  import com.facebook.react.modules.blob.BlobProvider;
@@ -47,4 +52,77 @@ public class WebGPUModule extends NativeWebGPUModuleSpec {
47
52
  @OptIn(markerClass = FrameworkAPI.class)
48
53
  @DoNotStrip
49
54
  private native void initializeNative(long jsRuntime, CallInvokerHolderImpl jsInvoker, BlobModule blobModule);
55
+
56
+ /**
57
+ * Called from C++ (AndroidPlatformContext::snapshotView) on an arbitrary
58
+ * thread for GPUQueue.drawElementImageToTexture. Hops to the UI thread,
59
+ * resolves the view for {@code tag}, rasterizes the requested rectangle and
60
+ * always reports back through {@link #onViewSnapshot}, with either a bitmap
61
+ * or an error message. {@code callback} is an opaque native pointer that
62
+ * must be handed back exactly once.
63
+ *
64
+ * The source rectangle is in density-independent points (React Native
65
+ * layout units) and is converted to pixels here; a non-positive
66
+ * sourceWidth/sourceHeight means the whole view. A zero width/height means
67
+ * the natural pixel size of the source rectangle.
68
+ */
69
+ @DoNotStrip
70
+ void snapshotView(
71
+ final int tag,
72
+ final double sourceX,
73
+ final double sourceY,
74
+ final double sourceWidth,
75
+ final double sourceHeight,
76
+ final int width,
77
+ final int height,
78
+ final long callback) {
79
+ UiThreadUtil.runOnUiThread(
80
+ () -> {
81
+ Bitmap bitmap = null;
82
+ String error = null;
83
+ try {
84
+ ReactApplicationContext context = getReactApplicationContext();
85
+ UIManager uiManager = UIManagerHelper.getUIManagerForReactTag(context, tag);
86
+ View view = uiManager == null ? null : uiManager.resolveView(tag);
87
+ if (view == null) {
88
+ throw new IllegalArgumentException(
89
+ "drawElementImageToTexture: no native view found for tag "
90
+ + tag
91
+ + " (is the view mounted, and rendered with collapsable={false}?)");
92
+ }
93
+ if (view.getWidth() <= 0 || view.getHeight() <= 0) {
94
+ throw new IllegalStateException(
95
+ "drawElementImageToTexture: the view has no size yet");
96
+ }
97
+ float density = view.getResources().getDisplayMetrics().density;
98
+ float sx = (float) (sourceX * density);
99
+ float sy = (float) (sourceY * density);
100
+ float sw = sourceWidth > 0 ? (float) (sourceWidth * density) : view.getWidth();
101
+ float sh = sourceHeight > 0 ? (float) (sourceHeight * density) : view.getHeight();
102
+ int w = width > 0 ? width : Math.round(sw);
103
+ int h = height > 0 ? height : Math.round(sh);
104
+ if (w <= 0 || h <= 0) {
105
+ throw new IllegalArgumentException(
106
+ "drawElementImageToTexture: the source rectangle is empty");
107
+ }
108
+ bitmap = ViewSnapshot.snapshot(view, sx, sy, sw, sh, w, h);
109
+ } catch (Throwable t) {
110
+ error = t.getMessage() != null ? t.getMessage() : t.toString();
111
+ }
112
+ onViewSnapshot(callback, bitmap, error);
113
+ });
114
+ }
115
+
116
+ @DoNotStrip
117
+ private native void onViewSnapshot(long callback, Bitmap bitmap, String error);
118
+
119
+ /**
120
+ * Called from C++ (AndroidPlatformContext::createVideoPlayer) for
121
+ * RNWebGPU.createVideoPlayer. Throws when the source cannot be opened; the
122
+ * caller turns the exception into a JS error.
123
+ */
124
+ @DoNotStrip
125
+ WebGPUVideoPlayer createVideoPlayer(String source) {
126
+ return new WebGPUVideoPlayer(getReactApplicationContext(), source);
127
+ }
50
128
  }
@@ -0,0 +1,420 @@
1
+ package com.webgpu;
2
+
3
+ import android.content.Context;
4
+ import android.graphics.ImageFormat;
5
+ import android.hardware.HardwareBuffer;
6
+ import android.media.AudioAttributes;
7
+ import android.media.Image;
8
+ import android.media.ImageReader;
9
+ import android.media.MediaExtractor;
10
+ import android.media.MediaFormat;
11
+ import android.media.MediaMetadataRetriever;
12
+ import android.media.MediaPlayer;
13
+ import android.net.Uri;
14
+ import android.os.Build;
15
+ import android.util.Log;
16
+
17
+ import com.facebook.proguard.annotations.DoNotStrip;
18
+
19
+ import java.io.File;
20
+ import java.io.IOException;
21
+ import java.util.HashSet;
22
+ import java.util.Set;
23
+
24
+ /**
25
+ * Decodes a video with {@link MediaPlayer} into an {@link ImageReader} whose
26
+ * buffers the GPU can sample ({@link ImageFormat#PRIVATE} with
27
+ * {@link HardwareBuffer#USAGE_GPU_SAMPLED_IMAGE}). Each frame is handed to C++
28
+ * (AndroidVideoPlayer) as an {@link Image} that stays acquired until the
29
+ * NativeVideoFrame is released, which hands the buffer back to the decoder.
30
+ *
31
+ * The frames keep the decoder's native YUV layout, so they are sampled through
32
+ * GPUDevice.importExternalTexture. MediaPlayer owns the audio, the clock and
33
+ * the A/V synchronization.
34
+ *
35
+ * MediaPlayer is driven from the caller's thread (its methods are thread
36
+ * safe); its callbacks arrive on the main looper, so the shared state is
37
+ * guarded by {@code lock}.
38
+ */
39
+ @DoNotStrip
40
+ public class WebGPUVideoPlayer {
41
+ private static final String TAG = "WebGPUVideoPlayer";
42
+
43
+ // Frames the consumer can hold at once: the one being drawn, the next one,
44
+ // and a spare. Holding more makes copyLatestFrame() return null until a
45
+ // frame is released.
46
+ private static final int MAX_IMAGES = 3;
47
+
48
+ private final Object lock = new Object();
49
+ private final Set<Image> acquired = new HashSet<>();
50
+ private MediaPlayer player;
51
+ private ImageReader reader;
52
+ private boolean prepared = false;
53
+ private boolean playing = false;
54
+ private boolean loop = true;
55
+ private float volume = 1f;
56
+ private double pendingSeekSeconds = -1;
57
+ private boolean released = false;
58
+
59
+ private volatile int width = 0;
60
+ private volatile int height = 0;
61
+ private volatile int rotation = 0;
62
+ private volatile double frameRate = 0;
63
+ private volatile double durationSeconds = 0;
64
+
65
+ WebGPUVideoPlayer(Context context, String source) {
66
+ Uri uri = parseSource(source);
67
+ readMetadata(context, uri);
68
+
69
+ MediaPlayer mp = new MediaPlayer();
70
+ mp.setAudioAttributes(
71
+ new AudioAttributes.Builder()
72
+ .setUsage(AudioAttributes.USAGE_MEDIA)
73
+ .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE)
74
+ .build());
75
+ mp.setOnVideoSizeChangedListener((p, w, h) -> onVideoSizeChanged(w, h));
76
+ mp.setOnPreparedListener(this::onPrepared);
77
+ mp.setOnCompletionListener(
78
+ p -> {
79
+ // Not called while looping: MediaPlayer restarts on its own.
80
+ synchronized (lock) {
81
+ if (!loop) {
82
+ playing = false;
83
+ }
84
+ }
85
+ });
86
+ mp.setOnErrorListener(
87
+ (p, what, extra) -> {
88
+ Log.e(TAG, "MediaPlayer error " + what + " (" + extra + ") for " + source);
89
+ synchronized (lock) {
90
+ playing = false;
91
+ }
92
+ return true;
93
+ });
94
+ try {
95
+ mp.setDataSource(context, uri);
96
+ } catch (IOException | IllegalArgumentException | SecurityException e) {
97
+ mp.release();
98
+ throw new RuntimeException("createVideoPlayer: cannot open " + source, e);
99
+ }
100
+ mp.setLooping(loop);
101
+ mp.prepareAsync();
102
+ synchronized (lock) {
103
+ player = mp;
104
+ }
105
+ }
106
+
107
+ // A URL (http, https, file, content) or a plain file path.
108
+ private static Uri parseSource(String source) {
109
+ Uri uri = Uri.parse(source);
110
+ if (uri.getScheme() == null) {
111
+ return Uri.fromFile(new File(source));
112
+ }
113
+ return uri;
114
+ }
115
+
116
+ // The frame geometry comes from the container (MediaMetadataRetriever for
117
+ // the size, rotation and duration, MediaExtractor for the frame rate). The
118
+ // metadata of a remote video is fetched off the calling thread; the getters
119
+ // report 0 until it is known.
120
+ private void readMetadata(Context context, Uri uri) {
121
+ Thread thread =
122
+ new Thread(
123
+ () -> {
124
+ MediaMetadataRetriever retriever = new MediaMetadataRetriever();
125
+ try {
126
+ String scheme = uri.getScheme();
127
+ if ("http".equals(scheme) || "https".equals(scheme)) {
128
+ retriever.setDataSource(uri.toString(), new java.util.HashMap<>());
129
+ } else {
130
+ retriever.setDataSource(context, uri);
131
+ }
132
+ int w = parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
133
+ int h = parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT));
134
+ if (w > 0 && h > 0 && width == 0) {
135
+ width = w;
136
+ height = h;
137
+ }
138
+ rotation = parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION));
139
+ long durationMs = parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));
140
+ if (durationMs > 0 && durationSeconds == 0) {
141
+ durationSeconds = durationMs / 1000.0;
142
+ }
143
+ } catch (RuntimeException e) {
144
+ Log.w(TAG, "Could not read the metadata of " + uri + ": " + e.getMessage());
145
+ } finally {
146
+ try {
147
+ retriever.release();
148
+ } catch (IOException ignored) {
149
+ // Nothing to recover.
150
+ }
151
+ }
152
+
153
+ MediaExtractor extractor = new MediaExtractor();
154
+ try {
155
+ extractor.setDataSource(context, uri, null);
156
+ for (int i = 0; i < extractor.getTrackCount(); i++) {
157
+ MediaFormat format = extractor.getTrackFormat(i);
158
+ String mime = format.getString(MediaFormat.KEY_MIME);
159
+ if (mime != null && mime.startsWith("video/")) {
160
+ if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) {
161
+ frameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE);
162
+ }
163
+ break;
164
+ }
165
+ }
166
+ } catch (IOException | RuntimeException e) {
167
+ Log.w(TAG, "Could not read the frame rate of " + uri + ": " + e.getMessage());
168
+ } finally {
169
+ extractor.release();
170
+ }
171
+ },
172
+ "WebGPUVideoPlayer metadata");
173
+ thread.setDaemon(true);
174
+ thread.start();
175
+ }
176
+
177
+ private static int parseInt(String value) {
178
+ if (value == null) {
179
+ return 0;
180
+ }
181
+ try {
182
+ return Integer.parseInt(value);
183
+ } catch (NumberFormatException e) {
184
+ return 0;
185
+ }
186
+ }
187
+
188
+ // The decoder reports the frame size (possibly more than once for adaptive
189
+ // streams); the ImageReader that receives the frames is sized accordingly.
190
+ private void onVideoSizeChanged(int w, int h) {
191
+ if (w <= 0 || h <= 0) {
192
+ return;
193
+ }
194
+ ImageReader previous;
195
+ synchronized (lock) {
196
+ if (released || player == null) {
197
+ return;
198
+ }
199
+ if (reader != null && reader.getWidth() == w && reader.getHeight() == h) {
200
+ return;
201
+ }
202
+ if (width == 0) {
203
+ width = w;
204
+ height = h;
205
+ }
206
+ previous = reader;
207
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
208
+ reader = ImageReader.newInstance(w, h, ImageFormat.PRIVATE, MAX_IMAGES, HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
209
+ } else {
210
+ reader = ImageReader.newInstance(w, h, ImageFormat.PRIVATE, MAX_IMAGES);
211
+ }
212
+ player.setSurface(reader.getSurface());
213
+ }
214
+ if (previous != null) {
215
+ // Frames still held by the consumer were acquired from the old reader;
216
+ // closing it closes them, and the native side keeps the buffers alive.
217
+ synchronized (lock) {
218
+ acquired.clear();
219
+ }
220
+ previous.close();
221
+ }
222
+ }
223
+
224
+ private void onPrepared(MediaPlayer mp) {
225
+ synchronized (lock) {
226
+ if (released) {
227
+ return;
228
+ }
229
+ prepared = true;
230
+ int durationMs = mp.getDuration();
231
+ if (durationMs > 0) {
232
+ durationSeconds = durationMs / 1000.0;
233
+ }
234
+ mp.setVolume(volume, volume);
235
+ mp.setLooping(loop);
236
+ if (pendingSeekSeconds >= 0) {
237
+ mp.seekTo((long) (pendingSeekSeconds * 1000), MediaPlayer.SEEK_CLOSEST);
238
+ pendingSeekSeconds = -1;
239
+ }
240
+ if (playing) {
241
+ mp.start();
242
+ }
243
+ }
244
+ }
245
+
246
+ /**
247
+ * The most recently decoded frame, or null when no new frame was decoded
248
+ * since the last call (or when the consumer holds {@link #MAX_IMAGES}
249
+ * frames). Close it with {@link #closeImage} once the frame is released.
250
+ */
251
+ @DoNotStrip
252
+ public Image copyLatestFrame() {
253
+ ImageReader current;
254
+ synchronized (lock) {
255
+ if (released || reader == null) {
256
+ return null;
257
+ }
258
+ current = reader;
259
+ }
260
+ Image image;
261
+ try {
262
+ image = current.acquireLatestImage();
263
+ } catch (IllegalStateException e) {
264
+ Log.w(TAG, "copyLatestFrame: every frame is held, release one to get a new frame");
265
+ return null;
266
+ }
267
+ if (image == null) {
268
+ return null;
269
+ }
270
+ synchronized (lock) {
271
+ acquired.add(image);
272
+ }
273
+ return image;
274
+ }
275
+
276
+ /** Hands the buffer of a frame returned by {@link #copyLatestFrame} back to the decoder. */
277
+ @DoNotStrip
278
+ public void closeImage(Image image) {
279
+ synchronized (lock) {
280
+ if (!acquired.remove(image)) {
281
+ return;
282
+ }
283
+ }
284
+ image.close();
285
+ }
286
+
287
+ @DoNotStrip
288
+ public void play() {
289
+ synchronized (lock) {
290
+ playing = true;
291
+ if (player != null && prepared) {
292
+ player.start();
293
+ }
294
+ }
295
+ }
296
+
297
+ @DoNotStrip
298
+ public void pause() {
299
+ synchronized (lock) {
300
+ playing = false;
301
+ if (player != null && prepared && player.isPlaying()) {
302
+ player.pause();
303
+ }
304
+ }
305
+ }
306
+
307
+ @DoNotStrip
308
+ public boolean getPaused() {
309
+ synchronized (lock) {
310
+ return !playing;
311
+ }
312
+ }
313
+
314
+ @DoNotStrip
315
+ public double getCurrentTime() {
316
+ synchronized (lock) {
317
+ if (player != null && prepared) {
318
+ return player.getCurrentPosition() / 1000.0;
319
+ }
320
+ return pendingSeekSeconds >= 0 ? pendingSeekSeconds : 0;
321
+ }
322
+ }
323
+
324
+ // An exact seek, also while paused: the decoder then renders the frame at
325
+ // the new position, which the next copyLatestFrame() returns.
326
+ @DoNotStrip
327
+ public void seek(double seconds) {
328
+ synchronized (lock) {
329
+ if (player != null && prepared) {
330
+ player.seekTo((long) (Math.max(0, seconds) * 1000), MediaPlayer.SEEK_CLOSEST);
331
+ } else {
332
+ pendingSeekSeconds = Math.max(0, seconds);
333
+ }
334
+ }
335
+ }
336
+
337
+ @DoNotStrip
338
+ public double getDuration() {
339
+ return durationSeconds;
340
+ }
341
+
342
+ @DoNotStrip
343
+ public double getVolume() {
344
+ synchronized (lock) {
345
+ return volume;
346
+ }
347
+ }
348
+
349
+ @DoNotStrip
350
+ public void setVolume(double value) {
351
+ synchronized (lock) {
352
+ volume = (float) Math.max(0, Math.min(1, value));
353
+ if (player != null) {
354
+ player.setVolume(volume, volume);
355
+ }
356
+ }
357
+ }
358
+
359
+ @DoNotStrip
360
+ public boolean getLoop() {
361
+ synchronized (lock) {
362
+ return loop;
363
+ }
364
+ }
365
+
366
+ @DoNotStrip
367
+ public void setLoop(boolean value) {
368
+ synchronized (lock) {
369
+ loop = value;
370
+ if (player != null) {
371
+ player.setLooping(value);
372
+ }
373
+ }
374
+ }
375
+
376
+ @DoNotStrip
377
+ public int getVideoWidth() {
378
+ return width;
379
+ }
380
+
381
+ @DoNotStrip
382
+ public int getVideoHeight() {
383
+ return height;
384
+ }
385
+
386
+ @DoNotStrip
387
+ public int getRotation() {
388
+ return rotation;
389
+ }
390
+
391
+ @DoNotStrip
392
+ public double getFrameRate() {
393
+ return frameRate;
394
+ }
395
+
396
+ /** Stops playback and releases the decoder. Frames already copied stay valid until closed. */
397
+ @DoNotStrip
398
+ public void release() {
399
+ MediaPlayer mp;
400
+ ImageReader current;
401
+ synchronized (lock) {
402
+ if (released) {
403
+ return;
404
+ }
405
+ released = true;
406
+ playing = false;
407
+ mp = player;
408
+ player = null;
409
+ current = reader;
410
+ reader = null;
411
+ acquired.clear();
412
+ }
413
+ if (mp != null) {
414
+ mp.release();
415
+ }
416
+ if (current != null) {
417
+ current.close();
418
+ }
419
+ }
420
+ }