react-native-theoplayer 10.7.1 → 10.8.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 (25) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/android/src/main/java/com/theoplayer/PlayerEventEmitter.kt +3 -0
  3. package/android/src/main/java/com/theoplayer/ReactTHEOplayerView.kt +5 -2
  4. package/android/src/main/java/com/theoplayer/ReactTHEOplayerViewManager.kt +3 -1
  5. package/android/src/main/java/com/theoplayer/ads/AdAdapter.kt +4 -2
  6. package/android/src/main/java/com/theoplayer/player/PlayerModule.kt +8 -7
  7. package/android/src/main/java/com/theoplayer/presentation/FullscreenLayoutObserver.kt +3 -25
  8. package/android/src/main/java/com/theoplayer/presentation/PresentationManager.kt +26 -39
  9. package/android/src/main/java/com/theoplayer/source/SourceAdapter.kt +2 -0
  10. package/android/src/main/java/com/theoplayer/util/ViewUtils.kt +67 -0
  11. package/ios/THEOplayerRCTDebug.swift +9 -2
  12. package/ios/THEOplayerRCTMainEventHandler.swift +4 -4
  13. package/ios/THEOplayerRCTPlayerStateBuilder.swift +100 -0
  14. package/ios/THEOplayerRCTSourceDescriptionAggregator.swift +12 -7
  15. package/ios/THEOplayerRCTSourceDescriptionBuilder.swift +6 -4
  16. package/ios/THEOplayerRCTTrackMetadataAggregator.swift +1 -1
  17. package/ios/THEOplayerRCTView+AppState.swift +30 -1
  18. package/ios/THEOplayerRCTView.swift +37 -9
  19. package/ios/ads/THEOplayerRCTSourceDescriptionBuilder+Ads.swift +5 -5
  20. package/ios/backgroundAudio/THEOplayerRCTNowPlayingManager.swift +29 -54
  21. package/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift +36 -18
  22. package/lib/commonjs/manifest.json +1 -1
  23. package/lib/module/manifest.json +1 -1
  24. package/package.json +5 -5
  25. package/src/manifest.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ 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.8.0] - 26-01-22
