react-native-theoplayer 10.4.0 → 10.5.1

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 (47) hide show
  1. package/CHANGELOG.md +26 -2
  2. package/android/src/main/java/com/theoplayer/ReactTHEOplayerContext.kt +6 -0
  3. package/android/src/main/java/com/theoplayer/audio/BackgroundAudioConfig.kt +7 -0
  4. package/android/src/main/java/com/theoplayer/audio/BackgroundAudioConfigAdapter.kt +12 -3
  5. package/android/src/main/java/com/theoplayer/drm/ContentProtectionAdapter.kt +1 -0
  6. package/android/src/main/java/com/theoplayer/presentation/PresentationManager.kt +8 -4
  7. package/android/src/main/java/com/theoplayer/source/SourceAdapter.kt +3 -0
  8. package/android/src/main/java/com/theoplayer/theolive/EndpointAdapter.kt +16 -0
  9. package/ios/THEOplayerRCTDebug.swift +1 -1
  10. package/ios/THEOplayerRCTMediaTrackEventHandler.swift +3 -38
  11. package/ios/THEOplayerRCTPlayerAPI.swift +1 -0
  12. package/ios/THEOplayerRCTSourceDescriptionBuilder.swift +51 -63
  13. package/ios/THEOplayerRCTTrackMetadataAggregator.swift +58 -6
  14. package/ios/THEOplayerRCTView+AppState.swift +33 -0
  15. package/ios/THEOplayerRCTView.swift +29 -1
  16. package/ios/backgroundAudio/THEOplayerRCTBackgroundAudioManager.swift +19 -17
  17. package/ios/backgroundAudio/THEOplayerRCTNowPlayingManager.swift +3 -3
  18. package/ios/backgroundAudio/THEOplayerRCTView+BackgroundAudioConfig.swift +8 -0
  19. package/ios/cache/THEOplayerRCTCacheAPI.swift +1 -2
  20. package/ios/pip/THEOplayerRCTPipControlsManager.swift +1 -1
  21. package/ios/pip/THEOplayerRCTPipManager.swift +38 -26
  22. package/ios/theolive/THEOplayerRCTSourceDescriptionBuilder+Theolive.swift +2 -2
  23. package/ios/theolive/THEOplayerRCTTHEOliveEventAdapter.swift +4 -0
  24. package/lib/commonjs/api/backgroundAudio/BackgroundAudioConfiguration.js.map +1 -1
  25. package/lib/commonjs/internal/adapter/THEOplayerWebAdapter.js +8 -3
  26. package/lib/commonjs/internal/adapter/THEOplayerWebAdapter.js.map +1 -1
  27. package/lib/commonjs/manifest.json +1 -1
  28. package/lib/module/api/backgroundAudio/BackgroundAudioConfiguration.js.map +1 -1
  29. package/lib/module/internal/adapter/THEOplayerWebAdapter.js +8 -3
  30. package/lib/module/internal/adapter/THEOplayerWebAdapter.js.map +1 -1
  31. package/lib/module/manifest.json +1 -1
  32. package/lib/typescript/api/backgroundAudio/BackgroundAudioConfiguration.d.ts +9 -0
  33. package/lib/typescript/api/backgroundAudio/BackgroundAudioConfiguration.d.ts.map +1 -1
  34. package/lib/typescript/api/media/MediaControlConfiguration.d.ts +1 -1
  35. package/lib/typescript/api/source/drm/DRMConfiguration.d.ts +10 -0
  36. package/lib/typescript/api/source/drm/DRMConfiguration.d.ts.map +1 -1
  37. package/lib/typescript/api/theolive/TheoLiveEndpoint.d.ts +7 -0
  38. package/lib/typescript/api/theolive/TheoLiveEndpoint.d.ts.map +1 -1
  39. package/lib/typescript/internal/adapter/THEOplayerWebAdapter.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/react-native-theoplayer.podspec +7 -7
  42. package/src/api/backgroundAudio/BackgroundAudioConfiguration.ts +10 -0
  43. package/src/api/media/MediaControlConfiguration.ts +1 -1
  44. package/src/api/source/drm/DRMConfiguration.ts +9 -0
  45. package/src/api/theolive/TheoLiveEndpoint.ts +8 -0
  46. package/src/internal/adapter/THEOplayerWebAdapter.ts +8 -3
  47. package/src/manifest.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.1.0/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [10.5.1] - 25-11-28
