@rtcstats/rtcstats-js 2.1.0 → 2.2.1
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 +37 -1
- package/package.json +1 -1
- package/peerconnection.js +89 -45
- package/trace-websocket.js +53 -17
package/README.md
CHANGED
|
@@ -39,12 +39,48 @@ trace.connect('ws://localhost:8080' + window.location.pathname);
|
|
|
39
39
|
const pc = new RTCPeerConnection();
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
### Nondefault options
|
|
43
|
+
|
|
44
|
+
### Counting reloads
|
|
45
|
+
|
|
46
|
+
rtcstats-js can count the number of times a user has reloaded the page by looking at the sessionStorage.
|
|
47
|
+
If enabled with
|
|
48
|
+
```
|
|
49
|
+
const trace = new WebSocketTrace({countReloads: true});
|
|
50
|
+
```
|
|
51
|
+
this will read the key `rtcstatsReloadCount` from session storage, increment it and write it back.
|
|
52
|
+
|
|
53
|
+
Reloading the page in the middle of a call is a last resort often used by users when something goes
|
|
54
|
+
really wrong. This feature does not count actual page reloads which would require more DOM event
|
|
55
|
+
listeners but the number of times the trace function has been instantiated which should be once
|
|
56
|
+
per page load.
|
|
57
|
+
|
|
58
|
+
### Tracing custom events
|
|
59
|
+
Sometimes you may want to send your own events to RTCStats to have them all in one dump file.
|
|
60
|
+
This can be accomplished with the `trace` function as well. The method signature for this is
|
|
61
|
+
```
|
|
62
|
+
trace(method name, e.g. `:userRating`,
|
|
63
|
+
peerconnection object to associate with or null if not associated,
|
|
64
|
+
javascript object with data or string)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
* The first argument is the method name MUST start with a colon (`:`) to avoid conflicts with any method names that rtcstats-js and rtcstats-server use internally.
|
|
68
|
+
* If the event is to be associated with a particular RTCPeerConnection, pass that connection as the second argument.
|
|
69
|
+
If it is not, e.g. for an end-of-call user rating, pass `null`.
|
|
70
|
+
* The third argument can be a Javascript string or object which will be encoded using JSON when sent to the server. If there is no additional data, pass `null`.
|
|
71
|
+
* The trace function will internally add a timestamp to the event.
|
|
72
|
+
* rtcstats-server will not process such events by default.
|
|
73
|
+
* A dump-importer should still display a generic rendering of the event.
|
|
74
|
+
|
|
42
75
|
### Using a JWT to connect to rtcstats-server
|
|
43
76
|
See [the server README](https://github.com/rtcstats/rtcstats/blob/main/packages/rtcstats-server/README.md) for how to
|
|
44
77
|
generate JWT token with information about the user, session and conference.
|
|
45
78
|
|
|
46
79
|
If the server is configured to require an authorization token, the websocket will
|
|
47
|
-
be closed with a 1008 policy-violation error
|
|
80
|
+
be closed with a 1008 policy-violation error which can be seen when configuring the logging callback:
|
|
81
|
+
```
|
|
82
|
+
const trace = new WebSocketTrace({log: console.warn.bind(console)});
|
|
83
|
+
```
|
|
48
84
|
|
|
49
85
|
### Bundling
|
|
50
86
|
To bundle rtcstats-js including its dependencies, use
|
package/package.json
CHANGED
package/peerconnection.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
statsCompression,
|
|
3
|
+
descriptionCompression,
|
|
4
|
+
compressMethod,
|
|
5
|
+
computePressureTable,
|
|
6
|
+
} from '@rtcstats/rtcstats-shared';
|
|
2
7
|
import {map2obj, dumpTrackWithStreams, copyAndSanitizeConfig} from '@rtcstats/rtcstats-shared';
|
|
3
8
|
|
|
4
9
|
/**
|
|
@@ -42,27 +47,27 @@ function wrapRTCRtpSender(trace, window) {
|
|
|
42
47
|
const nativeMethod = window.RTCRtpSender.prototype[method];
|
|
43
48
|
if (!nativeMethod) return;
|
|
44
49
|
|
|
45
|
-
window.RTCRtpSender.prototype[method] = function(...args) {
|
|
46
|
-
const serializedArgs = JSON.parse(JSON.stringify(args));
|
|
50
|
+
window.RTCRtpSender.prototype[method] = function(parameters, ...args) {
|
|
51
|
+
const serializedArgs = JSON.parse(JSON.stringify([parameters, ...args]));
|
|
47
52
|
delete serializedArgs[0].transactionId;
|
|
48
53
|
trace(method, this.__rtcStatsId,
|
|
49
54
|
serializedArgs,
|
|
50
55
|
this.__rtcStatsSenderId);
|
|
51
|
-
return nativeMethod.apply(this, args);
|
|
56
|
+
return nativeMethod.apply(this, [parameters, ...args]);
|
|
52
57
|
};
|
|
53
58
|
});
|
|
54
59
|
['replaceTrack'].forEach(method => {
|
|
55
60
|
const nativeMethod = window.RTCRtpSender.prototype[method];
|
|
56
61
|
if (!nativeMethod) return;
|
|
57
|
-
window.RTCRtpSender.prototype[method] = function(...args) {
|
|
62
|
+
window.RTCRtpSender.prototype[method] = function(track, ...args) {
|
|
58
63
|
const serializedArgs = [
|
|
59
64
|
this.track === null ? null : dumpTrackWithStreams(this.track),
|
|
60
|
-
|
|
65
|
+
track === null ? null : dumpTrackWithStreams(track),
|
|
61
66
|
];
|
|
62
67
|
trace(method, this.__rtcStatsId,
|
|
63
68
|
serializedArgs,
|
|
64
69
|
this.__rtcStatsSenderId);
|
|
65
|
-
return nativeMethod.apply(this, args);
|
|
70
|
+
return nativeMethod.apply(this, [track, ...args]);
|
|
66
71
|
};
|
|
67
72
|
});
|
|
68
73
|
}
|
|
@@ -84,6 +89,14 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
|
|
|
84
89
|
// Prevent double-wrapping.
|
|
85
90
|
return;
|
|
86
91
|
}
|
|
92
|
+
let lastComputePressureRecord;
|
|
93
|
+
if (window.PressureObserver) {
|
|
94
|
+
const observer = new PressureObserver((records) => {
|
|
95
|
+
const lastRecord = records[records.length - 1]; // Usually only one record.
|
|
96
|
+
lastComputePressureRecord = [Date.now(), lastRecord];
|
|
97
|
+
});
|
|
98
|
+
observer.observe('cpu', {sampleInterval: getStatsInterval});
|
|
99
|
+
}
|
|
87
100
|
wrapRTCRtpTransceiver(trace, window);
|
|
88
101
|
wrapRTCRtpSender(trace, window);
|
|
89
102
|
const OrigPeerConnection = window.RTCPeerConnection;
|
|
@@ -108,6 +121,15 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
|
|
|
108
121
|
}
|
|
109
122
|
|
|
110
123
|
pc.addEventListener('icecandidate', (e) => {
|
|
124
|
+
// Browser intentionally don't serialize .url and .relayProtocol in .toJSON
|
|
125
|
+
// but we want them for rtcstats.
|
|
126
|
+
if (e.candidate && (e.candidate.url || e.candidate.relayProtocol)) {
|
|
127
|
+
const serializedCandidate = e.candidate.toJSON();
|
|
128
|
+
serializedCandidate.url = e.candidate.url;
|
|
129
|
+
serializedCandidate.relayProtocol = e.candidate.relayProtocol;
|
|
130
|
+
trace('onicecandidate', pcId, serializedCandidate);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
111
133
|
trace('onicecandidate', pcId, e.candidate);
|
|
112
134
|
});
|
|
113
135
|
pc.addEventListener('icecandidateerror', (e) => {
|
|
@@ -176,6 +198,14 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
|
|
|
176
198
|
if (pc.signalingState === 'closed') {
|
|
177
199
|
return;
|
|
178
200
|
}
|
|
201
|
+
if (lastComputePressureRecord) {
|
|
202
|
+
const [timestamp, record] = lastComputePressureRecord;
|
|
203
|
+
stats['rtcStatsComputePressure'] = {
|
|
204
|
+
type: 'compute-pressure',
|
|
205
|
+
timestamp,
|
|
206
|
+
cpuState: computePressureTable[record.state] || record.state,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
179
209
|
const baseStats = JSON.parse(JSON.stringify(stats)); // our new prevStats.
|
|
180
210
|
const compressedStats = statsCompression(prevStats, stats, statsIdMap);
|
|
181
211
|
if (reason) {
|
|
@@ -201,9 +231,9 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
|
|
|
201
231
|
['createDataChannel'].forEach(method => {
|
|
202
232
|
const nativeMethod = OrigPeerConnection.prototype[method];
|
|
203
233
|
if (!nativeMethod) return;
|
|
204
|
-
OrigPeerConnection.prototype[method] = function(...args) {
|
|
205
|
-
trace(method, this.__rtcStatsId, args);
|
|
206
|
-
return nativeMethod.apply(this, args);
|
|
234
|
+
OrigPeerConnection.prototype[method] = function(label, ...args) {
|
|
235
|
+
trace(method, this.__rtcStatsId, [label, ...args]);
|
|
236
|
+
return nativeMethod.apply(this, [label, ...args]);
|
|
207
237
|
};
|
|
208
238
|
});
|
|
209
239
|
|
|
@@ -219,11 +249,10 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
|
|
|
219
249
|
['addTrack'].forEach(method => {
|
|
220
250
|
const nativeMethod = OrigPeerConnection.prototype[method];
|
|
221
251
|
if (!nativeMethod) return;
|
|
222
|
-
OrigPeerConnection.prototype[method] = function(...args) {
|
|
223
|
-
const
|
|
224
|
-
const streams = args.slice(1);
|
|
252
|
+
OrigPeerConnection.prototype[method] = function(track, ...args) {
|
|
253
|
+
const streams = args;
|
|
225
254
|
trace(method, this.__rtcStatsId, dumpTrackWithStreams(track, ...streams));
|
|
226
|
-
const sender = nativeMethod.apply(this, args);
|
|
255
|
+
const sender = nativeMethod.apply(this, [track, ...args]);
|
|
227
256
|
sender.__rtcStatsId = this.__rtcStatsId;
|
|
228
257
|
const transceiver = this.getTransceivers().find(t => t.sender === sender);
|
|
229
258
|
if (transceiver) {
|
|
@@ -238,18 +267,18 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
|
|
|
238
267
|
['addTransceiver'].forEach(method => {
|
|
239
268
|
const nativeMethod = OrigPeerConnection.prototype[method];
|
|
240
269
|
if (!nativeMethod) return;
|
|
241
|
-
OrigPeerConnection.prototype[method] = function(...args) {
|
|
270
|
+
OrigPeerConnection.prototype[method] = function(trackOrKind, ...args) {
|
|
242
271
|
const serializedArgs = [
|
|
243
|
-
typeof(
|
|
272
|
+
typeof(trackOrKind) === 'string' ? trackOrKind : dumpTrackWithStreams(trackOrKind),
|
|
244
273
|
];
|
|
245
|
-
if (args[
|
|
246
|
-
serializedArgs.push(JSON.parse(JSON.stringify(args[
|
|
247
|
-
if (args[
|
|
248
|
-
serializedArgs[serializedArgs.length - 1].streams = args[
|
|
274
|
+
if (args[0]) {
|
|
275
|
+
serializedArgs.push(JSON.parse(JSON.stringify(args[0])));
|
|
276
|
+
if (args[0].streams) {
|
|
277
|
+
serializedArgs[serializedArgs.length - 1].streams = args[0].streams.map(s => s.id);
|
|
249
278
|
}
|
|
250
279
|
}
|
|
251
280
|
trace(method, this.__rtcStatsId, serializedArgs);
|
|
252
|
-
const transceiver = nativeMethod.apply(this, args);
|
|
281
|
+
const transceiver = nativeMethod.apply(this, [trackOrKind, ...args]);
|
|
253
282
|
transceiver.__rtcStatsId = this.__rtcStatsId;
|
|
254
283
|
transceiver.sender.__rtcStatsId = this.__rtcStatsId;
|
|
255
284
|
transceiver.sender.__rtcStatsSenderId = transceiver.receiver.track.id;
|
|
@@ -261,9 +290,9 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
|
|
|
261
290
|
['removeTrack'].forEach(method => {
|
|
262
291
|
const nativeMethod = OrigPeerConnection.prototype[method];
|
|
263
292
|
if (!nativeMethod) return;
|
|
264
|
-
OrigPeerConnection.prototype[method] = function(...args) {
|
|
265
|
-
trace(method, this.__rtcStatsId,
|
|
266
|
-
return nativeMethod.apply(this, args);
|
|
293
|
+
OrigPeerConnection.prototype[method] = function(sender, ...args) {
|
|
294
|
+
trace(method, this.__rtcStatsId, sender.__rtcStatsSenderId);
|
|
295
|
+
return nativeMethod.apply(this, [sender, ...args]);
|
|
267
296
|
};
|
|
268
297
|
});
|
|
269
298
|
|
|
@@ -294,39 +323,33 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
|
|
|
294
323
|
};
|
|
295
324
|
});
|
|
296
325
|
|
|
297
|
-
['setLocalDescription'
|
|
326
|
+
['setLocalDescription'].forEach(method => {
|
|
298
327
|
const nativeMethod = OrigPeerConnection.prototype[method];
|
|
299
328
|
if (!nativeMethod) return;
|
|
300
329
|
OrigPeerConnection.prototype[method] = function(...args) {
|
|
301
330
|
const trackingId = compressMethod(method) + '-' + (counters[method]++);
|
|
302
331
|
let implicitBaseDescription;
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
explicitBaseDescription = this.__rtcStatsLastCreatedAnswer;
|
|
310
|
-
}
|
|
311
|
-
delete this.__rtcStatsLastCreatedOffer;
|
|
312
|
-
delete this.__rtcStatsLastCreatedAnswer;
|
|
313
|
-
trace(method, this.__rtcStatsId,
|
|
314
|
-
descriptionCompression(this.localDescription || explicitBaseDescription, args[0]),
|
|
315
|
-
trackingId);
|
|
316
|
-
} else {
|
|
317
|
-
// Save previous localDescription for delta.
|
|
318
|
-
implicitBaseDescription = JSON.parse(JSON.stringify(this.localDescription));
|
|
319
|
-
trace(method, this.__rtcStatsId, null, trackingId);
|
|
332
|
+
if (args[0]) {
|
|
333
|
+
let explicitBaseDescription;
|
|
334
|
+
if (args[0].type === 'offer') {
|
|
335
|
+
explicitBaseDescription = this.__rtcStatsLastCreatedOffer;
|
|
336
|
+
} else if (args[0].type === 'answer') {
|
|
337
|
+
explicitBaseDescription = this.__rtcStatsLastCreatedAnswer;
|
|
320
338
|
}
|
|
321
|
-
|
|
339
|
+
delete this.__rtcStatsLastCreatedOffer;
|
|
340
|
+
delete this.__rtcStatsLastCreatedAnswer;
|
|
322
341
|
trace(method, this.__rtcStatsId,
|
|
323
|
-
descriptionCompression(this.
|
|
342
|
+
descriptionCompression(this.localDescription || explicitBaseDescription, args[0]),
|
|
324
343
|
trackingId);
|
|
344
|
+
} else {
|
|
345
|
+
// Save previous localDescription for delta.
|
|
346
|
+
implicitBaseDescription = JSON.parse(JSON.stringify(this.localDescription));
|
|
347
|
+
trace(method, this.__rtcStatsId, null, trackingId);
|
|
325
348
|
}
|
|
326
349
|
|
|
327
350
|
return nativeMethod.apply(this, args)
|
|
328
351
|
.then(() => {
|
|
329
|
-
if (
|
|
352
|
+
if (args.length === 0) {
|
|
330
353
|
trace(method + 'OnSuccess', this.__rtcStatsId,
|
|
331
354
|
descriptionCompression(implicitBaseDescription, this.localDescription),
|
|
332
355
|
trackingId);
|
|
@@ -342,6 +365,27 @@ export function wrapRTCPeerConnection(trace, window, {getStatsInterval}) {
|
|
|
342
365
|
};
|
|
343
366
|
});
|
|
344
367
|
|
|
368
|
+
['setRemoteDescription'].forEach(method => {
|
|
369
|
+
const nativeMethod = OrigPeerConnection.prototype[method];
|
|
370
|
+
if (!nativeMethod) return;
|
|
371
|
+
OrigPeerConnection.prototype[method] = function(description, ...args) {
|
|
372
|
+
const trackingId = compressMethod(method) + '-' + (counters[method]++);
|
|
373
|
+
trace(method, this.__rtcStatsId,
|
|
374
|
+
descriptionCompression(this.remoteDescription, description),
|
|
375
|
+
trackingId);
|
|
376
|
+
|
|
377
|
+
return nativeMethod.apply(this, [description, ...args])
|
|
378
|
+
.then(() => {
|
|
379
|
+
trace(method + 'OnSuccess', this.__rtcStatsId, undefined,
|
|
380
|
+
trackingId);
|
|
381
|
+
}, (err) => {
|
|
382
|
+
trace(method + 'OnFailure', this.__rtcStatsId, err.toString(),
|
|
383
|
+
trackingId);
|
|
384
|
+
throw err;
|
|
385
|
+
});
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
|
|
345
389
|
['addIceCandidate'].forEach(method => {
|
|
346
390
|
const nativeMethod = OrigPeerConnection.prototype[method];
|
|
347
391
|
if (!nativeMethod) return;
|
package/trace-websocket.js
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
import {compressMethod} from '@rtcstats/rtcstats-shared';
|
|
2
2
|
|
|
3
3
|
const PROTOCOL_VERSION = '5.0';
|
|
4
|
+
const RELOAD_COUNT_KEY = 'rtcstatsReloadCount';
|
|
4
5
|
|
|
5
6
|
export function WebSocketTrace(config = {}) {
|
|
6
7
|
let buffer = [];
|
|
7
8
|
let connection;
|
|
8
|
-
let lastTime =
|
|
9
|
+
let lastTime = Date.now();
|
|
10
|
+
let connectionStartTime = 0;
|
|
11
|
+
const createTime = Date.now();
|
|
12
|
+
|
|
13
|
+
// This counts the number of times the trace itself has been initialized.
|
|
14
|
+
// Typically this is done once per session and counting (re)loads based
|
|
15
|
+
// on that does not require listening for onload etc.
|
|
16
|
+
let reloadCount = undefined;
|
|
17
|
+
if (window.sessionStorage && config.countReloads) {
|
|
18
|
+
reloadCount = window.sessionStorage.getItem(RELOAD_COUNT_KEY);
|
|
19
|
+
if (reloadCount === null || isNaN(reloadCount)) {
|
|
20
|
+
reloadCount = -1;
|
|
21
|
+
}
|
|
22
|
+
reloadCount = parseInt(reloadCount, 10) + 1;
|
|
23
|
+
window.sessionStorage.setItem(RELOAD_COUNT_KEY, reloadCount);
|
|
24
|
+
}
|
|
9
25
|
const trace = function(...args) {
|
|
10
26
|
const now = Date.now();
|
|
11
27
|
args.push(now - lastTime);
|
|
@@ -32,28 +48,21 @@ export function WebSocketTrace(config = {}) {
|
|
|
32
48
|
buffer.push(args);
|
|
33
49
|
}
|
|
34
50
|
};
|
|
35
|
-
trace('create', null, {
|
|
36
|
-
hardwareConcurrency: navigator.hardwareConcurrency,
|
|
37
|
-
userAgentData: navigator.userAgentData,
|
|
38
|
-
deviceMemory: navigator.deviceMemory,
|
|
39
|
-
screen: {
|
|
40
|
-
width: window.screen.availWidth,
|
|
41
|
-
height: window.screen.availHeight,
|
|
42
|
-
devicePixelRatio: window.devicePixelRatio,
|
|
43
|
-
},
|
|
44
|
-
window: {
|
|
45
|
-
width: window.innerWidth,
|
|
46
|
-
height: window.innerHeight,
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
51
|
|
|
50
52
|
trace.close = () => {
|
|
51
|
-
|
|
53
|
+
if (window.sessionStorage && config.countReloads) {
|
|
54
|
+
// A clean disconnect clears the reload count.
|
|
55
|
+
window.sessionStorage.removeItem(RELOAD_COUNT_KEY);
|
|
56
|
+
}
|
|
57
|
+
if (connection) {
|
|
58
|
+
connection.close();
|
|
59
|
+
}
|
|
52
60
|
};
|
|
53
61
|
trace.connect = (wsURL) => {
|
|
54
62
|
if (connection) {
|
|
55
63
|
connection.close();
|
|
56
64
|
}
|
|
65
|
+
connectionStartTime = Date.now();
|
|
57
66
|
connection = new WebSocket(wsURL, 'rtcstats#' + PROTOCOL_VERSION);
|
|
58
67
|
connection.addEventListener('error', (e) => {
|
|
59
68
|
// console.error('WS ERROR', e);
|
|
@@ -62,14 +71,41 @@ export function WebSocketTrace(config = {}) {
|
|
|
62
71
|
connection.addEventListener('close', (e) => {
|
|
63
72
|
if (e.code === 1008 && config.log) {
|
|
64
73
|
config.log('rtcstats websocket connection closed with error=1008. ' +
|
|
65
|
-
|
|
74
|
+
'Typically this means authorization is required and failed.');
|
|
66
75
|
}
|
|
67
76
|
// reconnect?
|
|
68
77
|
});
|
|
69
78
|
|
|
70
79
|
connection.addEventListener('open', () => {
|
|
80
|
+
// Note: open is called while the socket is still authenticating.
|
|
81
|
+
// This can lead to messages being send and dropped when the token
|
|
82
|
+
// is not valid.
|
|
83
|
+
|
|
84
|
+
// Note: this does not use trace so avoids the buffer.
|
|
85
|
+
connection.send(JSON.stringify([compressMethod('create'), null, {
|
|
86
|
+
hardwareConcurrency: navigator.hardwareConcurrency,
|
|
87
|
+
userAgentData: navigator.userAgentData,
|
|
88
|
+
deviceMemory: navigator.deviceMemory,
|
|
89
|
+
screen: {
|
|
90
|
+
width: window.screen.availWidth,
|
|
91
|
+
height: window.screen.availHeight,
|
|
92
|
+
devicePixelRatio: window.devicePixelRatio,
|
|
93
|
+
},
|
|
94
|
+
window: {
|
|
95
|
+
width: window.innerWidth,
|
|
96
|
+
height: window.innerHeight,
|
|
97
|
+
},
|
|
98
|
+
reloadCount,
|
|
99
|
+
}, createTime]));
|
|
100
|
+
const connectionTime = Date.now() - connectionStartTime;
|
|
71
101
|
setTimeout(function flush() {
|
|
72
102
|
if (!buffer.length) {
|
|
103
|
+
trace('websocket', null, {
|
|
104
|
+
connectionTime,
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (connection.readyState !== WebSocket.OPEN) {
|
|
73
109
|
return;
|
|
74
110
|
}
|
|
75
111
|
connection.send(JSON.stringify(buffer.shift()));
|