react-native-theoplayer 10.4.0 → 10.5.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 (42) hide show
  1. package/CHANGELOG.md +19 -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/theolive/EndpointAdapter.kt +16 -0
  8. package/ios/THEOplayerRCTDebug.swift +1 -1
  9. package/ios/THEOplayerRCTMediaTrackEventHandler.swift +47 -28
  10. package/ios/THEOplayerRCTPlayerAPI.swift +1 -0
  11. package/ios/THEOplayerRCTSourceDescriptionBuilder.swift +50 -62
  12. package/ios/THEOplayerRCTView+AppState.swift +33 -0
  13. package/ios/THEOplayerRCTView.swift +29 -1
  14. package/ios/backgroundAudio/THEOplayerRCTBackgroundAudioManager.swift +19 -17
  15. package/ios/backgroundAudio/THEOplayerRCTNowPlayingManager.swift +3 -3
  16. package/ios/backgroundAudio/THEOplayerRCTView+BackgroundAudioConfig.swift +8 -0
  17. package/ios/cache/THEOplayerRCTCacheAPI.swift +1 -2
  18. package/ios/pip/THEOplayerRCTPipControlsManager.swift +1 -1
  19. package/ios/pip/THEOplayerRCTPipManager.swift +38 -26
  20. package/ios/theolive/THEOplayerRCTTHEOliveEventAdapter.swift +4 -0
  21. package/lib/commonjs/api/backgroundAudio/BackgroundAudioConfiguration.js.map +1 -1
  22. package/lib/commonjs/internal/adapter/THEOplayerWebAdapter.js +8 -3
  23. package/lib/commonjs/internal/adapter/THEOplayerWebAdapter.js.map +1 -1
  24. package/lib/commonjs/manifest.json +1 -1
  25. package/lib/module/api/backgroundAudio/BackgroundAudioConfiguration.js.map +1 -1
  26. package/lib/module/internal/adapter/THEOplayerWebAdapter.js +8 -3
  27. package/lib/module/internal/adapter/THEOplayerWebAdapter.js.map +1 -1
  28. package/lib/module/manifest.json +1 -1
  29. package/lib/typescript/api/backgroundAudio/BackgroundAudioConfiguration.d.ts +9 -0
  30. package/lib/typescript/api/backgroundAudio/BackgroundAudioConfiguration.d.ts.map +1 -1
  31. package/lib/typescript/api/source/drm/DRMConfiguration.d.ts +10 -0
  32. package/lib/typescript/api/source/drm/DRMConfiguration.d.ts.map +1 -1
  33. package/lib/typescript/api/theolive/TheoLiveEndpoint.d.ts +7 -0
  34. package/lib/typescript/api/theolive/TheoLiveEndpoint.d.ts.map +1 -1
  35. package/lib/typescript/internal/adapter/THEOplayerWebAdapter.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/react-native-theoplayer.podspec +7 -7
  38. package/src/api/backgroundAudio/BackgroundAudioConfiguration.ts +10 -0
  39. package/src/api/source/drm/DRMConfiguration.ts +9 -0
  40. package/src/api/theolive/TheoLiveEndpoint.ts +8 -0
  41. package/src/internal/adapter/THEOplayerWebAdapter.ts +8 -3
  42. package/src/manifest.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ 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.0] - 25-11-24
9
+
10
+ ### Fixed
11
+
12
+ - Fixed an issue on iOS where the scrim of an IMA ad was in a wrong position due to incorrect `safeAreaInsets`.
13
+
14
+ ### Added
15
+
16
+ - Added `stopOnBackground` property to `BackgroundAudioConfiguration` to control whether playback should stop when the app goes to the background.
17
+ - Added `millicastSrc` to `TheoLiveEndpoint` for Web and Android.
18
+ - 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.
19
+
20
+ ### Changed
21
+
22
+ - Updated the active quality info extraction on iOS to use the activeQualityChange event data instead of player API.
23
+
8
24
  ## [10.4.0] - 25-11-13
9
25
 
10
26
  ### Fixed
@@ -15,6 +31,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
15
31
  ### Added
16
32
 
17
33
  - Pass `streamActivityMonitorId` property for `THEOAdDescription` on iOS and Android.
