Skip to content

Commit 2da98a9

Browse files
authored
Replace JSON.stringify with safe stringify module (video-dev#7045)
* Replace JSON.stringify with safe stringify module Resolves video-dev#7036 (Follow up to video-dev#7037) * Add custom replacer unit tests Resolves video-dev#7036 (Follow up to video-dev#7037)
1 parent d646786 commit 2da98a9

14 files changed

+104
-33
lines changed

src/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { TimelineController } from './controller/timeline-controller';
1515
import Cues from './utils/cues';
1616
import FetchLoader, { fetchSupported } from './utils/fetch-loader';
1717
import { requestMediaKeySystemAccess } from './utils/mediakeys-helper';
18+
import { stringify } from './utils/safe-json-stringify';
1819
import XhrLoader from './utils/xhr-loader';
1920
import type { MediaKeySessionContext } from './controller/eme-controller';
2021
import type Hls from './hls';
@@ -688,7 +689,7 @@ export function mergeConfig(
688689
logger.warn(
689690
`hls.js config: "${report.join(
690691
'", "',
691-
)}" setting(s) are deprecated, use "${policyName}": ${JSON.stringify(
692+
)}" setting(s) are deprecated, use "${policyName}": ${stringify(
692693
userConfig[policyName],
693694
)}`,
694695
);

src/controller/abr-controller.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
getCodecTiers,
1616
getStartCodecTier,
1717
} from '../utils/rendition-helper';
18+
import { stringify } from '../utils/safe-json-stringify';
1819
import type Hls from '../hls';
1920
import type { Fragment } from '../loader/fragment';
2021
import type { Part } from '../loader/fragment';
@@ -764,7 +765,7 @@ class AbrController extends Logger implements AbrComponentAPI {
764765
: videoRanges[0];
765766
currentFrameRate = minFramerate;
766767
currentBw = Math.max(currentBw, minBitrate);
767-
this.log(`picked start tier ${JSON.stringify(startTier)}`);
768+
this.log(`picked start tier ${stringify(startTier)}`);
768769
} else {
769770
currentCodecSet = level?.codecSet;
770771
currentVideoRange = level?.videoRange;
@@ -821,11 +822,11 @@ class AbrController extends Logger implements AbrComponentAPI {
821822
this.warn(
822823
`MediaCapabilities decodingInfo error: "${
823824
decodingInfo.error
824-
}" for level ${index} ${JSON.stringify(decodingInfo)}`,
825+
}" for level ${index} ${stringify(decodingInfo)}`,
825826
);
826827
} else if (!decodingInfo.supported) {
827828
this.warn(
828-
`Unsupported MediaCapabilities decodingInfo result for level ${index} ${JSON.stringify(
829+
`Unsupported MediaCapabilities decodingInfo result for level ${index} ${stringify(
829830
decodingInfo,
830831
)}`,
831832
);

src/controller/buffer-controller.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
isCompatibleTrackChange,
1616
isManagedMediaSource,
1717
} from '../utils/mediasource-helper';
18+
import { stringify } from '../utils/safe-json-stringify';
1819
import type { FragmentTracker } from './fragment-tracker';
1920
import type { HlsConfig } from '../config';
2021
import type Hls from '../hls';
@@ -353,8 +354,8 @@ export default class BufferController extends Logger implements ComponentAPI {
353354
}
354355
this
355356
.log(`attachTransferred: (bufferCodecEventsTotal ${this.bufferCodecEventsTotal})
356-
required tracks: ${JSON.stringify(requiredTracks, (key, value) => (key === 'initSegment' ? undefined : value))};
357-
transfer tracks: ${JSON.stringify(transferredTracks, (key, value) => (key === 'initSegment' ? undefined : value))}}`);
357+
required tracks: ${stringify(requiredTracks, (key, value) => (key === 'initSegment' ? undefined : value))};
358+
transfer tracks: ${stringify(transferredTracks, (key, value) => (key === 'initSegment' ? undefined : value))}}`);
358359
if (!isCompatibleTrackChange(transferredTracks, requiredTracks)) {
359360
// destroy attaching media source
360361
data.mediaSource = null;
@@ -1322,7 +1323,7 @@ transfer tracks: ${JSON.stringify(transferredTracks, (key, value) => (key === 'i
13221323
private checkPendingTracks() {
13231324
const { bufferCodecEventsTotal, pendingTrackCount, tracks } = this;
13241325
this.log(
1325-
`checkPendingTracks (pending: ${pendingTrackCount} codec events expected: ${bufferCodecEventsTotal}) ${JSON.stringify(tracks)}`,
1326+
`checkPendingTracks (pending: ${pendingTrackCount} codec events expected: ${bufferCodecEventsTotal}) ${stringify(tracks)}`,
13261327
);
13271328
// Check if we've received all of the expected bufferCodec events. When none remain, create all the sourceBuffers at once.
13281329
// This is important because the MSE spec allows implementations to throw QuotaExceededErrors if creating new sourceBuffers after
@@ -1391,7 +1392,7 @@ transfer tracks: ${JSON.stringify(transferredTracks, (key, value) => (key === 'i
13911392
const mimeType = `${track.container};codecs=${codec}`;
13921393
track.codec = codec;
13931394
this.log(
1394-
`creating sourceBuffer(${mimeType})${this.currentOp(type) ? ' Queued' : ''} ${JSON.stringify(track)}`,
1395+
`creating sourceBuffer(${mimeType})${this.currentOp(type) ? ' Queued' : ''} ${stringify(track)}`,
13951396
);
13961397
try {
13971398
const sb = mediaSource.addSourceBuffer(

src/controller/content-steering-controller.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { AttrList } from '../utils/attr-list';
1414
import { reassignFragmentLevelIndexes } from '../utils/level-helper';
1515
import { Logger } from '../utils/logger';
16+
import { stringify } from '../utils/safe-json-stringify';
1617
import type { RetryConfig } from '../config';
1718
import type Hls from '../hls';
1819
import type { NetworkComponentAPI } from '../types/component-api';
@@ -221,9 +222,9 @@ export default class ContentSteeringController
221222
data.error.message
222223
}") with content-steering for Pathway: ${errorPathway} levels: ${
223224
levels ? levels.length : levels
224-
} priorities: ${JSON.stringify(
225+
} priorities: ${stringify(
225226
pathwayPriority,
226-
)} penalized: ${JSON.stringify(this.penalizedPathways)}`,
227+
)} penalized: ${stringify(this.penalizedPathways)}`,
227228
);
228229
}
229230
}

src/controller/eme-controller.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
type PsshInvalidResult,
2828
} from '../utils/mp4-tools';
2929
import { base64Decode } from '../utils/numeric-encoding-utils';
30+
import { stringify } from '../utils/safe-json-stringify';
3031
import { strToUtf8array } from '../utils/utf8-utils';
3132
import type { EMEControllerConfig, HlsConfig, LoadPolicy } from '../config';
3233
import type Hls from '../hls';
@@ -254,7 +255,7 @@ class EMEController extends Logger implements ComponentAPI {
254255
let keySystemAccess = keySystemAccessPromises?.keySystemAccess;
255256
if (!keySystemAccess) {
256257
this.log(
257-
`Requesting encrypted media "${keySystem}" key-system access with config: ${JSON.stringify(
258+
`Requesting encrypted media "${keySystem}" key-system access with config: ${stringify(
258259
mediaKeySystemConfigs,
259260
)}`,
260261
);
@@ -519,7 +520,7 @@ class EMEController extends Logger implements ComponentAPI {
519520
details: ErrorDetails.KEY_SYSTEM_NO_CONFIGURED_LICENSE,
520521
fatal: true,
521522
},
522-
`Missing key-system license configuration options ${JSON.stringify({
523+
`Missing key-system license configuration options ${stringify({
523524
drmSystems: this.config.drmSystems,
524525
})}`,
525526
);

src/controller/gap-controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
addEventListener,
99
removeEventListener,
1010
} from '../utils/event-listener-helper';
11+
import { stringify } from '../utils/safe-json-stringify';
1112
import type { InFlightData } from './base-stream-controller';
1213
import type { InFlightFragments } from '../hls';
1314
import type Hls from '../hls';
@@ -486,7 +487,7 @@ export default class GapController extends TaskLoop {
486487
const error = new Error(
487488
`Playback stalling at @${
488489
media.currentTime
489-
} due to low buffer (${JSON.stringify(bufferInfo)})`,
490+
} due to low buffer (${stringify(bufferInfo)})`,
490491
);
491492
this.warn(error.message);
492493
hls.trigger(Events.ERROR, {

src/controller/id3-track-controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
isSCTE35Attribute,
77
} from '../loader/date-range';
88
import { MetadataSchema } from '../types/demuxer';
9+
import { stringify } from '../utils/safe-json-stringify';
910
import {
1011
clearCurrentCues,
1112
removeCuesInRange,
@@ -54,7 +55,7 @@ function createCueWithDataFields(
5455
cue = new Cue(
5556
startTime,
5657
endTime,
57-
JSON.stringify(type ? { type, ...data } : data),
58+
stringify(type ? { type, ...data } : data),
5859
);
5960
}
6061
return cue;

src/controller/level-controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
videoCodecPreferenceValue,
1313
} from '../utils/codecs';
1414
import { reassignFragmentLevelIndexes } from '../utils/level-helper';
15+
import { stringify } from '../utils/safe-json-stringify';
1516
import type ContentSteeringController from './content-steering-controller';
1617
import type Hls from '../hls';
1718
import type {
@@ -248,7 +249,7 @@ export default class LevelController extends BasePlaylistController {
248249
if (this.hls) {
249250
if (data.levels.length) {
250251
this.warn(
251-
`One or more CODECS in variant not supported: ${JSON.stringify(
252+
`One or more CODECS in variant not supported: ${stringify(
252253
data.levels[0].attrs,
253254
)}`,
254255
);

src/demux/transmuxer-interface.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { ErrorDetails, ErrorTypes } from '../errors';
1414
import { Events } from '../events';
1515
import { PlaylistLevelType } from '../types/loader';
1616
import { getM2TSSupportedAudioTypes } from '../utils/codecs';
17+
import { stringify } from '../utils/safe-json-stringify';
1718
import type { WorkerContext } from './inject-worker';
1819
import type { HlsEventEmitter, HlsListeners } from '../events';
1920
import type Hls from '../hls';
@@ -95,7 +96,7 @@ export default class TransmuxerInterface {
9596
cmd: 'init',
9697
typeSupported: m2tsTypeSupported,
9798
id,
98-
config: JSON.stringify(config),
99+
config: stringify(config),
99100
});
100101
} catch (err) {
101102
logger.warn(
@@ -143,7 +144,7 @@ export default class TransmuxerInterface {
143144
resetNo: instanceNo,
144145
typeSupported: m2tsTypeSupported,
145146
id: this.id,
146-
config: JSON.stringify(config),
147+
config: stringify(config),
147148
});
148149
}
149150
}

src/utils/cea-608-parser.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { stringify } from './safe-json-stringify';
12
import { logger } from '../utils/logger';
23
import type OutputFilter from './output-filter';
34

@@ -593,10 +594,7 @@ export class CaptionScreen {
593594
}
594595

595596
setPAC(pacData: PACData) {
596-
this.logger.log(
597-
VerboseLevel.INFO,
598-
() => 'pacData = ' + JSON.stringify(pacData),
599-
);
597+
this.logger.log(VerboseLevel.INFO, () => 'pacData = ' + stringify(pacData));
600598
let newRow = pacData.row - 1;
601599
if (this.nrRollUpRows && newRow < this.nrRollUpRows - 1) {
602600
newRow = this.nrRollUpRows - 1;
@@ -650,10 +648,7 @@ export class CaptionScreen {
650648
* Set background/extra foreground, but first do back_space, and then insert space (backwards compatibility).
651649
*/
652650
setBkgData(bkgData: Partial<PenStyles>) {
653-
this.logger.log(
654-
VerboseLevel.INFO,
655-
() => 'bkgData = ' + JSON.stringify(bkgData),
656-
);
651+
this.logger.log(VerboseLevel.INFO, () => 'bkgData = ' + stringify(bkgData));
657652
this.backSpace();
658653
this.setPen(bkgData);
659654
this.insertChar(0x20); // Space
@@ -951,7 +946,7 @@ class Cea608Channel {
951946
} else {
952947
styles.foreground = 'white';
953948
}
954-
this.logger.log(VerboseLevel.INFO, 'MIDROW: ' + JSON.stringify(styles));
949+
this.logger.log(VerboseLevel.INFO, 'MIDROW: ' + stringify(styles));
955950
this.writeScreen.setPen(styles);
956951
}
957952

src/utils/level-helper.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import { logger } from './logger';
6+
import { stringify } from './safe-json-stringify';
67
import { DateRange } from '../loader/date-range';
78
import { assignProgramDateTime, mapDateRanges } from '../loader/m3u8-parser';
89
import type { Fragment, MediaFragment, Part } from '../loader/fragment';
@@ -344,7 +345,7 @@ function mergeDateRanges(
344345
}
345346
} else {
346347
logger.warn(
347-
`Ignoring invalid Playlist Delta Update DATERANGE tag: "${JSON.stringify(
348+
`Ignoring invalid Playlist Delta Update DATERANGE tag: "${stringify(
348349
deltaDateRanges[id].attr,
349350
)}"`,
350351
);

src/utils/rendition-helper.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { codecsSetSelectionPreferenceValue } from './codecs';
22
import { getVideoSelectionOptions } from './hdr';
33
import { logger } from './logger';
4+
import { stringify } from './safe-json-stringify';
45
import type Hls from '../hls';
56
import type { Level, VideoRange } from '../types/level';
67
import type {
@@ -165,9 +166,7 @@ export function getStartCodecTier(
165166
) {
166167
logStartCodecCandidateIgnored(
167168
candidate,
168-
`no variants with VIDEO-RANGE of ${JSON.stringify(
169-
videoRanges,
170-
)} found`,
169+
`no variants with VIDEO-RANGE of ${stringify(videoRanges)} found`,
171170
);
172171
return selected;
173172
}

src/utils/safe-json-stringify.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
const replacer = () => {
1+
const omitCircularRefsReplacer = (
2+
replacer: ((this: any, key: string, value: any) => any) | undefined,
3+
) => {
24
const known = new WeakSet();
35
return (_, value) => {
6+
if (replacer) {
7+
value = replacer(_, value);
8+
}
49
if (typeof value === 'object' && value !== null) {
510
if (known.has(value)) {
611
return;
@@ -11,5 +16,7 @@ const replacer = () => {
1116
};
1217
};
1318

14-
export const stringify = <T>(object: T): string =>
15-
JSON.stringify(object, replacer());
19+
export const stringify = <T>(
20+
object: T,
21+
replacer?: (this: any, key: string, value: any) => any,
22+
): string => JSON.stringify(object, omitCircularRefsReplacer(replacer));

tests/unit/utils/safe-json-stringify.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,64 @@ describe('Stringify', function () {
1111

1212
expect(stringified).to.equal(JSON.stringify(originalObject));
1313
});
14+
15+
it('should use the optional custom replacer', function () {
16+
const stringified = stringify(
17+
{
18+
a: '1',
19+
b: { c: 2 },
20+
},
21+
(k, v) => (k === 'b' ? '2' : v),
22+
);
23+
24+
expect(stringified).to.equal(
25+
JSON.stringify({
26+
a: '1',
27+
b: '2',
28+
}),
29+
);
30+
});
31+
32+
it('uses the optional custom replacer before checking for cyclical references', function () {
33+
const originalObject = { a: 'test' };
34+
35+
const circularObject = { ...originalObject };
36+
circularObject.b = circularObject;
37+
38+
const stringified = stringify(circularObject, (k, v) =>
39+
k === 'b' ? 'replaced' : v,
40+
);
41+
42+
expect(stringified).to.equal(
43+
JSON.stringify({
44+
a: 'test',
45+
b: 'replaced',
46+
}),
47+
);
48+
});
49+
50+
it('cyclical references added by the custom replacer are removed', function () {
51+
const originalObject = { a: 'test' };
52+
53+
const circularObject = { ...originalObject };
54+
circularObject.b = {};
55+
circularObject.d = 'replace-with-circular-object';
56+
57+
const stringified = stringify(circularObject, (k, v) => {
58+
if (k === 'b') {
59+
v.b = circularObject;
60+
v.c = 'test';
61+
} else if (v === 'replace-with-circular-object') {
62+
v = circularObject;
63+
}
64+
return v;
65+
});
66+
67+
expect(stringified).to.equal(
68+
JSON.stringify({
69+
a: 'test',
70+
b: { c: 'test' },
71+
}),
72+
);
73+
});
1474
});

0 commit comments

Comments
 (0)