react-native-theoplayer 10.7.1 → 10.9.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 (44) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/LICENSE +29 -17
  3. package/android/src/main/java/com/theoplayer/PlayerEventEmitter.kt +25 -11
  4. package/android/src/main/java/com/theoplayer/ReactTHEOplayerView.kt +5 -2
  5. package/android/src/main/java/com/theoplayer/ReactTHEOplayerViewManager.kt +3 -1
  6. package/android/src/main/java/com/theoplayer/ads/AdAdapter.kt +4 -2
  7. package/android/src/main/java/com/theoplayer/audio/BackgroundAudioConfigAdapter.kt +4 -8
  8. package/android/src/main/java/com/theoplayer/player/PlayerModule.kt +8 -7
  9. package/android/src/main/java/com/theoplayer/presentation/FullscreenLayoutObserver.kt +3 -25
  10. package/android/src/main/java/com/theoplayer/presentation/PresentationManager.kt +21 -39
  11. package/android/src/main/java/com/theoplayer/source/SourceAdapter.kt +2 -0
  12. package/android/src/main/java/com/theoplayer/util/ReadableExtension.kt +10 -0
  13. package/android/src/main/java/com/theoplayer/util/ViewUtils.kt +67 -0
  14. package/ios/THEOplayerRCTBridge.m +1 -0
  15. package/ios/THEOplayerRCTDebug.swift +9 -2
  16. package/ios/THEOplayerRCTMainEventHandler.swift +4 -4
  17. package/ios/THEOplayerRCTPlayerStateBuilder.swift +100 -0
  18. package/ios/THEOplayerRCTSourceDescriptionAggregator.swift +12 -7
  19. package/ios/THEOplayerRCTSourceDescriptionBuilder.swift +6 -4
  20. package/ios/THEOplayerRCTTrackMetadataAggregator.swift +1 -1
  21. package/ios/THEOplayerRCTView+AppState.swift +30 -1
  22. package/ios/THEOplayerRCTView.swift +81 -17
  23. package/ios/ads/THEOplayerRCTSourceDescriptionBuilder+Ads.swift +5 -5
  24. package/ios/backgroundAudio/THEOplayerRCTNowPlayingManager.swift +29 -54
  25. package/ios/pip/THEOplayerRCTPipControlsManager.swift +1 -1
  26. package/ios/presentationMode/THEOplayerRCTPresentationModeManager.swift +36 -18
  27. package/lib/commonjs/internal/THEOplayerView.js +11 -2
  28. package/lib/commonjs/internal/THEOplayerView.js.map +1 -1
  29. package/lib/commonjs/internal/adapter/THEOplayerAdapter.js +5 -0
  30. package/lib/commonjs/internal/adapter/THEOplayerAdapter.js.map +1 -1
  31. package/lib/commonjs/manifest.json +1 -1
  32. package/lib/module/internal/THEOplayerView.js +13 -3
  33. package/lib/module/internal/THEOplayerView.js.map +1 -1
  34. package/lib/module/internal/adapter/THEOplayerAdapter.js +5 -0
  35. package/lib/module/internal/adapter/THEOplayerAdapter.js.map +1 -1
  36. package/lib/module/manifest.json +1 -1
  37. package/lib/typescript/internal/THEOplayerView.d.ts +1 -0
  38. package/lib/typescript/internal/THEOplayerView.d.ts.map +1 -1
  39. package/lib/typescript/internal/adapter/THEOplayerAdapter.d.ts +1 -0
  40. package/lib/typescript/internal/adapter/THEOplayerAdapter.d.ts.map +1 -1
  41. package/package.json +8 -8
  42. package/src/internal/THEOplayerView.tsx +12 -3
  43. package/src/internal/adapter/THEOplayerAdapter.ts +6 -0
  44. package/src/manifest.json +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,41 @@ 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.9.0] - 26-01-29
