react-native-theoplayer 2.2.0 → 2.4.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 (50) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/android/src/main/java/com/theoplayer/presentation/PipUtils.kt +248 -0
  3. package/android/src/main/java/com/theoplayer/presentation/PresentationManager.kt +22 -55
  4. package/android/src/main/java/com/theoplayer/track/TrackListAdapter.kt +3 -2
  5. package/android/src/main/java/com/theoplayer/util/TypeUtils.kt +6 -6
  6. package/android/src/main/res/drawable/ic_fast_forward.xml +9 -0
  7. package/android/src/main/res/drawable/ic_rewind.xml +9 -0
  8. package/android/src/main/res/values/strings.xml +8 -0
  9. package/ios/THEOplayerRCTMainEventHandler.swift +15 -12
  10. package/ios/THEOplayerRCTTrackMetadataAggregator.swift +2 -2
  11. package/ios/THEOplayerRCTView.swift +3 -1
  12. package/ios/backgroundAudio/THEOplayerRCTNowPlayingManager.swift +7 -5
  13. package/ios/backgroundAudio/THEOplayerRCTView+BackgroundAudioConfig.swift +12 -0
  14. package/lib/commonjs/api/source/SourceDescription.js.map +1 -1
  15. package/lib/commonjs/internal/THEOplayerView.js +6 -1
  16. package/lib/commonjs/internal/THEOplayerView.js.map +1 -1
  17. package/lib/commonjs/internal/THEOplayerView.web.js +7 -7
  18. package/lib/commonjs/internal/THEOplayerView.web.js.map +1 -1
  19. package/lib/commonjs/internal/adapter/THEOplayerAdapter.js +1 -1
  20. package/lib/commonjs/internal/adapter/THEOplayerAdapter.js.map +1 -1
  21. package/lib/commonjs/internal/adapter/web/TrackUtils.js +3 -2
  22. package/lib/commonjs/internal/adapter/web/TrackUtils.js.map +1 -1
  23. package/lib/commonjs/internal/adapter/web/WebMediaSession.js +17 -29
  24. package/lib/commonjs/internal/adapter/web/WebMediaSession.js.map +1 -1
  25. package/lib/commonjs/internal/adapter/web/WebPresentationModeManager.js +1 -1
  26. package/lib/commonjs/internal/adapter/web/WebPresentationModeManager.js.map +1 -1
  27. package/lib/module/api/source/SourceDescription.js.map +1 -1
  28. package/lib/module/internal/THEOplayerView.js +6 -1
  29. package/lib/module/internal/THEOplayerView.js.map +1 -1
  30. package/lib/module/internal/THEOplayerView.web.js +7 -7
  31. package/lib/module/internal/THEOplayerView.web.js.map +1 -1
  32. package/lib/module/internal/adapter/THEOplayerAdapter.js +1 -1
  33. package/lib/module/internal/adapter/THEOplayerAdapter.js.map +1 -1
  34. package/lib/module/internal/adapter/web/TrackUtils.js +3 -2
  35. package/lib/module/internal/adapter/web/TrackUtils.js.map +1 -1
  36. package/lib/module/internal/adapter/web/WebMediaSession.js +15 -29
  37. package/lib/module/internal/adapter/web/WebMediaSession.js.map +1 -1
  38. package/lib/module/internal/adapter/web/WebPresentationModeManager.js +1 -1
  39. package/lib/module/internal/adapter/web/WebPresentationModeManager.js.map +1 -1
  40. package/lib/typescript/api/source/SourceDescription.d.ts +6 -0
  41. package/lib/typescript/internal/adapter/web/WebMediaSession.d.ts +2 -1
  42. package/package.json +1 -1
  43. package/react-native-theoplayer.podspec +0 -1
  44. package/src/api/source/SourceDescription.ts +8 -0
  45. package/src/internal/THEOplayerView.tsx +6 -1
  46. package/src/internal/THEOplayerView.web.tsx +4 -7
  47. package/src/internal/adapter/THEOplayerAdapter.ts +1 -1
  48. package/src/internal/adapter/web/TrackUtils.ts +3 -2
  49. package/src/internal/adapter/web/WebMediaSession.ts +14 -29
  50. package/src/internal/adapter/web/WebPresentationModeManager.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ 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.0.0/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [unreleased]
