/
/
/
1"use strict";
2function setCookie(key, value, exdays = -1) {
3 let d = new Date();
4 if (exdays < 0)
5 exdays = 10 * 365;
6 d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
7 let expires = "expires=" + d.toUTCString();
8 document.cookie = key + "=" + value + ";" + expires + ";sameSite=Strict;path=/";
9}
10function getPersistentValue(key, defaultValue = "") {
11 if (!!window.localStorage) {
12 const value = window.localStorage.getItem(key);
13 if (value !== null) {
14 return value;
15 }
16 window.localStorage.setItem(key, defaultValue);
17 return defaultValue;
18 }
19 // Fallback to cookies if localStorage is not available.
20 let name = key + "=";
21 let decodedCookie = decodeURIComponent(document.cookie);
22 let ca = decodedCookie.split(';');
23 for (let c of ca) {
24 c = c.trimLeft();
25 if (c.indexOf(name) == 0) {
26 return c.substring(name.length, c.length);
27 }
28 }
29 setCookie(key, defaultValue);
30 return defaultValue;
31}
32function getChromeVersion() {
33 const raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
34 return raw ? parseInt(raw[2]) : null;
35}
36function uuidv4() {
37 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
38 var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
39 return v.toString(16);
40 });
41}
42class Tv {
43 constructor(sec, usec) {
44 this.sec = sec;
45 this.usec = usec;
46 }
47 setMilliseconds(ms) {
48 this.sec = Math.floor(ms / 1000);
49 this.usec = Math.floor(ms * 1000) % 1000000;
50 }
51 getMilliseconds() {
52 return this.sec * 1000 + this.usec / 1000;
53 }
54 sec = 0;
55 usec = 0;
56}
57class BaseMessage {
58 constructor(_buffer) {
59 }
60 deserialize(buffer) {
61 let view = new DataView(buffer);
62 this.type = view.getUint16(0, true);
63 this.id = view.getUint16(2, true);
64 this.refersTo = view.getUint16(4, true);
65 this.received = new Tv(view.getInt32(6, true), view.getInt32(10, true));
66 this.sent = new Tv(view.getInt32(14, true), view.getInt32(18, true));
67 this.size = view.getUint32(22, true);
68 }
69 serialize() {
70 this.size = 26 + this.getSize();
71 let buffer = new ArrayBuffer(this.size);
72 let view = new DataView(buffer);
73 view.setUint16(0, this.type, true);
74 view.setUint16(2, this.id, true);
75 view.setUint16(4, this.refersTo, true);
76 view.setInt32(6, this.sent.sec, true);
77 view.setInt32(10, this.sent.usec, true);
78 view.setInt32(14, this.received.sec, true);
79 view.setInt32(18, this.received.usec, true);
80 view.setUint32(22, this.size, true);
81 return buffer;
82 }
83 getSize() {
84 return 0;
85 }
86 type = 0;
87 id = 0;
88 refersTo = 0;
89 received = new Tv(0, 0);
90 sent = new Tv(0, 0);
91 size = 0;
92}
93class CodecMessage extends BaseMessage {
94 constructor(buffer) {
95 super(buffer);
96 this.payload = new ArrayBuffer(0);
97 if (buffer) {
98 this.deserialize(buffer);
99 }
100 this.type = 1;
101 }
102 deserialize(buffer) {
103 super.deserialize(buffer);
104 let view = new DataView(buffer);
105 let codecSize = view.getInt32(26, true);
106 let decoder = new TextDecoder("utf-8");
107 this.codec = decoder.decode(buffer.slice(30, 30 + codecSize));
108 let payloadSize = view.getInt32(30 + codecSize, true);
109 console.log("payload size: " + payloadSize);
110 this.payload = buffer.slice(34 + codecSize, 34 + codecSize + payloadSize);
111 console.log("payload: " + this.payload);
112 }
113 codec = "";
114 payload;
115}
116class TimeMessage extends BaseMessage {
117 constructor(buffer) {
118 super(buffer);
119 if (buffer) {
120 this.deserialize(buffer);
121 }
122 this.type = 4;
123 }
124 deserialize(buffer) {
125 super.deserialize(buffer);
126 let view = new DataView(buffer);
127 this.latency = new Tv(view.getInt32(26, true), view.getInt32(30, true));
128 }
129 serialize() {
130 let buffer = super.serialize();
131 let view = new DataView(buffer);
132 view.setInt32(26, this.latency.sec, true);
133 view.setInt32(30, this.latency.usec, true);
134 return buffer;
135 }
136 getSize() {
137 return 8;
138 }
139 latency = new Tv(0, 0);
140}
141class JsonMessage extends BaseMessage {
142 constructor(buffer) {
143 super(buffer);
144 if (buffer) {
145 this.deserialize(buffer);
146 }
147 }
148 deserialize(buffer) {
149 super.deserialize(buffer);
150 let view = new DataView(buffer);
151 let size = view.getUint32(26, true);
152 let decoder = new TextDecoder();
153 this.json = JSON.parse(decoder.decode(buffer.slice(30, 30 + size)));
154 }
155 serialize() {
156 let buffer = super.serialize();
157 let view = new DataView(buffer);
158 let jsonStr = JSON.stringify(this.json);
159 view.setUint32(26, jsonStr.length, true);
160 let encoder = new TextEncoder();
161 let encoded = encoder.encode(jsonStr);
162 for (let i = 0; i < encoded.length; ++i)
163 view.setUint8(30 + i, encoded[i]);
164 return buffer;
165 }
166 getSize() {
167 let encoder = new TextEncoder();
168 let encoded = encoder.encode(JSON.stringify(this.json));
169 return encoded.length + 4;
170 // return JSON.stringify(this.json).length;
171 }
172 json;
173}
174class HelloMessage extends JsonMessage {
175 constructor(buffer) {
176 super(buffer);
177 if (buffer) {
178 this.deserialize(buffer);
179 }
180 this.type = 5;
181 }
182 deserialize(buffer) {
183 super.deserialize(buffer);
184 this.mac = this.json["MAC"];
185 this.hostname = this.json["HostName"];
186 this.version = this.json["Version"];
187 this.clientName = this.json["ClientName"];
188 this.os = this.json["OS"];
189 this.arch = this.json["Arch"];
190 this.instance = this.json["Instance"];
191 this.uniqueId = this.json["ID"];
192 this.snapStreamProtocolVersion = this.json["SnapStreamProtocolVersion"];
193 }
194 serialize() {
195 this.json = { "MAC": this.mac, "HostName": this.hostname, "Version": this.version, "ClientName": this.clientName, "OS": this.os, "Arch": this.arch, "Instance": this.instance, "ID": this.uniqueId, "SnapStreamProtocolVersion": this.snapStreamProtocolVersion };
196 return super.serialize();
197 }
198 mac = "";
199 hostname = "";
200 version = "0.0.0";
201 clientName = "Snapweb";
202 os = "";
203 arch = "web";
204 instance = 1;
205 uniqueId = "";
206 snapStreamProtocolVersion = 2;
207}
208class ServerSettingsMessage extends JsonMessage {
209 constructor(buffer) {
210 super(buffer);
211 if (buffer) {
212 this.deserialize(buffer);
213 }
214 this.type = 3;
215 }
216 deserialize(buffer) {
217 super.deserialize(buffer);
218 this.bufferMs = this.json["bufferMs"];
219 this.latency = this.json["latency"];
220 this.volumePercent = this.json["volume"];
221 this.muted = this.json["muted"];
222 }
223 serialize() {
224 this.json = { "bufferMs": this.bufferMs, "latency": this.latency, "volume": this.volumePercent, "muted": this.muted };
225 return super.serialize();
226 }
227 bufferMs = 0;
228 latency = 0;
229 volumePercent = 0;
230 muted = false;
231}
232class PcmChunkMessage extends BaseMessage {
233 constructor(buffer, sampleFormat) {
234 super(buffer);
235 this.deserialize(buffer);
236 this.sampleFormat = sampleFormat;
237 this.type = 2;
238 }
239 deserialize(buffer) {
240 super.deserialize(buffer);
241 let view = new DataView(buffer);
242 this.timestamp = new Tv(view.getInt32(26, true), view.getInt32(30, true));
243 // this.payloadSize = view.getUint32(34, true);
244 this.payload = buffer.slice(38); //, this.payloadSize + 38));// , this.payloadSize);
245 // console.log("ts: " + this.timestamp.sec + " " + this.timestamp.usec + ", payload: " + this.payloadSize + ", len: " + this.payload.byteLength);
246 }
247 readFrames(frames) {
248 let frameCnt = frames;
249 let frameSize = this.sampleFormat.frameSize();
250 if (this.idx + frames > this.payloadSize() / frameSize)
251 frameCnt = (this.payloadSize() / frameSize) - this.idx;
252 let begin = this.idx * frameSize;
253 this.idx += frameCnt;
254 let end = begin + frameCnt * frameSize;
255 // console.log("readFrames: " + frames + ", result: " + frameCnt + ", begin: " + begin + ", end: " + end + ", payload: " + this.payload.byteLength);
256 return this.payload.slice(begin, end);
257 }
258 getFrameCount() {
259 return (this.payloadSize() / this.sampleFormat.frameSize());
260 }
261 isEndOfChunk() {
262 return this.idx >= this.getFrameCount();
263 }
264 startMs() {
265 return this.timestamp.getMilliseconds() + 1000 * (this.idx / this.sampleFormat.rate);
266 }
267 duration() {
268 return 1000 * ((this.getFrameCount() - this.idx) / this.sampleFormat.rate);
269 }
270 payloadSize() {
271 return this.payload.byteLength;
272 }
273 clearPayload() {
274 this.payload = new ArrayBuffer(0);
275 }
276 addPayload(buffer) {
277 let payload = new ArrayBuffer(this.payload.byteLength + buffer.byteLength);
278 let view = new DataView(payload);
279 let viewOld = new DataView(this.payload);
280 let viewNew = new DataView(buffer);
281 for (let i = 0; i < viewOld.byteLength; ++i) {
282 view.setInt8(i, viewOld.getInt8(i));
283 }
284 for (let i = 0; i < viewNew.byteLength; ++i) {
285 view.setInt8(i + viewOld.byteLength, viewNew.getInt8(i));
286 }
287 this.payload = payload;
288 }
289 timestamp = new Tv(0, 0);
290 // payloadSize: number = 0;
291 payload = new ArrayBuffer(0);
292 idx = 0;
293 sampleFormat;
294}
295class AudioStream {
296 timeProvider;
297 sampleFormat;
298 bufferMs;
299 constructor(timeProvider, sampleFormat, bufferMs) {
300 this.timeProvider = timeProvider;
301 this.sampleFormat = sampleFormat;
302 this.bufferMs = bufferMs;
303 }
304 chunks = new Array();
305 setVolume(percent, muted) {
306 // let base = 10;
307 this.volume = percent / 100; // (Math.pow(base, percent / 100) - 1) / (base - 1);
308 console.log("setVolume: " + percent + " => " + this.volume + ", muted: " + this.muted);
309 this.muted = muted;
310 }
311 addChunk(chunk) {
312 this.chunks.push(chunk);
313 // let oldest = this.timeProvider.serverNow() - this.chunks[0].timestamp.getMilliseconds();
314 // let newest = this.timeProvider.serverNow() - this.chunks[this.chunks.length - 1].timestamp.getMilliseconds();
315 // console.debug("chunks: " + this.chunks.length + ", oldest: " + oldest.toFixed(2) + ", newest: " + newest.toFixed(2));
316 while (this.chunks.length > 0) {
317 let age = this.timeProvider.serverNow() - this.chunks[0].timestamp.getMilliseconds();
318 // todo: consider buffer ms
319 if (age > 5000 + this.bufferMs) {
320 this.chunks.shift();
321 console.log("Dropping old chunk: " + age.toFixed(2) + ", left: " + this.chunks.length);
322 }
323 else
324 break;
325 }
326 }
327 getNextBuffer(buffer, playTimeMs) {
328 if (!this.chunk) {
329 this.chunk = this.chunks.shift();
330 }
331 // let age = this.timeProvider.serverTime(this.playTime * 1000) - startMs;
332 let frames = buffer.length;
333 // console.debug("getNextBuffer: " + frames + ", play time: " + playTimeMs.toFixed(2));
334 let left = new Float32Array(frames);
335 let right = new Float32Array(frames);
336 let read = 0;
337 let pos = 0;
338 // let volume = this.muted ? 0 : this.volume;
339 let serverPlayTimeMs = this.timeProvider.serverTime(playTimeMs);
340 if (this.chunk) {
341 let age = serverPlayTimeMs - this.chunk.startMs(); // - 500;
342 let reqChunkDuration = frames / this.sampleFormat.msRate();
343 let secs = Math.floor(Date.now() / 1000);
344 if (this.lastLog != secs) {
345 this.lastLog = secs;
346 console.log("age: " + age.toFixed(2) + ", req: " + reqChunkDuration);
347 }
348 if (age < -reqChunkDuration) {
349 console.log("age: " + age.toFixed(2) + " < req: " + reqChunkDuration * -1 + ", chunk.startMs: " + this.chunk.startMs().toFixed(2) + ", timestamp: " + this.chunk.timestamp.getMilliseconds().toFixed(2));
350 console.log("Chunk too young, returning silence");
351 }
352 else {
353 if (Math.abs(age) > 5) {
354 // We are 5ms apart, do a hard sync, i.e. don't play faster/slower,
355 // but seek to the desired position instead
356 while (this.chunk && age > this.chunk.duration()) {
357 console.log("Chunk too old, dropping (age: " + age.toFixed(2) + " > " + this.chunk.duration().toFixed(2) + ")");
358 this.chunk = this.chunks.shift();
359 if (!this.chunk)
360 break;
361 age = serverPlayTimeMs - this.chunk.startMs();
362 }
363 if (this.chunk) {
364 if (age > 0) {
365 console.log("Fast forwarding " + age.toFixed(2) + "ms");
366 this.chunk.readFrames(Math.floor(age * this.chunk.sampleFormat.msRate()));
367 }
368 else if (age < 0) {
369 console.log("Playing silence " + -age.toFixed(2) + "ms");
370 let silentFrames = Math.floor(-age * this.chunk.sampleFormat.msRate());
371 left.fill(0, 0, silentFrames);
372 right.fill(0, 0, silentFrames);
373 read = silentFrames;
374 pos = silentFrames;
375 }
376 age = 0;
377 }
378 }
379 // else if (age > 0.1) {
380 // let rate = age * 0.0005;
381 // rate = 1.0 - Math.min(rate, 0.0005);
382 // console.debug("Age > 0, rate: " + rate);
383 // // we are late (age > 0), this means we are not playing fast enough
384 // // => the real sample rate seems to be lower, we have to drop some frames
385 // this.setRealSampleRate(this.sampleFormat.rate * rate); // 0.9999);
386 // }
387 // else if (age < -0.1) {
388 // let rate = -age * 0.0005;
389 // rate = 1.0 + Math.min(rate, 0.0005);
390 // console.debug("Age < 0, rate: " + rate);
391 // // we are early (age > 0), this means we are playing too fast
392 // // => the real sample rate seems to be higher, we have to insert some frames
393 // this.setRealSampleRate(this.sampleFormat.rate * rate); // 0.9999);
394 // }
395 // else {
396 // this.setRealSampleRate(this.sampleFormat.rate);
397 // }
398 let addFrames = 0;
399 let everyN = 0;
400 if (age > 0.1) {
401 addFrames = Math.ceil(age); // / 5);
402 }
403 else if (age < -0.1) {
404 addFrames = Math.floor(age); // / 5);
405 }
406 // addFrames = -2;
407 let readFrames = frames + addFrames - read;
408 if (addFrames != 0)
409 everyN = Math.ceil((frames + addFrames - read) / (Math.abs(addFrames) + 1));
410 // addFrames = 0;
411 // console.debug("frames: " + frames + ", readFrames: " + readFrames + ", addFrames: " + addFrames + ", everyN: " + everyN);
412 while ((read < readFrames) && this.chunk) {
413 let pcmChunk = this.chunk;
414 let pcmBuffer = pcmChunk.readFrames(readFrames - read);
415 let payload = new Int16Array(pcmBuffer);
416 // console.debug("readFrames: " + (frames - read) + ", read: " + pcmBuffer.byteLength + ", payload: " + payload.length);
417 // read += (pcmBuffer.byteLength / this.sampleFormat.frameSize());
418 for (let i = 0; i < payload.length; i += 2) {
419 read++;
420 left[pos] = (payload[i] / 32768); // * volume;
421 right[pos] = (payload[i + 1] / 32768); // * volume;
422 if ((everyN != 0) && (read % everyN == 0)) {
423 if (addFrames > 0) {
424 pos--;
425 }
426 else {
427 left[pos + 1] = left[pos];
428 right[pos + 1] = right[pos];
429 pos++;
430 // console.log("Add: " + pos);
431 }
432 }
433 pos++;
434 }
435 if (pcmChunk.isEndOfChunk()) {
436 this.chunk = this.chunks.shift();
437 }
438 }
439 if (addFrames != 0)
440 console.debug("Pos: " + pos + ", frames: " + frames + ", add: " + addFrames + ", everyN: " + everyN);
441 if (read == readFrames)
442 read = frames;
443 }
444 }
445 if (read < frames) {
446 console.log("Failed to get chunk, read: " + read + "/" + frames + ", chunks left: " + this.chunks.length);
447 left.fill(0, pos);
448 right.fill(0, pos);
449 }
450 // copyToChannel is not supported by Safari
451 buffer.getChannelData(0).set(left);
452 buffer.getChannelData(1).set(right);
453 }
454 // setRealSampleRate(sampleRate: number) {
455 // if (sampleRate == this.sampleFormat.rate) {
456 // this.correctAfterXFrames = 0;
457 // }
458 // else {
459 // this.correctAfterXFrames = Math.ceil((this.sampleFormat.rate / sampleRate) / (this.sampleFormat.rate / sampleRate - 1.));
460 // console.debug("setRealSampleRate: " + sampleRate + ", correct after X: " + this.correctAfterXFrames);
461 // }
462 // }
463 chunk = undefined;
464 volume = 1;
465 muted = false;
466 lastLog = 0;
467}
468class TimeProvider {
469 constructor(ctx = undefined) {
470 if (ctx) {
471 this.setAudioContext(ctx);
472 }
473 }
474 setAudioContext(ctx) {
475 this.ctx = ctx;
476 this.reset();
477 }
478 reset() {
479 this.diffBuffer.length = 0;
480 this.diff = 0;
481 }
482 setDiff(c2s, s2c) {
483 if (this.now() == 0) {
484 this.reset();
485 }
486 else {
487 if (this.diffBuffer.push((c2s - s2c) / 2) > 100)
488 this.diffBuffer.shift();
489 let sorted = [...this.diffBuffer];
490 sorted.sort();
491 this.diff = sorted[Math.floor(sorted.length / 2)];
492 }
493 // console.debug("c2s: " + c2s.toFixed(2) + ", s2c: " + s2c.toFixed(2) + ", diff: " + this.diff.toFixed(2) + ", now: " + this.now().toFixed(2) + ", server.now: " + this.serverNow().toFixed(2) + ", win.now: " + window.performance.now().toFixed(2));
494 // console.log("now: " + this.now() + "\t" + this.now() + "\t" + this.now());
495 }
496 now() {
497 if (!this.ctx) {
498 return window.performance.now();
499 }
500 else {
501 // Use the more accurate getOutputTimestamp if available, fallback to ctx.currentTime otherwise.
502 const contextTime = !!this.ctx.getOutputTimestamp ? this.ctx.getOutputTimestamp().contextTime : undefined;
503 return (contextTime !== undefined ? contextTime : this.ctx.currentTime) * 1000;
504 }
505 }
506 nowSec() {
507 return this.now() / 1000;
508 }
509 serverNow() {
510 return this.serverTime(this.now());
511 }
512 serverTime(localTimeMs) {
513 return localTimeMs + this.diff;
514 }
515 diffBuffer = new Array();
516 diff = 0;
517 ctx;
518}
519class SampleFormat {
520 rate = 48000;
521 channels = 2;
522 bits = 16;
523 msRate() {
524 return this.rate / 1000;
525 }
526 toString() {
527 return this.rate + ":" + this.bits + ":" + this.channels;
528 }
529 sampleSize() {
530 if (this.bits == 24) {
531 return 4;
532 }
533 return this.bits / 8;
534 }
535 frameSize() {
536 return this.channels * this.sampleSize();
537 }
538 durationMs(bytes) {
539 return (bytes / this.frameSize()) * this.msRate();
540 }
541}
542class Decoder {
543 setHeader(_buffer) {
544 return new SampleFormat();
545 }
546 decode(_chunk) {
547 return null;
548 }
549}
550class OpusDecoder extends Decoder {
551 setHeader(buffer) {
552 let view = new DataView(buffer);
553 let ID_OPUS = 0x4F505553;
554 if (buffer.byteLength < 12) {
555 console.error("Opus header too small: " + buffer.byteLength);
556 return null;
557 }
558 else if (view.getUint32(0, true) != ID_OPUS) {
559 console.error("Opus header too small: " + buffer.byteLength);
560 return null;
561 }
562 let format = new SampleFormat();
563 format.rate = view.getUint32(4, true);
564 format.bits = view.getUint16(8, true);
565 format.channels = view.getUint16(10, true);
566 console.log("Opus samplerate: " + format.toString());
567 return format;
568 }
569 decode(_chunk) {
570 return null;
571 }
572}
573class FlacDecoder extends Decoder {
574 constructor() {
575 super();
576 this.decoder = Flac.create_libflac_decoder(true);
577 if (this.decoder) {
578 let init_status = Flac.init_decoder_stream(this.decoder, this.read_callback_fn.bind(this), this.write_callback_fn.bind(this), this.error_callback_fn.bind(this), this.metadata_callback_fn.bind(this), false);
579 console.log("Flac init: " + init_status);
580 Flac.setOptions(this.decoder, { analyseSubframes: true, analyseResiduals: true });
581 }
582 this.sampleFormat = new SampleFormat();
583 this.flacChunk = new ArrayBuffer(0);
584 // this.pcmChunk = new PcmChunkMessage();
585 // Flac.setOptions(this.decoder, {analyseSubframes: analyse_frames, analyseResiduals: analyse_residuals});
586 // flac_ok &= init_status == 0;
587 // console.log("flac init : " + flac_ok);//DEBUG
588 }
589 decode(chunk) {
590 // console.log("Flac decode: " + chunk.payload.byteLength);
591 this.flacChunk = chunk.payload.slice(0);
592 this.pcmChunk = chunk;
593 this.pcmChunk.clearPayload();
594 this.cacheInfo = { cachedBlocks: 0, isCachedChunk: true };
595 // console.log("Flac len: " + this.flacChunk.byteLength);
596 while (this.flacChunk.byteLength && Flac.FLAC__stream_decoder_process_single(this.decoder)) {
597 Flac.FLAC__stream_decoder_get_state(this.decoder);
598 // let state = Flac.FLAC__stream_decoder_get_state(this.decoder);
599 // console.log("State: " + state);
600 }
601 // console.log("Pcm payload: " + this.pcmChunk!.payloadSize());
602 if (this.cacheInfo.cachedBlocks > 0) {
603 let diffMs = this.cacheInfo.cachedBlocks / this.sampleFormat.msRate();
604 // console.log("Cached: " + this.cacheInfo.cachedBlocks + ", " + diffMs + "ms");
605 this.pcmChunk.timestamp.setMilliseconds(this.pcmChunk.timestamp.getMilliseconds() - diffMs);
606 }
607 return this.pcmChunk;
608 }
609 read_callback_fn(bufferSize) {
610 // console.log(' decode read callback, buffer bytes max=', bufferSize);
611 if (this.header) {
612 console.log(" header: " + this.header.byteLength);
613 let data = new Uint8Array(this.header);
614 this.header = null;
615 return { buffer: data, readDataLength: data.byteLength, error: false };
616 }
617 else if (this.flacChunk) {
618 // console.log(" flacChunk: " + this.flacChunk.byteLength);
619 // a fresh read => next call to write will not be from cached data
620 this.cacheInfo.isCachedChunk = false;
621 let data = new Uint8Array(this.flacChunk.slice(0, Math.min(bufferSize, this.flacChunk.byteLength)));
622 this.flacChunk = this.flacChunk.slice(data.byteLength);
623 return { buffer: data, readDataLength: data.byteLength, error: false };
624 }
625 return { buffer: new Uint8Array(0), readDataLength: 0, error: false };
626 }
627 write_callback_fn(data, frameInfo) {
628 // console.log(" write frame metadata: " + frameInfo + ", len: " + data.length);
629 if (this.cacheInfo.isCachedChunk) {
630 // there was no call to read, so it's some cached data
631 this.cacheInfo.cachedBlocks += frameInfo.blocksize;
632 }
633 let payload = new ArrayBuffer((frameInfo.bitsPerSample / 8) * frameInfo.channels * frameInfo.blocksize);
634 let view = new DataView(payload);
635 for (let channel = 0; channel < frameInfo.channels; ++channel) {
636 let channelData = new DataView(data[channel].buffer, 0, data[channel].buffer.byteLength);
637 // console.log("channelData: " + channelData.byteLength + ", blocksize: " + frameInfo.blocksize);
638 for (let i = 0; i < frameInfo.blocksize; ++i) {
639 view.setInt16(2 * (frameInfo.channels * i + channel), channelData.getInt16(2 * i, true), true);
640 }
641 }
642 this.pcmChunk.addPayload(payload);
643 // console.log("write: " + payload.byteLength + ", len: " + this.pcmChunk!.payloadSize());
644 }
645 /** @memberOf decode */
646 metadata_callback_fn(data) {
647 console.info('meta data: ', data);
648 // let view = new DataView(data);
649 this.sampleFormat.rate = data.sampleRate;
650 this.sampleFormat.channels = data.channels;
651 this.sampleFormat.bits = data.bitsPerSample;
652 console.log("metadata_callback_fn, sampleformat: " + this.sampleFormat.toString());
653 }
654 /** @memberOf decode */
655 error_callback_fn(err, errMsg) {
656 console.error('decode error callback', err, errMsg);
657 }
658 setHeader(buffer) {
659 this.header = buffer.slice(0);
660 Flac.FLAC__stream_decoder_process_until_end_of_metadata(this.decoder);
661 return this.sampleFormat;
662 }
663 sampleFormat;
664 decoder;
665 header = null;
666 flacChunk;
667 pcmChunk;
668 cacheInfo = { isCachedChunk: false, cachedBlocks: 0 };
669}
670class PlayBuffer {
671 constructor(buffer, playTime, source, destination) {
672 this.buffer = buffer;
673 this.playTime = playTime;
674 this.source = source;
675 this.source.buffer = this.buffer;
676 this.source.connect(destination);
677 this.onended = (_playBuffer) => { };
678 }
679 onended;
680 start() {
681 this.source.onended = () => {
682 this.onended(this);
683 };
684 this.source.start(this.playTime);
685 }
686 buffer;
687 playTime;
688 source;
689 num = 0;
690}
691class PcmDecoder extends Decoder {
692 setHeader(buffer) {
693 let sampleFormat = new SampleFormat();
694 let view = new DataView(buffer);
695 sampleFormat.channels = view.getUint16(22, true);
696 sampleFormat.rate = view.getUint32(24, true);
697 sampleFormat.bits = view.getUint16(34, true);
698 return sampleFormat;
699 }
700 decode(chunk) {
701 return chunk;
702 }
703}
704class SnapStream {
705 constructor(baseUrl) {
706 this.baseUrl = baseUrl;
707 this.timeProvider = new TimeProvider();
708 if (this.setupAudioContext()) {
709 this.connect();
710 }
711 else {
712 alert("Sorry, but the Web Audio API is not supported by your browser");
713 }
714 }
715 setupAudioContext() {
716 let AudioContext = window.AudioContext // Default
717 || window.webkitAudioContext // Safari and old versions of Chrome
718 || false;
719 if (AudioContext) {
720 let options;
721 options = { latencyHint: "playback", sampleRate: this.sampleFormat ? this.sampleFormat.rate : undefined };
722 const chromeVersion = getChromeVersion();
723 if ((chromeVersion !== null && chromeVersion < 55) || !window.AudioContext) {
724 // Some older browsers won't decode the stream if options are provided.
725 options = undefined;
726 }
727 this.ctx = new AudioContext(options);
728 this.gainNode = this.ctx.createGain();
729 this.gainNode.connect(this.ctx.destination);
730 }
731 else {
732 // Web Audio API is not supported
733 return false;
734 }
735 return true;
736 }
737 static getClientId() {
738 return getPersistentValue("uniqueId", uuidv4());
739 }
740 connect() {
741 this.streamsocket = new WebSocket(this.baseUrl + '/stream');
742 this.streamsocket.binaryType = "arraybuffer";
743 this.streamsocket.onmessage = (ev) => this.onMessage(ev);
744 this.streamsocket.onopen = () => {
745 console.log("on open");
746 let hello = new HelloMessage();
747 hello.mac = "00:00:00:00:00:00";
748 hello.arch = "web";
749 hello.os = navigator.platform;
750 hello.hostname = "Snapweb client";
751 hello.uniqueId = SnapStream.getClientId();
752 const versionElem = document.getElementsByTagName("meta").namedItem("version");
753 hello.version = versionElem ? versionElem.content : "0.0.0";
754 this.sendMessage(hello);
755 this.syncTime();
756 this.syncHandle = window.setInterval(() => this.syncTime(), 1000);
757 };
758 this.streamsocket.onerror = (ev) => { console.error('error:', ev); };
759 this.streamsocket.onclose = () => {
760 window.clearInterval(this.syncHandle);
761 console.info('connection lost, reconnecting in 1s');
762 setTimeout(() => this.connect(), 1000);
763 };
764 }
765 onMessage(msg) {
766 let view = new DataView(msg.data);
767 let type = view.getUint16(0, true);
768 if (type == 1) {
769 let codec = new CodecMessage(msg.data);
770 console.log("Codec: " + codec.codec);
771 if (codec.codec == "flac") {
772 this.decoder = new FlacDecoder();
773 }
774 else if (codec.codec == "pcm") {
775 this.decoder = new PcmDecoder();
776 }
777 else if (codec.codec == "opus") {
778 this.decoder = new OpusDecoder();
779 alert("Codec not supported: " + codec.codec);
780 }
781 else {
782 alert("Codec not supported: " + codec.codec);
783 }
784 if (this.decoder) {
785 this.sampleFormat = this.decoder.setHeader(codec.payload);
786 console.log("Sampleformat: " + this.sampleFormat.toString());
787 if ((this.sampleFormat.channels != 2) || (this.sampleFormat.bits != 16)) {
788 alert("Stream must be stereo with 16 bit depth, actual format: " + this.sampleFormat.toString());
789 }
790 else {
791 if (this.bufferDurationMs != 0) {
792 this.bufferFrameCount = Math.floor(this.bufferDurationMs * this.sampleFormat.msRate());
793 }
794 if (window.AudioContext) {
795 // we are not using webkitAudioContext, so it's safe to setup a new AudioContext with the new samplerate
796 // since this code is not triggered by direct user input, we cannt create a webkitAudioContext here
797 this.stopAudio();
798 this.setupAudioContext();
799 }
800 this.ctx.resume();
801 this.timeProvider.setAudioContext(this.ctx);
802 this.gainNode.gain.value = this.serverSettings.muted ? 0 : this.serverSettings.volumePercent / 100;
803 // this.timeProvider = new TimeProvider(this.ctx);
804 this.stream = new AudioStream(this.timeProvider, this.sampleFormat, this.bufferMs);
805 this.latency = (this.ctx.baseLatency !== undefined ? this.ctx.baseLatency : 0) + (this.ctx.outputLatency !== undefined ? this.ctx.outputLatency : 0);
806 console.log("Base latency: " + this.ctx.baseLatency + ", output latency: " + this.ctx.outputLatency + ", latency: " + this.latency);
807 this.play();
808 }
809 }
810 }
811 else if (type == 2) {
812 let pcmChunk = new PcmChunkMessage(msg.data, this.sampleFormat);
813 if (this.decoder) {
814 let decoded = this.decoder.decode(pcmChunk);
815 if (decoded) {
816 this.stream.addChunk(decoded);
817 }
818 }
819 }
820 else if (type == 3) {
821 this.serverSettings = new ServerSettingsMessage(msg.data);
822 this.gainNode.gain.value = this.serverSettings.muted ? 0 : this.serverSettings.volumePercent / 100;
823 this.bufferMs = this.serverSettings.bufferMs - this.serverSettings.latency;
824 console.log("ServerSettings bufferMs: " + this.serverSettings.bufferMs + ", latency: " + this.serverSettings.latency + ", volume: " + this.serverSettings.volumePercent + ", muted: " + this.serverSettings.muted);
825 }
826 else if (type == 4) {
827 if (this.timeProvider) {
828 let time = new TimeMessage(msg.data);
829 this.timeProvider.setDiff(time.latency.getMilliseconds(), this.timeProvider.now() - time.sent.getMilliseconds());
830 }
831 // console.log("Time sec: " + time.latency.sec + ", usec: " + time.latency.usec + ", diff: " + this.timeProvider.diff);
832 }
833 else {
834 console.info("Message not handled, type: " + type);
835 }
836 }
837 sendMessage(msg) {
838 msg.sent = new Tv(0, 0);
839 msg.sent.setMilliseconds(this.timeProvider.now());
840 msg.id = ++this.msgId;
841 if (this.streamsocket.readyState == this.streamsocket.OPEN) {
842 this.streamsocket.send(msg.serialize());
843 }
844 }
845 syncTime() {
846 let t = new TimeMessage();
847 t.latency.setMilliseconds(this.timeProvider.now());
848 this.sendMessage(t);
849 // console.log("prepareSource median: " + Math.round(this.median * 10) / 10);
850 }
851 stopAudio() {
852 // if (this.ctx) {
853 // this.ctx.close();
854 // }
855 this.ctx.suspend();
856 while (this.audioBuffers.length > 0) {
857 let buffer = this.audioBuffers.pop();
858 buffer.onended = () => { };
859 buffer.source.stop();
860 }
861 while (this.freeBuffers.length > 0) {
862 this.freeBuffers.pop();
863 }
864 }
865 stop() {
866 window.clearInterval(this.syncHandle);
867 this.stopAudio();
868 if ([WebSocket.OPEN, WebSocket.CONNECTING].includes(this.streamsocket.readyState)) {
869 this.streamsocket.onclose = () => { };
870 this.streamsocket.close();
871 }
872 }
873 play() {
874 this.playTime = this.timeProvider.nowSec() + 0.1;
875 for (let i = 1; i <= this.audioBufferCount; ++i) {
876 this.playNext();
877 }
878 }
879 playNext() {
880 let buffer = this.freeBuffers.pop() || this.ctx.createBuffer(this.sampleFormat.channels, this.bufferFrameCount, this.sampleFormat.rate);
881 let playTimeMs = (this.playTime + this.latency) * 1000 - this.bufferMs;
882 this.stream.getNextBuffer(buffer, playTimeMs);
883 let source = this.ctx.createBufferSource();
884 let playBuffer = new PlayBuffer(buffer, this.playTime, source, this.gainNode);
885 this.audioBuffers.push(playBuffer);
886 playBuffer.num = ++this.bufferNum;
887 playBuffer.onended = (buffer) => {
888 // let diff = this.timeProvider.nowSec() - buffer.playTime;
889 this.freeBuffers.push(this.audioBuffers.splice(this.audioBuffers.indexOf(buffer), 1)[0].buffer);
890 // console.debug("PlayBuffer " + playBuffer.num + " ended after: " + (diff * 1000) + ", in flight: " + this.audioBuffers.length);
891 this.playNext();
892 };
893 playBuffer.start();
894 this.playTime += this.bufferFrameCount / this.sampleFormat.rate;
895 }
896 baseUrl;
897 streamsocket;
898 playTime = 0;
899 msgId = 0;
900 bufferDurationMs = 80; // 0;
901 bufferFrameCount = 3844; // 9600; // 2400;//8192;
902 syncHandle = -1;
903 // ageBuffer: Array<number>;
904 audioBuffers = new Array();
905 freeBuffers = new Array();
906 timeProvider;
907 stream;
908 ctx; // | undefined;
909 gainNode;
910 serverSettings;
911 decoder;
912 sampleFormat;
913 // median: number = 0;
914 audioBufferCount = 3;
915 bufferMs = 1000;
916 bufferNum = 0;
917 latency = 0;
918}
919//# sourceMappingURL=snapstream.js.map
920