@jwplayer/jwplayer-react-native 1.0.0

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.
Files changed (61) hide show
  1. package/.github/CODEOWNERS +2 -0
  2. package/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  4. package/.github/ISSUE_TEMPLATE/question.md +11 -0
  5. package/.github/PULL_REQUEST_TEMPLATE.md +15 -0
  6. package/CODE_OF_CONDUCT.md +128 -0
  7. package/LICENSE +21 -0
  8. package/README.md +425 -0
  9. package/RNJWPlayer.podspec +44 -0
  10. package/android/.gradle/8.1.1/checksums/checksums.lock +0 -0
  11. package/android/.gradle/8.1.1/dependencies-accessors/dependencies-accessors.lock +0 -0
  12. package/android/.gradle/8.1.1/dependencies-accessors/gc.properties +0 -0
  13. package/android/.gradle/8.1.1/fileChanges/last-build.bin +0 -0
  14. package/android/.gradle/8.1.1/fileHashes/fileHashes.lock +0 -0
  15. package/android/.gradle/8.1.1/gc.properties +0 -0
  16. package/android/.gradle/8.2/checksums/checksums.lock +0 -0
  17. package/android/.gradle/8.2/dependencies-accessors/dependencies-accessors.lock +0 -0
  18. package/android/.gradle/8.2/dependencies-accessors/gc.properties +0 -0
  19. package/android/.gradle/8.2/fileChanges/last-build.bin +0 -0
  20. package/android/.gradle/8.2/fileHashes/fileHashes.lock +0 -0
  21. package/android/.gradle/8.2/gc.properties +0 -0
  22. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  23. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  24. package/android/.gradle/config.properties +2 -0
  25. package/android/.gradle/vcs-1/gc.properties +0 -0
  26. package/android/.idea/gradle.xml +12 -0
  27. package/android/.idea/migrations.xml +10 -0
  28. package/android/.idea/misc.xml +10 -0
  29. package/android/.idea/vcs.xml +6 -0
  30. package/android/.idea/workspace.xml +54 -0
  31. package/android/build.gradle +110 -0
  32. package/android/local.properties +8 -0
  33. package/android/src/main/AndroidManifest.xml +25 -0
  34. package/android/src/main/java/com/jwplayer/rnjwplayer/ArrayUtil.java +129 -0
  35. package/android/src/main/java/com/jwplayer/rnjwplayer/CastOptionsProvider.java +55 -0
  36. package/android/src/main/java/com/jwplayer/rnjwplayer/MapUtil.java +136 -0
  37. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayer.java +76 -0
  38. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerAds.java +239 -0
  39. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerModule.java +526 -0
  40. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerPackage.java +30 -0
  41. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerView.java +1499 -0
  42. package/android/src/main/java/com/jwplayer/rnjwplayer/RNJWPlayerViewManager.java +171 -0
  43. package/android/src/main/java/com/jwplayer/rnjwplayer/Util.java +219 -0
  44. package/android/src/main/java/com/jwplayer/rnjwplayer/WidevineCallback.java +62 -0
  45. package/badges/license.svg +1 -0
  46. package/badges/version.svg +1 -0
  47. package/docs/legacy_readme.md +634 -0
  48. package/docs/props.md +43 -0
  49. package/docs/types.md +254 -0
  50. package/index.d.ts +564 -0
  51. package/index.js +699 -0
  52. package/ios/RNJWPlayer/RCTConvert+RNJWPlayer.swift +119 -0
  53. package/ios/RNJWPlayer/RNJWPlayer-Bridging-Header.h +5 -0
  54. package/ios/RNJWPlayer/RNJWPlayerAds.swift +260 -0
  55. package/ios/RNJWPlayer/RNJWPlayerModels.swift +149 -0
  56. package/ios/RNJWPlayer/RNJWPlayerView.swift +1837 -0
  57. package/ios/RNJWPlayer/RNJWPlayerViewController.swift +616 -0
  58. package/ios/RNJWPlayer/RNJWPlayerViewManager.m +132 -0
  59. package/ios/RNJWPlayer/RNJWPlayerViewManager.swift +500 -0
  60. package/ios/RNJWPlayer.xcodeproj/project.pbxproj +323 -0
  61. package/package.json +45 -0