34
+ - Added `allowLivePlayPause` and `seekToLiveOnResume` properties to `PlayerConfiguration.mediaControl` to control pausing and resuming behavior on live streams from the lockscreen controls.
18
35
 
19
36
  ### Changed
20
37
 
@@ -138,11 +155,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
138
155
 
139
156
  - Deprecated the `BaseSource.integration` property in favor of `TypedSource.type`.
140
157
 
141
- ## Added
158
+ ### Added
142
159
 
143
160
  - Added support for `THEOlive` events.
144
161
 
145
- ## Fixed
162
+ ### Fixed
146
163
 
147
164
  - Fixed an issue on Android where, depending on the project structure, the Maven repository list would be incorrect.
148
165
 
@@ -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
  }
@@ -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,39 @@ 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
+ let activeQuality = event.quality
239
+ let identifier = String(activeQuality.bandwidth)
240
+ let label = self.labelFromBandWidth(activeQuality.bandwidth)
241
+
242
+ var width = player.videoWidth
243
+ var height = player.videoHeight
244
+ if let activeVideoQuality = activeQuality as? VideoQuality {
245
+ width = activeVideoQuality.width
246
+ height = activeVideoQuality.height
247
+ }
248
+
249
+ var quality: [String:Any] = [
250
+ "bandwidth": activeQuality.bandwidth,
251
+ "codecs": "",
252
+ "id": identifier,
253
+ "uid": identifier,
254
+ "name": label,
255
+ "label": label,
256
+ "available": true,
257
+ "width": width,
258
+ "height": height,
259
+ //"frameRate": 0, // not available on iOS SDK
260
+ //"firstFrame": 0 // not available on iOS SDK
261
+ ]
262
+ if let averageBandwidth = activeQuality.averageBandwidth {
263
+ quality["averageBandwidth"] = averageBandwidth
264
+ }
265
+
240
266
  forwardedMediaTrackEvent([
241
267
  "trackUid" : track.uid,
242
268
  "type" : MediaTrackEventType.ACTIVE_QUALITY_CHANGED.rawValue,
243
269
  "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
- ]
270
+ "qualities": [quality]
258
271
  ])
259
272
  }
260
273
  }
@@ -264,23 +277,29 @@ class THEOplayerRCTMediaTrackEventHandler {
264
277
  let player = self.player,
265
278
  let track = self.activeTrack(tracks: player.audioTracks) {
266
279
  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)
280
+ let activeQuality = event.quality
281
+ let identifier = String(activeQuality.bandwidth)
282
+ let label = self.labelFromBandWidth(activeQuality.bandwidth)
283
+
284
+ var quality: [String:Any] = [
285
+ "bandwidth": activeQuality.bandwidth,
286
+ "codecs": "",
287
+ "id": identifier,
288
+ "uid": identifier,
289
+ "name": label,
290
+ "label": label,
291
+ "available": true,
292
+ //"audioSamplingRate": 0 // not available on iOS SDK
293
+ ]
294
+ if let averageBandwidth = activeQuality.averageBandwidth {
295
+ quality["averageBandwidth"] = averageBandwidth
296
+ }
269
297
 
270
298
  forwardedMediaTrackEvent([
271
299
  "trackUid" : track.uid,
272
300
  "type" : MediaTrackEventType.ACTIVE_QUALITY_CHANGED.rawValue,
273
301
  "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
- ]
302
+ "qualities": [quality]
284
303
  ])
285
304
  }
286
305
  }
@@ -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"
@@ -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
+ guard let customIntegrationId = contentProtectionData[SD_PROP_INTEGRATION] as? String else { return nil }
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
 
