/* global $, Hls, __NETLIFY__ */
/* eslint camelcase: 0 */

import { pack } from 'jsonpack';
import 'promise-polyfill/src/polyfill';
import { sortObject, copyTextToClipboard } from './demo-utils';
import { TimelineChart } from './chart/timeline-chart';

const NETLIFY = __NETLIFY__; // replaced in build

const STORAGE_KEYS = {
  Editor_Persistence: 'hlsjs:config-editor-persist',
  Hls_Config: 'hlsjs:config',
  volume: 'hlsjs:volume',
  demo_tabs: 'hlsjs:demo-tabs',
};

const testStreams = require('../tests/test-streams');
const defaultTestStreamUrl = testStreams[Object.keys(testStreams)[0]].url;
const sourceURL = decodeURIComponent(getURLParam('src', defaultTestStreamUrl));

let demoConfig = getURLParam('demoConfig', null);
if (demoConfig) {
  demoConfig = JSON.parse(atob(demoConfig));
} else {
  demoConfig = {};
}

const hlsjsDefaults = {
  debug: true,
  enableWorker: true,
  lowLatencyMode: true,
  backBufferLength: 60 * 1.5,
};

let enableStreaming = getDemoConfigPropOrDefault('enableStreaming', true);
let autoRecoverError = getDemoConfigPropOrDefault('autoRecoverError', true);
let levelCapping = getDemoConfigPropOrDefault('levelCapping', -1);
let limitMetrics = getDemoConfigPropOrDefault('limitMetrics', -1);
let dumpfMP4 = getDemoConfigPropOrDefault('dumpfMP4', false);
let stopOnStall = getDemoConfigPropOrDefault('stopOnStall', false);

let bufferingIdx = -1;
let selectedTestStream = null;

let video = document.querySelector('#video');
const startTime = Date.now();

let lastSeekingIdx;
let lastStartPosition;
let lastDuration;
let lastAudioTrackSwitchingIdx;
let hls;
let url;
let events;
let stats;
let tracks;
let fmp4Data;
let configPersistenceEnabled = false;
let configEditor = null;
let chart;
let resizeAsyncCallbackId = -1;

const requestAnimationFrame = self.requestAnimationFrame || self.setTimeout;
const cancelAnimationFrame = self.cancelAnimationFrame || self.clearTimeout;
const resizeHandlers = [];
const resize = () => {
  cancelAnimationFrame(resizeAsyncCallbackId);
  resizeAsyncCallbackId = requestAnimationFrame(() => {
    resizeHandlers.forEach((handler) => {
      handler();
    });
  });
};

self.onresize = resize;
if (self.screen && self.screen.orientation) {
  self.screen.orientation.onchange = resize;
}

const playerResize = () => {
  const bounds = video.getBoundingClientRect();
  $('#currentSize').html(
    `${Math.round(bounds.width * 10) / 10} x ${
      Math.round(bounds.height * 10) / 10
    }`
  );
  if (video.videoWidth && video.videoHeight) {
    $('#currentResolution').html(`${video.videoWidth} x ${video.videoHeight}`);
  }
};
resizeHandlers.push(playerResize);

$(document).ready(function () {
  setupConfigEditor();

  chart = setupTimelineChart();

  Object.keys(testStreams).forEach((key, index) => {
    const stream = testStreams[key];
    const option = new Option(stream.description, key);
    $('#streamSelect').append(option);
    if (stream.url === sourceURL) {
      document.querySelector('#streamSelect').selectedIndex = index + 1;
    }
  });

  const videoWidth = video.style.width;
  if (videoWidth) {
    $('#videoSize option').each(function (i, option) {
      if (option.value === videoWidth) {
        document.querySelector('#videoSize').selectedIndex = i;
        $('#bufferedCanvas').width(videoWidth);
        resize();
        return false;
      }
    });
  }

  $('#streamSelect').change(function () {
    const key = $('#streamSelect').val() || Object.keys(testStreams)[0];
    selectedTestStream = testStreams[key];
    const streamUrl = selectedTestStream.url;
    $('#streamURL').val(streamUrl);
    loadSelectedStream();
  });

  $('#streamURL').change(function () {
    selectedTestStream = null;
    loadSelectedStream();
  });

  $('#videoSize').change(function () {
    $('#video').width($('#videoSize').val());
    $('#bufferedCanvas').width($('#videoSize').val());
    checkBuffer();
    resize();
  });

  $('#enableStreaming').click(function () {
    enableStreaming = this.checked;
    loadSelectedStream();
  });

  $('#autoRecoverError').click(function () {
    autoRecoverError = this.checked;
    onDemoConfigChanged();
  });

  $('#stopOnStall').click(function () {
    stopOnStall = this.checked;
    onDemoConfigChanged();
  });

  $('#dumpfMP4').click(function () {
    dumpfMP4 = this.checked;
    $('.btn-dump').toggle(dumpfMP4);
    onDemoConfigChanged();
  });

  $('#limitMetrics').change(function () {
    limitMetrics = this.value;
    onDemoConfigChanged();
  });

  $('#levelCapping').change(function () {
    levelCapping = this.value;
    onDemoConfigChanged();
  });

  $('#limitMetrics').val(limitMetrics);
  $('#enableStreaming').prop('checked', enableStreaming);
  $('#autoRecoverError').prop('checked', autoRecoverError);
  $('#stopOnStall').prop('checked', stopOnStall);
  $('#dumpfMP4').prop('checked', dumpfMP4);
  $('#levelCapping').val(levelCapping);

  // link to version on npm if canary
  // github branch for a branch version
  // github tag for a normal tag
  // github PR for a pr
  function getVersionLink(version) {
    const alphaRegex = /[-.]0\.alpha\./;
    if (alphaRegex.test(version)) {
      return `https://www.npmjs.com/package/hls.js/v/${encodeURIComponent(
        version
      )}`;
    } else if (NETLIFY.reviewID) {
      return `https://github.com/video-dev/hls.js/pull/${NETLIFY.reviewID}`;
    } else if (NETLIFY.branch) {
      return `https://github.com/video-dev/hls.js/tree/${encodeURIComponent(
        NETLIFY.branch
      )}`;
    }
    return `https://github.com/video-dev/hls.js/releases/tag/v${encodeURIComponent(
      version
    )}`;
  }

  const version = Hls.version;
  if (version) {
    const $a = $('<a />')
      .attr('target', '_blank')
      .attr('rel', 'noopener noreferrer')
      .attr('href', getVersionLink(version))
      .text('v' + version);
    $('.title').append(' ').append($a);
  }

  $('#streamURL').val(sourceURL);

  const volumeSettings = JSON.parse(
    localStorage.getItem(STORAGE_KEYS.volume)
  ) || {
    volume: 0.05,
    muted: false,
  };
  video.volume = volumeSettings.volume;
  video.muted = volumeSettings.muted;

  $('.btn-dump').toggle(dumpfMP4);
  $('#toggleButtons').show();

  $('#metricsButtonWindow').toggle(self.windowSliding);
  $('#metricsButtonFixed').toggle(!self.windowSliding);

  loadSelectedStream();

  let tabIndexesCSV = localStorage.getItem(STORAGE_KEYS.demo_tabs);
  if (tabIndexesCSV === null) {
    tabIndexesCSV = '0,1,2';
  }
  if (tabIndexesCSV) {
    tabIndexesCSV.split(',').forEach((indexString) => {
      toggleTab($('.demo-tab-btn')[parseInt(indexString) || 0], true);
    });
  }
  $(window).on('popstate', function () {
    window.location.reload();
  });
});

