@capgo/capacitor-video-player 8.1.7 → 8.1.9

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