@24i/smartapps-bigscreen-rnv-build-engine 0.17.21-alpha.2696 → 0.17.21

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/README.md CHANGED
@@ -228,7 +228,7 @@ If any platform needs any **platform-specific files**, those can be found also i
228
228
  ## How to set up new project with SmartApps BIGscreen build engine
229
229
  There are a few things that are not included in the package and need to be added manually.
230
230
  For example `renative.json` config file and `appConfigs` folder (both related to ReNative)
231
- and `.travis.yml`config file (related to Travis CI).
231
+ and `.github/workflows/*.yml` files (related to GitHub Actions CI).
232
232
 
233
233
  > To **start a new project** with usage of our build process,
234
234
  > please use [SmartApps BIGscreen Template repository](https://github.com/24i/smartapps-bigscreen-template),
@@ -37,15 +37,15 @@ const makePackagesPayload = (c) => {
37
37
  .flat();
38
38
  };
39
39
  const makeHeaderPayload = (c, title, buttons) => {
40
- const { travisBuild, gitHubRelease } = buttons || {};
40
+ const { gitHubActionsBuild, gitHubRelease } = buttons || {};
41
41
  const { owner, repo } = c.buildConfig.common.git || {};
42
42
  const version = getVersion(c);
43
43
  const envName = (0, resolvers_1.resolveBuildEnvName)(c.runtime.scheme);
44
44
  const buttonsData = [];
45
- if (travisBuild) {
45
+ if (gitHubActionsBuild) {
46
46
  buttonsData.push({
47
- text: 'View Travis Build Log',
48
- url: process.env.TRAVIS_BUILD_WEB_URL || `https://travis-ci.com/github/${owner}/${repo}/branches`,
47
+ text: 'View GitHub Actions Build Log',
48
+ url: process.env.CI_BUILD_WEB_URL || `https://github.com/${owner}/${repo}/actions/`,
49
49
  });
50
50
  }
51
51
  if (gitHubRelease) {
@@ -70,20 +70,20 @@ const makePayload = (c, title, buttons) => ({
70
70
  });
71
71
  const makeDevelopmentPayload = (c) => {
72
72
  const title = `There are new *development* builds for *${getCommonAppName(c)}* app!`;
73
- const buttons = { travisBuild: true };
73
+ const buttons = { gitHubActionsBuild: true };
74
74
  return makePayload(c, title, buttons);
75
75
  };
76
76
  exports.makeDevelopmentPayload = makeDevelopmentPayload;
77
77
  const makeReleaseCandidatePayload = (c) => {
78
78
  const title = `There are new *release candidate* builds for *${getCommonAppName(c)}* app!`;
79
- const buttons = { travisBuild: true };
79
+ const buttons = { gitHubActionsBuild: true };
80
80
  return makePayload(c, title, buttons);
81
81
  };
82
82
  exports.makeReleaseCandidatePayload = makeReleaseCandidatePayload;
83
83
  const makeDistributionPayload = (c) => {
84
84
  const title = `There are new *distribution* builds for *${getCommonAppName(c)}* app!`;
85
85
  const isGitHubRelease = c.runtime.scheme === buildSchemes_1.BuildSchemes.RELEASE_PROD_DIST;
86
- const buttons = { travisBuild: true, gitHubRelease: isGitHubRelease };
86
+ const buttons = { gitHubActionsBuild: true, gitHubRelease: isGitHubRelease };
87
87
  return makePayload(c, title, buttons);
88
88
  };
89
89
  exports.makeDistributionPayload = makeDistributionPayload;
@@ -27,8 +27,8 @@ const getSlackPayload = (params) => {
27
27
  ]),
28
28
  slackBlockKitBuilder_1.Actions.buttons([
29
29
  {
30
- url: process.env.TRAVIS_BUILD_WEB_URL || 'https://travis-ci.com/',
31
- text: 'View Travis Build Log',
30
+ url: process.env.CI_BUILD_WEB_URL || 'https://github.com/',
31
+ text: 'View GitHub Actions Build Log',
32
32
  },
33
33
  ...(logsZipUrl ? [{
34
34
  text: 'View Suitest Log files',
@@ -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', 33);
149
- const compileSdkVersion = rnv_1.Common.getConfigProp(c, c.platform, 'compileSdkVersion', 33);
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.9.0'
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.5.0"
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.8.0"
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.8.0"
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')
@@ -14,4 +14,5 @@ class BridgeConfig {
14
14
  var customizableUrl: Boolean = BuildConfig.DEBUG
15
15
  var startWithHighVideoQuality: Boolean = false
16
16
  var theoLicense: String = BuildConfig.THEO_LICENSE
17
+ var npawAccount: String = BuildConfig.NPAW_ACCOUNT_CODE
17
18
  }
@@ -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,255 @@
1
+ package com.twentyfouri.tvbridge.global.utils
2
+
3
+ import android.app.Application
4
+ import android.content.Context
5
+ import android.view.View
6
+ import android.view.ViewGroup
7
+ import com.google.android.exoplayer2.C
8
+ import com.google.android.exoplayer2.ExoPlayerLibraryInfo
9
+ import com.google.android.exoplayer2.drm.DefaultDrmSessionManager
10
+ import com.google.android.exoplayer2.drm.DrmSessionManager
11
+ import com.google.android.exoplayer2.drm.FrameworkMediaDrm
12
+ import com.google.android.exoplayer2.drm.MediaDrmCallback
13
+ import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
14
+ import com.google.android.exoplayer2.upstream.HttpDataSource
15
+ import com.google.android.exoplayer2.upstream.cache.CacheDataSource
16
+ import com.npaw.NpawPlugin
17
+ import com.npaw.analytics.video.VideoAdapter
18
+ import com.npaw.analytics.video.VideoOptions
19
+ import com.npaw.core.options.AnalyticsOptions
20
+ import com.npaw.exoplayer.ExoPlayerAdapter
21
+ import com.npaw.exoplayer.ExoPlayerBalancer
22
+ import com.twentyfouri.player.base.DrmPlayready
23
+ import com.twentyfouri.player.base.DrmWidevine
24
+ import com.twentyfouri.player.base.PlayerBase
25
+ import com.twentyfouri.player.base.Source
26
+ import com.twentyfouri.player.base.SourceAuthorization
27
+ import com.twentyfouri.player.base.SourceDrm
28
+ import com.twentyfouri.player.base.SourceMetadata
29
+ import com.twentyfouri.player.base.buildUpon
30
+ import com.twentyfouri.player.base.getBoolean
31
+ import com.twentyfouri.player.base.getString
32
+ import com.twentyfouri.player.exoplayer.AuthorizedDataSourceProvider
33
+ import com.twentyfouri.player.exoplayer.DefaultDrmSessionManagerProvider
34
+ import com.twentyfouri.player.exoplayer.DefaultPlayerComponents
35
+ import com.twentyfouri.player.exoplayer.DefaultSharedComponents
36
+ import com.twentyfouri.player.exoplayer.DrmSessionManagerProvider
37
+ import com.twentyfouri.player.exoplayer.PlayerComponents
38
+ import com.twentyfouri.player.exoplayer.PlayerExoPlayer
39
+ import com.twentyfouri.player.exoplayer.PlayerFactoryExoPlayer
40
+ import com.twentyfouri.player.exoplayer.PlayerViewExoPlayer
41
+ import com.twentyfouri.player.exoplayer.SharedComponents
42
+ import com.twentyfouri.tvbridge.BuildConfig
43
+ import com.twentyfouri.tvbridge.configuration.BridgeConfig
44
+ import com.npaw.core.util.extensions.Log as NpawLog
45
+
46
+ class PlayerFactoryExoPlayerNpaw(
47
+ private val config: BridgeConfig
48
+ ) : PlayerFactoryExoPlayer(), PlayerFactoryVersion {
49
+
50
+ override val engineName: String = "Exoplayer"
51
+
52
+ override val engineVersion: String = ExoPlayerLibraryInfo.VERSION
53
+
54
+ override fun createSharedComponents(context: Context): SharedComponents =
55
+ CustomizedSharedComponents(context)
56
+
57
+ override fun createView(parent: ViewGroup): View {
58
+ return PlayerViewExoPlayer(parent.context).also {
59
+ val basePlayer = createPlayer(parent.context)
60
+ val npawPlugin = (
61
+ (basePlayer.components as CustomizedPlayerComponents)
62
+ .sharedComponents as CustomizedSharedComponents
63
+ ).npawPlugin
64
+ val npawPlayer = NpawPlayer(basePlayer, npawPlugin)
65
+ trackPlayer(npawPlayer)
66
+ it.player = npawPlayer
67
+ }
68
+ }
69
+
70
+ private inner class CustomizedSharedComponents(context: Context) : DefaultSharedComponents(context) {
71
+
72
+ val application = context.applicationContext as? Application
73
+ ?: throw IllegalStateException("Missing application")
74
+
75
+ val npawPlugin: NpawPlugin = NpawPlugin
76
+ .Builder(application, config.npawAccount)
77
+ .setLogLevel(if (BuildConfig.DEBUG) NpawLog.Level.VERBOSE else NpawLog.Level.ERROR)
78
+ .build()
79
+
80
+ val npawBalancer = ExoPlayerBalancer(npawPlugin)
81
+
82
+ override fun destroy() {
83
+ super.destroy()
84
+ npawPlugin.destroy()
85
+ }
86
+
87
+ override val drmProvider: DrmSessionManagerProvider by lazy { CustomizedDrmSessionManagerProvider() }
88
+
89
+ override val applicationName: String
90
+ get() = config.playerAppName ?: super.applicationName
91
+
92
+ override fun buildIsolatedComponents(): PlayerComponents {
93
+ return CustomizedPlayerComponents(this)
94
+ }
95
+
96
+ override val baseHttpFactory: HttpDataSource.Factory by lazy {
97
+ npawBalancer.dataSourceFactory as OkHttpDataSource.Factory
98
+ }
99
+
100
+ override val dataSourceFactory: AuthorizedDataSourceProvider by lazy {
101
+ object : AuthorizedDataSourceProvider {
102
+ override fun buildDataSourceFactory(authorization: SourceAuthorization?): CacheDataSource.Factory {
103
+ return buildReadOnlyDataSourceFactory(baseHttpFactory)
104
+ }
105
+
106
+ override fun buildHttpDataSourceFactory(authorization: SourceAuthorization?): HttpDataSource.Factory {
107
+ return baseHttpFactory
108
+ }
109
+ }
110
+ }
111
+
112
+ override val cacheDataSourceFactory: AuthorizedDataSourceProvider by lazy {
113
+ dataSourceFactory
114
+ }
115
+ }
116
+
117
+ private inner class CustomizedPlayerComponents(shared: SharedComponents) : DefaultPlayerComponents(shared) {
118
+ override val handleAudioFocus: Boolean
119
+ get() = false
120
+ }
121
+
122
+ private class CustomizedDrmSessionManagerProvider : DefaultDrmSessionManagerProvider() {
123
+ override fun buildDrmSessionManager(
124
+ dataSourceFactory: AuthorizedDataSourceProvider,
125
+ source: Source,
126
+ sourceDrm: SourceDrm
127
+ ): DrmSessionManager? {
128
+ if (sourceDrm is SourceDrmOverridden) {
129
+ return buildDrmOverridden(dataSourceFactory, sourceDrm)
130
+ } else {
131
+ return super.buildDrmSessionManager(dataSourceFactory, source, sourceDrm)
132
+ }
133
+ }
134
+
135
+ private fun buildDrmOverridden(dataSourceFactory: AuthorizedDataSourceProvider, sourceDrm: SourceDrmOverridden): DrmSessionManager? {
136
+ val drmUuid = when (sourceDrm.packageName) {
137
+ DrmWidevine.packageName -> C.WIDEVINE_UUID
138
+ DrmPlayready.packageName -> C.PLAYREADY_UUID
139
+ else -> C.UUID_NIL
140
+ }
141
+ val licenseUrl = sourceDrm.licenseServer?.toString() ?: ""
142
+ val forceLicenseUrl = sourceDrm.forceLicenseServer
143
+ val httpDataSourceFactory = dataSourceFactory.buildHttpDataSourceFactory(sourceDrm.authorization)
144
+
145
+ val drmCallback = LoggingDrmCallback(
146
+ OverriddenHttpMediaDrmCallback(
147
+ licenseUrl,
148
+ forceLicenseUrl,
149
+ httpDataSourceFactory
150
+ ),
151
+ licenseUrl
152
+ )
153
+
154
+ return DefaultDrmSessionManager.Builder()
155
+ .setUuidAndExoMediaDrmProvider(drmUuid, FrameworkMediaDrm.DEFAULT_PROVIDER)
156
+ .setMultiSession(false)
157
+ .build(drmCallback)
158
+ }
159
+
160
+ override fun buildDrmCallback(licenseUrl: String, forceLicenseUrl: Boolean, httpDataSourceFactory: HttpDataSource.Factory): MediaDrmCallback {
161
+ return LoggingDrmCallback(super.buildDrmCallback(licenseUrl, forceLicenseUrl, httpDataSourceFactory), licenseUrl)
162
+ }
163
+ }
164
+
165
+ private class NpawPlayer(
166
+ val basePlayer: PlayerExoPlayer,
167
+ val npawPlugin: NpawPlugin
168
+ ) : PlayerBase by basePlayer {
169
+
170
+ var exoPlayerAdapter: ExoPlayerAdapter? = null
171
+
172
+ var videoAdapter: VideoAdapter? = null
173
+ set(value) {
174
+ field?.destroy()
175
+ field = value
176
+ }
177
+
178
+ fun extractYouboraAnalyticsOptions(source: Source?, target: AnalyticsOptions) {
179
+ if (source == null) return
180
+ val jsonAnalyticsArray = source.extras?.optJSONArray("analytics") ?: return
181
+ for (index in 0 until jsonAnalyticsArray.length()) {
182
+ val jsonAnalytics = jsonAnalyticsArray.getJSONObject(index)
183
+ if (jsonAnalytics.getString("integration", "") != "youbora") continue
184
+ jsonAnalytics.keys().forEach { key ->
185
+ when (key) {
186
+ "app.name" -> target.appName = jsonAnalytics.getString(key, null)
187
+ "app.releaseVersion" -> target.appReleaseVersion = jsonAnalytics.getString(key, null)
188
+ "device.id" -> target.deviceId = jsonAnalytics.getString(key, null)
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ fun extractYouboraVideoOptions(source: Source?, target: VideoOptions) {
195
+ if (source == null) return
196
+ val jsonAnalyticsArray = source.extras?.optJSONArray("analytics") ?: return
197
+ for (index in 0 until jsonAnalyticsArray.length()) {
198
+ val jsonAnalytics = jsonAnalyticsArray.getJSONObject(index)
199
+ if (jsonAnalytics.getString("integration", "") != "youbora") continue
200
+ jsonAnalytics.keys().forEach { key ->
201
+ when (key) {
202
+ "content.id" -> target.contentId = jsonAnalytics.getString(key, null)
203
+ "content.title" -> target.contentTitle = jsonAnalytics.getString(key, null)
204
+ "content.isLive" -> target.live = jsonAnalytics.getBoolean(key, false)
205
+ "content.type" -> target.contentType = jsonAnalytics.getString(key, null)
206
+ "content.program" -> target.program = jsonAnalytics.getString(key, null)
207
+ "content.channel" -> target.contentChannel = jsonAnalytics.getString(key, null)
208
+ "content.episodeTitle" -> target.contentEpisodeTitle = jsonAnalytics.getString(key, null)
209
+ "content.season" -> target.contentSeason = jsonAnalytics.getString(key, null)
210
+ "content.tvShow" -> target.contentTvShow = jsonAnalytics.getString(key, null)
211
+ "content.customDimension.1" -> target.contentCustomDimension1 = jsonAnalytics.getString(key, null)
212
+ "content.customDimension.2" -> target.contentCustomDimension2 = jsonAnalytics.getString(key, null)
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ override var source: Source?
219
+ get() = basePlayer.source
220
+ set(value) {
221
+ exoPlayerAdapter = null
222
+ videoAdapter = null
223
+ if (value != null) {
224
+ val videoOptions = VideoOptions()
225
+ extractYouboraAnalyticsOptions(value, npawPlugin.analyticsOptions)
226
+ extractYouboraVideoOptions(value, videoOptions)
227
+
228
+ var adjustedSource = value
229
+ if (value.metadata?.title.isNullOrEmpty() && !videoOptions.contentTitle.isNullOrEmpty()) {
230
+ adjustedSource = value.buildUpon {
231
+ metadata = (metadata?.buildUpon() ?: SourceMetadata.Builder()).apply {
232
+ title = videoOptions.contentTitle
233
+ }.build()
234
+ }
235
+ }
236
+
237
+ basePlayer.source = adjustedSource
238
+ exoPlayerAdapter = ExoPlayerAdapter(basePlayer.components.context, basePlayer.exoPlayer)
239
+ videoAdapter = npawPlugin.videoBuilder()
240
+ .setPlayerAdapter(exoPlayerAdapter)
241
+ .setOptions(videoOptions)
242
+ .build()
243
+ } else {
244
+ basePlayer.source = null
245
+ }
246
+ }
247
+
248
+ override fun destroy() {
249
+ exoPlayerAdapter = null
250
+ videoAdapter = null
251
+ basePlayer.destroy()
252
+ }
253
+ }
254
+ }
255
+
@@ -38,7 +38,9 @@ allprojects {
38
38
  password = "$mavenRepositoryPassword"
39
39
  }
40
40
  }
41
+ maven { url "https://maven.theoplayer.com/releases" }
41
42
  maven { url "https://jitpack.io" }
43
+ maven { url "https://artifact.plugin.npaw.com/artifactory/plugins/android" }
42
44
  }
43
45
  }
44
46
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@24i/smartapps-bigscreen-rnv-build-engine",
3
- "version": "0.17.21-alpha.2696",
3
+ "version": "0.17.21",
4
4
  "description": "Package with build engine for SmartApps BIGscreen projects...",
5
5
  "main": "dist/src/index.js",
6
6
  "scripts": {