function setupGlobals() {
  self.events = events = {
    url: url,
    t0: self.performance.now(),
    load: [],
    buffer: [],
    video: [],
    level: [],
    bitrate: [],
  };
  lastAudioTrackSwitchingIdx = undefined;
  lastSeekingIdx = undefined;
  bufferingIdx = -1;

  // actual values, only on window
  self.recoverDecodingErrorDate = null;
  self.recoverSwapAudioCodecDate = null;
  self.fmp4Data = fmp4Data = {
    audio: [],
    video: [],
  };
  self.onClickBufferedRange = onClickBufferedRange;
  self.updateLevelInfo = updateLevelInfo;
  self.onDemoConfigChanged = onDemoConfigChanged;
  self.createfMP4 = createfMP4;
  self.goToMetricsPermaLink = goToMetricsPermaLink;
  self.toggleTab = toggleTab;
  self.toggleTabClick = toggleTabClick;
  self.applyConfigEditorValue = applyConfigEditorValue;
}

function trimArray(target, limit) {
  if (limit < 0) {
    return;
  }

  while (target.length > limit) {
    target.shift();
  }
}

function trimEventHistory() {
  const x = limitMetrics;

  if (x < 0) {
    return;
  }

  trimArray(events.load, x);
  trimArray(events.buffer, x);
  trimArray(events.video, x);
  trimArray(events.level, x);
  trimArray(events.bitrate, x);
}

