rx-player 3.27.1-dev.2022041500 → 3.27.1-dev.2022051100
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 +11 -5
- package/VERSION +1 -1
- package/dist/_esm5.processed/core/api/public_api.js +2 -2
- package/dist/_esm5.processed/core/decrypt/content_decryptor.js +12 -10
- package/dist/_esm5.processed/core/decrypt/utils/key_session_record.js +2 -1
- package/dist/_esm5.processed/core/init/throw_on_media_error.js +19 -10
- package/dist/_esm5.processed/manifest/representation.js +4 -4
- package/dist/_esm5.processed/parsers/manifest/dash/common/flatten_overlapping_periods.js +11 -1
- package/dist/_esm5.processed/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.d.ts +2 -0
- package/dist/_esm5.processed/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.js +9 -2
- package/dist/_esm5.processed/parsers/manifest/dash/common/parse_representations.js +10 -3
- package/dist/_esm5.processed/parsers/manifest/types.d.ts +7 -2
- package/dist/_esm5.processed/parsers/texttracks/ttml/html/generate_css_test_outline.js +14 -4
- package/dist/rx-player.js +85 -33
- package/dist/rx-player.min.js +1 -1
- package/package.json +1 -1
- package/sonar-project.properties +1 -1
- package/src/core/api/public_api.ts +2 -2
- package/src/core/decrypt/content_decryptor.ts +12 -10
- package/src/core/decrypt/utils/key_session_record.ts +2 -1
- package/src/core/init/throw_on_media_error.ts +25 -15
- package/src/manifest/representation.ts +2 -2
- package/src/parsers/manifest/dash/common/flatten_overlapping_periods.ts +10 -1
- package/src/parsers/manifest/dash/common/indexes/timeline/timeline_representation_index.ts +12 -2
- package/src/parsers/manifest/dash/common/parse_representations.ts +9 -3
- package/src/parsers/manifest/types.ts +7 -2
- package/src/parsers/texttracks/ttml/html/generate_css_test_outline.ts +15 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## v3.27.1-dev.
|
|
3
|
+
## v3.27.1-dev.2022051100 (2022-05-11)
|
|
4
4
|
|
|
5
5
|
### Bug fixes
|
|
6
6
|
|
|
7
|
-
- Use the first compatible codec of the current AdaptationSet when creating a SourceBuffer [#1094]
|
|
8
|
-
- DASH:
|
|
7
|
+
- Use the first **compatible** codec of the current AdaptationSet when creating a SourceBuffer [#1094]
|
|
8
|
+
- DASH/DRM: Fix potential infinite rebuffering when a KID is not anounced in the MPD [#1113]
|
|
9
|
+
- DASH: Avoid infinite loop due to rounding errors while parsing multi-Periods MPDs [#1111, #1110]
|
|
10
|
+
- TTML: Add support for percent based thickness for textOutline in TTML Subtitles
|
|
11
|
+
- If seeking after the last potential position, load last segments before ending [#1097]
|
|
12
|
+
- DASH: Fix possibility of wrong segments being requested when a SegmentTimeline in a given Period (whose Period@end is set) had an S@r set to `-1` at its end [#1098]
|
|
13
|
+
- DASH: Don't include presentationTimeOffset for the default initial time of the first `<S>` element if its S@t is not set.
|
|
9
14
|
|
|
10
15
|
### Other improvements
|
|
11
16
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
17
|
+
- The duration set on the media element is now only relative to the current chosen tracks (it was previously relative to all potential track). This allows to seek later when switching e.g. to a longer video track [#1102]
|
|
18
|
+
- Errors coming from an HTMLMediaElement now have the browser's error message if it exists [#1112]
|
|
19
|
+
|
|
14
20
|
|
|
15
21
|
## v3.27.0 (2022-03-31)
|
|
16
22
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.27.1-dev.
|
|
1
|
+
3.27.1-dev.2022051100
|
|
@@ -87,7 +87,7 @@ var Player = /** @class */ (function (_super) {
|
|
|
87
87
|
// Workaround to support Firefox autoplay on FF 42.
|
|
88
88
|
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1194624
|
|
89
89
|
videoElement.preload = "auto";
|
|
90
|
-
_this.version = /* PLAYER_VERSION */ "3.27.1-dev.
|
|
90
|
+
_this.version = /* PLAYER_VERSION */ "3.27.1-dev.2022051100";
|
|
91
91
|
_this.log = log;
|
|
92
92
|
_this.state = "STOPPED";
|
|
93
93
|
_this.videoElement = videoElement;
|
|
@@ -2301,5 +2301,5 @@ var Player = /** @class */ (function (_super) {
|
|
|
2301
2301
|
};
|
|
2302
2302
|
return Player;
|
|
2303
2303
|
}(EventEmitter));
|
|
2304
|
-
Player.version = /* PLAYER_VERSION */ "3.27.1-dev.
|
|
2304
|
+
Player.version = /* PLAYER_VERSION */ "3.27.1-dev.2022051100";
|
|
2305
2305
|
export default Player;
|
|
@@ -668,16 +668,18 @@ function updateDecipherability(manifest, whitelistedKeyIds, blacklistedKeyIDs) {
|
|
|
668
668
|
return representation.decipherable;
|
|
669
669
|
}
|
|
670
670
|
var contentKIDs = representation.contentProtections.keyIds;
|
|
671
|
-
|
|
672
|
-
var
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
671
|
+
if (contentKIDs !== undefined) {
|
|
672
|
+
for (var i = 0; i < contentKIDs.length; i++) {
|
|
673
|
+
var elt = contentKIDs[i];
|
|
674
|
+
for (var j = 0; j < blacklistedKeyIDs.length; j++) {
|
|
675
|
+
if (areKeyIdsEqual(blacklistedKeyIDs[j], elt.keyId)) {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
676
678
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
679
|
+
for (var j = 0; j < whitelistedKeyIds.length; j++) {
|
|
680
|
+
if (areKeyIdsEqual(whitelistedKeyIds[j], elt.keyId)) {
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
681
683
|
}
|
|
682
684
|
}
|
|
683
685
|
}
|
|
@@ -862,7 +864,7 @@ function addKeyIdsFromPeriod(set, period) {
|
|
|
862
864
|
for (var _b = 0, _c = adaptation.representations; _b < _c.length; _b++) {
|
|
863
865
|
var representation = _c[_b];
|
|
864
866
|
if (representation.contentProtections !== undefined &&
|
|
865
|
-
representation.contentProtections.keyIds
|
|
867
|
+
representation.contentProtections.keyIds !== undefined) {
|
|
866
868
|
for (var _d = 0, _e = representation.contentProtections.keyIds; _d < _e.length; _d++) {
|
|
867
869
|
var kidInf = _e[_d];
|
|
868
870
|
set.add(kidInf.keyId);
|
|
@@ -129,7 +129,7 @@ var KeySessionRecord = /** @class */ (function () {
|
|
|
129
129
|
*/
|
|
130
130
|
KeySessionRecord.prototype.isCompatibleWith = function (initializationData) {
|
|
131
131
|
var keyIds = initializationData.keyIds;
|
|
132
|
-
if (keyIds !== undefined) {
|
|
132
|
+
if (keyIds !== undefined && keyIds.length > 0) {
|
|
133
133
|
if (this._keyIds !== null && areAllKeyIdsContainedIn(keyIds, this._keyIds)) {
|
|
134
134
|
return true;
|
|
135
135
|
}
|
|
@@ -141,6 +141,7 @@ var KeySessionRecord = /** @class */ (function () {
|
|
|
141
141
|
};
|
|
142
142
|
KeySessionRecord.prototype._checkInitializationDataCompatibility = function (initializationData) {
|
|
143
143
|
if (initializationData.keyIds !== undefined &&
|
|
144
|
+
initializationData.keyIds.length > 0 &&
|
|
144
145
|
this._initializationData.keyIds !== undefined) {
|
|
145
146
|
return areAllKeyIdsContainedIn(initializationData.keyIds, this._initializationData.keyIds);
|
|
146
147
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { fromEvent as observableFromEvent, mergeMap, } from "rxjs";
|
|
17
17
|
import { MediaError } from "../../errors";
|
|
18
|
+
import isNullOrUndefined from "../../utils/is_null_or_undefined";
|
|
18
19
|
/**
|
|
19
20
|
* Returns an observable which throws the right MediaError as soon an "error"
|
|
20
21
|
* event is received through the media element.
|
|
@@ -24,22 +25,30 @@ import { MediaError } from "../../errors";
|
|
|
24
25
|
export default function throwOnMediaError(mediaElement) {
|
|
25
26
|
return observableFromEvent(mediaElement, "error")
|
|
26
27
|
.pipe(mergeMap(function () {
|
|
27
|
-
var
|
|
28
|
-
|
|
28
|
+
var mediaError = mediaElement.error;
|
|
29
|
+
var errorCode;
|
|
30
|
+
var errorMessage;
|
|
31
|
+
if (!isNullOrUndefined(mediaError)) {
|
|
32
|
+
errorCode = mediaError.code;
|
|
33
|
+
errorMessage = mediaError.message;
|
|
34
|
+
}
|
|
29
35
|
switch (errorCode) {
|
|
30
36
|
case 1:
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
errorMessage = errorMessage !== null && errorMessage !== void 0 ? errorMessage : "The fetching of the associated resource was aborted by the user's request.";
|
|
38
|
+
throw new MediaError("MEDIA_ERR_ABORTED", errorMessage);
|
|
33
39
|
case 2:
|
|
34
|
-
|
|
35
|
-
"
|
|
40
|
+
errorMessage = errorMessage !== null && errorMessage !== void 0 ? errorMessage : "A network error occurred which prevented the media from being " +
|
|
41
|
+
"successfully fetched";
|
|
42
|
+
throw new MediaError("MEDIA_ERR_NETWORK", errorMessage);
|
|
36
43
|
case 3:
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
errorMessage = errorMessage !== null && errorMessage !== void 0 ? errorMessage : "An error occurred while trying to decode the media resource";
|
|
45
|
+
throw new MediaError("MEDIA_ERR_DECODE", errorMessage);
|
|
39
46
|
case 4:
|
|
40
|
-
|
|
47
|
+
errorMessage = errorMessage !== null && errorMessage !== void 0 ? errorMessage : "The media resource has been found to be unsuitable.";
|
|
48
|
+
throw new MediaError("MEDIA_ERR_SRC_NOT_SUPPORTED", errorMessage);
|
|
41
49
|
default:
|
|
42
|
-
|
|
50
|
+
errorMessage = errorMessage !== null && errorMessage !== void 0 ? errorMessage : "The HTMLMediaElement errored due to an unknown reason.";
|
|
51
|
+
throw new MediaError("MEDIA_ERR_UNKNOWN", errorMessage);
|
|
43
52
|
}
|
|
44
53
|
}));
|
|
45
54
|
}
|
|
@@ -82,7 +82,7 @@ var Representation = /** @class */ (function () {
|
|
|
82
82
|
* @returns {Array.<Object>}
|
|
83
83
|
*/
|
|
84
84
|
Representation.prototype.getEncryptionData = function (drmSystemId) {
|
|
85
|
-
var _a;
|
|
85
|
+
var _a, _b;
|
|
86
86
|
var allInitData = this.getAllEncryptionData();
|
|
87
87
|
var filtered = [];
|
|
88
88
|
for (var i = 0; i < allInitData.length; i++) {
|
|
@@ -91,7 +91,7 @@ var Representation = /** @class */ (function () {
|
|
|
91
91
|
for (var j = 0; j < initData.values.length; j++) {
|
|
92
92
|
if (initData.values[j].systemId.toLowerCase() === drmSystemId.toLowerCase()) {
|
|
93
93
|
if (!createdObjForType) {
|
|
94
|
-
var keyIds = (_a = this.contentProtections) === null || _a === void 0 ? void 0 : _a.keyIds.map(function (val) { return val.keyId; });
|
|
94
|
+
var keyIds = (_b = (_a = this.contentProtections) === null || _a === void 0 ? void 0 : _a.keyIds) === null || _b === void 0 ? void 0 : _b.map(function (val) { return val.keyId; });
|
|
95
95
|
filtered.push({ type: initData.type, keyIds: keyIds, values: [initData.values[j]] });
|
|
96
96
|
createdObjForType = true;
|
|
97
97
|
}
|
|
@@ -130,12 +130,12 @@ var Representation = /** @class */ (function () {
|
|
|
130
130
|
* @returns {Array.<Object>}
|
|
131
131
|
*/
|
|
132
132
|
Representation.prototype.getAllEncryptionData = function () {
|
|
133
|
-
var _a;
|
|
133
|
+
var _a, _b;
|
|
134
134
|
if (this.contentProtections === undefined ||
|
|
135
135
|
this.contentProtections.initData.length === 0) {
|
|
136
136
|
return [];
|
|
137
137
|
}
|
|
138
|
-
var keyIds = (_a = this.contentProtections) === null || _a === void 0 ? void 0 : _a.keyIds.map(function (val) { return val.keyId; });
|
|
138
|
+
var keyIds = (_b = (_a = this.contentProtections) === null || _a === void 0 ? void 0 : _a.keyIds) === null || _b === void 0 ? void 0 : _b.map(function (val) { return val.keyId; });
|
|
139
139
|
return this.contentProtections.initData.map(function (x) {
|
|
140
140
|
return { type: x.type, keyIds: keyIds, values: x.values };
|
|
141
141
|
});
|
|
@@ -53,7 +53,17 @@ export default function flattenOverlappingPeriods(parsedPeriods) {
|
|
|
53
53
|
log.warn("DASH: Updating overlapping Periods.", lastFlattenedPeriod === null || lastFlattenedPeriod === void 0 ? void 0 : lastFlattenedPeriod.start, parsedPeriod.start);
|
|
54
54
|
lastFlattenedPeriod.duration = parsedPeriod.start - lastFlattenedPeriod.start;
|
|
55
55
|
lastFlattenedPeriod.end = parsedPeriod.start;
|
|
56
|
-
if (lastFlattenedPeriod.duration
|
|
56
|
+
if (lastFlattenedPeriod.duration > 0) {
|
|
57
|
+
// Note: Calling `break` to quit the while loop should theoritically be
|
|
58
|
+
// unnecessary as the previous operations should ensure we do not re-enter
|
|
59
|
+
// the loop's condition.
|
|
60
|
+
// Yet we dit encounter infinite loops without it because of float-related
|
|
61
|
+
// rounding errors.
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// `lastFlattenedPeriod` has now a negative or `0` duration.
|
|
66
|
+
// Remove it, consider the next Period in its place, and re-start the loop.
|
|
57
67
|
flattenedPeriods.pop();
|
|
58
68
|
lastFlattenedPeriod = flattenedPeriods[flattenedPeriods.length - 1];
|
|
59
69
|
}
|
|
@@ -166,6 +166,8 @@ export default class TimelineRepresentationIndex implements IRepresentationIndex
|
|
|
166
166
|
private _lastUpdate;
|
|
167
167
|
/** Absolute start of the period, timescaled and converted to index time. */
|
|
168
168
|
private _scaledPeriodStart;
|
|
169
|
+
/** Actual un-scaled start of the Period as indicated in the MPD. */
|
|
170
|
+
private _periodStart;
|
|
169
171
|
/** Absolute end of the period, timescaled and converted to index time. */
|
|
170
172
|
private _scaledPeriodEnd;
|
|
171
173
|
/** Whether this RepresentationIndex can change over time. */
|
|
@@ -73,6 +73,7 @@ var TimelineRepresentationIndex = /** @class */ (function () {
|
|
|
73
73
|
mediaURLs: createIndexURLs(urlSources, index.media, representationId, representationBitrate),
|
|
74
74
|
startNumber: index.startNumber,
|
|
75
75
|
timeline: (_c = index.timeline) !== null && _c !== void 0 ? _c : null, timescale: timescale };
|
|
76
|
+
this._periodStart = periodStart;
|
|
76
77
|
this._scaledPeriodStart = toIndexTime(periodStart, this._index);
|
|
77
78
|
this._scaledPeriodEnd = periodEnd === undefined ? undefined :
|
|
78
79
|
toIndexTime(periodEnd, this._index);
|
|
@@ -201,6 +202,7 @@ var TimelineRepresentationIndex = /** @class */ (function () {
|
|
|
201
202
|
this._index = newIndex._index;
|
|
202
203
|
this._isDynamic = newIndex._isDynamic;
|
|
203
204
|
this._scaledPeriodStart = newIndex._scaledPeriodStart;
|
|
205
|
+
this._periodStart = newIndex._periodStart;
|
|
204
206
|
this._scaledPeriodEnd = newIndex._scaledPeriodEnd;
|
|
205
207
|
this._lastUpdate = newIndex._lastUpdate;
|
|
206
208
|
this._manifestBoundsCalculator = newIndex._manifestBoundsCalculator;
|
|
@@ -224,6 +226,7 @@ var TimelineRepresentationIndex = /** @class */ (function () {
|
|
|
224
226
|
}
|
|
225
227
|
this._isDynamic = newIndex._isDynamic;
|
|
226
228
|
this._scaledPeriodStart = newIndex._scaledPeriodStart;
|
|
229
|
+
this._periodStart = newIndex._periodStart;
|
|
227
230
|
this._scaledPeriodEnd = newIndex._scaledPeriodEnd;
|
|
228
231
|
this._lastUpdate = newIndex._lastUpdate;
|
|
229
232
|
this._isLastPeriod = newIndex._isLastPeriod;
|
|
@@ -279,6 +282,9 @@ var TimelineRepresentationIndex = /** @class */ (function () {
|
|
|
279
282
|
if (this._index.timeline === null) {
|
|
280
283
|
this._index.timeline = this._getTimeline();
|
|
281
284
|
}
|
|
285
|
+
if (!this._isDynamic) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
282
288
|
var firstPosition = this._manifestBoundsCalculator.estimateMinimumBound();
|
|
283
289
|
if (firstPosition == null) {
|
|
284
290
|
return; // we don't know yet
|
|
@@ -327,11 +333,12 @@ var TimelineRepresentationIndex = /** @class */ (function () {
|
|
|
327
333
|
}
|
|
328
334
|
var newElements = this._parseTimeline();
|
|
329
335
|
this._parseTimeline = null; // Free memory
|
|
336
|
+
var actualPeriodStart = this._periodStart * this._index.timescale;
|
|
330
337
|
var MIN_DASH_S_ELEMENTS_TO_PARSE_UNSAFELY = config.getCurrent().MIN_DASH_S_ELEMENTS_TO_PARSE_UNSAFELY;
|
|
331
338
|
if (this._unsafelyBaseOnPreviousIndex === null ||
|
|
332
339
|
newElements.length < MIN_DASH_S_ELEMENTS_TO_PARSE_UNSAFELY) {
|
|
333
340
|
// Just completely parse the current timeline
|
|
334
|
-
return constructTimelineFromElements(newElements,
|
|
341
|
+
return constructTimelineFromElements(newElements, actualPeriodStart);
|
|
335
342
|
}
|
|
336
343
|
// Construct previously parsed timeline if not already done
|
|
337
344
|
var prevTimeline;
|
|
@@ -343,7 +350,7 @@ var TimelineRepresentationIndex = /** @class */ (function () {
|
|
|
343
350
|
prevTimeline = this._unsafelyBaseOnPreviousIndex._index.timeline;
|
|
344
351
|
}
|
|
345
352
|
this._unsafelyBaseOnPreviousIndex = null; // Free memory
|
|
346
|
-
return constructTimelineFromPreviousTimeline(newElements, prevTimeline,
|
|
353
|
+
return constructTimelineFromPreviousTimeline(newElements, prevTimeline, actualPeriodStart);
|
|
347
354
|
};
|
|
348
355
|
return TimelineRepresentationIndex;
|
|
349
356
|
}());
|
|
@@ -175,7 +175,13 @@ export default function parseRepresentations(representationsIR, adaptation, cont
|
|
|
175
175
|
.toLowerCase();
|
|
176
176
|
}
|
|
177
177
|
if (cp.attributes.keyId !== undefined && cp.attributes.keyId.length > 0) {
|
|
178
|
-
|
|
178
|
+
var kidObj = { keyId: cp.attributes.keyId, systemId: systemId };
|
|
179
|
+
if (acc.keyIds === undefined) {
|
|
180
|
+
acc.keyIds = [kidObj];
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
acc.keyIds.push(kidObj);
|
|
184
|
+
}
|
|
179
185
|
}
|
|
180
186
|
if (systemId !== undefined) {
|
|
181
187
|
var cencPssh = cp.children.cencPssh;
|
|
@@ -195,9 +201,10 @@ export default function parseRepresentations(representationsIR, adaptation, cont
|
|
|
195
201
|
}
|
|
196
202
|
}
|
|
197
203
|
return acc;
|
|
198
|
-
}, { keyIds:
|
|
204
|
+
}, { keyIds: undefined, initData: [] });
|
|
199
205
|
if (Object.keys(contentProtections.initData).length > 0 ||
|
|
200
|
-
contentProtections.keyIds
|
|
206
|
+
(contentProtections.keyIds !== undefined &&
|
|
207
|
+
contentProtections.keyIds.length > 0)) {
|
|
201
208
|
parsedRepresentation.contentProtections = contentProtections;
|
|
202
209
|
}
|
|
203
210
|
}
|
|
@@ -67,8 +67,13 @@ export interface IContentProtectionInitData {
|
|
|
67
67
|
*/
|
|
68
68
|
/** Describes every encryption protection parsed for a given media. */
|
|
69
69
|
export interface IContentProtections {
|
|
70
|
-
/**
|
|
71
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The different encryption key IDs associated with that content.
|
|
72
|
+
*
|
|
73
|
+
* `undefined` if the key id(s) associated with that content may exist but are
|
|
74
|
+
* not known.
|
|
75
|
+
*/
|
|
76
|
+
keyIds: IContentProtectionKID[] | undefined;
|
|
72
77
|
/** The different encryption initialization data associated with that content. */
|
|
73
78
|
initData: IContentProtectionInitData[];
|
|
74
79
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
+
import isNonEmptyString from "../../../../utils/is_non_empty_string";
|
|
16
17
|
/**
|
|
17
18
|
* Try to replicate the textOutline TTML style property into CSS.
|
|
18
19
|
*
|
|
@@ -24,8 +25,17 @@
|
|
|
24
25
|
* @returns {string}
|
|
25
26
|
*/
|
|
26
27
|
export default function generateCSSTextOutline(color, thickness) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
var thick = thickness;
|
|
29
|
+
if (isNonEmptyString(thickness) && thickness.trim().endsWith("%")) {
|
|
30
|
+
// As em and % are basically equivalent in CSS
|
|
31
|
+
// (they both are relative to the font-size
|
|
32
|
+
// of the current element)
|
|
33
|
+
// We convert the non supported % into the supported em
|
|
34
|
+
thick = thickness.trim().slice(0, -1);
|
|
35
|
+
thick = (parseInt(thick, 10) / 100).toString() + "em";
|
|
36
|
+
}
|
|
37
|
+
return "-1px -1px ".concat(thick, " ").concat(color, ",") +
|
|
38
|
+
"1px -1px ".concat(thick, " ").concat(color, ",") +
|
|
39
|
+
"-1px 1px ".concat(thick, " ").concat(color, ",") +
|
|
40
|
+
"1px 1px ".concat(thick, " ").concat(color);
|
|
31
41
|
}
|