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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
|
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
|
-
|
|
486
|
-
|
|
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.
|
|
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",
|