spactureai-mobile-player 1.0.30 → 1.0.32

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.
@@ -60,6 +60,26 @@ final class HybridVideoPlayer: HybridVideoPlayerSpec, VLCPlaybackDelegateOwner {
60
60
  /// from `VideoComponentView` when the surface attaches.
61
61
  private var wantsPlaybackWhenDrawableReady: Bool = false
62
62
 
63
+ /// Marshal a closure onto the main thread, executing inline if we're
64
+ /// already there. VLC's renderer (`VLCOpenGLES2VideoView`) reaches into
65
+ /// `CAEAGLLayer` to manage its OpenGL renderbuffer when the active
66
+ /// media changes (`mediaPlayer.media = ...`), the player stops, or the
67
+ /// drawable is reattached. Those CALayer mutations must happen on the
68
+ /// main thread; otherwise iOS raises
69
+ /// `_raiseExceptionForBackgroundThreadLayerPropertyModification`.
70
+ /// JS-driven lifecycle calls (release / replaceSourceAsync /
71
+ /// constructor) can land on Nitro's worker thread, so we must hop
72
+ /// before touching any `mediaPlayer.*` setter that triggers a render
73
+ /// reset.
74
+ @inline(__always)
75
+ private static func runOnMain(_ block: @escaping () -> Void) {
76
+ if Thread.isMainThread {
77
+ block()
78
+ } else {
79
+ DispatchQueue.main.async(execute: block)
80
+ }
81
+ }
82
+
63
83
  init(source: (any HybridVideoPlayerSourceSpec)) throws {
64
84
  self.source = source
65
85
  self.eventEmitter = HybridVideoPlayerEventEmitter()
@@ -200,16 +220,19 @@ final class HybridVideoPlayer: HybridVideoPlayerSpec, VLCPlaybackDelegateOwner {
200
220
 
201
221
  func release() {
202
222
  invalidateStallWatchdog()
203
- if mediaPlayer.isPlaying {
204
- mediaPlayer.stop()
205
- }
206
- media?.delegate = nil
223
+
224
+ // Capture the VLC objects so they survive past `self` being
225
+ // deallocated — `release()` may be called from `deinit` on a
226
+ // background thread, and the closure below has to outlive the
227
+ // `HybridVideoPlayer` instance.
228
+ let mp = mediaPlayer
229
+ let m = media
230
+ let proxy = authProxy
231
+
232
+ // Clear JS-side bookkeeping first; it doesn't touch any UIKit
233
+ // layer state and can safely run on whatever thread we're on.
207
234
  media = nil
208
- mediaPlayer.media = nil
209
- mediaPlayer.delegate = nil
210
235
  vlcDelegates = nil
211
-
212
- authProxy?.stop()
213
236
  authProxy = nil
214
237
 
215
238
  try? _eventEmitter?.clearAllListeners()
@@ -224,6 +247,21 @@ final class HybridVideoPlayer: HybridVideoPlayerSpec, VLCPlaybackDelegateOwner {
224
247
  lastProgressTimeSeconds = 0
225
248
  lastProgressTickMonotonic = 0
226
249
 
250
+ // Tear down VLC on the main thread. `mp.stop()` and
251
+ // `mp.media = nil` cause `VLCOpenGLES2VideoView` to reset its
252
+ // OpenGL renderbuffer, which mutates `CAEAGLLayer.contents` —
253
+ // strictly main-thread-only. Without this hop, iOS raises
254
+ // `_raiseExceptionForBackgroundThreadLayerPropertyModification`
255
+ // whenever a player is released from a non-main queue (typical
256
+ // when the JS owner is dropped from a Nitro worker thread).
257
+ Self.runOnMain {
258
+ if mp.isPlaying { mp.stop() }
259
+ m?.delegate = nil
260
+ mp.media = nil
261
+ mp.delegate = nil
262
+ proxy?.stop()
263
+ }
264
+
227
265
  VideoManager.shared.unregister(player: self)
228
266
  }
229
267
 
@@ -238,16 +276,24 @@ final class HybridVideoPlayer: HybridVideoPlayerSpec, VLCPlaybackDelegateOwner {
238
276
 
239
277
  func play() throws {
240
278
  wantsPlaybackWhenDrawableReady = true
241
- mediaPlayer.play()
279
+ let mp = mediaPlayer
280
+ Self.runOnMain { mp.play() }
242
281
  VideoManager.shared.requestAudioSessionUpdate()
243
282
  }
244
283
 
245
284
  func pause() throws {
246
285
  wantsPlaybackWhenDrawableReady = false
247
- if mediaPlayer.canPause {
248
- mediaPlayer.pause()
249
- } else {
250
- mediaPlayer.stop()
286
+ let mp = mediaPlayer
287
+ // VLC's `stop()` (the not-canPause fallback) tears down the
288
+ // renderbuffer; must be on main. Even `pause()` proper sometimes
289
+ // touches the GL view on first call after attach, so keep both
290
+ // branches on main for symmetry.
291
+ Self.runOnMain {
292
+ if mp.canPause {
293
+ mp.pause()
294
+ } else {
295
+ mp.stop()
296
+ }
251
297
  }
252
298
  VideoManager.shared.requestAudioSessionUpdate()
253
299
  }
@@ -294,14 +340,20 @@ final class HybridVideoPlayer: HybridVideoPlayerSpec, VLCPlaybackDelegateOwner {
294
340
  case .second(let newSource):
295
341
  wantsPlaybackWhenDrawableReady = false
296
342
  invalidateStallWatchdog()
297
- if mediaPlayer.isPlaying {
298
- mediaPlayer.stop()
343
+
344
+ // Tear down the previous VLC media on main — same reasoning as
345
+ // `release()`. `mediaPlayer.stop()` and `mediaPlayer.media = nil`
346
+ // both poke `VLCOpenGLES2VideoView`'s renderbuffer.
347
+ let mp = mediaPlayer
348
+ let oldMedia = media
349
+ let oldProxy = authProxy
350
+ Self.runOnMain {
351
+ if mp.isPlaying { mp.stop() }
352
+ oldMedia?.delegate = nil
353
+ mp.media = nil
354
+ oldProxy?.stop()
299
355
  }
300
- media?.delegate = nil
301
356
  media = nil
302
- mediaPlayer.media = nil
303
-
304
- authProxy?.stop()
305
357
  authProxy = nil
306
358
 
307
359
  self.source = newSource
@@ -432,7 +484,17 @@ final class HybridVideoPlayer: HybridVideoPlayerSpec, VLCPlaybackDelegateOwner {
432
484
 
433
485
  media.delegate = vlcDelegates
434
486
  self.media = media
435
- mediaPlayer.media = media
487
+ // Assigning `mediaPlayer.media` causes VLC to swap the active
488
+ // `VLCOpenGLES2VideoView` renderbuffer, which mutates
489
+ // `CAEAGLLayer.contents`. Hop to main so the layer write is on
490
+ // the main thread. We capture `mediaPlayer` and the new `media`
491
+ // by value so the assignment is safe even if `self` is dropped
492
+ // before the dispatched block runs.
493
+ let mp = mediaPlayer
494
+ let mediaToAttach = media
495
+ Self.runOnMain {
496
+ mp.media = mediaToAttach
497
+ }
436
498
 
437
499
  if !hasFiredOnLoadStart {
438
500
  hasFiredOnLoadStart = true
@@ -482,17 +544,26 @@ final class HybridVideoPlayer: HybridVideoPlayerSpec, VLCPlaybackDelegateOwner {
482
544
  private func applySeek(seconds target: Double) {
483
545
  let ms = Int32(min(Double(Int32.max), max(0, target * 1000.0)))
484
546
 
485
- if mediaPlayer.isSeekable {
486
- mediaPlayer.time = VLCTime(int: ms)
547
+ // VLC's seek can prompt the demuxer to re-attach segments and
548
+ // refresh the renderbuffer (especially for HLS where a seek
549
+ // jumps to a different .ts segment). Marshal onto main so any
550
+ // resulting CALayer write happens there. We capture the player
551
+ // by value so the dispatch is safe even if the JS owner is
552
+ // released before the block fires.
553
+ let mp = mediaPlayer
554
+ let totalMs = Double(media?.length.intValue ?? 0)
555
+ let isSeekable = mediaPlayer.isSeekable
556
+
557
+ if isSeekable {
487
558
  pendingSeekSeconds = nil
559
+ Self.runOnMain { mp.time = VLCTime(int: ms) }
488
560
  return
489
561
  }
490
562
 
491
- let totalMs = Double(media?.length.intValue ?? 0)
492
563
  if totalMs > 0 {
493
564
  let position = Float(min(1.0, (Double(ms)) / totalMs))
494
- mediaPlayer.position = position
495
565
  pendingSeekSeconds = nil
566
+ Self.runOnMain { mp.position = position }
496
567
  } else {
497
568
  pendingSeekSeconds = target
498
569
  }
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "spactureai-mobile-player",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "description": "Spacture AI Mobile Player",
5
5
  "source": "./src/index.tsx",
6
6
  "main": "./lib/module/index.js",
7
7
  "types": "./lib/typescript/src/index.d.ts",
8
+ "author": "Spacture AI",
8
9
  "exports": {
9
10
  ".": {
10
11
  "types": "./lib/typescript/src/index.d.ts",