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.
- package/CHANGELOG.md +19 -2
- package/android/src/main/java/com/theoplayer/ReactTHEOplayerContext.kt +6 -0
- package/android/src/main/java/com/theoplayer/audio/BackgroundAudioConfig.kt +7 -0
- package/android/src/main/java/com/theoplayer/audio/BackgroundAudioConfigAdapter.kt +12 -3
- package/android/src/main/java/com/theoplayer/drm/ContentProtectionAdapter.kt +1 -0
- package/android/src/main/java/com/theoplayer/presentation/PresentationManager.kt +8 -4
- package/android/src/main/java/com/theoplayer/theolive/EndpointAdapter.kt +16 -0
- package/ios/THEOplayerRCTDebug.swift +1 -1
- package/ios/THEOplayerRCTMediaTrackEventHandler.swift +47 -28
- package/ios/THEOplayerRCTPlayerAPI.swift +1 -0
- package/ios/THEOplayerRCTSourceDescriptionBuilder.swift +50 -62
- package/ios/THEOplayerRCTView+AppState.swift +33 -0
- package/ios/THEOplayerRCTView.swift +29 -1
- package/ios/backgroundAudio/THEOplayerRCTBackgroundAudioManager.swift +19 -17
- package/ios/backgroundAudio/THEOplayerRCTNowPlayingManager.swift +3 -3
- package/ios/backgroundAudio/THEOplayerRCTView+BackgroundAudioConfig.swift +8 -0
- package/ios/cache/THEOplayerRCTCacheAPI.swift +1 -2
- package/ios/pip/THEOplayerRCTPipControlsManager.swift +1 -1
- package/ios/pip/THEOplayerRCTPipManager.swift +38 -26
- package/ios/theolive/THEOplayerRCTTHEOliveEventAdapter.swift +4 -0
- package/lib/commonjs/api/backgroundAudio/BackgroundAudioConfiguration.js.map +1 -1
- package/lib/commonjs/internal/adapter/THEOplayerWebAdapter.js +8 -3
- package/lib/commonjs/internal/adapter/THEOplayerWebAdapter.js.map +1 -1
- package/lib/commonjs/manifest.json +1 -1
- package/lib/module/api/backgroundAudio/BackgroundAudioConfiguration.js.map +1 -1
- package/lib/module/internal/adapter/THEOplayerWebAdapter.js +8 -3
- package/lib/module/internal/adapter/THEOplayerWebAdapter.js.map +1 -1
- package/lib/module/manifest.json +1 -1
- package/lib/typescript/api/backgroundAudio/BackgroundAudioConfiguration.d.ts +9 -0
- package/lib/typescript/api/backgroundAudio/BackgroundAudioConfiguration.d.ts.map +1 -1
- package/lib/typescript/api/source/drm/DRMConfiguration.d.ts +10 -0
- package/lib/typescript/api/source/drm/DRMConfiguration.d.ts.map +1 -1
- package/lib/typescript/api/theolive/TheoLiveEndpoint.d.ts +7 -0
- package/lib/typescript/api/theolive/TheoLiveEndpoint.d.ts.map +1 -1
- package/lib/typescript/internal/adapter/THEOplayerWebAdapter.d.ts.map +1 -1
- package/package.json +1 -1
- package/react-native-theoplayer.podspec +7 -7
- package/src/api/backgroundAudio/BackgroundAudioConfiguration.ts +10 -0
- package/src/api/source/drm/DRMConfiguration.ts +9 -0
- package/src/api/theolive/TheoLiveEndpoint.ts +8 -0
- package/src/internal/adapter/THEOplayerWebAdapter.ts +8 -3
- 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
|
-
|
|
158
|
+
### Added
|
|
142
159
|
|
|
143
160
|
- Added support for `THEOlive` events.
|
|
144
161
|
|
|
145
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
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
|
-
//
|
|
328
|
-
if (context?.pip == PresentationModeChangePipContext.CLOSED
|
|
329
|
-
|
|
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
|
|
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
|
|
239
|
-
let
|
|
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
|
|
268
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
344
|
-
- returns: a THEOplayer
|
|
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
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|