@nativescript/canvas-media 2.0.0-webgpu.9 → 2.0.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.
- package/audio/common.d.ts +2 -5
- package/audio/common.js +3 -20
- package/audio/common.js.map +1 -1
- package/audio/index.d.ts +2 -3
- package/audio/index.ios.js +3 -2
- package/audio/index.ios.js.map +1 -1
- package/common.d.ts +9 -6
- package/common.js +38 -8
- package/common.js.map +1 -1
- package/index.d.ts +4 -3
- package/index.js +4 -3
- package/index.js.map +1 -1
- package/package.json +1 -1
- package/platforms/android/canvas_media.aar +0 -0
- package/platforms/android/java/org/nativescript/canvas/media/VideoHelper.java +505 -12
- package/platforms/ios/build.xcconfig +1 -0
- package/platforms/ios/src/NSCCanvasMediaUmbrella.h +3 -0
- package/platforms/ios/src/NSCPlayerState.h +23 -0
- package/platforms/ios/src/NSCVideoHelper+Internal.h +26 -0
- package/platforms/ios/src/NSCVideoHelper.h +46 -0
- package/platforms/ios/src/NSCVideoHelper.m +263 -0
- package/platforms/ios/src/NSCVideoHelperListener.h +21 -0
- package/platforms/ios/src/module.modulemap +5 -0
- package/source.d.ts +11 -0
- package/source.js +29 -0
- package/source.js.map +1 -0
- package/video/common.d.ts +5 -11
- package/video/common.js +12 -63
- package/video/common.js.map +1 -1
- package/video/index.android.d.ts +8 -1
- package/video/index.android.js +118 -4
- package/video/index.android.js.map +1 -1
- package/video/index.d.ts +2 -3
- package/video/index.ios.d.ts +18 -21
- package/video/index.ios.js +235 -215
- package/video/index.ios.js.map +1 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
package org.nativescript.canvas.media;
|
|
2
2
|
|
|
3
3
|
import android.content.Context;
|
|
4
|
+
import android.graphics.ImageFormat;
|
|
5
|
+
import android.media.Image;
|
|
6
|
+
import android.media.ImageReader;
|
|
4
7
|
import android.net.Uri;
|
|
5
8
|
import android.os.Build;
|
|
6
9
|
import android.os.Handler;
|
|
@@ -20,6 +23,7 @@ import java.util.Collections;
|
|
|
20
23
|
import java.util.List;
|
|
21
24
|
import java.util.Timer;
|
|
22
25
|
import java.util.TimerTask;
|
|
26
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
23
27
|
|
|
24
28
|
|
|
25
29
|
import android.view.TextureView;
|
|
@@ -84,6 +88,7 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
84
88
|
private CacheDataSource.Factory _cdsf;
|
|
85
89
|
boolean _isCustom = false;
|
|
86
90
|
boolean _playing = false;
|
|
91
|
+
private final AtomicBoolean _loadedDataFired = new AtomicBoolean(false);
|
|
87
92
|
Timer _timer;
|
|
88
93
|
SurfaceTexture _st;
|
|
89
94
|
Surface _surface;
|
|
@@ -95,10 +100,47 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
95
100
|
int _videoWidth = 0;
|
|
96
101
|
int _videoHeight = 0;
|
|
97
102
|
|
|
98
|
-
boolean _hasFrame = false;
|
|
103
|
+
volatile boolean _hasFrame = false;
|
|
99
104
|
static int BUFFER_MS = 500;
|
|
100
105
|
public static boolean IS_DEBUG = true;
|
|
101
106
|
|
|
107
|
+
SurfaceTexture _glSt;
|
|
108
|
+
Surface _glSurface;
|
|
109
|
+
int _glTextureId = -1;
|
|
110
|
+
volatile boolean _glHasFrame = false;
|
|
111
|
+
Object _glRender; // TextureRender — kept alive to own the OES texture
|
|
112
|
+
private boolean _glInitFailed = false;
|
|
113
|
+
// Tracks which path currently owns the player's video surface to prevent conflicts.
|
|
114
|
+
private enum SurfaceOwner { NONE, WEBGL, GL2D, BITMAP }
|
|
115
|
+
private SurfaceOwner _surfaceOwner = SurfaceOwner.NONE;
|
|
116
|
+
|
|
117
|
+
// ImageReader-backed surface for the CPU/Vulkan bitmap path.
|
|
118
|
+
// Provides video frames without requiring any view hierarchy or window attachment —
|
|
119
|
+
// the Android equivalent of a detached <video> element on the web.
|
|
120
|
+
private ImageReader _imageReader;
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
// Cached reflection for drawVideoFrame2D — resolved once, reused per frame
|
|
124
|
+
private static Class<?> _drawImageClass;
|
|
125
|
+
private static Method _drawImageDxDy;
|
|
126
|
+
private static Method _drawImageDxDyDwDh;
|
|
127
|
+
private static Method _drawImageSrcDst;
|
|
128
|
+
private static boolean _drawImageResolved = false;
|
|
129
|
+
|
|
130
|
+
private static void ensureDrawImageMethods() {
|
|
131
|
+
if (_drawImageResolved) return;
|
|
132
|
+
try {
|
|
133
|
+
_drawImageClass = Class.forName("org.nativescript.canvas.NSCCanvasRenderingContext2D");
|
|
134
|
+
_drawImageDxDy = _drawImageClass.getMethod("drawImage", long.class, android.graphics.Bitmap.class, float.class, float.class);
|
|
135
|
+
_drawImageDxDyDwDh = _drawImageClass.getMethod("drawImage", long.class, android.graphics.Bitmap.class, float.class, float.class, float.class, float.class);
|
|
136
|
+
_drawImageSrcDst = _drawImageClass.getMethod("drawImage", long.class, android.graphics.Bitmap.class, float.class, float.class, float.class, float.class, float.class, float.class, float.class, float.class);
|
|
137
|
+
} catch (Exception ignored) {}
|
|
138
|
+
_drawImageResolved = true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Reusable bitmap for getCurrentBitmap to avoid per-frame allocation
|
|
142
|
+
private android.graphics.Bitmap _reusableBitmap;
|
|
143
|
+
|
|
102
144
|
public interface Callback {
|
|
103
145
|
void onDurationChange(long duration);
|
|
104
146
|
|
|
@@ -109,6 +151,10 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
109
151
|
void onCurrentTimeChanged(long time);
|
|
110
152
|
|
|
111
153
|
void onVideoFrame();
|
|
154
|
+
|
|
155
|
+
void onLoadedData();
|
|
156
|
+
|
|
157
|
+
void onError(String message);
|
|
112
158
|
}
|
|
113
159
|
|
|
114
160
|
private Callback _callback;
|
|
@@ -190,6 +236,9 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
190
236
|
if (VideoHelper.IS_DEBUG) {
|
|
191
237
|
android.util.Log.d("JS", "" + error);
|
|
192
238
|
}
|
|
239
|
+
if (this._callback != null) {
|
|
240
|
+
this._callback.onError(error.getMessage());
|
|
241
|
+
}
|
|
193
242
|
}
|
|
194
243
|
|
|
195
244
|
|
|
@@ -265,6 +314,12 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
265
314
|
|
|
266
315
|
private Method canvasUpdateTexImage;
|
|
267
316
|
|
|
317
|
+
private Method canvasCreate2DContextSurfaceTexture;
|
|
318
|
+
private Method canvasDrawVideoFrameGL;
|
|
319
|
+
|
|
320
|
+
private Method canvasUpdateTexImageFor3D;
|
|
321
|
+
private Method canvasUpdateTexSubImageFor3D;
|
|
322
|
+
|
|
268
323
|
Utils() {
|
|
269
324
|
try {
|
|
270
325
|
canvasUtils = Class.forName("org.nativescript.canvas.Utils");
|
|
@@ -276,6 +331,70 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
276
331
|
android.util.Log.d("JS", "" + e);
|
|
277
332
|
}
|
|
278
333
|
}
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
canvasCreate2DContextSurfaceTexture = canvasUtils.getDeclaredMethod(
|
|
337
|
+
"create2DContextSurfaceTexture", Long.TYPE);
|
|
338
|
+
canvasDrawVideoFrameGL = canvasUtils.getDeclaredMethod(
|
|
339
|
+
"drawVideoFrameGL",
|
|
340
|
+
Long.TYPE, // context
|
|
341
|
+
Integer.TYPE, // textureId
|
|
342
|
+
SurfaceTexture.class,
|
|
343
|
+
Integer.TYPE, // videoWidth
|
|
344
|
+
Integer.TYPE, // videoHeight
|
|
345
|
+
Float.TYPE, // sx
|
|
346
|
+
Float.TYPE, // sy
|
|
347
|
+
Float.TYPE, // sw
|
|
348
|
+
Float.TYPE, // sh
|
|
349
|
+
Float.TYPE, // dx
|
|
350
|
+
Float.TYPE, // dy
|
|
351
|
+
Float.TYPE, // dw
|
|
352
|
+
Float.TYPE // dh
|
|
353
|
+
);
|
|
354
|
+
} catch (Exception e) {
|
|
355
|
+
if (IS_DEBUG) {
|
|
356
|
+
android.util.Log.d("JS", "GL fast-path unavailable: " + e);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
Class<?> TextureRenderClass = Class.forName("org.nativescript.canvas.TextureRender");
|
|
362
|
+
canvasUpdateTexImageFor3D = canvasUtils.getDeclaredMethod(
|
|
363
|
+
"updateTexImageFor3D",
|
|
364
|
+
Long.TYPE, // state
|
|
365
|
+
Boolean.TYPE, // flipY
|
|
366
|
+
SurfaceTexture.class,
|
|
367
|
+
TextureRenderClass,
|
|
368
|
+
Integer.TYPE, // videoWidth
|
|
369
|
+
Integer.TYPE, // videoHeight
|
|
370
|
+
Integer.TYPE, // target
|
|
371
|
+
Integer.TYPE, // level
|
|
372
|
+
Integer.TYPE, // internalformat
|
|
373
|
+
Integer.TYPE, // width
|
|
374
|
+
Integer.TYPE, // height
|
|
375
|
+
Integer.TYPE, // depth
|
|
376
|
+
Integer.TYPE, // border
|
|
377
|
+
Integer.TYPE // zoffset
|
|
378
|
+
);
|
|
379
|
+
canvasUpdateTexSubImageFor3D = canvasUtils.getDeclaredMethod(
|
|
380
|
+
"updateTexSubImageFor3D",
|
|
381
|
+
Long.TYPE, // state
|
|
382
|
+
Boolean.TYPE, // flipY
|
|
383
|
+
SurfaceTexture.class,
|
|
384
|
+
TextureRenderClass,
|
|
385
|
+
Integer.TYPE, // target
|
|
386
|
+
Integer.TYPE, // level
|
|
387
|
+
Integer.TYPE, // xoffset
|
|
388
|
+
Integer.TYPE, // yoffset
|
|
389
|
+
Integer.TYPE, // zoffset
|
|
390
|
+
Integer.TYPE, // width
|
|
391
|
+
Integer.TYPE // height
|
|
392
|
+
);
|
|
393
|
+
} catch (Exception e) {
|
|
394
|
+
if (IS_DEBUG) {
|
|
395
|
+
android.util.Log.d("JS", "3D texture fast-path unavailable: " + e);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
279
398
|
}
|
|
280
399
|
|
|
281
400
|
Object createRenderAndAttachToGLContext(long state, SurfaceTexture st) {
|
|
@@ -307,6 +426,74 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
307
426
|
} catch (ClassNotFoundException ignored) {
|
|
308
427
|
}
|
|
309
428
|
}
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
public boolean updateTexImageFor3D(long state, boolean flipY, SurfaceTexture st, Object render,
|
|
432
|
+
int videoWidth, int videoHeight,
|
|
433
|
+
int target, int level, int internalformat,
|
|
434
|
+
int width, int height, int depth, int border, int zoffset) {
|
|
435
|
+
if (canvasUpdateTexImageFor3D == null) return false;
|
|
436
|
+
try {
|
|
437
|
+
Class<?> TextureRender = Class.forName("org.nativescript.canvas.TextureRender");
|
|
438
|
+
canvasUpdateTexImageFor3D.invoke(null, state, flipY, st, TextureRender.cast(render),
|
|
439
|
+
videoWidth, videoHeight, target, level, internalformat,
|
|
440
|
+
width, height, depth, border, zoffset);
|
|
441
|
+
return true;
|
|
442
|
+
} catch (Exception e) {
|
|
443
|
+
if (IS_DEBUG) android.util.Log.d("JS", "updateTexImageFor3D: " + e);
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
public boolean updateTexSubImageFor3D(long state, boolean flipY, SurfaceTexture st, Object render,
|
|
449
|
+
int target, int level,
|
|
450
|
+
int xoffset, int yoffset, int zoffset,
|
|
451
|
+
int width, int height) {
|
|
452
|
+
if (canvasUpdateTexSubImageFor3D == null) return false;
|
|
453
|
+
try {
|
|
454
|
+
Class<?> TextureRender = Class.forName("org.nativescript.canvas.TextureRender");
|
|
455
|
+
canvasUpdateTexSubImageFor3D.invoke(null, state, flipY, st, TextureRender.cast(render),
|
|
456
|
+
target, level, xoffset, yoffset, zoffset, width, height);
|
|
457
|
+
return true;
|
|
458
|
+
} catch (Exception e) {
|
|
459
|
+
if (IS_DEBUG) android.util.Log.d("JS", "updateTexSubImageFor3D: " + e);
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
Object[] create2DContextSurfaceTexture(long context) {
|
|
466
|
+
if (canvasCreate2DContextSurfaceTexture == null) return null;
|
|
467
|
+
try {
|
|
468
|
+
return (Object[]) canvasCreate2DContextSurfaceTexture.invoke(null, context);
|
|
469
|
+
} catch (IllegalAccessException ignored) {
|
|
470
|
+
return null;
|
|
471
|
+
} catch (InvocationTargetException e) {
|
|
472
|
+
if (IS_DEBUG) android.util.Log.d("JS", "create2DContextSurfaceTexture: " + e.getCause());
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
boolean drawVideoFrameGL(
|
|
478
|
+
long context, int textureId, SurfaceTexture surfaceTexture,
|
|
479
|
+
int videoWidth, int videoHeight,
|
|
480
|
+
float sx, float sy, float sw, float sh,
|
|
481
|
+
float dx, float dy, float dw, float dh) {
|
|
482
|
+
if (canvasDrawVideoFrameGL == null) return false;
|
|
483
|
+
try {
|
|
484
|
+
Object result = canvasDrawVideoFrameGL.invoke(
|
|
485
|
+
null, context, textureId, surfaceTexture,
|
|
486
|
+
videoWidth, videoHeight,
|
|
487
|
+
sx, sy, sw, sh,
|
|
488
|
+
dx, dy, dw, dh);
|
|
489
|
+
return result instanceof Boolean && (Boolean) result;
|
|
490
|
+
} catch (IllegalAccessException ignored) {
|
|
491
|
+
return false;
|
|
492
|
+
} catch (InvocationTargetException e) {
|
|
493
|
+
if (IS_DEBUG) android.util.Log.d("JS", "drawVideoFrameGL: " + e.getCause());
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
310
497
|
}
|
|
311
498
|
|
|
312
499
|
|
|
@@ -331,11 +518,16 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
331
518
|
Object[] result = _utils.createSurfaceTexture(state);
|
|
332
519
|
assert result != null;
|
|
333
520
|
this._st = (SurfaceTexture) result[0];
|
|
334
|
-
|
|
335
521
|
this._st.setOnFrameAvailableListener(this);
|
|
336
|
-
|
|
337
522
|
this._surface = new Surface(this._st);
|
|
523
|
+
// Release ImageReader if the bitmap path claimed the surface first.
|
|
524
|
+
if (_surfaceOwner == SurfaceOwner.BITMAP) {
|
|
525
|
+
if (_imageReader != null) { _imageReader.close(); _imageReader = null; }
|
|
526
|
+
_surfaceOwner = SurfaceOwner.NONE;
|
|
527
|
+
}
|
|
528
|
+
// Claim the surface for the WebGL path; GL2D path must not override this.
|
|
338
529
|
this._player.setVideoSurface(this._surface);
|
|
530
|
+
_surfaceOwner = SurfaceOwner.WEBGL;
|
|
339
531
|
this._render = result[1];
|
|
340
532
|
}
|
|
341
533
|
|
|
@@ -348,10 +540,288 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
348
540
|
}
|
|
349
541
|
}
|
|
350
542
|
|
|
543
|
+
|
|
544
|
+
public boolean getFrameForTexImage3D(boolean isLoaded, long state, boolean flipY,
|
|
545
|
+
int target, int level, int internalformat,
|
|
546
|
+
int width, int height, int depth, int border) {
|
|
547
|
+
ensureGLResources3D(state, width, height);
|
|
548
|
+
if (this._st == null || !this._hasFrame) return false;
|
|
549
|
+
|
|
550
|
+
boolean drawn = _utils.updateTexImageFor3D(state, flipY, this._st, this._render,
|
|
551
|
+
this._videoWidth, this._videoHeight,
|
|
552
|
+
target, level, internalformat, width, height, depth, border, 0);
|
|
553
|
+
this._hasFrame = false;
|
|
554
|
+
return drawn;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
public boolean getFrameForTexSubImage3D(boolean isLoaded, long state, boolean flipY,
|
|
558
|
+
int target, int level,
|
|
559
|
+
int xoffset, int yoffset, int zoffset,
|
|
560
|
+
int width, int height) {
|
|
561
|
+
ensureGLResources3D(state, width, height);
|
|
562
|
+
if (this._st == null || !this._hasFrame) return false;
|
|
563
|
+
|
|
564
|
+
boolean drawn = _utils.updateTexSubImageFor3D(state, flipY, this._st, this._render,
|
|
565
|
+
target, level, xoffset, yoffset, zoffset, width, height);
|
|
566
|
+
this._hasFrame = false;
|
|
567
|
+
return drawn;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
private void ensureGLResources3D(long state, int width, int height) {
|
|
572
|
+
if (this._st != null && this._render != null) return; // already set up
|
|
573
|
+
|
|
574
|
+
Object[] result = _utils.createSurfaceTexture(state);
|
|
575
|
+
if (result == null) return;
|
|
576
|
+
this._st = (SurfaceTexture) result[0];
|
|
577
|
+
this._st.setDefaultBufferSize(width, height);
|
|
578
|
+
this._st.setOnFrameAvailableListener(this);
|
|
579
|
+
this._surface = new Surface(this._st);
|
|
580
|
+
// Release the ImageReader if the bitmap path previously claimed the surface.
|
|
581
|
+
if (_surfaceOwner == SurfaceOwner.BITMAP) {
|
|
582
|
+
if (_imageReader != null) { _imageReader.close(); _imageReader = null; }
|
|
583
|
+
_surfaceOwner = SurfaceOwner.NONE;
|
|
584
|
+
}
|
|
585
|
+
this._player.setVideoSurface(this._surface);
|
|
586
|
+
_surfaceOwner = SurfaceOwner.WEBGL;
|
|
587
|
+
this._render = result[1];
|
|
588
|
+
}
|
|
589
|
+
|
|
351
590
|
public PlayerView getPlayerView() {
|
|
352
591
|
return this._playerView;
|
|
353
592
|
}
|
|
354
593
|
|
|
594
|
+
/**
|
|
595
|
+
* Initialise the ImageReader-based video surface for the CPU/Vulkan bitmap path.
|
|
596
|
+
* Called lazily once video dimensions are known. Works without any window or
|
|
597
|
+
* view-hierarchy attachment — the Android equivalent of a detached web <video>.
|
|
598
|
+
*/
|
|
599
|
+
private void setupBitmapSurface() {
|
|
600
|
+
if (IS_DEBUG) android.util.Log.d("JS", "setupBitmapSurface: owner=" + _surfaceOwner + " vw=" + _videoWidth + " vh=" + _videoHeight + " reader=" + (_imageReader != null));
|
|
601
|
+
if (_surfaceOwner != SurfaceOwner.NONE) return;
|
|
602
|
+
if (_videoWidth <= 0 || _videoHeight <= 0) return;
|
|
603
|
+
if (_imageReader != null) return;
|
|
604
|
+
|
|
605
|
+
try {
|
|
606
|
+
// PRIVATE format lets the hardware decoder keep frames in GPU memory.
|
|
607
|
+
// On API 33+ we can wrap the HardwareBuffer as a Bitmap at zero copy.
|
|
608
|
+
// On older APIs we fall back to RGBA_8888 (may force software decode on
|
|
609
|
+
// some devices, but keeps things readable without a GL readback pass).
|
|
610
|
+
int format = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
|
|
611
|
+
? ImageFormat.PRIVATE
|
|
612
|
+
: android.graphics.PixelFormat.RGBA_8888;
|
|
613
|
+
|
|
614
|
+
_imageReader = ImageReader.newInstance(_videoWidth, _videoHeight, format, 2);
|
|
615
|
+
_imageReader.setOnImageAvailableListener(reader -> {
|
|
616
|
+
_hasFrame = true;
|
|
617
|
+
if (_callback != null) _callback.onVideoFrame();
|
|
618
|
+
}, handler);
|
|
619
|
+
_player.setVideoSurface(_imageReader.getSurface());
|
|
620
|
+
_surfaceOwner = SurfaceOwner.BITMAP;
|
|
621
|
+
} catch (Exception e) {
|
|
622
|
+
if (IS_DEBUG) android.util.Log.d("JS", "setupBitmapSurface failed: " + e);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
public android.graphics.Bitmap getCurrentBitmap() {
|
|
627
|
+
// Lazily set up the ImageReader surface when dimensions are first known.
|
|
628
|
+
if (_imageReader == null && _surfaceOwner == SurfaceOwner.NONE) {
|
|
629
|
+
setupBitmapSurface();
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (_imageReader != null) {
|
|
633
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
634
|
+
// API 33+: PRIVATE format → HardwareBuffer → blit into software Bitmap.
|
|
635
|
+
// wrapHardwareBuffer() gives a Config.HARDWARE bitmap whose pixels are
|
|
636
|
+
// not CPU-accessible. Blitting it via Canvas copies it to the software
|
|
637
|
+
// _reusableBitmap so callers (texSubImage3D, drawImage…) can lockPixels.
|
|
638
|
+
try (Image image = _imageReader.acquireLatestImage()) {
|
|
639
|
+
if (image == null) return null;
|
|
640
|
+
android.hardware.HardwareBuffer hb = image.getHardwareBuffer();
|
|
641
|
+
if (hb == null) return null;
|
|
642
|
+
android.graphics.Bitmap hwBmp = android.graphics.Bitmap.wrapHardwareBuffer(hb, null);
|
|
643
|
+
hb.close();
|
|
644
|
+
int w = hwBmp.getWidth(), h = hwBmp.getHeight();
|
|
645
|
+
if (_reusableBitmap == null
|
|
646
|
+
|| _reusableBitmap.getWidth() != w
|
|
647
|
+
|| _reusableBitmap.getHeight() != h) {
|
|
648
|
+
if (_reusableBitmap != null) _reusableBitmap.recycle();
|
|
649
|
+
_reusableBitmap = android.graphics.Bitmap.createBitmap(
|
|
650
|
+
w, h, android.graphics.Bitmap.Config.ARGB_8888);
|
|
651
|
+
}
|
|
652
|
+
new android.graphics.Canvas(_reusableBitmap).drawBitmap(hwBmp, 0, 0, null);
|
|
653
|
+
hwBmp.recycle();
|
|
654
|
+
return _reusableBitmap;
|
|
655
|
+
} catch (Exception e) {
|
|
656
|
+
if (IS_DEBUG) android.util.Log.d("JS", "getCurrentBitmap HW: " + e);
|
|
657
|
+
}
|
|
658
|
+
} else {
|
|
659
|
+
// API < 33: RGBA_8888 format → read pixels from plane[0].
|
|
660
|
+
try (Image image = _imageReader.acquireLatestImage()) {
|
|
661
|
+
if (image == null) return null;
|
|
662
|
+
Image.Plane plane = image.getPlanes()[0];
|
|
663
|
+
java.nio.ByteBuffer buf = plane.getBuffer();
|
|
664
|
+
int w = image.getWidth();
|
|
665
|
+
int h = image.getHeight();
|
|
666
|
+
if (_reusableBitmap == null
|
|
667
|
+
|| _reusableBitmap.getWidth() != w
|
|
668
|
+
|| _reusableBitmap.getHeight() != h) {
|
|
669
|
+
if (_reusableBitmap != null) _reusableBitmap.recycle();
|
|
670
|
+
_reusableBitmap = android.graphics.Bitmap.createBitmap(
|
|
671
|
+
w, h, android.graphics.Bitmap.Config.ARGB_8888);
|
|
672
|
+
}
|
|
673
|
+
_reusableBitmap.copyPixelsFromBuffer(buf);
|
|
674
|
+
return _reusableBitmap;
|
|
675
|
+
} catch (Exception e) {
|
|
676
|
+
if (IS_DEBUG) android.util.Log.d("JS", "getCurrentBitmap SW: " + e);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Last-resort fallback: PlayerView's TextureView, only reachable when the
|
|
682
|
+
// player IS in the window (createNativeView() was called and view is visible).
|
|
683
|
+
View surfaceView = this._playerView.getVideoSurfaceView();
|
|
684
|
+
if (surfaceView instanceof TextureView && ((TextureView) surfaceView).isAvailable()) {
|
|
685
|
+
TextureView tv = (TextureView) surfaceView;
|
|
686
|
+
int w = _videoWidth > 0 ? _videoWidth : tv.getWidth();
|
|
687
|
+
int h = _videoHeight > 0 ? _videoHeight : tv.getHeight();
|
|
688
|
+
if (w <= 0 || h <= 0) return null;
|
|
689
|
+
if (_reusableBitmap == null || _reusableBitmap.getWidth() != w || _reusableBitmap.getHeight() != h) {
|
|
690
|
+
if (_reusableBitmap != null) _reusableBitmap.recycle();
|
|
691
|
+
_reusableBitmap = android.graphics.Bitmap.createBitmap(w, h, android.graphics.Bitmap.Config.ARGB_8888);
|
|
692
|
+
}
|
|
693
|
+
return tv.getBitmap(_reusableBitmap);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
private boolean drawFrame2DBitmap(long context, float sx, float sy, float sw, float sh, float dx, float dy, float dw, float dh) {
|
|
700
|
+
android.graphics.Bitmap bitmap = getCurrentBitmap();
|
|
701
|
+
if (bitmap == null) return false;
|
|
702
|
+
ensureDrawImageMethods();
|
|
703
|
+
if (_drawImageSrcDst == null) return false;
|
|
704
|
+
try {
|
|
705
|
+
Object result = _drawImageSrcDst.invoke(null, context, bitmap, sx, sy, sw, sh, dx, dy, dw, dh);
|
|
706
|
+
return result instanceof Boolean && (Boolean) result;
|
|
707
|
+
} catch (Exception e) {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
public boolean drawVideoFrame2D(int backendType, long context, float dx, float dy) {
|
|
713
|
+
if (context == 0) return false;
|
|
714
|
+
// Ensure the player surface is set up even before dimensions are known.
|
|
715
|
+
// On some devices onVideoSizeChanged only fires after the first rendered frame,
|
|
716
|
+
// so we must give ExoPlayer a surface first to break the deadlock.
|
|
717
|
+
if (backendType == 2) ensureGL2DSurface(context);
|
|
718
|
+
float w = (float) _videoWidth;
|
|
719
|
+
float h = (float) _videoHeight;
|
|
720
|
+
if (w <= 0 || h <= 0) return false;
|
|
721
|
+
if (backendType == 2) {
|
|
722
|
+
return drawFrame2DGL(context, 0, 0, w, h, dx, dy, w, h);
|
|
723
|
+
}
|
|
724
|
+
return drawFrame2DBitmap(context, 0, 0, w, h, dx, dy, w, h);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
public boolean drawVideoFrame2D(int backendType, long context, float dx, float dy, float dw, float dh) {
|
|
728
|
+
if (context == 0) return false;
|
|
729
|
+
if (backendType == 2) ensureGL2DSurface(context);
|
|
730
|
+
float w = (float) _videoWidth;
|
|
731
|
+
float h = (float) _videoHeight;
|
|
732
|
+
if (w <= 0 || h <= 0) return false;
|
|
733
|
+
if (backendType == 2) {
|
|
734
|
+
return drawFrame2DGL(context, 0, 0, w, h, dx, dy, dw, dh);
|
|
735
|
+
}
|
|
736
|
+
return drawFrame2DBitmap(context, 0, 0, w, h, dx, dy, dw, dh);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
public boolean drawVideoFrame2D(int backendType, long context, float sx, float sy, float sw, float sh, float dx, float dy, float dw, float dh) {
|
|
740
|
+
if (context == 0) return false;
|
|
741
|
+
if (backendType == 2) ensureGL2DSurface(context);
|
|
742
|
+
if (_videoWidth <= 0 || _videoHeight <= 0) return false;
|
|
743
|
+
if (backendType == 2) {
|
|
744
|
+
return drawFrame2DGL(context, sx, sy, sw, sh, dx, dy, dw, dh);
|
|
745
|
+
}
|
|
746
|
+
return drawFrame2DBitmap(context, sx, sy, sw, sh, dx, dy, dw, dh);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
private void ensureGL2DSurface(long context) {
|
|
751
|
+
if (_glInitFailed || _glSt != null) return;
|
|
752
|
+
if (IS_DEBUG) android.util.Log.d("JS", "ensureGL2DSurface: context=" + context);
|
|
753
|
+
Object[] result = _utils.create2DContextSurfaceTexture(context);
|
|
754
|
+
if (result == null) {
|
|
755
|
+
if (IS_DEBUG) android.util.Log.d("JS", "ensureGL2DSurface: create2DContextSurfaceTexture returned null → bitmap fallback");
|
|
756
|
+
_glInitFailed = true;
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (IS_DEBUG) android.util.Log.d("JS", "ensureGL2DSurface: created ok, textureId=" + result[1]);
|
|
760
|
+
_glSt = (SurfaceTexture) result[0];
|
|
761
|
+
_glTextureId = (Integer) result[1];
|
|
762
|
+
_glRender = result[2];
|
|
763
|
+
_glSt.setOnFrameAvailableListener(st -> {
|
|
764
|
+
_glHasFrame = true;
|
|
765
|
+
if (_callback != null) _callback.onVideoFrame();
|
|
766
|
+
});
|
|
767
|
+
_glSurface = new Surface(_glSt);
|
|
768
|
+
if (_surfaceOwner == SurfaceOwner.BITMAP) {
|
|
769
|
+
if (_imageReader != null) { _imageReader.close(); _imageReader = null; }
|
|
770
|
+
_surfaceOwner = SurfaceOwner.NONE;
|
|
771
|
+
}
|
|
772
|
+
if (_surfaceOwner != SurfaceOwner.WEBGL) {
|
|
773
|
+
_player.setVideoSurface(_glSurface);
|
|
774
|
+
_surfaceOwner = SurfaceOwner.GL2D;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
private boolean drawFrame2DGL(long context, float sx, float sy, float sw, float sh, float dx, float dy, float dw, float dh) {
|
|
779
|
+
// Permanently fall back to CPU path if GL init previously failed.
|
|
780
|
+
if (_glInitFailed) {
|
|
781
|
+
return drawFrame2DBitmap(context, sx, sy, sw, sh, dx, dy, dw, dh);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Surface should already be set up by ensureGL2DSurface() called from
|
|
785
|
+
// drawVideoFrame2D. If it somehow isn't (e.g. called directly), init now.
|
|
786
|
+
if (_glSt == null) {
|
|
787
|
+
ensureGL2DSurface(context);
|
|
788
|
+
if (_glSt == null) {
|
|
789
|
+
return drawFrame2DBitmap(context, sx, sy, sw, sh, dx, dy, dw, dh);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
if (!_glHasFrame) {
|
|
794
|
+
// No new decoded frame yet; nothing to draw.
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
boolean drawn = _utils.drawVideoFrameGL(
|
|
799
|
+
context, _glTextureId, _glSt,
|
|
800
|
+
_videoWidth, _videoHeight,
|
|
801
|
+
sx, sy, sw, sh,
|
|
802
|
+
dx, dy, dw, dh);
|
|
803
|
+
|
|
804
|
+
if (drawn) {
|
|
805
|
+
_glHasFrame = false;
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// drawVideoFrameGL failed with a live frame (Skia OES path not supported —
|
|
810
|
+
// common on emulators and some Vulkan-only devices). Permanently fall back to
|
|
811
|
+
// the CPU bitmap path via ImageReader so subsequent frames actually render.
|
|
812
|
+
if (IS_DEBUG) android.util.Log.d("JS", "drawVideoFrameGL: OES draw failed, switching to bitmap path");
|
|
813
|
+
_glInitFailed = true;
|
|
814
|
+
_glHasFrame = false;
|
|
815
|
+
// Release the GL surface so the player can be redirected to ImageReader.
|
|
816
|
+
if (_glSurface != null) { _glSurface.release(); _glSurface = null; }
|
|
817
|
+
if (_glSt != null) { _glSt.release(); _glSt = null; }
|
|
818
|
+
_surfaceOwner = SurfaceOwner.NONE;
|
|
819
|
+
// Set up ImageReader and redirect the player.
|
|
820
|
+
setupBitmapSurface();
|
|
821
|
+
// First frame after transition may be null (decoder just switched surfaces).
|
|
822
|
+
return drawFrame2DBitmap(context, sx, sy, sw, sh, dx, dy, dw, dh);
|
|
823
|
+
}
|
|
824
|
+
|
|
355
825
|
public void setPlayer() {
|
|
356
826
|
this._playerView.setPlayer(this._player);
|
|
357
827
|
}
|
|
@@ -388,23 +858,23 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
388
858
|
}
|
|
389
859
|
|
|
390
860
|
public boolean getMuted() {
|
|
391
|
-
return this._player.
|
|
861
|
+
return this._player.getVolume() == 0f;
|
|
392
862
|
}
|
|
393
863
|
|
|
394
864
|
public void setMuted(boolean value) {
|
|
395
|
-
this._player.
|
|
865
|
+
this._player.setVolume(value ? 0f : 1f);
|
|
396
866
|
}
|
|
397
867
|
|
|
398
868
|
public long getDuration() {
|
|
399
869
|
return this._player.getDuration();
|
|
400
870
|
}
|
|
401
871
|
|
|
402
|
-
public
|
|
403
|
-
return this._player.getCurrentPosition() / 1000;
|
|
872
|
+
public double getCurrentTime() {
|
|
873
|
+
return this._player.getCurrentPosition() / 1000.0;
|
|
404
874
|
}
|
|
405
875
|
|
|
406
|
-
public void setCurrentTime(
|
|
407
|
-
this._player.seekTo((
|
|
876
|
+
public void setCurrentTime(double value) {
|
|
877
|
+
this._player.seekTo((long) (value * 1000.0));
|
|
408
878
|
}
|
|
409
879
|
|
|
410
880
|
public String getSrc() {
|
|
@@ -413,6 +883,13 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
413
883
|
|
|
414
884
|
public void setSrc(String value) {
|
|
415
885
|
this._src = value;
|
|
886
|
+
this._loadedDataFired.set(false);
|
|
887
|
+
// Video dimensions will change — release the ImageReader so setupBitmapSurface()
|
|
888
|
+
// recreates it at the new size once onVideoSizeChanged fires.
|
|
889
|
+
if (_surfaceOwner == SurfaceOwner.BITMAP) {
|
|
890
|
+
if (_imageReader != null) { _imageReader.close(); _imageReader = null; }
|
|
891
|
+
_surfaceOwner = SurfaceOwner.NONE;
|
|
892
|
+
}
|
|
416
893
|
this._setSrc(value);
|
|
417
894
|
}
|
|
418
895
|
|
|
@@ -537,9 +1014,19 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
537
1014
|
@UnstableApi
|
|
538
1015
|
@Override
|
|
539
1016
|
public void onPlayerStateChanged(boolean playWhenReady, @Player.State int playbackState) {}
|
|
540
|
-
|
|
1017
|
+
|
|
1018
|
+
|
|
541
1019
|
@Override
|
|
542
|
-
public void onPlaybackStateChanged(@Player.State int playbackState) {
|
|
1020
|
+
public void onPlaybackStateChanged(@Player.State int playbackState) {
|
|
1021
|
+
if (playbackState == Player.STATE_READY) {
|
|
1022
|
+
this._readyState = 2; // HAVE_CURRENT_DATA
|
|
1023
|
+
if (_loadedDataFired.compareAndSet(false, true)) {
|
|
1024
|
+
if (this._callback != null) {
|
|
1025
|
+
this._callback.onLoadedData();
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
543
1030
|
|
|
544
1031
|
@Override
|
|
545
1032
|
public void onPlayWhenReadyChanged(
|
|
@@ -602,7 +1089,13 @@ public class VideoHelper implements Player.Listener, SurfaceTexture.OnFrameAvail
|
|
|
602
1089
|
public void onSurfaceSizeChanged(int width, int height) {}
|
|
603
1090
|
|
|
604
1091
|
@Override
|
|
605
|
-
public void onRenderedFirstFrame() {
|
|
1092
|
+
public void onRenderedFirstFrame() {
|
|
1093
|
+
if (_loadedDataFired.compareAndSet(false, true)) {
|
|
1094
|
+
if (this._callback != null) {
|
|
1095
|
+
this._callback.onLoadedData();
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
606
1099
|
|
|
607
1100
|
|
|
608
1101
|
@Deprecated
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
OTHER_LDFLAGS = $(inherited) -framework "AVFoundation" -framework "CoreMedia" -framework "AVKit"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//
|
|
2
|
+
// NSCPlayerState.h
|
|
3
|
+
// CanvasMedia
|
|
4
|
+
//
|
|
5
|
+
// Created by Osei Fortune on 20/05/2025.
|
|
6
|
+
//
|
|
7
|
+
#import <Foundation/Foundation.h>
|
|
8
|
+
|
|
9
|
+
typedef NS_ENUM(NSUInteger, NSCPlayerState) {
|
|
10
|
+
NSCPlayerStateIdle = 0,
|
|
11
|
+
NSCPlayerStatePlaying = 1,
|
|
12
|
+
NSCPlayerStatePaused = 2,
|
|
13
|
+
NSCPlayerStateStopped = 3,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
typedef NS_ENUM(NSUInteger, NSCPlayerReadyState) {
|
|
18
|
+
NSCPlayerReadyStateHaveNothing = 0,
|
|
19
|
+
NSCPlayerReadyStateHaveMetadata = 1,
|
|
20
|
+
NSCPlayerReadyStateHaveCurrentData = 2,
|
|
21
|
+
NSCPlayerReadyStateHaveFutureData = 3,
|
|
22
|
+
NSCPlayerReadyStateHaveEnoughData = 4,
|
|
23
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//
|
|
2
|
+
// NSCVideoHelper+Internal.h
|
|
3
|
+
// CanvasMedia
|
|
4
|
+
//
|
|
5
|
+
// Created by Osei Fortune on 20/05/2025.
|
|
6
|
+
//
|
|
7
|
+
#import <Foundation/Foundation.h>
|
|
8
|
+
#import <AVKit/AVKit.h>
|
|
9
|
+
#import <AVFoundation/AVFoundation.h>
|
|
10
|
+
#import <UIKit/UIKit.h>
|
|
11
|
+
#import <CoreMedia/CoreMedia.h>
|
|
12
|
+
|
|
13
|
+
@interface NSCVideoHelper()
|
|
14
|
+
@property (nonatomic, strong) AVURLAsset *asset;
|
|
15
|
+
@property (nonatomic, copy) NSString *currentSrc;
|
|
16
|
+
@property (nonatomic, strong) id playEndNotificationId;
|
|
17
|
+
@property (nonatomic, strong) id resumeListenerId;
|
|
18
|
+
@property (nonatomic, strong) id suspendListenerId;
|
|
19
|
+
@property (nonatomic, strong) id playbackFramesObserver;
|
|
20
|
+
@property (nonatomic, strong) id playbackTimeObserver;
|
|
21
|
+
@property (nonatomic, assign) NSCPlayerState state;
|
|
22
|
+
@property (nonatomic, assign) NSCPlayerReadyState readyState;
|
|
23
|
+
@property (nonatomic, assign) BOOL inForeground;
|
|
24
|
+
@property (nonatomic, assign) BOOL isLoop;
|
|
25
|
+
@property (nonatomic, assign) BOOL loadedDataFired;
|
|
26
|
+
@end
|