main.js 53 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816
  1. /* global $, Hls, __NETLIFY__ */
  2. /* eslint camelcase: 0 */
  3. import { pack } from 'jsonpack';
  4. import 'promise-polyfill/src/polyfill';
  5. import { sortObject, copyTextToClipboard } from './demo-utils';
  6. import { TimelineChart } from './chart/timeline-chart';
  7. const NETLIFY = __NETLIFY__; // replaced in build
  8. const STORAGE_KEYS = {
  9. Editor_Persistence: 'hlsjs:config-editor-persist',
  10. Hls_Config: 'hlsjs:config',
  11. volume: 'hlsjs:volume',
  12. demo_tabs: 'hlsjs:demo-tabs',
  13. };
  14. const testStreams = require('../tests/test-streams');
  15. const defaultTestStreamUrl = testStreams[Object.keys(testStreams)[0]].url;
  16. const sourceURL = decodeURIComponent(getURLParam('src', defaultTestStreamUrl));
  17. let demoConfig = getURLParam('demoConfig', null);
  18. if (demoConfig) {
  19. demoConfig = JSON.parse(atob(demoConfig));
  20. } else {
  21. demoConfig = {};
  22. }
  23. const hlsjsDefaults = {
  24. debug: true,
  25. enableWorker: true,
  26. lowLatencyMode: true,
  27. backBufferLength: 60 * 1.5,
  28. };
  29. let enableStreaming = getDemoConfigPropOrDefault('enableStreaming', true);
  30. let autoRecoverError = getDemoConfigPropOrDefault('autoRecoverError', true);
  31. let levelCapping = getDemoConfigPropOrDefault('levelCapping', -1);
  32. let limitMetrics = getDemoConfigPropOrDefault('limitMetrics', -1);
  33. let dumpfMP4 = getDemoConfigPropOrDefault('dumpfMP4', false);
  34. let stopOnStall = getDemoConfigPropOrDefault('stopOnStall', false);
  35. let bufferingIdx = -1;
  36. let selectedTestStream = null;
  37. let video = document.querySelector('#video');
  38. const startTime = Date.now();
  39. let lastSeekingIdx;
  40. let lastStartPosition;
  41. let lastDuration;
  42. let lastAudioTrackSwitchingIdx;
  43. let hls;
  44. let url;
  45. let events;
  46. let stats;
  47. let tracks;
  48. let fmp4Data;
  49. let configPersistenceEnabled = false;
  50. let configEditor = null;
  51. let chart;
  52. let resizeAsyncCallbackId = -1;
  53. const requestAnimationFrame = self.requestAnimationFrame || self.setTimeout;
  54. const cancelAnimationFrame = self.cancelAnimationFrame || self.clearTimeout;
  55. const resizeHandlers = [];
  56. const resize = () => {
  57. cancelAnimationFrame(resizeAsyncCallbackId);
  58. resizeAsyncCallbackId = requestAnimationFrame(() => {
  59. resizeHandlers.forEach((handler) => {
  60. handler();
  61. });
  62. });
  63. };
  64. self.onresize = resize;
  65. if (self.screen && self.screen.orientation) {
  66. self.screen.orientation.onchange = resize;
  67. }
  68. const playerResize = () => {
  69. const bounds = video.getBoundingClientRect();
  70. $('#currentSize').html(
  71. `${Math.round(bounds.width * 10) / 10} x ${
  72. Math.round(bounds.height * 10) / 10
  73. }`
  74. );
  75. if (video.videoWidth && video.videoHeight) {
  76. $('#currentResolution').html(`${video.videoWidth} x ${video.videoHeight}`);
  77. }
  78. };
  79. resizeHandlers.push(playerResize);
  80. $(document).ready(function () {
  81. setupConfigEditor();
  82. chart = setupTimelineChart();
  83. Object.keys(testStreams).forEach((key, index) => {
  84. const stream = testStreams[key];
  85. const option = new Option(stream.description, key);
  86. $('#streamSelect').append(option);
  87. if (stream.url === sourceURL) {
  88. document.querySelector('#streamSelect').selectedIndex = index + 1;
  89. }
  90. });
  91. const videoWidth = video.style.width;
  92. if (videoWidth) {
  93. $('#videoSize option').each(function (i, option) {
  94. if (option.value === videoWidth) {
  95. document.querySelector('#videoSize').selectedIndex = i;
  96. $('#bufferedCanvas').width(videoWidth);
  97. resize();
  98. return false;
  99. }
  100. });
  101. }
  102. $('#streamSelect').change(function () {
  103. const key = $('#streamSelect').val() || Object.keys(testStreams)[0];
  104. selectedTestStream = testStreams[key];
  105. const streamUrl = selectedTestStream.url;
  106. $('#streamURL').val(streamUrl);
  107. loadSelectedStream();
  108. });
  109. $('#streamURL').change(function () {
  110. selectedTestStream = null;
  111. loadSelectedStream();
  112. });
  113. $('#videoSize').change(function () {
  114. $('#video').width($('#videoSize').val());
  115. $('#bufferedCanvas').width($('#videoSize').val());
  116. checkBuffer();
  117. resize();
  118. });
  119. $('#enableStreaming').click(function () {
  120. enableStreaming = this.checked;
  121. loadSelectedStream();
  122. });
  123. $('#autoRecoverError').click(function () {
  124. autoRecoverError = this.checked;
  125. onDemoConfigChanged();
  126. });
  127. $('#stopOnStall').click(function () {
  128. stopOnStall = this.checked;
  129. onDemoConfigChanged();
  130. });
  131. $('#dumpfMP4').click(function () {
  132. dumpfMP4 = this.checked;
  133. $('.btn-dump').toggle(dumpfMP4);
  134. onDemoConfigChanged();
  135. });
  136. $('#limitMetrics').change(function () {
  137. limitMetrics = this.value;
  138. onDemoConfigChanged();
  139. });
  140. $('#levelCapping').change(function () {
  141. levelCapping = this.value;
  142. onDemoConfigChanged();
  143. });
  144. $('#limitMetrics').val(limitMetrics);
  145. $('#enableStreaming').prop('checked', enableStreaming);
  146. $('#autoRecoverError').prop('checked', autoRecoverError);
  147. $('#stopOnStall').prop('checked', stopOnStall);
  148. $('#dumpfMP4').prop('checked', dumpfMP4);
  149. $('#levelCapping').val(levelCapping);
  150. // link to version on npm if canary
  151. // github branch for a branch version
  152. // github tag for a normal tag
  153. // github PR for a pr
  154. function getVersionLink(version) {
  155. const alphaRegex = /[-.]0\.alpha\./;
  156. if (alphaRegex.test(version)) {
  157. return `https://www.npmjs.com/package/hls.js/v/${encodeURIComponent(
  158. version
  159. )}`;
  160. } else if (NETLIFY.reviewID) {
  161. return `https://github.com/video-dev/hls.js/pull/${NETLIFY.reviewID}`;
  162. } else if (NETLIFY.branch) {
  163. return `https://github.com/video-dev/hls.js/tree/${encodeURIComponent(
  164. NETLIFY.branch
  165. )}`;
  166. }
  167. return `https://github.com/video-dev/hls.js/releases/tag/v${encodeURIComponent(
  168. version
  169. )}`;
  170. }
  171. const version = Hls.version;
  172. if (version) {
  173. const $a = $('<a />')
  174. .attr('target', '_blank')
  175. .attr('rel', 'noopener noreferrer')
  176. .attr('href', getVersionLink(version))
  177. .text('v' + version);
  178. $('.title').append(' ').append($a);
  179. }
  180. $('#streamURL').val(sourceURL);
  181. const volumeSettings = JSON.parse(
  182. localStorage.getItem(STORAGE_KEYS.volume)
  183. ) || {
  184. volume: 0.05,
  185. muted: false,
  186. };
  187. video.volume = volumeSettings.volume;
  188. video.muted = volumeSettings.muted;
  189. $('.btn-dump').toggle(dumpfMP4);
  190. $('#toggleButtons').show();
  191. $('#metricsButtonWindow').toggle(self.windowSliding);
  192. $('#metricsButtonFixed').toggle(!self.windowSliding);
  193. loadSelectedStream();
  194. let tabIndexesCSV = localStorage.getItem(STORAGE_KEYS.demo_tabs);
  195. if (tabIndexesCSV === null) {
  196. tabIndexesCSV = '0,1,2';
  197. }
  198. if (tabIndexesCSV) {
  199. tabIndexesCSV.split(',').forEach((indexString) => {
  200. toggleTab($('.demo-tab-btn')[parseInt(indexString) || 0], true);
  201. });
  202. }
  203. $(window).on('popstate', function () {
  204. window.location.reload();
  205. });
  206. });
  207. function setupGlobals() {
  208. self.events = events = {
  209. url: url,
  210. t0: self.performance.now(),
  211. load: [],
  212. buffer: [],
  213. video: [],
  214. level: [],
  215. bitrate: [],
  216. };
  217. lastAudioTrackSwitchingIdx = undefined;
  218. lastSeekingIdx = undefined;
  219. bufferingIdx = -1;
  220. // actual values, only on window
  221. self.recoverDecodingErrorDate = null;
  222. self.recoverSwapAudioCodecDate = null;
  223. self.fmp4Data = fmp4Data = {
  224. audio: [],
  225. video: [],
  226. };
  227. self.onClickBufferedRange = onClickBufferedRange;
  228. self.updateLevelInfo = updateLevelInfo;
  229. self.onDemoConfigChanged = onDemoConfigChanged;
  230. self.createfMP4 = createfMP4;
  231. self.goToMetricsPermaLink = goToMetricsPermaLink;
  232. self.toggleTab = toggleTab;
  233. self.toggleTabClick = toggleTabClick;
  234. self.applyConfigEditorValue = applyConfigEditorValue;
  235. }
  236. function trimArray(target, limit) {
  237. if (limit < 0) {
  238. return;
  239. }
  240. while (target.length > limit) {
  241. target.shift();
  242. }
  243. }
  244. function trimEventHistory() {
  245. const x = limitMetrics;
  246. if (x < 0) {
  247. return;
  248. }
  249. trimArray(events.load, x);
  250. trimArray(events.buffer, x);
  251. trimArray(events.video, x);
  252. trimArray(events.level, x);
  253. trimArray(events.bitrate, x);
  254. }
  255. function loadSelectedStream() {
  256. $('#statusOut,#errorOut').empty();
  257. if (!Hls.isSupported()) {
  258. handleUnsupported();
  259. return;
  260. }
  261. url = $('#streamURL').val();
  262. setupGlobals();
  263. hideCanvas();
  264. if (hls) {
  265. hls.destroy();
  266. clearInterval(hls.bufferTimer);
  267. hls = null;
  268. }
  269. if (!enableStreaming) {
  270. logStatus('Streaming disabled');
  271. return;
  272. }
  273. logStatus('Loading ' + url);
  274. // Extending both a demo-specific config and the user config which can override all
  275. const hlsConfig = $.extend(
  276. {},
  277. hlsjsDefaults,
  278. getEditorValue({ parse: true })
  279. );
  280. if (selectedTestStream && selectedTestStream.config) {
  281. console.info(
  282. '[loadSelectedStream] extending hls config with stream-specific config: ',
  283. selectedTestStream.config
  284. );
  285. $.extend(hlsConfig, selectedTestStream.config);
  286. updateConfigEditorValue(hlsConfig);
  287. }
  288. onDemoConfigChanged(true);
  289. console.log('Using Hls.js config:', hlsConfig);
  290. self.hls = hls = new Hls(hlsConfig);
  291. logStatus('Loading manifest and attaching video element...');
  292. const expiredTracks = [].filter.call(
  293. video.textTracks,
  294. (track) => track.kind !== 'metadata'
  295. );
  296. if (expiredTracks.length) {
  297. const kinds = expiredTracks
  298. .map((track) => track.kind)
  299. .filter((kind, index, self) => self.indexOf(kind) === index);
  300. logStatus(
  301. `Replacing video element to remove ${kinds.join(' and ')} text tracks`
  302. );
  303. const videoWithExpiredTextTracks = video;
  304. video = videoWithExpiredTextTracks.cloneNode(false);
  305. video.removeAttribute('src');
  306. video.volume = videoWithExpiredTextTracks.volume;
  307. video.muted = videoWithExpiredTextTracks.muted;
  308. videoWithExpiredTextTracks.parentNode.insertBefore(
  309. video,
  310. videoWithExpiredTextTracks
  311. );
  312. videoWithExpiredTextTracks.parentNode.removeChild(
  313. videoWithExpiredTextTracks
  314. );
  315. }
  316. addChartEventListeners(hls);
  317. addVideoEventListeners(video);
  318. hls.loadSource(url);
  319. hls.autoLevelCapping = levelCapping;
  320. hls.attachMedia(video);
  321. hls.on(Hls.Events.MEDIA_ATTACHED, function () {
  322. logStatus('Media element attached');
  323. bufferingIdx = -1;
  324. events.video.push({
  325. time: self.performance.now() - events.t0,
  326. type: 'Media attached',
  327. });
  328. trimEventHistory();
  329. });
  330. hls.on(Hls.Events.MEDIA_DETACHED, function () {
  331. logStatus('Media element detached');
  332. clearInterval(hls.bufferTimer);
  333. bufferingIdx = -1;
  334. tracks = [];
  335. events.video.push({
  336. time: self.performance.now() - events.t0,
  337. type: 'Media detached',
  338. });
  339. trimEventHistory();
  340. });
  341. hls.on(Hls.Events.DESTROYING, function () {
  342. clearInterval(hls.bufferTimer);
  343. });
  344. hls.on(Hls.Events.BUFFER_RESET, function () {
  345. clearInterval(hls.bufferTimer);
  346. });
  347. hls.on(Hls.Events.FRAG_PARSING_INIT_SEGMENT, function (eventName, data) {
  348. showCanvas();
  349. events.video.push({
  350. time: self.performance.now() - events.t0,
  351. type: data.id + ' init segment',
  352. });
  353. trimEventHistory();
  354. });
  355. hls.on(Hls.Events.FRAG_PARSING_METADATA, function (eventName, data) {
  356. // console.log("Id3 samples ", data.samples);
  357. });
  358. hls.on(Hls.Events.LEVEL_SWITCHING, function (eventName, data) {
  359. events.level.push({
  360. time: self.performance.now() - events.t0,
  361. id: data.level,
  362. bitrate: Math.round(hls.levels[data.level].bitrate / 1000),
  363. });
  364. trimEventHistory();
  365. updateLevelInfo();
  366. });
  367. hls.on(Hls.Events.MANIFEST_PARSED, function (eventName, data) {
  368. events.load.push({
  369. type: 'manifest',
  370. name: '',
  371. start: 0,
  372. end: data.levels.length,
  373. time: data.stats.loading.start - events.t0,
  374. latency: data.stats.loading.first - data.stats.loading.start,
  375. load: data.stats.loading.end - data.stats.loading.first,
  376. duration: data.stats.loading.end - data.stats.loading.first,
  377. });
  378. trimEventHistory();
  379. self.refreshCanvas();
  380. });
  381. hls.on(Hls.Events.MANIFEST_PARSED, function (eventName, data) {
  382. logStatus(`${hls.levels.length} quality levels found`);
  383. logStatus('Manifest successfully loaded');
  384. stats = {
  385. levelNb: data.levels.length,
  386. levelParsed: 0,
  387. };
  388. trimEventHistory();
  389. updateLevelInfo();
  390. });
  391. hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, function (eventName, data) {
  392. logStatus('No of audio tracks found: ' + data.audioTracks.length);
  393. updateAudioTrackInfo();
  394. });
  395. hls.on(Hls.Events.AUDIO_TRACK_SWITCHING, function (eventName, data) {
  396. logStatus('Audio track switching...');
  397. updateAudioTrackInfo();
  398. events.video.push({
  399. time: self.performance.now() - events.t0,
  400. type: 'audio switching',
  401. name: '@' + data.id,
  402. });
  403. trimEventHistory();
  404. lastAudioTrackSwitchingIdx = events.video.length - 1;
  405. });
  406. hls.on(Hls.Events.AUDIO_TRACK_SWITCHED, function (eventName, data) {
  407. logStatus('Audio track switched');
  408. updateAudioTrackInfo();
  409. const event = {
  410. time: self.performance.now() - events.t0,
  411. type: 'audio switched',
  412. name: '@' + data.id,
  413. };
  414. if (lastAudioTrackSwitchingIdx !== undefined) {
  415. events.video[lastAudioTrackSwitchingIdx].duration =
  416. event.time - events.video[lastAudioTrackSwitchingIdx].time;
  417. lastAudioTrackSwitchingIdx = undefined;
  418. }
  419. events.video.push(event);
  420. trimEventHistory();
  421. });
  422. hls.on(Hls.Events.LEVEL_LOADED, function (eventName, data) {
  423. events.isLive = data.details.live;
  424. const event = {
  425. type: 'level',
  426. id: data.level,
  427. start: data.details.startSN,
  428. end: data.details.endSN,
  429. time: data.stats.loading.start - events.t0,
  430. latency: data.stats.loading.first - data.stats.loading.start,
  431. load: data.stats.loading.end - data.stats.loading.first,
  432. parsing: data.stats.parsing.end - data.stats.loading.end,
  433. duration: data.stats.loading.end - data.stats.loading.first,
  434. };
  435. const parsingDuration = data.stats.parsing.end - data.stats.loading.end;
  436. if (stats.levelParsed) {
  437. this.sumLevelParsingMs += parsingDuration;
  438. } else {
  439. this.sumLevelParsingMs = parsingDuration;
  440. }
  441. stats.levelParsed++;
  442. stats.levelParsingUs = Math.round(
  443. (1000 * this.sumLevelParsingMs) / stats.levelParsed
  444. );
  445. // console.log('parsing level duration :' + stats.levelParsingUs + 'us,count:' + stats.levelParsed);
  446. events.load.push(event);
  447. trimEventHistory();
  448. self.refreshCanvas();
  449. });
  450. hls.on(Hls.Events.AUDIO_TRACK_LOADED, function (eventName, data) {
  451. events.isLive = data.details.live;
  452. const event = {
  453. type: 'audio track',
  454. id: data.id,
  455. start: data.details.startSN,
  456. end: data.details.endSN,
  457. time: data.stats.loading.start - events.t0,
  458. latency: data.stats.loading.first - data.stats.loading.start,
  459. load: data.stats.loading.end - data.stats.loading.first,
  460. parsing: data.stats.parsing.end - data.stats.loading.end,
  461. duration: data.stats.loading.end - data.stats.loading.first,
  462. };
  463. events.load.push(event);
  464. trimEventHistory();
  465. self.refreshCanvas();
  466. });
  467. hls.on(Hls.Events.FRAG_BUFFERED, function (eventName, data) {
  468. const event = {
  469. type: data.frag.type + (data.part ? ' part' : ' fragment'),
  470. id: data.frag.level,
  471. id2: data.frag.sn,
  472. id3: data.part ? data.part.index : undefined,
  473. time: data.stats.loading.start - events.t0,
  474. latency: data.stats.loading.first - data.stats.loading.start,
  475. load: data.stats.loading.end - data.stats.loading.first,
  476. parsing: data.stats.parsing.end - data.stats.loading.end,
  477. buffer: data.stats.buffering.end - data.stats.parsing.end,
  478. duration: data.stats.buffering.end - data.stats.loading.first,
  479. bw: Math.round(
  480. (8 * data.stats.total) /
  481. (data.stats.buffering.end - data.stats.loading.start)
  482. ),
  483. size: data.stats.total,
  484. };
  485. events.load.push(event);
  486. events.bitrate.push({
  487. time: self.performance.now() - events.t0,
  488. bitrate: event.bw,
  489. duration: data.frag.duration,
  490. level: event.id,
  491. });
  492. if (events.buffer.length === 0) {
  493. events.buffer.push({
  494. time: 0,
  495. buffer: 0,
  496. pos: 0,
  497. });
  498. }
  499. clearInterval(hls.bufferTimer);
  500. hls.bufferTimer = self.setInterval(checkBuffer, 100);
  501. trimEventHistory();
  502. self.refreshCanvas();
  503. updateLevelInfo();
  504. const latency = data.stats.loading.first - data.stats.loading.start;
  505. const parsing = data.stats.parsing.end - data.stats.loading.end;
  506. const process = data.stats.buffering.end - data.stats.loading.start;
  507. const bitrate = Math.round(
  508. (8 * data.stats.total) /
  509. (data.stats.buffering.end - data.stats.loading.first)
  510. );
  511. if (stats.fragBuffered) {
  512. stats.fragMinLatency = Math.min(stats.fragMinLatency, latency);
  513. stats.fragMaxLatency = Math.max(stats.fragMaxLatency, latency);
  514. stats.fragMinProcess = Math.min(stats.fragMinProcess, process);
  515. stats.fragMaxProcess = Math.max(stats.fragMaxProcess, process);
  516. stats.fragMinKbps = Math.min(stats.fragMinKbps, bitrate);
  517. stats.fragMaxKbps = Math.max(stats.fragMaxKbps, bitrate);
  518. stats.autoLevelCappingMin = Math.min(
  519. stats.autoLevelCappingMin,
  520. hls.autoLevelCapping
  521. );
  522. stats.autoLevelCappingMax = Math.max(
  523. stats.autoLevelCappingMax,
  524. hls.autoLevelCapping
  525. );
  526. stats.fragBuffered++;
  527. } else {
  528. stats.fragMinLatency = stats.fragMaxLatency = latency;
  529. stats.fragMinProcess = stats.fragMaxProcess = process;
  530. stats.fragMinKbps = stats.fragMaxKbps = bitrate;
  531. stats.fragBuffered = 1;
  532. stats.fragBufferedBytes = 0;
  533. stats.autoLevelCappingMin = stats.autoLevelCappingMax =
  534. hls.autoLevelCapping;
  535. this.sumLatency = 0;
  536. this.sumKbps = 0;
  537. this.sumProcess = 0;
  538. this.sumParsing = 0;
  539. }
  540. stats.fraglastLatency = latency;
  541. this.sumLatency += latency;
  542. stats.fragAvgLatency = Math.round(this.sumLatency / stats.fragBuffered);
  543. stats.fragLastProcess = process;
  544. this.sumProcess += process;
  545. this.sumParsing += parsing;
  546. stats.fragAvgProcess = Math.round(this.sumProcess / stats.fragBuffered);
  547. stats.fragLastKbps = bitrate;
  548. this.sumKbps += bitrate;
  549. stats.fragAvgKbps = Math.round(this.sumKbps / stats.fragBuffered);
  550. stats.fragBufferedBytes += data.stats.total;
  551. stats.fragparsingKbps = Math.round(
  552. (8 * stats.fragBufferedBytes) / this.sumParsing
  553. );
  554. stats.fragparsingMs = Math.round(this.sumParsing);
  555. stats.autoLevelCappingLast = hls.autoLevelCapping;
  556. });
  557. hls.on(Hls.Events.LEVEL_SWITCHED, function (eventName, data) {
  558. const event = {
  559. time: self.performance.now() - events.t0,
  560. type: 'level switched',
  561. name: data.level,
  562. };
  563. events.video.push(event);
  564. trimEventHistory();
  565. self.refreshCanvas();
  566. updateLevelInfo();
  567. });
  568. hls.on(Hls.Events.FRAG_CHANGED, function (eventName, data) {
  569. const event = {
  570. time: self.performance.now() - events.t0,
  571. type: 'frag changed',
  572. name: data.frag.sn + ' @ ' + data.frag.level,
  573. };
  574. events.video.push(event);
  575. trimEventHistory();
  576. self.refreshCanvas();
  577. updateLevelInfo();
  578. stats.tagList = data.frag.tagList;
  579. const level = data.frag.level;
  580. const autoLevel = hls.autoLevelEnabled;
  581. if (stats.levelStart === undefined) {
  582. stats.levelStart = level;
  583. }
  584. stats.fragProgramDateTime = data.frag.programDateTime;
  585. stats.fragStart = data.frag.start;
  586. if (autoLevel) {
  587. if (stats.fragChangedAuto) {
  588. stats.autoLevelMin = Math.min(stats.autoLevelMin, level);
  589. stats.autoLevelMax = Math.max(stats.autoLevelMax, level);
  590. stats.fragChangedAuto++;
  591. if (this.levelLastAuto && level !== stats.autoLevelLast) {
  592. stats.autoLevelSwitch++;
  593. }
  594. } else {
  595. stats.autoLevelMin = stats.autoLevelMax = level;
  596. stats.autoLevelSwitch = 0;
  597. stats.fragChangedAuto = 1;
  598. this.sumAutoLevel = 0;
  599. }
  600. this.sumAutoLevel += level;
  601. stats.autoLevelAvg =
  602. Math.round((1000 * this.sumAutoLevel) / stats.fragChangedAuto) / 1000;
  603. stats.autoLevelLast = level;
  604. } else {
  605. if (stats.fragChangedManual) {
  606. stats.manualLevelMin = Math.min(stats.manualLevelMin, level);
  607. stats.manualLevelMax = Math.max(stats.manualLevelMax, level);
  608. stats.fragChangedManual++;
  609. if (!this.levelLastAuto && level !== stats.manualLevelLast) {
  610. stats.manualLevelSwitch++;
  611. }
  612. } else {
  613. stats.manualLevelMin = stats.manualLevelMax = level;
  614. stats.manualLevelSwitch = 0;
  615. stats.fragChangedManual = 1;
  616. }
  617. stats.manualLevelLast = level;
  618. }
  619. this.levelLastAuto = autoLevel;
  620. });
  621. hls.on(Hls.Events.FRAG_LOAD_EMERGENCY_ABORTED, function (eventName, data) {
  622. if (stats) {
  623. if (stats.fragLoadEmergencyAborted === undefined) {
  624. stats.fragLoadEmergencyAborted = 1;
  625. } else {
  626. stats.fragLoadEmergencyAborted++;
  627. }
  628. }
  629. });
  630. hls.on(Hls.Events.FRAG_DECRYPTED, function (eventName, data) {
  631. if (!stats.fragDecrypted) {
  632. stats.fragDecrypted = 0;
  633. this.totalDecryptTime = 0;
  634. stats.fragAvgDecryptTime = 0;
  635. }
  636. stats.fragDecrypted++;
  637. this.totalDecryptTime += data.stats.tdecrypt - data.stats.tstart;
  638. stats.fragAvgDecryptTime = this.totalDecryptTime / stats.fragDecrypted;
  639. });
  640. hls.on(Hls.Events.ERROR, function (eventName, data) {
  641. console.warn('Error event:', data);
  642. switch (data.details) {
  643. case Hls.ErrorDetails.MANIFEST_LOAD_ERROR:
  644. try {
  645. $('#errorOut').html(
  646. 'Cannot load <a href="' +
  647. data.context.url +
  648. '">' +
  649. url +
  650. '</a><br>HTTP response code:' +
  651. data.response.code +
  652. ' <br>' +
  653. data.response.text
  654. );
  655. if (data.response.code === 0) {
  656. $('#errorOut').append(
  657. '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'
  658. );
  659. }
  660. } catch (err) {
  661. $('#errorOut').html(
  662. 'Cannot load <a href="' +
  663. data.context.url +
  664. '">' +
  665. url +
  666. '</a><br>Response body: ' +
  667. data.response.text
  668. );
  669. }
  670. break;
  671. case Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT:
  672. logError('Timeout while loading manifest');
  673. break;
  674. case Hls.ErrorDetails.MANIFEST_PARSING_ERROR:
  675. logError('Error while parsing manifest:' + data.reason);
  676. break;
  677. case Hls.ErrorDetails.LEVEL_EMPTY_ERROR:
  678. logError(
  679. 'Loaded level contains no fragments ' + data.level + ' ' + data.url
  680. );
  681. // handleLevelError demonstrates how to remove a level that errors followed by a downswitch
  682. // handleLevelError(data);
  683. break;
  684. case Hls.ErrorDetails.LEVEL_LOAD_ERROR:
  685. logError(
  686. 'Error while loading level playlist ' +
  687. data.context.level +
  688. ' ' +
  689. data.url
  690. );
  691. // handleLevelError demonstrates how to remove a level that errors followed by a downswitch
  692. // handleLevelError(data);
  693. break;
  694. case Hls.ErrorDetails.LEVEL_LOAD_TIMEOUT:
  695. logError(
  696. 'Timeout while loading level playlist ' +
  697. data.context.level +
  698. ' ' +
  699. data.url
  700. );
  701. // handleLevelError demonstrates how to remove a level that errors followed by a downswitch
  702. // handleLevelError(data);
  703. break;
  704. case Hls.ErrorDetails.LEVEL_SWITCH_ERROR:
  705. logError('Error while trying to switch to level ' + data.level);
  706. break;
  707. case Hls.ErrorDetails.FRAG_LOAD_ERROR:
  708. logError('Error while loading fragment ' + data.frag.url);
  709. break;
  710. case Hls.ErrorDetails.FRAG_LOAD_TIMEOUT:
  711. logError('Timeout while loading fragment ' + data.frag.url);
  712. break;
  713. case Hls.ErrorDetails.FRAG_LOOP_LOADING_ERROR:
  714. logError('Fragment-loop loading error');
  715. break;
  716. case Hls.ErrorDetails.FRAG_DECRYPT_ERROR:
  717. logError('Decrypting error:' + data.reason);
  718. break;
  719. case Hls.ErrorDetails.FRAG_PARSING_ERROR:
  720. logError('Parsing error:' + data.reason);
  721. break;
  722. case Hls.ErrorDetails.KEY_LOAD_ERROR:
  723. logError('Error while loading key ' + data.frag.decryptdata.uri);
  724. break;
  725. case Hls.ErrorDetails.KEY_LOAD_TIMEOUT:
  726. logError('Timeout while loading key ' + data.frag.decryptdata.uri);
  727. break;
  728. case Hls.ErrorDetails.BUFFER_APPEND_ERROR:
  729. logError('Buffer append error');
  730. break;
  731. case Hls.ErrorDetails.BUFFER_ADD_CODEC_ERROR:
  732. logError(
  733. 'Buffer add codec error for ' +
  734. data.mimeType +
  735. ':' +
  736. data.error.message
  737. );
  738. break;
  739. case Hls.ErrorDetails.BUFFER_APPENDING_ERROR:
  740. logError('Buffer appending error');
  741. break;
  742. case Hls.ErrorDetails.BUFFER_STALLED_ERROR:
  743. logError('Buffer stalled error');
  744. if (stopOnStall) {
  745. hls.stopLoad();
  746. video.pause();
  747. }
  748. break;
  749. default:
  750. break;
  751. }
  752. if (data.fatal) {
  753. console.error(`Fatal error : ${data.details}`);
  754. switch (data.type) {
  755. case Hls.ErrorTypes.MEDIA_ERROR:
  756. logError(`A media error occurred: ${data.details}`);
  757. handleMediaError();
  758. break;
  759. case Hls.ErrorTypes.NETWORK_ERROR:
  760. logError(`A network error occurred: ${data.details}`);
  761. break;
  762. default:
  763. logError(`An unrecoverable error occurred: ${data.details}`);
  764. hls.destroy();
  765. break;
  766. }
  767. }
  768. if (!stats) {
  769. stats = {};
  770. }
  771. // track all errors independently
  772. if (stats[data.details] === undefined) {
  773. stats[data.details] = 1;
  774. } else {
  775. stats[data.details] += 1;
  776. }
  777. // track fatal error
  778. if (data.fatal) {
  779. if (stats.fatalError === undefined) {
  780. stats.fatalError = 1;
  781. } else {
  782. stats.fatalError += 1;
  783. }
  784. }
  785. $('#statisticsOut').text(JSON.stringify(sortObject(stats), null, '\t'));
  786. });
  787. hls.on(Hls.Events.BUFFER_CREATED, function (eventName, data) {
  788. tracks = data.tracks;
  789. });
  790. hls.on(Hls.Events.BUFFER_APPENDING, function (eventName, data) {
  791. if (dumpfMP4) {
  792. fmp4Data[data.type].push(data.data);
  793. }
  794. });
  795. hls.on(Hls.Events.FPS_DROP, function (eventName, data) {
  796. const event = {
  797. time: self.performance.now() - events.t0,
  798. type: 'frame drop',
  799. name: data.currentDropped + '/' + data.currentDecoded,
  800. };
  801. events.video.push(event);
  802. trimEventHistory();
  803. if (stats) {
  804. if (stats.fpsDropEvent === undefined) {
  805. stats.fpsDropEvent = 1;
  806. } else {
  807. stats.fpsDropEvent++;
  808. }
  809. stats.fpsTotalDroppedFrames = data.totalDroppedFrames;
  810. }
  811. });
  812. }
  813. function addVideoEventListeners(video) {
  814. video.removeEventListener('resize', handleVideoEvent);
  815. video.removeEventListener('seeking', handleVideoEvent);
  816. video.removeEventListener('seeked', handleVideoEvent);
  817. video.removeEventListener('pause', handleVideoEvent);
  818. video.removeEventListener('play', handleVideoEvent);
  819. video.removeEventListener('canplay', handleVideoEvent);
  820. video.removeEventListener('canplaythrough', handleVideoEvent);
  821. video.removeEventListener('ended', handleVideoEvent);
  822. video.removeEventListener('playing', handleVideoEvent);
  823. video.removeEventListener('error', handleVideoEvent);
  824. video.removeEventListener('loadedmetadata', handleVideoEvent);
  825. video.removeEventListener('loadeddata', handleVideoEvent);
  826. video.removeEventListener('durationchange', handleVideoEvent);
  827. video.removeEventListener('volumechange', handleVolumeEvent);
  828. video.addEventListener('resize', handleVideoEvent);
  829. video.addEventListener('seeking', handleVideoEvent);
  830. video.addEventListener('seeked', handleVideoEvent);
  831. video.addEventListener('pause', handleVideoEvent);
  832. video.addEventListener('play', handleVideoEvent);
  833. video.addEventListener('canplay', handleVideoEvent);
  834. video.addEventListener('canplaythrough', handleVideoEvent);
  835. video.addEventListener('ended', handleVideoEvent);
  836. video.addEventListener('playing', handleVideoEvent);
  837. video.addEventListener('error', handleVideoEvent);
  838. video.addEventListener('loadedmetadata', handleVideoEvent);
  839. video.addEventListener('loadeddata', handleVideoEvent);
  840. video.addEventListener('durationchange', handleVideoEvent);
  841. video.addEventListener('volumechange', handleVolumeEvent);
  842. }
  843. function handleUnsupported() {
  844. if (navigator.userAgent.toLowerCase().indexOf('firefox') !== -1) {
  845. logStatus(
  846. '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>'
  847. );
  848. } else {
  849. logStatus(
  850. 'Your Browser does not support MediaSourceExtension / MP4 mediasource'
  851. );
  852. }
  853. }
  854. function handleVideoEvent(evt) {
  855. let data = '';
  856. switch (evt.type) {
  857. case 'durationchange':
  858. if (evt.target.duration - lastDuration <= 0.5) {
  859. // some browsers report several duration change events with almost the same value ... avoid spamming video events
  860. return;
  861. }
  862. lastDuration = evt.target.duration;
  863. data = Math.round(evt.target.duration * 1000);
  864. break;
  865. case 'resize':
  866. data = evt.target.videoWidth + '/' + evt.target.videoHeight;
  867. playerResize();
  868. break;
  869. case 'loadedmetadata':
  870. case 'loadeddata':
  871. case 'canplay':
  872. case 'canplaythrough':
  873. case 'ended':
  874. case 'seeking':
  875. case 'seeked':
  876. case 'play':
  877. case 'playing':
  878. lastStartPosition = evt.target.currentTime;
  879. case 'pause':
  880. case 'waiting':
  881. case 'stalled':
  882. case 'error':
  883. data = Math.round(evt.target.currentTime * 1000);
  884. if (evt.type === 'error') {
  885. let errorTxt;
  886. const mediaError = evt.currentTarget.error;
  887. switch (mediaError.code) {
  888. case mediaError.MEDIA_ERR_ABORTED:
  889. errorTxt = 'You aborted the video playback';
  890. break;
  891. case mediaError.MEDIA_ERR_DECODE:
  892. errorTxt =
  893. 'The video playback was aborted due to a corruption problem or because the video used features your browser did not support';
  894. handleMediaError();
  895. break;
  896. case mediaError.MEDIA_ERR_NETWORK:
  897. errorTxt =
  898. 'A network error caused the video download to fail part-way';
  899. break;
  900. case mediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
  901. errorTxt =
  902. 'The video could not be loaded, either because the server or network failed or because the format is not supported';
  903. break;
  904. }
  905. if (mediaError.message) {
  906. errorTxt += ' - ' + mediaError.message;
  907. }
  908. logStatus(errorTxt);
  909. console.error(errorTxt);
  910. }
  911. break;
  912. default:
  913. break;
  914. }
  915. const event = {
  916. time: self.performance.now() - events.t0,
  917. type: evt.type,
  918. name: data,
  919. };
  920. events.video.push(event);
  921. if (evt.type === 'seeking') {
  922. lastSeekingIdx = events.video.length - 1;
  923. }
  924. if (evt.type === 'seeked') {
  925. events.video[lastSeekingIdx].duration =
  926. event.time - events.video[lastSeekingIdx].time;
  927. }
  928. trimEventHistory();
  929. }
  930. function handleVolumeEvent() {
  931. localStorage.setItem(
  932. STORAGE_KEYS.volume,
  933. JSON.stringify({
  934. muted: video.muted,
  935. volume: video.volume,
  936. })
  937. );
  938. }
  939. function handleLevelError(data) {
  940. var levelObj = data.context || data;
  941. hls.removeLevel(levelObj.level, levelObj.urlId || 0);
  942. if (!hls.levels.length) {
  943. logError('All levels have been removed');
  944. hls.destroy();
  945. return;
  946. }
  947. // Trigger an immediate downswitch to the first level
  948. // This is to handle the case where we start at an empty level, where switching to auto causes hlsjs to stall
  949. hls.currentLevel = 0;
  950. // Set the quality back to auto so that we return to optimal quality
  951. hls.currentLevel = -1;
  952. }
  953. function handleMediaError() {
  954. if (autoRecoverError) {
  955. const now = self.performance.now();
  956. if (
  957. !self.recoverDecodingErrorDate ||
  958. now - self.recoverDecodingErrorDate > 3000
  959. ) {
  960. self.recoverDecodingErrorDate = self.performance.now();
  961. $('#statusOut').append(', trying to recover media error.');
  962. hls.recoverMediaError();
  963. } else {
  964. if (
  965. !self.recoverSwapAudioCodecDate ||
  966. now - self.recoverSwapAudioCodecDate > 3000
  967. ) {
  968. self.recoverSwapAudioCodecDate = self.performance.now();
  969. $('#statusOut').append(
  970. ', trying to swap audio codec and recover media error.'
  971. );
  972. hls.swapAudioCodec();
  973. hls.recoverMediaError();
  974. } else {
  975. $('#statusOut').append(
  976. ', cannot recover. Last media error recovery failed.'
  977. );
  978. }
  979. }
  980. }
  981. }
  982. function timeRangesToString(r) {
  983. let log = '';
  984. for (let i = 0; i < r.length; i++) {
  985. log += '[' + r.start(i) + ', ' + r.end(i) + ']';
  986. log += ' ';
  987. }
  988. return log;
  989. }
  990. function checkBuffer() {
  991. const canvas = document.querySelector('#bufferedCanvas');
  992. const ctx = canvas.getContext('2d');
  993. const r = video.buffered;
  994. const seekableEnd = getSeekableEnd();
  995. let bufferingDuration;
  996. if (r) {
  997. ctx.fillStyle = 'black';
  998. if (!canvas.width || canvas.width !== video.clientWidth) {
  999. canvas.width = video.clientWidth;
  1000. }
  1001. ctx.fillRect(0, 0, canvas.width, canvas.height);
  1002. const pos = video.currentTime;
  1003. let bufferLen = 0;
  1004. ctx.fillStyle = 'gray';
  1005. for (let i = 0; i < r.length; i++) {
  1006. const start = (r.start(i) / seekableEnd) * canvas.width;
  1007. const end = (r.end(i) / seekableEnd) * canvas.width;
  1008. ctx.fillRect(start, 2, Math.max(2, end - start), 11);
  1009. if (pos >= r.start(i) && pos < r.end(i)) {
  1010. // play position is inside this buffer TimeRange, retrieve end of buffer position and buffer length
  1011. bufferLen = r.end(i) - pos;
  1012. }
  1013. }
  1014. // check if we are in buffering / or playback ended state
  1015. if (
  1016. bufferLen <= 0.1 &&
  1017. video.paused === false &&
  1018. pos - lastStartPosition > 0.5
  1019. ) {
  1020. if (lastDuration - pos <= 0.5 && events.isLive === false) {
  1021. // don't create buffering event if we are at the end of the playlist, don't report ended for live playlist
  1022. } else {
  1023. // we are not at the end of the playlist ... real buffering
  1024. if (bufferingIdx !== -1) {
  1025. bufferingDuration =
  1026. self.performance.now() -
  1027. events.t0 -
  1028. events.video[bufferingIdx].time;
  1029. events.video[bufferingIdx].duration = bufferingDuration;
  1030. events.video[bufferingIdx].name = bufferingDuration;
  1031. } else {
  1032. events.video.push({
  1033. type: 'buffering',
  1034. time: self.performance.now() - events.t0,
  1035. });
  1036. trimEventHistory();
  1037. // we are in buffering state
  1038. bufferingIdx = events.video.length - 1;
  1039. }
  1040. }
  1041. }
  1042. if (bufferLen > 0.1 && bufferingIdx !== -1) {
  1043. bufferingDuration =
  1044. self.performance.now() - events.t0 - events.video[bufferingIdx].time;
  1045. events.video[bufferingIdx].duration = bufferingDuration;
  1046. events.video[bufferingIdx].name = bufferingDuration;
  1047. // we are out of buffering state
  1048. bufferingIdx = -1;
  1049. }
  1050. // update buffer/position for current Time
  1051. const event = {
  1052. time: self.performance.now() - events.t0,
  1053. buffer: Math.round(bufferLen * 1000),
  1054. pos: Math.round(pos * 1000),
  1055. };
  1056. const bufEvents = events.buffer;
  1057. const bufEventLen = bufEvents.length;
  1058. if (bufEventLen > 1) {
  1059. const event0 = bufEvents[bufEventLen - 2];
  1060. const event1 = bufEvents[bufEventLen - 1];
  1061. const slopeBuf0 =
  1062. (event0.buffer - event1.buffer) / (event0.time - event1.time);
  1063. const slopeBuf1 =
  1064. (event1.buffer - event.buffer) / (event1.time - event.time);
  1065. const slopePos0 = (event0.pos - event1.pos) / (event0.time - event1.time);
  1066. const slopePos1 = (event1.pos - event.pos) / (event1.time - event.time);
  1067. // compute slopes. if less than 30% difference, remove event1
  1068. if (
  1069. (slopeBuf0 === slopeBuf1 ||
  1070. Math.abs(slopeBuf0 / slopeBuf1 - 1) <= 0.3) &&
  1071. (slopePos0 === slopePos1 || Math.abs(slopePos0 / slopePos1 - 1) <= 0.3)
  1072. ) {
  1073. bufEvents.pop();
  1074. }
  1075. }
  1076. events.buffer.push(event);
  1077. trimEventHistory();
  1078. self.refreshCanvas();
  1079. if ($('#statsDisplayTab').is(':visible')) {
  1080. let log = `Duration: ${video.duration}\nBuffered: ${timeRangesToString(
  1081. video.buffered
  1082. )}\nSeekable: ${timeRangesToString(
  1083. video.seekable
  1084. )}\nPlayed: ${timeRangesToString(video.played)}\n`;
  1085. if (hls.media) {
  1086. for (const type in tracks) {
  1087. log += `Buffer for ${type} contains:${timeRangesToString(
  1088. tracks[type].buffer.buffered
  1089. )}\n`;
  1090. }
  1091. const videoPlaybackQuality = video.getVideoPlaybackQuality;
  1092. if (
  1093. videoPlaybackQuality &&
  1094. typeof videoPlaybackQuality === typeof Function
  1095. ) {
  1096. log += `Dropped frames: ${
  1097. video.getVideoPlaybackQuality().droppedVideoFrames
  1098. }\n`;
  1099. log += `Corrupted frames: ${
  1100. video.getVideoPlaybackQuality().corruptedVideoFrames
  1101. }\n`;
  1102. } else if (video.webkitDroppedFrameCount) {
  1103. log += `Dropped frames: ${video.webkitDroppedFrameCount}\n`;
  1104. }
  1105. }
  1106. log += `Bandwidth Estimate: ${hls.bandwidthEstimate.toFixed(3)}\n`;
  1107. if (events.isLive) {
  1108. log +=
  1109. 'Live Stats:\n' +
  1110. ` Max Latency: ${hls.maxLatency}\n` +
  1111. ` Target Latency: ${hls.targetLatency.toFixed(3)}\n` +
  1112. ` Latency: ${hls.latency.toFixed(3)}\n` +
  1113. ` Drift: ${hls.drift.toFixed(3)} (edge advance rate)\n` +
  1114. ` Edge Stall: ${hls.latencyController.edgeStalled.toFixed(
  1115. 3
  1116. )} (playlist refresh over target duration/part)\n` +
  1117. ` Playback rate: ${video.playbackRate.toFixed(2)}\n`;
  1118. if (stats.fragProgramDateTime) {
  1119. const currentPDT =
  1120. stats.fragProgramDateTime +
  1121. (video.currentTime - stats.fragStart) * 1000;
  1122. log += ` Program Date Time: ${new Date(currentPDT).toISOString()}`;
  1123. const pdtLatency = (Date.now() - currentPDT) / 1000;
  1124. if (pdtLatency > 0) {
  1125. log += ` (${pdtLatency.toFixed(3)} seconds ago)`;
  1126. }
  1127. }
  1128. }
  1129. $('#bufferedOut').text(log);
  1130. $('#statisticsOut').text(JSON.stringify(sortObject(stats), null, '\t'));
  1131. }
  1132. ctx.fillStyle = 'blue';
  1133. const x = (video.currentTime / seekableEnd) * canvas.width;
  1134. ctx.fillRect(x, 0, 2, 15);
  1135. } else if (ctx.fillStyle !== 'black') {
  1136. ctx.fillStyle = 'black';
  1137. ctx.fillRect(0, 0, canvas.width, canvas.height);
  1138. }
  1139. }
  1140. function showCanvas() {
  1141. self.showMetrics();
  1142. $('#bufferedOut').show();
  1143. $('#bufferedCanvas').show();
  1144. }
  1145. function hideCanvas() {
  1146. self.hideMetrics();
  1147. $('#bufferedOut').hide();
  1148. $('#bufferedCanvas').hide();
  1149. }
  1150. function getMetrics() {
  1151. const json = JSON.stringify(events);
  1152. const jsonpacked = pack(json);
  1153. // console.log('packing JSON from ' + json.length + ' to ' + jsonpacked.length + ' bytes');
  1154. return btoa(jsonpacked);
  1155. }
  1156. self.copyMetricsToClipBoard = function () {
  1157. copyTextToClipboard(getMetrics());
  1158. };
  1159. self.goToMetrics = function () {
  1160. let url = document.URL;
  1161. url = url.slice(0, url.lastIndexOf('/') + 1) + 'metrics.html';
  1162. self.open(url, '_blank');
  1163. };
  1164. function goToMetricsPermaLink() {
  1165. let url = document.URL;
  1166. const b64 = getMetrics();
  1167. url = url.slice(0, url.lastIndexOf('/') + 1) + 'metrics.html#data=' + b64;
  1168. self.open(url, '_blank');
  1169. }
  1170. function onClickBufferedRange(event) {
  1171. const canvas = document.querySelector('#bufferedCanvas');
  1172. const target =
  1173. ((event.clientX - canvas.offsetLeft) / canvas.width) * getSeekableEnd();
  1174. video.currentTime = target;
  1175. }
  1176. function getSeekableEnd() {
  1177. if (isFinite(video.duration)) {
  1178. return video.duration;
  1179. }
  1180. if (video.seekable.length) {
  1181. return video.seekable.end(video.seekable.length - 1);
  1182. }
  1183. return 0;
  1184. }
  1185. function getLevelButtonHtml(key, levels, onclickReplace, autoEnabled) {
  1186. const onclickAuto = `${key}=-1`.replace(/^(\w+)=([^=]+)$/, onclickReplace);
  1187. const codecs = levels.reduce((uniqueCodecs, level) => {
  1188. const levelCodecs = codecs2label(level.attrs.CODECS);
  1189. if (levelCodecs && uniqueCodecs.indexOf(levelCodecs) === -1) {
  1190. uniqueCodecs.push(levelCodecs);
  1191. }
  1192. return uniqueCodecs;
  1193. }, []);
  1194. return (
  1195. `<button type="button" class="btn btn-sm ${
  1196. autoEnabled ? 'btn-primary' : 'btn-success'
  1197. }" onclick="${onclickAuto}">auto</button>` +
  1198. levels
  1199. .map((level, i) => {
  1200. const enabled = hls[key] === i;
  1201. const onclick = `${key}=${i}`.replace(/^(\w+)=(\w+)$/, onclickReplace);
  1202. const label = level2label(levels[i], i, codecs);
  1203. return `<button type="button" class="btn btn-sm ${
  1204. enabled ? 'btn-primary' : 'btn-success'
  1205. }" onclick="${onclick}">${label}</button>`;
  1206. })
  1207. .join('')
  1208. );
  1209. }
  1210. function updateLevelInfo() {
  1211. const levels = hls.levels;
  1212. if (!levels) {
  1213. return;
  1214. }
  1215. const htmlCurrentLevel = getLevelButtonHtml(
  1216. 'currentLevel',
  1217. levels,
  1218. 'hls.$1=$2',
  1219. hls.autoLevelEnabled
  1220. );
  1221. const htmlNextLevel = getLevelButtonHtml(
  1222. 'nextLevel',
  1223. levels,
  1224. 'hls.$1=$2',
  1225. hls.autoLevelEnabled
  1226. );
  1227. const htmlLoadLevel = getLevelButtonHtml(
  1228. 'loadLevel',
  1229. levels,
  1230. 'hls.$1=$2',
  1231. hls.autoLevelEnabled
  1232. );
  1233. const htmlCapLevel = getLevelButtonHtml(
  1234. 'autoLevelCapping',
  1235. levels,
  1236. 'levelCapping=hls.$1=$2;updateLevelInfo();onDemoConfigChanged();',
  1237. hls.autoLevelCapping === -1
  1238. );
  1239. if ($('#currentLevelControl').html() !== htmlCurrentLevel) {
  1240. $('#currentLevelControl').html(htmlCurrentLevel);
  1241. }
  1242. if ($('#nextLevelControl').html() !== htmlNextLevel) {
  1243. $('#nextLevelControl').html(htmlNextLevel);
  1244. }
  1245. if ($('#loadLevelControl').html() !== htmlLoadLevel) {
  1246. $('#loadLevelControl').html(htmlLoadLevel);
  1247. }
  1248. if ($('#levelCappingControl').html() !== htmlCapLevel) {
  1249. $('#levelCappingControl').html(htmlCapLevel);
  1250. }
  1251. }
  1252. function updateAudioTrackInfo() {
  1253. const buttonTemplate = '<button type="button" class="btn btn-sm ';
  1254. const buttonEnabled = 'btn-primary" ';
  1255. const buttonDisabled = 'btn-success" ';
  1256. let html1 = '';
  1257. const audioTrackId = hls.audioTrack;
  1258. const len = hls.audioTracks.length;
  1259. const track = hls.audioTracks[audioTrackId];
  1260. for (let i = 0; i < len; i++) {
  1261. html1 += buttonTemplate;
  1262. if (audioTrackId === i) {
  1263. html1 += buttonEnabled;
  1264. } else {
  1265. html1 += buttonDisabled;
  1266. }
  1267. html1 +=
  1268. 'onclick="hls.audioTrack=' +
  1269. i +
  1270. '">' +
  1271. hls.audioTracks[i].name +
  1272. '</button>';
  1273. }
  1274. $('#audioTrackLabel').text(
  1275. track ? track.lang || track.name : 'None selected'
  1276. );
  1277. $('#audioTrackControl').html(html1);
  1278. }
  1279. function codecs2label(levelCodecs) {
  1280. if (levelCodecs) {
  1281. return levelCodecs
  1282. .replace(/([ah]vc.)[^,;]+/, '$1')
  1283. .replace('mp4a.40.2', 'mp4a');
  1284. }
  1285. return '';
  1286. }
  1287. function level2label(level, i, manifestCodecs) {
  1288. const levelCodecs = codecs2label(level.attrs.CODECS);
  1289. const levelNameInfo = level.name ? `"${level.name}": ` : '';
  1290. const codecInfo =
  1291. levelCodecs && manifestCodecs.length > 1 ? ` / ${levelCodecs}` : '';
  1292. if (level.height) {
  1293. return `${i} (${levelNameInfo}${level.height}p / ${Math.round(
  1294. level.bitrate / 1024
  1295. )}kb${codecInfo})`;
  1296. }
  1297. if (level.bitrate) {
  1298. return `${i} (${levelNameInfo}${Math.round(
  1299. level.bitrate / 1024
  1300. )}kb${codecInfo})`;
  1301. }
  1302. if (codecInfo) {
  1303. return `${i} (${levelNameInfo}${levelCodecs})`;
  1304. }
  1305. if (level.name) {
  1306. return `${i} (${level.name})`;
  1307. }
  1308. return `${i}`;
  1309. }
  1310. function getDemoConfigPropOrDefault(propName, defaultVal) {
  1311. return typeof demoConfig[propName] !== 'undefined'
  1312. ? demoConfig[propName]
  1313. : defaultVal;
  1314. }
  1315. function getURLParam(sParam, defaultValue) {
  1316. const sPageURL = self.location.search.substring(1);
  1317. const sURLVariables = sPageURL.split('&');
  1318. for (let i = 0; i < sURLVariables.length; i++) {
  1319. const sParameterName = sURLVariables[i].split('=');
  1320. if (sParameterName[0] === sParam) {
  1321. return sParameterName[1] === 'undefined'
  1322. ? undefined
  1323. : sParameterName[1] === 'false'
  1324. ? false
  1325. : sParameterName[1];
  1326. }
  1327. }
  1328. return defaultValue;
  1329. }
  1330. function onDemoConfigChanged(firstLoad) {
  1331. demoConfig = {
  1332. enableStreaming,
  1333. autoRecoverError,
  1334. stopOnStall,
  1335. dumpfMP4,
  1336. levelCapping,
  1337. limitMetrics,
  1338. };
  1339. if (configPersistenceEnabled) {
  1340. persistEditorValue();
  1341. }
  1342. const serializedDemoConfig = btoa(JSON.stringify(demoConfig));
  1343. const baseURL = document.URL.split('?')[0];
  1344. const streamURL = $('#streamURL').val();
  1345. const permalinkURL = `${baseURL}?src=${encodeURIComponent(
  1346. streamURL
  1347. )}&demoConfig=${serializedDemoConfig}`;
  1348. $('#StreamPermalink').html(`<a href="${permalinkURL}">${permalinkURL}</a>`);
  1349. if (!firstLoad && window.location.href !== permalinkURL) {
  1350. window.history.pushState(null, null, permalinkURL);
  1351. }
  1352. }
  1353. function onConfigPersistenceChanged(event) {
  1354. configPersistenceEnabled = event.target.checked;
  1355. localStorage.setItem(
  1356. STORAGE_KEYS.Editor_Persistence,
  1357. JSON.stringify(configPersistenceEnabled)
  1358. );
  1359. if (configPersistenceEnabled) {
  1360. persistEditorValue();
  1361. } else {
  1362. localStorage.removeItem(STORAGE_KEYS.Hls_Config);
  1363. }
  1364. }
  1365. function getEditorValue(options) {
  1366. options = $.extend({ parse: false }, options || {});
  1367. let value = configEditor.session.getValue();
  1368. if (options.parse) {
  1369. try {
  1370. value = JSON.parse(value);
  1371. } catch (e) {
  1372. console.warn('[getEditorValue] could not parse editor value', e);
  1373. value = {};
  1374. }
  1375. }
  1376. return value;
  1377. }
  1378. function getPersistedHlsConfig() {
  1379. let value = localStorage.getItem(STORAGE_KEYS.Hls_Config);
  1380. if (value === null) {
  1381. return value;
  1382. }
  1383. try {
  1384. value = JSON.parse(value);
  1385. } catch (e) {
  1386. console.warn('[getPersistedHlsConfig] could not hls config json', e);
  1387. value = {};
  1388. }
  1389. return value;
  1390. }
  1391. function persistEditorValue() {
  1392. localStorage.setItem(STORAGE_KEYS.Hls_Config, getEditorValue());
  1393. }
  1394. function setupConfigEditor() {
  1395. configEditor = self.ace.edit('config-editor');
  1396. configEditor.setTheme('ace/theme/github');
  1397. configEditor.session.setMode('ace/mode/json');
  1398. const contents = hlsjsDefaults;
  1399. const shouldRestorePersisted =
  1400. JSON.parse(localStorage.getItem(STORAGE_KEYS.Editor_Persistence)) === true;
  1401. if (shouldRestorePersisted) {
  1402. $.extend(contents, getPersistedHlsConfig());
  1403. }
  1404. const elPersistence = document.querySelector('#config-persistence');
  1405. elPersistence.addEventListener('change', onConfigPersistenceChanged);
  1406. elPersistence.checked = shouldRestorePersisted;
  1407. configPersistenceEnabled = shouldRestorePersisted;
  1408. updateConfigEditorValue(contents);
  1409. }
  1410. function setupTimelineChart() {
  1411. const canvas = document.querySelector('#timeline-chart');
  1412. const chart = new TimelineChart(canvas, {
  1413. responsive: false,
  1414. });
  1415. resizeHandlers.push(() => {
  1416. chart.resize();
  1417. });
  1418. chart.resize();
  1419. return chart;
  1420. }
  1421. function addChartEventListeners(hls) {
  1422. const updateLevelOrTrack = (eventName, data) => {
  1423. chart.updateLevelOrTrack(data.details);
  1424. };
  1425. const updateFragment = (eventName, data) => {
  1426. if (data.stats) {
  1427. // Convert 0.x stats to partial v1 stats
  1428. const { retry, loaded, total, trequest, tfirst, tload } = data.stats;
  1429. if (trequest && tload) {
  1430. data.frag.stats = {
  1431. loaded,
  1432. retry,
  1433. total,
  1434. loading: {
  1435. start: trequest,
  1436. first: tfirst,
  1437. end: tload,
  1438. },
  1439. };
  1440. }
  1441. }
  1442. chart.updateFragment(data);
  1443. };
  1444. const updateChart = () => {
  1445. chart.update();
  1446. };
  1447. hls.on(
  1448. Hls.Events.MANIFEST_LOADING,
  1449. () => {
  1450. chart.reset();
  1451. },
  1452. chart
  1453. );
  1454. hls.on(
  1455. Hls.Events.MANIFEST_PARSED,
  1456. (eventName, data) => {
  1457. const { levels } = data;
  1458. chart.removeType('level');
  1459. chart.removeType('audioTrack');
  1460. chart.removeType('subtitleTrack');
  1461. chart.updateLevels(levels);
  1462. },
  1463. chart
  1464. );
  1465. hls.on(
  1466. Hls.Events.BUFFER_CREATED,
  1467. (eventName, { tracks }) => {
  1468. chart.updateSourceBuffers(tracks, hls.media);
  1469. },
  1470. chart
  1471. );
  1472. hls.on(
  1473. Hls.Events.BUFFER_RESET,
  1474. () => {
  1475. chart.removeSourceBuffers();
  1476. },
  1477. chart
  1478. );
  1479. hls.on(Hls.Events.LEVELS_UPDATED, (eventName, { levels }) => {
  1480. chart.removeType('level');
  1481. chart.updateLevels(levels);
  1482. });
  1483. hls.on(
  1484. Hls.Events.LEVEL_SWITCHED,
  1485. (eventName, { level }) => {
  1486. chart.removeType('level');
  1487. chart.updateLevels(hls.levels, level);
  1488. },
  1489. chart
  1490. );
  1491. hls.on(
  1492. Hls.Events.LEVEL_LOADING,
  1493. () => {
  1494. // TODO: mutate level datasets
  1495. // Update loadLevel
  1496. chart.removeType('level');
  1497. chart.updateLevels(hls.levels);
  1498. },
  1499. chart
  1500. );
  1501. hls.on(
  1502. Hls.Events.LEVEL_UPDATED,
  1503. (eventName, { details }) => {
  1504. chart.updateLevelOrTrack(details);
  1505. },
  1506. chart
  1507. );
  1508. hls.on(
  1509. Hls.Events.AUDIO_TRACKS_UPDATED,
  1510. (eventName, { audioTracks }) => {
  1511. chart.removeType('audioTrack');
  1512. chart.updateAudioTracks(audioTracks);
  1513. },
  1514. chart
  1515. );
  1516. hls.on(
  1517. Hls.Events.SUBTITLE_TRACKS_UPDATED,
  1518. (eventName, { subtitleTracks }) => {
  1519. chart.removeType('subtitleTrack');
  1520. chart.updateSubtitleTracks(subtitleTracks);
  1521. },
  1522. chart
  1523. );
  1524. hls.on(
  1525. Hls.Events.AUDIO_TRACK_SWITCHED,
  1526. (eventName) => {
  1527. // TODO: mutate level datasets
  1528. chart.removeType('audioTrack');
  1529. chart.updateAudioTracks(hls.audioTracks);
  1530. },
  1531. chart
  1532. );
  1533. hls.on(
  1534. Hls.Events.SUBTITLE_TRACK_SWITCH,
  1535. (eventName) => {
  1536. // TODO: mutate level datasets
  1537. chart.removeType('subtitleTrack');
  1538. chart.updateSubtitleTracks(hls.subtitleTracks);
  1539. },
  1540. chart
  1541. );
  1542. hls.on(Hls.Events.AUDIO_TRACK_LOADED, updateLevelOrTrack, chart);
  1543. hls.on(Hls.Events.SUBTITLE_TRACK_LOADED, updateLevelOrTrack, chart);
  1544. hls.on(Hls.Events.LEVEL_PTS_UPDATED, updateLevelOrTrack, chart);
  1545. hls.on(Hls.Events.FRAG_LOADED, updateFragment, chart);
  1546. hls.on(Hls.Events.FRAG_PARSED, updateFragment, chart);
  1547. hls.on(Hls.Events.FRAG_CHANGED, updateFragment, chart);
  1548. hls.on(Hls.Events.BUFFER_APPENDING, updateChart, chart);
  1549. hls.on(Hls.Events.BUFFER_APPENDED, updateChart, chart);
  1550. hls.on(Hls.Events.BUFFER_FLUSHED, updateChart, chart);
  1551. }
  1552. function updateConfigEditorValue(obj) {
  1553. const json = JSON.stringify(obj, null, 2);
  1554. configEditor.session.setValue(json);
  1555. }
  1556. function applyConfigEditorValue() {
  1557. onDemoConfigChanged();
  1558. loadSelectedStream();
  1559. }
  1560. function createfMP4(type) {
  1561. if (fmp4Data[type].length) {
  1562. const blob = new Blob([arrayConcat(fmp4Data[type])], {
  1563. type: 'application/octet-stream',
  1564. });
  1565. const filename = type + '-' + new Date().toISOString() + '.mp4';
  1566. self.saveAs(blob, filename);
  1567. // $('body').append('<a download="hlsjs-' + filename + '" href="' + self.URL.createObjectURL(blob) + '">Download ' + filename + ' track</a><br>');
  1568. } else if (!dumpfMP4) {
  1569. console.error(
  1570. 'Check "Dump transmuxed fMP4 data" first to make appended media available for saving.'
  1571. );
  1572. }
  1573. }
  1574. function arrayConcat(inputArray) {
  1575. const totalLength = inputArray.reduce(function (prev, cur) {
  1576. return prev + cur.length;
  1577. }, 0);
  1578. const result = new Uint8Array(totalLength);
  1579. let offset = 0;
  1580. inputArray.forEach(function (element) {
  1581. result.set(element, offset);
  1582. offset += element.length;
  1583. });
  1584. return result;
  1585. }
  1586. function hideAllTabs() {
  1587. $('.demo-tab-btn').css('background-color', '');
  1588. $('.demo-tab').hide();
  1589. }
  1590. function toggleTabClick(btn) {
  1591. toggleTab(btn);
  1592. const tabIndexes = $('.demo-tab-btn')
  1593. .toArray()
  1594. .map((el, i) => ($('#' + $(el).data('tab')).is(':visible') ? i : null))
  1595. .filter((i) => i !== null);
  1596. localStorage.setItem(STORAGE_KEYS.demo_tabs, tabIndexes.join(','));
  1597. }
  1598. function toggleTab(btn, dontHideOpenTabs) {
  1599. const tabElId = $(btn).data('tab');
  1600. // eslint-disable-next-line no-restricted-globals
  1601. const modifierPressed =
  1602. dontHideOpenTabs ||
  1603. (self.event && (self.event.metaKey || self.event.shiftKey));
  1604. if (!modifierPressed) {
  1605. hideAllTabs();
  1606. }
  1607. if (modifierPressed) {
  1608. $(`#${tabElId}`).toggle();
  1609. } else {
  1610. $(`#${tabElId}`).show();
  1611. }
  1612. $(btn).css(
  1613. 'background-color',
  1614. $(`#${tabElId}`).is(':visible') ? 'orange' : ''
  1615. );
  1616. if (!$('#statsDisplayTab').is(':visible')) {
  1617. self.hideMetrics();
  1618. }
  1619. if (hls) {
  1620. if ($('#timelineTab').is(':visible')) {
  1621. chart.show();
  1622. chart.resize(chart.chart.data ? chart.chart.data.datasets : null);
  1623. } else {
  1624. chart.hide();
  1625. }
  1626. }
  1627. }
  1628. function appendLog(textElId, message) {
  1629. const el = $('#' + textElId);
  1630. let logText = el.text();
  1631. if (logText.length) {
  1632. logText += '\n';
  1633. }
  1634. const timestamp = (Date.now() - startTime) / 1000;
  1635. const newMessage = timestamp + ' | ' + message;
  1636. logText += newMessage;
  1637. // update
  1638. el.text(logText);
  1639. const element = el[0];
  1640. element.scrollTop = element.scrollHeight - element.clientHeight;
  1641. }
  1642. function logStatus(message) {
  1643. appendLog('statusOut', message);
  1644. }
  1645. function logError(message) {
  1646. appendLog('errorOut', message);
  1647. }