9
+
10
+ ### Added
11
+
12
+ - Added support on iOS to push the initial state of the player from the iOS bridge to the React native adapter.
13
+
14
+ ### Fixed
15
+
16
+ - Fixed an issue on iOS where the integrationParameters from the contentProtection section of the source were not processed correctly, resulting in failures for DRM connectors that depend on them.
17
+ - Fixed an issue on iOS where the player could crash when terminating the app while the player is backgrounded in fullscreen.
18
+ - Fixed an issue on Android where `player.ads.currentAds` would not return an array of ads due to a native error.
19
+ - Fixed an issue on Android where the player would sometimes have wrong dimensions after transitioning to fullscreen presentation mode.
20
+ - Fixed an issue on Android where the `useEMSG` property was not included when setting a TheoAds source description.
21
+
22
+ ### Added
23
+
24
+ - Added RNRepo support for Android to the example app. More info on the official [documentation](https://rnrepo.org/) page.
25
+
8
26
  ## [10.7.1] - 26-01-06
9
27
 
10
28
  ### Added
@@ -291,7 +291,10 @@ class PlayerEventEmitter internal constructor(
291
291
 
292
292
  fun preparePlayer(player: Player) {
293
293
  attachListeners(player)
294
+ emitPlayerReady(player)
295
+ }
294
296
 
297
+ fun emitPlayerReady(player: Player) {
295
298
  val payload = Arguments.createMap()
296
299
  payload.putMap(
297
300
  EVENT_PROP_STATE,
@@ -20,12 +20,15 @@ private const val TAG = "ReactTHEOplayerView"
20
20
  class ReactTHEOplayerView(private val reactContext: ThemedReactContext) :
21
21
  FrameLayout(reactContext), LifecycleEventListener {
22
22
 
23
- private val eventEmitter: PlayerEventEmitter =
23
+ // Set true to bypass ReactNative lifecycle, avoiding THEOplayerView being disposed by Fabric
24
+ var bypassDropInstanceOnReactLifecycle = false
25
+ val eventEmitter: PlayerEventEmitter =
24
26
  PlayerEventEmitter(reactContext.reactApplicationContext, this)
25
27
  val broadcast = EventBroadcastAdapter(this)
26
28
  var presentationManager: PresentationManager? = null
27
29
  var playerContext: ReactTHEOplayerContext? = null
28
- private var isInitialized: Boolean = false
30
+ var isInitialized: Boolean = false
31
+ private set
29
32
  private var config: PlayerConfigAdapter? = null
30
33
 
31
34
  val adsApi: AdsApiWrapper
@@ -23,7 +23,9 @@ class ReactTHEOplayerViewManager : ViewGroupManager<ReactTHEOplayerView>() {
23
23
  * {@link ViewManager} subclass.
24
24
  */
25
25
  override fun onDropViewInstance(view: ReactTHEOplayerView) {
26
- view.releasePlayer()
26
+ if (!view.bypassDropInstanceOnReactLifecycle) {
27
+ view.releasePlayer()
28
+ }
27
29
  }
28
30
 
29
31
  @ReactProp(name = PROP_CONFIG)
@@ -56,10 +56,12 @@ object AdAdapter {
56
56
  /**
57
57
  * Convert a list of native Ads to a ReactNative Ads.
58
58
  */
59
- fun fromAds(ads: List<Ad>): WritableArray {
59
+ fun fromAds(ads: List<Ad?>): WritableArray {
60
60
  val payload = Arguments.createArray()
61
61
  for (ad in ads) {
62
- payload.pushMap(fromAd(ad, true))
62
+ if (ad !== null) {
63
+ payload.pushMap(fromAd(ad, true))
64
+ }
63
65
  }
64
66
  return payload
65
67
  }
@@ -1,6 +1,7 @@
1
1
  package com.theoplayer.player
2
2
 
3
3
  import android.os.Build
4
+ import android.util.Log
4
5
  import com.facebook.react.bridge.*
5
6
  import com.facebook.react.module.annotations.ReactModule
6
7
  import com.facebook.react.module.model.ReactModuleInfo
@@ -18,6 +19,8 @@ import com.theoplayer.audio.BackgroundAudioConfigAdapter
18
19
  import com.theoplayer.presentation.PipConfigAdapter
19
20
  import com.theoplayer.track.TextTrackStyleAdapter
20
21
  import com.theoplayer.util.ViewResolver
22
+ import com.theoplayer.util.findReactRootView
23
+ import com.theoplayer.util.getClosestParentOfType
21
24
 
22
25
  @Suppress("unused")
23
26
  @ReactModule(name = PlayerModule.NAME)
@@ -287,13 +290,11 @@ class PlayerModule(context: ReactApplicationContext) : ReactContextBaseJavaModul
287
290
  @ReactMethod(isBlockingSynchronousMethod = true)
288
291
  fun getUsableScreenDimensions(): WritableMap {
289
292
  // Pass the dimensions of the top-most View in the view hierarchy.
290
- val topView = reactApplicationContext.currentActivity?.window?.decorView?.rootView
291
- reactApplicationContext.resources.displayMetrics.also {
292
- val density = it.density
293
- return Arguments.createMap().apply {
294
- putDouble("width", (topView?.width ?: 0) / density.toDouble())
295
- putDouble("height", (topView?.height ?: 0) / density.toDouble())
296
- }
293
+ val topView = findReactRootView( reactApplicationContext)
294
+ val density = reactApplicationContext.resources.displayMetrics.density.toDouble()
295
+ return Arguments.createMap().apply {
296
+ putDouble("width", (topView?.width ?: 0) / density)
297
+ putDouble("height", (topView?.height ?: 0) / density)
297
298
  }
298
299
  }
299
300
  }
@@ -2,11 +2,11 @@ package com.theoplayer.presentation
2
2
 
3
3
  import android.util.Log
4
4
  import android.view.View
5
- import android.view.ViewGroup
6
5
  import android.view.ViewTreeObserver
7
- import androidx.core.view.children
6
+ import com.facebook.react.ReactRootView
8
7
  import com.facebook.react.views.view.ReactViewGroup
9
8
  import com.theoplayer.ReactTHEOplayerView
9
+ import com.theoplayer.util.applyOnViewTree
10
10
 
11
11
  private const val TAG = "FSLayoutObserver"
12
12
 
@@ -20,14 +20,13 @@ class FullScreenLayoutObserver {
20
20
  private var globalLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
21
21
  private var attached: ReactViewGroup? = null
22
22
 
23
- fun attach(viewGroup: ReactViewGroup?) {
23
+ fun attach(viewGroup: ReactViewGroup?, root: ReactRootView) {
24
24
  if (attached != null) {
25
25
  Log.w(TAG, "A previously attached ViewGroup was not properly detached.")
26
26
  }
27
27
 
28
28
  viewGroup?.let { reactPlayerGroup ->
29
29
  globalLayoutListener = ViewTreeObserver.OnGlobalLayoutListener {
30
- val root = getRootViewFrom(reactPlayerGroup)
31
30
  reactPlayerGroup.post {
32
31
  applyOnViewTree(reactPlayerGroup) { view ->
33
32
  if (view == reactPlayerGroup || view is ReactTHEOplayerView) {
@@ -51,24 +50,3 @@ class FullScreenLayoutObserver {
51
50
  globalLayoutListener = null
52
51
  }
53
52
  }
54
-
55
- /**
56
- * Find the view root, most likely the decorView
57
- */
58
- fun getRootViewFrom(view: View): View {
59
- var current = view
60
- while (current.parent is View) {
61
- current = current.parent as View
62
- }
63
- return current
64
- }
65
-
66
- /**
67
- * Conditionally apply an operation on each view in a hierarchy.
68
- */
69
- fun applyOnViewTree(view: View, doOp: (View) -> Unit) {
70
- doOp(view)
71
- if (view is ViewGroup) {
72
- view.children.forEach { ch -> applyOnViewTree(ch, doOp) }
73
- }
74
- }
@@ -10,13 +10,14 @@ import android.content.pm.PackageManager
10
10
  import android.os.Build
11
11
  import android.view.View
12
12
  import android.view.ViewGroup
13
- import android.view.ViewParent
14
13
  import androidx.activity.ComponentActivity
15
14
  import androidx.core.view.WindowInsetsCompat
16
15
  import androidx.core.view.WindowInsetsControllerCompat
17
16
  import androidx.core.view.children
18
17
  import androidx.lifecycle.Lifecycle
19
18
  import com.facebook.react.ReactRootView
19
+ import com.facebook.react.bridge.Arguments
20
+ import com.facebook.react.bridge.ReactContext
20
21
  import com.facebook.react.uimanager.ThemedReactContext
21
22
  import com.facebook.react.views.view.ReactViewGroup
22
23
  import com.theoplayer.BuildConfig
@@ -26,6 +27,8 @@ import com.theoplayer.ReactTHEOplayerView
26
27
  import com.theoplayer.android.api.error.ErrorCode
27
28
  import com.theoplayer.android.api.error.THEOplayerException
28
29
  import com.theoplayer.android.api.player.PresentationMode
30
+ import com.theoplayer.util.findReactRootView
31
+ import com.theoplayer.util.getClosestParentOfType
29
32
 
30
33
  const val IS_TRANSITION_INTO_PIP = "isTransitioningToPip"
31
34
  const val IS_IN_PIP_MODE = "isInPictureInPictureMode"
@@ -71,6 +74,22 @@ class PresentationManager(
71
74
  }
72
75
  }
73
76
  }
77
+
78
+ // Emit dimension updates when layout changes
79
+ // This makes sure a dimension update is sent after fullscreen immersive animations complete.
80
+ rootView?.addOnLayoutChangeListener { view, _, _, _, _, _, _, _, _ ->
81
+ val density = reactContext.resources.displayMetrics.density.toDouble()
82
+ val params = Arguments.createMap().apply {
83
+ putMap("window", Arguments.createMap().apply {
84
+ putDouble("width", (view.width) / density)
85
+ putDouble("height", (view.height) / density)
86
+ })
87
+ }
88
+ reactContext
89
+ .getJSModule(ReactContext.RCTDeviceEventEmitter::class.java)
90
+ .emit("didUpdateDimensions", params)
91
+ }
92
+
74
93
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
75
94
  supportsPip =
76
95
  reactContext.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
@@ -269,12 +288,7 @@ class PresentationManager(
269
288
  get() = viewCtx.playerView.getClosestParentOfType()
270
289
 
271
290
  private val rootView: ReactRootView?
272
- get() {
273
- val activity = reactContext.currentActivity ?: return null
274
- // Try to search in parents and as a fallback option from root to bottom using depth-first order
275
- return reactPlayerGroup?.getClosestParentOfType()
276
- ?: (activity.window.decorView.rootView as? ViewGroup)?.getClosestParentOfType(false)
277
- }
291
+ get() = findReactRootView( reactContext, reactPlayerGroup)
278
292
 
279
293
  private fun reparentPlayerToRoot() {
280
294
  reactPlayerGroup?.let { playerGroup ->
@@ -283,10 +297,12 @@ class PresentationManager(
283
297
 
284
298
  // Re-parent the playerViewGroup to the root node
285
299
  parent.removeView(playerGroup)
286
- rootView?.addView(playerGroup)
300
+ rootView?.let { root ->
301
+ root.addView(playerGroup)
287
302
 
288
- // Attach an observer that overrides the react-native lay-out and forces fullscreen.
289
- fullScreenLayoutObserver.attach(playerGroup)
303
+ // Attach an observer that overrides the react-native lay-out and forces fullscreen.
304
+ fullScreenLayoutObserver.attach(playerGroup, root)
305
+ }
290
306
  }
291
307
  }
292
308
  }
@@ -335,35 +351,6 @@ class PresentationManager(
335
351
  }
336
352
  }
337
353
 
338
- inline fun <reified T : View> ViewGroup.getClosestParentOfType(upward: Boolean = true): T? {
339
- if (upward) {
340
- // Search in the parent views of `this` view up to the root
341
- var parent: ViewParent? = parent
342
- while (parent != null && parent !is T) {
343
- parent = parent.parent
344
- }
345
- return parent as? T
346
- } else {
347
- // Search in the children collection.
348
- val viewStack = ArrayDeque(children.toList())
349
- // Use Stack/LIFO instead of recursion
350
- while (viewStack.isNotEmpty()) {
351
- when (val view = viewStack.removeAt(0)) {
352
- is T -> {
353
- return view
354
- }
355
-
356
- is ViewGroup -> {
357
- // Filling LIFO with all children of the ViewGroup: depth-first order
358
- viewStack.addAll(0, view.children.toList())
359
- }
360
- }
361
- }
362
- // Found nothing
363
- return null
364
- }
365
- }
366
-
367
354
  private class PlayerGroupRestoreOptions {
368
355
  var childIndex: Int? = null
369
356
  var parentNode: ViewGroup? = null
@@ -68,6 +68,7 @@ private const val PROP_CUSTOM_ASSET_KEY = "customAssetKey"
68
68
  private const val PROP_OVERRIDE_LAYOUT = "overrideLayout"
69
69
  private const val PROP_NETWORK_CODE = "networkCode"
70
70
  private const val PROP_USE_ID3 = "useId3"
71
+ private const val PROP_USE_EMSG = "useEMSG"
71
72
  private const val PROP_RETRIEVE_POD_ID_URI = "retrievePodIdURI"
72
73
  private const val PROP_INITIALIZATION_DELAY = "initializationDelay"
73
74
  private const val PROP_SSE_ENDPOINT = "sseEndpoint"
@@ -360,6 +361,7 @@ class SourceAdapter {
360
361
  networkCode = jsonAdDescription.optString(PROP_NETWORK_CODE).takeIf { it.isNotEmpty() },
361
362
  overrideLayout = parseOverrideLayout(jsonAdDescription.optString(PROP_OVERRIDE_LAYOUT)),
362
363
  useId3 = jsonAdDescription.optBoolean(PROP_USE_ID3, false),
364
+ useEMSG = jsonAdDescription.optBoolean(PROP_USE_EMSG, false),
363
365
  retrievePodIdURI = jsonAdDescription.optString(PROP_RETRIEVE_POD_ID_URI).takeIf { it.isNotEmpty() },
364
366
  initializationDelay = jsonAdDescription.optDouble(PROP_INITIALIZATION_DELAY).takeIf { it.isFinite() },
365
367
  sseEndpoint = jsonAdDescription.optString(PROP_SSE_ENDPOINT).takeIf { it.isNotEmpty() },
@@ -0,0 +1,67 @@
1
+ package com.theoplayer.util
2
+
3
+ import android.view.View
4
+ import android.view.ViewGroup
5
+ import android.view.ViewParent
6
+ import androidx.core.view.children
7
+ import com.facebook.react.ReactRootView
8
+ import com.facebook.react.bridge.ReactContext
9
+ import com.facebook.react.views.view.ReactViewGroup
10
+ import kotlin.sequences.forEach
11
+
12
+ /**
13
+ * Conditionally apply an operation on each view in a hierarchy.
14
+ */
15
+ fun applyOnViewTree(view: View, doOp: (View) -> Unit) {
16
+ doOp(view)
17
+ if (view is ViewGroup) {
18
+ view.children.forEach { ch -> applyOnViewTree(ch, doOp) }
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Finds the closest ReactRootView either by searching upwards from the given ReactViewGroup
24
+ * or, if that fails, by searching downwards from the root view of the current activity.
25
+ */
26
+ fun findReactRootView(reactContext: ReactContext, reactPlayerGroup: ReactViewGroup? = null): ReactRootView? {
27
+ val activity = reactContext.currentActivity ?: return null
28
+ // Try to search in parents and as a fallback option from root to bottom using depth-first order
29
+ return reactPlayerGroup?.getClosestParentOfType()
30
+ ?: (activity.window.decorView.rootView as? ViewGroup)?.getClosestParentOfType(false)
31
+ }
32
+
33
+ /**
34
+ * Finds the closest parent view of the specified type [T].
35
+ *
36
+ * @param upward If true, searches upwards in the view hierarchy (towards the root).
37
+ * If false, searches downwards in the view hierarchy (towards the children).
38
+ * @return The closest parent view of type [T], or null if none is found.
39
+ */
40
+ inline fun <reified T : View> ViewGroup.getClosestParentOfType(upward: Boolean = true): T? {
41
+ if (upward) {
42
+ // Search in the parent views of `this` view up to the root
43
+ var parent: ViewParent? = parent
44
+ while (parent != null && parent !is T) {
45
+ parent = parent.parent
46
+ }
47
+ return parent as? T
48
+ } else {
49
+ // Search in the children collection.
50
+ val viewStack = ArrayDeque(children.toList())
51
+ // Use Stack/LIFO instead of recursion
52
+ while (viewStack.isNotEmpty()) {
53
+ when (val view = viewStack.removeAt(0)) {
54
+ is T -> {
55
+ return view
56
+ }
57
+
58
+ is ViewGroup -> {
59
+ // Filling LIFO with all children of the ViewGroup: depth-first order
60
+ viewStack.addAll(0, view.children.toList())
61
+ }
62
+ }
63
+ }
64
+ // Found nothing
65
+ return null
66
+ }
67
+ }
@@ -19,7 +19,7 @@ let DEBUG_CONTENT_PROTECTION_API = DEBUG && false
19
19
  let DEBUG_VIEW = DEBUG && false
20
20
 
21
21
  // Debug flag to monitor correct SourceDescription buildup
22
- let DEBUG_SOURCE_DESCRIPTION_BUIDER = DEBUG && false
22
+ let DEBUG_SOURCE_DESCRIPTION_BUILDER = DEBUG && false
23
23
 
24
24
  // Debug flag to monitor ads API usage
25
25
  let DEBUG_ADS_API = DEBUG && false
@@ -51,5 +51,12 @@ let DEBUG_THEOLIVE_API = DEBUG && false
51
51
  // Debug flag to monitor THEOAds API usage
52
52
  let DEBUG_THEOADS_API = DEBUG && false
53
53
 
54
- // Debug flag to monitor AudioSession interruptions (e.g. phone cll)
54
+ // Debug flag to monitor AudioSession interruptions (e.g. phone call)
55
55
  let DEBUG_INTERRUPTIONS = DEBUG && false
56
+
57
+ // Debug flag to monitor Presentationmode changes
58
+ let DEBUG_PRESENTATIONMODES = DEBUG && false
59
+
60
+ // Debug flag to monitor App state changes
61
+ let DEBUG_APPSTATE = DEBUG && false
62
+
@@ -7,7 +7,7 @@ public class THEOplayerRCTMainEventHandler {
7
7
  // MARK: Members
8
8
  private weak var player: THEOplayer?
9
9
  private weak var presentationModeContext: THEOplayerRCTPresentationModeContext?
10
- private var loadedMetadataAndChapterTracksInfo: [[String:Any]] = []
10
+ private(set) var loadedMetadataAndChapterTracksInfo: [[String:Any]] = []
11
11
 
12
12
  // MARK: Events
13
13
  var onNativePlay: RCTDirectEventBlock?
@@ -274,9 +274,9 @@ public class THEOplayerRCTMainEventHandler {
274
274
  self.loadedMetadataListener = player.addEventListener(type: PlayerEventTypes.LOADED_META_DATA) { [weak self, weak player] event in
275
275
  if DEBUG_THEOPLAYER_EVENTS { PrintUtils.printLog(logText: "[NATIVE] Received LOADED_META_DATA event from THEOplayer") }
276
276
  if let wplayer = player,
277
- let welf = self,
278
- let forwardedLoadedMetadataEvent = self?.onNativeLoadedMetadata {
279
- let metadata = THEOplayerRCTTrackMetadataAggregator.aggregateTrackMetadata(player: wplayer, metadataTracksInfo: welf.loadedMetadataAndChapterTracksInfo)
277
+ let self,
278
+ let forwardedLoadedMetadataEvent = self.onNativeLoadedMetadata {
279
+ let metadata = THEOplayerRCTTrackMetadataAggregator.aggregateTrackInfo(player: wplayer, metadataTracksInfo: self.loadedMetadataAndChapterTracksInfo)
280
280
  forwardedLoadedMetadataEvent(metadata)
281
281
  }
282
282
  }
@@ -0,0 +1,100 @@
1
+ import Foundation
2
+ import THEOplayerSDK
3
+
4
+ let STATE_SOURCE = "source"
5
+ let STATE_CURRENT_TIME = "currentTime"
6
+ let STATE_CURRENT_PROGRAM_DATE_TIME = "currentProgramDateTime"
7
+ let STATE_PAUSED = "paused"
8
+ let STATE_PLAYBACK_RATE = "playbackRate"
9
+ let STATE_DURATION = "duration"
10
+ let STATE_VOLUME = "volume"
11
+ let STATE_MUTED = "muted"
12
+ let STATE_SEEKABLE = "seekable"
13
+ let STATE_BUFFERED = "buffered"
14
+ let STATE_START = "start"
15
+ let STATE_END = "end"
16
+
17
+ class THEOplayerRCTPlayerStateBuilder {
18
+ private var playerState: [String: Any] = [:]
19
+
20
+ func build() -> [String: Any] {
21
+ return playerState
22
+ }
23
+
24
+ func source(_ sourceDescription: SourceDescription?) -> Self {
25
+ if let sourceDesc = sourceDescription {
26
+ playerState[STATE_SOURCE] = THEOplayerRCTSourceDescriptionAggregator.aggregateSourceDescription(sourceDescription: sourceDesc)
27
+ }
28
+ return self
29
+ }
30
+
31
+ func currentTime(_ timeInSec: Double) -> Self {
32
+ playerState[STATE_CURRENT_TIME] = timeInSec * 1000 // sec -> msec
33
+ return self
34
+ }
35
+
36
+ func currentProgramDateTime(_ programDataTime: Date?) -> Self {
37
+ if let date = programDataTime {
38
+ playerState[STATE_CURRENT_PROGRAM_DATE_TIME] = date.timeIntervalSince1970 * 1000 // sec -> msec
39
+ }
40
+ return self
41
+ }
42
+
43
+ func paused(_ paused: Bool) -> Self {
44
+ playerState[STATE_PAUSED] = paused
45
+ return self
46
+ }
47
+
48
+ func playbackRate(_ rate: Double) -> Self {
49
+ playerState[STATE_PLAYBACK_RATE] = rate
50
+ return self
51
+ }
52
+
53
+ func duration(_ durationInSec: Double?) -> Self {
54
+ if let durationInSec = durationInSec {
55
+ playerState[PROP_DURATION] = THEOplayerRCTTypeUtils.encodeInfNan(durationInSec * 1000) // sec -> msec
56
+ }
57
+ return self
58
+ }
59
+
60
+ func volume(_ volume: Float) -> Self {
61
+ playerState[STATE_VOLUME] = Double(volume)
62
+ return self
63
+ }
64
+
65
+ func muted(_ muted: Bool) -> Self {
66
+ playerState[STATE_MUTED] = muted
67
+ return self
68
+ }
69
+
70
+ func seekable(_ seekableRanges: [THEOplayerSDK.TimeRange]?) -> Self {
71
+ playerState[STATE_SEEKABLE] = fromTimeRanges(seekableRanges)
72
+ return self
73
+ }
74
+
75
+ func buffered(_ bufferedRanges: [THEOplayerSDK.TimeRange]?) -> Self {
76
+ playerState[STATE_BUFFERED] = fromTimeRanges(bufferedRanges)
77
+ return self
78
+ }
79
+
80
+ func trackInfo(_ trackInfo: [String: Any]) -> Self {
81
+ playerState.merge(trackInfo) { (current, _) in current }
82
+ return self
83
+ }
84
+
85
+ private func fromTimeRanges(_ ranges: [THEOplayerSDK.TimeRange]?) -> [[String: Double]] {
86
+ guard let inRanges = ranges else { return [] }
87
+
88
+ var outRanges: [[String:Double]] = []
89
+ inRanges.forEach({ timeRange in
90
+ outRanges.append(
91
+ [
92
+ STATE_START: timeRange.start * 1000, // sec -> msec
93
+ STATE_END: timeRange.end * 1000 // sec -> msec
94
+ ]
95
+ )
96
+ })
97
+ return outRanges
98
+ }
99
+ }
100
+
@@ -4,24 +4,30 @@ import Foundation
4
4
  import THEOplayerSDK
5
5
  import UIKit
6
6
 
7
- #if os(iOS)
8
7
  class THEOplayerRCTSourceDescriptionAggregator {
9
- class func aggregateCacheTaskSourceDescription(sourceDescription: SourceDescription, cachingTaskId: String) -> [String:Any]? {
8
+ class func aggregateSourceDescription(sourceDescription: SourceDescription) -> [String:Any]? {
10
9
  do {
11
10
  let jsonEncoder = JSONEncoder()
12
11
  let data = try jsonEncoder.encode(sourceDescription)
13
12
  if let result = try? JSONSerialization.jsonObject(with: data, options: []) as? [String:Any] {
14
- let srcDescription = THEOplayerRCTSourceDescriptionAggregator.sanitiseSourceDescriptionMetadata(input: result)
15
- let extendedSrcDescription = THEOplayerRCTSourceDescriptionAggregator.addCachingTaskIdToMetadata(input: srcDescription, cachingTaskId: cachingTaskId)
16
- return extendedSrcDescription
13
+ return THEOplayerRCTSourceDescriptionAggregator.sanitiseSourceDescriptionMetadata(input: result)
17
14
  }
18
15
  } catch {
19
- if DEBUG { PrintUtils.printLog(logText: "[NATIVE] Could not aggregate sourceDescription for caching task: \(error.localizedDescription)")}
16
+ if DEBUG { PrintUtils.printLog(logText: "[NATIVE] Could not aggregate sourceDescription: \(error.localizedDescription)")}
20
17
  return nil
21
18
  }
22
19
  return nil
23
20
  }
24
21
 
22
+ class func aggregateCacheTaskSourceDescription(sourceDescription: SourceDescription, cachingTaskId: String) -> [String:Any]? {
23
+ if let result = THEOplayerRCTSourceDescriptionAggregator.aggregateSourceDescription(sourceDescription: sourceDescription) {
24
+ let srcDescription = THEOplayerRCTSourceDescriptionAggregator.sanitiseSourceDescriptionMetadata(input: result)
25
+ let extendedSrcDescription = THEOplayerRCTSourceDescriptionAggregator.addCachingTaskIdToMetadata(input: srcDescription, cachingTaskId: cachingTaskId)
26
+ return extendedSrcDescription
27
+ }
28
+ return nil
29
+ }
30
+
25
31
  private class func sanitiseSourceDescriptionMetadata(input: [String:Any]) -> [String:Any] {
26
32
  var output: [String:Any] = input
27
33
  if let metadata = output[SD_PROP_METADATA] as? [String:Any],
@@ -53,4 +59,3 @@ class THEOplayerRCTSourceDescriptionAggregator {
53
59
  return output
54
60
  }
55
61
  }
56
- #endif
@@ -111,7 +111,7 @@ class THEOplayerRCTSourceDescriptionBuilder {
111
111
  if let typedSource = THEOplayerRCTSourceDescriptionBuilder.buildTypedSource(typedSourceData) {
112
112
  typedSources.append(typedSource)
113
113
  } else {
114
- if DEBUG_SOURCE_DESCRIPTION_BUIDER {
114
+ if DEBUG_SOURCE_DESCRIPTION_BUILDER {
115
115
  PrintUtils.printLog(logText: "[NATIVE] Could not create THEOplayer TypedSource from sourceData array")
116
116
  }
117
117
  return (nil, nil)
@@ -123,7 +123,7 @@ class THEOplayerRCTSourceDescriptionBuilder {
123
123
  if let typedSource = THEOplayerRCTSourceDescriptionBuilder.buildTypedSource(typedSourceData) {
124
124
  typedSources.append(typedSource)
125
125
  } else {
126
- if DEBUG_SOURCE_DESCRIPTION_BUIDER {
126
+ if DEBUG_SOURCE_DESCRIPTION_BUILDER {
127
127
  PrintUtils.printLog(logText: "[NATIVE] Could not create THEOplayer TypedSource from sourceData")
128
128
  }
129
129
  return (nil, nil)
@@ -147,7 +147,7 @@ class THEOplayerRCTSourceDescriptionBuilder {
147
147
  textTrackDescriptions?.append(textTrackDescription)
148
148
  }
149
149
  } else {
150
- if DEBUG_SOURCE_DESCRIPTION_BUIDER {
150
+ if DEBUG_SOURCE_DESCRIPTION_BUILDER {
151
151
  PrintUtils.printLog(logText: "[NATIVE] Could not create THEOplayer TextTrackDescription from textTrackData array")
152
152
  }
153
153
  return (nil, nil)
@@ -220,7 +220,7 @@ class THEOplayerRCTSourceDescriptionBuilder {
220
220
  return daiSource
221
221
  }
222
222
 
223
- if DEBUG_SOURCE_DESCRIPTION_BUIDER {
223
+ if DEBUG_SOURCE_DESCRIPTION_BUILDER {
224
224
  PrintUtils.printLog(logText: "[NATIVE] THEOplayer TypedSource requires 'src' property in 'sources' description")
225
225
  }
226
226
  return nil
@@ -392,9 +392,11 @@ class THEOplayerRCTSourceDescriptionBuilder {
392
392
 
393
393
  // global query parameters
394
394
  let queryParameters = contentProtectionData[SD_PROP_QUERY_PARAMETERS] as? [String:String]
395
+ let integrationParameters = contentProtectionData[SD_PROP_INTEGRATION_PARAMETERS] as? [String:Any] ?? [:]
395
396
 
396
397
  return MultiplatformDRMConfiguration(
397
398
  customIntegrationId: customIntegrationId,
399
+ integrationParameters: integrationParameters,
398
400
  keySystemConfigurations: KeySystemConfigurationCollection(fairplay: fairplayKeySystem, widevine: widevineKeySystem),
399
401
  queryParameters: queryParameters
400
402
  )
@@ -38,7 +38,7 @@ let PROP_END_ON_NEXT: String = "endOnNext"
38
38
 
39
39
  class THEOplayerRCTTrackMetadataAggregator {
40
40
 
41
- class func aggregateTrackMetadata(player: THEOplayer, metadataTracksInfo: [[String:Any]]) -> [String:Any] {
41
+ class func aggregateTrackInfo(player: THEOplayer, metadataTracksInfo: [[String:Any]]) -> [String:Any] {
42
42
  let textTracks: TextTrackList = player.textTracks
43
43
  let audioTracks: AudioTrackList = player.audioTracks
44
44
  let videoTracks: VideoTrackList = player.videoTracks
@@ -1,6 +1,8 @@
1
1
  // THEOplayerRCTView+AppState.swift
2
2
 
3
3
  import Foundation
4
+ import THEOplayerSDK
5
+ import MediaPlayer
4
6
 
5
7
  extension THEOplayerRCTView {
6
8
  func setupAppStateObservers() {
@@ -17,6 +19,13 @@ extension THEOplayerRCTView {
17
19
  name: UIApplication.willEnterForegroundNotification,
18
20
  object: nil
19
21
  )
22
+
23
+ NotificationCenter.default.addObserver(
24
+ self,
25
+ selector: #selector(appWillTerminate),
26
+ name: UIApplication.willTerminateNotification,
27
+ object: nil
28
+ )
20
29
  }
21
30
 
22
31
  func clearAppStateObservers() {
@@ -24,10 +33,30 @@ extension THEOplayerRCTView {
24
33
  }
25
34
 
26
35
  @objc private func applicationDidEnterBackground() {
36
+ // When player was fullscreen:
37
+ // - it either went to PiP before applicationDidEnterBackground was called. PiP always starts from fullscreen (startsAutomaticallyFromInline applies only to inline setup)
38
+ // - or we push the player back to inline to prevent view index issues on app termination (e.g. when the app is killed from the app switcher while in fullscreen)
39
+ if self.presentationModeManager.presentationMode == .fullscreen {
40
+ if DEBUG_APPSTATE { PrintUtils.printLog(logText: "[NATIVE] presentationMode is fullscreen while backgrounding => back to inline.") }
41
+ self.setPresentationMode(newPresentationMode: PresentationMode.inline)
42
+ }
43
+
27
44
  self.isApplicationInBackground = true
28
45
  }
29
46
 
30
- @objc private func applicationWillEnterForeground() {
47
+ @objc
48
+ private func applicationWillEnterForeground() {
31
49
  self.isApplicationInBackground = false
32
50
  }
51
+
52
+ @objc
53
+ private func appWillTerminate() {
54
+ if DEBUG_APPSTATE { PrintUtils.printLog(logText: "[NATIVE] App will terminate notification received.") }
55
+
56
+ // Clear any now playing info
57
+ if DEBUG_APPSTATE { PrintUtils.printLog(logText: "[NATIVE] Clearing nowPlayingInfo on app termination.") }
58
+ MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
59
+
60
+ if DEBUG_APPSTATE { PrintUtils.printLog(logText: "[NATIVE] Player is prepared for app termination.") }
61
+ }
33
62
  }
@@ -121,10 +121,11 @@ public class THEOplayerRCTView: UIView {
121
121
  }
122
122
 
123
123
  func willUnmount() {
124
+ if DEBUG_PRESENTATIONMODES {PrintUtils.printLog(logText: "[NATIVE] willUnmount with presentationMode: \(self.presentationModeManager.presentationMode._rawValue)")}
125
+
124
126
  // before destruction, make sure the view reparenting is reset when player was put in fullscreen
125
127
  if self.presentationModeManager.presentationMode == .fullscreen {
126
- if DEBUG_THEOPLAYER_INTERACTION {PrintUtils.printLog(logText: "[NATIVE] willUnmount with presentationMode: \(self.presentationModeManager.presentationMode._rawValue)")}
127
-
128
+ if DEBUG_PRESENTATIONMODES { PrintUtils.printLog(logText: "[NATIVE] presentationMode is fullscreen => back to inline for player unmount.") }
128
129
  self.setPresentationMode(newPresentationMode: PresentationMode.inline)
129
130
  }
130
131
  }
@@ -209,16 +210,43 @@ public class THEOplayerRCTView: UIView {
209
210
  }
210
211
  }
211
212
 
212
- private func notifyNativePlayerReady() {
213
+ public func notifyNativePlayerReady() {
213
214
  DispatchQueue.main.async {
214
215
  let versionString = THEOplayer.version
215
216
  if let forwardedNativeReady = self.onNativePlayerReady {
216
- forwardedNativeReady([
217
- "version": [
218
- "version" : versionString,
219
- "playerSuiteVersion": versionString
220
- ],
221
- ])
217
+ var payload: [String: Any] = [:]
218
+
219
+ // pass initial player state
220
+ if let player = self.player {
221
+ // collect stored track metadata
222
+ let trackInfo = THEOplayerRCTTrackMetadataAggregator.aggregateTrackInfo(
223
+ player: player,
224
+ metadataTracksInfo: self.mainEventHandler.loadedMetadataAndChapterTracksInfo
225
+ )
226
+
227
+ // build state
228
+ payload["state"] = THEOplayerRCTPlayerStateBuilder()
229
+ .source(player.source)
230
+ .currentTime(player.currentTime)
231
+ .currentProgramDateTime(player.currentProgramDateTime)
232
+ .paused(player.paused)
233
+ .playbackRate(player.playbackRate)
234
+ .duration(player.duration)
235
+ .volume(player.volume)
236
+ .muted(player.muted)
237
+ .seekable(player.seekable)
238
+ .buffered(player.buffered)
239
+ .trackInfo(trackInfo)
240
+ .build()
241
+ }
242
+
243
+ // pass version onfo
244
+ payload["version"] = [
245
+ "version": versionString,
246
+ "playerSuiteVersion": versionString
247
+ ]
248
+
249
+ forwardedNativeReady(payload)
222
250
  }
223
251
  }
224
252
  }
@@ -23,7 +23,7 @@ extension THEOplayerRCTSourceDescriptionBuilder {
23
23
  if let adDescription = THEOplayerRCTSourceDescriptionBuilder.buildSingleAdDescription(adsData) {
24
24
  adsDescriptions?.append(adDescription)
25
25
  } else {
26
- if DEBUG_SOURCE_DESCRIPTION_BUIDER {
26
+ if DEBUG_SOURCE_DESCRIPTION_BUILDER {
27
27
  PrintUtils.printLog(logText: "[NATIVE] Could not create THEOplayer GoogleImaAdDescription from adsData array")
28
28
  }
29
29
  return nil
@@ -35,7 +35,7 @@ extension THEOplayerRCTSourceDescriptionBuilder {
35
35
  if let adDescription = THEOplayerRCTSourceDescriptionBuilder.buildSingleAdDescription(adsData) {
36
36
  adsDescriptions?.append(adDescription)
37
37
  } else {
38
- if DEBUG_SOURCE_DESCRIPTION_BUIDER {
38
+ if DEBUG_SOURCE_DESCRIPTION_BUILDER {
39
39
  PrintUtils.printLog(logText: "[NATIVE] Could not create THEOplayer GoogleImaAdDescription from adsData")
40
40
  }
41
41
  return nil
@@ -58,7 +58,7 @@ extension THEOplayerRCTSourceDescriptionBuilder {
58
58
  case "theoads":
59
59
  return THEOplayerRCTSourceDescriptionBuilder.buildSingleTHEOadsDescription(adsData)
60
60
  default:
61
- if DEBUG_SOURCE_DESCRIPTION_BUIDER { PrintUtils.printLog(logText: "[NATIVE] We currently require and only support the 'google-ima' or 'sgai' integration in the 'ads' description.") }
61
+ if DEBUG_SOURCE_DESCRIPTION_BUILDER { PrintUtils.printLog(logText: "[NATIVE] We currently require and only support the 'google-ima' or 'sgai' integration in the 'ads' description.") }
62
62
  }
63
63
  }
64
64
  return nil
@@ -81,7 +81,7 @@ extension THEOplayerRCTSourceDescriptionBuilder {
81
81
  if let src = srcString {
82
82
  return GoogleImaAdDescription(src: src, timeOffset: timeOffset)
83
83
  } else {
84
- if DEBUG_SOURCE_DESCRIPTION_BUIDER { PrintUtils.printLog(logText: "[NATIVE] AdDescription requires 'src' property in 'ads' description.") }
84
+ if DEBUG_SOURCE_DESCRIPTION_BUILDER { PrintUtils.printLog(logText: "[NATIVE] AdDescription requires 'src' property in 'ads' description.") }
85
85
  }
86
86
  #endif
87
87
  return nil
@@ -120,7 +120,7 @@ extension THEOplayerRCTSourceDescriptionBuilder {
120
120
  adTagParameters: adTagParameters)
121
121
  }
122
122
  default:
123
- if DEBUG_SOURCE_DESCRIPTION_BUIDER {
123
+ if DEBUG_SOURCE_DESCRIPTION_BUILDER {
124
124
  PrintUtils.printLog(logText: "[NATIVE] THEOplayer ssai 'availabilityType' must be 'live' or 'vod'")
125
125
  }
126
126
  return nil
@@ -16,7 +16,6 @@ class THEOplayerRCTNowPlayingManager {
16
16
  private var rateChangeListener: EventListener?
17
17
  private var seekedListener: EventListener?
18
18
  private var sourceChangeListener: EventListener?
19
- private var appTerminationObserver: Any?
20
19
 
21
20
  // MARK: - destruction
22
21
  func destroy() {
@@ -33,7 +32,7 @@ class THEOplayerRCTNowPlayingManager {
33
32
  self.nowPlayingInfo = [:]
34
33
  self.clearNowPlayingOnInfoCenter()
35
34
 
36
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] Destroy, nowPlayingInfo cleared on infoCenter.") }
35
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] Destroy, nowPlayingInfo cleared on infoCenter.") }
37
36
  }
38
37
 
39
38
  // MARK: - player setup / breakdown
@@ -47,16 +46,16 @@ class THEOplayerRCTNowPlayingManager {
47
46
  func printCurrentNowPlayingInfo() {
48
47
  Task { @MainActor [weak self] in
49
48
  if let info = MPNowPlayingInfoCenter.default().nowPlayingInfo {
50
- PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO CURRENT INFO] MPNowPlayingInfoCenter.default().nowPlayingInfo = ")
49
+ PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO CURRENT INFO] MPNowPlayingInfoCenter.default().nowPlayingInfo = ")
51
50
  info.forEach { (key: String, value: Any) in
52
- PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO CURRENT INFO] -> \(key): \(value)")
51
+ PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO CURRENT INFO] -> \(key): \(value)")
53
52
  }
54
53
  if let player = self?.player {
55
- PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO CURRENT INFO] playerInfo = ")
56
- PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO CURRENT INFO] -> currentTime: \(player.currentTime)")
57
- PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO CURRENT INFO] -> playbackRate: \(player.playbackRate)")
58
- PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO CURRENT INFO] -> paused: \(player.paused)")
59
- PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO CURRENT INFO] -> duration: \(player.duration ?? -1)")
54
+ PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO CURRENT INFO] playerInfo = ")
55
+ PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO CURRENT INFO] -> currentTime: \(player.currentTime)")
56
+ PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO CURRENT INFO] -> playbackRate: \(player.playbackRate)")
57
+ PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO CURRENT INFO] -> paused: \(player.paused)")
58
+ PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO CURRENT INFO] -> duration: \(player.duration ?? -1)")
60
59
  }
61
60
  }
62
61
  }
@@ -95,7 +94,7 @@ class THEOplayerRCTNowPlayingManager {
95
94
  if !nowPlayingInfo.isEmpty {
96
95
  Task { @MainActor in
97
96
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
98
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] nowPlayingInfo processed to infoCenter.") }
97
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] nowPlayingInfo processed to infoCenter.") }
99
98
 
100
99
  if DEBUG_NOWINFO {
101
100
  self.printCurrentNowPlayingInfo()
@@ -109,7 +108,7 @@ class THEOplayerRCTNowPlayingManager {
109
108
  private func clearNowPlayingOnInfoCenter() {
110
109
  Task { @MainActor in
111
110
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
112
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] clearing nowPlayingInfo (to nil) on infoCenter.") }
111
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] clearing nowPlayingInfo (to nil) on infoCenter.") }
113
112
 
114
113
  if DEBUG_NOWINFO {
115
114
  self.printCurrentNowPlayingInfo()
@@ -132,48 +131,48 @@ class THEOplayerRCTNowPlayingManager {
132
131
  private func updateTitle(_ metadataTitle: String?) {
133
132
  if let title = metadataTitle {
134
133
  self.nowPlayingInfo[MPMediaItemPropertyTitle] = title
135
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] title [\(title)] stored in nowPlayingInfo.") }
134
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] title [\(title)] stored in nowPlayingInfo.") }
136
135
  }
137
136
  }
138
137
 
139
138
  private func updateArtist(_ metadataArtist: String?) {
140
139
  if let artist = metadataArtist {
141
140
  self.nowPlayingInfo[MPMediaItemPropertyArtist] = artist
142
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] artist [\(artist)] stored in nowPlayingInfo.") }
141
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] artist [\(artist)] stored in nowPlayingInfo.") }
143
142
  }
144
143
  }
145
144
 
146
145
  private func updateAlbum(_ metadataAlbum: String?) {
147
146
  if let album = metadataAlbum {
148
147
  self.nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = album
149
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] album [\(album)] stored in nowPlayingInfo.") }
148
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] album [\(album)] stored in nowPlayingInfo.") }
150
149
  }
151
150
  }
152
151
 
153
152
  private func updateSubtitle(_ metadataSubtitle: String?) {
154
153
  if let subtitle = metadataSubtitle {
155
154
  self.nowPlayingInfo[MPMediaItemPropertyArtist] = subtitle
156
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] subtitle [\(subtitle)] stored in nowPlayingInfo.") }
155
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] subtitle [\(subtitle)] stored in nowPlayingInfo.") }
157
156
  }
158
157
  }
159
158
 
160
159
  private func updateServiceIdentifier(_ serviceId: String?) {
161
160
  if let serviceId = serviceId {
162
161
  self.nowPlayingInfo[MPNowPlayingInfoPropertyServiceIdentifier] = serviceId
163
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] serviceId [\(serviceId)] stored in nowPlayingInfo.") }
162
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] serviceId [\(serviceId)] stored in nowPlayingInfo.") }
164
163
  }
165
164
  }
166
165
 
167
166
  private func updateContentIdentifier(_ contentId: String?) {
168
167
  if let contentId = contentId {
169
168
  self.nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] = contentId
170
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] contentId [\(contentId)] stored in nowPlayingInfo.") }
169
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] contentId [\(contentId)] stored in nowPlayingInfo.") }
171
170
  }
172
171
  }
173
172
 
174
173
  private func updateMediaType() {
175
174
  self.nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = NSNumber(value: 2)
176
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] mediaType [hardcoded 2, for video] stored in nowPlayingInfo.") }
175
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] mediaType [hardcoded 2, for video] stored in nowPlayingInfo.") }
177
176
  }
178
177
 
179
178
  private func updateArtWork(_ urlString: String?, completion: (() -> Void)?) {
@@ -185,9 +184,9 @@ class THEOplayerRCTNowPlayingManager {
185
184
  self?.nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: displayIcon.size) { size in
186
185
  return displayIcon
187
186
  }
188
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] Artwork stored in nowPlayingInfo.") }
187
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] Artwork stored in nowPlayingInfo.") }
189
188
  } else {
190
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] Failed to store artwork in nowPlayingInfo.") }
189
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] Failed to store artwork in nowPlayingInfo.") }
191
190
  }
192
191
  completion?()
193
192
  }
@@ -199,49 +198,34 @@ class THEOplayerRCTNowPlayingManager {
199
198
 
200
199
  private func updatePlaybackRate(_ playerPlaybackRate: Double) {
201
200
  self.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = NSNumber(value: playerPlaybackRate)
202
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] playbackrate [\(playerPlaybackRate)] stored in nowPlayingInfo.") }
201
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] playbackrate [\(playerPlaybackRate)] stored in nowPlayingInfo.") }
203
202
  }
204
203
 
205
204
  private func updateCurrentTime(_ currentTime: Double) {
206
205
  self.nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = NSNumber(value: currentTime)
207
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] currentTime [\(currentTime)] stored in nowPlayingInfo.") }
206
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] currentTime [\(currentTime)] stored in nowPlayingInfo.") }
208
207
  }
209
208
 
210
209
  private func updateDuration(_ duration: Double?) {
211
210
  if let duration = duration {
212
211
  let isLiveStream = duration.isInfinite
213
212
  self.nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = isLiveStream
214
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] isLiveStream [\(isLiveStream)] stored in nowPlayingInfo.") }
213
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] isLiveStream [\(isLiveStream)] stored in nowPlayingInfo.") }
215
214
  if !isLiveStream {
216
215
  self.nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = NSNumber(value: duration)
217
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] duration [\(duration)] stored in nowPlayingInfo.") }
216
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO] duration [\(duration)] stored in nowPlayingInfo.") }
218
217
  }
219
218
  }
220
219
  }
221
220
 
222
- @objc
223
- private func appWillTerminate() {
224
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] App will terminate notification received.") }
225
- MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
226
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO] nowPlayingInfo cleared for app termination.") }
227
- }
228
-
229
221
  private func attachListeners() {
230
222
  guard let player = self.player else {
231
223
  return
232
224
  }
233
225
 
234
- // When app terminates, clear the nowPlayingInfo.
235
- self.appTerminationObserver = NotificationCenter.default.addObserver(
236
- self,
237
- selector: #selector(appWillTerminate),
238
- name: UIApplication.willTerminateNotification,
239
- object: nil
240
- )
241
-
242
226
  // DURATION_CHANGE
243
227
  self.durationChangeListener = player.addEventListener(type: PlayerEventTypes.DURATION_CHANGE) { [weak self, weak player] event in
244
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO-EVENT] DURATION_CHANGE") }
228
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO-EVENT] DURATION_CHANGE") }
245
229
  if let welf = self,
246
230
  let wplayer = player,
247
231
  let duration = wplayer.duration {
@@ -252,7 +236,7 @@ class THEOplayerRCTNowPlayingManager {
252
236
 
253
237
  // PLAYING
254
238
  self.playingListener = player.addEventListener(type: PlayerEventTypes.PLAYING) { [weak self, weak player] event in
255
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO-EVENT] PLAYING") }
239
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO-EVENT] PLAYING") }
256
240
  if let welf = self,
257
241
  let wplayer = player {
258
242
  welf.updatePlaybackRate(wplayer.playbackRate)
@@ -263,7 +247,7 @@ class THEOplayerRCTNowPlayingManager {
263
247
 
264
248
  // PAUSE
265
249
  self.pauseListener = player.addEventListener(type: PlayerEventTypes.PAUSE) { [weak self, weak player] event in
266
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO-EVENT] PAUSE") }
250
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO-EVENT] PAUSE") }
267
251
  if let welf = self,
268
252
  let wplayer = player {
269
253
  welf.updatePlaybackRate(0)
@@ -275,7 +259,7 @@ class THEOplayerRCTNowPlayingManager {
275
259
 
276
260
  // RATE_CHANGE
277
261
  self.rateChangeListener = player.addEventListener(type: PlayerEventTypes.RATE_CHANGE) { [weak self, weak player] event in
278
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO-EVENT] RATE_CHANGE") }
262
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO-EVENT] RATE_CHANGE") }
279
263
  if let welf = self,
280
264
  let wplayer = player {
281
265
  welf.updatePlaybackRate(wplayer.playbackRate)
@@ -286,7 +270,7 @@ class THEOplayerRCTNowPlayingManager {
286
270
 
287
271
  // SEEKED
288
272
  self.seekedListener = player.addEventListener(type: PlayerEventTypes.SEEKED) { [weak self, weak player] event in
289
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO-EVENT] SEEKED") }
273
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO-EVENT] SEEKED") }
290
274
  if let welf = self,
291
275
  let wplayer = player {
292
276
  welf.updatePlaybackRate(wplayer.playbackRate)
@@ -297,7 +281,7 @@ class THEOplayerRCTNowPlayingManager {
297
281
 
298
282
  // SOURCE_CHANGE
299
283
  self.sourceChangeListener = player.addEventListener(type: PlayerEventTypes.SOURCE_CHANGE) { [weak self] event in
300
- if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE-NOWPLAYINGINFO-EVENT] SOURCE_CHANGE \(event.source == nil ? "to nil" : "")") }
284
+ if DEBUG_NOWINFO { PrintUtils.printLog(logText: "[NATIVE][NOWPLAYINGINFO-EVENT] SOURCE_CHANGE \(event.source == nil ? "to nil" : "")") }
301
285
  self?.updateNowPlaying()
302
286
  }
303
287
  }
@@ -307,15 +291,6 @@ class THEOplayerRCTNowPlayingManager {
307
291
  return
308
292
  }
309
293
 
310
- // When app terminates, clear the nowPlayingInfo.
311
- if let appTerminationObserver = self.appTerminationObserver {
312
- NotificationCenter.default.removeObserver(
313
- appTerminationObserver,
314
- name: UIApplication.willTerminateNotification,
315
- object: nil)
316
- self.appTerminationObserver = nil
317
- }
318
-
319
294
  // DURATION_CHANGE
320
295
  if let durationChangeListener = self.durationChangeListener {
321
296
  player.removeEventListener(type: PlayerEventTypes.DURATION_CHANGE, listener: durationChangeListener)
@@ -11,12 +11,12 @@ public class THEOplayerRCTPresentationModeManager {
11
11
  var presentationModeContext = THEOplayerRCTPresentationModeContext()
12
12
  private(set) var presentationMode: THEOplayerSDK.PresentationMode = .inline
13
13
  private var rnInlineMode: THEOplayerSDK.PresentationMode = .inline // while native player is inline, RN player can be inline or fullsceen
14
-
15
-
14
+
15
+
16
16
  private weak var containerView: UIView? // view containing the playerView and it's siblings (e.g. UI)
17
17
  private weak var inlineParentView: UIView? // target view for inline representation
18
18
  private var inlineParentViewIndex: Int?
19
-
19
+
20
20
  // MARK: Events
21
21
  var onNativePresentationModeChange: RCTDirectEventBlock?
22
22
 
@@ -58,7 +58,7 @@ public class THEOplayerRCTPresentationModeManager {
58
58
  }
59
59
  return viewControllers
60
60
  }
61
-
61
+
62
62
  /**
63
63
  * Moves a view to a new parent.
64
64
  * The view is inserted at the provided targetViewIndex or else at the front.
@@ -70,16 +70,31 @@ public class THEOplayerRCTPresentationModeManager {
70
70
  movedVC.removeFromParent()
71
71
  }
72
72
 
73
+ let parentView = movingView.superview
74
+ if DEBUG_PRESENTATIONMODES { PrintUtils.printLog(logText: "[NATIVE] Before moving:") }
75
+ if DEBUG_PRESENTATIONMODES { PrintUtils.printLog(logText: "[NATIVE] parentView:") }
76
+ self.logSubviews(of: parentView)
77
+ if DEBUG_PRESENTATIONMODES { PrintUtils.printLog(logText: "[NATIVE] targetView:") }
78
+ self.logSubviews(of: targetView)
79
+
73
80
  // move the actual view
74
81
  movingView.removeFromSuperview()
75
82
  if let viewIndex = targetViewIndex {
76
83
  let safeIndex = min(viewIndex, targetView.subviews.count)
84
+ if DEBUG_PRESENTATIONMODES { PrintUtils.printLog(logText: "[NATIVE] moveView: insertSubview at safeIndex \(safeIndex) (for targetViewIndex \(targetViewIndex ?? -1)) on targetView with \(targetView.subviews.count) subviews.") }
77
85
  targetView.insertSubview(movingView, at: safeIndex)
78
86
  } else {
87
+ if DEBUG_PRESENTATIONMODES { PrintUtils.printLog(logText: "[NATIVE] moveView: addSubview on targetView with \(targetView.subviews.count) subviews.") }
79
88
  targetView.addSubview(movingView)
80
89
  targetView.bringSubviewToFront(movingView)
81
90
  }
82
91
 
92
+ if DEBUG_PRESENTATIONMODES { PrintUtils.printLog(logText: "[NATIVE] After moving:") }
93
+ if DEBUG_PRESENTATIONMODES { PrintUtils.printLog(logText: "[NATIVE] updated parentView:") }
94
+ self.logSubviews(of: parentView)
95
+ if DEBUG_PRESENTATIONMODES { PrintUtils.printLog(logText: "[NATIVE] updated targetView:") }
96
+ self.logSubviews(of: targetView)
97
+
83
98
  // attach the moving viewControllers to their new parent
84
99
  if let targetViewController = targetView.findViewController() {
85
100
  movingViewControllers.forEach { movedVC in
@@ -98,6 +113,7 @@ public class THEOplayerRCTPresentationModeManager {
98
113
  let inlineParentView = self.inlineParentView,
99
114
  let fullscreenParentView = self.view?.findParentViewOfType(["RCTRootContentView", "RCTRootComponentView"]) {
100
115
  self.inlineParentViewIndex = inlineParentView.subviews.firstIndex(of: containerView)
116
+ if DEBUG_PRESENTATIONMODES { PrintUtils.printLog(logText: "[NATIVE] storing inlineParentViewIndex = \(self.inlineParentViewIndex ?? -1) of \(inlineParentView.subviews.count) subviews.") }
101
117
  self.moveView(containerView, to: fullscreenParentView, targetViewIndex: nil)
102
118
 
103
119
  // start hiding home indicator
@@ -109,7 +125,7 @@ public class THEOplayerRCTPresentationModeManager {
109
125
  private func exitFullscreen() {
110
126
  // stop hiding home indicator
111
127
  setHomeIndicatorHidden(false)
112
-
128
+
113
129
  // move the player
114
130
  if let containerView = self.containerView,
115
131
  let inlineParentView = self.inlineParentView {
@@ -118,22 +134,24 @@ public class THEOplayerRCTPresentationModeManager {
118
134
  self.rnInlineMode = .inline
119
135
  }
120
136
 
121
- /*private func logSubviews(of parent: UIView) {
122
- for (index, subview) in parent.subviews.enumerated() {
123
- print("Index \(index): \(type(of: subview)) - \(subview)")
137
+ private func logSubviews(of viewToLog: UIView?) {
138
+ if let parentView = viewToLog {
139
+ for (index, subview) in parentView.subviews.enumerated() {
140
+ if DEBUG_PRESENTATIONMODES { PrintUtils.printLog(logText: "[NATIVE] Index \(index): \(type(of: subview)) - \(subview)") }
141
+ }
124
142
  }
125
- }*/
126
-
143
+ }
144
+
127
145
  private func setHomeIndicatorHidden(_ hidden: Bool) {
128
146
  #if os(iOS)
129
147
  if let fullscreenParentView = self.view?.findParentViewOfType(["RCTRootContentView", "RCTRootComponentView"]),
130
148
  let customRootViewController = fullscreenParentView.findViewController() as? HomeIndicatorViewController {
131
- customRootViewController.prefersAutoHidden = hidden
132
- customRootViewController.setNeedsUpdateOfHomeIndicatorAutoHidden()
149
+ customRootViewController.prefersAutoHidden = hidden
150
+ customRootViewController.setNeedsUpdateOfHomeIndicatorAutoHidden()
133
151
  }
134
152
  #endif
135
153
  }
136
-
154
+
137
155
  func setPresentationModeFromRN(newPresentationMode: THEOplayerSDK.PresentationMode) {
138
156
  guard newPresentationMode != self.presentationMode else { return }
139
157
 
@@ -170,7 +188,7 @@ public class THEOplayerRCTPresentationModeManager {
170
188
  break;
171
189
  }
172
190
  }
173
-
191
+
174
192
  func setPresentationModeFromNative(newPresentationMode: THEOplayerSDK.PresentationMode) {
175
193
  // store old presentationMode
176
194
  let oldPresentationMode = self.presentationMode
@@ -180,16 +198,16 @@ public class THEOplayerRCTPresentationModeManager {
180
198
  if newPresentationMode == .inline {
181
199
  self.presentationMode = self.rnInlineMode
182
200
  }
183
-
201
+
184
202
  // notify the presentationMode change
185
203
  self.notifyPresentationModeChange(oldPresentationMode: oldPresentationMode, newPresentationMode: self.presentationMode)
186
204
  }
187
-
205
+
188
206
  private func setNativePresentationMode(_ presentationMode: THEOplayerSDK.PresentationMode) {
189
207
  guard let player = self.player else { return }
190
208
  player.presentationMode = presentationMode
191
209
  }
192
-
210
+
193
211
  private func notifyPresentationModeChange(oldPresentationMode: THEOplayerSDK.PresentationMode, newPresentationMode: THEOplayerSDK.PresentationMode) {
194
212
  // update the current presentationMode
195
213
  self.presentationMode = newPresentationMode
@@ -208,7 +226,7 @@ public class THEOplayerRCTPresentationModeManager {
208
226
  // PRESENTATION_MODE_CHANGE
209
227
  self.presentationModeChangeListener = player.addEventListener(type: PlayerEventTypes.PRESENTATION_MODE_CHANGE) { [weak self] event in
210
228
  if let welf = self {
211
- if DEBUG_THEOPLAYER_EVENTS || true { PrintUtils.printLog(logText: "[NATIVE] Received PRESENTATION_MODE_CHANGE event from THEOplayer (to \(event.presentationMode._rawValue))") }
229
+ if DEBUG_THEOPLAYER_EVENTS { PrintUtils.printLog(logText: "[NATIVE] Received PRESENTATION_MODE_CHANGE event from THEOplayer (to \(event.presentationMode._rawValue))") }
212
230
  welf.setPresentationModeFromNative(newPresentationMode: event.presentationMode)
213
231
  }
214
232
  }
@@ -1 +1 @@
1
- {"version":"10.7.1","buildDate":"2026-01-06T15:39:29.476Z"}
1
+ {"version":"10.8.0","buildDate":"2026-01-22T15:21:17.281Z"}
@@ -1 +1 @@
1
- {"version":"10.7.1","buildDate":"2026-01-06T15:39:29.476Z"}
1
+ {"version":"10.8.0","buildDate":"2026-01-22T15:21:17.281Z"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-theoplayer",
3
- "version": "10.7.1",
3
+ "version": "10.8.0",
4
4
  "description": "A THEOplayer video component for react-native.",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
@@ -58,8 +58,8 @@
58
58
  "devDependencies": {
59
59
  "@eslint/js": "^9.13.0",
60
60
  "@expo/config-plugins": "^10.0.2",
61
- "@react-native/eslint-config": "^0.82.0",
62
- "@types/react": "^19.1.1",
61
+ "@react-native/eslint-config": "^0.83.1",
62
+ "@types/react": "^19.2.0",
63
63
  "eslint": "^8.57.1",
64
64
  "eslint-config-prettier": "^9.1.0",
65
65
  "eslint-plugin-react-hooks": "^7.0.0",
@@ -67,8 +67,8 @@
67
67
  "lint-staged": "^15.5.1",
68
68
  "pod-install": "^0.1.39",
69
69
  "prettier": "^3.5.3",
70
- "react": "^19.1.1",
71
- "react-native": "^0.82.0",
70
+ "react": "^19.2.0",
71
+ "react-native": "^0.83.1",
72
72
  "react-native-builder-bob": "^0.39.1",
73
73
  "theoplayer": "^10",
74
74
  "typedoc": "^0.25.13",
package/src/manifest.json CHANGED
@@ -1 +1 @@
1
- {"version":"10.7.1","buildDate":"2026-01-06T15:39:29.476Z"}
1
+ {"version":"10.8.0","buildDate":"2026-01-22T15:21:17.281Z"}