text: "use strict";
  function setCookie(key, value, exdays = -1) {
      let d = new Date();
      if (exdays < 0)
          exdays = 10 * 365;
      d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
      let expires = "expires=" + d.toUTCString();
      document.cookie = key + "=" + value + ";" + expires + ";sameSite=Strict;path=/";
  }
  function getPersistentValue(key, defaultValue = "") {
      if (!!window.localStorage) {
          const value = window.localStorage.getItem(key);
          if (value !== null) {
              return value;
          }
          window.localStorage.setItem(key, defaultValue);
          return defaultValue;
      }
      // Fallback to cookies if localStorage is not available.
      let name = key + "=";
      let decodedCookie = decodeURIComponent(document.cookie);
      let ca = decodedCookie.split(';');
      for (let c of ca) {
          c = c.trimLeft();
          if (c.indexOf(name) == 0) {
              return c.substring(name.length, c.length);
          }
      }
      setCookie(key, defaultValue);
      return defaultValue;
  }
  function getChromeVersion() {
      const raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
      return raw ? parseInt(raw[2]) : null;
  }
  function uuidv4() {
      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
          var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
          return v.toString(16);
      });
  }
  class Tv {
      constructor(sec, usec) {
          this.sec = sec;
          this.usec = usec;
      }
      setMilliseconds(ms) {
          this.sec = Math.floor(ms / 1000);
          this.usec = Math.floor(ms * 1000) % 1000000;
      }
      getMilliseconds() {
          return this.sec * 1000 + this.usec / 1000;
      }
      sec = 0;
      usec = 0;
  }
  class BaseMessage {
      constructor(_buffer) {
      }
      deserialize(buffer) {
          let view = new DataView(buffer);
          this.type = view.getUint16(0, true);
          this.id = view.getUint16(2, true);
          this.refersTo = view.getUint16(4, true);
          this.received = new Tv(view.getInt32(6, true), view.getInt32(10, true));
          this.sent = new Tv(view.getInt32(14, true), view.getInt32(18, true));
          this.size = view.getUint32(22, true);
      }
      serialize() {
          this.size = 26 + this.getSize();
          let buffer = new ArrayBuffer(this.size);
          let view = new DataView(buffer);
          view.setUint16(0, this.type, true);
          view.setUint16(2, this.id, true);
          view.setUint16(4, this.refersTo, true);
          view.setInt32(6, this.sent.sec, true);
          view.setInt32(10, this.sent.usec, true);
          view.setInt32(14, this.received.sec, true);
          view.setInt32(18, this.received.usec, true);
          view.setUint32(22, this.size, true);
          return buffer;
      }
      getSize() {
          return 0;
      }
      type = 0;
      id = 0;
      refersTo = 0;
      received = new Tv(0, 0);
      sent = new Tv(0, 0);
      size = 0;
  }
  class CodecMessage extends BaseMessage {
      constructor(buffer) {
          super(buffer);
          this.payload = new ArrayBuffer(0);
          if (buffer) {
              this.deserialize(buffer);
          }
          this.type = 1;
      }
      deserialize(buffer) {
          super.deserialize(buffer);
          let view = new DataView(buffer);
          let codecSize = view.getInt32(26, true);
          let decoder = new TextDecoder("utf-8");
          this.codec = decoder.decode(buffer.slice(30, 30 + codecSize));
          let payloadSize = view.getInt32(30 + codecSize, true);
          console.log("payload size: " + payloadSize);
          this.payload = buffer.slice(34 + codecSize, 34 + codecSize + payloadSize);
          console.log("payload: " + this.payload);
      }
      codec = "";
      payload;
  }
  class TimeMessage extends BaseMessage {
      constructor(buffer) {
          super(buffer);
          if (buffer) {
              this.deserialize(buffer);
          }
          this.type = 4;
      }
      deserialize(buffer) {
          super.deserialize(buffer);
          let view = new DataView(buffer);
          this.latency = new Tv(view.getInt32(26, true), view.getInt32(30, true));
      }
      serialize() {
          let buffer = super.serialize();
          let view = new DataView(buffer);
          view.setInt32(26, this.latency.sec, true);
          view.setInt32(30, this.latency.usec, true);
          return buffer;
      }
      getSize() {
          return 8;
      }
      latency = new Tv(0, 0);
  }
  class JsonMessage extends BaseMessage {
      constructor(buffer) {
          super(buffer);
          if (buffer) {
              this.deserialize(buffer);
          }
      }
      deserialize(buffer) {
          super.deserialize(buffer);
          let view = new DataView(buffer);
          let size = view.getUint32(26, true);
          let decoder = new TextDecoder();
          this.json = JSON.parse(decoder.decode(buffer.slice(30, 30 + size)));
      }
      serialize() {
          let buffer = super.serialize();
          let view = new DataView(buffer);
          let jsonStr = JSON.stringify(this.json);
          view.setUint32(26, jsonStr.length, true);
          let encoder = new TextEncoder();
          let encoded = encoder.encode(jsonStr);
          for (let i = 0; i < encoded.length; ++i)
              view.setUint8(30 + i, encoded[i]);
          return buffer;
      }
      getSize() {
          let encoder = new TextEncoder();
          let encoded = encoder.encode(JSON.stringify(this.json));
          return encoded.length + 4;
          // return JSON.stringify(this.json).length;
      }
      json;
  }
  class HelloMessage extends JsonMessage {
      constructor(buffer) {
          super(buffer);
          if (buffer) {
              this.deserialize(buffer);
          }
          this.type = 5;
      }
      deserialize(buffer) {
          super.deserialize(buffer);
          this.mac = this.json["MAC"];
          this.hostname = this.json["HostName"];
          this.version = this.json["Version"];
          this.clientName = this.json["ClientName"];
          this.os = this.json["OS"];
          this.arch = this.json["Arch"];
          this.instance = this.json["Instance"];
          this.uniqueId = this.json["ID"];
          this.snapStreamProtocolVersion = this.json["SnapStreamProtocolVersion"];
      }
      serialize() {
          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 };
          return super.serialize();
      }
      mac = "";
      hostname = "";
      version = "0.1.0";
      clientName = "Snapweb";
      os = "";
      arch = "web";
      instance = 1;
      uniqueId = "";
      snapStreamProtocolVersion = 2;
  }
  class ServerSettingsMessage extends JsonMessage {
      constructor(buffer) {
          super(buffer);
          if (buffer) {
              this.deserialize(buffer);
          }
          this.type = 3;
      }
      deserialize(buffer) {
          super.deserialize(buffer);
          this.bufferMs = this.json["bufferMs"];
          this.latency = this.json["latency"];
          this.volumePercent = this.json["volume"];
          this.muted = this.json["muted"];
      }
      serialize() {
          this.json = { "bufferMs": this.bufferMs, "latency": this.latency, "volume": this.volumePercent, "muted": this.muted };
          return super.serialize();
      }
      bufferMs = 0;
      latency = 0;
      volumePercent = 0;
      muted = false;
  }
  class PcmChunkMessage extends BaseMessage {
      constructor(buffer, sampleFormat) {
          super(buffer);
          this.deserialize(buffer);
          this.sampleFormat = sampleFormat;
          this.type = 2;
      }
      deserialize(buffer) {
          super.deserialize(buffer);
          let view = new DataView(buffer);
          this.timestamp = new Tv(view.getInt32(26, true), view.getInt32(30, true));
          // this.payloadSize = view.getUint32(34, true);
          this.payload = buffer.slice(38); //, this.payloadSize + 38));// , this.payloadSize);
          // console.log("ts: " + this.timestamp.sec + " " + this.timestamp.usec + ", payload: " + this.payloadSize + ", len: " + this.payload.byteLength);
      }
      readFrames(frames) {
          let frameCnt = frames;
          let frameSize = this.sampleFormat.frameSize();
          if (this.idx + frames > this.payloadSize() / frameSize)
              frameCnt = (this.payloadSize() / frameSize) - this.idx;
          let begin = this.idx * frameSize;
          this.idx += frameCnt;
          let end = begin + frameCnt * frameSize;
          // console.log("readFrames: " + frames + ", result: " + frameCnt + ", begin: " + begin + ", end: " + end + ", payload: " + this.payload.byteLength);
          return this.payload.slice(begin, end);
      }
      getFrameCount() {
          return (this.payloadSize() / this.sampleFormat.frameSize());
      }
      isEndOfChunk() {
          return this.idx >= this.getFrameCount();
      }
      startMs() {
          return this.timestamp.getMilliseconds() + 1000 * (this.idx / this.sampleFormat.rate);
      }
      duration() {
          return 1000 * ((this.getFrameCount() - this.idx) / this.sampleFormat.rate);
      }
      payloadSize() {
          return this.payload.byteLength;
      }
      clearPayload() {
          this.payload = new ArrayBuffer(0);
      }
      addPayload(buffer) {
          let payload = new ArrayBuffer(this.payload.byteLength + buffer.byteLength);
          let view = new DataView(payload);
          let viewOld = new DataView(this.payload);
          let viewNew = new DataView(buffer);
          for (let i = 0; i < viewOld.byteLength; ++i) {
              view.setInt8(i, viewOld.getInt8(i));
          }
          for (let i = 0; i < viewNew.byteLength; ++i) {
              view.setInt8(i + viewOld.byteLength, viewNew.getInt8(i));
          }
          this.payload = payload;
      }
      timestamp = new Tv(0, 0);
      // payloadSize: number = 0;
      payload = new ArrayBuffer(0);
      idx = 0;
      sampleFormat;
  }
  class AudioStream {
      timeProvider;
      sampleFormat;
      bufferMs;
      constructor(timeProvider, sampleFormat, bufferMs) {
          this.timeProvider = timeProvider;
          this.sampleFormat = sampleFormat;
          this.bufferMs = bufferMs;
      }
      chunks = new Array();
      setVolume(percent, muted) {
          // let base = 10;
          this.volume = percent / 100; // (Math.pow(base, percent / 100) - 1) / (base - 1);
          console.log("setVolume: " + percent + " => " + this.volume + ", muted: " + this.muted);
          this.muted = muted;
      }
      addChunk(chunk) {
          this.chunks.push(chunk);
          // let oldest = this.timeProvider.serverNow() - this.chunks[0].timestamp.getMilliseconds();
          // let newest = this.timeProvider.serverNow() - this.chunks[this.chunks.length - 1].timestamp.getMilliseconds();
          // console.debug("chunks: " + this.chunks.length + ", oldest: " + oldest.toFixed(2) + ", newest: " + newest.toFixed(2));
          while (this.chunks.length > 0) {
              let age = this.timeProvider.serverNow() - this.chunks[0].timestamp.getMilliseconds();
              // todo: consider buffer ms
              if (age > 5000 + this.bufferMs) {
                  this.chunks.shift();
                  console.log("Dropping old chunk: " + age.toFixed(2) + ", left: " + this.chunks.length);
              }
              else
                  break;
          }
      }
      getNextBuffer(buffer, playTimeMs) {
          if (!this.chunk) {
              this.chunk = this.chunks.shift();
          }
          // let age = this.timeProvider.serverTime(this.playTime * 1000) - startMs;
          let frames = buffer.length;
          // console.debug("getNextBuffer: " + frames + ", play time: " + playTimeMs.toFixed(2));
          let left = new Float32Array(frames);
          let right = new Float32Array(frames);
          let read = 0;
          let pos = 0;
          // let volume = this.muted ? 0 : this.volume;
          let serverPlayTimeMs = this.timeProvider.serverTime(playTimeMs);
          if (this.chunk) {
              let age = serverPlayTimeMs - this.chunk.startMs(); // - 500;
              let reqChunkDuration = frames / this.sampleFormat.msRate();
              let secs = Math.floor(Date.now() / 1000);
              if (this.lastLog != secs) {
                  this.lastLog = secs;
                  console.log("age: " + age.toFixed(2) + ", req: " + reqChunkDuration);
              }
              if (age < -reqChunkDuration) {
                  console.log("age: " + age.toFixed(2) + " < req: " + reqChunkDuration * -1 + ", chunk.startMs: " + this.chunk.startMs().toFixed(2) + ", timestamp: " + this.chunk.timestamp.getMilliseconds().toFixed(2));
                  console.log("Chunk too young, returning silence");
              }
              else {
                  if (Math.abs(age) > 5) {
                      // We are 5ms apart, do a hard sync, i.e. don't play faster/slower, 
                      // but seek to the desired position instead
                      while (this.chunk && age > this.chunk.duration()) {
                          console.log("Chunk too old, dropping (age: " + age.toFixed(2) + " > " + this.chunk.duration().toFixed(2) + ")");
                          this.chunk = this.chunks.shift();
                          if (!this.chunk)
                              break;
                          age = serverPlayTimeMs - this.chunk.startMs();
                      }
                      if (this.chunk) {
                          if (age > 0) {
                              console.log("Fast forwarding " + age.toFixed(2) + "ms");
                              this.chunk.readFrames(Math.floor(age * this.chunk.sampleFormat.msRate()));
                          }
                          else if (age < 0) {
                              console.log("Playing silence " + -age.toFixed(2) + "ms");
                              let silentFrames = Math.floor(-age * this.chunk.sampleFormat.msRate());
                              left.fill(0, 0, silentFrames);
                              right.fill(0, 0, silentFrames);
                              read = silentFrames;
                              pos = silentFrames;
                          }
                          age = 0;
                      }
                  }
                  // else if (age > 0.1) {
                  //     let rate = age * 0.0005;
                  //     rate = 1.0 - Math.min(rate, 0.0005);
                  //     console.debug("Age > 0, rate: " + rate);
                  //     // we are late (age > 0), this means we are not playing fast enough
                  //     // => the real sample rate seems to be lower, we have to drop some frames
                  //     this.setRealSampleRate(this.sampleFormat.rate * rate); // 0.9999);    
                  // }
                  // else if (age < -0.1) {
                  //     let rate = -age * 0.0005;
                  //     rate = 1.0 + Math.min(rate, 0.0005);
                  //     console.debug("Age < 0, rate: " + rate);
                  //     // we are early (age > 0), this means we are playing too fast
                  //     // => the real sample rate seems to be higher, we have to insert some frames
                  //     this.setRealSampleRate(this.sampleFormat.rate * rate); // 0.9999);    
                  // }
                  // else {
                  //     this.setRealSampleRate(this.sampleFormat.rate);
                  // }
                  let addFrames = 0;
                  let everyN = 0;
                  if (age > 0.1) {
                      addFrames = Math.ceil(age); // / 5);
                  }
                  else if (age < -0.1) {
                      addFrames = Math.floor(age); // / 5);
                  }
                  // addFrames = -2;
                  let readFrames = frames + addFrames - read;
                  if (addFrames != 0)
                      everyN = Math.ceil((frames + addFrames - read) / (Math.abs(addFrames) + 1));
                  // addFrames = 0;
                  // console.debug("frames: " + frames + ", readFrames: " + readFrames + ", addFrames: " + addFrames + ", everyN: " + everyN);
                  while ((read < readFrames) && this.chunk) {
                      let pcmChunk = this.chunk;
                      let pcmBuffer = pcmChunk.readFrames(readFrames - read);
                      let payload = new Int16Array(pcmBuffer);
                      // console.debug("readFrames: " + (frames - read) + ", read: " + pcmBuffer.byteLength + ", payload: " + payload.length);
                      // read += (pcmBuffer.byteLength / this.sampleFormat.frameSize());
                      for (let i = 0; i < payload.length; i += 2) {
                          read++;
                          left[pos] = (payload[i] / 32768); // * volume;
                          right[pos] = (payload[i + 1] / 32768); // * volume;
                          if ((everyN != 0) && (read % everyN == 0)) {
                              if (addFrames > 0) {
                                  pos--;
                              }
                              else {
                                  left[pos + 1] = left[pos];
                                  right[pos + 1] = right[pos];
                                  pos++;
                                  // console.log("Add: " + pos);
                              }
                          }
                          pos++;
                      }
                      if (pcmChunk.isEndOfChunk()) {
                          this.chunk = this.chunks.shift();
                      }
                  }
                  if (addFrames != 0)
                      console.debug("Pos: " + pos + ", frames: " + frames + ", add: " + addFrames + ", everyN: " + everyN);
                  if (read == readFrames)
                      read = frames;
              }
          }
          if (read < frames) {
              console.log("Failed to get chunk, read: " + read + "/" + frames + ", chunks left: " + this.chunks.length);
              left.fill(0, pos);
              right.fill(0, pos);
          }
          // copyToChannel is not supported by Safari
          buffer.getChannelData(0).set(left);
          buffer.getChannelData(1).set(right);
      }
      // setRealSampleRate(sampleRate: number) {
      //     if (sampleRate == this.sampleFormat.rate) {
      //         this.correctAfterXFrames = 0;
      //     }
      //     else {
      //         this.correctAfterXFrames = Math.ceil((this.sampleFormat.rate / sampleRate) / (this.sampleFormat.rate / sampleRate - 1.));
      //         console.debug("setRealSampleRate: " + sampleRate + ", correct after X: " + this.correctAfterXFrames);
      //     }
      // }
      chunk = undefined;
      volume = 1;
      muted = false;
      lastLog = 0;
  }
  class TimeProvider {
      constructor(ctx = undefined) {
          if (ctx) {
              this.setAudioContext(ctx);
          }
      }
      setAudioContext(ctx) {
          this.ctx = ctx;
          this.reset();
      }
      reset() {
          this.diffBuffer.length = 0;
          this.diff = 0;
      }
      setDiff(c2s, s2c) {
          if (this.now() == 0) {
              this.reset();
          }
          else {
              if (this.diffBuffer.push((c2s - s2c) / 2) > 100)
                  this.diffBuffer.shift();
              let sorted = [...this.diffBuffer];
              sorted.sort();
              this.diff = sorted[Math.floor(sorted.length / 2)];
          }
          // 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));
          // console.log("now: " + this.now() + "\t" + this.now() + "\t" + this.now());
      }
      now() {
          if (!this.ctx) {
              return window.performance.now();
          }
          else {
              // Use the more accurate getOutputTimestamp if available, fallback to ctx.currentTime otherwise.
              const contextTime = !!this.ctx.getOutputTimestamp ? this.ctx.getOutputTimestamp().contextTime : undefined;
              return (contextTime !== undefined ? contextTime : this.ctx.currentTime) * 1000;
          }
      }
      nowSec() {
          return this.now() / 1000;
      }
      serverNow() {
          return this.serverTime(this.now());
      }
      serverTime(localTimeMs) {
          return localTimeMs + this.diff;
      }
      diffBuffer = new Array();
      diff = 0;
      ctx;
  }
  class SampleFormat {
      rate = 48000;
      channels = 2;
      bits = 16;
      msRate() {
          return this.rate / 1000;
      }
      toString() {
          return this.rate + ":" + this.bits + ":" + this.channels;
      }
      sampleSize() {
          if (this.bits == 24) {
              return 4;
          }
          return this.bits / 8;
      }
      frameSize() {
          return this.channels * this.sampleSize();
      }
      durationMs(bytes) {
          return (bytes / this.frameSize()) * this.msRate();
      }
  }
  class Decoder {
      setHeader(_buffer) {
          return new SampleFormat();
      }
      decode(_chunk) {
          return null;
      }
  }
  class OpusDecoder extends Decoder {
      setHeader(buffer) {
          let view = new DataView(buffer);
          let ID_OPUS = 0x4F505553;
          if (buffer.byteLength < 12) {
              console.error("Opus header too small: " + buffer.byteLength);
              return null;
          }
          else if (view.getUint32(0, true) != ID_OPUS) {
              console.error("Opus header too small: " + buffer.byteLength);
              return null;
          }
          let format = new SampleFormat();
          format.rate = view.getUint32(4, true);
          format.bits = view.getUint16(8, true);
          format.channels = view.getUint16(10, true);
          console.log("Opus samplerate: " + format.toString());
          return format;
      }
      decode(_chunk) {
          return null;
      }
  }
  class FlacDecoder extends Decoder {
      constructor() {
          super();
          this.decoder = Flac.create_libflac_decoder(true);
          if (this.decoder) {
              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);
              console.log("Flac init: " + init_status);
              Flac.setOptions(this.decoder, { analyseSubframes: true, analyseResiduals: true });
          }
          this.sampleFormat = new SampleFormat();
          this.flacChunk = new ArrayBuffer(0);
          // this.pcmChunk  = new PcmChunkMessage();
          // Flac.setOptions(this.decoder, {analyseSubframes: analyse_frames, analyseResiduals: analyse_residuals});
          // flac_ok &= init_status == 0;
          // console.log("flac init     : " + flac_ok);//DEBUG
      }
      decode(chunk) {
          // console.log("Flac decode: " + chunk.payload.byteLength);
          this.flacChunk = chunk.payload.slice(0);
          this.pcmChunk = chunk;
          this.pcmChunk.clearPayload();
          this.cacheInfo = { cachedBlocks: 0, isCachedChunk: true };
          // console.log("Flac len: " + this.flacChunk.byteLength);
          while (this.flacChunk.byteLength && Flac.FLAC__stream_decoder_process_single(this.decoder)) {
              Flac.FLAC__stream_decoder_get_state(this.decoder);
              // let state = Flac.FLAC__stream_decoder_get_state(this.decoder);
              // console.log("State: " + state);
          }
          // console.log("Pcm payload: " + this.pcmChunk!.payloadSize());
          if (this.cacheInfo.cachedBlocks > 0) {
              let diffMs = this.cacheInfo.cachedBlocks / this.sampleFormat.msRate();
              // console.log("Cached: " + this.cacheInfo.cachedBlocks + ", " + diffMs + "ms");
              this.pcmChunk.timestamp.setMilliseconds(this.pcmChunk.timestamp.getMilliseconds() - diffMs);
          }
          return this.pcmChunk;
      }
      read_callback_fn(bufferSize) {
          // console.log('  decode read callback, buffer bytes max=', bufferSize);
          if (this.header) {
              console.log("  header: " + this.header.byteLength);
              let data = new Uint8Array(this.header);
              this.header = null;
              return { buffer: data, readDataLength: data.byteLength, error: false };
          }
          else if (this.flacChunk) {
              // console.log("  flacChunk: " + this.flacChunk.byteLength);
              // a fresh read => next call to write will not be from cached data
              this.cacheInfo.isCachedChunk = false;
              let data = new Uint8Array(this.flacChunk.slice(0, Math.min(bufferSize, this.flacChunk.byteLength)));
              this.flacChunk = this.flacChunk.slice(data.byteLength);
              return { buffer: data, readDataLength: data.byteLength, error: false };
          }
          return { buffer: new Uint8Array(0), readDataLength: 0, error: false };
      }
      write_callback_fn(data, frameInfo) {
          // console.log("  write frame metadata: " + frameInfo + ", len: " + data.length);
          if (this.cacheInfo.isCachedChunk) {
              // there was no call to read, so it's some cached data
              this.cacheInfo.cachedBlocks += frameInfo.blocksize;
          }
          let payload = new ArrayBuffer((frameInfo.bitsPerSample / 8) * frameInfo.channels * frameInfo.blocksize);
          let view = new DataView(payload);
          for (let channel = 0; channel < frameInfo.channels; ++channel) {
              let channelData = new DataView(data[channel].buffer, 0, data[channel].buffer.byteLength);
              // console.log("channelData: " + channelData.byteLength + ", blocksize: " + frameInfo.blocksize);
              for (let i = 0; i < frameInfo.blocksize; ++i) {
                  view.setInt16(2 * (frameInfo.channels * i + channel), channelData.getInt16(2 * i, true), true);
              }
          }
          this.pcmChunk.addPayload(payload);
          // console.log("write: " + payload.byteLength + ", len: " + this.pcmChunk!.payloadSize());
      }
      /** @memberOf decode */
      metadata_callback_fn(data) {
          console.info('meta data: ', data);
          // let view = new DataView(data);
          this.sampleFormat.rate = data.sampleRate;
          this.sampleFormat.channels = data.channels;
          this.sampleFormat.bits = data.bitsPerSample;
          console.log("metadata_callback_fn, sampleformat: " + this.sampleFormat.toString());
      }
      /** @memberOf decode */
      error_callback_fn(err, errMsg) {
          console.error('decode error callback', err, errMsg);
      }
      setHeader(buffer) {
          this.header = buffer.slice(0);
          Flac.FLAC__stream_decoder_process_until_end_of_metadata(this.decoder);
          return this.sampleFormat;
      }
      sampleFormat;
      decoder;
      header = null;
      flacChunk;
      pcmChunk;
      cacheInfo = { isCachedChunk: false, cachedBlocks: 0 };
  }
  class PlayBuffer {
      constructor(buffer, playTime, source, destination) {
          this.buffer = buffer;
          this.playTime = playTime;
          this.source = source;
          this.source.buffer = this.buffer;
          this.source.connect(destination);
          this.onended = (_playBuffer) => { };
      }
      onended;
      start() {
          this.source.onended = () => {
              this.onended(this);
          };
          this.source.start(this.playTime);
      }
      buffer;
      playTime;
      source;
      num = 0;
  }
  class PcmDecoder extends Decoder {
      setHeader(buffer) {
          let sampleFormat = new SampleFormat();
          let view = new DataView(buffer);
          sampleFormat.channels = view.getUint16(22, true);
          sampleFormat.rate = view.getUint32(24, true);
          sampleFormat.bits = view.getUint16(34, true);
          return sampleFormat;
      }
      decode(chunk) {
          return chunk;
      }
  }
  class SnapStream {
      constructor(baseUrl) {
          this.baseUrl = baseUrl;
          this.timeProvider = new TimeProvider();
          if (this.setupAudioContext()) {
              this.connect();
          }
          else {
              alert("Sorry, but the Web Audio API is not supported by your browser");
          }
      }
      setupAudioContext() {
          let AudioContext = window.AudioContext // Default
              || window.webkitAudioContext // Safari and old versions of Chrome
              || false;
          if (AudioContext) {
              let options;
              options = { latencyHint: "playback", sampleRate: this.sampleFormat ? this.sampleFormat.rate : undefined };
              const chromeVersion = getChromeVersion();
              if ((chromeVersion !== null && chromeVersion < 55) || !window.AudioContext) {
                  // Some older browsers won't decode the stream if options are provided.
                  options = undefined;
              }
              this.ctx = new AudioContext(options);
              this.gainNode = this.ctx.createGain();
              this.gainNode.connect(this.ctx.destination);
          }
          else {
              // Web Audio API is not supported
              return false;
          }
          return true;
      }
      connect() {
          this.streamsocket = new WebSocket(this.baseUrl + '/stream');
          this.streamsocket.binaryType = "arraybuffer";
          this.streamsocket.onmessage = (ev) => this.onMessage(ev);
          this.streamsocket.onopen = () => {
              console.log("on open");
              let hello = new HelloMessage();
              hello.mac = "00:00:00:00:00:00";
              hello.arch = "web";
              hello.os = navigator.platform;
              hello.hostname = "Snapweb client";
              hello.uniqueId = getPersistentValue("uniqueId", uuidv4());
              this.sendMessage(hello);
              this.syncTime();
              this.syncHandle = window.setInterval(() => this.syncTime(), 1000);
          };
          this.streamsocket.onerror = (ev) => { console.error('error:', ev); };
          this.streamsocket.onclose = () => {
              window.clearInterval(this.syncHandle);
              console.info('connection lost, reconnecting in 1s');
              setTimeout(() => this.connect(), 1000);
          };
      }
      onMessage(msg) {
          let view = new DataView(msg.data);
          let type = view.getUint16(0, true);
          if (type == 1) {
              let codec = new CodecMessage(msg.data);
              console.log("Codec: " + codec.codec);
              if (codec.codec == "flac") {
                  this.decoder = new FlacDecoder();
              }
              else if (codec.codec == "pcm") {
                  this.decoder = new PcmDecoder();
              }
              else if (codec.codec == "opus") {
                  this.decoder = new OpusDecoder();
                  alert("Codec not supported: " + codec.codec);
              }
              else {
                  alert("Codec not supported: " + codec.codec);
              }
              if (this.decoder) {
                  this.sampleFormat = this.decoder.setHeader(codec.payload);
                  console.log("Sampleformat: " + this.sampleFormat.toString());
                  if ((this.sampleFormat.channels != 2) || (this.sampleFormat.bits != 16)) {
                      alert("Stream must be stereo with 16 bit depth, actual format: " + this.sampleFormat.toString());
                  }
                  else {
                      if (this.bufferDurationMs != 0) {
                          this.bufferFrameCount = Math.floor(this.bufferDurationMs * this.sampleFormat.msRate());
                      }
                      if (window.AudioContext) {
                          // we are not using webkitAudioContext, so it's safe to setup a new AudioContext with the new samplerate
                          // since this code is not triggered by direct user input, we cannt create a webkitAudioContext here
                          this.stopAudio();
                          this.setupAudioContext();
                      }
                      this.ctx.resume();
                      this.timeProvider.setAudioContext(this.ctx);
                      this.gainNode.gain.value = this.serverSettings.muted ? 0 : this.serverSettings.volumePercent / 100;
                      // this.timeProvider = new TimeProvider(this.ctx);
                      this.stream = new AudioStream(this.timeProvider, this.sampleFormat, this.bufferMs);
                      this.latency = (this.ctx.baseLatency !== undefined ? this.ctx.baseLatency : 0) + (this.ctx.outputLatency !== undefined ? this.ctx.outputLatency : 0);
                      console.log("Base latency: " + this.ctx.baseLatency + ", output latency: " + this.ctx.outputLatency + ", latency: " + this.latency);
                      this.play();
                  }
              }
          }
          else if (type == 2) {
              let pcmChunk = new PcmChunkMessage(msg.data, this.sampleFormat);
              if (this.decoder) {
                  let decoded = this.decoder.decode(pcmChunk);
                  if (decoded) {
                      this.stream.addChunk(decoded);
                  }
              }
          }
          else if (type == 3) {
              this.serverSettings = new ServerSettingsMessage(msg.data);
              this.gainNode.gain.value = this.serverSettings.muted ? 0 : this.serverSettings.volumePercent / 100;
              this.bufferMs = this.serverSettings.bufferMs - this.serverSettings.latency;
              console.log("ServerSettings bufferMs: " + this.serverSettings.bufferMs + ", latency: " + this.serverSettings.latency + ", volume: " + this.serverSettings.volumePercent + ", muted: " + this.serverSettings.muted);
          }
          else if (type == 4) {
              if (this.timeProvider) {
                  let time = new TimeMessage(msg.data);
                  this.timeProvider.setDiff(time.latency.getMilliseconds(), this.timeProvider.now() - time.sent.getMilliseconds());
              }
              // console.log("Time sec: " + time.latency.sec + ", usec: " + time.latency.usec + ", diff: " + this.timeProvider.diff);
          }
          else {
              console.info("Message not handled, type: " + type);
          }
      }
      sendMessage(msg) {
          msg.sent = new Tv(0, 0);
          msg.sent.setMilliseconds(this.timeProvider.now());
          msg.id = ++this.msgId;
          if (this.streamsocket.readyState == this.streamsocket.OPEN) {
              this.streamsocket.send(msg.serialize());
          }
      }
      syncTime() {
          let t = new TimeMessage();
          t.latency.setMilliseconds(this.timeProvider.now());
          this.sendMessage(t);
          // console.log("prepareSource median: " + Math.round(this.median * 10) / 10);
      }
      stopAudio() {
          // if (this.ctx) {
          //     this.ctx.close();
          // }
          this.ctx.suspend();
          while (this.audioBuffers.length > 0) {
              let buffer = this.audioBuffers.pop();
              buffer.onended = () => { };
              buffer.source.stop();
          }
          while (this.freeBuffers.length > 0) {
              this.freeBuffers.pop();
          }
      }
      stop() {
          window.clearInterval(this.syncHandle);
          this.stopAudio();
          if ([WebSocket.OPEN, WebSocket.CONNECTING].includes(this.streamsocket.readyState)) {
              this.streamsocket.onclose = () => { };
              this.streamsocket.close();
          }
      }
      play() {
          this.playTime = this.timeProvider.nowSec() + 0.1;
          for (let i = 1; i <= this.audioBufferCount; ++i) {
              this.playNext();
          }
      }
      playNext() {
          let buffer = this.freeBuffers.pop() || this.ctx.createBuffer(this.sampleFormat.channels, this.bufferFrameCount, this.sampleFormat.rate);
          let playTimeMs = (this.playTime + this.latency) * 1000 - this.bufferMs;
          this.stream.getNextBuffer(buffer, playTimeMs);
          let source = this.ctx.createBufferSource();
          let playBuffer = new PlayBuffer(buffer, this.playTime, source, this.gainNode);
          this.audioBuffers.push(playBuffer);
          playBuffer.num = ++this.bufferNum;
          playBuffer.onended = (buffer) => {
              // let diff = this.timeProvider.nowSec() - buffer.playTime;
              this.freeBuffers.push(this.audioBuffers.splice(this.audioBuffers.indexOf(buffer), 1)[0].buffer);
              // console.debug("PlayBuffer " + playBuffer.num + " ended after: " + (diff * 1000) + ", in flight: " + this.audioBuffers.length);
              this.playNext();
          };
          playBuffer.start();
          this.playTime += this.bufferFrameCount / this.sampleFormat.rate;
      }
      baseUrl;
      streamsocket;
      playTime = 0;
      msgId = 0;
      bufferDurationMs = 80; // 0;
      bufferFrameCount = 3844; // 9600; // 2400;//8192;
      syncHandle = -1;
      // ageBuffer: Array<number>;
      audioBuffers = new Array();
      freeBuffers = new Array();
      timeProvider;
      stream;
      ctx; // | undefined;
      gainNode;
      serverSettings;
      decoder;
      sampleFormat;
      // median: number = 0;
      audioBufferCount = 3;
      bufferMs = 1000;
      bufferNum = 0;
      latency = 0;
  }
  //# sourceMappingURL=snapstream.js.map