@nativescript/canvas-media 2.0.0-webgpu.9 → 2.0.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.
@@ -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.isDeviceMuted();
861
+ return this._player.getVolume() == 0f;
392
862
  }
393
863
 
394
864
  public void setMuted(boolean value) {
395
- this._player.setDeviceMuted(value);
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 long getCurrentTime() {
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(long value) {
407
- this._player.seekTo((int) (value * 1000), 0);
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,3 @@
1
+ #import "NSCPlayerState.h"
2
+ #import "NSCVideoHelper.h"
3
+ #import "NSCVideoHelperListener.h"
@@ -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