9
+
10
+ ### Added
11
+
12
+ - Support for contentProtection query parameters on THEOlive sources.
13
+
14
+ ## [10.5.0] - 25-11-24
15
+
16
+ ### Fixed
17
+
18
+ - Fixed an issue on iOS where the scrim of an IMA ad was in a wrong position due to incorrect `safeAreaInsets`.
19
+
20
+ ### Added
21
+
22
+ - Added `stopOnBackground` property to `BackgroundAudioConfiguration` to control whether playback should stop when the app goes to the background.
23
+ - Added `millicastSrc` to `TheoLiveEndpoint` for Web and Android.
24
+ - Added support for configuring query parameters that are common to multiple key system configurations. The parameters defined in `contentProtection.queryParameters` will be merged with any query parameters that are explicitly defined on a key system configuration, whereby the latter takes precedence.
25
+
26
+ ### Changed
27
+
28
+ - Updated the active quality info extraction on iOS to use the activeQualityChange event data instead of player API.
29
+ - On iOS, we now also pass all available qualities and the current active quality when bridging video- and audio track data.
30
+
8
31
  ## [10.4.0] - 25-11-13
9
32
 
10
33
  ### Fixed
@@ -15,6 +38,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
15
38
  ### Added
16
39
 
17
40
  - Pass `streamActivityMonitorId` property for `THEOAdDescription` on iOS and Android.
41
+ - Added `allowLivePlayPause` and `seekToLiveOnResume` properties to `PlayerConfiguration.mediaControl` to control pausing and resuming behavior on live streams from the lockscreen controls.
18
42
 
19
43
  ### Changed
20
44
 
@@ -138,11 +162,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
138
162
 
139
163
  - Deprecated the `BaseSource.integration` property in favor of `TypedSource.type`.
140
164
 
141
- ## Added
165
+ ### Added
142
166
 
143
167
  - Added support for `THEOlive` events.
144
168
 
145
- ## Fixed
169
+ ### Fixed
146
170
 
147
171
  - Fixed an issue on Android where, depending on the project structure, the Maven repository list would be incorrect.
148
172
 
@@ -458,6 +458,12 @@ class ReactTHEOplayerContext private constructor(
458
458
  // The player pauses and goes to the background, we can abandon audio focus.
459
459
  audioFocusManager?.abandonAudioFocus()
460
460
  }
461
+
462
+ // Optionally fully stop play-out, unless PiP mode is active.
463
+ if (backgroundAudioConfig.stopOnBackground
464
+ && reactContext.currentActivity?.isInPictureInPictureMode != true) {
465
+ player.stop()
466
+ }
461
467
  }
462
468
 
463
469
  /**
@@ -7,4 +7,11 @@ data class BackgroundAudioConfig(
7
7
  * @defaultValue `false`
8
8
  */
9
9
  val enabled: Boolean,
10
+
11
+ /**
12
+ * Set whether the player should stop play-out when the app goes to background.
13
+ *
14
+ * @defaultValue `false`
15
+ */
16
+ val stopOnBackground: Boolean = false
10
17
  )
@@ -4,12 +4,21 @@ import com.facebook.react.bridge.ReadableMap
4
4
 
5
5
  object BackgroundAudioConfigAdapter {
6
6
  private const val PROP_ENABLED = "enabled"
7
+ private const val PROP_STOP_ON_BACKGROUND = "stopOnBackground"
7
8
 
8
9
  private const val DEFAULT_ENABLED = false
10
+ private const val DEFAULT_STOP_ON_BACKGROUND = false
9
11
 
10
12
  fun fromProps(props: ReadableMap?): BackgroundAudioConfig {
11
- val enabled =
12
- if (props?.hasKey(PROP_ENABLED) == true) props.getBoolean(PROP_ENABLED) else DEFAULT_ENABLED
13
- return BackgroundAudioConfig(enabled)
13
+ val enabled = props?.hasKey(PROP_ENABLED)?.let {
14
+ props.getBoolean(PROP_ENABLED)
15
+ } ?: DEFAULT_ENABLED
16
+ val destroy = props?.hasKey(PROP_STOP_ON_BACKGROUND)?.let {
17
+ props.getBoolean(PROP_STOP_ON_BACKGROUND)
18
+ } ?: DEFAULT_STOP_ON_BACKGROUND
19
+ return BackgroundAudioConfig(
20
+ enabled = enabled,
21
+ stopOnBackground = destroy
22
+ )
14
23
  }
15
24
  }
@@ -104,6 +104,7 @@ object ContentProtectionAdapter {
104
104
  if (jsonConfig.has(PROP_INTEGRATION_PARAMETERS)) {
105
105
  integrationParameters(fromJSONObjectToMap(jsonConfig.getJSONObject(PROP_INTEGRATION_PARAMETERS)))
106
106
  }
107
+ queryParameters(fromJSONObjectToMap(jsonConfig.optJSONObject(PROP_QUERY_PARAMETERS)))
107
108
  }.build()
