@capgo/capacitor-video-player 8.1.8 → 8.1.10

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.
@@ -0,0 +1,792 @@
1
+ import AVFoundation
2
+ import AVKit
3
+ import Foundation
4
+ import UIKit
5
+
6
+ #if canImport(GoogleCast)
7
+ import GoogleCast
8
+
9
+ final class VideoPlayerCastController: NSObject {
10
+ private enum PendingCastCommand {
11
+ case play
12
+ case pause
13
+ case seek(Double)
14
+ case volume(Float)
15
+ case muted(Bool)
16
+ case rate(Float)
17
+ }
18
+
19
+ private let videoUrl: String
20
+ private let title: String?
21
+ private let smallTitle: String?
22
+ private let artwork: String?
23
+
24
+ private weak var player: AVPlayer?
25
+ private weak var playerViewController: AVPlayerViewController?
26
+ private weak var castButton: GCKUICastButton?
27
+ private weak var castIndicatorLabel: UILabel?
28
+ private weak var observedRemoteMediaClient: GCKRemoteMediaClient?
29
+ private var mediaLoadRequest: GCKRequest?
30
+ private var pendingCastCommands: [PendingCastCommand] = []
31
+ private var isLoadedOnCast = false
32
+ private var isLoadingOnCast = false
33
+ private var localWasPlaying = false
34
+ private var isDetached = false
35
+ private var lastRemoteIsPlaying: Bool?
36
+ private var didNotifyRemoteEnd = false
37
+ private var onPlay: (() -> Void)?
38
+ private var onPause: (() -> Void)?
39
+ private var onEnd: (() -> Void)?
40
+ private var controlsHideTimer: Timer?
41
+ private weak var tapGestureRecognizer: UITapGestureRecognizer?
42
+ private static let overlayAutoHideDuration: TimeInterval = 3.0
43
+
44
+ var isCasting: Bool {
45
+ return remoteMediaClient != nil && isLoadedOnCast
46
+ }
47
+
48
+ private var remoteMediaClient: GCKRemoteMediaClient? {
49
+ guard GCKCastContext.isSharedInstanceInitialized() else {
50
+ return nil
51
+ }
52
+ return GCKCastContext.sharedInstance().sessionManager.currentCastSession?.remoteMediaClient
53
+ }
54
+
55
+ init(videoUrl: String, title: String?, smallTitle: String?, artwork: String?) {
56
+ self.videoUrl = videoUrl
57
+ self.title = title
58
+ self.smallTitle = smallTitle
59
+ self.artwork = artwork
60
+ super.init()
61
+ }
62
+
63
+ func attach(to playerViewController: AVPlayerViewController, player: AVPlayer) {
64
+ self.playerViewController = playerViewController
65
+ self.player = player
66
+
67
+ let attachOnMain = { [weak self] in
68
+ guard let self = self,
69
+ Self.configureCastContext() else {
70
+ return
71
+ }
72
+
73
+ GCKCastContext.sharedInstance().sessionManager.add(self)
74
+ self.addCastButton(to: playerViewController)
75
+ self.addCastIndicator(to: playerViewController)
76
+ self.beginObservingPlayerTaps(playerViewController)
77
+ self.loadMediaIfCastSessionAvailable()
78
+ }
79
+
80
+ if Thread.isMainThread {
81
+ attachOnMain()
82
+ } else {
83
+ DispatchQueue.main.async(execute: attachOnMain)
84
+ }
85
+ }
86
+
87
+ func detach(stopRemoteMedia: Bool) {
88
+ guard !isDetached else {
89
+ return
90
+ }
91
+ isDetached = true
92
+ clearMediaLoadRequest()
93
+ pendingCastCommands.removeAll()
94
+
95
+ DispatchQueue.main.async { [weak self] in
96
+ guard let self = self,
97
+ GCKCastContext.isSharedInstanceInitialized() else {
98
+ return
99
+ }
100
+
101
+ if stopRemoteMedia {
102
+ self.remoteMediaClient?.stop()
103
+ }
104
+
105
+ self.stopRemoteMediaObservation()
106
+ GCKCastContext.sharedInstance().sessionManager.remove(self)
107
+ self.controlsHideTimer?.invalidate()
108
+ self.controlsHideTimer = nil
109
+ if let tapRecognizer = self.tapGestureRecognizer,
110
+ let view = self.playerViewController?.view {
111
+ view.removeGestureRecognizer(tapRecognizer)
112
+ }
113
+ self.tapGestureRecognizer = nil
114
+ self.castButton?.removeFromSuperview()
115
+ self.castButton = nil
116
+ self.castIndicatorLabel?.removeFromSuperview()
117
+ self.castIndicatorLabel = nil
118
+ self.player = nil
119
+ self.playerViewController = nil
120
+ self.isLoadedOnCast = false
121
+ self.isLoadingOnCast = false
122
+ }
123
+ }
124
+
125
+ func play() -> Bool {
126
+ guard let remoteMediaClient = remoteMediaClient else {
127
+ return false
128
+ }
129
+ guard isLoadedOnCast else {
130
+ return enqueuePendingCastCommand(.play)
131
+ }
132
+ remoteMediaClient.play()
133
+ return true
134
+ }
135
+
136
+ func pause() -> Bool {
137
+ guard let remoteMediaClient = remoteMediaClient else {
138
+ return false
139
+ }
140
+ guard isLoadedOnCast else {
141
+ return enqueuePendingCastCommand(.pause)
142
+ }
143
+ remoteMediaClient.pause()
144
+ return true
145
+ }
146
+
147
+ func setCurrentTime(_ time: Double) -> Bool {
148
+ guard let remoteMediaClient = remoteMediaClient else {
149
+ return false
150
+ }
151
+ guard isLoadedOnCast else {
152
+ return enqueuePendingCastCommand(.seek(time))
153
+ }
154
+ let options = GCKMediaSeekOptions()
155
+ options.interval = time
156
+ options.relative = false
157
+ options.resumeState = .unchanged
158
+ remoteMediaClient.seek(with: options)
159
+ return true
160
+ }
161
+
162
+ func isPlaying() -> Bool {
163
+ guard let playerState = remoteMediaClient?.mediaStatus?.playerState else {
164
+ return false
165
+ }
166
+ return playerState == .playing || playerState == .buffering
167
+ }
168
+
169
+ func getDuration() -> Double {
170
+ return remoteMediaClient?.mediaStatus?.mediaInformation?.streamDuration ?? 0
171
+ }
172
+
173
+ func getCurrentTime() -> Double {
174
+ return remoteMediaClient?.approximateStreamPosition() ?? 0
175
+ }
176
+
177
+ func getVolume() -> Float {
178
+ return remoteMediaClient?.mediaStatus?.volume ?? 0
179
+ }
180
+
181
+ func setVolume(_ volume: Float) -> Bool {
182
+ guard let remoteMediaClient = remoteMediaClient else {
183
+ return false
184
+ }
185
+ guard isLoadedOnCast else {
186
+ return enqueuePendingCastCommand(.volume(volume))
187
+ }
188
+ remoteMediaClient.setStreamVolume(volume)
189
+ return true
190
+ }
191
+
192
+ func getMuted() -> Bool {
193
+ return remoteMediaClient?.mediaStatus?.isMuted ?? false
194
+ }
195
+
196
+ func setMuted(_ muted: Bool) -> Bool {
197
+ guard let remoteMediaClient = remoteMediaClient else {
198
+ return false
199
+ }
200
+ guard isLoadedOnCast else {
201
+ return enqueuePendingCastCommand(.muted(muted))
202
+ }
203
+ remoteMediaClient.setStreamMuted(muted)
204
+ return true
205
+ }
206
+
207
+ func getRate() -> Float {
208
+ return remoteMediaClient?.mediaStatus?.playbackRate ?? 0
209
+ }
210
+
211
+ func setRate(_ rate: Float) -> Bool {
212
+ guard let remoteMediaClient = remoteMediaClient else {
213
+ return false
214
+ }
215
+ guard isLoadedOnCast else {
216
+ return enqueuePendingCastCommand(.rate(rate))
217
+ }
218
+ remoteMediaClient.setPlaybackRate(rate)
219
+ return true
220
+ }
221
+
222
+ func restartPlayback() -> Bool {
223
+ guard let remoteMediaClient = remoteMediaClient,
224
+ let mediaInfo = makeMediaInformation() else {
225
+ return false
226
+ }
227
+
228
+ didNotifyRemoteEnd = false
229
+ isLoadedOnCast = false
230
+ isLoadingOnCast = true
231
+ clearMediaLoadRequest()
232
+
233
+ let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
234
+ mediaLoadRequestDataBuilder.mediaInformation = mediaInfo
235
+ mediaLoadRequestDataBuilder.autoplay = NSNumber(value: true)
236
+ mediaLoadRequestDataBuilder.startTime = 0
237
+ let request = remoteMediaClient.loadMedia(with: mediaLoadRequestDataBuilder.build())
238
+ request.delegate = self
239
+ mediaLoadRequest = request
240
+ return true
241
+ }
242
+
243
+ func setOnPlay(_ callback: @escaping () -> Void) {
244
+ onPlay = callback
245
+ }
246
+
247
+ func setOnPause(_ callback: @escaping () -> Void) {
248
+ onPause = callback
249
+ }
250
+
251
+ func setOnEnd(_ callback: @escaping () -> Void) {
252
+ onEnd = callback
253
+ }
254
+ }
255
+
256
+ private extension VideoPlayerCastController {
257
+ static func configureCastContext() -> Bool {
258
+ guard Thread.isMainThread else {
259
+ return false
260
+ }
261
+
262
+ if GCKCastContext.isSharedInstanceInitialized() {
263
+ return true
264
+ }
265
+
266
+ // This plugin intentionally uses the default media receiver.
267
+ let criteria = GCKDiscoveryCriteria(applicationID: kGCKDefaultMediaReceiverApplicationID)
268
+ let options = GCKCastOptions(discoveryCriteria: criteria)
269
+ var error: GCKError?
270
+ return GCKCastContext.setSharedInstanceWith(options, error: &error)
271
+ }
272
+
273
+ func addCastButton(to playerViewController: AVPlayerViewController) {
274
+ guard castButton == nil else {
275
+ return
276
+ }
277
+
278
+ guard let overlayView = playerViewController.view else {
279
+ return
280
+ }
281
+
282
+ overlayView.isUserInteractionEnabled = true
283
+ let button = GCKUICastButton(frame: .zero)
284
+ button.translatesAutoresizingMaskIntoConstraints = false
285
+ button.tintColor = .white
286
+ button.backgroundColor = UIColor.black.withAlphaComponent(0.35)
287
+ button.layer.cornerRadius = 22
288
+ button.clipsToBounds = true
289
+ button.accessibilityLabel = "Cast"
290
+
291
+ overlayView.addSubview(button)
292
+ NSLayoutConstraint.activate([
293
+ button.widthAnchor.constraint(equalToConstant: 44),
294
+ button.heightAnchor.constraint(equalToConstant: 44),
295
+ // Position below AVPlayerViewController's top row of controls (Done/X and route picker)
296
+ // using trailing to stay away from the leading-side dismiss button.
297
+ button.topAnchor.constraint(equalTo: overlayView.safeAreaLayoutGuide.topAnchor, constant: 60),
298
+ button.trailingAnchor.constraint(equalTo: overlayView.safeAreaLayoutGuide.trailingAnchor, constant: -16)
299
+ ])
300
+
301
+ castButton = button
302
+ }
303
+
304
+ func addCastIndicator(to playerViewController: AVPlayerViewController) {
305
+ guard castIndicatorLabel == nil else {
306
+ return
307
+ }
308
+
309
+ guard let overlayView = playerViewController.view else {
310
+ return
311
+ }
312
+
313
+ let label = UILabel(frame: .zero)
314
+ label.translatesAutoresizingMaskIntoConstraints = false
315
+ label.text = "Casting"
316
+ label.textColor = .white
317
+ label.font = UIFont.preferredFont(forTextStyle: .caption1)
318
+ label.backgroundColor = UIColor.black.withAlphaComponent(0.55)
319
+ label.layer.cornerRadius = 10
320
+ label.clipsToBounds = true
321
+ label.isHidden = true
322
+
323
+ overlayView.addSubview(label)
324
+
325
+ if let castButton = castButton {
326
+ NSLayoutConstraint.activate([
327
+ label.centerYAnchor.constraint(equalTo: castButton.centerYAnchor),
328
+ label.trailingAnchor.constraint(equalTo: castButton.leadingAnchor, constant: -8)
329
+ ])
330
+ } else {
331
+ NSLayoutConstraint.activate([
332
+ label.topAnchor.constraint(equalTo: overlayView.safeAreaLayoutGuide.topAnchor, constant: 60),
333
+ label.trailingAnchor.constraint(equalTo: overlayView.safeAreaLayoutGuide.trailingAnchor, constant: -68)
334
+ ])
335
+ }
336
+
337
+ castIndicatorLabel = label
338
+ }
339
+
340
+ func beginObservingPlayerTaps(_ playerViewController: AVPlayerViewController) {
341
+ guard let overlayView = playerViewController.view else { return }
342
+ // Remove any existing recognizer before adding a new one to prevent duplicates.
343
+ if let existing = tapGestureRecognizer {
344
+ overlayView.removeGestureRecognizer(existing)
345
+ }
346
+ let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handlePlayerTap))
347
+ tapRecognizer.cancelsTouchesInView = false
348
+ overlayView.addGestureRecognizer(tapRecognizer)
349
+ self.tapGestureRecognizer = tapRecognizer
350
+ // Show the overlay immediately; it will auto-hide after the standard controls delay.
351
+ showOverlayControls()
352
+ }
353
+
354
+ @objc func handlePlayerTap() {
355
+ showOverlayControls()
356
+ }
357
+
358
+ func showOverlayControls() {
359
+ castButton?.isHidden = false
360
+ castIndicatorLabel?.isHidden = !isCasting
361
+ controlsHideTimer?.invalidate()
362
+ controlsHideTimer = Timer.scheduledTimer(withTimeInterval: Self.overlayAutoHideDuration, repeats: false) { [weak self] _ in
363
+ self?.hideOverlayControls()
364
+ }
365
+ }
366
+
367
+ func hideOverlayControls() {
368
+ controlsHideTimer = nil
369
+ let button = castButton
370
+ let label = castIndicatorLabel
371
+ UIView.animate(withDuration: 0.3, animations: {
372
+ button?.alpha = 0
373
+ label?.alpha = 0
374
+ }, completion: { _ in
375
+ button?.isHidden = true
376
+ label?.isHidden = true
377
+ button?.alpha = 1
378
+ label?.alpha = 1
379
+ })
380
+ }
381
+
382
+ func loadMediaIfCastSessionAvailable() {
383
+ guard let remoteMediaClient = remoteMediaClient,
384
+ let mediaInfo = makeMediaInformation() else {
385
+ return
386
+ }
387
+
388
+ observeRemoteMediaClient(remoteMediaClient)
389
+ let playPosition = player?.currentTime().seconds ?? 0
390
+ localWasPlaying = (player?.rate ?? 0) > 0
391
+ player?.pause()
392
+ isLoadedOnCast = false
393
+ isLoadingOnCast = true
394
+ lastRemoteIsPlaying = nil
395
+ didNotifyRemoteEnd = false
396
+ clearMediaLoadRequest()
397
+
398
+ let mediaLoadRequestDataBuilder = GCKMediaLoadRequestDataBuilder()
399
+ mediaLoadRequestDataBuilder.mediaInformation = mediaInfo
400
+ mediaLoadRequestDataBuilder.autoplay = NSNumber(value: localWasPlaying)
401
+ mediaLoadRequestDataBuilder.startTime = playPosition.isFinite ? playPosition : 0
402
+ let request = remoteMediaClient.loadMedia(with: mediaLoadRequestDataBuilder.build())
403
+ request.delegate = self
404
+ mediaLoadRequest = request
405
+ }
406
+
407
+ func resumeCastSession(_ session: GCKSession) {
408
+ clearMediaLoadRequest()
409
+ pendingCastCommands.removeAll()
410
+ isLoadingOnCast = false
411
+
412
+ guard let remoteMediaClient = session.remoteMediaClient else {
413
+ isLoadedOnCast = false
414
+ stopRemoteMediaObservation()
415
+ return
416
+ }
417
+
418
+ observeRemoteMediaClient(remoteMediaClient)
419
+ isLoadedOnCast = isCurrentVideo(remoteMediaClient.mediaStatus?.mediaInformation)
420
+ handleRemoteMediaStatus(remoteMediaClient.mediaStatus)
421
+ }
422
+
423
+ func observeRemoteMediaClient(_ remoteMediaClient: GCKRemoteMediaClient) {
424
+ if let observedRemoteMediaClient = observedRemoteMediaClient,
425
+ observedRemoteMediaClient === remoteMediaClient {
426
+ return
427
+ }
428
+
429
+ observedRemoteMediaClient?.remove(self)
430
+ remoteMediaClient.add(self)
431
+ observedRemoteMediaClient = remoteMediaClient
432
+ }
433
+
434
+ func stopRemoteMediaObservation() {
435
+ observedRemoteMediaClient?.remove(self)
436
+ observedRemoteMediaClient = nil
437
+ lastRemoteIsPlaying = nil
438
+ }
439
+
440
+ private func enqueuePendingCastCommand(_ command: PendingCastCommand) -> Bool {
441
+ guard isLoadingOnCast else {
442
+ return false
443
+ }
444
+
445
+ pendingCastCommands.append(command)
446
+ if pendingCastCommands.count > 20 {
447
+ pendingCastCommands.removeFirst(pendingCastCommands.count - 20)
448
+ }
449
+ return true
450
+ }
451
+
452
+ func flushPendingCastCommands() {
453
+ guard let remoteMediaClient = remoteMediaClient,
454
+ isLoadedOnCast else {
455
+ pendingCastCommands.removeAll()
456
+ return
457
+ }
458
+
459
+ let commands = pendingCastCommands
460
+ pendingCastCommands.removeAll()
461
+ for command in commands {
462
+ apply(command, to: remoteMediaClient)
463
+ }
464
+ }
465
+
466
+ private func apply(_ command: PendingCastCommand, to remoteMediaClient: GCKRemoteMediaClient) {
467
+ switch command {
468
+ case .play:
469
+ remoteMediaClient.play()
470
+ case .pause:
471
+ remoteMediaClient.pause()
472
+ case .seek(let time):
473
+ let options = GCKMediaSeekOptions()
474
+ options.interval = time
475
+ options.relative = false
476
+ options.resumeState = .unchanged
477
+ remoteMediaClient.seek(with: options)
478
+ case .volume(let volume):
479
+ remoteMediaClient.setStreamVolume(volume)
480
+ case .muted(let muted):
481
+ remoteMediaClient.setStreamMuted(muted)
482
+ case .rate(let rate):
483
+ remoteMediaClient.setPlaybackRate(rate)
484
+ }
485
+ }
486
+
487
+ func applyPendingCastCommandsToLocalPlayer() -> Bool {
488
+ var didApplyPlaybackCommand = false
489
+ let commands = pendingCastCommands
490
+ pendingCastCommands.removeAll()
491
+ for command in commands {
492
+ switch command {
493
+ case .play:
494
+ player?.play()
495
+ didApplyPlaybackCommand = true
496
+ case .pause:
497
+ player?.pause()
498
+ didApplyPlaybackCommand = true
499
+ case .seek(let time):
500
+ player?.seek(to: CMTime(seconds: time, preferredTimescale: 600))
501
+ case .volume(let volume):
502
+ player?.volume = volume
503
+ case .muted(let muted):
504
+ player?.isMuted = muted
505
+ case .rate(let rate):
506
+ player?.rate = rate
507
+ didApplyPlaybackCommand = true
508
+ }
509
+ }
510
+ return didApplyPlaybackCommand
511
+ }
512
+
513
+ func makeMediaInformation() -> GCKMediaInformation? {
514
+ guard let url = URL(string: videoUrl) else {
515
+ return nil
516
+ }
517
+
518
+ let metadata = GCKMediaMetadata()
519
+ metadata.setString(title ?? url.lastPathComponent, forKey: kGCKMetadataKeyTitle)
520
+ if let smallTitle = smallTitle, !smallTitle.isEmpty {
521
+ metadata.setString(smallTitle, forKey: kGCKMetadataKeySubtitle)
522
+ }
523
+ if let artwork = artwork,
524
+ let artworkUrl = URL(string: artwork) {
525
+ metadata.addImage(GCKImage(url: artworkUrl, width: 480, height: 360))
526
+ }
527
+
528
+ let builder = GCKMediaInformationBuilder(contentURL: url)
529
+ builder.streamType = .buffered
530
+ builder.contentType = contentType(for: url)
531
+ builder.metadata = metadata
532
+ if let duration = player?.currentItem?.duration.seconds,
533
+ duration.isFinite,
534
+ duration > 0 {
535
+ builder.streamDuration = duration
536
+ }
537
+ return builder.build()
538
+ }
539
+
540
+ func isCurrentVideo(_ mediaInformation: GCKMediaInformation?) -> Bool {
541
+ guard let mediaInformation = mediaInformation else {
542
+ return false
543
+ }
544
+ if mediaInformation.contentID == videoUrl {
545
+ return true
546
+ }
547
+ guard let expectedUrl = URL(string: videoUrl) else {
548
+ return false
549
+ }
550
+ return mediaInformation.contentURL == expectedUrl
551
+ }
552
+
553
+ func contentType(for url: URL) -> String {
554
+ switch url.pathExtension.lowercased() {
555
+ case "m3u8":
556
+ return "application/x-mpegURL"
557
+ case "mpd":
558
+ return "application/dash+xml"
559
+ case "mov":
560
+ return "video/quicktime"
561
+ case "m4v":
562
+ return "video/x-m4v"
563
+ case "webm":
564
+ return "video/webm"
565
+ case "mp4":
566
+ return "video/mp4"
567
+ default:
568
+ return "video/mp4"
569
+ }
570
+ }
571
+
572
+ func resumeLocalPlayback(from session: GCKSession) {
573
+ guard !isDetached,
574
+ isLoadedOnCast else {
575
+ return
576
+ }
577
+
578
+ let remoteMediaClient = session.remoteMediaClient
579
+ let position = remoteMediaClient?.approximateStreamPosition() ?? 0
580
+ let shouldResume = remoteMediaClient?.mediaStatus?.playerState == .playing ||
581
+ remoteMediaClient?.mediaStatus?.playerState == .buffering
582
+
583
+ isLoadedOnCast = false
584
+ isLoadingOnCast = false
585
+ stopRemoteMediaObservation()
586
+ clearMediaLoadRequest()
587
+ pendingCastCommands.removeAll()
588
+ let seekTime = CMTime(seconds: position, preferredTimescale: 600)
589
+ player?.seek(to: seekTime) { [weak self] _ in
590
+ if shouldResume {
591
+ self?.player?.play()
592
+ }
593
+ }
594
+ }
595
+
596
+ func clearMediaLoadRequest() {
597
+ mediaLoadRequest?.delegate = nil
598
+ mediaLoadRequest = nil
599
+ }
600
+
601
+ func completeMediaLoadRequest(_ request: GCKRequest) {
602
+ guard request === mediaLoadRequest else {
603
+ return
604
+ }
605
+ clearMediaLoadRequest()
606
+ isLoadingOnCast = false
607
+ isLoadedOnCast = true
608
+ didNotifyRemoteEnd = false
609
+ DispatchQueue.main.async { [weak self] in
610
+ // Reveal the cast indicator and extend the controls visibility window.
611
+ self?.showOverlayControls()
612
+ }
613
+ flushPendingCastCommands()
614
+ }
615
+
616
+ func failMediaLoadRequest(_ request: GCKRequest) {
617
+ guard request === mediaLoadRequest else {
618
+ return
619
+ }
620
+ cancelPendingCastHandoff(resumeLocalIfNeeded: true)
621
+ }
622
+
623
+ func cancelPendingCastHandoff(resumeLocalIfNeeded: Bool) {
624
+ clearMediaLoadRequest()
625
+ isLoadingOnCast = false
626
+ isLoadedOnCast = false
627
+ if !pendingCastCommands.isEmpty {
628
+ let didApplyPlaybackCommand = applyPendingCastCommandsToLocalPlayer()
629
+ if localWasPlaying && !didApplyPlaybackCommand {
630
+ player?.play()
631
+ }
632
+ return
633
+ }
634
+ if localWasPlaying {
635
+ player?.play()
636
+ } else if resumeLocalIfNeeded {
637
+ player?.pause()
638
+ }
639
+ }
640
+
641
+ func shouldHandleRemoteMediaStatus(_ mediaStatus: GCKMediaStatus) -> Bool {
642
+ if isLoadedOnCast || isLoadingOnCast {
643
+ return true
644
+ }
645
+ guard isCurrentVideo(mediaStatus.mediaInformation) else {
646
+ return false
647
+ }
648
+ isLoadedOnCast = true
649
+ return true
650
+ }
651
+
652
+ func handleRemoteMediaStatus(_ mediaStatus: GCKMediaStatus?) {
653
+ guard !isDetached,
654
+ let mediaStatus = mediaStatus,
655
+ shouldHandleRemoteMediaStatus(mediaStatus) else {
656
+ return
657
+ }
658
+
659
+ switch mediaStatus.playerState {
660
+ case .playing, .buffering:
661
+ didNotifyRemoteEnd = false
662
+ if lastRemoteIsPlaying != true {
663
+ DispatchQueue.main.async { [weak self] in
664
+ self?.onPlay?()
665
+ }
666
+ }
667
+ lastRemoteIsPlaying = true
668
+ case .paused:
669
+ if lastRemoteIsPlaying == true {
670
+ DispatchQueue.main.async { [weak self] in
671
+ self?.onPause?()
672
+ }
673
+ }
674
+ lastRemoteIsPlaying = false
675
+ case .idle:
676
+ if mediaStatus.idleReason == .finished,
677
+ !didNotifyRemoteEnd {
678
+ didNotifyRemoteEnd = true
679
+ DispatchQueue.main.async { [weak self] in
680
+ self?.onEnd?()
681
+ }
682
+ }
683
+ lastRemoteIsPlaying = false
684
+ default:
685
+ break
686
+ }
687
+ }
688
+ }
689
+
690
+ extension VideoPlayerCastController: GCKSessionManagerListener {
691
+ @objc func sessionManager(_ sessionManager: GCKSessionManager, didStart session: GCKSession) {
692
+ loadMediaIfCastSessionAvailable()
693
+ }
694
+
695
+ @objc func sessionManager(_ sessionManager: GCKSessionManager, didResumeSession session: GCKSession) {
696
+ resumeCastSession(session)
697
+ }
698
+
699
+ @objc func sessionManager(_ sessionManager: GCKSessionManager, didEnd session: GCKSession, withError error: Error?) {
700
+ if isLoadedOnCast {
701
+ resumeLocalPlayback(from: session)
702
+ } else if isLoadingOnCast {
703
+ cancelPendingCastHandoff(resumeLocalIfNeeded: true)
704
+ }
705
+ }
706
+
707
+ @objc func sessionManager(_ sessionManager: GCKSessionManager, didFailToStart session: GCKSession, withError error: Error) {
708
+ cancelPendingCastHandoff(resumeLocalIfNeeded: true)
709
+ }
710
+ }
711
+
712
+ extension VideoPlayerCastController: GCKRequestDelegate {
713
+ @objc func requestDidComplete(_ request: GCKRequest) {
714
+ completeMediaLoadRequest(request)
715
+ }
716
+
717
+ @objc func request(_ request: GCKRequest, didFailWithError error: GCKError) {
718
+ failMediaLoadRequest(request)
719
+ }
720
+
721
+ @objc func request(_ request: GCKRequest, didAbortWith abortReason: GCKRequestAbortReason) {
722
+ failMediaLoadRequest(request)
723
+ }
724
+ }
725
+
726
+ extension VideoPlayerCastController: GCKRemoteMediaClientListener {
727
+ @objc(remoteMediaClient:didUpdateMediaStatus:)
728
+ func remoteMediaClient(_ client: GCKRemoteMediaClient, didUpdate mediaStatus: GCKMediaStatus?) {
729
+ handleRemoteMediaStatus(mediaStatus)
730
+ }
731
+ }
732
+
733
+ #else
734
+
735
+ final class VideoPlayerCastController {
736
+ var isCasting: Bool {
737
+ return false
738
+ }
739
+
740
+ init(videoUrl: String, title: String?, smallTitle: String?, artwork: String?) {
741
+ _ = videoUrl
742
+ _ = title
743
+ _ = smallTitle
744
+ _ = artwork
745
+ }
746
+
747
+ func attach(to playerViewController: AVPlayerViewController, player: AVPlayer) {
748
+ _ = playerViewController
749
+ _ = player
750
+ }
751
+
752
+ func detach(stopRemoteMedia: Bool) {
753
+ _ = stopRemoteMedia
754
+ }
755
+
756
+ func play() -> Bool { return false }
757
+ func pause() -> Bool { return false }
758
+ func isPlaying() -> Bool { return false }
759
+ func getDuration() -> Double { return 0 }
760
+ func getCurrentTime() -> Double { return 0 }
761
+ func setCurrentTime(_ time: Double) -> Bool {
762
+ _ = time
763
+ return false
764
+ }
765
+ func getVolume() -> Float { return 0 }
766
+ func setVolume(_ volume: Float) -> Bool {
767
+ _ = volume
768
+ return false
769
+ }
770
+ func getMuted() -> Bool { return false }
771
+ func setMuted(_ muted: Bool) -> Bool {
772
+ _ = muted
773
+ return false
774
+ }
775
+ func getRate() -> Float { return 0 }
776
+ func setRate(_ rate: Float) -> Bool {
777
+ _ = rate
778
+ return false
779
+ }
780
+ func restartPlayback() -> Bool { return false }
781
+ func setOnPlay(_ callback: @escaping () -> Void) {
782
+ _ = callback
783
+ }
784
+ func setOnPause(_ callback: @escaping () -> Void) {
785
+ _ = callback
786
+ }
787
+ func setOnEnd(_ callback: @escaping () -> Void) {
788
+ _ = callback
789
+ }
790
+ }
791
+
792
+ #endif