@@ -0,0 +1,1837 @@
1
+ //
2
+ // RNJWPlayerViewController.m
3
+ // RNJWPlayer
4
+ //
5
+ // Created by Chaim Paneth on 3/30/22.
6
+ //
7
+
8
+ import UIKit
9
+ import AVFoundation
10
+ import AVKit
11
+ import MediaPlayer
12
+ import React
13
+ import GoogleCast
14
+ import JWPlayerKit
15
+
16
+ class RNJWPlayerView : UIView, JWPlayerDelegate, JWPlayerStateDelegate, JWAdDelegate, JWCastDelegate, JWAVDelegate, JWPlayerViewDelegate, JWPlayerViewControllerDelegate, JWDRMContentKeyDataSource, AVPictureInPictureControllerDelegate {
17
+
18
+ // MARK: - RNJWPlayer allocation
19
+
20
+ var playerViewController:RNJWPlayerViewController!
21
+ var playerView: JWPlayerView!
22
+ var audioSession: AVAudioSession!
23
+ var pipEnabled: Bool = true
24
+ var backgroundAudioEnabled: Bool = true
25
+ var userPaused: Bool = false
26
+ var wasInterrupted: Bool = false
27
+ var interfaceBehavior: JWInterfaceBehavior!
28
+ var fairplayCertUrl: String!
29
+ var processSpcUrl: String!
30
+ var contentUUID: String!
31
+ var audioCategory: String!
32
+ var audioMode: String!
33
+ var audioCategoryOptions: [String]!
34
+ var settingConfig: Bool = false
35
+ var pendingConfig: Bool = false
36
+ var currentConfig: [String : Any]!
37
+ var playerFailed = false
38
+ var castController: JWCastController!
39
+ var isCasting: Bool = false
40
+ var availableDevices: [AnyObject]!
41
+
42
+ @objc var onBuffer:RCTDirectEventBlock?
43
+ @objc var onUpdateBuffer:RCTDirectEventBlock?
44
+ @objc var onPlay:RCTDirectEventBlock?
45
+ @objc var onBeforePlay:RCTDirectEventBlock?
46
+ @objc var onAttemptPlay:RCTDirectEventBlock?
47
+ @objc var onPause:RCTDirectEventBlock?
48
+ @objc var onIdle:RCTDirectEventBlock?
49
+ @objc var onPlaylistItem:RCTDirectEventBlock?
50
+ @objc var onLoaded:RCTDirectEventBlock?
51
+ @objc var onVisible:RCTDirectEventBlock?
52
+ @objc var onTime:RCTDirectEventBlock?
53
+ @objc var onSeek:RCTDirectEventBlock?
54
+ @objc var onSeeked:RCTDirectEventBlock?
55
+ @objc var onRateChanged:RCTDirectEventBlock?
56
+ @objc var onPlaylist:RCTDirectEventBlock?
57
+ @objc var onPlaylistComplete:RCTDirectEventBlock?
58
+ @objc var onBeforeComplete:RCTDirectEventBlock?
59
+ @objc var onComplete:RCTDirectEventBlock?
60
+ @objc var onAudioTracks:RCTDirectEventBlock?
61
+ @objc var onPlayerReady:RCTDirectEventBlock?
62
+ @objc var onSetupPlayerError:RCTDirectEventBlock?
63
+ @objc var onPlayerError:RCTDirectEventBlock?
64
+ @objc var onPlayerWarning:RCTDirectEventBlock?
65
+ @objc var onPlayerAdWarning:RCTDirectEventBlock?
66
+ @objc var onPlayerAdError:RCTDirectEventBlock?
67
+ @objc var onAdEvent:RCTDirectEventBlock?
68
+ @objc var onAdTime:RCTDirectEventBlock?
69
+ @objc var onScreenTapped:RCTDirectEventBlock?
70
+ @objc var onControlBarVisible:RCTDirectEventBlock?
71
+ @objc var onFullScreen:RCTDirectEventBlock?
72
+ @objc var onFullScreenRequested:RCTDirectEventBlock?
73
+ @objc var onFullScreenExit:RCTDirectEventBlock?
74
+ @objc var onFullScreenExitRequested:RCTDirectEventBlock?
75
+ @objc var onPlayerSizeChange:RCTDirectEventBlock?
76
+ @objc var onCastingDevicesAvailable:RCTDirectEventBlock?
77
+ @objc var onConnectedToCastingDevice:RCTDirectEventBlock?
78
+ @objc var onDisconnectedFromCastingDevice:RCTDirectEventBlock?
79
+ @objc var onConnectionTemporarilySuspended:RCTDirectEventBlock?
80
+ @objc var onConnectionRecovered:RCTDirectEventBlock?
81
+ @objc var onConnectionFailed:RCTDirectEventBlock?
82
+ @objc var onCasting:RCTDirectEventBlock?
83
+ @objc var onCastingEnded:RCTDirectEventBlock?
84
+ @objc var onCastingFailed:RCTDirectEventBlock?
85
+
86
+ init() {
87
+ super.init(frame: CGRect(x: 20, y: 0, width: UIScreen.main.bounds.width - 40, height: 300))
88
+ }
89
+
90
+ required init?(coder: NSCoder) {
91
+ super.init(coder: coder)
92
+ NotificationCenter.default.addObserver(self, selector: #selector(rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
93
+ }
94
+
95
+ deinit {
96
+ self.startDeinitProcess()
97
+ }
98
+
99
+ override func removeFromSuperview() {
100
+ self.startDeinitProcess()
101
+ }
102
+
103
+ func startDeinitProcess() {
104
+ NotificationCenter.default.removeObserver(self, name:UIDevice.orientationDidChangeNotification, object:nil)
105
+
106
+ self.reset()
107
+ super.removeFromSuperview()
108
+ }
109
+
110
+ func reset() {
111
+ NotificationCenter.default.removeObserver(self, name:AVAudioSession.mediaServicesWereResetNotification, object:audioSession)
112
+ NotificationCenter.default.removeObserver(self, name:AVAudioSession.interruptionNotification, object:audioSession)
113
+
114
+ NotificationCenter.default.removeObserver(self, name:UIApplication.willResignActiveNotification, object:nil)
115
+ NotificationCenter.default.removeObserver(self, name:UIApplication.didEnterBackgroundNotification, object:nil)
116
+ NotificationCenter.default.removeObserver(self, name:UIApplication.willEnterForegroundNotification, object:nil)
117
+ NotificationCenter.default.removeObserver(self, name:AVAudioSession.routeChangeNotification, object:nil)
118
+
119
+ if (playerViewController != nil) || (playerView != nil) {
120
+ // playerViewController.player.currentItem!.removeObserver(self, forKeyPath:"playbackLikelyToKeepUp", context:nil)
121
+ if (playerView != nil) {
122
+ NotificationCenter.default.removeObserver(self, forKeyPath:"isPictureInPicturePossible", context:nil)
123
+ }
124
+ }
125
+
126
+ self.removePlayerView()
127
+ self.dismissPlayerViewController()
128
+
129
+ self.deinitAudioSession()
130
+
131
+ }
132
+
133
+ override func layoutSubviews() {
134
+ super.layoutSubviews()
135
+
136
+ if self.playerView != nil {
137
+ self.playerView.frame = self.frame
138
+ }
139
+
140
+ if self.playerViewController != nil {
141
+ self.playerViewController.view.frame = self.frame
142
+ }
143
+ }
144
+
145
+ @objc func rotated(notification: Notification) {
146
+ if UIDevice.current.orientation.isLandscape {
147
+ print("Landscape")
148
+ }
149
+
150
+ if UIDevice.current.orientation.isPortrait {
151
+ print("Portrait")
152
+ }
153
+
154
+ self.layoutSubviews()
155
+ }
156
+
157
+ func shouldAutorotate() -> Bool {
158
+ return false
159
+ }
160
+
161
+ // MARK: - RNJWPlayer props
162
+
163
+ func setLicense(license:String?) {
164
+ if (license != nil) {
165
+ JWPlayerKitLicense.setLicenseKey(license!)
166
+ } else {
167
+ print("JW SDK License key not set.")
168
+ }
169
+ }
170
+
171
+ func keysForDifferingValues(in dict1: [String: Any], and dict2: [String: Any]) -> [String] {
172
+ var diffKeys = [String]()
173
+
174
+ for key in dict1.keys {
175
+ if let value1 = dict1[key], let value2 = dict2[key] {
176
+ if !areEqual(value1, value2) {
177
+ diffKeys.append(key)
178
+ }
179
+ } else {
180
+ // One of the dictionaries does not contain the key
181
+ diffKeys.append(key)
182
+ }
183
+ }
184
+
185
+ return diffKeys
186
+ }
187
+
188
+ func areEqual(_ value1: Any, _ value2: Any) -> Bool {
189
+ let mirror1 = Mirror(reflecting: value1)
190
+ let mirror2 = Mirror(reflecting: value2)
191
+
192
+ if mirror1.displayStyle != mirror2.displayStyle {
193
+ // Different types
194
+ return false
195
+ }
196
+
197
+ switch (value1, value2) {
198
+ case let (dict1 as [String: Any], dict2 as [String: Any]):
199
+ // Both are dictionaries, recursively compare
200
+ return keysForDifferingValues(in: dict1, and: dict2).isEmpty
201
+ case let (array1 as [Any], array2 as [Any]):
202
+ // Both are arrays, compare elements
203
+ return array1.count == array2.count && zip(array1, array2).allSatisfy(areEqual)
204
+ default:
205
+ // Use default String description for comparison
206
+ return String(describing: value1) == String(describing: value2)
207
+ }
208
+ }
209
+
210
+
211
+ func dictionariesAreEqual(_ dict1: [String: Any]?, _ dict2: [String: Any]?) -> Bool {
212
+ return NSDictionary(dictionary: dict1 ?? [:]).isEqual(to: dict2 ?? [:])
213
+ }
214
+
215
+ @objc func setJsonConfig(_ config: JSONObject) {
216
+ do {
217
+ let configuration = try JWPlayerConfigurationBuilder().configuration(json: config).build()
218
+ playerViewController.player.configurePlayer(with: configuration)
219
+ } catch {
220
+ print(error)
221
+ }
222
+ }
223
+
224
+ @objc func setConfig(_ config: [String: Any]) {
225
+ if (playerFailed) {
226
+ playerFailed = false
227
+ setNewConfig(config: config)
228
+ return
229
+ }
230
+
231
+ // Create mutable copies of the dictionaries
232
+ var configCopy = config
233
+ var currentConfigCopy = currentConfig
234
+
235
+ // Remove the playlist key
236
+ configCopy.removeValue(forKey: "playlist")
237
+ currentConfigCopy?.removeValue(forKey: "playlist")
238
+
239
+ // Compare dictionaries without the playlist key
240
+ if (currentConfigCopy == nil) || !dictionariesAreEqual(configCopy, currentConfigCopy!) {
241
+ print("There are differences other than the 'playlist' key.")
242
+
243
+ if (currentConfigCopy != nil) {
244
+ let diffKeys = keysForDifferingValues(in: configCopy, and: currentConfigCopy!)
245
+ print("There are differences in these keys: \(diffKeys)")
246
+ } else {
247
+ print("It's a new config")
248
+ }
249
+
250
+ setNewConfig(config: config)
251
+ } else {
252
+ // Compare original dictionaries
253
+ if !dictionariesAreEqual(currentConfig, config) {
254
+ print("The only difference is the 'playlist' key.")
255
+
256
+ var playlistArray = [JWPlayerItem]()
257
+
258
+ if let playlist = config["playlist"] as? [AnyObject] {
259
+ for item in playlist {
260
+ if let playerItem = try? getPlayerItem(item: item as! [String: Any]) {
261
+ playlistArray.append(playerItem)
262
+ }
263
+ }
264
+ }
265
+
266
+ if let playerViewController = playerViewController {
267
+ playerViewController.player.loadPlaylist(items: playlistArray)
268
+ } else if let playerView = playerView {
269
+ playerView.player.loadPlaylist(items: playlistArray)
270
+ } else {
271
+ setNewConfig(config: config)
272
+ }
273
+ } else {
274
+ print("There are no differences.")
275
+ }
276
+ }
277
+ }
278
+
279
+ func setNewConfig(config: [String : Any]) {
280
+ let forceLegacyConfig = config["forceLegacyConfig"] as? Bool?
281
+ let data:Data! = try? JSONSerialization.data(withJSONObject: config, options:.prettyPrinted)
282
+ let jwConfig = try? JWJSONParser.configFromJSON(data)
283
+
284
+ currentConfig = config
285
+
286
+ if !settingConfig {
287
+ pendingConfig = false
288
+ settingConfig = true
289
+
290
+ let license = config["license"] as? String
291
+ self.setLicense(license: license)
292
+
293
+ if let bae = config["backgroundAudioEnabled"] as? Bool, let pe = config["pipEnabled"] as? Bool {
294
+ backgroundAudioEnabled = bae
295
+ pipEnabled = pe
296
+ }
297
+
298
+ if backgroundAudioEnabled || pipEnabled {
299
+ let category = config["category"] as? String
300
+ let categoryOptions = config["categoryOptions"] as? [String]
301
+ let mode = config["mode"] as? String
302
+
303
+ self.initAudioSession(category: category, categoryOptions: categoryOptions, mode: mode)
304
+ } else {
305
+ self.deinitAudioSession()
306
+ }
307
+
308
+ if forceLegacyConfig == true {
309
+ // Pull from top level of config
310
+ processSpcUrl = config["processSpcUrl"] as? String
311
+ fairplayCertUrl = config["certificateUrl"] as? String
312
+ contentUUID = config["contentUUID"] as? String
313
+
314
+ // Dangerous: check playlist for processSpcUrl / fairplayCertUrl in playlist
315
+ // Only checks first playlist item as multi-item DRM playlists are ill advised
316
+ if let playlist = config["playlist"] as? [AnyObject] {
317
+ let item = playlist.first
318
+ if let itemMap = item as? [String: Any] {
319
+ if itemMap["processSpcUrl"] != nil {
320
+ processSpcUrl = itemMap["processSpcUrl"] as? String
321
+ }
322
+ if itemMap["certificateUrl"] != nil {
323
+ fairplayCertUrl = itemMap["certificateUrl"] as? String
324
+ }
325
+ }
326
+ }
327
+ } else {
328
+ // TODO -- Ensure JWJSONParser pulls out cert/spc for sources (Expected in JW iOS SDK v4.19.0)
329
+ }
330
+
331
+ do {
332
+ let viewOnly = config["viewOnly"] as? Bool
333
+ if viewOnly == true {
334
+ if forceLegacyConfig == true {
335
+ self.setupPlayerView(config: config, playerConfig: try self.getPlayerConfiguration(config: config))
336
+ } else {
337
+ self.setupPlayerView(config: config, playerConfig: jwConfig!)
338
+ }
339
+ } else {
340
+ if forceLegacyConfig == true {
341
+ self.setupPlayerViewController(config: config, playerConfig: try self.getPlayerConfiguration(config: config))
342
+ } else {
343
+ self.setupPlayerViewController(config: config, playerConfig: jwConfig!)
344
+ }
345
+ }
346
+ } catch {
347
+ print(error)
348
+ }
349
+ } else {
350
+ pendingConfig = true
351
+ }
352
+ }
353
+
354
+ @objc func setControls(_ controls:Bool) {
355
+ if let playerViewControllerView = playerViewController?.view {
356
+ self.toggleUIGroup(view: playerViewControllerView, name: "JWPlayerKit.InterfaceView", ofSubview: nil, show: controls)
357
+ }
358
+ }
359
+
360
+ // MARK: - RNJWPlayer styling
361
+
362
+ func colorWithHexString(hex:String!) -> UIColor! {
363
+ var cString = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
364
+
365
+ // String should be 6 or 8 characters
366
+ if cString.count < 6 {
367
+ return UIColor.gray
368
+ }
369
+
370
+ // Strip 0X if it appears
371
+ if cString.hasPrefix("0X") {
372
+ cString = String(cString.dropFirst(2))
373
+ }
374
+
375
+ if cString.count != 6 {
376
+ return UIColor.gray
377
+ }
378
+
379
+ // Separate into r, g, b substrings
380
+ let startIndex = cString.startIndex
381
+ let endIndex = cString.index(startIndex, offsetBy: 2)
382
+ let rString = String(cString[startIndex..<endIndex])
383
+
384
+ let startIndexG = cString.index(startIndex, offsetBy: 2)
385
+ let endIndexG = cString.index(startIndexG, offsetBy: 2)
386
+ let gString = String(cString[startIndexG..<endIndexG])
387
+
388
+ let startIndexB = cString.index(startIndexG, offsetBy: 2)
389
+ let endIndexB = cString.index(startIndexB, offsetBy: 2)
390
+ let bString = String(cString[startIndexB..<endIndexB])
391
+
392
+ // Scan values
393
+ var r: UInt64 = 0, g: UInt64 = 0, b: UInt64 = 0
394
+ Scanner(string: rString).scanHexInt64(&r)
395
+ Scanner(string: gString).scanHexInt64(&g)
396
+ Scanner(string: bString).scanHexInt64(&b)
397
+
398
+ return UIColor(
399
+ red: CGFloat(r) / 255.0,
400
+ green: CGFloat(g) / 255.0,
401
+ blue: CGFloat(b) / 255.0,
402
+ alpha: 1.0
403
+ )
404
+ }
405
+
406
+ func setStyling(styling: [String: Any]) {
407
+ let skinStylingBuilder = JWPlayerSkinBuilder()
408
+
409
+ let colors: AnyObject! = styling["colors"] as AnyObject
410
+ if let colorsDict = colors as? [String: AnyObject] {
411
+ let timeSlider: AnyObject! = colorsDict["timeslider"] as AnyObject
412
+ if let timeSliderDict = timeSlider as? [String: AnyObject] {
413
+ let timeSliderStyleBuilder = JWTimeSliderStyleBuilder()
414
+
415
+ if let progress = timeSliderDict["progress"] as? String {
416
+ timeSliderStyleBuilder.minimumTrackColor(self.colorWithHexString(hex: progress))
417
+ }
418
+
419
+ if let rail = timeSliderDict["rail"] as? String {
420
+ timeSliderStyleBuilder.maximumTrackColor(self.colorWithHexString(hex: rail))
421
+ }
422
+
423
+ if let thumb = timeSliderDict["thumb"] as? String {
424
+ timeSliderStyleBuilder.thumbColor(self.colorWithHexString(hex: thumb))
425
+ }
426
+
427
+ // Build the time slider style and set it in the skin styling builder
428
+ do {
429
+ let timeSliderStyle: JWTimeSliderStyle = try timeSliderStyleBuilder.build()
430
+ skinStylingBuilder.timeSliderStyle(timeSliderStyle)
431
+ } catch let error {
432
+ // Handle error when building time slider style
433
+ print("Error building time slider style: \(error.localizedDescription)")
434
+ }
435
+
436
+ let buttons: AnyObject! = colorsDict["buttons"] as AnyObject
437
+ if let buttonsColor = buttons as? String {
438
+ skinStylingBuilder.buttonsColor(self.colorWithHexString(hex: buttonsColor))
439
+ }
440
+
441
+ let backgroundColor: AnyObject! = colorsDict["backgroundColor"] as AnyObject
442
+ if let bgColor = backgroundColor as? String {
443
+ skinStylingBuilder.backgroundColor(self.colorWithHexString(hex: bgColor))
444
+ }
445
+
446
+ let fontColor: AnyObject! = colorsDict["fontColor"] as AnyObject
447
+ if let fontColorValue = fontColor as? String {
448
+ skinStylingBuilder.fontColor(self.colorWithHexString(hex: fontColorValue))
449
+ }
450
+ }
451
+ }
452
+
453
+ let font: AnyObject! = styling["font"] as AnyObject
454
+ if let fontDict = font as? [String: AnyObject] {
455
+ let name: AnyObject! = fontDict["name"] as AnyObject
456
+ let size: AnyObject! = fontDict["size"] as AnyObject
457
+
458
+ if let fontName = name as? String, let fontSize = size as? CGFloat {
459
+ skinStylingBuilder.font(UIFont(name: fontName, size: fontSize)!)
460
+ }
461
+ }
462
+
463
+ let showTitle: AnyObject! = styling["displayTitle"] as AnyObject
464
+ if let titleVisible = showTitle as? Bool {
465
+ skinStylingBuilder.titleIsVisible(titleVisible)
466
+ }
467
+
468
+ let showDesc: AnyObject! = styling["displayDescription"] as AnyObject
469
+ if let descVisible = showDesc as? Bool {
470
+ skinStylingBuilder.descriptionIsVisible(descVisible)
471
+ }
472
+
473
+ let capStyle: AnyObject! = styling["captionsStyle"] as AnyObject
474
+ if let capStyleDict = capStyle as? [String: AnyObject] {
475
+ let capStyleBuilder: JWCaptionStyleBuilder! = JWCaptionStyleBuilder()
476
+
477
+ let font: AnyObject! = capStyleDict["font"] as AnyObject
478
+ if let fontDict = font as? [String: AnyObject] {
479
+ let name: AnyObject! = fontDict["name"] as AnyObject
480
+ let size: AnyObject! = fontDict["size"] as AnyObject
481
+ if let fontName = name as? String, let fontSize = size as? CGFloat {
482
+ capStyleBuilder.font(UIFont(name: fontName, size: fontSize)!)
483
+ }
484
+ }
485
+
486
+ let fontColor: AnyObject! = capStyleDict["fontColor"] as AnyObject
487
+ if let fontColorString = fontColor as? String {
488
+ capStyleBuilder.fontColor(self.colorWithHexString(hex: fontColorString))
489
+ }
490
+
491
+ let backgroundColor: AnyObject! = capStyleDict["backgroundColor"] as AnyObject
492
+ if let bgColorString = backgroundColor as? String {
493
+ capStyleBuilder.backgroundColor(self.colorWithHexString(hex: bgColorString))
494
+ }
495
+
496
+ let highlightColor: AnyObject! = capStyleDict["highlightColor"] as AnyObject
497
+ if let highlightColorString = highlightColor as? String {
498
+ capStyleBuilder.highlightColor(self.colorWithHexString(hex: highlightColorString))
499
+ }
500
+
501
+ let edgeStyle: AnyObject! = capStyleDict["edgeStyle"] as AnyObject
502
+ if let edgeStyleString = edgeStyle as? String {
503
+ capStyleBuilder.edgeStyle(RCTConvert.JWCaptionEdgeStyle(edgeStyleString))
504
+ }
505
+
506
+ do {
507
+ let captionStyle = try capStyleBuilder.build()
508
+ skinStylingBuilder.captionStyle(captionStyle)
509
+ } catch let error {
510
+ // Handle error when building time slider style
511
+ print("Error building caption style: \(error.localizedDescription)")
512
+ }
513
+ }
514
+
515
+ let menuStyle: AnyObject! = styling["menuStyle"] as AnyObject
516
+ if let menuStyleDict = menuStyle as? [String: AnyObject] {
517
+ let menuStyleBuilder: JWMenuStyleBuilder! = JWMenuStyleBuilder()
518
+
519
+ if let font = menuStyleDict["font"] as? [String: AnyObject],
520
+ let name = font["name"] as? String,
521
+ let size = font["size"] as? CGFloat {
522
+ menuStyleBuilder.font(UIFont(name: name, size: size)!)
523
+ }
524
+
525
+ if let fontColorString = menuStyleDict["fontColor"] as? String {
526
+ menuStyleBuilder.fontColor(self.colorWithHexString(hex: fontColorString))
527
+ }
528
+
529
+ if let backgroundColorString = menuStyleDict["backgroundColor"] as? String {
530
+ menuStyleBuilder.backgroundColor(self.colorWithHexString(hex: backgroundColorString))
531
+ }
532
+
533
+ do {
534
+ let jwMenuStyle = try menuStyleBuilder.build()
535
+ skinStylingBuilder.menuStyle(jwMenuStyle)
536
+ } catch let error {
537
+ // Handle error when building time slider style
538
+ print("Error building menu style: \(error.localizedDescription)")
539
+ }
540
+ }
541
+
542
+ // TODO this was broken when using `.playlist(string)`
543
+ do {
544
+ let skinStyling = try skinStylingBuilder.build()
545
+ DispatchQueue.main.async { [self] in
546
+ playerViewController.styling = skinStyling
547
+ }
548
+ } catch {
549
+ print(error)
550
+ }
551
+ }
552
+
553
+ // MARK: - RNJWPlayer config helpers
554
+
555
+ func getPlayerItem(item: [String: Any]) throws -> JWPlayerItem {
556
+ let itemBuilder = JWPlayerItemBuilder()
557
+
558
+ if let newFile = item["file"] as? String,
559
+ let url = URL(string: newFile) {
560
+
561
+ if url.scheme != nil && url.host != nil {
562
+ itemBuilder.file(url)
563
+ } else {
564
+ if let encodedString = newFile.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed),
565
+ let encodedUrl = URL(string: encodedString) {
566
+ itemBuilder.file(encodedUrl)
567
+ }
568
+ }
569
+ }
570
+
571
+ // Process sources
572
+ if let itemSources = item["sources"] as? [AnyObject], !itemSources.isEmpty {
573
+ var sourcesArray = [JWVideoSource]()
574
+
575
+ for source in itemSources.compactMap({ $0 as? [String: Any] }) {
576
+ if let file = source["file"] as? String,
577
+ let fileURL = URL(string: file),
578
+ let label = source["label"] as? String,
579
+ let isDefault = source["default"] as? Bool {
580
+
581
+ let sourceBuilder = JWVideoSourceBuilder()
582
+ sourceBuilder.file(fileURL)
583
+ sourceBuilder.label(label)
584
+ sourceBuilder.defaultVideo(isDefault)
585
+
586
+ if let sourceItem = try? sourceBuilder.build() {
587
+ sourcesArray.append(sourceItem)
588
+ }
589
+ }
590
+ }
591
+
592
+ itemBuilder.videoSources(sourcesArray)
593
+ }
594
+
595
+ // Process other properties
596
+ if let mediaId = item["mediaId"] as? String {
597
+ itemBuilder.mediaId(mediaId)
598
+ }
599
+
600
+ if let title = item["title"] as? String {
601
+ itemBuilder.title(title)
602
+ }
603
+
604
+ if let description = item["description"] as? String {
605
+ itemBuilder.description(description)
606
+ }
607
+
608
+ if let image = item["image"] as? String, let imageURL = URL(string: image) {
609
+ itemBuilder.posterImage(imageURL)
610
+ }
611
+
612
+ if let startTime = item["startTime"] as? Double {
613
+ itemBuilder.startTime(startTime)
614
+ }
615
+
616
+ if let recommendations = item["recommendations"] as? String, let recURL = URL(string: recommendations) {
617
+ itemBuilder.recommendations(recURL)
618
+ }
619
+
620
+ // Process tracks
621
+ if let tracksItem = item["tracks"] as? [AnyObject], !tracksItem.isEmpty {
622
+ var tracksArray = [JWMediaTrack]()
623
+
624
+ for trackItem in tracksItem.compactMap({ $0 as? [String: Any] }) {
625
+ if let file = trackItem["file"] as? String,
626
+ let fileURL = URL(string: file),
627
+ let label = trackItem["label"] as? String,
628
+ let isDefault = trackItem["default"] as? Bool {
629
+
630
+ let trackBuilder = JWCaptionTrackBuilder()
631
+ trackBuilder.file(fileURL)
632
+ trackBuilder.label(label)
633
+ trackBuilder.defaultTrack(isDefault)
634
+
635
+ if let track = try? trackBuilder.build() {
636
+ tracksArray.append(track)
637
+ }
638
+ }
639
+ }
640
+
641
+ itemBuilder.mediaTracks(tracksArray)
642
+ }
643
+
644
+ // Process adSchedule
645
+ if let adsItem = item["adSchedule"] as? [AnyObject], !adsItem.isEmpty {
646
+ var adsArray = [JWAdBreak]()
647
+
648
+ for adItem in adsItem.compactMap({ $0 as? [String: Any] }) {
649
+ if let offsetString = adItem["offset"] as? String,
650
+ let tag = adItem["tag"] as? String,
651
+ let encodedString = tag.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed),
652
+ let tagURL = URL(string: encodedString),
653
+ let offset = JWAdOffset.from(string: offsetString) {
654
+ let adBreakBuilder = JWAdBreakBuilder()
655
+ adBreakBuilder.offset(offset)
656
+ adBreakBuilder.tags([tagURL])
657
+
658
+ if let adBreak = try? adBreakBuilder.build() {
659
+ adsArray.append(adBreak)
660
+ }
661
+ }
662
+ }
663
+
664
+ if !adsArray.isEmpty {
665
+ itemBuilder.adSchedule(breaks: adsArray)
666
+ }
667
+ }
668
+
669
+ // Process adVmap
670
+ if let adVmap = item["adVmap"] as? String, let adVmapURL = URL(string: adVmap) {
671
+ itemBuilder.adSchedule(vmapURL: adVmapURL)
672
+ }
673
+
674
+ let playerItem = try itemBuilder.build()
675
+
676
+ return playerItem
677
+
678
+ }
679
+
680
+ func getPlayerConfiguration(config: [String: Any]) throws -> JWPlayerConfiguration {
681
+ let configBuilder:JWPlayerConfigurationBuilder! = JWPlayerConfigurationBuilder()
682
+
683
+ var playlistArray = [JWPlayerItem]()
684
+
685
+ if let playlist = config["playlist"] as? [[String: Any]] {
686
+ for item in playlist {
687
+ if let playerItem = try? self.getPlayerItem(item: item) {
688
+ playlistArray.append(playerItem)
689
+ }
690
+ }
691
+ configBuilder.playlist(items: playlistArray)
692
+ }
693
+
694
+ if let autostart = config["autostart"] as? Bool {
695
+ configBuilder.autostart(autostart)
696
+ }
697
+
698
+ if let repeatContent = config["repeat"] as? Bool, repeatContent {
699
+ configBuilder.repeatContent(repeatContent)
700
+ }
701
+
702
+ if let preload = config["preload"] as? String {
703
+ configBuilder.preload(RCTConvert.JWPreload(preload))
704
+ }
705
+
706
+ if let related = config["related"] as? [String: Any] {
707
+ let relatedBuilder = JWRelatedContentConfigurationBuilder()
708
+
709
+ relatedBuilder.onClick(RCTConvert.JWRelatedOnClick(related["onClick"] as! String))
710
+ relatedBuilder.onComplete(RCTConvert.JWRelatedOnComplete(related["onComplete"] as! String))
711
+ relatedBuilder.heading(related["heading"] as! String)
712
+ relatedBuilder.url(URL(string: related["url"] as? String ?? "")!)
713
+ relatedBuilder.autoplayMessage(related["autoplayMessage"] as! String)
714
+ relatedBuilder.autoplayTimer(related["autoplayTimer"] as? Int ?? 0)
715
+
716
+ let relatedContent = relatedBuilder.build()
717
+ configBuilder.related(relatedContent)
718
+ }
719
+
720
+ if let ads = config["advertising"] as? [String: Any] {
721
+ var advertisingConfig: JWAdvertisingConfig?
722
+
723
+ var jwAdClient = JWAdClient.unknown
724
+ if let adClientString = ads["adClient"] as? String {
725
+ jwAdClient = RCTConvert.JWAdClient(adClientString)
726
+ }
727
+
728
+ switch jwAdClient {
729
+ case .JWPlayer:
730
+ advertisingConfig = RNJWPlayerAds.configureVAST(with: ads)
731
+ case .GoogleIMA:
732
+ advertisingConfig = RNJWPlayerAds.configureIMA(with: ads)
733
+ case .GoogleIMADAI:
734
+ advertisingConfig = RNJWPlayerAds.configureIMADAI(with: ads)
735
+ default:
736
+ advertisingConfig = RNJWPlayerAds.configureVAST(with: ads)
737
+ break
738
+ }
739
+
740
+ if (advertisingConfig != nil) {
741
+ configBuilder.advertising(advertisingConfig!)
742
+ }
743
+ }
744
+
745
+ let playerConfig = try configBuilder.build()
746
+
747
+ return playerConfig
748
+ }
749
+
750
+ // MARK: - JWPlayer View Controller helpers
751
+
752
+ func setupPlayerViewController(config: [String: Any], playerConfig: JWPlayerConfiguration) {
753
+ if playerViewController == nil {
754
+ playerViewController = RNJWPlayerViewController()
755
+ playerViewController.parentView = self
756
+
757
+ DispatchQueue.main.async { [self] in
758
+ if self.reactViewController() != nil {
759
+ self.reactViewController()!.addChild(self.playerViewController)
760
+ self.playerViewController.didMove(toParent: self.reactViewController())
761
+ } else {
762
+ self.reactAddController(toClosestParent: self.playerViewController)
763
+ }
764
+ }
765
+
766
+ playerViewController.view.frame = self.frame
767
+ self.addSubview(playerViewController.view)
768
+ playerViewController.setDelegates()
769
+ }
770
+
771
+ if let ib = config["interfaceBehavior"] as? String {
772
+ interfaceBehavior = RCTConvert.JWInterfaceBehavior(ib)
773
+ }
774
+
775
+ if let interfaceFadeDelay = config["interfaceFadeDelay"] as? NSNumber {
776
+ playerViewController.interfaceFadeDelay = interfaceFadeDelay.doubleValue
777
+ }
778
+
779
+ if let forceFullScreenOnLandscape = config["fullScreenOnLandscape"] as? Bool {
780
+ playerViewController.forceFullScreenOnLandscape = forceFullScreenOnLandscape
781
+ }
782
+
783
+ if let forceLandscapeOnFullScreen = config["landscapeOnFullScreen"] as? Bool {
784
+ playerViewController.forceLandscapeOnFullScreen = forceLandscapeOnFullScreen
785
+ }
786
+
787
+ if let enableLockScreenControls = config["enableLockScreenControls"] as? Bool {
788
+ playerViewController.enableLockScreenControls = enableLockScreenControls && backgroundAudioEnabled
789
+ }
790
+
791
+ if let allowsPictureInPicturePlayback = config["allowsPictureInPicturePlayback"] as? Bool {
792
+ playerViewController.allowsPictureInPicturePlayback = allowsPictureInPicturePlayback
793
+ }
794
+
795
+ if let styling = config["styling"] as? [String: Any] {
796
+ self.setStyling(styling: styling)
797
+ }
798
+
799
+ if let nextUpStyle = config["nextUpStyle"] as? [String: Any] {
800
+ let nextUpBuilder = JWNextUpStyleBuilder()
801
+
802
+ if let offsetSeconds = nextUpStyle["offsetSeconds"] as? Double {
803
+ nextUpBuilder.timeOffset(seconds: offsetSeconds)
804
+ }
805
+
806
+ if let offsetPercentage = nextUpStyle["offsetPercentage"] as? Double {
807
+ nextUpBuilder.timeOffset(seconds: offsetPercentage)
808
+ }
809
+
810
+ do {
811
+ playerViewController.nextUpStyle = try nextUpBuilder.build()
812
+ } catch {
813
+ print(error)
814
+ }
815
+ }
816
+
817
+ // playerViewController.adInterfaceStyle
818
+ // playerViewController.logo
819
+ // playerView.videoGravity = 0;
820
+ // playerView.captionStyle
821
+
822
+ if let offlineMsg = config["offlineMessage"] as? String {
823
+ playerViewController.offlineMessage = offlineMsg
824
+ }
825
+
826
+ if let offlineImg = config["offlineImage"] as? String {
827
+ if let imageUrl = URL(string: offlineImg), imageUrl.isFileURL {
828
+ if let imageData = try? Data(contentsOf: imageUrl),
829
+ let image = UIImage(data: imageData) {
830
+ playerViewController.offlinePosterImage = image
831
+ }
832
+ }
833
+ }
834
+
835
+ self.presentPlayerViewController(configuration: playerConfig)
836
+ }
837
+
838
+ func dismissPlayerViewController() {
839
+ if (playerViewController != nil) {
840
+ playerViewController.player.pause() // hack for stop not always stopping on unmount
841
+ playerViewController.player.stop()
842
+ playerViewController.enableLockScreenControls = false
843
+
844
+ // hack for stop not always stopping on unmount
845
+ let configBuilder:JWPlayerConfigurationBuilder! = JWPlayerConfigurationBuilder()
846
+ configBuilder.playlist(items: [])
847
+
848
+ do {
849
+ let configuration: JWPlayerConfiguration = try configBuilder.build()
850
+ playerViewController.player.configurePlayer(with: configuration)
851
+ } catch {
852
+ print(error)
853
+ }
854
+
855
+
856
+ playerViewController.parentView = nil
857
+ playerViewController.setVisibility(.hidden, for:[.pictureInPictureButton])
858
+ playerViewController.view.removeFromSuperview()
859
+ playerViewController.removeFromParent()
860
+ playerViewController.willMove(toParent: nil)
861
+ playerViewController.removeDelegates()
862
+ playerViewController = nil
863
+ }
864
+ }
865
+
866
+ func presentPlayerViewController(configuration: JWPlayerConfiguration!) {
867
+ if configuration != nil {
868
+ playerViewController.player.configurePlayer(with: configuration)
869
+ if (interfaceBehavior != nil) {
870
+ playerViewController.interfaceBehavior = interfaceBehavior
871
+ }
872
+ }
873
+ }
874
+
875
+ // MARK: - JWPlayer View helpers
876
+
877
+ func setupPlayerView(config: [String: Any], playerConfig: JWPlayerConfiguration) {
878
+ playerView = JWPlayerView(frame:self.superview!.frame)
879
+
880
+ playerView.delegate = self
881
+ playerView.player.delegate = self
882
+ playerView.player.playbackStateDelegate = self
883
+ playerView.player.adDelegate = self
884
+ playerView.player.avDelegate = self
885
+ playerView.player.contentKeyDataSource = self
886
+
887
+ playerView.player.configurePlayer(with: playerConfig)
888
+
889
+ if pipEnabled {
890
+ let pipController:AVPictureInPictureController! = playerView.pictureInPictureController
891
+ pipController.delegate = self
892
+
893
+ pipController.addObserver(self, forKeyPath:"isPictureInPicturePossible", options:[.new, .initial], context:nil)
894
+ }
895
+
896
+ self.addSubview(self.playerView)
897
+
898
+ if let autostart = config["autostart"] as? Bool, autostart {
899
+ playerView.player.play()
900
+ }
901
+
902
+ // Time observers
903
+ weak var weakSelf:RNJWPlayerView! = self
904
+ playerView.player.adTimeObserver = { (time:JWTimeData!) in
905
+ weakSelf.onAdTime?(["position": time.position, "duration": time.duration])
906
+ }
907
+
908
+ playerView.player.mediaTimeObserver = { (time:JWTimeData!) in
909
+ weakSelf.onTime?(["position": time.position, "duration": time.duration])
910
+ }
911
+ }
912
+
913
+ func removePlayerView() {
914
+ if (playerView != nil) {
915
+ playerView.player.stop()
916
+ playerView.removeFromSuperview()
917
+ playerView = nil
918
+ }
919
+ }
920
+
921
+ func toggleUIGroup(view: UIView, name: String, ofSubview: String?, show: Bool) {
922
+ let subviews = view.subviews
923
+
924
+ for subview in subviews {
925
+ if NSStringFromClass(subview.classForCoder) == name && (ofSubview == nil || NSStringFromClass(subview.superview!.classForCoder) == name) {
926
+ subview.isHidden = !show
927
+ } else {
928
+ toggleUIGroup(view: subview, name: name, ofSubview: ofSubview, show: show)
929
+ }
930
+ }
931
+ }
932
+
933
+ func setVisibility(isVisible:Bool, forControls controls:[String]) {
934
+ var _controls:[JWControlType]! = [JWControlType]()
935
+
936
+ for control:String? in controls {
937
+ if (control != nil && !control!.isEmpty) {
938
+ let type:JWControlType = RCTConvert.JWControlType(control!)
939
+ _controls.append(type)
940
+ }
941
+ }
942
+
943
+ if (_controls.count > 0) {
944
+ playerViewController.setVisibility(isVisible ? .visible : .hidden, for: _controls!)
945
+ }
946
+ }
947
+
948
+ // MARK: - JWPlayer Delegate
949
+
950
+ func jwplayerIsReady(_ player:JWPlayer) {
951
+ settingConfig = false
952
+ self.onPlayerReady?([:])
953
+
954
+ if pendingConfig && currentConfig != nil {
955
+ self.setConfig(currentConfig)
956
+ }
957
+ }
958
+
959
+ func jwplayer(_ player:JWPlayer, failedWithError code:UInt, message:String) {
960
+ self.onPlayerError?(["error": message])
961
+ playerFailed = true
962
+ }
963
+
964
+ func jwplayer(_ player:JWPlayer, failedWithSetupError code:UInt, message:String) {
965
+ self.onSetupPlayerError?(["error": message])
966
+ playerFailed = true
967
+ }
968
+
969
+ func jwplayer(_ player:JWPlayer, encounteredWarning code:UInt, message:String) {
970
+ self.onPlayerWarning?(["warning": message])
971
+ }
972
+
973
+ func jwplayer(_ player:JWPlayer, encounteredAdError code:UInt, message:String) {
974
+ self.onPlayerAdError?(["error": message])
975
+ }
976
+
977
+
978
+ func jwplayer(_ player:JWPlayer, encounteredAdWarning code:UInt, message:String) {
979
+ self.onPlayerAdWarning?(["warning": message])
980
+ }
981
+
982
+
983
+ // MARK: - JWPlayer View Delegate
984
+
985
+ func playerView(_ view:JWPlayerView, sizeChangedFrom oldSize:CGSize, to newSize:CGSize) {
986
+ let oldSizeDict: [String: Any] = [
987
+ "width": oldSize.width,
988
+ "height": oldSize.height
989
+ ]
990
+
991
+ let newSizeDict: [String: Any] = [
992
+ "width": newSize.width,
993
+ "height": newSize.height
994
+ ]
995
+
996
+ let sizesDict: [String: Any] = [
997
+ "oldSize": oldSizeDict,
998
+ "newSize": newSizeDict
999
+ ]
1000
+
1001
+ do {
1002
+ let data = try JSONSerialization.data(withJSONObject: sizesDict, options: .prettyPrinted)
1003
+ self.onPlayerSizeChange?(["sizes": data])
1004
+ } catch {
1005
+ print("Error converting dictionary to JSON data: \(error)")
1006
+ }
1007
+ }
1008
+
1009
+ // MARK: - JWPlayer View Controller Delegate
1010
+
1011
+ func playerViewController(_ controller:JWPlayerViewController, sizeChangedFrom oldSize:CGSize, to newSize:CGSize) {
1012
+ let oldSizeDict: [String: Any] = [
1013
+ "width": oldSize.width,
1014
+ "height": oldSize.height
1015
+ ]
1016
+
1017
+ let newSizeDict: [String: Any] = [
1018
+ "width": newSize.width,
1019
+ "height": newSize.height
1020
+ ]
1021
+
1022
+ let sizesDict: [String: Any] = [
1023
+ "oldSize": oldSizeDict,
1024
+ "newSize": newSizeDict
1025
+ ]
1026
+
1027
+ do {
1028
+ let data = try JSONSerialization.data(withJSONObject: sizesDict, options: .prettyPrinted)
1029
+ self.onPlayerSizeChange?(["sizes": data])
1030
+ } catch {
1031
+ print("Error converting dictionary to JSON data: \(error)")
1032
+ }
1033
+ }
1034
+
1035
+ func playerViewController(_ controller:JWPlayerViewController, screenTappedAt position:CGPoint) {
1036
+ self.onScreenTapped?(["x": position.x, "y": position.y])
1037
+ }
1038
+
1039
+ func playerViewController(_ controller:JWPlayerViewController, controlBarVisibilityChanged isVisible:Bool, frame:CGRect) {
1040
+ self.onControlBarVisible?(["visible": isVisible])
1041
+ }
1042
+
1043
+ func playerViewControllerWillGoFullScreen(_ controller:JWPlayerViewController) -> JWFullScreenViewController? {
1044
+ self.onFullScreenRequested?([:])
1045
+ return nil
1046
+ }
1047
+
1048
+ func playerViewControllerDidGoFullScreen(_ controller:JWPlayerViewController) {
1049
+ self.onFullScreen?([:])
1050
+ }
1051
+
1052
+ func playerViewControllerWillDismissFullScreen(_ controller:JWPlayerViewController) {
1053
+ self.onFullScreenExitRequested?([:])
1054
+ }
1055
+
1056
+ func playerViewControllerDidDismissFullScreen(_ controller:JWPlayerViewController) {
1057
+ self.onFullScreenExit?([:])
1058
+ }
1059
+
1060
+ func playerViewController(_ controller:JWPlayerViewController, relatedMenuClosedWithMethod method: JWRelatedInteraction) {
1061
+
1062
+ }
1063
+
1064
+ func playerViewController(_ controller: JWPlayerViewController, relatedMenuOpenedWithItems items: [JWPlayerItem], withMethod method: JWRelatedInteraction) {
1065
+
1066
+ }
1067
+
1068
+ func playerViewController(_ controller: JWPlayerViewController, relatedItemBeganPlaying item: JWPlayerItem, atIndex index: Int, withMethod method: JWRelatedMethod) {
1069
+
1070
+ }
1071
+
1072
+ // MARK: Time events
1073
+
1074
+ func onAdTimeEvent(time:JWTimeData) {
1075
+ self.onAdTime?(["position": time.position, "duration": time.duration])
1076
+ }
1077
+
1078
+ func onMediaTimeEvent(time:JWTimeData) {
1079
+ self.onTime?(["position": time.position, "duration": time.duration])
1080
+ }
1081
+
1082
+ // MARK: - DRM Delegate
1083
+
1084
+ func contentIdentifierForURL(_ url: URL, completionHandler handler: @escaping (Data?) -> Void) {
1085
+ let data:Data! = url.host?.data(using: String.Encoding.utf8)
1086
+ handler(data)
1087
+ }
1088
+
1089
+ func appIdentifierForURL(_ url: URL, completionHandler handler: @escaping (Data?) -> Void) {
1090
+ guard let fairplayCertUrlString = fairplayCertUrl, let finalUrl = URL(string: fairplayCertUrlString) else {
1091
+ return
1092
+ }
1093
+
1094
+ let request = URLRequest(url: finalUrl)
1095
+ let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
1096
+ if let error = error {
1097
+ print("DRM cert request error - \(error.localizedDescription)")
1098
+ }
1099
+ handler(data)
1100
+ }
1101
+ task.resume()
1102
+ }
1103
+
1104
+ func contentKeyWithSPCData(_ spcData: Data, completionHandler handler: @escaping (Data?, Date?, String?) -> Void) {
1105
+ if processSpcUrl == nil {
1106
+ return
1107
+ }
1108
+
1109
+ guard let processSpcUrl = URL(string: processSpcUrl) else {
1110
+ print("Invalid processSpcUrl")
1111
+ handler(nil, nil, nil)
1112
+ return
1113
+ }
1114
+
1115
+ var ckcRequest = URLRequest(url: processSpcUrl)
1116
+ ckcRequest.httpMethod = "POST"
1117
+ ckcRequest.httpBody = spcData
1118
+ ckcRequest.addValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
1119
+
1120
+ URLSession.shared.dataTask(with: ckcRequest) { (data, response, error) in
1121
+ if let httpResponse = response as? HTTPURLResponse, (error != nil || httpResponse.statusCode != 200) {
1122
+ print("DRM ckc request error - %@", error?.localizedDescription ?? "Unknown error")
1123
+ handler(nil, nil, nil)
1124
+ return
1125
+ }
1126
+
1127
+ handler(data, nil, "application/octet-stream")
1128
+ }.resume()
1129
+ }
1130
+
1131
+ // MARK: - AV Picture In Picture Delegate
1132
+
1133
+ override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
1134
+ // if let playerView = playerView {
1135
+ // if keyPath == "playbackLikelyToKeepUp" {
1136
+ // playerView.player.play()
1137
+ // }
1138
+ // } else if let playerViewController = playerViewController {
1139
+ // if keyPath == "playbackLikelyToKeepUp" {
1140
+ // playerViewController.player.play()
1141
+ // }
1142
+ // }
1143
+ //
1144
+ if let keyPath = keyPath, keyPath == "isPictureInPicturePossible", let playerView = playerView, object as? AVPictureInPictureController == playerView.pictureInPictureController {
1145
+ // Your code here for handling isPictureInPicturePossible
1146
+ }
1147
+ }
1148
+
1149
+ func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
1150
+
1151
+ }
1152
+
1153
+ func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
1154
+
1155
+ }
1156
+
1157
+ func pictureInPictureControllerWillStopPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
1158
+
1159
+ }
1160
+
1161
+ func pictureInPictureController(pictureInPictureController:AVPictureInPictureController!, failedToStartPictureInPictureWithError error:NSError!) {
1162
+
1163
+ }
1164
+
1165
+ func pictureInPictureControllerWillStartPictureInPicture(_ pictureInPictureController:AVPictureInPictureController) {
1166
+
1167
+ }
1168
+
1169
+ func pictureInPictureController(_ pictureInPictureController:AVPictureInPictureController, restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler:@escaping (Bool) -> Void) {
1170
+
1171
+ }
1172
+
1173
+ func pictureInPictureController(_ pictureInPictureController: AVPictureInPictureController, failedToStartPictureInPictureWithError error: Error) {
1174
+
1175
+ }
1176
+
1177
+ // MARK: - JWPlayer State Delegate
1178
+
1179
+ func jwplayerContentIsBuffering(_ player:JWPlayer) {
1180
+ self.onBuffer?([:])
1181
+ }
1182
+
1183
+ func jwplayer(_ player:JWPlayer, isBufferingWithReason reason:JWBufferReason) {
1184
+ self.onBuffer?([:])
1185
+ }
1186
+
1187
+ func jwplayer(_ player:JWPlayer, updatedBuffer percent:Double, position time:JWTimeData) {
1188
+ self.onUpdateBuffer?(["percent": percent, "position": time])
1189
+ }
1190
+
1191
+ func jwplayer(_ player:JWPlayer, didFinishLoadingWithTime loadTime:TimeInterval) {
1192
+ self.onLoaded?([:])
1193
+ }
1194
+
1195
+ func jwplayer(_ player:JWPlayer, isAttemptingToPlay playlistItem:JWPlayerItem, reason:JWPlayReason) {
1196
+ self.onAttemptPlay?([:])
1197
+ }
1198
+
1199
+ func jwplayer(_ player:JWPlayer, isPlayingWithReason reason:JWPlayReason) {
1200
+ self.onPlay?([:])
1201
+
1202
+ userPaused = false
1203
+ wasInterrupted = false
1204
+ }
1205
+
1206
+ func jwplayer(_ player:JWPlayer, willPlayWithReason reason:JWPlayReason) {
1207
+ self.onBeforePlay?([:])
1208
+ }
1209
+
1210
+ func jwplayer(_ player:JWPlayer, didPauseWithReason reason:JWPauseReason) {
1211
+ self.onPause?([:])
1212
+
1213
+ if !wasInterrupted {
1214
+ userPaused = true
1215
+ }
1216
+ }
1217
+
1218
+ func jwplayer(_ player:JWPlayer, didBecomeIdleWithReason reason:JWIdleReason) {
1219
+ self.onIdle?([:])
1220
+ }
1221
+
1222
+ func jwplayer(_ player:JWPlayer, isVisible:Bool) {
1223
+ self.onVisible?(["visible": isVisible])
1224
+ }
1225
+
1226
+ func jwplayerContentWillComplete(_ player:JWPlayer) {
1227
+ self.onBeforeComplete?([:])
1228
+ }
1229
+
1230
+ func jwplayerContentDidComplete(_ player:JWPlayer) {
1231
+ self.onComplete?([:])
1232
+ }
1233
+
1234
+ func jwplayer(_ player:JWPlayer, didLoadPlaylistItem item:JWPlayerItem, at index:UInt) {
1235
+ // var sourceDict: [String: Any] = [:]
1236
+ // var file: String?
1237
+ //
1238
+ // for source in item.videoSources {
1239
+ // sourceDict["file"] = source.file?.absoluteString
1240
+ // sourceDict["label"] = source.label
1241
+ // sourceDict["default"] = source.defaultVideo
1242
+ //
1243
+ // if source.defaultVideo {
1244
+ // file = source.file?.absoluteString
1245
+ // }
1246
+ // }
1247
+
1248
+ // var schedDict: [String: Any] = [:]
1249
+ //
1250
+ // if let schedules = item.adSchedule {
1251
+ // for sched in schedules {
1252
+ // schedDict["offset"] = sched.offset
1253
+ // schedDict["tags"] = sched.tags
1254
+ // schedDict["type"] = sched.type
1255
+ // }
1256
+ // }
1257
+
1258
+ // var trackDict: [String: Any] = [:]
1259
+
1260
+ // if let tracks = item.mediaTracks {
1261
+ // for track in tracks {
1262
+ // trackDict["file"] = track.file?.absoluteString
1263
+ // trackDict["label"] = track.label
1264
+ // trackDict["default"] = track.defaultTrack
1265
+ // }
1266
+ // }
1267
+
1268
+ // let itemDict: [String: Any] = [
1269
+ // "file": file ?? "",
1270
+ // "mediaId": item.mediaId as Any,
1271
+ // "title": item.title as Any,
1272
+ // "description": item.description,
1273
+ // "image": item.posterImage?.absoluteString ?? "",
1274
+ // "startTime": item.startTime,
1275
+ // "adVmap": item.vmapURL?.absoluteString ?? "",
1276
+ // "recommendations": item.recommendations?.absoluteString ?? "",
1277
+ // "sources": sourceDict,
1278
+ // "adSchedule": schedDict,
1279
+ // "tracks": trackDict
1280
+ // ]
1281
+
1282
+ do {
1283
+ let data:Data! = try JSONSerialization.data(withJSONObject: item.toJSONObject(), options:.prettyPrinted)
1284
+ self.onPlaylistItem?(["playlistItem": String(data:data, encoding:String.Encoding.utf8) as Any, "index": index])
1285
+ } catch {
1286
+ print("Error converting dictionary to JSON data: \(error)")
1287
+ }
1288
+
1289
+ // item.addObserver(self, forKeyPath:"playbackLikelyToKeepUp", options:.new, context:nil)
1290
+ }
1291
+
1292
+ func jwplayer(_ player:JWPlayer, didLoadPlaylist playlist:[JWPlayerItem]) {
1293
+ let playlistArray:NSMutableArray! = NSMutableArray()
1294
+
1295
+ for item:JWPlayerItem? in playlist {
1296
+ // var file:String!
1297
+ //
1298
+ // var sourceDict: [String: Any] = [:]
1299
+ //
1300
+ // for source in item?.videoSources ?? [] {
1301
+ // sourceDict["file"] = source.file?.absoluteString
1302
+ // sourceDict["label"] = source.label
1303
+ // sourceDict["default"] = source.defaultVideo
1304
+ //
1305
+ // if source.defaultVideo {
1306
+ // file = source.file?.absoluteString ?? ""
1307
+ // }
1308
+ // }
1309
+ //
1310
+ // var schedDict: [String: Any] = [:]
1311
+ // if let adSchedule = item?.adSchedule {
1312
+ // for sched in adSchedule {
1313
+ // schedDict["offset"] = sched.offset
1314
+ // schedDict["tags"] = sched.tags
1315
+ // schedDict["type"] = sched.type
1316
+ // }
1317
+ // }
1318
+ //
1319
+ // var trackDict: [String: Any] = [:]
1320
+ //
1321
+ // if let mediaTracks = item?.mediaTracks {
1322
+ // for track in mediaTracks {
1323
+ // trackDict["file"] = track.file?.absoluteString
1324
+ // trackDict["label"] = track.label
1325
+ // trackDict["default"] = track.defaultTrack
1326
+ // }
1327
+ // }
1328
+ //
1329
+ // let itemDict: [String: Any] = [
1330
+ // "file": file ?? "",
1331
+ // "mediaId": item?.mediaId ?? "",
1332
+ // "title": item?.title ?? "",
1333
+ // "description": item?.description ?? "",
1334
+ // "image": item?.posterImage?.absoluteString ?? "",
1335
+ // "startTime": item?.startTime ?? 0,
1336
+ // "adVmap": item?.vmapURL?.absoluteString ?? "",
1337
+ // "recommendations": item?.recommendations?.absoluteString ?? "",
1338
+ // "sources": sourceDict as Any,
1339
+ // "adSchedule": trackDict,
1340
+ // "tracks": schedDict
1341
+ // ]
1342
+
1343
+ playlistArray.add(item?.toJSONObject() as Any)
1344
+ }
1345
+
1346
+ do {
1347
+ let data:Data! = try JSONSerialization.data(withJSONObject: playlistArray as Any, options:.prettyPrinted)
1348
+
1349
+ self.onPlaylist?(["playlist": String(data:data as Data, encoding:String.Encoding.utf8) as Any])
1350
+ } catch {
1351
+ print("Error converting dictionary to JSON data: \(error)")
1352
+ }
1353
+ }
1354
+
1355
+ func jwplayerPlaylistHasCompleted(_ player:JWPlayer) {
1356
+ self.onPlaylistComplete?([:])
1357
+ }
1358
+
1359
+ func jwplayer(_ player:JWPlayer, usesMediaType type:JWMediaType) {
1360
+
1361
+ }
1362
+
1363
+ func jwplayer(_ player: JWPlayer, playbackRateChangedTo rate: Double, at time: TimeInterval) {
1364
+ self.onRateChanged?(["rate": rate, "at": time])
1365
+ }
1366
+
1367
+ func jwplayerHasSeeked(_ player:JWPlayer) {
1368
+ self.onSeeked?([:])
1369
+ }
1370
+
1371
+ func jwplayer(_ player: JWPlayer, seekedFrom oldPosition: TimeInterval, to newPosition: TimeInterval) {
1372
+ self.onSeek?(["from": oldPosition, "to": newPosition])
1373
+ }
1374
+
1375
+ func jwplayer(_ player:JWPlayer, updatedCues cues:[JWCue]) {
1376
+
1377
+ }
1378
+
1379
+ // MARK: - JWPlayer Ad Delegate
1380
+
1381
+ func jwplayer(_ player:JWPlayer, adEvent event:JWAdEvent) {
1382
+ self.onAdEvent?(["client": event.client, "type": event.type])
1383
+ }
1384
+
1385
+ // pragma Mark - Casting methods
1386
+
1387
+ func setUpCastController() {
1388
+ if (playerView != nil) && playerView.player as! Bool && (castController == nil) {
1389
+ castController = JWCastController(player:playerView.player)
1390
+ castController.delegate = self
1391
+ }
1392
+
1393
+ self.scanForDevices()
1394
+ }
1395
+
1396
+ func scanForDevices() {
1397
+ if castController != nil {
1398
+ castController.startDiscovery()
1399
+ }
1400
+ }
1401
+
1402
+ func stopScanForDevices() {
1403
+ if castController != nil {
1404
+ castController.stopDiscovery()
1405
+ }
1406
+ }
1407
+
1408
+ func presentCastDialog() {
1409
+ GCKCastContext.sharedInstance().presentCastDialog()
1410
+ }
1411
+
1412
+ func startDiscovery() {
1413
+ GCKCastContext.sharedInstance().discoveryManager.startDiscovery()
1414
+ }
1415
+
1416
+ func stopDiscovery() {
1417
+ GCKCastContext.sharedInstance().discoveryManager.stopDiscovery()
1418
+ }
1419
+
1420
+ func discoveryActive() -> Bool {
1421
+ return GCKCastContext.sharedInstance().discoveryManager.discoveryActive
1422
+ }
1423
+
1424
+ func hasDiscoveredDevices() -> Bool {
1425
+ return GCKCastContext.sharedInstance().discoveryManager.hasDiscoveredDevices
1426
+ }
1427
+
1428
+ func discoveryState() -> GCKDiscoveryState {
1429
+ return GCKCastContext.sharedInstance().discoveryManager.discoveryState
1430
+ }
1431
+
1432
+ func setPassiveScan(passive:Bool) {
1433
+ GCKCastContext.sharedInstance().discoveryManager.passiveScan = passive
1434
+ }
1435
+
1436
+ func castState() -> GCKCastState {
1437
+ return GCKCastContext.sharedInstance().castState
1438
+ }
1439
+
1440
+ func deviceCount() -> UInt {
1441
+ return GCKCastContext.sharedInstance().discoveryManager.deviceCount
1442
+ }
1443
+
1444
+ func getAvailableDevices() -> [JWCastingDevice]! {
1445
+ return castController.availableDevices
1446
+ }
1447
+
1448
+ func connectedDevice() -> JWCastingDevice! {
1449
+ return castController.connectedDevice
1450
+ }
1451
+
1452
+ func connectToDevice(device:JWCastingDevice!) {
1453
+ return castController.connectToDevice(device)
1454
+ }
1455
+
1456
+ func cast() {
1457
+ return castController.cast()
1458
+ }
1459
+
1460
+ func stopCasting() {
1461
+ return castController.stopCasting()
1462
+ }
1463
+
1464
+ // MARK: - JWPlayer Cast Delegate
1465
+
1466
+ func castController(_ controller: JWCastController, castingBeganWithDevice device: JWCastingDevice) {
1467
+ self.onCasting?([:])
1468
+ }
1469
+
1470
+ func castController(_ controller:JWCastController, castingEndedWithError error: Error?) {
1471
+ self.onCastingEnded?(["error": error as Any])
1472
+ }
1473
+
1474
+ func castController(_ controller:JWCastController, castingFailedWithError error: Error) {
1475
+ self.onCastingFailed?(["error": error as Any])
1476
+ }
1477
+
1478
+ func castController(_ controller:JWCastController, connectedTo device: JWCastingDevice) {
1479
+ let dict:NSMutableDictionary! = NSMutableDictionary()
1480
+
1481
+ dict.setObject(device.name, forKey:"name" as NSCopying)
1482
+ dict.setObject(device.identifier, forKey:"identifier" as NSCopying)
1483
+
1484
+ do {
1485
+ let data:Data! = try JSONSerialization.data(withJSONObject: dict as Any, options:.prettyPrinted)
1486
+
1487
+ self.onConnectedToCastingDevice?(["device": String(data:data as Data, encoding:String.Encoding.utf8) as Any])
1488
+ } catch {
1489
+ print("Error converting dictionary to JSON data: \(error)")
1490
+ }
1491
+ }
1492
+
1493
+ func castController(_ controller:JWCastController, connectionFailedWithError error: Error) {
1494
+ self.onConnectionFailed?(["error": error as Any])
1495
+ }
1496
+
1497
+ func castController(_ controller: JWCastController, connectionRecoveredWithDevice device:JWCastingDevice) {
1498
+ self.onConnectionRecovered?([:])
1499
+ }
1500
+
1501
+ func castController(_ controller: JWCastController, connectionSuspendedWithDevice device: JWCastingDevice) {
1502
+ self.onConnectionTemporarilySuspended?([:])
1503
+ }
1504
+
1505
+ func castController(_ controller: JWCastController, devicesAvailable devices:[JWCastingDevice]) {
1506
+ self.availableDevices = devices
1507
+
1508
+ var devicesInfo: [[String: Any]] = []
1509
+ for device in devices {
1510
+ var dict: [String: Any] = [:]
1511
+
1512
+ dict["name"] = device.name
1513
+ dict["identifier"] = device.identifier
1514
+
1515
+ devicesInfo.append(dict)
1516
+ }
1517
+
1518
+ do {
1519
+ let data:Data! = try JSONSerialization.data(withJSONObject: devicesInfo as Any, options:.prettyPrinted)
1520
+
1521
+ self.onCastingDevicesAvailable?(["devices": String(data:data as Data, encoding:String.Encoding.utf8) as Any])
1522
+ } catch {
1523
+ print("Error converting dictionary to JSON data: \(error)")
1524
+ }
1525
+ }
1526
+
1527
+ func castController(_ controller: JWCastController, disconnectedWithError error: (Error)?) {
1528
+ self.onDisconnectedFromCastingDevice?(["error": error as Any])
1529
+ }
1530
+
1531
+ // MARK: - JWPlayer AV Delegate
1532
+
1533
+ func jwplayer(_ player:JWPlayer, audioTracksUpdated levels:[JWMediaSelectionOption]) {
1534
+ self.onAudioTracks?([:])
1535
+ }
1536
+
1537
+ func jwplayer(_ player:JWPlayer, audioTrackChanged currentLevel:Int) {
1538
+
1539
+ }
1540
+
1541
+ func jwplayer(_ player: JWPlayer, captionPresented caption: [String], at time: JWTimeData) {
1542
+
1543
+ }
1544
+
1545
+ func jwplayer(_ player:JWPlayer, captionTrackChanged index:Int) {
1546
+
1547
+ }
1548
+
1549
+ func jwplayer(_ player: JWPlayer, visualQualityChanged currentVisualQuality: JWVisualQuality) {
1550
+
1551
+ }
1552
+
1553
+ func jwplayer(_ player:JWPlayer, qualityLevelChanged currentLevel:Int) {
1554
+
1555
+ }
1556
+
1557
+ func jwplayer(_ player:JWPlayer, qualityLevelsUpdated levels:[JWVideoSource]) {
1558
+
1559
+ }
1560
+
1561
+ func jwplayer(_ player:JWPlayer, updatedCaptionList options:[JWMediaSelectionOption]) {
1562
+
1563
+ }
1564
+
1565
+ // MARK: - JWPlayer audio session && interruption handling
1566
+
1567
+ func initAudioSession(category:String?, categoryOptions:[String]?, mode:String?) {
1568
+ self.setObservers()
1569
+
1570
+ var somethingChanged:Bool = false
1571
+
1572
+ if !(category == audioCategory) || (categoryOptions != nil && !categoryOptions!.elementsEqual(audioCategoryOptions)) {
1573
+ somethingChanged = true
1574
+ audioCategory = category
1575
+ audioCategoryOptions = categoryOptions
1576
+ self.setCategory(categoryName: category, categoryOptions:categoryOptions)
1577
+ }
1578
+
1579
+ if !(mode == audioMode) {
1580
+ somethingChanged = true
1581
+ audioMode = mode
1582
+ self.setMode(modeName: mode)
1583
+ }
1584
+
1585
+ if somethingChanged {
1586
+ do {
1587
+ try audioSession.setActive(true)
1588
+ print("setActive - success")
1589
+ } catch {
1590
+ print("setActive - error: @%@", error)
1591
+ }
1592
+ }
1593
+ }
1594
+
1595
+ func deinitAudioSession() {
1596
+ do {
1597
+ try audioSession?.setActive(false, options: .notifyOthersOnDeactivation)
1598
+ print("setUnactive - success")
1599
+ } catch {
1600
+ print("setUnactive - error: @%@", error)
1601
+ }
1602
+ audioSession = nil
1603
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
1604
+ UIApplication.shared.endReceivingRemoteControlEvents()
1605
+ }
1606
+
1607
+ func setObservers() {
1608
+ if audioSession == nil {
1609
+ audioSession = AVAudioSession.sharedInstance()
1610
+
1611
+ NotificationCenter.default.addObserver(self,
1612
+ selector:#selector(handleMediaServicesReset),
1613
+ name:AVAudioSession.mediaServicesWereResetNotification,
1614
+ object:audioSession)
1615
+
1616
+ NotificationCenter.default.addObserver(self,
1617
+ selector: #selector(audioSessionInterrupted(_:)),
1618
+ name: AVAudioSession.interruptionNotification,
1619
+ object: audioSession)
1620
+
1621
+ NotificationCenter.default.addObserver(self,
1622
+ selector:#selector(applicationWillResignActive(_:)),
1623
+ name:UIApplication.willResignActiveNotification, object:nil)
1624
+
1625
+ NotificationCenter.default.addObserver(self,
1626
+ selector:#selector(applicationDidEnterBackground(_:)),
1627
+ name:UIApplication.didEnterBackgroundNotification,
1628
+ object:nil)
1629
+
1630
+ NotificationCenter.default.addObserver(self,
1631
+ selector:#selector(applicationWillEnterForeground(_:)),
1632
+ name:UIApplication.willEnterForegroundNotification, object:nil)
1633
+
1634
+ NotificationCenter.default.addObserver(self,
1635
+ selector: #selector(audioRouteChanged(_:)),
1636
+ name: AVAudioSession.routeChangeNotification,
1637
+ object: nil)
1638
+ }
1639
+ }
1640
+
1641
+ func setCategory(categoryName:String!, categoryOptions:[String]!) {
1642
+ if (audioSession == nil) {
1643
+ audioSession = AVAudioSession.sharedInstance()
1644
+ }
1645
+
1646
+ var category:AVAudioSession.Category! = nil
1647
+ if categoryName.isEqual("Ambient") {
1648
+ category = .ambient
1649
+ } else if categoryName.isEqual("SoloAmbient") {
1650
+ category = .soloAmbient
1651
+ } else if categoryName.isEqual("Playback") {
1652
+ category = .playback
1653
+ } else if categoryName.isEqual("Record") {
1654
+ category = .record
1655
+ } else if categoryName.isEqual("PlayAndRecord") {
1656
+ category = .playAndRecord
1657
+ } else if categoryName.isEqual("MultiRoute") {
1658
+ category = .multiRoute
1659
+ } else {
1660
+ category = .playback
1661
+ }
1662
+
1663
+ var options: AVAudioSession.CategoryOptions = []
1664
+ if categoryOptions.contains("MixWithOthers") {
1665
+ options.insert(.mixWithOthers)
1666
+ }
1667
+ if categoryOptions.contains("DuckOthers") {
1668
+ options.insert(.duckOthers)
1669
+ }
1670
+ if categoryOptions.contains("AllowBluetooth") {
1671
+ options.insert(.allowBluetooth)
1672
+ }
1673
+ if categoryOptions.contains("InterruptSpokenAudioAndMix") {
1674
+ options.insert(.interruptSpokenAudioAndMixWithOthers)
1675
+ }
1676
+ if categoryOptions.contains("AllowBluetoothA2DP") {
1677
+ options.insert(.allowBluetoothA2DP)
1678
+ }
1679
+ if categoryOptions.contains("AllowAirPlay") {
1680
+ options.insert(.allowAirPlay)
1681
+ }
1682
+ if categoryOptions.contains("OverrideMutedMicrophone") {
1683
+ if #available(iOS 14.5, *) {
1684
+ options.insert(.overrideMutedMicrophoneInterruption)
1685
+ } else {
1686
+ // Handle the case for earlier versions if needed
1687
+ }
1688
+ }
1689
+
1690
+ do {
1691
+ try audioSession.setCategory(category, options: options)
1692
+ print("setCategory - success")
1693
+ } catch {
1694
+ print("setCategory - error: @%@", error)
1695
+ }
1696
+ }
1697
+
1698
+ func setMode(modeName:String!) {
1699
+ if (audioSession == nil) {
1700
+ audioSession = AVAudioSession.sharedInstance()
1701
+ }
1702
+
1703
+ var mode:AVAudioSession.Mode! = nil
1704
+
1705
+ if modeName.isEqual("Default") {
1706
+ mode = .default
1707
+ } else if modeName.isEqual("VoiceChat") {
1708
+ mode = .voiceChat
1709
+ } else if modeName.isEqual("VideoChat") {
1710
+ mode = .videoChat
1711
+ } else if modeName.isEqual("GameChat") {
1712
+ mode = .gameChat
1713
+ } else if modeName.isEqual("VideoRecording") {
1714
+ mode = .videoRecording
1715
+ } else if modeName.isEqual("Measurement") {
1716
+ mode = .measurement
1717
+ } else if modeName.isEqual("MoviePlayback") {
1718
+ mode = .moviePlayback
1719
+ } else if modeName.isEqual("SpokenAudio") {
1720
+ mode = .spokenAudio
1721
+ } else if modeName.isEqual("VoicePrompt") {
1722
+ if #available(iOS 12.0, *) {
1723
+ mode = .voicePrompt
1724
+ } else {
1725
+ // Fallback on earlier versions
1726
+ }
1727
+ }
1728
+
1729
+ if (mode != nil) {
1730
+ do {
1731
+ try audioSession.setMode(mode)
1732
+ print("setMode - success")
1733
+ } catch {
1734
+ print("setMode - error: @%@", error)
1735
+ }
1736
+ }
1737
+ }
1738
+
1739
+ @objc func audioSessionInterrupted(_ notification: Notification) {
1740
+ guard let userInfo = notification.userInfo,
1741
+ let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
1742
+ let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
1743
+ return
1744
+ }
1745
+
1746
+ switch type {
1747
+ case .began:
1748
+ DispatchQueue.main.async { [self] in
1749
+ wasInterrupted = true
1750
+
1751
+ if (playerView != nil) {
1752
+ playerView.player.pause()
1753
+ } else if (playerViewController != nil) {
1754
+ playerViewController.player.pause()
1755
+ }
1756
+ print("handleInterruption :- Pause")
1757
+ }
1758
+ case .ended:
1759
+ guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
1760
+ let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
1761
+ if options.contains(.shouldResume) && !userPaused && backgroundAudioEnabled {
1762
+ // Interruption ended. Playback should resume.
1763
+ DispatchQueue.main.async { [self] in
1764
+ if (playerView != nil) {
1765
+ playerView.player.play()
1766
+ } else if (playerViewController != nil) {
1767
+ playerViewController.player.play()
1768
+ }
1769
+ print("handleInterruption :- Play")
1770
+ }
1771
+ }
1772
+ else {
1773
+ // Interruption ended. Playback should not resume.
1774
+ DispatchQueue.main.async { [self] in
1775
+ wasInterrupted = true
1776
+
1777
+ if (playerView != nil) {
1778
+ playerView.player.pause()
1779
+ } else if (playerViewController != nil) {
1780
+ playerViewController.player.pause()
1781
+ }
1782
+ print("handleInterruption :- Pause")
1783
+ }
1784
+ }
1785
+ @unknown default:
1786
+ break
1787
+ }
1788
+ }
1789
+
1790
+ // Service reset
1791
+ @objc func handleMediaServicesReset() {
1792
+ // • Handle this notification by fully reconfiguring audio
1793
+ }
1794
+
1795
+ // Inactive
1796
+ // Hack for ios 14 stopping audio when going to background
1797
+ @objc func applicationWillResignActive(_ notification: Notification) {
1798
+ if !userPaused && backgroundAudioEnabled {
1799
+ if (playerView != nil) && playerView.player.getState() == .playing {
1800
+ playerView.player.play()
1801
+ } else if (playerViewController != nil) && playerViewController.player.getState() == .playing {
1802
+ playerViewController.player.play()
1803
+ }
1804
+ }
1805
+ }
1806
+
1807
+ // Background
1808
+ @objc func applicationDidEnterBackground(_ notification: Notification) {
1809
+
1810
+ }
1811
+
1812
+ // Active
1813
+ @objc func applicationWillEnterForeground(_ notification: Notification) {
1814
+ if !userPaused && backgroundAudioEnabled {
1815
+ if (playerView != nil) && playerView.player.getState() == .playing {
1816
+ playerView.player.play()
1817
+ } else if (playerViewController != nil) && playerViewController.player.getState() == .playing {
1818
+ playerViewController.player.play()
1819
+ }
1820
+ }
1821
+ }
1822
+
1823
+ // Route change
1824
+ @objc func audioRouteChanged(_ notification: Notification) {
1825
+ guard let userInfo = notification.userInfo else { return }
1826
+ guard let reason = userInfo[AVAudioSessionRouteChangeReasonKey] as? Int else { return }
1827
+
1828
+ if reason == AVAudioSession.RouteChangeReason.oldDeviceUnavailable.hashValue {
1829
+ if (playerView != nil) {
1830
+ playerView.player.pause()
1831
+ } else if (playerViewController != nil) {
1832
+ playerViewController.player.pause()
1833
+ }
1834
+ }
1835
+ }
1836
+
1837
+ }