9
+
10
+ ### Fixed
11
+
12
+ - Fixed an issue on iOS and Android where cue event properties `startTime` and `endTime` with value `Infinity` or `NaN` were not passed correctly.
13
+ - Fixed an issue on iOS Safari where switching to fullscreen presentation during an ad would not work.
14
+ - Fixed an issue on iOS Safari where an ad could be skipped during unmuted autoplay.
15
+ - Fixed a memory leak on iOS where the player would be allocated after being destroyed.
16
+ - Fixed an issue on Android where building the SDK would require IMA to be enabled.
17
+
18
+ ### Changed
19
+
20
+ - Changed Web media session controls to only show trick-play buttons if the player is in foreground, or `backgroundAudioEnabled` is `true`, and never for ads and live stream.
21
+ - Changed Web media session controls to only show a play/pause button if the player is in foreground, or `backgroundAudioEnabled` is `true`, and never for ads.
22
+
23
+ ### Added
24
+
25
+ - Added the `crossOrigin` property to `SourceDescription` for requesting CORS access to content.
26
+
27
+ ## [2.3.0] - 23-04-14
28
+
29
+ ### Changed
30
+
31
+ - Updated picture-in-picture controls on Android to include forward/rewind buttons and disabled pause button for ads.
32
+
8
33
  ## [2.2.0] - 23-04-12
9
34
 
10
35
  ### Fixed
