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.
- package/CHANGELOG.md +25 -0
- package/android/src/main/java/com/theoplayer/presentation/PipUtils.kt +248 -0
- package/android/src/main/java/com/theoplayer/presentation/PresentationManager.kt +22 -55
- package/android/src/main/java/com/theoplayer/track/TrackListAdapter.kt +3 -2
- package/android/src/main/java/com/theoplayer/util/TypeUtils.kt +6 -6
- package/android/src/main/res/drawable/ic_fast_forward.xml +9 -0
- package/android/src/main/res/drawable/ic_rewind.xml +9 -0
- package/android/src/main/res/values/strings.xml +8 -0
- package/ios/THEOplayerRCTMainEventHandler.swift +15 -12
- package/ios/THEOplayerRCTTrackMetadataAggregator.swift +2 -2
- package/ios/THEOplayerRCTView.swift +3 -1
- package/ios/backgroundAudio/THEOplayerRCTNowPlayingManager.swift +7 -5
- package/ios/backgroundAudio/THEOplayerRCTView+BackgroundAudioConfig.swift +12 -0
- package/lib/commonjs/api/source/SourceDescription.js.map +1 -1
- package/lib/commonjs/internal/THEOplayerView.js +6 -1
- package/lib/commonjs/internal/THEOplayerView.js.map +1 -1
- package/lib/commonjs/internal/THEOplayerView.web.js +7 -7
- package/lib/commonjs/internal/THEOplayerView.web.js.map +1 -1
- package/lib/commonjs/internal/adapter/THEOplayerAdapter.js +1 -1
- package/lib/commonjs/internal/adapter/THEOplayerAdapter.js.map +1 -1
- package/lib/commonjs/internal/adapter/web/TrackUtils.js +3 -2
- package/lib/commonjs/internal/adapter/web/TrackUtils.js.map +1 -1
- package/lib/commonjs/internal/adapter/web/WebMediaSession.js +17 -29
- package/lib/commonjs/internal/adapter/web/WebMediaSession.js.map +1 -1
- package/lib/commonjs/internal/adapter/web/WebPresentationModeManager.js +1 -1
- package/lib/commonjs/internal/adapter/web/WebPresentationModeManager.js.map +1 -1
- package/lib/module/api/source/SourceDescription.js.map +1 -1
- package/lib/module/internal/THEOplayerView.js +6 -1
- package/lib/module/internal/THEOplayerView.js.map +1 -1
- package/lib/module/internal/THEOplayerView.web.js +7 -7
- package/lib/module/internal/THEOplayerView.web.js.map +1 -1
- package/lib/module/internal/adapter/THEOplayerAdapter.js +1 -1
- package/lib/module/internal/adapter/THEOplayerAdapter.js.map +1 -1
- package/lib/module/internal/adapter/web/TrackUtils.js +3 -2
- package/lib/module/internal/adapter/web/TrackUtils.js.map +1 -1
- package/lib/module/internal/adapter/web/WebMediaSession.js +15 -29
- package/lib/module/internal/adapter/web/WebMediaSession.js.map +1 -1
- package/lib/module/internal/adapter/web/WebPresentationModeManager.js +1 -1
- package/lib/module/internal/adapter/web/WebPresentationModeManager.js.map +1 -1
- package/lib/typescript/api/source/SourceDescription.d.ts +6 -0
- package/lib/typescript/internal/adapter/web/WebMediaSession.d.ts +2 -1
- package/package.json +1 -1
- package/react-native-theoplayer.podspec +0 -1
- package/src/api/source/SourceDescription.ts +8 -0
- package/src/internal/THEOplayerView.tsx +6 -1
- package/src/internal/THEOplayerView.web.tsx +4 -7
- package/src/internal/adapter/THEOplayerAdapter.ts +1 -1
- package/src/internal/adapter/web/TrackUtils.ts +3 -2
- package/src/internal/adapter/web/WebMediaSession.ts +14 -29
- 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
|
-
|
|
51
|
+
onEnterPip()
|
|
61
52
|
} else {
|
|
62
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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)
|
|
79
|
-
cuePayload.putDouble(PROP_ENDTIME, (1e3 * cue.endTime)
|
|
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
|
-
|
|
10
|
-
|
|
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
|
|
137
|
+
if let wplayer = player,
|
|
138
|
+
let forwardedVolumeChangeEvent = self?.onNativeVolumeChange {
|
|
138
139
|
forwardedVolumeChangeEvent(
|
|
139
140
|
[
|
|
140
141
|
"volume": event.volume,
|
|
141
|
-
"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
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
269
|
-
|
|
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
|
|
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
|
-
|
|
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
|