108
109
  }
109
110
 
@@ -319,14 +319,18 @@ class PresentationManager(
319
319
  currentPresentationModeChangeContext = context
320
320
  eventEmitter.emitPresentationModeChange(presentationMode, prevPresentationMode, context)
321
321
 
322
- // Resume playing when going to PiP and player was playing
322
+ // Resume play-out when going to PiP and player was playing
323
323
  if (presentationMode == PresentationMode.PICTURE_IN_PICTURE && viewCtx.wasPlayingOnHostPause) {
324
324
  viewCtx.player.play()
325
325
  }
326
326
 
327
- // Apply background audio config when closing PiP window
328
- if (context?.pip == PresentationModeChangePipContext.CLOSED && !viewCtx.backgroundAudioConfig.enabled) {
329
- viewCtx.player.pause()
327
+ // When closing PiP window
328
+ if (context?.pip == PresentationModeChangePipContext.CLOSED) {
329
+ // Pause if background audio is not enabled
330
+ if (!viewCtx.backgroundAudioConfig.enabled) viewCtx.player.pause()
331
+
332
+ // Optionally fully stop play-out
333
+ if (viewCtx.backgroundAudioConfig.stopOnBackground) viewCtx.player.stop()
330
334
  }
331
335
  }
332
336
  }
@@ -178,6 +178,9 @@ class SourceAdapter {
178
178
  },
179
179
  latencyConfiguration=jsonTypedSource.optJSONObject(PROP_LATENCY_CONFIGURATION)?.let {
180
180
  parseLatencyConfiguration(it)
181
+ },
182
+ drm=jsonTypedSource.optJSONObject(PROP_CONTENT_PROTECTION)?.let {
183
+ ContentProtectionAdapter.drmConfigurationFromJson(it)
181
184
  }
182
185
  )
183
186
  }
@@ -4,9 +4,15 @@ import com.facebook.react.bridge.Arguments
4
4
  import com.facebook.react.bridge.WritableMap
5
5
  import com.theoplayer.android.api.theolive.ContentProtectionConfiguration
6
6
  import com.theoplayer.android.api.theolive.Endpoint
7
+ import com.theoplayer.android.api.theolive.EndpointMillicastSource
7
8
  import com.theoplayer.android.api.theolive.FairPlayConfiguration
8
9
  import com.theoplayer.android.api.theolive.KeySystemConfiguration
9
10
 
11
+ private const val PROP_MILLICAST_SRC = "millicastSrc"
12
+ private const val PROP_MILLICAST_NAME = "name"
13
+ private const val PROP_MILLICAST_ACCOUNTID = "accountId"
14
+ private const val PROP_MILLICAST_SUBSCRIBER_TOKEN ="subscriberToken"
15
+ private const val PROP_MILLICAST_DIRECTOR_URL = "directorUrl"
10
16
  private const val PROP_HESP_SRC = "hespSrc"
11
17
  private const val PROP_HLS_SRC = "hlsSrc"
12
18
  private const val PROP_AD_SRC = "adSrc"
@@ -24,8 +30,18 @@ private const val PROP_CERTIFICATE_URL = "certificateUrl"
24
30
 