function loadSelectedStream() {
  $('#statusOut,#errorOut').empty();

  if (!Hls.isSupported()) {
    handleUnsupported();
    return;
  }

  url = $('#streamURL').val();

  setupGlobals();
  hideCanvas();

  if (hls) {
    hls.destroy();
    clearInterval(hls.bufferTimer);
    hls = null;
  }

  if (!enableStreaming) {
    logStatus('Streaming disabled');
    return;
  }

  logStatus('Loading ' + url);

  // Extending both a demo-specific config and the user config which can override all
  const hlsConfig = $.extend(
    {},
    hlsjsDefaults,
    getEditorValue({ parse: true })
  );

  if (selectedTestStream && selectedTestStream.config) {
    console.info(
      '[loadSelectedStream] extending hls config with stream-specific config: ',
      selectedTestStream.config
    );
    $.extend(hlsConfig, selectedTestStream.config);
    updateConfigEditorValue(hlsConfig);
  }

  onDemoConfigChanged(true);
  console.log('Using Hls.js config:', hlsConfig);

  self.hls = hls = new Hls(hlsConfig);

  logStatus('Loading manifest and attaching video element...');

  const expiredTracks = [].filter.call(
    video.textTracks,
    (track) => track.kind !== 'metadata'
  );
  if (expiredTracks.length) {
    const kinds = expiredTracks
      .map((track) => track.kind)
      .filter((kind, index, self) => self.indexOf(kind) === index);
    logStatus(
      `Replacing video element to remove ${kinds.join(' and ')} text tracks`
    );
    const videoWithExpiredTextTracks = video;
    video = videoWithExpiredTextTracks.cloneNode(false);
    video.removeAttribute('src');
    video.volume = videoWithExpiredTextTracks.volume;
    video.muted = videoWithExpiredTextTracks.muted;
    videoWithExpiredTextTracks.parentNode.insertBefore(
      video,
      videoWithExpiredTextTracks
    );
    videoWithExpiredTextTracks.parentNode.removeChild(
      videoWithExpiredTextTracks
    );
  }
  addChartEventListeners(hls);
  addVideoEventListeners(video);

  hls.loadSource(url);
  hls.autoLevelCapping = levelCapping;
  hls.attachMedia(video);

  hls.on(Hls.Events.MEDIA_ATTACHED, function () {
    logStatus('Media element attached');
    bufferingIdx = -1;
    events.video.push({
      time: self.performance.now() - events.t0,
      type: 'Media attached',
    });
    trimEventHistory();
  });

  hls.on(Hls.Events.MEDIA_DETACHED, function () {
    logStatus('Media element detached');
    clearInterval(hls.bufferTimer);
    bufferingIdx = -1;
    tracks = [];
    events.video.push({
      time: self.performance.now() - events.t0,
      type: 'Media detached',
    });
    trimEventHistory();
  });

  hls.on(Hls.Events.DESTROYING, function () {
    clearInterval(hls.bufferTimer);
  });
  hls.on(Hls.Events.BUFFER_RESET, function () {
    clearInterval(hls.bufferTimer);
  });

  hls.on(Hls.Events.FRAG_PARSING_INIT_SEGMENT, function (eventName, data) {
    showCanvas();
    events.video.push({
      time: self.performance.now() - events.t0,
      type: data.id + ' init segment',
    });
    trimEventHistory();
  });

  hls.on(Hls.Events.FRAG_PARSING_METADATA, function (eventName, data) {
    // console.log("Id3 samples ", data.samples);
  });

  hls.on(Hls.Events.LEVEL_SWITCHING, function (eventName, data) {
    events.level.push({
      time: self.performance.now() - events.t0,
      id: data.level,
      bitrate: Math.round(hls.levels[data.level].bitrate / 1000),
    });
    trimEventHistory();
    updateLevelInfo();
  });

  hls.on(Hls.Events.MANIFEST_PARSED, function (eventName, data) {
    events.load.push({
      type: 'manifest',
      name: '',
      start: 0,
      end: data.levels.length,
      time: data.stats.loading.start - events.t0,
      latency: data.stats.loading.first - data.stats.loading.start,
      load: data.stats.loading.end - data.stats.loading.first,
      duration: data.stats.loading.end - data.stats.loading.first,
    });
    trimEventHistory();
    self.refreshCanvas();
  });

  hls.on(Hls.Events.MANIFEST_PARSED, function (eventName, data) {
    logStatus(`${hls.levels.length} quality levels found`);
    logStatus('Manifest successfully loaded');
    stats = {
      levelNb: data.levels.length,
      levelParsed: 0,
    };
    trimEventHistory();
    updateLevelInfo();
  });

  hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, function (eventName, data) {
    logStatus('No of audio tracks found: ' + data.audioTracks.length);
    updateAudioTrackInfo();
  });

  hls.on(Hls.Events.AUDIO_TRACK_SWITCHING, function (eventName, data) {
    logStatus('Audio track switching...');
    updateAudioTrackInfo();
    events.video.push({
      time: self.performance.now() - events.t0,
      type: 'audio switching',
      name: '@' + data.id,
    });
    trimEventHistory();
    lastAudioTrackSwitchingIdx = events.video.length - 1;
  });

  hls.on(Hls.Events.AUDIO_TRACK_SWITCHED, function (eventName, data) {
    logStatus('Audio track switched');
    updateAudioTrackInfo();
    const event = {
      time: self.performance.now() - events.t0,
      type: 'audio switched',
      name: '@' + data.id,
    };
    if (lastAudioTrackSwitchingIdx !== undefined) {
      events.video[lastAudioTrackSwitchingIdx].duration =
        event.time - events.video[lastAudioTrackSwitchingIdx].time;
      lastAudioTrackSwitchingIdx = undefined;
    }
    events.video.push(event);
    trimEventHistory();
  });

  hls.on(Hls.Events.LEVEL_LOADED, function (eventName, data) {
    events.isLive = data.details.live;
    const event = {
      type: 'level',
      id: data.level,
      start: data.details.startSN,
      end: data.details.endSN,
      time: data.stats.loading.start - events.t0,
      latency: data.stats.loading.first - data.stats.loading.start,
      load: data.stats.loading.end - data.stats.loading.first,
      parsing: data.stats.parsing.end - data.stats.loading.end,
      duration: data.stats.loading.end - data.stats.loading.first,
    };

    const parsingDuration = data.stats.parsing.end - data.stats.loading.end;
    if (stats.levelParsed) {
      this.sumLevelParsingMs += parsingDuration;
    } else {
      this.sumLevelParsingMs = parsingDuration;
    }

    stats.levelParsed++;
    stats.levelParsingUs = Math.round(
      (1000 * this.sumLevelParsingMs) / stats.levelParsed
    );

    // console.log('parsing level duration :' + stats.levelParsingUs + 'us,count:' + stats.levelParsed);

    events.load.push(event);
    trimEventHistory();
    self.refreshCanvas();
  });

  hls.on(Hls.Events.AUDIO_TRACK_LOADED, function (eventName, data) {
    events.isLive = data.details.live;
    const event = {
      type: 'audio track',
      id: data.id,
      start: data.details.startSN,
      end: data.details.endSN,
      time: data.stats.loading.start - events.t0,
      latency: data.stats.loading.first - data.stats.loading.start,
      load: data.stats.loading.end - data.stats.loading.first,
      parsing: data.stats.parsing.end - data.stats.loading.end,
      duration: data.stats.loading.end - data.stats.loading.first,
    };
    events.load.push(event);
    trimEventHistory();
    self.refreshCanvas();
  });

  hls.on(Hls.Events.FRAG_BUFFERED, function (eventName, data) {
    const event = {
      type: data.frag.type + (data.part ? ' part' : ' fragment'),
      id: data.frag.level,
      id2: data.frag.sn,
      id3: data.part ? data.part.index : undefined,
      time: data.stats.loading.start - events.t0,
      latency: data.stats.loading.first - data.stats.loading.start,
      load: data.stats.loading.end - data.stats.loading.first,
      parsing: data.stats.parsing.end - data.stats.loading.end,
      buffer: data.stats.buffering.end - data.stats.parsing.end,
      duration: data.stats.buffering.end - data.stats.loading.first,
      bw: Math.round(
        (8 * data.stats.total) /
          (data.stats.buffering.end - data.stats.loading.start)
      ),
      size: data.stats.total,
    };
    events.load.push(event);
    events.bitrate.push({
      time: self.performance.now() - events.t0,
      bitrate: event.bw,
      duration: data.frag.duration,
      level: event.id,
    });
    if (events.buffer.length === 0) {
      events.buffer.push({
        time: 0,
        buffer: 0,
        pos: 0,
      });
    }
    clearInterval(hls.bufferTimer);
    hls.bufferTimer = self.setInterval(checkBuffer, 100);
    trimEventHistory();
    self.refreshCanvas();
    updateLevelInfo();

    const latency = data.stats.loading.first - data.stats.loading.start;
    const parsing = data.stats.parsing.end - data.stats.loading.end;
    const process = data.stats.buffering.end - data.stats.loading.start;
    const bitrate = Math.round(
      (8 * data.stats.total) /
        (data.stats.buffering.end - data.stats.loading.first)
    );

    if (stats.fragBuffered) {
      stats.fragMinLatency = Math.min(stats.fragMinLatency, latency);
      stats.fragMaxLatency = Math.max(stats.fragMaxLatency, latency);
      stats.fragMinProcess = Math.min(stats.fragMinProcess, process);
      stats.fragMaxProcess = Math.max(stats.fragMaxProcess, process);
      stats.fragMinKbps = Math.min(stats.fragMinKbps, bitrate);
      stats.fragMaxKbps = Math.max(stats.fragMaxKbps, bitrate);
      stats.autoLevelCappingMin = Math.min(
        stats.autoLevelCappingMin,
        hls.autoLevelCapping
      );
      stats.autoLevelCappingMax = Math.max(
        stats.autoLevelCappingMax,
        hls.autoLevelCapping
      );
      stats.fragBuffered++;
    } else {
      stats.fragMinLatency = stats.fragMaxLatency = latency;
      stats.fragMinProcess = stats.fragMaxProcess = process;
      stats.fragMinKbps = stats.fragMaxKbps = bitrate;
      stats.fragBuffered = 1;
      stats.fragBufferedBytes = 0;
      stats.autoLevelCappingMin = stats.autoLevelCappingMax =
        hls.autoLevelCapping;
      this.sumLatency = 0;
      this.sumKbps = 0;
      this.sumProcess = 0;
      this.sumParsing = 0;
    }
    stats.fraglastLatency = latency;
    this.sumLatency += latency;
    stats.fragAvgLatency = Math.round(this.sumLatency / stats.fragBuffered);
    stats.fragLastProcess = process;
    this.sumProcess += process;
    this.sumParsing += parsing;
    stats.fragAvgProcess = Math.round(this.sumProcess / stats.fragBuffered);
    stats.fragLastKbps = bitrate;
    this.sumKbps += bitrate;
    stats.fragAvgKbps = Math.round(this.sumKbps / stats.fragBuffered);
    stats.fragBufferedBytes += data.stats.total;
    stats.fragparsingKbps = Math.round(
      (8 * stats.fragBufferedBytes) / this.sumParsing
    );
    stats.fragparsingMs = Math.round(this.sumParsing);
    stats.autoLevelCappingLast = hls.autoLevelCapping;
  });

  hls.on(Hls.Events.LEVEL_SWITCHED, function (eventName, data) {
    const event = {
      time: self.performance.now() - events.t0,
      type: 'level switched',
      name: data.level,
    };
    events.video.push(event);
    trimEventHistory();
    self.refreshCanvas();
    updateLevelInfo();
  });

  hls.on(Hls.Events.FRAG_CHANGED, function (eventName, data) {
    const event = {
      time: self.performance.now() - events.t0,
      type: 'frag changed',
      name: data.frag.sn + ' @ ' + data.frag.level,
    };
    events.video.push(event);
    trimEventHistory();
    self.refreshCanvas();
    updateLevelInfo();
    stats.tagList = data.frag.tagList;

    const level = data.frag.level;
    const autoLevel = hls.autoLevelEnabled;
    if (stats.levelStart === undefined) {
      stats.levelStart = level;
    }

    stats.fragProgramDateTime = data.frag.programDateTime;
    stats.fragStart = data.frag.start;

    if (autoLevel) {
      if (stats.fragChangedAuto) {
        stats.autoLevelMin = Math.min(stats.autoLevelMin, level);
        stats.autoLevelMax = Math.max(stats.autoLevelMax, level);
        stats.fragChangedAuto++;
        if (this.levelLastAuto && level !== stats.autoLevelLast) {
          stats.autoLevelSwitch++;
        }
      } else {
        stats.autoLevelMin = stats.autoLevelMax = level;
        stats.autoLevelSwitch = 0;
        stats.fragChangedAuto = 1;
        this.sumAutoLevel = 0;
      }
      this.sumAutoLevel += level;
      stats.autoLevelAvg =
        Math.round((1000 * this.sumAutoLevel) / stats.fragChangedAuto) / 1000;
      stats.autoLevelLast = level;
    } else {
      if (stats.fragChangedManual) {
        stats.manualLevelMin = Math.min(stats.manualLevelMin, level);
        stats.manualLevelMax = Math.max(stats.manualLevelMax, level);
        stats.fragChangedManual++;
        if (!this.levelLastAuto && level !== stats.manualLevelLast) {
          stats.manualLevelSwitch++;
        }
      } else {
        stats.manualLevelMin = stats.manualLevelMax = level;
        stats.manualLevelSwitch = 0;
        stats.fragChangedManual = 1;
      }
      stats.manualLevelLast = level;
    }
    this.levelLastAuto = autoLevel;
  });

  hls.on(Hls.Events.FRAG_LOAD_EMERGENCY_ABORTED, function (eventName, data) {
    if (stats) {
      if (stats.fragLoadEmergencyAborted === undefined) {
        stats.fragLoadEmergencyAborted = 1;
      } else {
        stats.fragLoadEmergencyAborted++;
      }
    }
  });

  hls.on(Hls.Events.FRAG_DECRYPTED, function (eventName, data) {
    if (!stats.fragDecrypted) {
      stats.fragDecrypted = 0;
      this.totalDecryptTime = 0;
      stats.fragAvgDecryptTime = 0;
    }
    stats.fragDecrypted++;
    this.totalDecryptTime += data.stats.tdecrypt - data.stats.tstart;
    stats.fragAvgDecryptTime = this.totalDecryptTime / stats.fragDecrypted;
  });

  hls.on(Hls.Events.ERROR, function (eventName, data) {
    console.warn('Error event:', data);
    switch (data.details) {
      case Hls.ErrorDetails.MANIFEST_LOAD_ERROR:
        try {
          $('#errorOut').html(
            'Cannot load <a href="' +
              data.context.url +
              '">' +
              url +
              '</a><br>HTTP response code:' +
              data.response.code +
              ' <br>' +
              data.response.text
          );
          if (data.response.code === 0) {
            $('#errorOut').append(
              'This might be a CORS issue, consider installing <a href="https://chrome.google.com/webstore/detail/allow-control-allow-origi/nlfbmbojpeacfghkpbjhddihlkkiljbi">Allow-Control-Allow-Origin</a> Chrome Extension'
            );
          }
        } catch (err) {
          $('#errorOut').html(
            'Cannot load <a href="' +
              data.context.url +
              '">' +
              url +
              '</a><br>Response body: ' +
              data.response.text
          );
        }
        break;
      case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT:
        logError('Timeout while loading manifest');
        break;
      case Hls.ErrorDetails.MANIFEST_PARSING_ERROR:
        logError('Error while parsing manifest:' + data.reason);
        break;
      case Hls.ErrorDetails.LEVEL_EMPTY_ERROR:
        logError(
          'Loaded level contains no fragments ' + data.level + ' ' + data.url
        );
        // handleLevelError demonstrates how to remove a level that errors followed by a downswitch
        // handleLevelError(data);
        break;
      case Hls.ErrorDetails.LEVEL_LOAD_ERROR:
        logError(
          'Error while loading level playlist ' +
            data.context.level +
            ' ' +
            data.url
        );
        // handleLevelError demonstrates how to remove a level that errors followed by a downswitch
        // handleLevelError(data);
        break;
      case Hls.ErrorDetails.LEVEL_LOAD_TIMEOUT:
        logError(
          'Timeout while loading level playlist ' +
            data.context.level +
            ' ' +
            data.url
        );
        // handleLevelError demonstrates how to remove a level that errors followed by a downswitch
        // handleLevelError(data);
        break;
      case Hls.ErrorDetails.LEVEL_SWITCH_ERROR:
        logError('Error while trying to switch to level ' + data.level);
        break;
      case Hls.ErrorDetails.FRAG_LOAD_ERROR:
        logError('Error while loading fragment ' + data.frag.url);
        break;
      case Hls.ErrorDetails.FRAG_LOAD_TIMEOUT:
        logError('Timeout while loading fragment ' + data.frag.url);
        break;
      case Hls.ErrorDetails.FRAG_LOOP_LOADING_ERROR:
        logError('Fragment-loop loading error');
        break;
      case Hls.ErrorDetails.FRAG_DECRYPT_ERROR:
        logError('Decrypting error:' + data.reason);
        break;
      case Hls.ErrorDetails.FRAG_PARSING_ERROR:
        logError('Parsing error:' + data.reason);
        break;
      case Hls.ErrorDetails.KEY_LOAD_ERROR:
        logError('Error while loading key ' + data.frag.decryptdata.uri);
        break;
      case Hls.ErrorDetails.KEY_LOAD_TIMEOUT:
        logError('Timeout while loading key ' + data.frag.decryptdata.uri);
        break;
      case Hls.ErrorDetails.BUFFER_APPEND_ERROR:
        logError('Buffer append error');
        break;
      case Hls.ErrorDetails.BUFFER_ADD_CODEC_ERROR:
        logError(
          'Buffer add codec error for ' +
            data.mimeType +
            ':' +
            data.error.message
        );
        break;
      case Hls.ErrorDetails.BUFFER_APPENDING_ERROR:
        logError('Buffer appending error');
        break;
      case Hls.ErrorDetails.BUFFER_STALLED_ERROR:
        logError('Buffer stalled error');
        if (stopOnStall) {
          hls.stopLoad();
          video.pause();
        }
        break;
      default:
        break;
    }

    if (data.fatal) {
      console.error(`Fatal error : ${data.details}`);
      switch (data.type) {
        case Hls.ErrorTypes.MEDIA_ERROR:
          logError(`A media error occurred: ${data.details}`);
          handleMediaError();
          break;
        case Hls.ErrorTypes.NETWORK_ERROR:
          logError(`A network error occurred: ${data.details}`);
          break;
        default:
          logError(`An unrecoverable error occurred: ${data.details}`);
          hls.destroy();
          break;
      }
    }
    if (!stats) {
      stats = {};
    }

    // track all errors independently
    if (stats[data.details] === undefined) {
      stats[data.details] = 1;
    } else {
      stats[data.details] += 1;
    }

    // track fatal error
    if (data.fatal) {
      if (stats.fatalError === undefined) {
        stats.fatalError = 1;
      } else {
        stats.fatalError += 1;
      }
    }
    $('#statisticsOut').text(JSON.stringify(sortObject(stats), null, '\t'));
  });

  hls.on(Hls.Events.BUFFER_CREATED, function (eventName, data) {
    tracks = data.tracks;
  });

  hls.on(Hls.Events.BUFFER_APPENDING, function (eventName, data) {
    if (dumpfMP4) {
      fmp4Data[data.type].push(data.data);
    }
  });

  hls.on(Hls.Events.FPS_DROP, function (eventName, data) {
    const event = {
      time: self.performance.now() - events.t0,
      type: 'frame drop',
      name: data.currentDropped + '/' + data.currentDecoded,
    };
    events.video.push(event);
    trimEventHistory();
    if (stats) {
      if (stats.fpsDropEvent === undefined) {
        stats.fpsDropEvent = 1;
      } else {
        stats.fpsDropEvent++;
      }

      stats.fpsTotalDroppedFrames = data.totalDroppedFrames;
    }
  });
}

