/* 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 = $('')
.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 ' +
url +
'
HTTP response code:' +
data.response.code +
'
' +
data.response.text
);
if (data.response.code === 0) {
$('#errorOut').append(
'This might be a CORS issue, consider installing Allow-Control-Allow-Origin Chrome Extension'
);
}
} catch (err) {
$('#errorOut').html(
'Cannot load ' +
url +
'
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,
please ensure the following keys are set appropriately in about:config
media.mediasource.enabled=true
media.mediasource.mp4.enabled=true
media.mediasource.whitelist=false'
);
} 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 (
`` +
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 ``;
})
.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 = '';
}
$('#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(`${permalinkURL}`);
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('Download ' + filename + ' track
');
} 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);
}