9
+
10
+ ### Fixed
11
+
12
+ - Fixed an issue on Android where the player could disrupt the layout by sending an unexpected dimension change event.
13
+ - Fixed an issue on Android where the default properties of the player's `backgroundAudioConfiguration` would not be properly applied.
14
+
15
+ ### Added
16
+
17
+ - Added functionality to synchronize the player state from the native mobile players to React Native.
18
+ - Added a bypass property on iOS to control the THEOplayerView's ability to detach from its superView.
19
+ - Trigger an update for the iOS control centers (nowPlayingInfo and remoteCommands) to reflect a player state sync.
20
+
21
+ ### Changed
22
+
23
+ - Changed license to BSD 3-Cause Clear. See [LICENSE](./LICENSE) file for more information.
24
+
25
+ ## [10.8.0] - 26-01-22
26
+
27
+ ### Added
28
+
29
+ - Added support on iOS to push the initial state of the player from the iOS bridge to the React native adapter.
30
+
31
+ ### Fixed
32
+
33
+ - 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.
34
+ - Fixed an issue on iOS where the player could crash when terminating the app while the player is backgrounded in fullscreen.
35
+ - Fixed an issue on Android where `player.ads.currentAds` would not return an array of ads due to a native error.
36
+ - Fixed an issue on Android where the player would sometimes have wrong dimensions after transitioning to fullscreen presentation mode.
37
+ - Fixed an issue on Android where the `useEMSG` property was not included when setting a TheoAds source description.
38
+
39
+ ### Added
40
+
41
+ - Added RNRepo support for Android to the example app. More info on the official [documentation](https://rnrepo.org/) page.
42
+
8
43
  ## [10.7.1] - 26-01-06
9
44
 
10
45
  ### Added
package/LICENSE CHANGED
@@ -1,21 +1,33 @@
1
- MIT License
1
+ The Clear BSD License
2
2
 
3
- Copyright (c) 2021 THEOplayer
3
+ Copyright (c) 2025 Dolby International AB
4
+ All rights reserved.
4
5
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted (subject to the limitations in the disclaimer
8
+ below) provided that the following conditions are met:
11
9
 
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
10
+ 1. Redistributions of source code must retain the above copyright
11
+ notice, this list of conditions and the following disclaimer.
14
12
 
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
13
+ 2. Redistributions in binary form must reproduce the above copyright
14
+ notice, this list of conditions and the following disclaimer in
15
+ the documentation and/or other materials provided with the
16
+ distribution.
17
+
18
+ 3. Neither the name of the copyright holder nor the names of its
19
+ contributors may be used to endorse or promote products derived
20
+ from this software without specific prior written permission.
21
+
22
+ NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED
23
+ BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
24
+ CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
25
+ BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
26
+ FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
27
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
28
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
29
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
30
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
31
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
32
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -53,6 +53,7 @@ import kotlin.math.floor
53
53
  private val TAG = PlayerEventEmitter::class.java.name
54
54
 
55
55
  private const val EVENT_PLAYER_READY = "onNativePlayerReady"
56
+ private const val EVENT_PLAYER_STATE_SYNC = "onNativePlayerStateSync"
56
57
  private const val EVENT_SOURCECHANGE = "onNativeSourceChange"
57
58
  private const val EVENT_LOADSTART = "onNativeLoadStart"
58
59
  private const val EVENT_LOADEDMETADATA = "onNativeLoadedMetadata"
@@ -98,6 +99,7 @@ class PlayerEventEmitter internal constructor(
98
99
  @Retention(AnnotationRetention.SOURCE)
99
100
  @StringDef(
100
101
  EVENT_PLAYER_READY,
102
+ EVENT_PLAYER_STATE_SYNC,
101
103
  EVENT_SOURCECHANGE,
102
104
  EVENT_LOADSTART,
103
105
  EVENT_LOADEDMETADATA,
@@ -135,6 +137,7 @@ class PlayerEventEmitter internal constructor(
135
137
  companion object {
136
138
  val Events = arrayOf(
137
139
  EVENT_PLAYER_READY,
140
+ EVENT_PLAYER_STATE_SYNC,
138
141
  EVENT_SOURCECHANGE,
139
142
  EVENT_LOADSTART,
140
143
  EVENT_LOADEDMETADATA,
@@ -291,10 +294,28 @@ class PlayerEventEmitter internal constructor(
291
294
 
292
295
  fun preparePlayer(player: Player) {
293
296
  attachListeners(player)
297
+ emitPlayerReady()
298
+ }
294
299
 
295
- val payload = Arguments.createMap()
296
- payload.putMap(
297
- EVENT_PROP_STATE,
300
+ fun emitPlayerReady() {
301
+ // Notify the player is ready
302
+ receiveEvent(EVENT_PLAYER_READY, Arguments.createMap().apply {
303
+ putMap(EVENT_PROP_STATE, collectPlayerState())
304
+ putMap(EVENT_PROP_VERSION, WritableNativeMap().apply {
305
+ putString(EVENT_PROP_VERSION, THEOplayerGlobal.getVersion())
306
+ putString(EVENT_PROP_SUITE_VERSION, "")
307
+ })
308
+ })
309
+ }
310
+
311
+ fun emitPlayerStateSync() {
312
+ receiveEvent(EVENT_PLAYER_STATE_SYNC, Arguments.createMap().apply {
313
+ putMap(EVENT_PROP_STATE, collectPlayerState())
314
+ })
315
+ }
316
+
317
+ private fun collectPlayerState(): WritableMap {
318
+ return playerView.player?.let { player ->
298
319
  PayloadBuilder()
299
320
  .source(player.source)
300
321
  .currentTime(player.currentTime)
@@ -312,14 +333,7 @@ class PlayerEventEmitter internal constructor(
312
333
  .selectedAudioTrack(getSelectedAudioTrack(player))
313
334
  .selectedVideoTrack(getSelectedVideoTrack(player))
314
335
  .build()
315
- )
316
- payload.putMap(EVENT_PROP_VERSION, WritableNativeMap().apply {
317
- putString(EVENT_PROP_VERSION, THEOplayerGlobal.getVersion())
318
- putString(EVENT_PROP_SUITE_VERSION, "")
319
- })
320
-
321
- // Notify the player is ready
322
- receiveEvent(EVENT_PLAYER_READY, payload)
336
+ } ?: Arguments.createMap()
323
337
  }
324
338
 
325
339
  private fun emitError(code: String, message: String?) {
@@ -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.audio
2
2
 
3
3
  import com.facebook.react.bridge.ReadableMap
4
+ import com.theoplayer.util.getBooleanOrNull
4
5
 
5
6
  object BackgroundAudioConfigAdapter {
6
7
  private const val PROP_ENABLED = "enabled"
@@ -10,15 +11,10 @@ object BackgroundAudioConfigAdapter {
10
11
  private const val DEFAULT_STOP_ON_BACKGROUND = false
11
12
 
12
13
  fun fromProps(props: ReadableMap?): BackgroundAudioConfig {
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
14
  return BackgroundAudioConfig(
20
- enabled = enabled,
21
- stopOnBackground = destroy
15
+ enabled = props?.getBooleanOrNull(PROP_ENABLED) ?: DEFAULT_ENABLED,
16
+ stopOnBackground = props?.getBooleanOrNull(PROP_STOP_ON_BACKGROUND)
17
+ ?: DEFAULT_STOP_ON_BACKGROUND,
22
18
  )
23
19
  }
24
20
  }
@@ -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,13 @@ 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.ReactContext
20
20
  import com.facebook.react.uimanager.ThemedReactContext
21
21
  import com.facebook.react.views.view.ReactViewGroup
22
22
  import com.theoplayer.BuildConfig
@@ -26,11 +26,14 @@ import com.theoplayer.ReactTHEOplayerView
26
26
  import com.theoplayer.android.api.error.ErrorCode
27
27
  import com.theoplayer.android.api.error.THEOplayerException
28
28
  import com.theoplayer.android.api.player.PresentationMode
29
+ import com.theoplayer.util.findReactRootView
30
+ import com.theoplayer.util.getClosestParentOfType
29
31
 
30
32
  const val IS_TRANSITION_INTO_PIP = "isTransitioningToPip"
31
33
  const val IS_IN_PIP_MODE = "isInPictureInPictureMode"
32
34
  const val ON_USER_LEAVE_HINT = "onUserLeaveHint"
33
35
  const val ON_PIP_MODE_CHANGED = "onPictureInPictureModeChanged"
36
+ const val ON_DID_UPDATE_DIMENSIONS = "_didUpdateDimensions"
34
37
 
35
38
  @Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
36
39
  @SuppressLint("UnspecifiedRegisterReceiverFlag")
@@ -52,6 +55,11 @@ class PresentationManager(
52
55
  private val playerGroupRestoreOptions by lazy {
53
56
  PlayerGroupRestoreOptions()
54
57
  }
58
+ private val layoutChangeListener = View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
59
+ reactContext
60
+ .getJSModule(ReactContext.RCTDeviceEventEmitter::class.java)
61
+ .emit(ON_DID_UPDATE_DIMENSIONS, null)
62
+ }
55
63
  var currentPresentationMode: PresentationMode = PresentationMode.INLINE
56
64
  private set
57
65
  var currentPresentationModeChangeContext: PresentationModeChangeContext? = null
@@ -71,6 +79,11 @@ class PresentationManager(
71
79
  }
72
80
  }
73
81
  }
82
+
83
+ // Emit a private dimension update event when layout changes
84
+ // This makes sure a dimension update is sent after fullscreen immersive animations complete.
85
+ rootView?.addOnLayoutChangeListener(layoutChangeListener)
86
+
74
87
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
75
88
  supportsPip =
76
89
  reactContext.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
@@ -118,6 +131,7 @@ class PresentationManager(
118
131
  }
119
132
 
120
133
  fullScreenLayoutObserver.remove()
134
+ rootView?.removeOnLayoutChangeListener(layoutChangeListener)
121
135
  pipUtils.destroy()
122
136
  } catch (_: Exception) {
123
137
  }
@@ -269,12 +283,7 @@ class PresentationManager(
269
283
  get() = viewCtx.playerView.getClosestParentOfType()
270
284
 
271
285
  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
- }
286
+ get() = findReactRootView( reactContext, reactPlayerGroup)
278
287
 
279
288
  private fun reparentPlayerToRoot() {
280
289
  reactPlayerGroup?.let { playerGroup ->
@@ -283,10 +292,12 @@ class PresentationManager(
283
292
 
284
293
  // Re-parent the playerViewGroup to the root node
285
294
  parent.removeView(playerGroup)
286
- rootView?.addView(playerGroup)
295
+ rootView?.let { root ->
296
+ root.addView(playerGroup)
287
297
 
288
- // Attach an observer that overrides the react-native lay-out and forces fullscreen.
289
- fullScreenLayoutObserver.attach(playerGroup)
298
+ // Attach an observer that overrides the react-native lay-out and forces fullscreen.
299
+ fullScreenLayoutObserver.attach(playerGroup, root)
300
+ }
290
301
  }
291
302
  }
292
303
  }
@@ -335,35 +346,6 @@ class PresentationManager(
335
346
  }
336
347
  }
337
348
 
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
349
  private class PlayerGroupRestoreOptions {
368
350
  var childIndex: Int? = null
369
351
  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,10 @@
1
+ package com.theoplayer.util
2
+
3
+ import com.facebook.react.bridge.ReadableMap
4
+
5
+ /**
6
+ * Gets a boolean value from the map, or null if the key does not exist.
7
+ */
8
+ fun ReadableMap.getBooleanOrNull(name: String): Boolean? {
9
+ return if (hasKey(name)) getBoolean(name) else null
10
+ }
@@ -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
+ }
@@ -52,6 +52,7 @@ RCT_EXPORT_VIEW_PROPERTY(onNativeAdEvent, RCTDirectEventBlock);
52
52
  RCT_EXPORT_VIEW_PROPERTY(onNativeTHEOliveEvent, RCTDirectEventBlock);
53
53
  RCT_EXPORT_VIEW_PROPERTY(onNativeTHEOadsEvent, RCTDirectEventBlock);
54
54
  RCT_EXPORT_VIEW_PROPERTY(onNativeCastEvent, RCTDirectEventBlock);
55
+ RCT_EXPORT_VIEW_PROPERTY(onNativePlayerStateSync, RCTDirectEventBlock);
55
56
 
56
57
  @end
57
58
 
@@ -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
  }