Files
shaka-player/lib/hls/hls_utils.js
T
Martin Stark 2146c81f28 perf(HLS): Optimise manifest text parser (#9131)
These changes were made in an attempt to address issues with Shaka on
Chromecast getting stuck with buffer underruns while parsing large
manifests (~7k segments). Instead of parsing once, it gets stuck parsing
a manifest over and over on encrypted streams, each manifest parse
taking over ~1.2s.

The changes here did not solve the issue, but [I was asked to make a
PR](https://github.com/martinstark/shaka-player/commit/786d1370e566e0bdecf3ec8a847b62c6ebc1f7af).
While the manifest parsing is slow, it's appears not to be the root
cause of the issue we've been seeing.

In synthetic testing, with HLS manifests ranging from 100 to 100,000,000
segments on an AMD processor, Node22, arch linux, the updated manifest
parser uses somewhere between 12-40% less processor time. Assuming my
tests were representative.

I don't have enough real world data to say if this makes any significant
difference. I haven't run isolated manifest parsing tests on CC hardware
(yet, and not sure if I will have time).
2025-09-29 11:21:42 +02:00

173 lines
4.4 KiB
JavaScript

/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.hls.Utils');
goog.require('shaka.log');
goog.require('shaka.util.Error');
goog.require('shaka.util.ManifestParserUtils');
goog.requireType('shaka.hls.Tag');
shaka.hls.Utils = class {
/**
*
* @param {!Array<!shaka.hls.Tag>} tags
* @param {string} name
* @return {!Array<!shaka.hls.Tag>}
*/
static filterTagsByName(tags, name) {
return tags.filter((tag) => {
return tag.name == name;
});
}
/**
*
* @param {!Array<!shaka.hls.Tag>} tags
* @param {string} type
* @return {!Array<!shaka.hls.Tag>}
*/
static filterTagsByType(tags, type) {
return tags.filter((tag) => {
const tagType = tag.getRequiredAttrValue('TYPE');
return tagType == type;
});
}
/**
*
* @param {!Array<!shaka.hls.Tag>} tags
* @param {string} name
* @return {?shaka.hls.Tag}
*/
static getFirstTagWithName(tags, name) {
for (const tag of tags) {
if (tag.name === name) {
return tag;
}
}
return null;
}
/**
* Get the numerical value of the first tag with given name if available.
* Return the default value if the tag is not present.
*
* @param {!Array<!shaka.hls.Tag>} tags
* @param {string} name
* @param {number=} defaultValue
* @return {number}
*/
static getFirstTagWithNameAsNumber(tags, name, defaultValue = 0) {
const tag = shaka.hls.Utils.getFirstTagWithName(tags, name);
const value = tag ? Number(tag.value) : defaultValue;
return value;
}
/**
* @param {!Array<string>} baseUris
* @param {?string} relativeUri
* @param {?Map<string, string>=} variables
* @return {!Array<string>}
*/
static constructSegmentUris(baseUris, relativeUri, variables) {
if (!relativeUri) {
return [];
}
return shaka.hls.Utils.constructUris(baseUris, [relativeUri], variables);
}
/**
* @param {!Array<string>} baseUris
* @param {!Array<string>} relativeUris
* @param {?Map<string, string>=} variables
* @return {!Array<string>}
*/
static constructUris(baseUris, relativeUris, variables) {
if (!relativeUris.length) {
return [];
}
let newRelativeUris = relativeUris;
if (variables && variables.size) {
newRelativeUris = relativeUris.map((uri) => {
return shaka.hls.Utils.variableSubstitution(uri, variables);
});
}
return shaka.util.ManifestParserUtils.resolveUris(
baseUris, newRelativeUris);
}
/**
* Replaces the variables of a given URI.
*
* @param {string} uri
* @param {?Map<string, string>=} variables
* @return {string}
*/
static variableSubstitution(uri, variables) {
if (!variables || !variables.size) {
return uri;
}
let newUri = String(uri).replace(/%7B/g, '{').replace(/%7D/g, '}');
const uriVariables = newUri.match(/{\$\w*}/g);
if (uriVariables) {
for (const variable of uriVariables) {
// Note: All variables have the structure {$...}
const variableName = variable.slice(2, variable.length - 1);
const replaceValue = variables.get(variableName);
if (replaceValue) {
newUri = newUri.replace(variable, replaceValue);
} else {
shaka.log.error('A variable has been found that is not declared',
variableName);
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.HLS_VARIABLE_NOT_FOUND,
variableName);
}
}
}
return newUri;
}
/**
* Matches a string to an HLS comment format and returns the result.
*
* @param {string} line
* @return {boolean}
*/
static isComment(line) {
return line.startsWith('#') && !line.startsWith('#EXT');
}
/**
* Given an HLS tag, gets the supplemental codec string without the
* supplemental profile.
*
* @param {shaka.hls.Tag} tag
* @return {string}
*/
static getSupplementalCodecs(tag) {
const supplementalCodecsString =
tag.getAttributeValue('SUPPLEMENTAL-CODECS');
if (!supplementalCodecsString) {
return '';
}
const supplementalCodecs = supplementalCodecsString.split(/\s*,\s*/)
.map((codec) => {
return codec.split('/')[0];
});
return supplementalCodecs.join(',');
}
};