function addVideoEventListeners(video) {
  video.removeEventListener('resize', handleVideoEvent);
  video.removeEventListener('seeking', handleVideoEvent);
  video.removeEventListener('seeked', handleVideoEvent);
  video.removeEventListener('pause', handleVideoEvent);
  video.removeEventListener('play', handleVideoEvent);
  video.removeEventListener('canplay', handleVideoEvent);
  video.removeEventListener('canplaythrough', handleVideoEvent);
  video.removeEventListener('ended', handleVideoEvent);
  video.removeEventListener('playing', handleVideoEvent);
  video.removeEventListener('error', handleVideoEvent);
  video.removeEventListener('loadedmetadata', handleVideoEvent);
  video.removeEventListener('loadeddata', handleVideoEvent);
  video.removeEventListener('durationchange', handleVideoEvent);
  video.removeEventListener('volumechange', handleVolumeEvent);
  video.addEventListener('resize', handleVideoEvent);
  video.addEventListener('seeking', handleVideoEvent);
  video.addEventListener('seeked', handleVideoEvent);
  video.addEventListener('pause', handleVideoEvent);
  video.addEventListener('play', handleVideoEvent);
  video.addEventListener('canplay', handleVideoEvent);
  video.addEventListener('canplaythrough', handleVideoEvent);
  video.addEventListener('ended', handleVideoEvent);
  video.addEventListener('playing', handleVideoEvent);
  video.addEventListener('error', handleVideoEvent);
  video.addEventListener('loadedmetadata', handleVideoEvent);
  video.addEventListener('loadeddata', handleVideoEvent);
  video.addEventListener('durationchange', handleVideoEvent);
  video.addEventListener('volumechange', handleVolumeEvent);
}