@@ -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() {
@@ -2,20 +2,13 @@
2
2
 
3
3
  import Foundation
4
4
  import THEOplayerSDK
5
- import AVFAudio
6
5
  import AVKit
7
6
 
8
- struct BackgroundAudioConfig {
9
- var enabled: Bool = false
10
- var shouldResumeAfterInterruption: Bool = false
11
- var audioSessionMode: AVAudioSession.Mode = .moviePlayback
12
- }
13
-
14
7
  class THEOplayerRCTBackgroundAudioManager: NSObject, BackgroundPlaybackDelegate {
15
8
  // MARK: Members
16
9
  private weak var player: THEOplayer?
17
10
  private weak var view: THEOplayerRCTView?
18
-
11
+
19
12
  // MARK: - player setup / breakdown
20
13
  func setPlayer(_ player: THEOplayer, view: THEOplayerRCTView?) {
21
14
  self.player = player
@@ -26,25 +19,34 @@ class THEOplayerRCTBackgroundAudioManager: NSObject, BackgroundPlaybackDelegate
26
19
  func destroy() {
27
20
  self.cancelInterruptionNotifications()
28
21
  }
29
-
22
+
30
23
  // MARK: - logic
31
24
  func shouldContinueAudioPlaybackInBackground() -> Bool {
32
- if let view = self.view {
25
+ if let view = self.view, let player = self.player {
26
+ let stopOnBackground = view.backgroundAudioConfig.stopOnBackground
27
+ let inPip = view.presentationModeManager.presentationMode == .pictureInPicture
28
+
29
+ if stopOnBackground && !inPip {
30
+ if true || DEBUG_THEOPLAYER_INTERACTION { PrintUtils.printLog(logText: "[NATIVE] Moved to background, not in pip and stopOnBackground is enabled => stopping playback") }
31
+ player.stop()
32
+ return false
33
+ }
34
+
33
35
  view.nowPlayingManager.updateNowPlaying()
34
36
  return view.backgroundAudioConfig.enabled
35
37
  }
36
38
  return false
37
39
  }
38
-
40
+
39
41
  func cancelInterruptionNotifications() {
40
42
  NotificationCenter.default.removeObserver(self,
41
43
  name: AVAudioSession.interruptionNotification,
42
44
  object: AVAudioSession.sharedInstance())
43
45
  }
44
-
46
+
45
47
  func updateInterruptionNotifications() {
46
48
  guard let view = self.view else { return }
47
-
49
+
48
50
  // Get the default notification center instance.
49
51
  if view.backgroundAudioConfig.shouldResumeAfterInterruption {
50
52
  NotificationCenter.default.addObserver(self,
@@ -57,10 +59,10 @@ class THEOplayerRCTBackgroundAudioManager: NSObject, BackgroundPlaybackDelegate
57
59
  object: AVAudioSession.sharedInstance())
58
60
  }
59
61
  }
60
-
62
+
61
63
  func updateAVAudioSessionMode() {
62
64
  guard let view = self.view else { return }
63
-
65
+
64
66
  do {
65
67
  THEOplayer.automaticallyManageAudioSession = (view.backgroundAudioConfig.audioSessionMode == .moviePlayback)
66
68
  try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: view.backgroundAudioConfig.audioSessionMode)
@@ -71,14 +73,14 @@ class THEOplayerRCTBackgroundAudioManager: NSObject, BackgroundPlaybackDelegate
71
73
  if DEBUG_INTERRUPTIONS { PrintUtils.printLog(logText: "[NATIVE] Unable to update AVAudioSession mode to \(view.backgroundAudioConfig.audioSessionMode.rawValue): \(error)") }
72
74
  }
73
75
  }
74
-
76
+
75
77
  @objc func handleInterruption(notification: Notification) {
76
78
  guard let userInfo = notification.userInfo,
77
79
  let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
78
80
  let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
79
81
  return
80
82
  }
81
-
83
+
82
84
  // Switch over the interruption type.
83
85
  switch type {
84
86
  case .began:
@@ -22,7 +22,7 @@ class THEOplayerRCTNowPlayingManager {
22
22
  func destroy() {
23
23
  // dettach listeners
24
24
  self.detachListeners()
25
-
25
+
26
26
  // update elapsed time on close
27
27
  if let player = self.player {
28
28
  updateCurrentTime(player.currentTime)
@@ -105,7 +105,7 @@ class THEOplayerRCTNowPlayingManager {
105
105
  self.clearNowPlayingOnInfoCenter()
106
106
  }
107
107
  }
108
-
108
+
109
109
  private func clearNowPlayingOnInfoCenter() {
110
110
  Task { @MainActor in
111
111
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
@@ -271,7 +271,7 @@ class THEOplayerRCTNowPlayingManager {
271
271
  welf.processNowPlayingToInfoCenter()
272
272
  }
273
273
  }
274
-
274
+
275
275
 
276
276
  // RATE_CHANGE
277
277
  self.rateChangeListener = player.addEventListener(type: PlayerEventTypes.RATE_CHANGE) { [weak self, weak player] event in