@capgo/capacitor-video-player 8.2.5 → 8.2.8
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/src/main/java/com/capgo/videoplayer/VideoPlayerPlugin.java +1 -1
- package/ios/Sources/VideoPlayerPlugin/AVPlayerControlsVisibilityObserver.swift +227 -0
- package/ios/Sources/VideoPlayerPlugin/DismissObservingPlayerViewController.swift +43 -0
- package/ios/Sources/VideoPlayerPlugin/ExitEmissionGuard.swift +11 -0
- package/ios/Sources/VideoPlayerPlugin/FullscreenVideoPlayer.swift +40 -15
- package/ios/Sources/VideoPlayerPlugin/PlayerDismissExitDetection.swift +16 -0
- package/ios/Sources/VideoPlayerPlugin/VideoPlayerCastController.swift +200 -63
- package/ios/Sources/VideoPlayerPlugin/VideoPlayerPlugin.swift +1 -1
- package/ios/Tests/VideoPlayerPluginTests/PlayerDismissExitDetectionTests.swift +49 -0
- package/ios/Tests/VideoPlayerPluginTests/VideoPlayerCastControllerTests.swift +103 -0
- package/ios/Tests/VideoPlayerPluginTests/VideoPlayerPluginTests.swift +9 -11
- package/package.json +4 -2
- package/scripts/check-capacitor-plugin-wiring.mjs +254 -0
- package/scripts/generate-ai-changelog.mjs +135 -0
- package/scripts/test-ios.sh +109 -0
|
@@ -38,9 +38,20 @@ final class VideoPlayerCastController: NSObject {
|
|
|
38
38
|
private var onPlay: (() -> Void)?
|
|
39
39
|
private var onPause: (() -> Void)?
|
|
40
40
|
private var onEnd: (() -> Void)?
|
|
41
|
-
private
|
|
42
|
-
private
|
|
43
|
-
private
|
|
41
|
+
private let controlsVisibilityObserver = AVPlayerControlsVisibilityObserver()
|
|
42
|
+
private var localSeekTimeObserver: Any?
|
|
43
|
+
private var timeJumpObserver: NSObjectProtocol?
|
|
44
|
+
private var observedPlayerForSeek: AVPlayer?
|
|
45
|
+
private var overlayInstallRetryWorkItem: DispatchWorkItem?
|
|
46
|
+
private var overlayInstallAttemptsRemaining = 0
|
|
47
|
+
private var lastForwardedSeekTime: Double?
|
|
48
|
+
private var arePlaybackControlsVisible = true
|
|
49
|
+
|
|
50
|
+
deinit {
|
|
51
|
+
overlayInstallRetryWorkItem?.cancel()
|
|
52
|
+
stopObservingLocalSeek()
|
|
53
|
+
controlsVisibilityObserver.stop()
|
|
54
|
+
}
|
|
44
55
|
|
|
45
56
|
var isCasting: Bool {
|
|
46
57
|
return remoteMediaClient != nil && isLoadedOnCast
|
|
@@ -73,9 +84,7 @@ final class VideoPlayerCastController: NSObject {
|
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
GCKCastContext.sharedInstance().sessionManager.add(self)
|
|
76
|
-
self.
|
|
77
|
-
self.addCastIndicator(to: playerViewController)
|
|
78
|
-
self.beginObservingPlayerTaps(playerViewController)
|
|
87
|
+
self.installOverlayIfNeeded()
|
|
79
88
|
self.loadMediaIfCastSessionAvailable()
|
|
80
89
|
}
|
|
81
90
|
|
|
@@ -86,6 +95,20 @@ final class VideoPlayerCastController: NSObject {
|
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
97
|
|
|
98
|
+
func installOverlayIfNeeded() {
|
|
99
|
+
overlayInstallAttemptsRemaining = 30
|
|
100
|
+
let installOnMain = { [weak self] in
|
|
101
|
+
guard let self else { return }
|
|
102
|
+
self.installOverlayIfNeededOnMain()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if Thread.isMainThread {
|
|
106
|
+
installOnMain()
|
|
107
|
+
} else {
|
|
108
|
+
DispatchQueue.main.async(execute: installOnMain)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
89
112
|
func detach(stopRemoteMedia: Bool) {
|
|
90
113
|
guard !isDetached else {
|
|
91
114
|
return
|
|
@@ -95,24 +118,21 @@ final class VideoPlayerCastController: NSObject {
|
|
|
95
118
|
pendingCastCommands.removeAll()
|
|
96
119
|
|
|
97
120
|
DispatchQueue.main.async { [weak self] in
|
|
98
|
-
guard let self
|
|
99
|
-
GCKCastContext.isSharedInstanceInitialized() else {
|
|
100
|
-
return
|
|
101
|
-
}
|
|
121
|
+
guard let self else { return }
|
|
102
122
|
|
|
103
|
-
if
|
|
104
|
-
|
|
123
|
+
if GCKCastContext.isSharedInstanceInitialized() {
|
|
124
|
+
if stopRemoteMedia {
|
|
125
|
+
self.remoteMediaClient?.stop()
|
|
126
|
+
}
|
|
127
|
+
self.stopRemoteMediaObservation()
|
|
128
|
+
GCKCastContext.sharedInstance().sessionManager.remove(self)
|
|
129
|
+
} else {
|
|
130
|
+
self.stopRemoteMediaObservation()
|
|
105
131
|
}
|
|
106
132
|
|
|
107
|
-
self.
|
|
108
|
-
|
|
109
|
-
self.
|
|
110
|
-
self.controlsHideTimer = nil
|
|
111
|
-
if let tapRecognizer = self.tapGestureRecognizer,
|
|
112
|
-
let view = self.playerViewController?.view {
|
|
113
|
-
view.removeGestureRecognizer(tapRecognizer)
|
|
114
|
-
}
|
|
115
|
-
self.tapGestureRecognizer = nil
|
|
133
|
+
self.cancelOverlayInstallRetry()
|
|
134
|
+
self.stopObservingLocalSeek()
|
|
135
|
+
self.controlsVisibilityObserver.stop()
|
|
116
136
|
self.castButton?.removeFromSuperview()
|
|
117
137
|
self.castButton = nil
|
|
118
138
|
self.castIndicatorLabel?.removeFromSuperview()
|
|
@@ -151,6 +171,7 @@ final class VideoPlayerCastController: NSObject {
|
|
|
151
171
|
return false
|
|
152
172
|
}
|
|
153
173
|
guard isLoadedOnCast else {
|
|
174
|
+
syncLocalPlayhead(to: time)
|
|
154
175
|
return enqueuePendingCastCommand(.seek(time))
|
|
155
176
|
}
|
|
156
177
|
let options = GCKMediaSeekOptions()
|
|
@@ -158,6 +179,7 @@ final class VideoPlayerCastController: NSObject {
|
|
|
158
179
|
options.relative = false
|
|
159
180
|
options.resumeState = .unchanged
|
|
160
181
|
remoteMediaClient.seek(with: options)
|
|
182
|
+
syncLocalPlayhead(to: time)
|
|
161
183
|
return true
|
|
162
184
|
}
|
|
163
185
|
|
|
@@ -272,12 +294,63 @@ private extension VideoPlayerCastController {
|
|
|
272
294
|
return GCKCastContext.setSharedInstanceWith(options, error: &error)
|
|
273
295
|
}
|
|
274
296
|
|
|
275
|
-
func
|
|
276
|
-
guard
|
|
297
|
+
func installOverlayIfNeededOnMain() {
|
|
298
|
+
guard !isDetached,
|
|
299
|
+
castButton == nil,
|
|
300
|
+
let playerViewController = playerViewController else {
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
guard let overlayView = VideoPlayerCastOverlayLayout.overlayView(for: playerViewController) else {
|
|
305
|
+
scheduleOverlayInstallRetry()
|
|
277
306
|
return
|
|
278
307
|
}
|
|
279
308
|
|
|
280
|
-
|
|
309
|
+
cancelOverlayInstallRetry()
|
|
310
|
+
addCastButton(to: overlayView)
|
|
311
|
+
addCastIndicator(to: overlayView)
|
|
312
|
+
beginObservingControlsVisibility(playerViewController)
|
|
313
|
+
updateCastOverlayVisibility(visible: arePlaybackControlsVisible)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
func scheduleOverlayInstallRetry() {
|
|
317
|
+
guard !isDetached, castButton == nil, overlayInstallAttemptsRemaining > 0 else {
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
overlayInstallAttemptsRemaining -= 1
|
|
321
|
+
overlayInstallRetryWorkItem?.cancel()
|
|
322
|
+
let workItem = DispatchWorkItem { [weak self] in
|
|
323
|
+
guard let self, !self.isDetached else { return }
|
|
324
|
+
self.installOverlayIfNeededOnMain()
|
|
325
|
+
}
|
|
326
|
+
overlayInstallRetryWorkItem = workItem
|
|
327
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: workItem)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
func cancelOverlayInstallRetry() {
|
|
331
|
+
overlayInstallRetryWorkItem?.cancel()
|
|
332
|
+
overlayInstallRetryWorkItem = nil
|
|
333
|
+
overlayInstallAttemptsRemaining = 0
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
func syncLocalPlayhead(to time: Double) {
|
|
337
|
+
let apply = { [weak self] in
|
|
338
|
+
self?.player?.seek(
|
|
339
|
+
to: CMTime(seconds: time, preferredTimescale: 600),
|
|
340
|
+
toleranceBefore: .zero,
|
|
341
|
+
toleranceAfter: .zero
|
|
342
|
+
)
|
|
343
|
+
self?.lastForwardedSeekTime = time
|
|
344
|
+
}
|
|
345
|
+
if Thread.isMainThread {
|
|
346
|
+
apply()
|
|
347
|
+
} else {
|
|
348
|
+
DispatchQueue.main.async(execute: apply)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
func addCastButton(to overlayView: UIView) {
|
|
353
|
+
guard castButton == nil else {
|
|
281
354
|
return
|
|
282
355
|
}
|
|
283
356
|
|
|
@@ -303,15 +376,11 @@ private extension VideoPlayerCastController {
|
|
|
303
376
|
castButton = button
|
|
304
377
|
}
|
|
305
378
|
|
|
306
|
-
func addCastIndicator(to
|
|
379
|
+
func addCastIndicator(to overlayView: UIView) {
|
|
307
380
|
guard castIndicatorLabel == nil else {
|
|
308
381
|
return
|
|
309
382
|
}
|
|
310
383
|
|
|
311
|
-
guard let overlayView = playerViewController.view else {
|
|
312
|
-
return
|
|
313
|
-
}
|
|
314
|
-
|
|
315
384
|
let label = UILabel(frame: .zero)
|
|
316
385
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
317
386
|
label.text = "Casting"
|
|
@@ -339,46 +408,90 @@ private extension VideoPlayerCastController {
|
|
|
339
408
|
castIndicatorLabel = label
|
|
340
409
|
}
|
|
341
410
|
|
|
342
|
-
func
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
overlayView.removeGestureRecognizer(existing)
|
|
411
|
+
func beginObservingControlsVisibility(_ playerViewController: AVPlayerViewController) {
|
|
412
|
+
controlsVisibilityObserver.start(observing: playerViewController) { [weak self] visible in
|
|
413
|
+
self?.arePlaybackControlsVisible = visible
|
|
414
|
+
self?.updateCastOverlayVisibility(visible: visible)
|
|
347
415
|
}
|
|
348
|
-
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handlePlayerTap))
|
|
349
|
-
tapRecognizer.cancelsTouchesInView = false
|
|
350
|
-
overlayView.addGestureRecognizer(tapRecognizer)
|
|
351
|
-
self.tapGestureRecognizer = tapRecognizer
|
|
352
|
-
// Show the overlay immediately; it will auto-hide after the standard controls delay.
|
|
353
|
-
showOverlayControls()
|
|
354
416
|
}
|
|
355
417
|
|
|
356
|
-
|
|
357
|
-
|
|
418
|
+
func updateCastOverlayVisibility(visible: Bool) {
|
|
419
|
+
castButton?.isHidden = !visible
|
|
420
|
+
castButton?.alpha = visible ? 1 : 0
|
|
421
|
+
castIndicatorLabel?.isHidden = !visible || !isCasting
|
|
422
|
+
castIndicatorLabel?.alpha = visible ? 1 : 0
|
|
358
423
|
}
|
|
359
424
|
|
|
360
|
-
func
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
425
|
+
func beginObservingLocalSeekWhileCasting() {
|
|
426
|
+
stopObservingLocalSeek()
|
|
427
|
+
guard let player = player else {
|
|
428
|
+
return
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
observedPlayerForSeek = player
|
|
432
|
+
timeJumpObserver = NotificationCenter.default.addObserver(
|
|
433
|
+
forName: .AVPlayerItemTimeJumped,
|
|
434
|
+
object: nil,
|
|
435
|
+
queue: .main
|
|
436
|
+
) { [weak self] notification in
|
|
437
|
+
guard let self,
|
|
438
|
+
let item = notification.object as? AVPlayerItem,
|
|
439
|
+
item === self.player?.currentItem else {
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
self.forwardLocalSeekToRemoteIfNeeded()
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
let interval = CMTime(seconds: 0.25, preferredTimescale: 600)
|
|
446
|
+
localSeekTimeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] _ in
|
|
447
|
+
self?.forwardLocalSeekToRemoteIfNeeded()
|
|
366
448
|
}
|
|
367
449
|
}
|
|
368
450
|
|
|
369
|
-
func
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
451
|
+
func stopObservingLocalSeek() {
|
|
452
|
+
if let timeJumpObserver {
|
|
453
|
+
NotificationCenter.default.removeObserver(timeJumpObserver)
|
|
454
|
+
self.timeJumpObserver = nil
|
|
455
|
+
}
|
|
456
|
+
if let localSeekTimeObserver {
|
|
457
|
+
observedPlayerForSeek?.removeTimeObserver(localSeekTimeObserver)
|
|
458
|
+
self.localSeekTimeObserver = nil
|
|
459
|
+
}
|
|
460
|
+
observedPlayerForSeek = nil
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
func syncCastingObserversIfNeeded() {
|
|
464
|
+
guard isCasting else {
|
|
465
|
+
stopObservingLocalSeek()
|
|
466
|
+
return
|
|
467
|
+
}
|
|
468
|
+
if localSeekTimeObserver == nil {
|
|
469
|
+
beginObservingLocalSeekWhileCasting()
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
func forwardLocalSeekToRemoteIfNeeded() {
|
|
474
|
+
guard !isDetached,
|
|
475
|
+
isCasting,
|
|
476
|
+
let remoteMediaClient = remoteMediaClient,
|
|
477
|
+
let player = player else {
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
let localTime = player.currentTime().seconds
|
|
482
|
+
guard VideoPlayerCastSeekSync.shouldForwardLocalSeek(
|
|
483
|
+
localTime: localTime,
|
|
484
|
+
lastForwardedTime: lastForwardedSeekTime
|
|
485
|
+
) else {
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
lastForwardedSeekTime = localTime
|
|
490
|
+
let options = GCKMediaSeekOptions()
|
|
491
|
+
options.interval = localTime
|
|
492
|
+
options.relative = false
|
|
493
|
+
options.resumeState = .unchanged
|
|
494
|
+
remoteMediaClient.seek(with: options)
|
|
382
495
|
}
|
|
383
496
|
|
|
384
497
|
func loadMediaIfCastSessionAvailable() {
|
|
@@ -401,6 +514,9 @@ private extension VideoPlayerCastController {
|
|
|
401
514
|
mediaLoadRequestDataBuilder.mediaInformation = mediaInfo
|
|
402
515
|
mediaLoadRequestDataBuilder.autoplay = NSNumber(value: localWasPlaying)
|
|
403
516
|
mediaLoadRequestDataBuilder.startTime = playPosition.isFinite ? playPosition : 0
|
|
517
|
+
if playPosition.isFinite {
|
|
518
|
+
lastForwardedSeekTime = playPosition
|
|
519
|
+
}
|
|
404
520
|
let request = remoteMediaClient.loadMedia(with: mediaLoadRequestDataBuilder.build())
|
|
405
521
|
request.delegate = self
|
|
406
522
|
mediaLoadRequest = request
|
|
@@ -419,6 +535,14 @@ private extension VideoPlayerCastController {
|
|
|
419
535
|
|
|
420
536
|
observeRemoteMediaClient(remoteMediaClient)
|
|
421
537
|
isLoadedOnCast = isCurrentVideo(remoteMediaClient.mediaStatus?.mediaInformation)
|
|
538
|
+
if isLoadedOnCast {
|
|
539
|
+
player?.pause()
|
|
540
|
+
let position = remoteMediaClient.approximateStreamPosition()
|
|
541
|
+
if position.isFinite {
|
|
542
|
+
syncLocalPlayhead(to: position)
|
|
543
|
+
}
|
|
544
|
+
syncCastingObserversIfNeeded()
|
|
545
|
+
}
|
|
422
546
|
handleRemoteMediaStatus(remoteMediaClient.mediaStatus)
|
|
423
547
|
}
|
|
424
548
|
|
|
@@ -477,6 +601,7 @@ private extension VideoPlayerCastController {
|
|
|
477
601
|
options.relative = false
|
|
478
602
|
options.resumeState = .unchanged
|
|
479
603
|
remoteMediaClient.seek(with: options)
|
|
604
|
+
syncLocalPlayhead(to: time)
|
|
480
605
|
case .volume(let volume):
|
|
481
606
|
remoteMediaClient.setStreamVolume(volume)
|
|
482
607
|
case .muted(let muted):
|
|
@@ -591,6 +716,9 @@ private extension VideoPlayerCastController {
|
|
|
591
716
|
stopRemoteMediaObservation()
|
|
592
717
|
clearMediaLoadRequest()
|
|
593
718
|
pendingCastCommands.removeAll()
|
|
719
|
+
DispatchQueue.main.async { [weak self] in
|
|
720
|
+
self?.syncCastingObserversIfNeeded()
|
|
721
|
+
}
|
|
594
722
|
let seekTime = CMTime(seconds: position, preferredTimescale: 600)
|
|
595
723
|
player?.seek(to: seekTime) { [weak self] _ in
|
|
596
724
|
if shouldResume {
|
|
@@ -613,8 +741,12 @@ private extension VideoPlayerCastController {
|
|
|
613
741
|
isLoadedOnCast = true
|
|
614
742
|
didNotifyRemoteEnd = false
|
|
615
743
|
DispatchQueue.main.async { [weak self] in
|
|
616
|
-
|
|
617
|
-
self?.
|
|
744
|
+
guard let self else { return }
|
|
745
|
+
if let position = self.player?.currentTime().seconds, position.isFinite {
|
|
746
|
+
self.lastForwardedSeekTime = position
|
|
747
|
+
}
|
|
748
|
+
self.syncCastingObserversIfNeeded()
|
|
749
|
+
self.updateCastOverlayVisibility(visible: self.arePlaybackControlsVisible)
|
|
618
750
|
}
|
|
619
751
|
flushPendingCastCommands()
|
|
620
752
|
}
|
|
@@ -630,6 +762,9 @@ private extension VideoPlayerCastController {
|
|
|
630
762
|
clearMediaLoadRequest()
|
|
631
763
|
isLoadingOnCast = false
|
|
632
764
|
isLoadedOnCast = false
|
|
765
|
+
DispatchQueue.main.async { [weak self] in
|
|
766
|
+
self?.syncCastingObserversIfNeeded()
|
|
767
|
+
}
|
|
633
768
|
if !pendingCastCommands.isEmpty {
|
|
634
769
|
let didApplyPlaybackCommand = applyPendingCastCommandsToLocalPlayer()
|
|
635
770
|
if localWasPlaying && !didApplyPlaybackCommand {
|
|
@@ -755,6 +890,8 @@ final class VideoPlayerCastController {
|
|
|
755
890
|
_ = player
|
|
756
891
|
}
|
|
757
892
|
|
|
893
|
+
func installOverlayIfNeeded() {}
|
|
894
|
+
|
|
758
895
|
func detach(stopRemoteMedia: Bool) {
|
|
759
896
|
_ = stopRemoteMedia
|
|
760
897
|
}
|
|
@@ -8,7 +8,7 @@ import AVKit
|
|
|
8
8
|
*/
|
|
9
9
|
@objc(VideoPlayerPlugin)
|
|
10
10
|
public class VideoPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
11
|
-
private let pluginVersion: String = "8.2.
|
|
11
|
+
private let pluginVersion: String = "8.2.8"
|
|
12
12
|
public let identifier = "VideoPlayerPlugin"
|
|
13
13
|
public let jsName = "VideoPlayer"
|
|
14
14
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
@testable import VideoPlayerPlugin
|
|
3
|
+
|
|
4
|
+
final class PlayerDismissExitDetectionTests: XCTestCase {
|
|
5
|
+
func testReportsExitWhenModalHierarchyIsGone() {
|
|
6
|
+
XCTAssertTrue(
|
|
7
|
+
PlayerDismissExitDetection.shouldReportExit(
|
|
8
|
+
presentingViewController: nil,
|
|
9
|
+
viewWindow: nil
|
|
10
|
+
)
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
func testDoesNotReportExitWhenBeingDismissedButStillInHierarchy() {
|
|
15
|
+
XCTAssertFalse(
|
|
16
|
+
PlayerDismissExitDetection.shouldReportExit(
|
|
17
|
+
presentingViewController: UIViewController(),
|
|
18
|
+
viewWindow: UIWindow()
|
|
19
|
+
)
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func testDoesNotReportExitWhenCoveredByAnotherViewController() {
|
|
24
|
+
XCTAssertFalse(
|
|
25
|
+
PlayerDismissExitDetection.shouldReportExit(
|
|
26
|
+
presentingViewController: UIViewController(),
|
|
27
|
+
viewWindow: UIWindow()
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func testDoesNotReportExitWhenOnlyPresenterIsNil() {
|
|
33
|
+
XCTAssertFalse(
|
|
34
|
+
PlayerDismissExitDetection.shouldReportExit(
|
|
35
|
+
presentingViewController: nil,
|
|
36
|
+
viewWindow: UIWindow()
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func testDoesNotReportExitWhenOnlyWindowIsNil() {
|
|
42
|
+
XCTAssertFalse(
|
|
43
|
+
PlayerDismissExitDetection.shouldReportExit(
|
|
44
|
+
presentingViewController: UIViewController(),
|
|
45
|
+
viewWindow: nil
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import AVKit
|
|
2
|
+
import XCTest
|
|
3
|
+
@testable import VideoPlayerPlugin
|
|
4
|
+
|
|
5
|
+
final class VideoPlayerCastControllerTests: XCTestCase {
|
|
6
|
+
func testCastOverlayTargetsContentOverlayView() {
|
|
7
|
+
let playerViewController = AVPlayerViewController()
|
|
8
|
+
playerViewController.loadViewIfNeeded()
|
|
9
|
+
|
|
10
|
+
let overlayView = VideoPlayerCastOverlayLayout.overlayView(for: playerViewController)
|
|
11
|
+
|
|
12
|
+
XCTAssertNotNil(overlayView)
|
|
13
|
+
XCTAssertIdentical(overlayView, playerViewController.contentOverlayView)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func testSeekSyncRequiresBaselineAndMeaningfulDelta() {
|
|
17
|
+
XCTAssertFalse(
|
|
18
|
+
VideoPlayerCastSeekSync.shouldForwardLocalSeek(localTime: 42, lastForwardedTime: nil)
|
|
19
|
+
)
|
|
20
|
+
XCTAssertFalse(
|
|
21
|
+
VideoPlayerCastSeekSync.shouldForwardLocalSeek(localTime: 10.1, lastForwardedTime: 10)
|
|
22
|
+
)
|
|
23
|
+
XCTAssertTrue(
|
|
24
|
+
VideoPlayerCastSeekSync.shouldForwardLocalSeek(localTime: 10.5, lastForwardedTime: 10)
|
|
25
|
+
)
|
|
26
|
+
XCTAssertFalse(
|
|
27
|
+
VideoPlayerCastSeekSync.shouldForwardLocalSeek(localTime: .nan, lastForwardedTime: 10)
|
|
28
|
+
)
|
|
29
|
+
XCTAssertFalse(
|
|
30
|
+
VideoPlayerCastSeekSync.shouldForwardLocalSeek(localTime: .infinity, lastForwardedTime: 10)
|
|
31
|
+
)
|
|
32
|
+
XCTAssertTrue(
|
|
33
|
+
VideoPlayerCastSeekSync.shouldForwardLocalSeek(localTime: 10.25, lastForwardedTime: 10)
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func testControlsVisibilityReflectsHiddenAndAlpha() {
|
|
38
|
+
let controlsView = UIView()
|
|
39
|
+
controlsView.isHidden = false
|
|
40
|
+
controlsView.alpha = 1
|
|
41
|
+
|
|
42
|
+
XCTAssertTrue(AVPlayerControlsVisibilityObserver.areControlsVisible(in: controlsView))
|
|
43
|
+
|
|
44
|
+
controlsView.isHidden = true
|
|
45
|
+
XCTAssertFalse(AVPlayerControlsVisibilityObserver.areControlsVisible(in: controlsView))
|
|
46
|
+
|
|
47
|
+
controlsView.isHidden = false
|
|
48
|
+
controlsView.alpha = 0
|
|
49
|
+
XCTAssertFalse(AVPlayerControlsVisibilityObserver.areControlsVisible(in: controlsView))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func testControlsContainerLocatorFindsLastSubviewInFirstContainer() {
|
|
53
|
+
let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 240))
|
|
54
|
+
let container = UIView(frame: rootView.bounds)
|
|
55
|
+
let contentView = UIView(frame: rootView.bounds)
|
|
56
|
+
let controlsView = UIView(frame: CGRect(x: 0, y: 180, width: 320, height: 60))
|
|
57
|
+
|
|
58
|
+
rootView.addSubview(container)
|
|
59
|
+
container.addSubview(contentView)
|
|
60
|
+
container.addSubview(controlsView)
|
|
61
|
+
|
|
62
|
+
XCTAssertIdentical(
|
|
63
|
+
AVPlayerControlsVisibilityObserver.controlsContainer(in: rootView),
|
|
64
|
+
controlsView
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func testShouldReanchorWhenControlsViewChanges() {
|
|
69
|
+
let oldControls = UIView()
|
|
70
|
+
let newControls = UIView()
|
|
71
|
+
|
|
72
|
+
XCTAssertTrue(
|
|
73
|
+
AVPlayerControlsVisibilityObserver.shouldReanchor(
|
|
74
|
+
monitoredView: oldControls,
|
|
75
|
+
currentControlsView: newControls
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
func testShouldReanchorWhenMonitoredViewIsDetached() {
|
|
81
|
+
let detachedControls = UIView()
|
|
82
|
+
|
|
83
|
+
XCTAssertTrue(
|
|
84
|
+
AVPlayerControlsVisibilityObserver.shouldReanchor(
|
|
85
|
+
monitoredView: detachedControls,
|
|
86
|
+
currentControlsView: detachedControls
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
func testShouldNotReanchorWhenMonitoringCurrentControls() {
|
|
92
|
+
let container = UIView()
|
|
93
|
+
let controls = UIView()
|
|
94
|
+
container.addSubview(controls)
|
|
95
|
+
|
|
96
|
+
XCTAssertFalse(
|
|
97
|
+
AVPlayerControlsVisibilityObserver.shouldReanchor(
|
|
98
|
+
monitoredView: controls,
|
|
99
|
+
currentControlsView: controls
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -2,13 +2,6 @@ import XCTest
|
|
|
2
2
|
@testable import VideoPlayerPlugin
|
|
3
3
|
|
|
4
4
|
class VideoPlayerTests: XCTestCase {
|
|
5
|
-
func testEcho() {
|
|
6
|
-
let implementation = VideoPlayer()
|
|
7
|
-
let value = "Hello, World!"
|
|
8
|
-
let result = implementation.echo(value)
|
|
9
|
-
XCTAssertEqual(value, result)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
5
|
func testSubtitleTrackParserPrefersSubtitlesArray() {
|
|
13
6
|
let tracks = VideoSubtitleTrackParser.parse(
|
|
14
7
|
from: [
|
|
@@ -25,9 +18,14 @@ class VideoPlayerTests: XCTestCase {
|
|
|
25
18
|
XCTAssertEqual(tracks[1].language, "fr")
|
|
26
19
|
}
|
|
27
20
|
|
|
28
|
-
func
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
21
|
+
func testExitEmissionGuardEmitsOnce() {
|
|
22
|
+
var guardState = ExitEmissionGuard()
|
|
23
|
+
var emitCount = 0
|
|
24
|
+
|
|
25
|
+
guardState.emitIfNeeded { emitCount += 1 }
|
|
26
|
+
guardState.emitIfNeeded { emitCount += 1 }
|
|
27
|
+
|
|
28
|
+
XCTAssertEqual(emitCount, 1)
|
|
29
|
+
XCTAssertTrue(guardState.didEmit)
|
|
32
30
|
}
|
|
33
31
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-video-player",
|
|
3
|
-
"version": "8.2.
|
|
3
|
+
"version": "8.2.8",
|
|
4
4
|
"description": "Capacitor plugin to play video in native player",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"dist/",
|
|
13
13
|
"ios/Sources",
|
|
14
14
|
"ios/Tests",
|
|
15
|
+
"scripts/",
|
|
15
16
|
"Package.swift",
|
|
16
17
|
"CapgoCapacitorVideoPlayer.podspec"
|
|
17
18
|
],
|
|
@@ -33,8 +34,9 @@
|
|
|
33
34
|
"player"
|
|
34
35
|
],
|
|
35
36
|
"scripts": {
|
|
36
|
-
"verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
|
|
37
|
+
"verify": "npm run verify:ios && npm run test:ios && npm run verify:android && npm run verify:web",
|
|
37
38
|
"verify:ios": "xcodebuild -scheme CapgoCapacitorVideoPlayer -destination generic/platform=iOS",
|
|
39
|
+
"test:ios": "./scripts/test-ios.sh",
|
|
38
40
|
"verify:android": "cd android && ./gradlew clean build test && cd ..",
|
|
39
41
|
"verify:web": "npm run build",
|
|
40
42
|
"lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
|