function handleUnsupported() {
  if (navigator.userAgent.toLowerCase().indexOf('firefox') !== -1) {
    logStatus(
      'You are using Firefox, it looks like MediaSource is not enabled,<br>please ensure the following keys are set appropriately in <b>about:config</b><br>media.mediasource.enabled=true<br>media.mediasource.mp4.enabled=true<br><b>media.mediasource.whitelist=false</b>'
    );
  } else {
    logStatus(
      'Your Browser does not support MediaSourceExtension / MP4 mediasource'
    );
  }
}

function handleVideoEvent(evt) {
  let data = '';
  switch (evt.type) {
    case 'durationchange':
      if (evt.target.duration - lastDuration <= 0.5) {
        // some browsers report several duration change events with almost the same value ... avoid spamming video events
        return;
      }
      lastDuration = evt.target.duration;
      data = Math.round(evt.target.duration * 1000);
      break;
    case 'resize':
      data = evt.target.videoWidth + '/' + evt.target.videoHeight;
      playerResize();
      break;
    case 'loadedmetadata':
    case 'loadeddata':
    case 'canplay':
    case 'canplaythrough':
    case 'ended':
    case 'seeking':
    case 'seeked':
    case 'play':
    case 'playing':
      lastStartPosition = evt.target.currentTime;
    case 'pause':
    case 'waiting':
    case 'stalled':
    case 'error':
      data = Math.round(evt.target.currentTime * 1000);
      if (evt.type === 'error') {
        let errorTxt;
        const mediaError = evt.currentTarget.error;
        switch (mediaError.code) {
          case mediaError.MEDIA_ERR_ABORTED:
            errorTxt = 'You aborted the video playback';
            break;
          case mediaError.MEDIA_ERR_DECODE:
            errorTxt =
              'The video playback was aborted due to a corruption problem or because the video used features your browser did not support';
            handleMediaError();
            break;
          case mediaError.MEDIA_ERR_NETWORK:
            errorTxt =
              'A network error caused the video download to fail part-way';
            break;
          case mediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
            errorTxt =
              'The video could not be loaded, either because the server or network failed or because the format is not supported';
            break;
        }

        if (mediaError.message) {
          errorTxt += ' - ' + mediaError.message;
        }

        logStatus(errorTxt);
        console.error(errorTxt);
      }
      break;
    default:
      break;
  }

  const event = {
    time: self.performance.now() - events.t0,
    type: evt.type,
    name: data,
  };

  events.video.push(event);
  if (evt.type === 'seeking') {
    lastSeekingIdx = events.video.length - 1;
  }

  if (evt.type === 'seeked') {
    events.video[lastSeekingIdx].duration =
      event.time - events.video[lastSeekingIdx].time;
  }

  trimEventHistory();
}

function handleVolumeEvent() {
  localStorage.setItem(
    STORAGE_KEYS.volume,
    JSON.stringify({
      muted: video.muted,
      volume: video.volume,
    })
  );
}

function handleLevelError(data) {
  var levelObj = data.context || data;
  hls.removeLevel(levelObj.level, levelObj.urlId || 0);
  if (!hls.levels.length) {
    logError('All levels have been removed');
    hls.destroy();
    return;
  }
  // Trigger an immediate downswitch to the first level
  // This is to handle the case where we start at an empty level, where switching to auto causes hlsjs to stall
  hls.currentLevel = 0;
  // Set the quality back to auto so that we return to optimal quality
  hls.currentLevel = -1;
}

function handleMediaError() {
  if (autoRecoverError) {
    const now = self.performance.now();
    if (
      !self.recoverDecodingErrorDate ||
      now - self.recoverDecodingErrorDate > 3000
    ) {
      self.recoverDecodingErrorDate = self.performance.now();
      $('#statusOut').append(', trying to recover media error.');
      hls.recoverMediaError();
    } else {
      if (
        !self.recoverSwapAudioCodecDate ||
        now - self.recoverSwapAudioCodecDate > 3000
      ) {
        self.recoverSwapAudioCodecDate = self.performance.now();
        $('#statusOut').append(
          ', trying to swap audio codec and recover media error.'
        );
        hls.swapAudioCodec();
        hls.recoverMediaError();
      } else {
        $('#statusOut').append(
          ', cannot recover. Last media error recovery failed.'
        );
      }
    }
  }
}

function timeRangesToString(r) {
  let log = '';
  for (let i = 0; i < r.length; i++) {
    log += '[' + r.start(i) + ', ' + r.end(i) + ']';
    log += ' ';
  }
  return log;
}

