@zenvor/hls.js 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.md +472 -0
- package/dist/hls-demo.js +26995 -0
- package/dist/hls-demo.js.map +1 -0
- package/dist/hls.d.mts +4204 -0
- package/dist/hls.d.ts +4204 -0
- package/dist/hls.js +40050 -0
- package/dist/hls.js.d.ts +4204 -0
- package/dist/hls.js.map +1 -0
- package/dist/hls.light.js +27145 -0
- package/dist/hls.light.js.map +1 -0
- package/dist/hls.light.min.js +2 -0
- package/dist/hls.light.min.js.map +1 -0
- package/dist/hls.light.mjs +26392 -0
- package/dist/hls.light.mjs.map +1 -0
- package/dist/hls.min.js +2 -0
- package/dist/hls.min.js.map +1 -0
- package/dist/hls.mjs +38956 -0
- package/dist/hls.mjs.map +1 -0
- package/dist/hls.worker.js +2 -0
- package/dist/hls.worker.js.map +1 -0
- package/package.json +143 -0
- package/src/config.ts +794 -0
- package/src/controller/abr-controller.ts +1019 -0
- package/src/controller/algo-data-controller.ts +794 -0
- package/src/controller/audio-stream-controller.ts +1099 -0
- package/src/controller/audio-track-controller.ts +454 -0
- package/src/controller/base-playlist-controller.ts +438 -0
- package/src/controller/base-stream-controller.ts +2526 -0
- package/src/controller/buffer-controller.ts +2015 -0
- package/src/controller/buffer-operation-queue.ts +159 -0
- package/src/controller/cap-level-controller.ts +367 -0
- package/src/controller/cmcd-controller.ts +422 -0
- package/src/controller/content-steering-controller.ts +622 -0
- package/src/controller/eme-controller.ts +1617 -0
- package/src/controller/error-controller.ts +627 -0
- package/src/controller/fps-controller.ts +146 -0
- package/src/controller/fragment-finders.ts +256 -0
- package/src/controller/fragment-tracker.ts +567 -0
- package/src/controller/gap-controller.ts +719 -0
- package/src/controller/id3-track-controller.ts +488 -0
- package/src/controller/interstitial-player.ts +302 -0
- package/src/controller/interstitials-controller.ts +2895 -0
- package/src/controller/interstitials-schedule.ts +698 -0
- package/src/controller/latency-controller.ts +294 -0
- package/src/controller/level-controller.ts +776 -0
- package/src/controller/stream-controller.ts +1597 -0
- package/src/controller/subtitle-stream-controller.ts +508 -0
- package/src/controller/subtitle-track-controller.ts +617 -0
- package/src/controller/timeline-controller.ts +677 -0
- package/src/crypt/aes-crypto.ts +36 -0
- package/src/crypt/aes-decryptor.ts +339 -0
- package/src/crypt/decrypter-aes-mode.ts +4 -0
- package/src/crypt/decrypter.ts +225 -0
- package/src/crypt/fast-aes-key.ts +39 -0
- package/src/define-plugin.d.ts +17 -0
- package/src/demux/audio/aacdemuxer.ts +126 -0
- package/src/demux/audio/ac3-demuxer.ts +170 -0
- package/src/demux/audio/adts.ts +249 -0
- package/src/demux/audio/base-audio-demuxer.ts +205 -0
- package/src/demux/audio/dolby.ts +21 -0
- package/src/demux/audio/mp3demuxer.ts +85 -0
- package/src/demux/audio/mpegaudio.ts +177 -0
- package/src/demux/chunk-cache.ts +42 -0
- package/src/demux/dummy-demuxed-track.ts +13 -0
- package/src/demux/inject-worker.ts +75 -0
- package/src/demux/mp4demuxer.ts +234 -0
- package/src/demux/sample-aes.ts +198 -0
- package/src/demux/transmuxer-interface.ts +449 -0
- package/src/demux/transmuxer-worker.ts +221 -0
- package/src/demux/transmuxer.ts +560 -0
- package/src/demux/tsdemuxer.ts +1256 -0
- package/src/demux/video/avc-video-parser.ts +401 -0
- package/src/demux/video/base-video-parser.ts +198 -0
- package/src/demux/video/exp-golomb.ts +153 -0
- package/src/demux/video/hevc-video-parser.ts +736 -0
- package/src/empty-es.js +5 -0
- package/src/empty.js +3 -0
- package/src/errors.ts +107 -0
- package/src/events.ts +548 -0
- package/src/exports-default.ts +3 -0
- package/src/exports-named.ts +81 -0
- package/src/hls.ts +1613 -0
- package/src/is-supported.ts +54 -0
- package/src/loader/date-range.ts +207 -0
- package/src/loader/fragment-loader.ts +403 -0
- package/src/loader/fragment.ts +487 -0
- package/src/loader/interstitial-asset-list.ts +162 -0
- package/src/loader/interstitial-event.ts +337 -0
- package/src/loader/key-loader.ts +439 -0
- package/src/loader/level-details.ts +203 -0
- package/src/loader/level-key.ts +259 -0
- package/src/loader/load-stats.ts +17 -0
- package/src/loader/m3u8-parser.ts +1072 -0
- package/src/loader/playlist-loader.ts +839 -0
- package/src/polyfills/number.ts +15 -0
- package/src/remux/aac-helper.ts +81 -0
- package/src/remux/mp4-generator.ts +1380 -0
- package/src/remux/mp4-remuxer.ts +1261 -0
- package/src/remux/passthrough-remuxer.ts +434 -0
- package/src/task-loop.ts +130 -0
- package/src/types/algo.ts +44 -0
- package/src/types/buffer.ts +105 -0
- package/src/types/component-api.ts +20 -0
- package/src/types/demuxer.ts +208 -0
- package/src/types/events.ts +574 -0
- package/src/types/fragment-tracker.ts +23 -0
- package/src/types/level.ts +268 -0
- package/src/types/loader.ts +198 -0
- package/src/types/media-playlist.ts +92 -0
- package/src/types/network-details.ts +3 -0
- package/src/types/remuxer.ts +104 -0
- package/src/types/track.ts +12 -0
- package/src/types/transmuxer.ts +46 -0
- package/src/types/tuples.ts +6 -0
- package/src/types/vtt.ts +11 -0
- package/src/utils/arrays.ts +22 -0
- package/src/utils/attr-list.ts +192 -0
- package/src/utils/binary-search.ts +46 -0
- package/src/utils/buffer-helper.ts +173 -0
- package/src/utils/cea-608-parser.ts +1413 -0
- package/src/utils/chunker.ts +41 -0
- package/src/utils/codecs.ts +314 -0
- package/src/utils/cues.ts +96 -0
- package/src/utils/discontinuities.ts +174 -0
- package/src/utils/encryption-methods-util.ts +21 -0
- package/src/utils/error-helper.ts +95 -0
- package/src/utils/event-listener-helper.ts +16 -0
- package/src/utils/ewma-bandwidth-estimator.ts +97 -0
- package/src/utils/ewma.ts +43 -0
- package/src/utils/fetch-loader.ts +331 -0
- package/src/utils/global.ts +2 -0
- package/src/utils/hash.ts +10 -0
- package/src/utils/hdr.ts +67 -0
- package/src/utils/hex.ts +32 -0
- package/src/utils/imsc1-ttml-parser.ts +261 -0
- package/src/utils/keysystem-util.ts +45 -0
- package/src/utils/level-helper.ts +629 -0
- package/src/utils/logger.ts +120 -0
- package/src/utils/media-option-attributes.ts +49 -0
- package/src/utils/mediacapabilities-helper.ts +301 -0
- package/src/utils/mediakeys-helper.ts +210 -0
- package/src/utils/mediasource-helper.ts +37 -0
- package/src/utils/mp4-tools.ts +1473 -0
- package/src/utils/number.ts +3 -0
- package/src/utils/numeric-encoding-utils.ts +26 -0
- package/src/utils/output-filter.ts +46 -0
- package/src/utils/rendition-helper.ts +505 -0
- package/src/utils/safe-json-stringify.ts +22 -0
- package/src/utils/texttrack-utils.ts +164 -0
- package/src/utils/time-ranges.ts +17 -0
- package/src/utils/timescale-conversion.ts +46 -0
- package/src/utils/utf8-utils.ts +18 -0
- package/src/utils/variable-substitution.ts +105 -0
- package/src/utils/vttcue.ts +384 -0
- package/src/utils/vttparser.ts +497 -0
- package/src/utils/webvtt-parser.ts +166 -0
- package/src/utils/xhr-loader.ts +337 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export interface ILogFunction {
|
|
2
|
+
(message?: any, ...optionalParams: any[]): void;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface ILogger {
|
|
6
|
+
trace: ILogFunction;
|
|
7
|
+
debug: ILogFunction;
|
|
8
|
+
log: ILogFunction;
|
|
9
|
+
warn: ILogFunction;
|
|
10
|
+
info: ILogFunction;
|
|
11
|
+
error: ILogFunction;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class Logger implements ILogger {
|
|
15
|
+
trace: ILogFunction;
|
|
16
|
+
debug: ILogFunction;
|
|
17
|
+
log: ILogFunction;
|
|
18
|
+
warn: ILogFunction;
|
|
19
|
+
info: ILogFunction;
|
|
20
|
+
error: ILogFunction;
|
|
21
|
+
|
|
22
|
+
constructor(label: string, logger: ILogger) {
|
|
23
|
+
const lb = `[${label}]:`;
|
|
24
|
+
this.trace = noop;
|
|
25
|
+
this.debug = logger.debug.bind(null, lb);
|
|
26
|
+
this.log = logger.log.bind(null, lb);
|
|
27
|
+
this.warn = logger.warn.bind(null, lb);
|
|
28
|
+
this.info = logger.info.bind(null, lb);
|
|
29
|
+
this.error = logger.error.bind(null, lb);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const noop: ILogFunction = function () {};
|
|
34
|
+
|
|
35
|
+
const fakeLogger: ILogger = {
|
|
36
|
+
trace: noop,
|
|
37
|
+
debug: noop,
|
|
38
|
+
log: noop,
|
|
39
|
+
warn: noop,
|
|
40
|
+
info: noop,
|
|
41
|
+
error: noop,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function createLogger() {
|
|
45
|
+
return Object.assign({}, fakeLogger);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// let lastCallTime;
|
|
49
|
+
// function formatMsgWithTimeInfo(type, msg) {
|
|
50
|
+
// const now = Date.now();
|
|
51
|
+
// const diff = lastCallTime ? '+' + (now - lastCallTime) : '0';
|
|
52
|
+
// lastCallTime = now;
|
|
53
|
+
// msg = (new Date(now)).toISOString() + ' | [' + type + '] > ' + msg + ' ( ' + diff + ' ms )';
|
|
54
|
+
// return msg;
|
|
55
|
+
// }
|
|
56
|
+
|
|
57
|
+
function consolePrintFn(type: string, id: string | undefined): ILogFunction {
|
|
58
|
+
const func: ILogFunction = self.console[type];
|
|
59
|
+
return func
|
|
60
|
+
? func.bind(self.console, `${id ? '[' + id + '] ' : ''}[${type}] >`)
|
|
61
|
+
: noop;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getLoggerFn(
|
|
65
|
+
key: string,
|
|
66
|
+
debugConfig: boolean | Partial<ILogger>,
|
|
67
|
+
id?: string,
|
|
68
|
+
): ILogFunction {
|
|
69
|
+
return debugConfig[key]
|
|
70
|
+
? debugConfig[key].bind(debugConfig)
|
|
71
|
+
: consolePrintFn(key, id);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const exportedLogger: ILogger = createLogger();
|
|
75
|
+
|
|
76
|
+
export function enableLogs(
|
|
77
|
+
debugConfig: boolean | ILogger,
|
|
78
|
+
context: string,
|
|
79
|
+
id?: string | undefined,
|
|
80
|
+
): ILogger {
|
|
81
|
+
// check that console is available
|
|
82
|
+
const newLogger = createLogger();
|
|
83
|
+
if (
|
|
84
|
+
(typeof console === 'object' && debugConfig === true) ||
|
|
85
|
+
typeof debugConfig === 'object'
|
|
86
|
+
) {
|
|
87
|
+
const keys: (keyof ILogger)[] = [
|
|
88
|
+
// Remove out from list here to hard-disable a log-level
|
|
89
|
+
// 'trace',
|
|
90
|
+
'debug',
|
|
91
|
+
'log',
|
|
92
|
+
'info',
|
|
93
|
+
'warn',
|
|
94
|
+
'error',
|
|
95
|
+
];
|
|
96
|
+
keys.forEach((key) => {
|
|
97
|
+
newLogger[key] = getLoggerFn(key, debugConfig, id);
|
|
98
|
+
});
|
|
99
|
+
// Some browsers don't allow to use bind on console object anyway
|
|
100
|
+
// fallback to default if needed
|
|
101
|
+
try {
|
|
102
|
+
newLogger.log(
|
|
103
|
+
`Debug logs enabled for "${context}" in hls.js version ${__VERSION__}`,
|
|
104
|
+
);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
/* log fn threw an exception. All logger methods are no-ops. */
|
|
107
|
+
return createLogger();
|
|
108
|
+
}
|
|
109
|
+
// global exported logger uses the same functions as new logger without `id`
|
|
110
|
+
keys.forEach((key) => {
|
|
111
|
+
exportedLogger[key] = getLoggerFn(key, debugConfig);
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
// Reset global exported logger
|
|
115
|
+
Object.assign(exportedLogger, newLogger);
|
|
116
|
+
}
|
|
117
|
+
return newLogger;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const logger: ILogger = exportedLogger;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Level } from '../types/level';
|
|
2
|
+
import type { MediaAttributes, MediaPlaylist } from '../types/media-playlist';
|
|
3
|
+
|
|
4
|
+
export function subtitleOptionsIdentical(
|
|
5
|
+
trackList1: MediaPlaylist[] | Level[],
|
|
6
|
+
trackList2: MediaPlaylist[],
|
|
7
|
+
): boolean {
|
|
8
|
+
if (trackList1.length !== trackList2.length) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
for (let i = 0; i < trackList1.length; i++) {
|
|
12
|
+
if (
|
|
13
|
+
!mediaAttributesIdentical(
|
|
14
|
+
trackList1[i].attrs as MediaAttributes,
|
|
15
|
+
trackList2[i].attrs,
|
|
16
|
+
)
|
|
17
|
+
) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function mediaAttributesIdentical(
|
|
25
|
+
attrs1: MediaAttributes,
|
|
26
|
+
attrs2: MediaAttributes,
|
|
27
|
+
customAttributes?: string[],
|
|
28
|
+
): boolean {
|
|
29
|
+
// Media options with the same rendition ID must be bit identical
|
|
30
|
+
const stableRenditionId = attrs1['STABLE-RENDITION-ID'];
|
|
31
|
+
if (stableRenditionId && !customAttributes) {
|
|
32
|
+
return stableRenditionId === attrs2['STABLE-RENDITION-ID'];
|
|
33
|
+
}
|
|
34
|
+
// When rendition ID is not present, compare attributes
|
|
35
|
+
return !(
|
|
36
|
+
customAttributes || [
|
|
37
|
+
'LANGUAGE',
|
|
38
|
+
'NAME',
|
|
39
|
+
'CHARACTERISTICS',
|
|
40
|
+
'AUTOSELECT',
|
|
41
|
+
'DEFAULT',
|
|
42
|
+
'FORCED',
|
|
43
|
+
'ASSOC-LANGUAGE',
|
|
44
|
+
]
|
|
45
|
+
).some(
|
|
46
|
+
(subtitleAttribute) =>
|
|
47
|
+
attrs1[subtitleAttribute] !== attrs2[subtitleAttribute],
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fillInMissingAV01Params,
|
|
3
|
+
getCodecsForMimeType,
|
|
4
|
+
mimeTypeForCodec,
|
|
5
|
+
userAgentHevcSupportIsInaccurate,
|
|
6
|
+
} from './codecs';
|
|
7
|
+
import { isHEVC } from './mp4-tools';
|
|
8
|
+
import type { AudioTracksByGroup } from './rendition-helper';
|
|
9
|
+
import type { Level, VideoRange } from '../types/level';
|
|
10
|
+
import type { AudioSelectionOption } from '../types/media-playlist';
|
|
11
|
+
|
|
12
|
+
export type MediaDecodingInfo = {
|
|
13
|
+
supported: boolean;
|
|
14
|
+
configurations: readonly MediaDecodingConfiguration[];
|
|
15
|
+
decodingInfoResults: readonly MediaCapabilitiesDecodingInfo[];
|
|
16
|
+
error?: Error;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
const supportedResult: MediaCapabilitiesDecodingInfo = {
|
|
21
|
+
supported: true,
|
|
22
|
+
powerEfficient: true,
|
|
23
|
+
smooth: true,
|
|
24
|
+
// keySystemAccess: null,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
const unsupportedResult: MediaCapabilitiesDecodingInfo = {
|
|
29
|
+
supported: false,
|
|
30
|
+
smooth: false,
|
|
31
|
+
powerEfficient: false,
|
|
32
|
+
// keySystemAccess: null,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const SUPPORTED_INFO_DEFAULT: MediaDecodingInfo = {
|
|
36
|
+
supported: true,
|
|
37
|
+
configurations: [] as MediaDecodingConfiguration[],
|
|
38
|
+
decodingInfoResults: [supportedResult],
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
export function getUnsupportedResult(
|
|
42
|
+
error: Error,
|
|
43
|
+
configurations: MediaDecodingConfiguration[],
|
|
44
|
+
): MediaDecodingInfo {
|
|
45
|
+
return {
|
|
46
|
+
supported: false,
|
|
47
|
+
configurations,
|
|
48
|
+
decodingInfoResults: [unsupportedResult],
|
|
49
|
+
error,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function requiresMediaCapabilitiesDecodingInfo(
|
|
54
|
+
level: Level,
|
|
55
|
+
audioTracksByGroup: AudioTracksByGroup,
|
|
56
|
+
currentVideoRange: VideoRange | undefined,
|
|
57
|
+
currentFrameRate: number,
|
|
58
|
+
currentBw: number,
|
|
59
|
+
audioPreference: AudioSelectionOption | undefined,
|
|
60
|
+
): boolean {
|
|
61
|
+
// Only test support when configuration is exceeds minimum options
|
|
62
|
+
const videoCodecs = level.videoCodec;
|
|
63
|
+
const audioGroups = level.audioCodec ? level.audioGroups : null;
|
|
64
|
+
const audioCodecPreference = audioPreference?.audioCodec;
|
|
65
|
+
const channelsPreference = audioPreference?.channels;
|
|
66
|
+
const maxChannels = channelsPreference
|
|
67
|
+
? parseInt(channelsPreference)
|
|
68
|
+
: audioCodecPreference
|
|
69
|
+
? Infinity
|
|
70
|
+
: 2;
|
|
71
|
+
let audioChannels: Record<string, number> | null = null;
|
|
72
|
+
if (audioGroups?.length) {
|
|
73
|
+
try {
|
|
74
|
+
if (audioGroups.length === 1 && audioGroups[0]) {
|
|
75
|
+
audioChannels = audioTracksByGroup.groups[audioGroups[0]].channels;
|
|
76
|
+
} else {
|
|
77
|
+
audioChannels = audioGroups.reduce(
|
|
78
|
+
(acc, groupId) => {
|
|
79
|
+
if (groupId) {
|
|
80
|
+
const audioTrackGroup = audioTracksByGroup.groups[groupId];
|
|
81
|
+
if (!audioTrackGroup) {
|
|
82
|
+
throw new Error(`Audio track group ${groupId} not found`);
|
|
83
|
+
}
|
|
84
|
+
// Sum all channel key values
|
|
85
|
+
Object.keys(audioTrackGroup.channels).forEach((key) => {
|
|
86
|
+
acc[key] = (acc[key] || 0) + audioTrackGroup.channels[key];
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return acc;
|
|
90
|
+
},
|
|
91
|
+
{ 2: 0 },
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return (
|
|
99
|
+
(videoCodecs !== undefined &&
|
|
100
|
+
// Force media capabilities check for HEVC to avoid failure on Windows
|
|
101
|
+
(videoCodecs.split(',').some((videoCodec) => isHEVC(videoCodec)) ||
|
|
102
|
+
(level.width > 1920 && level.height > 1088) ||
|
|
103
|
+
(level.height > 1920 && level.width > 1088) ||
|
|
104
|
+
level.frameRate > Math.max(currentFrameRate, 30) ||
|
|
105
|
+
(level.videoRange !== 'SDR' &&
|
|
106
|
+
level.videoRange !== currentVideoRange) ||
|
|
107
|
+
level.bitrate > Math.max(currentBw, 8e6))) ||
|
|
108
|
+
(!!audioChannels &&
|
|
109
|
+
Number.isFinite(maxChannels) &&
|
|
110
|
+
Object.keys(audioChannels).some(
|
|
111
|
+
(channels) => parseInt(channels) > maxChannels,
|
|
112
|
+
))
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getMediaDecodingInfoPromise(
|
|
117
|
+
level: Level,
|
|
118
|
+
audioTracksByGroup: AudioTracksByGroup,
|
|
119
|
+
mediaCapabilities: MediaCapabilities | undefined,
|
|
120
|
+
cache: Record<
|
|
121
|
+
string,
|
|
122
|
+
Promise<MediaCapabilitiesDecodingInfo> | undefined
|
|
123
|
+
> = {},
|
|
124
|
+
): Promise<MediaDecodingInfo> {
|
|
125
|
+
const videoCodecs = level.videoCodec;
|
|
126
|
+
if ((!videoCodecs && !level.audioCodec) || !mediaCapabilities) {
|
|
127
|
+
return Promise.resolve(SUPPORTED_INFO_DEFAULT);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const configurations: MediaDecodingConfiguration[] = [];
|
|
131
|
+
|
|
132
|
+
const videoDecodeList = makeVideoConfigurations(level);
|
|
133
|
+
const videoCount = videoDecodeList.length;
|
|
134
|
+
const audioDecodeList = makeAudioConfigurations(
|
|
135
|
+
level,
|
|
136
|
+
audioTracksByGroup,
|
|
137
|
+
videoCount > 0,
|
|
138
|
+
);
|
|
139
|
+
const audioCount = audioDecodeList.length;
|
|
140
|
+
for (let i = videoCount || 1 * audioCount || 1; i--; ) {
|
|
141
|
+
const configuration: MediaDecodingConfiguration = {
|
|
142
|
+
type: 'media-source',
|
|
143
|
+
};
|
|
144
|
+
if (videoCount) {
|
|
145
|
+
configuration.video = videoDecodeList[i % videoCount];
|
|
146
|
+
}
|
|
147
|
+
if (audioCount) {
|
|
148
|
+
configuration.audio = audioDecodeList[i % audioCount];
|
|
149
|
+
const audioBitrate = configuration.audio.bitrate;
|
|
150
|
+
if (configuration.video && audioBitrate) {
|
|
151
|
+
configuration.video.bitrate -= audioBitrate;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
configurations.push(configuration);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (videoCodecs) {
|
|
158
|
+
// Override Windows Firefox HEVC MediaCapabilities result (https://github.com/video-dev/hls.js/issues/7046)
|
|
159
|
+
const ua = navigator.userAgent;
|
|
160
|
+
if (
|
|
161
|
+
videoCodecs.split(',').some((videoCodec) => isHEVC(videoCodec)) &&
|
|
162
|
+
userAgentHevcSupportIsInaccurate()
|
|
163
|
+
) {
|
|
164
|
+
return Promise.resolve(
|
|
165
|
+
getUnsupportedResult(
|
|
166
|
+
new Error(
|
|
167
|
+
`Overriding Windows Firefox HEVC MediaCapabilities result based on user-agent string: (${ua})`,
|
|
168
|
+
),
|
|
169
|
+
configurations,
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return Promise.all(
|
|
176
|
+
configurations.map((configuration) => {
|
|
177
|
+
// Cache MediaCapabilities promises
|
|
178
|
+
const decodingInfoKey = getMediaDecodingInfoKey(configuration);
|
|
179
|
+
return (
|
|
180
|
+
cache[decodingInfoKey] ||
|
|
181
|
+
(cache[decodingInfoKey] = mediaCapabilities.decodingInfo(configuration))
|
|
182
|
+
);
|
|
183
|
+
}),
|
|
184
|
+
)
|
|
185
|
+
.then((decodingInfoResults) => ({
|
|
186
|
+
supported: !decodingInfoResults.some((info) => !info.supported),
|
|
187
|
+
configurations,
|
|
188
|
+
decodingInfoResults,
|
|
189
|
+
}))
|
|
190
|
+
.catch((error) => ({
|
|
191
|
+
supported: false,
|
|
192
|
+
configurations,
|
|
193
|
+
decodingInfoResults: [] as MediaCapabilitiesDecodingInfo[],
|
|
194
|
+
error,
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function makeVideoConfigurations(level: Level): VideoConfiguration[] {
|
|
199
|
+
const videoCodecs = level.videoCodec?.split(',');
|
|
200
|
+
const bitrate = getVariantDecodingBitrate(level);
|
|
201
|
+
const width = level.width || 640;
|
|
202
|
+
const height = level.height || 480;
|
|
203
|
+
// Assume a framerate of 30fps since MediaCapabilities will not accept Level default of 0.
|
|
204
|
+
const framerate = level.frameRate || 30;
|
|
205
|
+
const videoRange = level.videoRange.toLowerCase() as 'sdr' | 'pq' | 'hlg';
|
|
206
|
+
return videoCodecs
|
|
207
|
+
? videoCodecs.map((videoCodec: string) => {
|
|
208
|
+
const videoConfiguration: VideoConfiguration = {
|
|
209
|
+
contentType: mimeTypeForCodec(
|
|
210
|
+
fillInMissingAV01Params(videoCodec),
|
|
211
|
+
'video',
|
|
212
|
+
),
|
|
213
|
+
width,
|
|
214
|
+
height,
|
|
215
|
+
bitrate,
|
|
216
|
+
framerate,
|
|
217
|
+
};
|
|
218
|
+
if (videoRange !== 'sdr') {
|
|
219
|
+
videoConfiguration.transferFunction = videoRange as TransferFunction;
|
|
220
|
+
}
|
|
221
|
+
return videoConfiguration;
|
|
222
|
+
})
|
|
223
|
+
: [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function makeAudioConfigurations(
|
|
227
|
+
level: Level,
|
|
228
|
+
audioTracksByGroup: AudioTracksByGroup,
|
|
229
|
+
hasVideo: boolean,
|
|
230
|
+
): AudioConfiguration[] {
|
|
231
|
+
const audioCodecs = level.audioCodec?.split(',');
|
|
232
|
+
const combinedBitrate = getVariantDecodingBitrate(level);
|
|
233
|
+
if (audioCodecs && level.audioGroups) {
|
|
234
|
+
return level.audioGroups.reduce((configurations, audioGroupId) => {
|
|
235
|
+
const tracks = audioGroupId
|
|
236
|
+
? audioTracksByGroup.groups[audioGroupId]?.tracks
|
|
237
|
+
: null;
|
|
238
|
+
if (tracks) {
|
|
239
|
+
return tracks.reduce((configs, audioTrack) => {
|
|
240
|
+
if (audioTrack.groupId === audioGroupId) {
|
|
241
|
+
const channelsNumber = parseFloat(audioTrack.channels || '');
|
|
242
|
+
audioCodecs.forEach((audioCodec) => {
|
|
243
|
+
const audioConfiguration: AudioConfiguration = {
|
|
244
|
+
contentType: mimeTypeForCodec(audioCodec, 'audio'),
|
|
245
|
+
bitrate: hasVideo
|
|
246
|
+
? estimatedAudioBitrate(audioCodec, combinedBitrate)
|
|
247
|
+
: combinedBitrate,
|
|
248
|
+
};
|
|
249
|
+
if (channelsNumber) {
|
|
250
|
+
audioConfiguration.channels = '' + channelsNumber;
|
|
251
|
+
}
|
|
252
|
+
configs.push(audioConfiguration);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return configs;
|
|
256
|
+
}, configurations);
|
|
257
|
+
}
|
|
258
|
+
return configurations;
|
|
259
|
+
}, [] as AudioConfiguration[]);
|
|
260
|
+
}
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function estimatedAudioBitrate(
|
|
265
|
+
audioCodec: string | undefined,
|
|
266
|
+
levelBitrate: number,
|
|
267
|
+
): number {
|
|
268
|
+
if (levelBitrate <= 1) {
|
|
269
|
+
return 1;
|
|
270
|
+
}
|
|
271
|
+
let audioBitrate = 128000;
|
|
272
|
+
if (audioCodec === 'ec-3') {
|
|
273
|
+
audioBitrate = 768000;
|
|
274
|
+
} else if (audioCodec === 'ac-3') {
|
|
275
|
+
audioBitrate = 640000;
|
|
276
|
+
}
|
|
277
|
+
return Math.min(levelBitrate / 2, audioBitrate); // Don't exceed some % of level bitrate
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function getVariantDecodingBitrate(level: Level): number {
|
|
281
|
+
return (
|
|
282
|
+
Math.ceil(Math.max(level.bitrate * 0.9, level.averageBitrate) / 1000) *
|
|
283
|
+
1000 || 1
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function getMediaDecodingInfoKey(config: MediaDecodingConfiguration): string {
|
|
288
|
+
let key = '';
|
|
289
|
+
const { audio, video } = config;
|
|
290
|
+
if (video) {
|
|
291
|
+
const codec = getCodecsForMimeType(video.contentType);
|
|
292
|
+
key += `${codec}_r${video.height}x${video.width}f${Math.ceil(video.framerate)}${
|
|
293
|
+
video.transferFunction || 'sd'
|
|
294
|
+
}_${Math.ceil(video.bitrate / 1e5)}`;
|
|
295
|
+
}
|
|
296
|
+
if (audio) {
|
|
297
|
+
const codec = getCodecsForMimeType(audio.contentType);
|
|
298
|
+
key += `${video ? '_' : ''}${codec}_c${audio.channels}`;
|
|
299
|
+
}
|
|
300
|
+
return key;
|
|
301
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { optionalSelf } from './global';
|
|
2
|
+
import { changeEndianness } from './keysystem-util';
|
|
3
|
+
import { base64Decode } from './numeric-encoding-utils';
|
|
4
|
+
import type { DRMSystemOptions, EMEControllerConfig } from '../config';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/requestMediaKeySystemAccess
|
|
8
|
+
*/
|
|
9
|
+
export const enum KeySystems {
|
|
10
|
+
CLEARKEY = 'org.w3.clearkey',
|
|
11
|
+
FAIRPLAY = 'com.apple.fps',
|
|
12
|
+
PLAYREADY = 'com.microsoft.playready',
|
|
13
|
+
WIDEVINE = 'com.widevine.alpha',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Playlist #EXT-X-KEY KEYFORMAT values
|
|
17
|
+
export const enum KeySystemFormats {
|
|
18
|
+
CLEARKEY = 'org.w3.clearkey',
|
|
19
|
+
FAIRPLAY = 'com.apple.streamingkeydelivery',
|
|
20
|
+
PLAYREADY = 'com.microsoft.playready',
|
|
21
|
+
WIDEVINE = 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed',
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function keySystemFormatToKeySystemDomain(
|
|
25
|
+
format: KeySystemFormats,
|
|
26
|
+
): KeySystems | undefined {
|
|
27
|
+
switch (format) {
|
|
28
|
+
case KeySystemFormats.FAIRPLAY:
|
|
29
|
+
return KeySystems.FAIRPLAY;
|
|
30
|
+
case KeySystemFormats.PLAYREADY:
|
|
31
|
+
return KeySystems.PLAYREADY;
|
|
32
|
+
case KeySystemFormats.WIDEVINE:
|
|
33
|
+
return KeySystems.WIDEVINE;
|
|
34
|
+
case KeySystemFormats.CLEARKEY:
|
|
35
|
+
return KeySystems.CLEARKEY;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// System IDs for which we can extract a key ID from "encrypted" event PSSH
|
|
40
|
+
export const enum KeySystemIds {
|
|
41
|
+
CENC = '1077efecc0b24d02ace33c1e52e2fb4b',
|
|
42
|
+
CLEARKEY = 'e2719d58a985b3c9781ab030af78d30e',
|
|
43
|
+
FAIRPLAY = '94ce86fb07ff4f43adb893d2fa968ca2',
|
|
44
|
+
PLAYREADY = '9a04f07998404286ab92e65be0885f95',
|
|
45
|
+
WIDEVINE = 'edef8ba979d64acea3c827dcd51d21ed',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function keySystemIdToKeySystemDomain(
|
|
49
|
+
systemId: KeySystemIds,
|
|
50
|
+
): KeySystems | undefined {
|
|
51
|
+
if (systemId === KeySystemIds.WIDEVINE) {
|
|
52
|
+
return KeySystems.WIDEVINE;
|
|
53
|
+
} else if (systemId === KeySystemIds.PLAYREADY) {
|
|
54
|
+
return KeySystems.PLAYREADY;
|
|
55
|
+
} else if (
|
|
56
|
+
systemId === KeySystemIds.CENC ||
|
|
57
|
+
systemId === KeySystemIds.CLEARKEY
|
|
58
|
+
) {
|
|
59
|
+
return KeySystems.CLEARKEY;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function keySystemDomainToKeySystemFormat(
|
|
64
|
+
keySystem: KeySystems,
|
|
65
|
+
): KeySystemFormats | undefined {
|
|
66
|
+
switch (keySystem) {
|
|
67
|
+
case KeySystems.FAIRPLAY:
|
|
68
|
+
return KeySystemFormats.FAIRPLAY;
|
|
69
|
+
case KeySystems.PLAYREADY:
|
|
70
|
+
return KeySystemFormats.PLAYREADY;
|
|
71
|
+
case KeySystems.WIDEVINE:
|
|
72
|
+
return KeySystemFormats.WIDEVINE;
|
|
73
|
+
case KeySystems.CLEARKEY:
|
|
74
|
+
return KeySystemFormats.CLEARKEY;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getKeySystemsForConfig(
|
|
79
|
+
config: EMEControllerConfig,
|
|
80
|
+
): KeySystems[] {
|
|
81
|
+
const { drmSystems, widevineLicenseUrl } = config;
|
|
82
|
+
const keySystemsToAttempt: KeySystems[] = drmSystems
|
|
83
|
+
? [
|
|
84
|
+
KeySystems.FAIRPLAY,
|
|
85
|
+
KeySystems.WIDEVINE,
|
|
86
|
+
KeySystems.PLAYREADY,
|
|
87
|
+
KeySystems.CLEARKEY,
|
|
88
|
+
].filter((keySystem) => !!drmSystems[keySystem])
|
|
89
|
+
: [];
|
|
90
|
+
if (!keySystemsToAttempt[KeySystems.WIDEVINE] && widevineLicenseUrl) {
|
|
91
|
+
keySystemsToAttempt.push(KeySystems.WIDEVINE);
|
|
92
|
+
}
|
|
93
|
+
return keySystemsToAttempt;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type MediaKeyFunc = (
|
|
97
|
+
keySystem: KeySystems,
|
|
98
|
+
supportedConfigurations: MediaKeySystemConfiguration[],
|
|
99
|
+
) => Promise<MediaKeySystemAccess>;
|
|
100
|
+
|
|
101
|
+
export const requestMediaKeySystemAccess = (function (): MediaKeyFunc | null {
|
|
102
|
+
if (optionalSelf?.navigator?.requestMediaKeySystemAccess) {
|
|
103
|
+
return self.navigator.requestMediaKeySystemAccess.bind(self.navigator);
|
|
104
|
+
} else {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
})();
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/MediaKeySystemConfiguration
|
|
111
|
+
*/
|
|
112
|
+
export function getSupportedMediaKeySystemConfigurations(
|
|
113
|
+
keySystem: KeySystems,
|
|
114
|
+
audioCodecs: string[],
|
|
115
|
+
videoCodecs: string[],
|
|
116
|
+
drmSystemOptions: DRMSystemOptions,
|
|
117
|
+
): MediaKeySystemConfiguration[] {
|
|
118
|
+
let initDataTypes: string[];
|
|
119
|
+
switch (keySystem) {
|
|
120
|
+
case KeySystems.FAIRPLAY:
|
|
121
|
+
initDataTypes = ['cenc', 'sinf'];
|
|
122
|
+
break;
|
|
123
|
+
case KeySystems.WIDEVINE:
|
|
124
|
+
case KeySystems.PLAYREADY:
|
|
125
|
+
initDataTypes = ['cenc'];
|
|
126
|
+
break;
|
|
127
|
+
case KeySystems.CLEARKEY:
|
|
128
|
+
initDataTypes = ['cenc', 'keyids'];
|
|
129
|
+
break;
|
|
130
|
+
default:
|
|
131
|
+
throw new Error(`Unknown key-system: ${keySystem}`);
|
|
132
|
+
}
|
|
133
|
+
return createMediaKeySystemConfigurations(
|
|
134
|
+
initDataTypes,
|
|
135
|
+
audioCodecs,
|
|
136
|
+
videoCodecs,
|
|
137
|
+
drmSystemOptions,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function createMediaKeySystemConfigurations(
|
|
142
|
+
initDataTypes: string[],
|
|
143
|
+
audioCodecs: string[],
|
|
144
|
+
videoCodecs: string[],
|
|
145
|
+
drmSystemOptions: DRMSystemOptions,
|
|
146
|
+
): MediaKeySystemConfiguration[] {
|
|
147
|
+
const baseConfig: MediaKeySystemConfiguration = {
|
|
148
|
+
initDataTypes: initDataTypes,
|
|
149
|
+
persistentState: drmSystemOptions.persistentState || 'optional',
|
|
150
|
+
distinctiveIdentifier: drmSystemOptions.distinctiveIdentifier || 'optional',
|
|
151
|
+
sessionTypes: drmSystemOptions.sessionTypes || [
|
|
152
|
+
drmSystemOptions.sessionType || 'temporary',
|
|
153
|
+
],
|
|
154
|
+
audioCapabilities: audioCodecs.map((codec) => ({
|
|
155
|
+
contentType: `audio/mp4; codecs=${codec}`,
|
|
156
|
+
robustness: drmSystemOptions.audioRobustness || '',
|
|
157
|
+
encryptionScheme: drmSystemOptions.audioEncryptionScheme || null,
|
|
158
|
+
})),
|
|
159
|
+
videoCapabilities: videoCodecs.map((codec) => ({
|
|
160
|
+
contentType: `video/mp4; codecs=${codec}`,
|
|
161
|
+
robustness: drmSystemOptions.videoRobustness || '',
|
|
162
|
+
encryptionScheme: drmSystemOptions.videoEncryptionScheme || null,
|
|
163
|
+
})),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return [baseConfig];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function isPersistentSessionType(
|
|
170
|
+
drmSystemOptions: DRMSystemOptions | undefined,
|
|
171
|
+
): boolean {
|
|
172
|
+
return (
|
|
173
|
+
!!drmSystemOptions &&
|
|
174
|
+
(drmSystemOptions.sessionType === 'persistent-license' ||
|
|
175
|
+
!!drmSystemOptions.sessionTypes?.some(
|
|
176
|
+
(type) => type === 'persistent-license',
|
|
177
|
+
))
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function parsePlayReadyWRM(keyBytes: Uint8Array<ArrayBuffer>) {
|
|
182
|
+
const keyBytesUtf16 = new Uint16Array(
|
|
183
|
+
keyBytes.buffer,
|
|
184
|
+
keyBytes.byteOffset,
|
|
185
|
+
keyBytes.byteLength / 2,
|
|
186
|
+
);
|
|
187
|
+
const keyByteStr = String.fromCharCode.apply(null, Array.from(keyBytesUtf16));
|
|
188
|
+
|
|
189
|
+
// Parse Playready WRMHeader XML
|
|
190
|
+
const xmlKeyBytes = keyByteStr.substring(
|
|
191
|
+
keyByteStr.indexOf('<'),
|
|
192
|
+
keyByteStr.length,
|
|
193
|
+
);
|
|
194
|
+
const parser = new DOMParser();
|
|
195
|
+
const xmlDoc = parser.parseFromString(xmlKeyBytes, 'text/xml');
|
|
196
|
+
const keyData = xmlDoc.getElementsByTagName('KID')[0];
|
|
197
|
+
if (keyData) {
|
|
198
|
+
const keyId = keyData.childNodes[0]
|
|
199
|
+
? keyData.childNodes[0].nodeValue
|
|
200
|
+
: keyData.getAttribute('VALUE');
|
|
201
|
+
if (keyId) {
|
|
202
|
+
const keyIdArray = base64Decode(keyId).subarray(0, 16);
|
|
203
|
+
// KID value in PRO is a base64-encoded little endian GUID interpretation of UUID
|
|
204
|
+
// KID value in ‘tenc’ is a big endian UUID GUID interpretation of UUID
|
|
205
|
+
changeEndianness(keyIdArray);
|
|
206
|
+
return keyIdArray;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|