@@ -0,0 +1,248 @@
1
+ package com.theoplayer.presentation
2
+
3
+ import android.app.PendingIntent
4
+ import android.app.PictureInPictureParams
5
+ import android.app.RemoteAction
6
+ import android.content.BroadcastReceiver
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.content.IntentFilter
10
+ import android.graphics.Rect
11
+ import android.graphics.drawable.Icon
12
+ import android.os.Build
13
+ import android.util.Rational
14
+ import android.view.SurfaceView
15
+ import android.view.TextureView
16
+ import android.view.View
17
+ import android.view.ViewGroup
18
+ import androidx.annotation.RequiresApi
19
+ import com.facebook.react.uimanager.ThemedReactContext
20
+ import com.theoplayer.BuildConfig
21
+ import com.theoplayer.R
22
+ import com.theoplayer.ReactTHEOplayerContext
23
+ import com.theoplayer.android.api.ads.ima.GoogleImaAdEvent
24
+ import com.theoplayer.android.api.ads.ima.GoogleImaAdEventType
25
+ import com.theoplayer.android.api.event.EventListener
26
+ import com.theoplayer.android.api.event.player.PlayerEvent
27
+ import com.theoplayer.android.api.event.player.PlayerEventTypes
28
+ import com.theoplayer.android.api.player.Player
29
+
30
+ private const val EXTRA_ACTION = "EXTRA_ACTION"
31
+ private const val ACTION_MEDIA_CONTROL = "pip_media_control"
32
+ private const val ACTION_PLAY = 0
33
+ private const val ACTION_PAUSE = ACTION_PLAY + 1
34
+ private const val ACTION_RWD = ACTION_PLAY + 2
35
+ private const val ACTION_FFD = ACTION_PLAY + 3
36
+ private const val ACTION_IGNORE = ACTION_PLAY + 999
37
+ private const val SKIP_TIME = 15
38
+
39
+ private val PIP_ASPECT_RATIO_DEFAULT = Rational(16, 9)
40
+ private val PIP_ASPECT_RATIO_MIN = Rational(100, 239)
41
+ private val PIP_ASPECT_RATIO_MAX = Rational(239, 100)
42
+
43
+ class PipUtils(
44
+ private val viewCtx: ReactTHEOplayerContext,
45
+ private val reactContext: ThemedReactContext
46
+ ) {
47
+
48
+ private var enabled: Boolean = false
49
+ private var onPlayerAction: EventListener<PlayerEvent<*>>? = null
50
+ private var onAdAction: EventListener<GoogleImaAdEvent>? = null
51
+ private val playerEvents = listOf(PlayerEventTypes.PLAY, PlayerEventTypes.PAUSE)
52
+ private var adEvents = listOf<GoogleImaAdEventType>()
53
+ private val broadcastReceiver: BroadcastReceiver = buildBroadcastReceiver()
54
+
55
+ private val player: Player
56
+ get() = viewCtx.player
57
+
58
+ init {
59
+ onPlayerAction = EventListener {
60
+ updatePipParams()
61
+ }
62
+ onAdAction = EventListener {
63
+ updatePipParams()
64
+ }
65
+ if (BuildConfig.EXTENSION_GOOGLE_IMA) {
66
+ adEvents = listOf(GoogleImaAdEventType.STARTED, GoogleImaAdEventType.CONTENT_RESUME_REQUESTED)
67
+ }
68
+ }
69
+
70
+ fun enable() {
71
+ if (enabled) {
72
+ return
73
+ }
74
+ playerEvents.forEach { action ->
75
+ player.addEventListener(action, onPlayerAction)
76
+ }
77
+ adEvents.forEach { action ->
78
+ player.ads.addEventListener(action, onAdAction)
79
+ }
80
+ reactContext.currentActivity?.registerReceiver(
81
+ broadcastReceiver,
82
+ IntentFilter(ACTION_MEDIA_CONTROL)
83
+ )
84
+ enabled = true
85
+ }
86
+
87
+ fun disable() {
88
+ if (!enabled) {
89
+ return
90
+ }
91
+ playerEvents.forEach { action ->
92
+ player.removeEventListener(action, onPlayerAction)
93
+ }
94
+ adEvents.forEach { action ->
95
+ player.ads.removeEventListener(action, onAdAction)
96
+ }
97
+ try {
98
+ reactContext.currentActivity?.unregisterReceiver(broadcastReceiver)
99
+ } catch (ignore: IllegalArgumentException) { /*ignore*/}
100
+ enabled = false
101
+ }
102
+
103
+ fun destroy() {
104
+ disable()
105
+ }
106
+
107
+ @RequiresApi(Build.VERSION_CODES.O)
108
+ fun buildPipActions(
109
+ paused: Boolean,
110
+ enablePlayPause: Boolean,
111
+ enableTrickPlay: Boolean
112
+ ): List<RemoteAction> {
113
+ return mutableListOf<RemoteAction>().apply {
114
+
115
+ // Trick-play: Rewind
116
+ if (enableTrickPlay) {
117
+ add(
118
+ buildRemoteAction(
119
+ ACTION_RWD,
120
+ R.drawable.ic_rewind,
121
+ R.string.rwd_pip,
122
+ R.string.rwd_desc_pip
123
+ )
124
+ )
125
+ }
126
+
127
+ // Play/pause
128
+ // Always add this button, but send an ACTION_IGNORE if disabled.
129
+ add(
130
+ if (paused) {
131
+ buildRemoteAction(
132
+ if (enablePlayPause) ACTION_PLAY else ACTION_IGNORE,
133
+ R.drawable.ic_play,
134
+ R.string.play_pip,
135
+ R.string.play_desc_pip
136
+ )
137
+ } else {
138
+ buildRemoteAction(
139
+ if (enablePlayPause) ACTION_PAUSE else ACTION_IGNORE,
140
+ R.drawable.ic_pause,
141
+ R.string.pause_pip,
142
+ R.string.pause_desc_pip
143
+ )
144
+ }
145
+ )
146
+
147
+ // Trick-play: Fast Forward
148
+ if (enableTrickPlay) {
149
+ add(
150
+ buildRemoteAction(
151
+ ACTION_FFD,
152
+ R.drawable.ic_fast_forward,
153
+ R.string.ffd_pip,
154
+ R.string.ffd_desc_pip
155
+ )
156
+ )
157
+ }
158
+ }
159
+ }
160
+
161
+ private fun getSafeAspectRatio(width: Int, height: Int): Rational {
162
+ val aspectRatio = Rational(width, height)
163
+ if (aspectRatio.isNaN || aspectRatio.isInfinite || aspectRatio.isZero) {
164
+ // Default aspect ratio
165
+ return PIP_ASPECT_RATIO_DEFAULT
166
+ }
167
+ if (aspectRatio > PIP_ASPECT_RATIO_MAX) {
168
+ return PIP_ASPECT_RATIO_MAX
169
+ }
170
+ if (aspectRatio < PIP_ASPECT_RATIO_MIN) {
171
+ return PIP_ASPECT_RATIO_MIN
172
+ }
173
+ return aspectRatio
174
+ }
175
+
176
+ private fun getContentViewRect(view: ViewGroup): Rect? {
177
+ for (i in 0 until view.childCount) {
178
+ val child: View = view.getChildAt(i)
179
+ if (child is ViewGroup) {
180
+ return getContentViewRect(child)
181
+ } else if (child as? SurfaceView != null || child as? TextureView != null) {
182
+ val visibleRect = Rect()
183
+ child.getGlobalVisibleRect(visibleRect)
184
+ return visibleRect
185
+ }
186
+ }
187
+ return null
188
+ }
189
+
190
+ @RequiresApi(Build.VERSION_CODES.O)
191
+ fun getPipParams(): PictureInPictureParams {
192
+ val view = viewCtx.playerView
193
+ val player = view.player
194
+ val visibleRect = getContentViewRect(view)
195
+ val isAd = player.ads.isPlaying
196
+ val isLive = player.duration.isInfinite()
197
+ val enablePlayPause = !isAd
198
+ val enableTrickPlay = !isAd && !isLive
199
+
200
+ return PictureInPictureParams.Builder()
201
+ .setSourceRectHint(visibleRect)
202
+ // Must be between 2.39:1 and 1:2.39 (inclusive)
203
+ .setAspectRatio(getSafeAspectRatio(view.player.videoWidth, view.player.videoHeight))
204
+ .setActions(
205
+ buildPipActions(player.isPaused, enablePlayPause, enableTrickPlay)
206
+ )
207
+ .build()
208
+ }
209
+
210
+ private fun updatePipParams() {
211
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
212
+ reactContext.currentActivity?.setPictureInPictureParams(getPipParams())
213
+ }
214
+ }
215
+
216
+ private fun buildBroadcastReceiver(): BroadcastReceiver {
217
+ return object : BroadcastReceiver() {
218
+ @RequiresApi(Build.VERSION_CODES.O)
219
+ override fun onReceive(context: Context?, intent: Intent?) {
220
+ intent?.getIntExtra(EXTRA_ACTION, -1)?.let { action ->
221
+ when (action) {
222
+ ACTION_PLAY -> player.play()
223
+ ACTION_PAUSE -> player.pause()
224
+ ACTION_FFD -> player.currentTime += SKIP_TIME
225
+ ACTION_RWD -> player.currentTime -= SKIP_TIME
226
+ }
227
+ reactContext.currentActivity?.setPictureInPictureParams(getPipParams())
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ @RequiresApi(Build.VERSION_CODES.O)
234
+ private fun buildRemoteAction(
235
+ requestId: Int,
236
+ iconId: Int,
237
+ titleId: Int,
238
+ descId: Int
239
+ ): RemoteAction {
240
+ val intent = Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_ACTION, requestId)
241
+ val pendingIntent =
242
+ PendingIntent.getBroadcast(reactContext, requestId, intent, PendingIntent.FLAG_IMMUTABLE)
243
+ val icon: Icon = Icon.createWithResource(reactContext, iconId)
244
+ val title = reactContext.getString(titleId)
245
+ val desc = reactContext.getString(descId)
246
+ return RemoteAction(icon, title, desc, pendingIntent)
247
+ }
248
+ }
@@ -1,19 +1,12 @@
1
1
  package com.theoplayer.presentation
2
2
 
3
3
  import android.app.AppOpsManager
4
- import android.app.PictureInPictureParams
5
4
  import android.content.BroadcastReceiver
6
5
  import android.content.Context
7
6
  import android.content.Intent
8
7
  import android.content.IntentFilter
9
8
  import android.content.pm.PackageManager
10
- import android.graphics.Rect
11
9
  import android.os.Build
12
- import android.util.Rational
13
- import android.view.SurfaceView
14
- import android.view.TextureView
15
- import android.view.View
16
- import android.view.ViewGroup
17
10
  import androidx.activity.ComponentActivity
18
11
  import androidx.core.view.WindowInsetsCompat
19
12
  import androidx.core.view.WindowInsetsControllerCompat
@@ -25,10 +18,6 @@ import com.theoplayer.android.api.error.ErrorCode
25
18
  import com.theoplayer.android.api.error.THEOplayerException
26
19
  import com.theoplayer.android.api.player.PresentationMode
27
20
 
28
- private val PIP_ASPECT_RATIO_DEFAULT = Rational(16, 9)
29
- private val PIP_ASPECT_RATIO_MIN = Rational(100, 239)
30
- private val PIP_ASPECT_RATIO_MAX = Rational(239, 100)
31
-
32
21
  class PresentationManager(
33
22
  private val viewCtx: ReactTHEOplayerContext,
34
23
  private val reactContext: ThemedReactContext,
@@ -38,6 +27,8 @@ class PresentationManager(
38
27
  private var onUserLeaveHintReceiver: BroadcastReceiver? = null
39
28
  private var onPictureInPictureModeChanged: BroadcastReceiver? = null
40
29
 
30
+ private val pipUtils: PipUtils = PipUtils(viewCtx, reactContext)
31
+
41
32
  var currentPresentationMode: PresentationMode = PresentationMode.INLINE
42
33
  private set
43
34
 
@@ -57,15 +48,9 @@ class PresentationManager(
57
48
  // Dispatch event on every PiP mode change
58
49
  val inPip = intent?.getBooleanExtra("isInPictureInPictureMode", false) ?: false
59
50
  if (inPip) {
60
- updatePresentationMode(PresentationMode.PICTURE_IN_PICTURE)
51
+ onEnterPip()
61
52
  } else {
62
- val pipCtx: PresentationModeChangePipContext = if ((reactContext.currentActivity as? ComponentActivity)
63
- ?.lifecycle?.currentState == Lifecycle.State.CREATED) {
64
- PresentationModeChangePipContext.CLOSED
65
- } else {
66
- PresentationModeChangePipContext.RESTORED
67
- }
68
- updatePresentationMode(PresentationMode.INLINE, PresentationModeChangeContext(pipCtx))
53
+ onExitPip()
69
54
  }
70
55
  }
71
56
  }
@@ -85,6 +70,7 @@ class PresentationManager(
85
70
  try {
86
71
  reactContext.currentActivity?.unregisterReceiver(onUserLeaveHintReceiver)
87
72
  reactContext.currentActivity?.unregisterReceiver(onPictureInPictureModeChanged)
73
+ pipUtils.destroy()
88
74
  } catch (ignore: Exception) {
89
75
  }
90
76
  }
@@ -104,20 +90,6 @@ class PresentationManager(
104
90
  }
105
91
  }
106
92
 
107
- private fun getContentViewRect(view: ViewGroup): Rect? {
108
- for (i in 0 until view.childCount) {
109
- val child: View = view.getChildAt(i)
110
- if (child is ViewGroup) {
111
- return getContentViewRect(child)
112
- } else if (child as? SurfaceView != null || child as? TextureView != null) {
113
- val visibleRect = Rect()
114
- child.getGlobalVisibleRect(visibleRect)
115
- return visibleRect
116
- }
117
- }
118
- return null
119
- }
120
-
121
93
  private fun enterPip() {
122
94
  // PiP not supported
123
95
  if (!supportsPip || Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
@@ -141,33 +113,28 @@ class PresentationManager(
141
113
  }
142
114
 
143
115
  try {
144
- val view = viewCtx.playerView
145
- val visibleRect = getContentViewRect(view)
146
- reactContext.currentActivity?.enterPictureInPictureMode(
147
- PictureInPictureParams.Builder().setSourceRectHint(visibleRect)
148
- // Must be between 2.39:1 and 1:2.39 (inclusive)
149
- .setAspectRatio(getSafeAspectRatio(view.player.videoWidth, view.player.videoHeight))
150
- // The active MediaSession will connect the controls
151
- .build()
152
- )
116
+ pipUtils.enable()
117
+ reactContext.currentActivity?.enterPictureInPictureMode(pipUtils.getPipParams())
153
118
  } catch (_: Exception) {
154
119
  onPipError()
155
120
  }
156
121
  }
157
122
 
158
- private fun getSafeAspectRatio(width: Int, height: Int): Rational {
159
- val aspectRatio = Rational(width, height)
160
- if (aspectRatio.isNaN || aspectRatio.isInfinite || aspectRatio.isZero) {
161
- // Default aspect ratio
162
- return PIP_ASPECT_RATIO_DEFAULT
163
- }
164
- if (aspectRatio > PIP_ASPECT_RATIO_MAX) {
165
- return PIP_ASPECT_RATIO_MAX
166
- }
167
- if (aspectRatio < PIP_ASPECT_RATIO_MIN) {
168
- return PIP_ASPECT_RATIO_MIN
169
- }
170
- return aspectRatio
123
+ private fun onEnterPip() {
124
+ updatePresentationMode(PresentationMode.PICTURE_IN_PICTURE)
125
+ }
126
+
127
+ private fun onExitPip() {
128
+ val pipCtx: PresentationModeChangePipContext =
129
+ if ((reactContext.currentActivity as? ComponentActivity)
130
+ ?.lifecycle?.currentState == Lifecycle.State.CREATED
131
+ ) {
132
+ PresentationModeChangePipContext.CLOSED
133
+ } else {
134
+ PresentationModeChangePipContext.RESTORED
135
+ }
136
+ updatePresentationMode(PresentationMode.INLINE, PresentationModeChangeContext(pipCtx))
137
+ pipUtils.disable()
171
138
  }
172
139
 
173
140
  private fun hasPipPermission(): Boolean {
@@ -11,6 +11,7 @@ import com.theoplayer.android.api.player.track.mediatrack.quality.Quality
11
11
  import com.theoplayer.android.api.player.track.mediatrack.quality.AudioQuality
12
12
  import com.theoplayer.android.api.player.track.mediatrack.quality.VideoQuality
13
13
  import com.theoplayer.android.api.player.track.mediatrack.MediaTrackList
14
+ import com.theoplayer.util.TypeUtils
14
15
 
15
16
  private const val PROP_ID = "id"
16
17
  private const val PROP_UID = "uid"
@@ -75,8 +76,8 @@ object TrackListAdapter {
75
76
  val cuePayload = Arguments.createMap()
76
77
  cuePayload.putString(PROP_ID, cue.id)
77
78
  cuePayload.putDouble(PROP_UID, cue.uid.toDouble())
78
- cuePayload.putDouble(PROP_STARTTIME, (1e3 * cue.startTime).toLong().toDouble())
79
- cuePayload.putDouble(PROP_ENDTIME, (1e3 * cue.endTime).toLong().toDouble())
79
+ cuePayload.putDouble(PROP_STARTTIME, TypeUtils.encodeInfNan(1e3 * cue.startTime))
80
+ cuePayload.putDouble(PROP_ENDTIME, TypeUtils.encodeInfNan(1e3 * cue.endTime))
80
81
  val content = cue.content
81
82
  if (content != null) {
82
83
  cuePayload.putString(
@@ -6,12 +6,12 @@ const val POS_INF_VALUE: Double = -2.0
6
6
  object TypeUtils {
7
7
  // Make sure we do not send INF or NaN double values over the bridge. It will break debug sessions.
8
8
  fun encodeInfNan(v: Double): Double {
9
- return when (v) {
10
- Double.NaN -> NAN_VALUE
11
- Double.POSITIVE_INFINITY -> POS_INF_VALUE
12
- else -> {
13
- return v
14
- }
9
+ if (v.isNaN()) {
10
+ return NAN_VALUE
15
11
  }
12
+ if (v.isInfinite()) {
13
+ return POS_INF_VALUE
14
+ }
15
+ return v
16
16
  }
17
17
  }
@@ -0,0 +1,9 @@
1
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
2
+ android:width="24dp"
3
+ android:height="24dp"
4
+ android:viewportWidth="24"
5
+ android:viewportHeight="24">
6
+ <path
7
+ android:fillColor="#FF000000"
8
+ android:pathData="M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z"/>
9
+ </vector>
@@ -0,0 +1,9 @@
1
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
2
+ android:width="24dp"
3
+ android:height="24dp"
4
+ android:viewportWidth="24"
5
+ android:viewportHeight="24">
6
+ <path
7
+ android:fillColor="#FF000000"
8
+ android:pathData="M11,18L11,6l-8.5,6 8.5,6zM11.5,12l8.5,6L20,6l-8.5,6z"/>
9
+ </vector>
@@ -2,6 +2,14 @@
2
2
  <resources>
3
3
  <string name="pause">Pause</string>
4
4
  <string name="play">Play</string>
5
+ <string name="pause_pip">Pause</string>
6
+ <string name="pause_desc_pip">Pause video</string>
7
+ <string name="play_pip">Play</string>
8
+ <string name="play_desc_pip">Play video</string>
9
+ <string name="ffd_pip">Fast Forward</string>
10
+ <string name="ffd_desc_pip">Fast Forward video</string>
11
+ <string name="rwd_pip">Rewind</string>
12
+ <string name="rwd_desc_pip">Rewind video</string>
5
13
 
6
14
  <string name="background_playback_service_description">THEOplayer service providing background playback.</string>
7
15
  <string name="notification_channel_id">theoplayer_default_channel</string>
@@ -3,7 +3,7 @@
3
3
  import Foundation
4
4
  import THEOplayerSDK
5
5
 
6
- class THEOplayerRCTMainEventHandler {
6
+ public class THEOplayerRCTMainEventHandler {
7
7
  // MARK: Members
8
8
  private weak var player: THEOplayer?
9
9
  private weak var presentationModeContext: THEOplayerRCTPresentationModeContext?
@@ -22,7 +22,7 @@ class THEOplayerRCTMainEventHandler {
22
22
  var onNativeSeeking: RCTDirectEventBlock?
23
23
  var onNativeSeeked: RCTDirectEventBlock?
24
24
  var onNativeEnded: RCTDirectEventBlock?
25
- var onNativeError: RCTDirectEventBlock?
25
+ public internal(set) var onNativeError: RCTDirectEventBlock?
26
26
  var onNativeLoadedData: RCTDirectEventBlock?
27
27
  var onNativeLoadedMetadata: RCTDirectEventBlock?
28
28
  var onNativeRateChange: RCTDirectEventBlock?
@@ -132,13 +132,14 @@ class THEOplayerRCTMainEventHandler {
132
132
  if DEBUG_EVENTHANDLER { print("[NATIVE] DurationChange listener attached to THEOplayer") }
133
133
 
134
134
  // VOLUME_CHANGE
135
- self.volumeChangeListener = player.addEventListener(type: PlayerEventTypes.VOLUME_CHANGE) { [weak self] event in
135
+ self.volumeChangeListener = player.addEventListener(type: PlayerEventTypes.VOLUME_CHANGE) { [weak self, weak player] event in
136
136
  if DEBUG_THEOPLAYER_EVENTS { print("[NATIVE] Received VOLUME_CHANGE event from THEOplayer") }
137
- if let forwardedVolumeChangeEvent = self?.onNativeVolumeChange {
137
+ if let wplayer = player,
138
+ let forwardedVolumeChangeEvent = self?.onNativeVolumeChange {
138
139
  forwardedVolumeChangeEvent(
139
140
  [
140
141
  "volume": event.volume,
141
- "muted": player.muted
142
+ "muted": wplayer.muted
142
143
  ]
143
144
  )
144
145
  }
@@ -146,11 +147,12 @@ class THEOplayerRCTMainEventHandler {
146
147
  if DEBUG_EVENTHANDLER { print("[NATIVE] VolumeChange listener attached to THEOplayer") }
147
148
 
148
149
  // PROGRESS
149
- self.progressListener = player.addEventListener(type: PlayerEventTypes.PROGRESS) { [weak self] event in
150
+ self.progressListener = player.addEventListener(type: PlayerEventTypes.PROGRESS) { [weak self, weak player] event in
150
151
  //if DEBUG_THEOPLAYER_EVENTS { print("[NATIVE] Received PROGRESS event from THEOplayer") }
151
- if let forwardedProgressEvent = self?.onNativeProgress {
152
- player.requestSeekable(completionHandler: { seekableTimeRanges, error in
153
- player.requestBuffered(completionHandler: { bufferedTimeRanges, error in
152
+ if let wplayer = player,
153
+ let forwardedProgressEvent = self?.onNativeProgress {
154
+ wplayer.requestSeekable(completionHandler: { seekableTimeRanges, error in
155
+ wplayer.requestBuffered(completionHandler: { bufferedTimeRanges, error in
154
156
  var seekable: [[String:Double]] = []
155
157
  seekableTimeRanges?.forEach({ timeRange in
156
158
  seekable.append(
@@ -263,10 +265,11 @@ class THEOplayerRCTMainEventHandler {
263
265
  if DEBUG_EVENTHANDLER { print("[NATIVE] LoadedData listener attached to THEOplayer") }
264
266
 
265
267
  // LOADED_META_DATA
266
- self.loadedMetadataListener = player.addEventListener(type: PlayerEventTypes.LOADED_META_DATA) { [weak self] event in
268
+ self.loadedMetadataListener = player.addEventListener(type: PlayerEventTypes.LOADED_META_DATA) { [weak self, weak player] event in
267
269
  if DEBUG_THEOPLAYER_EVENTS { print("[NATIVE] Received LOADED_META_DATA event from THEOplayer") }
268
- if let forwardedLoadedMetadataEvent = self?.onNativeLoadedMetadata {
269
- let metadata = THEOplayerRCTTrackMetadataAggregator.aggregateTrackMetadata(player: player)
270
+ if let wplayer = player,
271
+ let forwardedLoadedMetadataEvent = self?.onNativeLoadedMetadata {
272
+ let metadata = THEOplayerRCTTrackMetadataAggregator.aggregateTrackMetadata(player: wplayer)
270
273
  print(metadata)
271
274
  forwardedLoadedMetadataEvent(metadata)
272
275
  }
@@ -95,8 +95,8 @@ class THEOplayerRCTTrackMetadataAggregator {
95
95
  var entry: [String:Any] = [:]
96
96
  entry[PROP_ID] = textTrackCue.id
97
97
  entry[PROP_UID] = textTrackCue.uid
98
- entry[PROP_STARTTIME] = (textTrackCue.startTime ?? 0) * 1000
99
- entry[PROP_ENDTIME] = (textTrackCue.endTime ?? 0) * 1000
98
+ entry[PROP_STARTTIME] = THEOplayerRCTTypeUtils.encodeInfNan((textTrackCue.startTime ?? 0) * 1000)
99
+ entry[PROP_ENDTIME] = THEOplayerRCTTypeUtils.encodeInfNan((textTrackCue.endTime ?? 0) * 1000)
100
100
  if let content = textTrackCue.content {
101
101
  entry[PROP_CUE_CONTENT] = content
102
102
  } else if let contentString = textTrackCue.contentString {
@@ -7,7 +7,7 @@ import THEOplayerSDK
7
7
  public class THEOplayerRCTView: UIView {
8
8
  // MARK: Members
9
9
  public private(set) var player: THEOplayer?
10
- var mainEventHandler: THEOplayerRCTMainEventHandler
10
+ public private(set) var mainEventHandler: THEOplayerRCTMainEventHandler
11
11
  var textTrackEventHandler: THEOplayerRCTTextTrackEventHandler
12
12
  var mediaTrackEventHandler: THEOplayerRCTMediaTrackEventHandler
13
13
  var adEventHandler: THEOplayerRCTAdsEventHandler
@@ -136,6 +136,8 @@ public class THEOplayerRCTView: UIView {
136
136
  self.remoteCommandsManager.destroy()
137
137
  self.pipControlsManager.destroy()
138
138
 
139
+ self.destroyBackgroundAudio()
140
+ self.player?.removeAllIntegrations()
139
141
  self.player?.destroy()
140
142
  self.player = nil
141
143
  if DEBUG_THEOPLAYER_INTERACTION { print("[NATIVE] THEOplayer instance destroyed.") }
@@ -155,9 +155,10 @@ class THEOplayerRCTNowPlayingManager {
155
155
  }
156
156
 
157
157
  // DURATION_CHANGE
158
- self.durationChangeListener = player.addEventListener(type: PlayerEventTypes.DURATION_CHANGE) { [weak self] event in
158
+ self.durationChangeListener = player.addEventListener(type: PlayerEventTypes.DURATION_CHANGE) { [weak self, weak player] event in
159
159
  if let welf = self,
160
- let duration = player.duration {
160
+ let wplayer = player,
161
+ let duration = wplayer.duration {
161
162
  welf.nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = duration.isInfinite
162
163
  if (!duration.isInfinite) {
163
164
  welf.nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
@@ -191,9 +192,10 @@ class THEOplayerRCTNowPlayingManager {
191
192
 
192
193
 
193
194
  // RATE_CHANGE
194
- self.rateChangeListener = player.addEventListener(type: PlayerEventTypes.RATE_CHANGE) { [weak self] event in
195
- if let welf = self {
196
- welf.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = NSNumber(value: player.playbackRate)
195
+ self.rateChangeListener = player.addEventListener(type: PlayerEventTypes.RATE_CHANGE) { [weak self, weak player] event in
196
+ if let welf = self,
197
+ let wplayer = player {
198
+ welf.nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = NSNumber(value: wplayer.playbackRate)
197
199
  MPNowPlayingInfoCenter.default().nowPlayingInfo = welf.nowPlayingInfo
198
200
  if DEBUG_NOWINFO { print("[NATIVE] RATE_CHANGE: PlaybackRate updated on NowPlayingInfoCenter.") }
199
201
  }
@@ -11,6 +11,7 @@ struct BackgroundAudioConfig {
11
11
 
12
12
  extension THEOplayerRCTView {
13
13
  func initBackgroundAudio() {}
14
+ func destroyBackgroundAudio() {}
14
15
  public func shouldContinueAudioPlaybackInBackground() -> Bool { return false }
15
16
  }
16
17
 
@@ -22,6 +23,13 @@ extension THEOplayerRCTView: BackgroundPlaybackDelegate {
22
23
  self.player?.backgroundPlaybackDelegate = self
23
24
  }
24
25
 
26
+ func destroyBackgroundAudio() {
27
+ guard let player = self.player else {
28
+ return
29
+ }
30
+ player.backgroundPlaybackDelegate = DefaultBackgroundPlaybackDelegate()
31
+ }
32
+
25
33
  public func shouldContinueAudioPlaybackInBackground() -> Bool {
26
34
  // Make sure to go to the background with updated NowPlayingInfo
27
35
  self.nowPlayingManager.updateNowPlaying()
@@ -30,4 +38,8 @@ extension THEOplayerRCTView: BackgroundPlaybackDelegate {
30
38
  }
31
39
  }
32
40
 
41
+ struct DefaultBackgroundPlaybackDelegate: BackgroundPlaybackDelegate {
42
+ func shouldContinueAudioPlaybackInBackground() -> Bool { false }
43
+ }
44
+
33
45
  #endif