function checkBuffer() {
  const canvas = document.querySelector('#bufferedCanvas');
  const ctx = canvas.getContext('2d');
  const r = video.buffered;
  const seekableEnd = getSeekableEnd();
  let bufferingDuration;
  if (r) {
    ctx.fillStyle = 'black';
    if (!canvas.width || canvas.width !== video.clientWidth) {
      canvas.width = video.clientWidth;
    }
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    const pos = video.currentTime;
    let bufferLen = 0;
    ctx.fillStyle = 'gray';
    for (let i = 0; i < r.length; i++) {
      const start = (r.start(i) / seekableEnd) * canvas.width;
      const end = (r.end(i) / seekableEnd) * canvas.width;
      ctx.fillRect(start, 2, Math.max(2, end - start), 11);
      if (pos >= r.start(i) && pos < r.end(i)) {
        // play position is inside this buffer TimeRange, retrieve end of buffer position and buffer length
        bufferLen = r.end(i) - pos;
      }
    }
    // check if we are in buffering / or playback ended state
    if (
      bufferLen <= 0.1 &&
      video.paused === false &&
      pos - lastStartPosition > 0.5
    ) {
      if (lastDuration - pos <= 0.5 && events.isLive === false) {
        // don't create buffering event if we are at the end of the playlist, don't report ended for live playlist
      } else {
        // we are not at the end of the playlist ... real buffering
        if (bufferingIdx !== -1) {
          bufferingDuration =
            self.performance.now() -
            events.t0 -
            events.video[bufferingIdx].time;
          events.video[bufferingIdx].duration = bufferingDuration;
          events.video[bufferingIdx].name = bufferingDuration;
        } else {
          events.video.push({
            type: 'buffering',
            time: self.performance.now() - events.t0,
          });
          trimEventHistory();
          // we are in buffering state
          bufferingIdx = events.video.length - 1;
        }
      }
    }

    if (bufferLen > 0.1 && bufferingIdx !== -1) {
      bufferingDuration =
        self.performance.now() - events.t0 - events.video[bufferingIdx].time;
      events.video[bufferingIdx].duration = bufferingDuration;
      events.video[bufferingIdx].name = bufferingDuration;
      // we are out of buffering state
      bufferingIdx = -1;
    }

    // update buffer/position for current Time
    const event = {
      time: self.performance.now() - events.t0,
      buffer: Math.round(bufferLen * 1000),
      pos: Math.round(pos * 1000),
    };
    const bufEvents = events.buffer;
    const bufEventLen = bufEvents.length;
    if (bufEventLen > 1) {
      const event0 = bufEvents[bufEventLen - 2];
      const event1 = bufEvents[bufEventLen - 1];
      const slopeBuf0 =
        (event0.buffer - event1.buffer) / (event0.time - event1.time);
      const slopeBuf1 =
        (event1.buffer - event.buffer) / (event1.time - event.time);

      const slopePos0 = (event0.pos - event1.pos) / (event0.time - event1.time);
      const slopePos1 = (event1.pos - event.pos) / (event1.time - event.time);
      // compute slopes. if less than 30% difference, remove event1
      if (
        (slopeBuf0 === slopeBuf1 ||
          Math.abs(slopeBuf0 / slopeBuf1 - 1) <= 0.3) &&
        (slopePos0 === slopePos1 || Math.abs(slopePos0 / slopePos1 - 1) <= 0.3)
      ) {
        bufEvents.pop();
      }
    }
    events.buffer.push(event);
    trimEventHistory();
    self.refreshCanvas();

    if ($('#statsDisplayTab').is(':visible')) {
      let log = `Duration: ${video.duration}\nBuffered: ${timeRangesToString(
        video.buffered
      )}\nSeekable: ${timeRangesToString(
        video.seekable
      )}\nPlayed: ${timeRangesToString(video.played)}\n`;
      if (hls.media) {
        for (const type in tracks) {
          log += `Buffer for ${type} contains:${timeRangesToString(
            tracks[type].buffer.buffered
          )}\n`;
        }
        const videoPlaybackQuality = video.getVideoPlaybackQuality;
        if (
          videoPlaybackQuality &&
          typeof videoPlaybackQuality === typeof Function
        ) {
          log += `Dropped frames: ${
            video.getVideoPlaybackQuality().droppedVideoFrames
          }\n`;
          log += `Corrupted frames: ${
            video.getVideoPlaybackQuality().corruptedVideoFrames
          }\n`;
        } else if (video.webkitDroppedFrameCount) {
          log += `Dropped frames: ${video.webkitDroppedFrameCount}\n`;
        }
      }
      log += `Bandwidth Estimate: ${hls.bandwidthEstimate.toFixed(3)}\n`;
      if (events.isLive) {
        log +=
          'Live Stats:\n' +
          `  Max Latency: ${hls.maxLatency}\n` +
          `  Target Latency: ${hls.targetLatency.toFixed(3)}\n` +
          `  Latency: ${hls.latency.toFixed(3)}\n` +
          `  Drift: ${hls.drift.toFixed(3)} (edge advance rate)\n` +
          `  Edge Stall: ${hls.latencyController.edgeStalled.toFixed(
            3
          )} (playlist refresh over target duration/part)\n` +
          `  Playback rate: ${video.playbackRate.toFixed(2)}\n`;
        if (stats.fragProgramDateTime) {
          const currentPDT =
            stats.fragProgramDateTime +
            (video.currentTime - stats.fragStart) * 1000;
          log += `  Program Date Time: ${new Date(currentPDT).toISOString()}`;
          const pdtLatency = (Date.now() - currentPDT) / 1000;
          if (pdtLatency > 0) {
            log += ` (${pdtLatency.toFixed(3)} seconds ago)`;
          }
        }
      }

      $('#bufferedOut').text(log);
      $('#statisticsOut').text(JSON.stringify(sortObject(stats), null, '\t'));
    }

    ctx.fillStyle = 'blue';
    const x = (video.currentTime / seekableEnd) * canvas.width;
    ctx.fillRect(x, 0, 2, 15);
  } else if (ctx.fillStyle !== 'black') {
    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
  }
}

function showCanvas() {
  self.showMetrics();
  $('#bufferedOut').show();
  $('#bufferedCanvas').show();
}

function hideCanvas() {
  self.hideMetrics();
  $('#bufferedOut').hide();
  $('#bufferedCanvas').hide();
}

function getMetrics() {
  const json = JSON.stringify(events);
  const jsonpacked = pack(json);
  // console.log('packing JSON from ' + json.length + ' to ' + jsonpacked.length + ' bytes');
  return btoa(jsonpacked);
}

self.copyMetricsToClipBoard = function () {
  copyTextToClipboard(getMetrics());
};

self.goToMetrics = function () {
  let url = document.URL;
  url = url.slice(0, url.lastIndexOf('/') + 1) + 'metrics.html';
  self.open(url, '_blank');
};

function goToMetricsPermaLink() {
  let url = document.URL;
  const b64 = getMetrics();
  url = url.slice(0, url.lastIndexOf('/') + 1) + 'metrics.html#data=' + b64;
  self.open(url, '_blank');
}

function onClickBufferedRange(event) {
  const canvas = document.querySelector('#bufferedCanvas');
  const target =
    ((event.clientX - canvas.offsetLeft) / canvas.width) * getSeekableEnd();
  video.currentTime = target;
}

function getSeekableEnd() {
  if (isFinite(video.duration)) {
    return video.duration;
  }
  if (video.seekable.length) {
    return video.seekable.end(video.seekable.length - 1);
  }
  return 0;
}

function getLevelButtonHtml(key, levels, onclickReplace, autoEnabled) {
  const onclickAuto = `${key}=-1`.replace(/^(\w+)=([^=]+)$/, onclickReplace);
  const codecs = levels.reduce((uniqueCodecs, level) => {
    const levelCodecs = codecs2label(level.attrs.CODECS);
    if (levelCodecs && uniqueCodecs.indexOf(levelCodecs) === -1) {
      uniqueCodecs.push(levelCodecs);
    }
    return uniqueCodecs;
  }, []);
  return (
    `<button type="button" class="btn btn-sm ${
      autoEnabled ? 'btn-primary' : 'btn-success'
    }" onclick="${onclickAuto}">auto</button>` +
    levels
      .map((level, i) => {
        const enabled = hls[key] === i;
        const onclick = `${key}=${i}`.replace(/^(\w+)=(\w+)$/, onclickReplace);
        const label = level2label(levels[i], i, codecs);
        return `<button type="button" class="btn btn-sm ${
          enabled ? 'btn-primary' : 'btn-success'
        }" onclick="${onclick}">${label}</button>`;
      })
      .join('')
  );
}

