stormcloud-video-player 0.3.17 → 0.3.18

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.
@@ -49,6 +49,13 @@ declare class StormcloudVideoPlayer {
49
49
  private isShowingPlaceholder;
50
50
  private timeUpdateHandler?;
51
51
  private emptiedHandler?;
52
+ private consecutiveEmptyResponses;
53
+ private totalAdRequestsInBreak;
54
+ private lastEmptyResponseTimeMs;
55
+ private readonly maxTotalAdRequestsPerBreak;
56
+ private readonly maxConsecutiveEmptyResponses;
57
+ private readonly baseEmptyResponseDelayMs;
58
+ private readonly maxEmptyResponseDelayMs;
52
59
  constructor(config: StormcloudVideoPlayerConfig);
53
60
  private createAdPlayer;
54
61
  load(): Promise<void>;
@@ -309,6 +309,20 @@ function _ts_generator(thisArg, body) {
309
309
  };
310
310
  }
311
311
  }
312
+ function _ts_values(o) {
313
+ var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
314
+ if (m) return m.call(o);
315
+ if (o && typeof o.length === "number") return {
316
+ next: function() {
317
+ if (o && i >= o.length) o = void 0;
318
+ return {
319
+ value: o && o[i++],
320
+ done: !o
321
+ };
322
+ }
323
+ };
324
+ throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
325
+ }
312
326
  var __create = Object.create;
313
327
  var __defProp = Object.defineProperty;
314
328
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -1299,6 +1313,7 @@ function createImaController(video, options) {
1299
1313
  },
1300
1314
  showPlaceholder: function showPlaceholder() {
1301
1315
  ensurePlaceholderContainer();
1316
+ hideContentVideo();
1302
1317
  if (adContainerEl) {
1303
1318
  adContainerEl.style.display = "flex";
1304
1319
  adContainerEl.style.backgroundColor = "#000";
@@ -1318,6 +1333,9 @@ function createImaController(video, options) {
1318
1333
  }
1319
1334
  }, 300);
1320
1335
  }
1336
+ if (!adPlaying) {
1337
+ showContentVideo();
1338
+ }
1321
1339
  }
1322
1340
  };
1323
1341
  }
@@ -2092,6 +2110,8 @@ function createHlsAdPlayer(contentVideo, options) {
2092
2110
  return 1;
2093
2111
  },
2094
2112
  showPlaceholder: function showPlaceholder() {
2113
+ contentVideo.style.opacity = "0";
2114
+ contentVideo.style.visibility = "hidden";
2095
2115
  if (!adContainerEl) {
2096
2116
  var _contentVideo_parentElement;
2097
2117
  var container = document.createElement("div");
@@ -2119,6 +2139,10 @@ function createHlsAdPlayer(contentVideo, options) {
2119
2139
  adContainerEl.style.display = "none";
2120
2140
  adContainerEl.style.pointerEvents = "none";
2121
2141
  }
2142
+ if (!adPlaying) {
2143
+ contentVideo.style.visibility = "visible";
2144
+ contentVideo.style.opacity = "1";
2145
+ }
2122
2146
  }
2123
2147
  };
2124
2148
  }
@@ -2773,6 +2797,13 @@ var StormcloudVideoPlayer = /*#__PURE__*/ function() {
2773
2797
  this.maxPlaceholderDurationMs = 5e3;
2774
2798
  this.placeholderStartTimeMs = null;
2775
2799
  this.isShowingPlaceholder = false;
2800
+ this.consecutiveEmptyResponses = 0;
2801
+ this.totalAdRequestsInBreak = 0;
2802
+ this.lastEmptyResponseTimeMs = 0;
2803
+ this.maxTotalAdRequestsPerBreak = 20;
2804
+ this.maxConsecutiveEmptyResponses = 5;
2805
+ this.baseEmptyResponseDelayMs = 2e3;
2806
+ this.maxEmptyResponseDelayMs = 3e4;
2776
2807
  initializePolyfills();
2777
2808
  var browserOverrides = getBrowserConfigOverrides();
2778
2809
  this.config = _object_spread({}, config, browserOverrides);
@@ -2962,6 +2993,87 @@ var StormcloudVideoPlayer = /*#__PURE__*/ function() {
2962
2993
  });
