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.
- package/android/cpp/AndroidPlatformContext.h +100 -9
- package/android/cpp/AndroidVideoPlayer.h +307 -0
- package/android/cpp/cpp-adapter.cpp +66 -6
- package/android/src/main/java/com/webgpu/ViewSnapshot.java +162 -0
- package/android/src/main/java/com/webgpu/WebGPUModule.java +78 -0
- package/android/src/main/java/com/webgpu/WebGPUVideoPlayer.java +420 -0
- package/apple/ApplePlatformContext.h +14 -1
- package/apple/ApplePlatformContext.mm +176 -1
- package/apple/AppleVideoPlayer.mm +157 -47
- package/apple/WebGPUModule.mm +20 -1
- package/cpp/rnwgpu/PlatformContext.h +76 -1
- package/cpp/rnwgpu/RNWebGPUManager.cpp +7 -0
- package/cpp/rnwgpu/api/GPUDevice.cpp +2 -1
- package/cpp/rnwgpu/api/GPUDevice.h +6 -0
- package/cpp/rnwgpu/api/GPUQueue.cpp +261 -4
- package/cpp/rnwgpu/api/GPUQueue.h +29 -2
- package/cpp/rnwgpu/api/VideoFrameBlit.h +355 -0
- package/cpp/rnwgpu/api/VideoPlayer.h +50 -3
- package/cpp/rnwgpu/api/descriptors/GPUDrawElementImageDestination.h +60 -0
- package/cpp/rnwgpu/api/descriptors/GPUDrawElementImageSource.h +106 -0
- package/cpp/rnwgpu/api/descriptors/GPUImageCopyExternalImage.h +30 -3
- package/lib/commonjs/importDevice.js +2 -2
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/main/index.js +45 -0
- package/lib/commonjs/main/index.js.map +1 -1
- package/lib/commonjs/main/index.web.js +11 -0
- package/lib/commonjs/main/index.web.js.map +1 -1
- package/lib/commonjs/video.js +58 -0
- package/lib/commonjs/video.js.map +1 -0
- package/lib/module/importDevice.js +2 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/main/index.js +34 -0
- package/lib/module/main/index.js.map +1 -1
- package/lib/module/main/index.web.js +1 -0
- package/lib/module/main/index.web.js.map +1 -1
- package/lib/module/video.js +49 -0
- package/lib/module/video.js.map +1 -0
- package/lib/typescript/lib/commonjs/video.d.ts +30 -0
- package/lib/typescript/lib/commonjs/video.d.ts.map +1 -0
- package/lib/typescript/lib/module/main/index.d.ts +1 -0
- package/lib/typescript/lib/module/main/index.web.d.ts +1 -0
- package/lib/typescript/lib/module/video.d.ts +4 -0
- package/lib/typescript/lib/module/video.d.ts.map +1 -0
- package/lib/typescript/src/importDevice.d.ts +2 -2
- package/lib/typescript/src/index.d.ts +8 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/main/index.d.ts +1 -0
- package/lib/typescript/src/main/index.d.ts.map +1 -1
- package/lib/typescript/src/main/index.web.d.ts +1 -0
- package/lib/typescript/src/main/index.web.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +32 -0
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/video.d.ts +41 -0
- package/lib/typescript/src/video.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/importDevice.ts +2 -2
- package/src/index.tsx +57 -3
- package/src/main/index.tsx +50 -0
- package/src/main/index.web.tsx +1 -0
- package/src/types.ts +89 -7
- 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
|
+
}
|