function updateLevelInfo() {
  const levels = hls.levels;
  if (!levels) {
    return;
  }

  const htmlCurrentLevel = getLevelButtonHtml(
    'currentLevel',
    levels,
    'hls.$1=$2',
    hls.autoLevelEnabled
  );
  const htmlNextLevel = getLevelButtonHtml(
    'nextLevel',
    levels,
    'hls.$1=$2',
    hls.autoLevelEnabled
  );
  const htmlLoadLevel = getLevelButtonHtml(
    'loadLevel',
    levels,
    'hls.$1=$2',
    hls.autoLevelEnabled
  );
  const htmlCapLevel = getLevelButtonHtml(
    'autoLevelCapping',
    levels,
    'levelCapping=hls.$1=$2;updateLevelInfo();onDemoConfigChanged();',
    hls.autoLevelCapping === -1
  );

  if ($('#currentLevelControl').html() !== htmlCurrentLevel) {
    $('#currentLevelControl').html(htmlCurrentLevel);
  }

  if ($('#nextLevelControl').html() !== htmlNextLevel) {
    $('#nextLevelControl').html(htmlNextLevel);
  }

  if ($('#loadLevelControl').html() !== htmlLoadLevel) {
    $('#loadLevelControl').html(htmlLoadLevel);
  }

  if ($('#levelCappingControl').html() !== htmlCapLevel) {
    $('#levelCappingControl').html(htmlCapLevel);
  }
}

function updateAudioTrackInfo() {
  const buttonTemplate = '<button type="button" class="btn btn-sm ';
  const buttonEnabled = 'btn-primary" ';
  const buttonDisabled = 'btn-success" ';
  let html1 = '';
  const audioTrackId = hls.audioTrack;
  const len = hls.audioTracks.length;
  const track = hls.audioTracks[audioTrackId];

  for (let i = 0; i < len; i++) {
    html1 += buttonTemplate;
    if (audioTrackId === i) {
      html1 += buttonEnabled;
    } else {
      html1 += buttonDisabled;
    }

    html1 +=
      'onclick="hls.audioTrack=' +
      i +
      '">' +
      hls.audioTracks[i].name +
      '</button>';
  }

  $('#audioTrackLabel').text(
    track ? track.lang || track.name : 'None selected'
  );
  $('#audioTrackControl').html(html1);
}

function codecs2label(levelCodecs) {
  if (levelCodecs) {
    return levelCodecs
      .replace(/([ah]vc.)[^,;]+/, '$1')
      .replace('mp4a.40.2', 'mp4a');
  }
  return '';
}

function level2label(level, i, manifestCodecs) {
  const levelCodecs = codecs2label(level.attrs.CODECS);
  const levelNameInfo = level.name ? `"${level.name}": ` : '';
  const codecInfo =
    levelCodecs && manifestCodecs.length > 1 ? ` / ${levelCodecs}` : '';
  if (level.height) {
    return `${i} (${levelNameInfo}${level.height}p / ${Math.round(
      level.bitrate / 1024
    )}kb${codecInfo})`;
  }
  if (level.bitrate) {
    return `${i} (${levelNameInfo}${Math.round(
      level.bitrate / 1024
    )}kb${codecInfo})`;
  }
  if (codecInfo) {
    return `${i} (${levelNameInfo}${levelCodecs})`;
  }
  if (level.name) {
    return `${i} (${level.name})`;
  }
  return `${i}`;
}

function getDemoConfigPropOrDefault(propName, defaultVal) {
  return typeof demoConfig[propName] !== 'undefined'
    ? demoConfig[propName]
    : defaultVal;
}

function getURLParam(sParam, defaultValue) {
  const sPageURL = self.location.search.substring(1);
  const sURLVariables = sPageURL.split('&');
  for (let i = 0; i < sURLVariables.length; i++) {
    const sParameterName = sURLVariables[i].split('=');
    if (sParameterName[0] === sParam) {
      return sParameterName[1] === 'undefined'
        ? undefined
        : sParameterName[1] === 'false'
        ? false
        : sParameterName[1];
    }
  }
  return defaultValue;
}

function onDemoConfigChanged(firstLoad) {
  demoConfig = {
    enableStreaming,
    autoRecoverError,
    stopOnStall,
    dumpfMP4,
    levelCapping,
    limitMetrics,
  };

  if (configPersistenceEnabled) {
    persistEditorValue();
  }

  const serializedDemoConfig = btoa(JSON.stringify(demoConfig));
  const baseURL = document.URL.split('?')[0];
  const streamURL = $('#streamURL').val();
  const permalinkURL = `${baseURL}?src=${encodeURIComponent(
    streamURL
  )}&demoConfig=${serializedDemoConfig}`;

  $('#StreamPermalink').html(`<a href="${permalinkURL}">${permalinkURL}</a>`);
  if (!firstLoad && window.location.href !== permalinkURL) {
    window.history.pushState(null, null, permalinkURL);
  }
}

function onConfigPersistenceChanged(event) {
  configPersistenceEnabled = event.target.checked;
  localStorage.setItem(
    STORAGE_KEYS.Editor_Persistence,
    JSON.stringify(configPersistenceEnabled)
  );

  if (configPersistenceEnabled) {
    persistEditorValue();
  } else {
    localStorage.removeItem(STORAGE_KEYS.Hls_Config);
  }
}

function getEditorValue(options) {
  options = $.extend({ parse: false }, options || {});
  let value = configEditor.session.getValue();

  if (options.parse) {
    try {
      value = JSON.parse(value);
    } catch (e) {
      console.warn('[getEditorValue] could not parse editor value', e);
      value = {};
    }
  }

  return value;
}

function getPersistedHlsConfig() {
  let value = localStorage.getItem(STORAGE_KEYS.Hls_Config);
  if (value === null) {
    return value;
  }

  try {
    value = JSON.parse(value);
  } catch (e) {
    console.warn('[getPersistedHlsConfig] could not hls config json', e);
    value = {};
  }

  return value;
}

function persistEditorValue() {
  localStorage.setItem(STORAGE_KEYS.Hls_Config, getEditorValue());
}

function setupConfigEditor() {
  configEditor = self.ace.edit('config-editor');
  configEditor.setTheme('ace/theme/github');
  configEditor.session.setMode('ace/mode/json');

  const contents = hlsjsDefaults;
  const shouldRestorePersisted =
    JSON.parse(localStorage.getItem(STORAGE_KEYS.Editor_Persistence)) === true;

  if (shouldRestorePersisted) {
    $.extend(contents, getPersistedHlsConfig());
  }

  const elPersistence = document.querySelector('#config-persistence');
  elPersistence.addEventListener('change', onConfigPersistenceChanged);
  elPersistence.checked = shouldRestorePersisted;
  configPersistenceEnabled = shouldRestorePersisted;

  updateConfigEditorValue(contents);
}