25
31
  object EndpointAdapter {
26
32
 
33
+ fun fromEndPointMillicastSource(millicastSource: EndpointMillicastSource): WritableMap {
34
+ return Arguments.createMap().apply {
35
+ putString(PROP_MILLICAST_NAME , millicastSource.name)
36
+ putString(PROP_MILLICAST_ACCOUNTID, millicastSource.accountId)
37
+ millicastSource.directorUrl?.let { putString(PROP_MILLICAST_DIRECTOR_URL, it) }
38
+ millicastSource.subscriberToken?.let { putString(PROP_MILLICAST_SUBSCRIBER_TOKEN, it) }
39
+ }
40
+ }
41
+
27
42
  fun fromEndpoint(endPoint: Endpoint): WritableMap {
28
43
  return Arguments.createMap().apply {
44
+ endPoint.millicastSrc?.let { putMap(PROP_MILLICAST_SRC, fromEndPointMillicastSource(it)) }
29
45
  endPoint.hespSrc?.let { putString(PROP_HESP_SRC, it) }
30
46
  endPoint.hlsSrc?.let { putString(PROP_HLS_SRC, it) }
31
47
  endPoint.adSrc?.let { putString(PROP_AD_SRC, it) }
@@ -15,7 +15,7 @@ let DEBUG_THEOPLAYER_INTERACTION = DEBUG && false
15
15
  // Debug flag to monitor contentProtection integration handling
16
16
  let DEBUG_CONTENT_PROTECTION_API = DEBUG && false
17
17
 
18
- // Debug flag to monitor all updates made on bridged properties
18
+ // Debug flag to monitor all updates made on bridged properties
19
19
  let DEBUG_VIEW = DEBUG && false
20
20
 
21
21
  // Debug flag to monitor correct SourceDescription buildup
@@ -235,26 +235,12 @@ class THEOplayerRCTMediaTrackEventHandler {
235
235
  let player = self.player,
236
236
  let track = self.activeTrack(tracks: player.videoTracks) {
237
237
  if DEBUG_THEOPLAYER_EVENTS { PrintUtils.printLog(logText: "[NATIVE] Received ACTIVE_QUALITY_CHANGED event for videoTrack") }
238
- let identifier = String(track.activeQualityBandwidth)
239
- let label = self.labelFromBandWidth(track.activeQualityBandwidth)
238
+
240
239
  forwardedMediaTrackEvent([
241
240
  "trackUid" : track.uid,
242
241
  "type" : MediaTrackEventType.ACTIVE_QUALITY_CHANGED.rawValue,
243
242
  "trackType": MediaTrackType.VIDEO.rawValue,
244
- "qualities": [
245
- "bandwidth": track.activeQualityBandwidth,
246
- "codecs": "",
247
- "id": identifier,
248
- "uid": identifier,
249
- "name": label,
250
- "label": label,
251
- "available": true,
252
- "width": player.videoWidth,
253
- "height": player.videoHeight,
254
- //"frameRate": 0, // not available on iOS SDK
255
- //"firstFrame": 0 // not available on iOS SDK
256
-
257
- ]
243
+ "qualities": THEOplayerRCTTrackMetadataAggregator.aggregatedQualityInfo(quality: event.quality)
258
244
  ])
259
245
  }
260
246
  }
@@ -264,23 +250,12 @@ class THEOplayerRCTMediaTrackEventHandler {
264
250
  let player = self.player,
265
251
  let track = self.activeTrack(tracks: player.audioTracks) {
266
252
  if DEBUG_THEOPLAYER_EVENTS { PrintUtils.printLog(logText: "[NATIVE] Received ACTIVE_QUALITY_CHANGED event for audioTrack") }
267
- let identifier = String(track.activeQualityBandwidth)
268
- let label = self.labelFromBandWidth(track.activeQualityBandwidth)
269
253
 
270
254
  forwardedMediaTrackEvent([
271
255
  "trackUid" : track.uid,
272
256
  "type" : MediaTrackEventType.ACTIVE_QUALITY_CHANGED.rawValue,
273
257
  "trackType": MediaTrackType.AUDIO.rawValue,
274
- "qualities": [
275
- "bandwidth": track.activeQualityBandwidth,
276
- "codecs": "",
277
- "id": identifier,
278
- "uid": identifier,
279
- "name": label,
280
- "label": label,
281
- "available": true,
282
- //"audioSamplingRate": 0 // not available on iOS SDK
283
- ]
258
+ "qualities": THEOplayerRCTTrackMetadataAggregator.aggregatedQualityInfo(quality: event.quality)
284
259
  ])
285
260
  }
286
261
  }
@@ -299,14 +274,4 @@ class THEOplayerRCTMediaTrackEventHandler {
299
274
  }
300
275
  return nil;
301
276
  }
302
-
303
- private func labelFromBandWidth(_ bandWidth: Int) -> String {
304
- if bandWidth > 1000000 {
305
- return "\(Double(bandWidth / 1000) / 1000) Mbps"
306
- } else if bandWidth > 1000 {
307
- return "\(bandWidth / 1000) kbps"
308
- } else {
309
- return "No Label"
310
- }
311
- }
312
277
  }
@@ -268,6 +268,7 @@ class THEOplayerRCTPlayerAPI: NSObject, RCTBridgeModule {
268
268
  private func parseBackgroundAudioConfig(configDict: NSDictionary) -> BackgroundAudioConfig {
269
269
  var backgroundAudio = BackgroundAudioConfig()
270
270
  backgroundAudio.enabled = configDict["enabled"] as? Bool ?? false
271
+ backgroundAudio.stopOnBackground = configDict["stopOnBackground"] as? Bool ?? false
271
272
  backgroundAudio.shouldResumeAfterInterruption = configDict["shouldResumeAfterInterruption"] as? Bool ?? false
272
273
  if let audioSessionModeString = configDict["audioSessionMode"] as? String {
273
274
  backgroundAudio.audioSessionMode = THEOplayerRCTTypeUtils.audioSessionModeFromString(audioSessionModeString)
@@ -63,6 +63,7 @@ let SD_PROP_RETRIEVE_POD_ID_URI: String = "retrievePodIdURI"
63
63
  let SD_PROP_INITIALIZATION_DELAY: String = "initializationDelay"
64
64
  let SD_PROP_HLS_DATE_RANGE: String = "hlsDateRange"
65
65
  let SD_PROP_CMCD: String = "cmcd"
66
+ let SD_PROP_QUERY_PARAMETERS = "queryParameters"
66
67
 
67
68
  let EXTENSION_HLS: String = ".m3u8"
68
69
  let EXTENSION_MP4: String = ".mp4"
@@ -194,7 +195,7 @@ class THEOplayerRCTSourceDescriptionBuilder {
194
195
  let type = typedSourceData[SD_PROP_TYPE] as? String
195
196
 
196
197
  if integration == "theolive" || type == "theolive" {
197
- return THEOplayerRCTSourceDescriptionBuilder.buildTHEOliveDescription(typedSourceData)
198
+ return THEOplayerRCTSourceDescriptionBuilder.buildTHEOliveDescription(typedSourceData, contentPotection: contentProtection)
198
199
  }
199
200
 
200
201
  if type == "millicast" {
@@ -230,8 +231,7 @@ class THEOplayerRCTSourceDescriptionBuilder {
230
231
  guard let contentProtectionData = typedSourceData[SD_PROP_CONTENT_PROTECTION] as? [String:Any] else {
231
232
  return nil
232
233
  }
233
- let sanitisedContentProtectionData = THEOplayerRCTSourceDescriptionBuilder.sanitiseContentProtectionData(contentProtectionData)
234
- return THEOplayerRCTSourceDescriptionBuilder.buildContentProtection(sanitisedContentProtectionData)
234
+ return THEOplayerRCTSourceDescriptionBuilder.buildContentProtection(contentProtectionData)
235
235
  }
236
236
 
237
237
  /**
@@ -339,77 +339,65 @@ class THEOplayerRCTSourceDescriptionBuilder {
339
339
  }
340
340
  #endif
341
341
 
342
- /**
343
- Updates the contentProtectionData to a valid iOS SDK contentProtectionData, flattening out cross SDK differences
344
- - returns: a THEOplayer valid contentProtection data map
342
+ /**
343
+ Creates a THEOplayer DRMConfiguration. This requires a contentProtection property in the RN source description.
344
+ - returns: a THEOplayer DRMConfiguration
345
345
  */
346
- static func sanitiseContentProtectionData(_ contentProtectionData: [String:Any]) -> [String:Any] {
347
- var sanitisedContentProtectionData: [String:Any] = contentProtectionData
348
- // fairplay update
346
+ static func buildContentProtection(_ contentProtectionData: [String:Any]) -> MultiplatformDRMConfiguration? {
347
+ let customIntegrationId = contentProtectionData[SD_PROP_INTEGRATION] as? String ?? "internal"
348
+
349
+ // fairplay
350
+ var fairplayKeySystem: THEOplayerSDK.KeySystemConfiguration? = nil
349
351
  if let fairplayData = contentProtectionData[SD_PROP_FAIRPLAY] as? [String:Any] {
350
- var sanitisedFairplayData: [String:Any] = [:]
351
352
  // certificateUrl
352
- if let certificateUrl = fairplayData[SD_PROP_CERTIFICATE_URL] as? String {
353
- sanitisedFairplayData[SD_PROP_CERTIFICATE_URL] = certificateUrl
354
- }
355
- // convert certificate into certificateUrl with marker prefix (also supported by THEOplayer Web SDK)
353
+ var certificateUrl = fairplayData[SD_PROP_CERTIFICATE_URL] as? String
356
354
  if let certificate = fairplayData[SD_PROP_CERTIFICATE] as? String {
357
- sanitisedFairplayData[SD_PROP_CERTIFICATE_URL] = "\(CERTIFICATE_MARKER)\(certificate)"
358
- }
359
- // licenseAcquisitionURL
360
- if let licenseAcquisitionURL = fairplayData[SD_PROP_LICENSE_URL] as? String {
361
- sanitisedFairplayData[SD_PROP_LICENSE_URL] = licenseAcquisitionURL
355
+ certificateUrl = "\(CERTIFICATE_MARKER)\(certificate)"
362
356
  }
363
- // headers
364
- if let headers = fairplayData[SD_PROP_HEADERS] as? [String:String] {
365
- sanitisedFairplayData[SD_PROP_HEADERS] = headers
366
- }
367
- // licenseType
368
- if let licenseType = fairplayData[SD_PROP_LICENSE_TYPE] as? String {
369
- sanitisedFairplayData[SD_PROP_LICENSE_TYPE] = licenseType
370
- }
371
- sanitisedContentProtectionData[SD_PROP_FAIRPLAY] = sanitisedFairplayData
357
+ let licenseAcquisitionURL = fairplayData[SD_PROP_LICENSE_URL] as? String
358
+ let headers = fairplayData[SD_PROP_HEADERS] as? [String:String]
359
+ let licenseType = fairplayData[SD_PROP_LICENSE_TYPE] as? String
360
+ let queryParameters = fairplayData[SD_PROP_QUERY_PARAMETERS] as? [String:String]
361
+
362
+ fairplayKeySystem = KeySystemConfiguration(
363
+ licenseAcquisitionURL: licenseAcquisitionURL,
364
+ certificateURL: certificateUrl,
365
+ licenseType: licenseType == "persistent" ? LicenseType.persistent : LicenseType.temporary,
366
+ headers: headers,
367
+ queryParameters: queryParameters
368
+ )
372
369
  }
373
- // widevine update
370
+
371
+ // widevine
372
+ var widevineKeySystem: THEOplayerSDK.KeySystemConfiguration? = nil
374
373
  if let widevineData = contentProtectionData[SD_PROP_WIDEVINE] as? [String:Any] {
375
- var sanitisedWidevineData: [String:Any] = [:]
376
374
  // certificateUrl
377
- if let certificateUrl = widevineData[SD_PROP_CERTIFICATE_URL] as? String {
378
- sanitisedWidevineData[SD_PROP_CERTIFICATE_URL] = certificateUrl
379
- }
380
- // convert certificate into certificateUrl with marker prefix (also supported by THEOplayer Web SDK)
375
+ var certificateUrl = widevineData[SD_PROP_CERTIFICATE_URL] as? String
381
376
  if let certificate = widevineData[SD_PROP_CERTIFICATE] as? String {
382
- sanitisedWidevineData[SD_PROP_CERTIFICATE_URL] = "\(CERTIFICATE_MARKER)\(certificate)"
383
- }
384
- // licenseAcquisitionURL
385
- if let licenseAcquisitionURL = widevineData[SD_PROP_LICENSE_URL] as? String {
386
- sanitisedWidevineData[SD_PROP_LICENSE_URL] = licenseAcquisitionURL
387
- }
388
- // headers
389
- if let headers = widevineData[SD_PROP_HEADERS] as? [String:String] {
390
- sanitisedWidevineData[SD_PROP_HEADERS] = headers
377
+ certificateUrl = "\(CERTIFICATE_MARKER)\(certificate)"
391
378
  }
392
- // licenseType
393
- if let licenseType = widevineData[SD_PROP_LICENSE_TYPE] as? String {
394
- sanitisedWidevineData[SD_PROP_LICENSE_TYPE] = licenseType
395
- }
396
- sanitisedContentProtectionData[SD_PROP_WIDEVINE] = sanitisedWidevineData
397
- }
398
- return sanitisedContentProtectionData
399
- }
400
-
401
- /**
402
- Creates a THEOplayer DRMConfiguration. This requires a contentProtection property in the RN source description.
403
- - returns: a THEOplayer DRMConfiguration
404
- */
405
- static func buildContentProtection(_ contentProtectionData: [String:Any]) -> MultiplatformDRMConfiguration? {
406
- do {
407
- let data = try JSONSerialization.data(withJSONObject: contentProtectionData)
408
- return try JSONDecoder().decode(MultiplatformDRMConfiguration.self, from: data)
409
- } catch {
410
- PrintUtils.printLog(logText: "[NATIVE] unsupported contentProtection data format")
379
+ let licenseAcquisitionURL = widevineData[SD_PROP_LICENSE_URL] as? String
380
+ let headers = widevineData[SD_PROP_HEADERS] as? [String:String]
381
+ let licenseType = widevineData[SD_PROP_LICENSE_TYPE] as? String
382
+ let queryParameters = widevineData[SD_PROP_QUERY_PARAMETERS] as? [String:String]
383
+
384
+ widevineKeySystem = KeySystemConfiguration(
385
+ licenseAcquisitionURL: licenseAcquisitionURL,
386
+ certificateURL: certificateUrl,
387
+ licenseType: licenseType == "persistent" ? LicenseType.persistent : LicenseType.temporary,
388
+ headers: headers,
389
+ queryParameters: queryParameters
390
+ )
411
391
  }
412
- return nil
392
+
393
+ // global query parameters
394
+ let queryParameters = contentProtectionData[SD_PROP_QUERY_PARAMETERS] as? [String:String]
395
+
396
+ return MultiplatformDRMConfiguration(
397
+ customIntegrationId: customIntegrationId,
398
+ keySystemConfigurations: KeySystemConfigurationCollection(fairplay: fairplayKeySystem, widevine: widevineKeySystem),
399
+ queryParameters: queryParameters
400
+ )
413
401
  }
414
402
 
415
403
 
@@ -179,9 +179,16 @@ class THEOplayerRCTTrackMetadataAggregator {
179
179
  entry[PROP_LABEL] = audioTrack.label
180
180
  entry[PROP_UNLOCALIZED_LABEL] = audioTrack.unlocalizedLabel
181
181
  entry[PROP_ENABLED] = audioTrack.enabled
182
- entry[PROP_QUALITIES] = [] // empty: qualities are not being exposed on iOS
183
- //entry[PROP_ACTIVE_QUALITY] = // undefined: qualities are not being exposed on iOS
184
- //entry[PROP_TARGET_QUALITY] = // undefined: qualities are not being exposed on iOS
182
+
183
+ // add known qualities
184
+ entry[PROP_QUALITIES] = (0..<audioTrack.qualities.count).map { index in
185
+ return THEOplayerRCTTrackMetadataAggregator.aggregatedQualityInfo(quality: audioTrack.qualities.get(index))
186
+ }
187
+
188
+ // add active quality
189
+ if let activeQuality = audioTrack.activeQuality {
190
+ entry[PROP_ACTIVE_QUALITY] = THEOplayerRCTTrackMetadataAggregator.aggregatedQualityInfo(quality: activeQuality)
191
+ }
185
192
  return entry
186
193
  }
187
194
 
@@ -221,9 +228,17 @@ class THEOplayerRCTTrackMetadataAggregator {
221
228
  entry[PROP_LABEL] = videoTrack.label
222
229
  entry[PROP_UNLOCALIZED_LABEL] = videoTrack.unlocalizedLabel
223
230
  entry[PROP_ENABLED] = videoTrack.enabled
224
- entry[PROP_QUALITIES] = [] // empty: qualities are not being exposed on iOS
225
- //entry[PROP_ACTIVE_QUALITY] = // undefined: qualities are not being exposed on iOS
226
- //entry[PROP_TARGET_QUALITY] = // undefined: qualities are not being exposed on iOS
231
+
232
+ // add known qualities
233
+ entry[PROP_QUALITIES] = (0..<videoTrack.qualities.count).map { index in
234
+ return THEOplayerRCTTrackMetadataAggregator.aggregatedQualityInfo(quality: videoTrack.qualities.get(index))
235
+ }
236
+
237
+ // add active quality
238
+ if let activeQuality = videoTrack.activeQuality {
239
+ entry[PROP_ACTIVE_QUALITY] = THEOplayerRCTTrackMetadataAggregator.aggregatedQualityInfo(quality: activeQuality)
240
+ }
241
+
227
242
  return entry
228
243
  }
229
244
 
@@ -240,6 +255,43 @@ class THEOplayerRCTTrackMetadataAggregator {
240
255
  return 0
241
256
  }
242
257
 
258
+ class func labelFromBandWidth(_ bandWidth: Int) -> String {
259
+ if bandWidth > 1000000 {
260
+ return "\(Double(bandWidth / 1000) / 1000) Mbps"
261
+ } else if bandWidth > 1000 {
262
+ return "\(bandWidth / 1000) kbps"
263
+ } else {
264
+ return "No Label"
265
+ }
266
+ }
267
+
268
+ class func aggregatedQualityInfo(quality: Quality) -> [String:Any] {
269
+ let identifier = String(quality.bandwidth)
270
+ let label = THEOplayerRCTTrackMetadataAggregator.labelFromBandWidth(quality.bandwidth)
271
+
272
+ // common properties
273
+ var entry: [String:Any] = [
274
+ "bandwidth": quality.bandwidth,
275
+ "codecs": "",
276
+ "id": identifier,
277
+ "uid": identifier,
278
+ "name": label,
279
+ "label": label,
280
+ "available": true,
281
+ ]
282
+ if let averageBandwidth = quality.averageBandwidth {
283
+ entry["averageBandwidth"] = averageBandwidth
284
+ }
285
+
286
+ // videoQuality specific properties
287
+ if let videoQuality = quality as? VideoQuality {
288
+ entry["width"] = videoQuality.width
289
+ entry["height"] = videoQuality.height
290
+ }
291
+
292
+ return entry
293
+ }
294
+
243
295
  class func aggregatedMetadataAndChapterTrackInfo(trackDescriptions: [TextTrackDescription], completed: (([[String:Any]]) -> Void)? ) {
244
296
  var trackIndex = 0
245
297
  var tracksInfo: [[String:Any]] = []
@@ -0,0 +1,33 @@
1
+ // THEOplayerRCTView+AppState.swift
2
+
3
+ import Foundation
4
+
5
+ extension THEOplayerRCTView {
6
+ func setupAppStateObservers() {
7
+ NotificationCenter.default.addObserver(
8
+ self,
9
+ selector: #selector(applicationDidEnterBackground),
10
+ name: UIApplication.didEnterBackgroundNotification,
11
+ object: nil
12
+ )
13
+
14
+ NotificationCenter.default.addObserver(
15
+ self,
16
+ selector: #selector(applicationWillEnterForeground),
17
+ name: UIApplication.willEnterForegroundNotification,
18
+ object: nil
19
+ )
20
+ }
21
+
22
+ func clearAppStateObservers() {
23
+ NotificationCenter.default.removeObserver(self)
24
+ }
25
+
26
+ @objc private func applicationDidEnterBackground() {
27
+ self.isApplicationInBackground = true
28
+ }
29
+
30
+ @objc private func applicationWillEnterForeground() {
31
+ self.isApplicationInBackground = false
32
+ }
33
+ }
@@ -21,6 +21,8 @@ import THEOplayerMillicastIntegration
21
21
  import THEOplayerTHEOliveIntegration
22
22
  #endif
23
23
 
24
+ let SAFE_AREA_INSET_OFFSET: CGFloat = 47.0
25
+
24
26
  public class THEOplayerRCTView: UIView {
25
27
  // MARK: Members
26
28
  public private(set) var player: THEOplayer?
@@ -40,6 +42,7 @@ public class THEOplayerRCTView: UIView {
40
42
  var remoteCommandsManager: THEOplayerRCTRemoteCommandsManager
41
43
  var pipManager: THEOplayerRCTPipManager
42
44
  var pipControlsManager: THEOplayerRCTPipControlsManager
45
+ var isApplicationInBackground: Bool = (UIApplication.shared.applicationState == .background)
43
46
 
44
47
  var adsConfig = AdsConfig()
45
48
  var castConfig = CastConfig()
@@ -108,8 +111,9 @@ public class THEOplayerRCTView: UIView {
108
111
  self.remoteCommandsManager = THEOplayerRCTRemoteCommandsManager()
109
112
  self.pipManager = THEOplayerRCTPipManager()
110
113
  self.pipControlsManager = THEOplayerRCTPipControlsManager()
111
-
112
114
  super.init(frame: .zero)
115
+
116
+ self.setupAppStateObservers()
113
117
  }
114
118
 
115
119
  required init?(coder aDecoder: NSCoder) {
@@ -126,6 +130,8 @@ public class THEOplayerRCTView: UIView {
126
130
  }
127
131
 
128
132
  deinit {
133
+ self.clearAppStateObservers()
134
+
129
135
  self.mainEventHandler.destroy()
130
136
  self.textTrackEventHandler.destroy()
131
137
  self.mediaTrackEventHandler.destroy()
@@ -144,9 +150,12 @@ public class THEOplayerRCTView: UIView {
144
150
  self.destroyBackgroundAudio()
145
151
  self.player?.removeAllIntegrations()
146
152
  self.player = nil
153
+
147
154
  if DEBUG_THEOPLAYER_INTERACTION { PrintUtils.printLog(logText: "[NATIVE] THEOplayer instance destroyed.") }
148
155
  }
149
156
 
157
+ // MARK: - View Layout
158
+
150
159
  override public func layoutSubviews() {
151
160
  super.layoutSubviews()
152
161
  if let player = self.player {
@@ -157,6 +166,25 @@ public class THEOplayerRCTView: UIView {
157
166
  }
158
167
  }
159
168
 
169
+ override public var safeAreaInsets: UIEdgeInsets {
170
+ #if os(iOS)
171
+ // When in fullscreen mode, we need to provide some insets
172
+ // to avoid content being obscured by notches or home indicators.
173
+ if self.presentationModeManager.presentationMode == .fullscreen,
174
+ let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
175
+ let orientation = windowScene.interfaceOrientation
176
+ let isPortrait = orientation.rawValue <= 2
177
+ let verticalInset = isPortrait ? SAFE_AREA_INSET_OFFSET: 0.0
178
+ let horizontalInset = isPortrait ? 0.0: SAFE_AREA_INSET_OFFSET
179
+ return UIEdgeInsets.init(top: verticalInset, left: horizontalInset, bottom: verticalInset, right: horizontalInset)
180
+ }
181
+ #endif
182
+ // When inline, the THEOplayerView itself should be possitioned correctly by the customer,
183
+ // taking into account their app's safe areas, so no explicit safe area insets needed.
184
+ // On tvOS no insets required.
185
+ return .zero
186
+ }
187
+
160
188
  // MARK: - Create Player
161
189
 
162
190
  func createPlayer() {