2963
2994
  }).call(_this);
2964
2995
  });
2996
+ this.hls.on(import_hls2.default.Events.LEVEL_LOADED, function(_evt, data) {
2997
+ if (_this.inAdBreak) {
2998
+ return;
2999
+ }
3000
+ var details = data === null || data === void 0 ? void 0 : data.details;
3001
+ if (!details || !details.fragments || details.fragments.length === 0) {
3002
+ return;
3003
+ }
3004
+ var fragmentsToScan = Math.min(5, details.fragments.length);
3005
+ for(var i = 0; i < fragmentsToScan; i++){
3006
+ var frag = details.fragments[i];
3007
+ var tagList = frag === null || frag === void 0 ? void 0 : frag.tagList;
3008
+ if (!Array.isArray(tagList)) continue;
3009
+ var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
3010
+ try {
3011
+ for(var _iterator = tagList[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true){
3012
+ var entry = _step.value;
3013
+ var tag = "";
3014
+ var value = "";
3015
+ if (Array.isArray(entry)) {
3016
+ var _entry_;
3017
+ tag = String((_entry_ = entry[0]) !== null && _entry_ !== void 0 ? _entry_ : "");
3018
+ var _entry_1;
3019
+ value = String((_entry_1 = entry[1]) !== null && _entry_1 !== void 0 ? _entry_1 : "");
3020
+ } else if (typeof entry === "string") {
3021
+ var idx = entry.indexOf(":");
3022
+ if (idx >= 0) {
3023
+ tag = entry.substring(0, idx);
3024
+ value = entry.substring(idx + 1);
3025
+ } else {
3026
+ tag = entry;
3027
+ }
3028
+ }
3029
+ if (!tag) continue;
3030
+ if (tag.includes("EXT-X-CUE-OUT") || tag.includes("EXT-X-DATERANGE")) {
3031
+ var attrs = tag.includes("EXT-X-DATERANGE") ? _this.parseAttributeList(value) : {};
3032
+ var hasScteOut = tag.includes("EXT-X-CUE-OUT") || "SCTE35-OUT" in attrs || attrs["SCTE35-OUT"] !== void 0;
3033
+ if (hasScteOut) {
3034
+ if (_this.config.debugAdTiming) {
3035
+ console.log("[StormcloudVideoPlayer] \uD83C\uDFAF EARLY SCTE-35 DETECTION: Ad break marker found in fragment", i, "- triggering ad handling immediately");
3036
+ }
3037
+ var durationSeconds = _this.parseCueOutDuration(value);
3038
+ var marker = _object_spread_props(_object_spread({
3039
+ type: "start"
3040
+ }, durationSeconds !== void 0 ? {
3041
+ durationSeconds: durationSeconds
3042
+ } : {}), {
3043
+ raw: {
3044
+ tag: tag,
3045
+ value: value,
3046
+ earlyDetection: true
3047
+ }
3048
+ });
3049
+ _this.inAdBreak = true;
3050
+ _this.expectedAdBreakDurationMs = durationSeconds ? durationSeconds * 1e3 : void 0;
3051
+ _this.currentAdBreakStartWallClockMs = Date.now();
3052
+ _this.clearAdStartTimer();
3053
+ _this.handleAdStart(marker);
3054
+ if (_this.expectedAdBreakDurationMs != null) {
3055
+ _this.scheduleAdStopCountdown(_this.expectedAdBreakDurationMs);
3056
+ }
3057
+ return;
3058
+ }
3059
+ }
3060
+ }
3061
+ } catch (err) {
3062
+ _didIteratorError = true;
3063
+ _iteratorError = err;
3064
+ } finally{
3065
+ try {
3066
+ if (!_iteratorNormalCompletion && _iterator.return != null) {
3067
+ _iterator.return();
3068
+ }
3069
+ } finally{
3070
+ if (_didIteratorError) {
3071
+ throw _iteratorError;
3072
+ }
3073
+ }
3074
+ }
3075
+ }
3076
+ });
2965
3077
  this.hls.on(import_hls2.default.Events.FRAG_BUFFERED, function(_evt, data) {
2966
3078
  return _async_to_generator(function() {
2967
3079
  var _this, _this_config_minSegmentsBeforePlay, minSegments, _this_video_play;
@@ -3169,11 +3281,16 @@ var StormcloudVideoPlayer = /*#__PURE__*/ function() {
3169
3281
  this.ima.initialize();
3170
3282
  this.ima.updateOriginalMutedState(this.video.muted, this.video.volume);
3171
3283
  this.ima.on("all_ads_completed", function() {
3284
+ var remaining = _this.getRemainingAdMs();
3172
3285
  if (_this.config.debugAdTiming) {
3173
- console.log("[StormcloudVideoPlayer] IMA all_ads_completed event received - ending ad break");
3286
+ console.log("[StormcloudVideoPlayer] IMA all_ads_completed event received - remaining=".concat(remaining, "ms, queued ads=").concat(_this.adRequestQueue.length));
3174
3287
  }
3175
3288
  if (_this.inAdBreak) {
3176
- _this.handleAdPodComplete();
3289
+ if (remaining > 500) {
3290
+ _this.tryNextAvailableAd();
3291
+ } else {
3292
+ _this.handleAdPodComplete();
3293
+ }
3177
3294
  }
3178
3295
  });
3179
3296
  this.ima.on("ad_error", function(errorPayload) {
@@ -3968,6 +4085,9 @@ var StormcloudVideoPlayer = /*#__PURE__*/ function() {
3968
4085
  this.continuousFetchingActive = true;
3969
4086
  this.isShowingPlaceholder = false;
3970
4087
  this.placeholderStartTimeMs = null;
4088
+ this.consecutiveEmptyResponses = 0;
4089
+ this.totalAdRequestsInBreak = 0;
4090
+ this.lastEmptyResponseTimeMs = 0;
3971
4091
  currentMuted = this.video.muted;
3972
4092
  currentVolume = this.video.volume;
3973
4093
  this.ima.updateOriginalMutedState(currentMuted, currentVolume);
@@ -3975,6 +4095,8 @@ var StormcloudVideoPlayer = /*#__PURE__*/ function() {
3975
4095
  this.currentAdIndex = 0;
3976
4096
  this.totalAdsInBreak = 1;
3977
4097
  this.adPodQueue = [];
4098
+ this.showAds = true;
4099
+ this.ima.showPlaceholder();
3978
4100
  if (this.expectedAdBreakDurationMs == null && adBreakDurationMs != null) {
3979
4101
  this.expectedAdBreakDurationMs = adBreakDurationMs;
3980
4102
  this.currentAdBreakStartWallClockMs = Date.now();
@@ -4061,158 +4183,200 @@ var StormcloudVideoPlayer = /*#__PURE__*/ function() {
4061
4183
  key: "continuousFetchLoop",
4062
4184
  value: function continuousFetchLoop(baseVastUrl) {
4063
4185
  return _async_to_generator(function() {
4064
- var remaining, maxQueueSize, newAdUrl, response, xmlText, parser, xmlDoc, mediaFiles, error;
4186
+ var _this, _loop, _ret;
4065
4187
  return _ts_generator(this, function(_state) {
4066
4188
  switch(_state.label){
4067
4189
  case 0:
4068
- if (!(this.continuousFetchingActive && this.inAdBreak)) return [
4069
- 3,
4070
- 14
4071
- ];
4072
- remaining = this.getRemainingAdMs();
4073
- if (remaining <= 0) {
4074
- if (this.config.debugAdTiming) {
4075
- console.log("[CONTINUOUS-FETCH] \u23F9\uFE0F Ad break time expired, stopping fetch loop");
4076
- }
4077
- return [
4078
- 3,
4079
- 14
4080
- ];
4081
- }
4082
- maxQueueSize = 3;
4083
- if (!(this.adRequestQueue.length >= maxQueueSize)) return [
4084
- 3,
4085
- 2
4086
- ];
4087
- if (this.config.debugAdTiming) {
4088
- console.log("[CONTINUOUS-FETCH] ⏸️ Queue full (".concat(this.adRequestQueue.length, "), pausing fetching..."));
4089
- }
4090
- return [
4091
- 4,
4092
- new Promise(function(resolve) {
4093
- return setTimeout(resolve, 2e3);
4094
- })
4095
- ];
4190
+ _loop = function() {
4191
+ var remaining, maxQueueSize, newAdUrl, _this_ima_hasPreloadedAd, _this_ima, _this_ima_hasPreloadedAd1, hasPreloadedAd, backoffDelay, error, backoffDelay1;
4192
+ return _ts_generator(this, function(_state) {
4193
+ switch(_state.label){
4194
+ case 0:
4195
+ remaining = _this.getRemainingAdMs();
4196
+ if (remaining <= 0) {
4197
+ if (_this.config.debugAdTiming) {
4198
+ console.log("[CONTINUOUS-FETCH] \u23F9\uFE0F Ad break time expired, stopping fetch loop");
4199
+ }
4200
+ return [
4201
+ 2,
4202
+ "break"
4203
+ ];
4204
+ }
4205
+ if (_this.totalAdRequestsInBreak >= _this.maxTotalAdRequestsPerBreak) {
4206
+ if (_this.config.debugAdTiming) {
4207
+ console.log("[CONTINUOUS-FETCH] \uD83D\uDED1 Maximum ad requests reached (".concat(_this.maxTotalAdRequestsPerBreak, "), stopping fetch loop to prevent server blocks"));
4208
+ }
4209
+ return [
4210
+ 2,
4211
+ "break"
4212
+ ];
4213
+ }
4214
+ if (_this.consecutiveEmptyResponses >= _this.maxConsecutiveEmptyResponses) {
4215
+ if (_this.config.debugAdTiming) {
4216
+ console.log("[CONTINUOUS-FETCH] \uD83D\uDED1 Too many consecutive empty responses (".concat(_this.maxConsecutiveEmptyResponses, "), stopping fetch loop"));
4217
+ }
4218
+ return [
4219
+ 2,
4220
+ "break"
4221
+ ];
4222
+ }
4223
+ maxQueueSize = 3;
4224
+ if (!(_this.adRequestQueue.length >= maxQueueSize)) return [
4225
+ 3,
4226
+ 2
4227
+ ];
4228
+ if (_this.config.debugAdTiming) {
4229
+ console.log("[CONTINUOUS-FETCH] ⏸️ Queue full (".concat(_this.adRequestQueue.length, "), pausing fetching..."));
4230
+ }
4231
+ return [
4232
+ 4,
4233
+ new Promise(function(resolve) {
4234
+ return setTimeout(resolve, 2e3);
4235
+ })
4236
+ ];
4237
+ case 1:
4238
+ _state.sent();
4239
+ return [
4240
+ 2,
4241
+ "continue"
4242
+ ];
4243
+ case 2:
4244
+ newAdUrl = _this.generateVastUrlsWithCorrelators(baseVastUrl, 1)[0];
4245
+ if (!(!newAdUrl || _this.failedVastUrls.has(newAdUrl))) return [
4246
+ 3,
4247
+ 4
4248
+ ];
4249
+ return [
4250
+ 4,
4251
+ new Promise(function(resolve) {
4252
+ return setTimeout(resolve, 1e3);
4253
+ })
4254
+ ];
4255
+ case 3:
4256
+ _state.sent();
4257
+ return [
4258
+ 2,
4259
+ "continue"
4260
+ ];
4261
+ case 4:
4262
+ _this.totalAdRequestsInBreak++;
4263
+ if (_this.config.debugAdTiming) {
4264
+ console.log("[CONTINUOUS-FETCH] \uD83D\uDCE1 Attempting to fetch ad (request ".concat(_this.totalAdRequestsInBreak, "/").concat(_this.maxTotalAdRequestsPerBreak, ", queue: ").concat(_this.adRequestQueue.length, ")..."));
4265
+ }
4266
+ _state.label = 5;
4267
+ case 5:
4268
+ _state.trys.push([
4269
+ 5,
4270
+ 11,
4271
+ ,
4272
+ 13
4273
+ ]);
4274
+ if (!_this.ima.preloadAds) return [
4275
+ 3,
4276
+ 7
4277
+ ];
4278
+ return [
4279
+ 4,
4280
+ _this.ima.preloadAds(newAdUrl)
4281
+ ];
4282
+ case 6:
4283
+ _state.sent();
4284
+ _state.label = 7;
4285
+ case 7:
4286
+ hasPreloadedAd = (_this_ima_hasPreloadedAd1 = (_this_ima_hasPreloadedAd = (_this_ima = _this.ima).hasPreloadedAd) === null || _this_ima_hasPreloadedAd === void 0 ? void 0 : _this_ima_hasPreloadedAd.call(_this_ima, newAdUrl)) !== null && _this_ima_hasPreloadedAd1 !== void 0 ? _this_ima_hasPreloadedAd1 : false;
4287
+ if (!!hasPreloadedAd) return [
4288
+ 3,
4289
+ 9
4290
+ ];
4291
+ _this.consecutiveEmptyResponses++;
4292
+ _this.lastEmptyResponseTimeMs = Date.now();
4293
+ backoffDelay = Math.min(_this.baseEmptyResponseDelayMs * Math.pow(2, _this.consecutiveEmptyResponses - 1), _this.maxEmptyResponseDelayMs);
4294
+ if (_this.config.debugAdTiming) {
4295
+ console.log("[CONTINUOUS-FETCH] ⚠️ Empty/invalid VAST response (".concat(_this.consecutiveEmptyResponses, "/").concat(_this.maxConsecutiveEmptyResponses, " consecutive), backing off for ").concat(backoffDelay, "ms"));
4296
+ }
4297
+ _this.failedVastUrls.add(newAdUrl);
4298
+ return [
4299
+ 4,
4300
+ new Promise(function(resolve) {
4301
+ return setTimeout(resolve, backoffDelay);
4302
+ })
4303
+ ];
4304
+ case 8:
4305
+ _state.sent();
4306
+ return [
4307
+ 2,
4308
+ "continue"
4309
+ ];
4310
+ case 9:
4311
+ _this.consecutiveEmptyResponses = 0;
4312
+ if (_this.config.debugAdTiming) {
4313
+ console.log("[CONTINUOUS-FETCH] ✅ Successfully preloaded ad, adding to queue (queue size: ".concat(_this.adRequestQueue.length + 1, ")"));
4314
+ }
4315
+ _this.adRequestQueue.push(newAdUrl);
4316
+ _this.totalAdsInBreak++;
4317
+ return [
4318
+ 4,
4319
+ new Promise(function(resolve) {
4320
+ return setTimeout(resolve, 500);
4321
+ })
4322
+ ];
4323
+ case 10:
4324
+ _state.sent();
4325
+ return [
4326
+ 3,
4327
+ 13
4328
+ ];
4329
+ case 11:
4330
+ error = _state.sent();
4331
+ if (_this.config.debugAdTiming) {
4332
+ console.log("[CONTINUOUS-FETCH] \u274C Ad preload failed:", error.message);
4333
+ }
4334
+ _this.failedVastUrls.add(newAdUrl);
4335
+ _this.consecutiveEmptyResponses++;
4336
+ backoffDelay1 = Math.min(_this.baseEmptyResponseDelayMs * Math.pow(2, _this.consecutiveEmptyResponses - 1), _this.maxEmptyResponseDelayMs);
4337
+ return [
4338
+ 4,
4339
+ new Promise(function(resolve) {
4340
+ return setTimeout(resolve, backoffDelay1);
4341
+ })
4342
+ ];
4343
+ case 12:
4344
+ _state.sent();
4345
+ return [
4346
+ 3,
4347
+ 13
4348
+ ];
4349
+ case 13:
4350
+ return [
4351
+ 2
4352
+ ];
4353
+ }
4354
+ });
4355
+ };
4356
+ _state.label = 1;
4096
4357
  case 1:
4097
- _state.sent();
4098
- return [
4099
- 3,
4100
- 0
4101
- ];
4102
- case 2:
4103
- newAdUrl = this.generateVastUrlsWithCorrelators(baseVastUrl, 1)[0];
4104
- if (!(!newAdUrl || this.failedVastUrls.has(newAdUrl))) return [
4358
+ if (!(this.continuousFetchingActive && this.inAdBreak)) return [
4105
4359
  3,
4106
- 4
4107
- ];
4108
- return [
4109
- 4,
4110
- new Promise(function(resolve) {
4111
- return setTimeout(resolve, 1e3);
4112
- })
4360
+ 3
4113
4361
  ];
4114
- case 3:
4115
- _state.sent();
4362
+ _this = this;
4116
4363
  return [
4117
- 3,
4118
- 0
4119
- ];
4120
- case 4:
4121
- if (this.config.debugAdTiming) {
4122
- console.log("[CONTINUOUS-FETCH] \uD83D\uDCE1 Attempting to fetch ad (".concat(this.successfulAdRequests.length + this.adRequestQueue.length + 1, " total)..."));
4123
- }
4124
- _state.label = 5;
4125
- case 5:
4126
- _state.trys.push([
4127
4364
  5,
4128
- 11,
4129
- ,
4130
- 13
4131
- ]);
4132
- return [
4133
- 4,
4134
- fetch(newAdUrl, {
4135
- mode: "cors"
4136
- })
4137
- ];
4138
- case 6:
4139
- response = _state.sent();
4140
- if (!response.ok) {
4141
- throw new Error("Failed to fetch VAST: ".concat(response.status));
4142
- }
4143
- return [
4144
- 4,
4145
- response.text()
4146
- ];
4147
- case 7:
4148
- xmlText = _state.sent();
4149
- parser = new DOMParser();
4150
- xmlDoc = parser.parseFromString(xmlText, "text/xml");
4151
- mediaFiles = xmlDoc.querySelectorAll("MediaFile");
4152
- if (!(mediaFiles.length === 0)) return [
4153
- 3,
4154
- 9
4365
+ _ts_values(_loop())
4155
4366
  ];
4156
- if (this.config.debugAdTiming) {
4157
- console.log("[CONTINUOUS-FETCH] \u26A0\uFE0F VAST response has no media files, skipping");
4158
- }
4159
- this.failedVastUrls.add(newAdUrl);
4160
- return [
4161
- 4,
4162
- new Promise(function(resolve) {
4163
- return setTimeout(resolve, 1e3);
4164
- })
4165
- ];
4166
- case 8:
4167
- _state.sent();
4168
- return [
4169
- 3,
4170
- 0
4171
- ];
4172
- case 9:
4173
- if (this.config.debugAdTiming) {
4174
- console.log("[CONTINUOUS-FETCH] ✅ Successfully fetched ad, adding to queue (queue size: ".concat(this.adRequestQueue.length + 1, ")"));
4175
- }
4176
- this.adRequestQueue.push(newAdUrl);
4177
- this.totalAdsInBreak++;
4178
- return [
4179
- 4,
4180
- new Promise(function(resolve) {
4181
- return setTimeout(resolve, 500);
4182
- })
4183
- ];
4184
- case 10:
4185
- _state.sent();
4186
- return [
4187
- 3,
4188
- 13
4189
- ];
4190
- case 11:
4191
- error = _state.sent();
4192
- if (this.config.debugAdTiming) {
4193
- console.log("[CONTINUOUS-FETCH] \u274C Ad fetch failed:", error.message);
4194
- }
4195
- this.failedVastUrls.add(newAdUrl);
4196
- return [
4197
- 4,
4198
- new Promise(function(resolve) {
4199
- return setTimeout(resolve, 2e3);
4200
- })
4201
- ];
4202
- case 12:
4203
- _state.sent();
4204
- return [
4367
+ case 2:
4368
+ _ret = _state.sent();
4369
+ if (_ret === "break") return [
4205
4370
  3,
4206
- 13
4371
+ 3
4207
4372
  ];
4208
- case 13:
4209
4373
  return [
4210
4374
  3,
4211
- 0
4375
+ 1
4212
4376
  ];
4213
- case 14:
4377
+ case 3:
4214
4378
  if (this.config.debugAdTiming) {
4215
- console.log("[CONTINUOUS-FETCH] \uD83D\uDED1 Continuous fetch loop ended");
4379
+ console.log("[CONTINUOUS-FETCH] \uD83D\uDED1 Continuous fetch loop ended (total requests: ".concat(this.totalAdRequestsInBreak, ", empty responses: ").concat(this.consecutiveEmptyResponses, ")"));
4216
4380
  }
4217
4381
  return [
4218
4382
  2