function setupTimelineChart() {
  const canvas = document.querySelector('#timeline-chart');
  const chart = new TimelineChart(canvas, {
    responsive: false,
  });

  resizeHandlers.push(() => {
    chart.resize();
  });

  chart.resize();

  return chart;
}

function addChartEventListeners(hls) {
  const updateLevelOrTrack = (eventName, data) => {
    chart.updateLevelOrTrack(data.details);
  };
  const updateFragment = (eventName, data) => {
    if (data.stats) {
      // Convert 0.x stats to partial v1 stats
      const { retry, loaded, total, trequest, tfirst, tload } = data.stats;
      if (trequest && tload) {
        data.frag.stats = {
          loaded,
          retry,
          total,
          loading: {
            start: trequest,
            first: tfirst,
            end: tload,
          },
        };
      }
    }
    chart.updateFragment(data);
  };
  const updateChart = () => {
    chart.update();
  };
  hls.on(
    Hls.Events.MANIFEST_LOADING,
    () => {
      chart.reset();
    },
    chart
  );
  hls.on(
    Hls.Events.MANIFEST_PARSED,
    (eventName, data) => {
      const { levels } = data;
      chart.removeType('level');
      chart.removeType('audioTrack');
      chart.removeType('subtitleTrack');
      chart.updateLevels(levels);
    },
    chart
  );
  hls.on(
    Hls.Events.BUFFER_CREATED,
    (eventName, { tracks }) => {
      chart.updateSourceBuffers(tracks, hls.media);
    },
    chart
  );
  hls.on(
    Hls.Events.BUFFER_RESET,
    () => {
      chart.removeSourceBuffers();
    },
    chart
  );
  hls.on(Hls.Events.LEVELS_UPDATED, (eventName, { levels }) => {
    chart.removeType('level');
    chart.updateLevels(levels);
  });
  hls.on(
    Hls.Events.LEVEL_SWITCHED,
    (eventName, { level }) => {
      chart.removeType('level');
      chart.updateLevels(hls.levels, level);
    },
    chart
  );
  hls.on(
    Hls.Events.LEVEL_LOADING,
    () => {
      // TODO: mutate level datasets
      // Update loadLevel
      chart.removeType('level');
      chart.updateLevels(hls.levels);
    },
    chart
  );
  hls.on(
    Hls.Events.LEVEL_UPDATED,
    (eventName, { details }) => {
      chart.updateLevelOrTrack(details);
    },
    chart
  );

  hls.on(
    Hls.Events.AUDIO_TRACKS_UPDATED,
    (eventName, { audioTracks }) => {
      chart.removeType('audioTrack');
      chart.updateAudioTracks(audioTracks);
    },
    chart
  );
  hls.on(
    Hls.Events.SUBTITLE_TRACKS_UPDATED,
    (eventName, { subtitleTracks }) => {
      chart.removeType('subtitleTrack');
      chart.updateSubtitleTracks(subtitleTracks);
    },
    chart
  );

  hls.on(
    Hls.Events.AUDIO_TRACK_SWITCHED,
    (eventName) => {
      // TODO: mutate level datasets
      chart.removeType('audioTrack');
      chart.updateAudioTracks(hls.audioTracks);
    },
    chart
  );
  hls.on(
    Hls.Events.SUBTITLE_TRACK_SWITCH,
    (eventName) => {
      // TODO: mutate level datasets
      chart.removeType('subtitleTrack');
      chart.updateSubtitleTracks(hls.subtitleTracks);
    },
    chart
  );
  hls.on(Hls.Events.AUDIO_TRACK_LOADED, updateLevelOrTrack, chart);
  hls.on(Hls.Events.SUBTITLE_TRACK_LOADED, updateLevelOrTrack, chart);
  hls.on(Hls.Events.LEVEL_PTS_UPDATED, updateLevelOrTrack, chart);
  hls.on(Hls.Events.FRAG_LOADED, updateFragment, chart);
  hls.on(Hls.Events.FRAG_PARSED, updateFragment, chart);
  hls.on(Hls.Events.FRAG_CHANGED, updateFragment, chart);
  hls.on(Hls.Events.BUFFER_APPENDING, updateChart, chart);
  hls.on(Hls.Events.BUFFER_APPENDED, updateChart, chart);
  hls.on(Hls.Events.BUFFER_FLUSHED, updateChart, chart);
}

function updateConfigEditorValue(obj) {
  const json = JSON.stringify(obj, null, 2);
  configEditor.session.setValue(json);
}

function applyConfigEditorValue() {
  onDemoConfigChanged();
  loadSelectedStream();
}

function createfMP4(type) {
  if (fmp4Data[type].length) {
    const blob = new Blob([arrayConcat(fmp4Data[type])], {
      type: 'application/octet-stream',
    });
    const filename = type + '-' + new Date().toISOString() + '.mp4';
    self.saveAs(blob, filename);
    // $('body').append('<a download="hlsjs-' + filename + '" href="' + self.URL.createObjectURL(blob) + '">Download ' + filename + ' track</a><br>');
  } else if (!dumpfMP4) {
    console.error(
      'Check "Dump transmuxed fMP4 data" first to make appended media available for saving.'
    );
  }
}

function arrayConcat(inputArray) {
  const totalLength = inputArray.reduce(function (prev, cur) {
    return prev + cur.length;
  }, 0);
  const result = new Uint8Array(totalLength);
  let offset = 0;
  inputArray.forEach(function (element) {
    result.set(element, offset);
    offset += element.length;
  });
  return result;
}

function hideAllTabs() {
  $('.demo-tab-btn').css('background-color', '');
  $('.demo-tab').hide();
}

function toggleTabClick(btn) {
  toggleTab(btn);
  const tabIndexes = $('.demo-tab-btn')
    .toArray()
    .map((el, i) => ($('#' + $(el).data('tab')).is(':visible') ? i : null))
    .filter((i) => i !== null);
  localStorage.setItem(STORAGE_KEYS.demo_tabs, tabIndexes.join(','));
}

function toggleTab(btn, dontHideOpenTabs) {
  const tabElId = $(btn).data('tab');
  // eslint-disable-next-line no-restricted-globals
  const modifierPressed =
    dontHideOpenTabs ||
    (self.event && (self.event.metaKey || self.event.shiftKey));
  if (!modifierPressed) {
    hideAllTabs();
  }
  if (modifierPressed) {
    $(`#${tabElId}`).toggle();
  } else {
    $(`#${tabElId}`).show();
  }
  $(btn).css(
    'background-color',
    $(`#${tabElId}`).is(':visible') ? 'orange' : ''
  );
  if (!$('#statsDisplayTab').is(':visible')) {
    self.hideMetrics();
  }
  if (hls) {
    if ($('#timelineTab').is(':visible')) {
      chart.show();
      chart.resize(chart.chart.data ? chart.chart.data.datasets : null);
    } else {
      chart.hide();
    }
  }
}

function appendLog(textElId, message) {
  const el = $('#' + textElId);
  let logText = el.text();
  if (logText.length) {
    logText += '\n';
  }
  const timestamp = (Date.now() - startTime) / 1000;
  const newMessage = timestamp + ' | ' + message;
  logText += newMessage;
  // update
  el.text(logText);
  const element = el[0];
  element.scrollTop = element.scrollHeight - element.clientHeight;
}

function logStatus(message) {
  appendLog('statusOut', message);
}

function logError(message) {
  appendLog('errorOut', message);
}
