music-assistant-server

35.9 KBJS
snapstream.js
35.9 KB920 lines • javascript
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