@24i/smartapps-bigscreen-rnv-build-engine 0.17.21-alpha.2697 → 0.17.22
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/dist/src/sdks/sdk-androidtv/sdkAndroidtv.js +7 -2
- package/dist/templates/platforms/androidtv/project/app/build.gradle +17 -4
- package/dist/templates/platforms/androidtv/project/app/src/main/java/com/twentyfouri/tvbridge/configuration/BridgeConfig.kt +1 -0
- package/dist/templates/platforms/androidtv/project/app/src/main/java/com/twentyfouri/tvbridge/global/utils/PlayerFactoryConfigured.kt +11 -0
- package/dist/templates/platforms/androidtv/project/app/src/main/java/com/twentyfouri/tvbridge/global/utils/PlayerFactoryExoPlayerNpaw.kt +521 -0
- package/dist/templates/platforms/androidtv/project/build.gradle +2 -0
- package/package.json +1 -1
|
@@ -145,8 +145,8 @@ const configureProject = (c) => {
|
|
|
145
145
|
}
|
|
146
146
|
const appId = rnv_1.Common.getConfigProp(c, c.platform, 'id', '');
|
|
147
147
|
const minSdkVersion = rnv_1.Common.getConfigProp(c, c.platform, 'minSdkVersion', 21);
|
|
148
|
-
const targetSdkVersion = rnv_1.Common.getConfigProp(c, c.platform, 'targetSdkVersion',
|
|
149
|
-
const compileSdkVersion = rnv_1.Common.getConfigProp(c, c.platform, 'compileSdkVersion',
|
|
148
|
+
const targetSdkVersion = rnv_1.Common.getConfigProp(c, c.platform, 'targetSdkVersion', 34);
|
|
149
|
+
const compileSdkVersion = rnv_1.Common.getConfigProp(c, c.platform, 'compileSdkVersion', 34);
|
|
150
150
|
let versionCode = rnv_1.Common.getConfigProp(c, c.platform, 'versionCode');
|
|
151
151
|
if (typeof versionCode === 'undefined') {
|
|
152
152
|
const major = `${semVerObj.major}`.padStart(2, '0');
|
|
@@ -202,12 +202,17 @@ const configureEntryUrl = (c) => {
|
|
|
202
202
|
const entryUrl = getHostedAppEntryUrl(c);
|
|
203
203
|
const theoLicense = process.env['THEO_LICENSE'] ||
|
|
204
204
|
rnv_1.Common.getConfigProp(c, c.platform, 'theoLicense', '');
|
|
205
|
+
const npawAccountCode = process.env['NPAW_ACCOUNT_CODE'] ||
|
|
206
|
+
rnv_1.Common.getConfigProp(c, c.platform, 'npawAccountCode', '');
|
|
205
207
|
const injects = [{
|
|
206
208
|
pattern: '{{APP_URL}}',
|
|
207
209
|
override: entryUrl,
|
|
208
210
|
}, {
|
|
209
211
|
pattern: '{{THEO_LICENSE}}',
|
|
210
212
|
override: theoLicense,
|
|
213
|
+
}, {
|
|
214
|
+
pattern: '{{NPAW_ACCOUNT_CODE}}',
|
|
215
|
+
override: npawAccountCode,
|
|
211
216
|
}];
|
|
212
217
|
rnv_1.Common.addSystemInjects(c, injects);
|
|
213
218
|
rnv_1.FileUtils.writeCleanFile(buildGradleFilePath, buildGradleFilePath, injects, null, c);
|
|
@@ -4,6 +4,8 @@ apply plugin: 'kotlin-kapt'
|
|
|
4
4
|
|
|
5
5
|
def theoLicense = '{{THEO_LICENSE}}'
|
|
6
6
|
def useTheoPlayer = theoLicense != ''
|
|
7
|
+
def npawAccountCode = '{{NPAW_ACCOUNT_CODE}}'
|
|
8
|
+
def useNpawPlugin = npawAccountCode != ''
|
|
7
9
|
|
|
8
10
|
android {
|
|
9
11
|
namespace = 'com.twentyfouri.tvbridge'
|
|
@@ -37,6 +39,7 @@ android {
|
|
|
37
39
|
|
|
38
40
|
buildConfigField "String", "APP_URL", '"{{APP_URL}}"'
|
|
39
41
|
buildConfigField "String", "THEO_LICENSE", "\"$theoLicense\""
|
|
42
|
+
buildConfigField "String", "NPAW_ACCOUNT_CODE", "\"$npawAccountCode\""
|
|
40
43
|
buildConfigField "Boolean", "startWithHighVideoQuality", "true"
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -86,7 +89,7 @@ dependencies {
|
|
|
86
89
|
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
|
|
87
90
|
|
|
88
91
|
compileOnly "javax.annotation:jsr250-api:1.0"
|
|
89
|
-
implementation 'androidx.core:core-ktx:1.
|
|
92
|
+
implementation 'androidx.core:core-ktx:1.12.0'
|
|
90
93
|
implementation "androidx.appcompat:appcompat:1.6.1"
|
|
91
94
|
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
|
|
92
95
|
|
|
@@ -94,17 +97,27 @@ dependencies {
|
|
|
94
97
|
kapt "com.google.dagger:dagger-compiler:2.48.1"
|
|
95
98
|
|
|
96
99
|
implementation "com.squareup.retrofit2:retrofit:2.9.0"
|
|
97
|
-
implementation "com.squareup.retrofit2:converter-gson:2.
|
|
100
|
+
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
|
|
98
101
|
implementation "com.squareup.okhttp3:logging-interceptor:4.9.2"
|
|
99
102
|
implementation "com.squareup.okhttp3:okhttp:4.9.2"
|
|
100
103
|
if (useTheoPlayer) {
|
|
101
104
|
compileOnly "com.twentyfouri.player:exoplayer:0.7.0-alpha.3"
|
|
102
105
|
implementation "com.twentyfouri.player:theoplayer:0.7.0-alpha.3"
|
|
103
|
-
implementation "com.theoplayer.theoplayer-sdk-android:core:6.
|
|
106
|
+
implementation "com.theoplayer.theoplayer-sdk-android:core:6.2.1"
|
|
104
107
|
} else {
|
|
105
108
|
implementation "com.twentyfouri.player:exoplayer:0.7.0-alpha.3"
|
|
106
109
|
compileOnly "com.twentyfouri.player:theoplayer:0.7.0-alpha.3"
|
|
107
|
-
compileOnly "com.theoplayer.theoplayer-sdk-android:core:6.
|
|
110
|
+
compileOnly "com.theoplayer.theoplayer-sdk-android:core:6.2.1"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (useNpawPlugin) {
|
|
114
|
+
implementation 'com.npaw.plugin:plugin:7.2.31'
|
|
115
|
+
implementation 'com.npaw.plugin:plugin-exoplayer:2.19.1-7.2.31'
|
|
116
|
+
implementation 'com.google.android.exoplayer:extension-okhttp:2.19.1'
|
|
117
|
+
} else {
|
|
118
|
+
compileOnly 'com.npaw.plugin:plugin:7.2.31'
|
|
119
|
+
compileOnly 'com.npaw.plugin:plugin-exoplayer:2.19.1-7.2.31'
|
|
120
|
+
compileOnly 'com.google.android.exoplayer:extension-okhttp:2.19.1'
|
|
108
121
|
}
|
|
109
122
|
|
|
110
123
|
implementation project(':common')
|
|
@@ -11,6 +11,9 @@ object PlayerFactoryConfigured {
|
|
|
11
11
|
if (bridgeConfig.theoLicense.isNotEmpty()) {
|
|
12
12
|
factory = factory ?: buildTheoFactory(bridgeConfig)
|
|
13
13
|
}
|
|
14
|
+
if (bridgeConfig.npawAccount.isNotEmpty()) {
|
|
15
|
+
factory = factory ?: buildNpawFactory(bridgeConfig)
|
|
16
|
+
}
|
|
14
17
|
factory = factory ?: buildExoFactory(bridgeConfig)
|
|
15
18
|
return factory ?: throw Exception("Cannot create player factory")
|
|
16
19
|
}
|
|
@@ -23,6 +26,14 @@ object PlayerFactoryConfigured {
|
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
private fun buildNpawFactory(bridgeConfig: BridgeConfig): PlayerFactoryBase? {
|
|
30
|
+
try {
|
|
31
|
+
return PlayerFactoryExoPlayerNpaw(bridgeConfig)
|
|
32
|
+
} catch (e: NoClassDefFoundError) {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
private fun buildTheoFactory(bridgeConfig: BridgeConfig): PlayerFactoryBase? {
|
|
27
38
|
try {
|
|
28
39
|
return PlayerFactoryTheoPlayerCustomized(bridgeConfig)
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
package com.twentyfouri.tvbridge.global.utils
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.app.Application
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.ContextWrapper
|
|
7
|
+
import android.os.Bundle
|
|
8
|
+
import android.os.Parcelable
|
|
9
|
+
import android.util.AttributeSet
|
|
10
|
+
import android.view.Gravity
|
|
11
|
+
import android.view.MotionEvent
|
|
12
|
+
import android.view.SurfaceView
|
|
13
|
+
import android.view.View
|
|
14
|
+
import android.view.ViewGroup
|
|
15
|
+
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
|
16
|
+
import android.widget.FrameLayout
|
|
17
|
+
import android.widget.ImageView
|
|
18
|
+
import android.widget.Space
|
|
19
|
+
import androidx.appcompat.widget.AppCompatImageView
|
|
20
|
+
import androidx.lifecycle.DefaultLifecycleObserver
|
|
21
|
+
import androidx.lifecycle.Lifecycle
|
|
22
|
+
import androidx.lifecycle.LifecycleOwner
|
|
23
|
+
import com.google.android.exoplayer2.C
|
|
24
|
+
import com.google.android.exoplayer2.ExoPlayerLibraryInfo
|
|
25
|
+
import com.google.android.exoplayer2.Player
|
|
26
|
+
import com.google.android.exoplayer2.drm.DefaultDrmSessionManager
|
|
27
|
+
import com.google.android.exoplayer2.drm.DrmSessionManager
|
|
28
|
+
import com.google.android.exoplayer2.drm.FrameworkMediaDrm
|
|
29
|
+
import com.google.android.exoplayer2.drm.MediaDrmCallback
|
|
30
|
+
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
|
|
31
|
+
import com.google.android.exoplayer2.text.Cue
|
|
32
|
+
import com.google.android.exoplayer2.text.CueGroup
|
|
33
|
+
import com.google.android.exoplayer2.text.TextOutput
|
|
34
|
+
import com.google.android.exoplayer2.ui.AdOverlayInfo
|
|
35
|
+
import com.google.android.exoplayer2.ui.AdViewProvider
|
|
36
|
+
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
|
|
37
|
+
import com.google.android.exoplayer2.ui.SubtitleView
|
|
38
|
+
import com.google.android.exoplayer2.upstream.HttpDataSource
|
|
39
|
+
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
|
|
40
|
+
import com.google.android.exoplayer2.video.VideoSize
|
|
41
|
+
import com.npaw.NpawPlugin
|
|
42
|
+
import com.npaw.analytics.video.VideoAdapter
|
|
43
|
+
import com.npaw.analytics.video.VideoOptions
|
|
44
|
+
import com.npaw.core.options.AnalyticsOptions
|
|
45
|
+
import com.npaw.exoplayer.ExoPlayerAdapter
|
|
46
|
+
import com.npaw.exoplayer.ExoPlayerBalancer
|
|
47
|
+
import com.twentyfouri.player.base.CastingEnded
|
|
48
|
+
import com.twentyfouri.player.base.CastingStarted
|
|
49
|
+
import com.twentyfouri.player.base.DrmPlayready
|
|
50
|
+
import com.twentyfouri.player.base.DrmWidevine
|
|
51
|
+
import com.twentyfouri.player.base.EventListenerDispose
|
|
52
|
+
import com.twentyfouri.player.base.PlayerBase
|
|
53
|
+
import com.twentyfouri.player.base.PlayerGesturesTarget
|
|
54
|
+
import com.twentyfouri.player.base.PlayerSurface
|
|
55
|
+
import com.twentyfouri.player.base.ResizeEvent
|
|
56
|
+
import com.twentyfouri.player.base.ScaleType
|
|
57
|
+
import com.twentyfouri.player.base.Source
|
|
58
|
+
import com.twentyfouri.player.base.SourceAuthorization
|
|
59
|
+
import com.twentyfouri.player.base.SourceDrm
|
|
60
|
+
import com.twentyfouri.player.base.SourceMetadata
|
|
61
|
+
import com.twentyfouri.player.base.addEventListener
|
|
62
|
+
import com.twentyfouri.player.base.buildUpon
|
|
63
|
+
import com.twentyfouri.player.base.getBoolean
|
|
64
|
+
import com.twentyfouri.player.base.getString
|
|
65
|
+
import com.twentyfouri.player.exoplayer.AuthorizedDataSourceProvider
|
|
66
|
+
import com.twentyfouri.player.exoplayer.DefaultDrmSessionManagerProvider
|
|
67
|
+
import com.twentyfouri.player.exoplayer.DefaultPlayerComponents
|
|
68
|
+
import com.twentyfouri.player.exoplayer.DefaultSharedComponents
|
|
69
|
+
import com.twentyfouri.player.exoplayer.DrmSessionManagerProvider
|
|
70
|
+
import com.twentyfouri.player.exoplayer.PlayerComponents
|
|
71
|
+
import com.twentyfouri.player.exoplayer.PlayerExoPlayer
|
|
72
|
+
import com.twentyfouri.player.exoplayer.PlayerFactoryExoPlayer
|
|
73
|
+
import com.twentyfouri.player.exoplayer.PlayerProxy
|
|
74
|
+
import com.twentyfouri.player.exoplayer.PlayerViewAdvertisement
|
|
75
|
+
import com.twentyfouri.player.exoplayer.SharedComponents
|
|
76
|
+
import com.twentyfouri.tvbridge.BuildConfig
|
|
77
|
+
import com.twentyfouri.tvbridge.configuration.BridgeConfig
|
|
78
|
+
import com.npaw.core.util.extensions.Log as NpawLog
|
|
79
|
+
|
|
80
|
+
class PlayerFactoryExoPlayerNpaw(
|
|
81
|
+
private val config: BridgeConfig
|
|
82
|
+
) : PlayerFactoryExoPlayer(), PlayerFactoryVersion {
|
|
83
|
+
|
|
84
|
+
override val engineName: String = "Exoplayer"
|
|
85
|
+
|
|
86
|
+
override val engineVersion: String = ExoPlayerLibraryInfo.VERSION
|
|
87
|
+
|
|
88
|
+
override fun createSharedComponents(context: Context): SharedComponents =
|
|
89
|
+
CustomizedSharedComponents(context)
|
|
90
|
+
|
|
91
|
+
override fun createView(parent: ViewGroup): View {
|
|
92
|
+
return NpawPlayerView(parent.context).also {
|
|
93
|
+
val basePlayer = createPlayer(parent.context)
|
|
94
|
+
val npawPlugin = (
|
|
95
|
+
(basePlayer.components as CustomizedPlayerComponents)
|
|
96
|
+
.sharedComponents as CustomizedSharedComponents
|
|
97
|
+
).npawPlugin
|
|
98
|
+
val npawPlayer = NpawPlayer(basePlayer, npawPlugin)
|
|
99
|
+
trackPlayer(npawPlayer)
|
|
100
|
+
it.player = npawPlayer
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override fun getPlayerSurface(view: View): PlayerSurface {
|
|
105
|
+
return view as NpawPlayerView
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
override fun getPlayer(view: View): PlayerBase {
|
|
109
|
+
return (view as NpawPlayerView).player
|
|
110
|
+
?: throw IllegalStateException("Player was not initialized by PlayerFactory")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private inner class CustomizedSharedComponents(context: Context) : DefaultSharedComponents(context) {
|
|
114
|
+
|
|
115
|
+
val application = context.applicationContext as? Application
|
|
116
|
+
?: throw IllegalStateException("Missing application")
|
|
117
|
+
|
|
118
|
+
val npawPlugin: NpawPlugin = NpawPlugin
|
|
119
|
+
.Builder(application, config.npawAccount)
|
|
120
|
+
.setLogLevel(if (BuildConfig.DEBUG) NpawLog.Level.VERBOSE else NpawLog.Level.ERROR)
|
|
121
|
+
.build()
|
|
122
|
+
|
|
123
|
+
val npawBalancer = ExoPlayerBalancer(npawPlugin)
|
|
124
|
+
|
|
125
|
+
override fun destroy() {
|
|
126
|
+
super.destroy()
|
|
127
|
+
npawPlugin.destroy()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
override val drmProvider: DrmSessionManagerProvider by lazy { CustomizedDrmSessionManagerProvider() }
|
|
131
|
+
|
|
132
|
+
override val applicationName: String
|
|
133
|
+
get() = config.playerAppName ?: super.applicationName
|
|
134
|
+
|
|
135
|
+
override fun buildIsolatedComponents(): PlayerComponents {
|
|
136
|
+
return CustomizedPlayerComponents(this)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
override val baseHttpFactory: HttpDataSource.Factory by lazy {
|
|
140
|
+
npawBalancer.dataSourceFactory as OkHttpDataSource.Factory
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
override val dataSourceFactory: AuthorizedDataSourceProvider by lazy {
|
|
144
|
+
object : AuthorizedDataSourceProvider {
|
|
145
|
+
override fun buildDataSourceFactory(authorization: SourceAuthorization?): CacheDataSource.Factory {
|
|
146
|
+
return buildReadOnlyDataSourceFactory(baseHttpFactory)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
override fun buildHttpDataSourceFactory(authorization: SourceAuthorization?): HttpDataSource.Factory {
|
|
150
|
+
return baseHttpFactory
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
override val cacheDataSourceFactory: AuthorizedDataSourceProvider by lazy {
|
|
156
|
+
dataSourceFactory
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private inner class CustomizedPlayerComponents(shared: SharedComponents) : DefaultPlayerComponents(shared) {
|
|
161
|
+
override val handleAudioFocus: Boolean
|
|
162
|
+
get() = false
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private class CustomizedDrmSessionManagerProvider : DefaultDrmSessionManagerProvider() {
|
|
166
|
+
override fun buildDrmSessionManager(
|
|
167
|
+
dataSourceFactory: AuthorizedDataSourceProvider,
|
|
168
|
+
source: Source,
|
|
169
|
+
sourceDrm: SourceDrm
|
|
170
|
+
): DrmSessionManager? {
|
|
171
|
+
if (sourceDrm is SourceDrmOverridden) {
|
|
172
|
+
return buildDrmOverridden(dataSourceFactory, sourceDrm)
|
|
173
|
+
} else {
|
|
174
|
+
return super.buildDrmSessionManager(dataSourceFactory, source, sourceDrm)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private fun buildDrmOverridden(dataSourceFactory: AuthorizedDataSourceProvider, sourceDrm: SourceDrmOverridden): DrmSessionManager? {
|
|
179
|
+
val drmUuid = when (sourceDrm.packageName) {
|
|
180
|
+
DrmWidevine.packageName -> C.WIDEVINE_UUID
|
|
181
|
+
DrmPlayready.packageName -> C.PLAYREADY_UUID
|
|
182
|
+
else -> C.UUID_NIL
|
|
183
|
+
}
|
|
184
|
+
val licenseUrl = sourceDrm.licenseServer?.toString() ?: ""
|
|
185
|
+
val forceLicenseUrl = sourceDrm.forceLicenseServer
|
|
186
|
+
val httpDataSourceFactory = dataSourceFactory.buildHttpDataSourceFactory(sourceDrm.authorization)
|
|
187
|
+
|
|
188
|
+
val drmCallback = LoggingDrmCallback(
|
|
189
|
+
OverriddenHttpMediaDrmCallback(
|
|
190
|
+
licenseUrl,
|
|
191
|
+
forceLicenseUrl,
|
|
192
|
+
httpDataSourceFactory
|
|
193
|
+
),
|
|
194
|
+
licenseUrl
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return DefaultDrmSessionManager.Builder()
|
|
198
|
+
.setUuidAndExoMediaDrmProvider(drmUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
|
|
199
|
+
.setMultiSession(false)
|
|
200
|
+
.build(drmCallback)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
override fun buildDrmCallback(licenseUrl: String, forceLicenseUrl: Boolean, httpDataSourceFactory: HttpDataSource.Factory): MediaDrmCallback {
|
|
204
|
+
return LoggingDrmCallback(super.buildDrmCallback(licenseUrl, forceLicenseUrl, httpDataSourceFactory), licenseUrl)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private class NpawPlayer(
|
|
209
|
+
val basePlayer: PlayerExoPlayer,
|
|
210
|
+
val npawPlugin: NpawPlugin
|
|
211
|
+
) : PlayerBase by basePlayer {
|
|
212
|
+
var exoPlayerAdapter: ExoPlayerAdapter? = null
|
|
213
|
+
|
|
214
|
+
var videoAdapter: VideoAdapter? = null
|
|
215
|
+
set(value) {
|
|
216
|
+
field?.destroy()
|
|
217
|
+
field = value
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fun extractYouboraAnalyticsOptions(source: Source?, target: AnalyticsOptions) {
|
|
221
|
+
if (source == null) return
|
|
222
|
+
val jsonAnalyticsArray = source.extras?.optJSONArray("analytics") ?: return
|
|
223
|
+
for (index in 0 until jsonAnalyticsArray.length()) {
|
|
224
|
+
val jsonAnalytics = jsonAnalyticsArray.getJSONObject(index)
|
|
225
|
+
if (jsonAnalytics.getString("integration", "") != "youbora") continue
|
|
226
|
+
jsonAnalytics.keys().forEach { key ->
|
|
227
|
+
when (key) {
|
|
228
|
+
"app.name" -> target.appName = jsonAnalytics.getString(key, null)
|
|
229
|
+
"app.releaseVersion" -> target.appReleaseVersion = jsonAnalytics.getString(key, null)
|
|
230
|
+
"device.id" -> target.deviceId = jsonAnalytics.getString(key, null)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fun extractYouboraVideoOptions(source: Source?, target: VideoOptions) {
|
|
237
|
+
if (source == null) return
|
|
238
|
+
val jsonAnalyticsArray = source.extras?.optJSONArray("analytics") ?: return
|
|
239
|
+
for (index in 0 until jsonAnalyticsArray.length()) {
|
|
240
|
+
val jsonAnalytics = jsonAnalyticsArray.getJSONObject(index)
|
|
241
|
+
if (jsonAnalytics.getString("integration", "") != "youbora") continue
|
|
242
|
+
jsonAnalytics.keys().forEach { key ->
|
|
243
|
+
when (key) {
|
|
244
|
+
"content.id" -> target.contentId = jsonAnalytics.getString(key, null)
|
|
245
|
+
"content.title" -> target.contentTitle = jsonAnalytics.getString(key, null)
|
|
246
|
+
"content.isLive" -> target.live = jsonAnalytics.getBoolean(key, false)
|
|
247
|
+
"content.type" -> target.contentType = jsonAnalytics.getString(key, null)
|
|
248
|
+
"content.program" -> target.program = jsonAnalytics.getString(key, null)
|
|
249
|
+
"content.channel" -> target.contentChannel = jsonAnalytics.getString(key, null)
|
|
250
|
+
"content.episodeTitle" -> target.contentEpisodeTitle = jsonAnalytics.getString(key, null)
|
|
251
|
+
"content.season" -> target.contentSeason = jsonAnalytics.getString(key, null)
|
|
252
|
+
"content.tvShow" -> target.contentTvShow = jsonAnalytics.getString(key, null)
|
|
253
|
+
"content.customDimension.1" -> target.contentCustomDimension1 = jsonAnalytics.getString(key, null)
|
|
254
|
+
"content.customDimension.2" -> target.contentCustomDimension2 = jsonAnalytics.getString(key, null)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
override var source: Source?
|
|
261
|
+
get() = basePlayer.source
|
|
262
|
+
set(value) {
|
|
263
|
+
exoPlayerAdapter = null
|
|
264
|
+
videoAdapter = null
|
|
265
|
+
if (value != null) {
|
|
266
|
+
val videoOptions = VideoOptions()
|
|
267
|
+
extractYouboraAnalyticsOptions(value, npawPlugin.analyticsOptions)
|
|
268
|
+
extractYouboraVideoOptions(value, videoOptions)
|
|
269
|
+
|
|
270
|
+
var adjustedSource = value
|
|
271
|
+
if (value.metadata?.title.isNullOrEmpty() && !videoOptions.contentTitle.isNullOrEmpty()) {
|
|
272
|
+
adjustedSource = value.buildUpon {
|
|
273
|
+
metadata = (metadata?.buildUpon() ?: SourceMetadata.Builder()).apply {
|
|
274
|
+
title = videoOptions.contentTitle
|
|
275
|
+
}.build()
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
basePlayer.source = adjustedSource
|
|
280
|
+
exoPlayerAdapter = ExoPlayerAdapter(basePlayer.components.context, basePlayer.exoPlayer)
|
|
281
|
+
videoAdapter = npawPlugin.videoBuilder()
|
|
282
|
+
.setPlayerAdapter(exoPlayerAdapter)
|
|
283
|
+
.setOptions(videoOptions)
|
|
284
|
+
.build()
|
|
285
|
+
} else {
|
|
286
|
+
basePlayer.source = null
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
override fun destroy() {
|
|
291
|
+
exoPlayerAdapter = null
|
|
292
|
+
videoAdapter = null
|
|
293
|
+
basePlayer.destroy()
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
class NpawPlayerView @JvmOverloads constructor(
|
|
298
|
+
context: Context,
|
|
299
|
+
attrs: AttributeSet? = null,
|
|
300
|
+
defStyleAttr: Int = 0
|
|
301
|
+
) : FrameLayout(context, attrs, defStyleAttr), AdViewProvider, PlayerSurface {
|
|
302
|
+
|
|
303
|
+
private val componentListener = ComponentListener()
|
|
304
|
+
private val onDetachPlayer = EventListenerDispose()
|
|
305
|
+
|
|
306
|
+
val videoFrame: AspectRatioFrameLayout = AspectRatioFrameLayout(context)
|
|
307
|
+
val surfaceView: View = when {
|
|
308
|
+
isInEditMode -> Space(context)
|
|
309
|
+
else -> SurfaceView(context)
|
|
310
|
+
}
|
|
311
|
+
val posterView: ImageView = AppCompatImageView(context).also {
|
|
312
|
+
it.scaleType = ImageView.ScaleType.FIT_CENTER
|
|
313
|
+
}
|
|
314
|
+
val subtitleView: SubtitleView = SubtitleView(context)
|
|
315
|
+
val adOverlay: PlayerViewAdvertisement = PlayerViewAdvertisement(context).also {
|
|
316
|
+
it.fitsSystemWindows = true
|
|
317
|
+
}
|
|
318
|
+
override var adObstructions: List<View> = emptyList()
|
|
319
|
+
override var gesturesTarget: PlayerGesturesTarget?
|
|
320
|
+
get() = adOverlay.gesturesTarget
|
|
321
|
+
set(value) {
|
|
322
|
+
adOverlay.gesturesTarget = value
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
var player: PlayerBase? = null
|
|
326
|
+
set(value) {
|
|
327
|
+
if (field == value) return
|
|
328
|
+
field?.also { detachPlayer(it) }
|
|
329
|
+
field = value
|
|
330
|
+
field?.also { attachPlayer(it) }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
var pausedByBackground: Boolean = false
|
|
334
|
+
get() = player?.asPlayerExoPlayer()?.pausedByBackground ?: field
|
|
335
|
+
set(value) {
|
|
336
|
+
field = value
|
|
337
|
+
player?.asPlayerExoPlayer()?.pausedByBackground = value
|
|
338
|
+
}
|
|
339
|
+
var surfaceAttached: Boolean = false
|
|
340
|
+
|
|
341
|
+
init {
|
|
342
|
+
if (!isInEditMode) {
|
|
343
|
+
subtitleView.setUserDefaultTextSize()
|
|
344
|
+
subtitleView.setUserDefaultStyle()
|
|
345
|
+
subtitleView.setApplyEmbeddedStyles(false)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
addView(videoFrame, LayoutParams(MATCH_PARENT, MATCH_PARENT, Gravity.CENTER))
|
|
349
|
+
videoFrame.addView(surfaceView, MATCH_PARENT, MATCH_PARENT)
|
|
350
|
+
videoFrame.addView(subtitleView, MATCH_PARENT, MATCH_PARENT)
|
|
351
|
+
addView(posterView, MATCH_PARENT, MATCH_PARENT)
|
|
352
|
+
addView(adOverlay, MATCH_PARENT, MATCH_PARENT)
|
|
353
|
+
|
|
354
|
+
context.lifecycle?.addObserver(componentListener)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private val Context.lifecycle: Lifecycle?
|
|
358
|
+
get() = when (this) {
|
|
359
|
+
is LifecycleOwner -> lifecycle
|
|
360
|
+
is ContextWrapper -> baseContext.lifecycle
|
|
361
|
+
else -> null
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private fun detachPlayer(player: PlayerBase) {
|
|
365
|
+
onDetachPlayer()
|
|
366
|
+
player.asPlayerExoPlayer()?.also { detachSurface(it) }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private fun detachSurface(player: PlayerExoPlayer) {
|
|
370
|
+
if (!surfaceAttached) return
|
|
371
|
+
surfaceAttached = false
|
|
372
|
+
player.exoPlayer.removeListener(componentListener)
|
|
373
|
+
player.adViewProvider = null
|
|
374
|
+
player.posterSurface = null
|
|
375
|
+
player.videoSurface = null
|
|
376
|
+
subtitleView.setCues(null)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private fun attachPlayer(player: PlayerBase) {
|
|
380
|
+
player.asPlayerExoPlayer()?.also { attachSurface(it) }
|
|
381
|
+
player.fullscreen = isFullscreen
|
|
382
|
+
onDetachPlayer += player.addEventListener<ResizeEvent> {
|
|
383
|
+
videoFrame.resizeMode = player.scaleType.toResizeMode()
|
|
384
|
+
}
|
|
385
|
+
onDetachPlayer += player.addEventListener<CastingStarted> {
|
|
386
|
+
player.asPlayerExoPlayer()?.also { detachSurface(it) }
|
|
387
|
+
}
|
|
388
|
+
onDetachPlayer += player.addEventListener<CastingEnded> {
|
|
389
|
+
player.asPlayerExoPlayer()?.also { attachSurface(it) }
|
|
390
|
+
}
|
|
391
|
+
videoFrame.resizeMode = player.scaleType.toResizeMode()
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private fun attachSurface(player: PlayerExoPlayer) {
|
|
395
|
+
if (surfaceAttached) return
|
|
396
|
+
surfaceAttached = true
|
|
397
|
+
player.exoPlayer.addListener(componentListener)
|
|
398
|
+
player.adViewProvider = this
|
|
399
|
+
player.posterSurface = posterView
|
|
400
|
+
player.videoSurface = surfaceView as? SurfaceView
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private fun PlayerBase.asPlayerExoPlayer(): PlayerExoPlayer? = when {
|
|
404
|
+
this is PlayerExoPlayer -> this
|
|
405
|
+
this is PlayerProxy -> this.base?.asPlayerExoPlayer()
|
|
406
|
+
this is NpawPlayer -> this.basePlayer
|
|
407
|
+
else -> null
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
protected fun ScaleType?.toResizeMode(): Int = when (this) {
|
|
411
|
+
ScaleType.STRETCH -> AspectRatioFrameLayout.RESIZE_MODE_FILL
|
|
412
|
+
ScaleType.ZOOM -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
|
413
|
+
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private var isFullscreen = false
|
|
417
|
+
set(value) {
|
|
418
|
+
if (field == value) return
|
|
419
|
+
field = value
|
|
420
|
+
player?.fullscreen = isFullscreen
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
424
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
425
|
+
|
|
426
|
+
val displayMetrics = context.resources.displayMetrics
|
|
427
|
+
val screenWidth = displayMetrics.widthPixels
|
|
428
|
+
val minFullWidth = screenWidth * 9 / 10
|
|
429
|
+
val screenHeight = displayMetrics.heightPixels
|
|
430
|
+
val minFullHeight = screenHeight * 9 / 10
|
|
431
|
+
val width = right - left
|
|
432
|
+
val height = bottom - top
|
|
433
|
+
isFullscreen = width >= minFullWidth && height >= minFullHeight
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
@SuppressLint("ClickableViewAccessibility")
|
|
437
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
438
|
+
return gesturesTarget?.onDelegatedTouchEvent(event) ?: super.onTouchEvent(event)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
override fun getAdViewGroup(): ViewGroup =
|
|
442
|
+
adOverlay
|
|
443
|
+
|
|
444
|
+
override fun getAdOverlayInfos(): List<AdOverlayInfo> =
|
|
445
|
+
adObstructions.map { AdOverlayInfo.Builder(it, AdOverlayInfo.PURPOSE_CONTROLS).build() }
|
|
446
|
+
|
|
447
|
+
override fun onSaveInstanceState(): Parcelable {
|
|
448
|
+
val state = Bundle()
|
|
449
|
+
state.putParcelable("base", super.onSaveInstanceState())
|
|
450
|
+
state.putBoolean("pausedByBackground", pausedByBackground)
|
|
451
|
+
return state
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
override fun onRestoreInstanceState(state: Parcelable?) {
|
|
455
|
+
val bundle = state as? Bundle
|
|
456
|
+
if (bundle == null) {
|
|
457
|
+
super.onRestoreInstanceState(state)
|
|
458
|
+
} else {
|
|
459
|
+
super.onRestoreInstanceState(bundle.getParcelable("base"))
|
|
460
|
+
pausedByBackground = bundle.getBoolean("pausedByBackground")
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private inner class ComponentListener : TextOutput, Player.Listener,
|
|
465
|
+
DefaultLifecycleObserver {
|
|
466
|
+
override fun onCues(cueGroup: CueGroup) {
|
|
467
|
+
subtitleView.setCues(cueGroup.cues)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
@Deprecated("No longer used")
|
|
471
|
+
override fun onCues(cues: MutableList<Cue>) {
|
|
472
|
+
// only overridden because compiler is confused about multiple default implementations
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
override fun onVideoSizeChanged(videoSize: VideoSize) {
|
|
476
|
+
videoFrame.resizeMode = player?.scaleType.toResizeMode()
|
|
477
|
+
videoFrame.setAspectRatio(
|
|
478
|
+
when {
|
|
479
|
+
videoSize.height <= 0 -> 1f
|
|
480
|
+
else -> videoSize.width * videoSize.pixelWidthHeightRatio / videoSize.height
|
|
481
|
+
}
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
|
486
|
+
this@NpawPlayerView.keepScreenOn = isPlaying
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
override fun onPause(owner: LifecycleOwner) {
|
|
490
|
+
val player = this@NpawPlayerView.player ?: return
|
|
491
|
+
player.asPlayerExoPlayer()?.also {
|
|
492
|
+
detachSurface(it)
|
|
493
|
+
}
|
|
494
|
+
if (!player.continueInBackground && !player.paused && player.state.source != null) {
|
|
495
|
+
if (player.live) {
|
|
496
|
+
player.stop()
|
|
497
|
+
} else {
|
|
498
|
+
player.pause()
|
|
499
|
+
}
|
|
500
|
+
pausedByBackground = true
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
override fun onResume(owner: LifecycleOwner) {
|
|
505
|
+
val player = this@NpawPlayerView.player ?: return
|
|
506
|
+
if (pausedByBackground) {
|
|
507
|
+
pausedByBackground = false
|
|
508
|
+
player.play()
|
|
509
|
+
}
|
|
510
|
+
player.asPlayerExoPlayer()?.also {
|
|
511
|
+
attachSurface(it)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
override fun onDestroy(owner: LifecycleOwner) {
|
|
516
|
+
context.lifecycle?.removeObserver(this)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
package/package.json
CHANGED