Buckets:
| import { codecsSetSelectionPreferenceValue } from './codecs'; | |
| import { getVideoSelectionOptions } from './hdr'; | |
| import { logger } from './logger'; | |
| import { stringify } from './safe-json-stringify'; | |
| import type Hls from '../hls'; | |
| import type { Level, VideoRange } from '../types/level'; | |
| import type { | |
| AudioSelectionOption, | |
| MediaPlaylist, | |
| SubtitleSelectionOption, | |
| VideoSelectionOption, | |
| } from '../types/media-playlist'; | |
| export type CodecSetTier = { | |
| minBitrate: number; | |
| minHeight: number; | |
| minFramerate: number; | |
| minIndex: number; | |
| maxScore: number; | |
| videoRanges: Record<string, number>; | |
| channels: Record<string, number>; | |
| hasDefaultAudio: boolean; | |
| fragmentError: number; | |
| }; | |
| type AudioTrackGroup = { | |
| tracks: MediaPlaylist[]; | |
| channels: Record<string, number>; | |
| hasDefault: boolean; | |
| hasAutoSelect: boolean; | |
| }; | |
| type StartParameters = { | |
| codecSet: string | undefined; | |
| videoRanges: Array<VideoRange>; | |
| preferHDR: boolean; | |
| minFramerate: number; | |
| minBitrate: number; | |
| minIndex: number; | |
| }; | |
| export function getStartCodecTier( | |
| codecTiers: Record<string, CodecSetTier>, | |
| currentVideoRange: VideoRange | undefined, | |
| currentBw: number, | |
| audioPreference: AudioSelectionOption | undefined, | |
| videoPreference: VideoSelectionOption | undefined, | |
| ): StartParameters { | |
| const codecSets = Object.keys(codecTiers); | |
| const channelsPreference = audioPreference?.channels; | |
| const audioCodecPreference = audioPreference?.audioCodec; | |
| const videoCodecPreference = videoPreference?.videoCodec; | |
| const preferStereo = channelsPreference && parseInt(channelsPreference) === 2; | |
| // Use first level set to determine stereo, and minimum resolution and framerate | |
| let hasStereo = false; | |
| let hasCurrentVideoRange = false; | |
| let minHeight = Infinity; | |
| let minFramerate = Infinity; | |
| let minBitrate = Infinity; | |
| let minIndex = Infinity; | |
| let selectedScore = 0; | |
| let videoRanges: Array<VideoRange> = []; | |
| const { preferHDR, allowedVideoRanges } = getVideoSelectionOptions( | |
| currentVideoRange, | |
| videoPreference, | |
| ); | |
| for (let i = codecSets.length; i--; ) { | |
| const tier = codecTiers[codecSets[i]]; | |
| hasStereo ||= tier.channels[2] > 0; | |
| minHeight = Math.min(minHeight, tier.minHeight); | |
| minFramerate = Math.min(minFramerate, tier.minFramerate); | |
| minBitrate = Math.min(minBitrate, tier.minBitrate); | |
| const matchingVideoRanges = allowedVideoRanges.filter( | |
| (range) => tier.videoRanges[range] > 0, | |
| ); | |
| if (matchingVideoRanges.length > 0) { | |
| hasCurrentVideoRange = true; | |
| } | |
| } | |
| minHeight = Number.isFinite(minHeight) ? minHeight : 0; | |
| minFramerate = Number.isFinite(minFramerate) ? minFramerate : 0; | |
| const maxHeight = Math.max(1080, minHeight); | |
| const maxFramerate = Math.max(30, minFramerate); | |
| minBitrate = Number.isFinite(minBitrate) ? minBitrate : currentBw; | |
| currentBw = Math.max(minBitrate, currentBw); | |
| // If there are no variants with matching preference, set currentVideoRange to undefined | |
| if (!hasCurrentVideoRange) { | |
| currentVideoRange = undefined; | |
| } | |
| const hasMultipleSets = codecSets.length > 1; | |
| const codecSet = codecSets.reduce( | |
| (selected: string | undefined, candidate: string) => { | |
| // Remove candiates which do not meet bitrate, default audio, stereo or channels preference, 1080p or lower, 30fps or lower, or SDR/HDR selection if present | |
| const candidateTier = codecTiers[candidate]; | |
| if (candidate === selected) { | |
| return selected; | |
| } | |
| videoRanges = hasCurrentVideoRange | |
| ? allowedVideoRanges.filter( | |
| (range) => candidateTier.videoRanges[range] > 0, | |
| ) | |
| : []; | |
| if (hasMultipleSets) { | |
| if (candidateTier.minBitrate > currentBw) { | |
| logStartCodecCandidateIgnored( | |
| candidate, | |
| `min bitrate of ${candidateTier.minBitrate} > current estimate of ${currentBw}`, | |
| ); | |
| return selected; | |
| } | |
| if (!candidateTier.hasDefaultAudio) { | |
| logStartCodecCandidateIgnored( | |
| candidate, | |
| `no renditions with default or auto-select sound found`, | |
| ); | |
| return selected; | |
| } | |
| if ( | |
| audioCodecPreference && | |
| candidate.indexOf(audioCodecPreference.substring(0, 4)) % 5 !== 0 | |
| ) { | |
| logStartCodecCandidateIgnored( | |
| candidate, | |
| `audio codec preference "${audioCodecPreference}" not found`, | |
| ); | |
| return selected; | |
| } | |
| if (channelsPreference && !preferStereo) { | |
| if (!candidateTier.channels[channelsPreference]) { | |
| logStartCodecCandidateIgnored( | |
| candidate, | |
| `no renditions with ${channelsPreference} channel sound found (channels options: ${Object.keys( | |
| candidateTier.channels, | |
| )})`, | |
| ); | |
| return selected; | |
| } | |
| } else if ( | |
| (!audioCodecPreference || preferStereo) && | |
| hasStereo && | |
| candidateTier.channels['2'] === 0 | |
| ) { | |
| logStartCodecCandidateIgnored( | |
| candidate, | |
| `no renditions with stereo sound found`, | |
| ); | |
| return selected; | |
| } | |
| if (candidateTier.minHeight > maxHeight) { | |
| logStartCodecCandidateIgnored( | |
| candidate, | |
| `min resolution of ${candidateTier.minHeight} > maximum of ${maxHeight}`, | |
| ); | |
| return selected; | |
| } | |
| if (candidateTier.minFramerate > maxFramerate) { | |
| logStartCodecCandidateIgnored( | |
| candidate, | |
| `min framerate of ${candidateTier.minFramerate} > maximum of ${maxFramerate}`, | |
| ); | |
| return selected; | |
| } | |
| if ( | |
| !videoRanges.some((range) => candidateTier.videoRanges[range] > 0) | |
| ) { | |
| logStartCodecCandidateIgnored( | |
| candidate, | |
| `no variants with VIDEO-RANGE of ${stringify(videoRanges)} found`, | |
| ); | |
| return selected; | |
| } | |
| if ( | |
| videoCodecPreference && | |
| candidate.indexOf(videoCodecPreference.substring(0, 4)) % 5 !== 0 | |
| ) { | |
| logStartCodecCandidateIgnored( | |
| candidate, | |
| `video codec preference "${videoCodecPreference}" not found`, | |
| ); | |
| return selected; | |
| } | |
| if (candidateTier.maxScore < selectedScore) { | |
| logStartCodecCandidateIgnored( | |
| candidate, | |
| `max score of ${candidateTier.maxScore} < selected max of ${selectedScore}`, | |
| ); | |
| return selected; | |
| } | |
| } | |
| // Remove candiates with less preferred codecs or more errors | |
| if ( | |
| selected && | |
| (codecsSetSelectionPreferenceValue(candidate) >= | |
| codecsSetSelectionPreferenceValue(selected) || | |
| candidateTier.fragmentError > codecTiers[selected].fragmentError) | |
| ) { | |
| return selected; | |
| } | |
| minIndex = candidateTier.minIndex; | |
| selectedScore = candidateTier.maxScore; | |
| return candidate; | |
| }, | |
| undefined, | |
| ); | |
| return { | |
| codecSet, | |
| videoRanges, | |
| preferHDR, | |
| minFramerate, | |
| minBitrate, | |
| minIndex, | |
| }; | |
| } | |
| function logStartCodecCandidateIgnored(codeSet: string, reason: string) { | |
| logger.log( | |
| `[abr] start candidates with "${codeSet}" ignored because ${reason}`, | |
| ); | |
| } | |
| export type AudioTracksByGroup = { | |
| hasDefaultAudio: boolean; | |
| hasAutoSelectAudio: boolean; | |
| groups: Record<string, AudioTrackGroup>; | |
| }; | |
| export function getAudioTracksByGroup(allAudioTracks: MediaPlaylist[]) { | |
| return allAudioTracks.reduce( | |
| (audioTracksByGroup: AudioTracksByGroup, track) => { | |
| let trackGroup = audioTracksByGroup.groups[track.groupId]; | |
| if (!trackGroup) { | |
| trackGroup = audioTracksByGroup.groups[track.groupId] = { | |
| tracks: [], | |
| channels: { 2: 0 }, | |
| hasDefault: false, | |
| hasAutoSelect: false, | |
| }; | |
| } | |
| trackGroup.tracks.push(track); | |
| const channelsKey = track.channels || '2'; | |
| trackGroup.channels[channelsKey] = | |
| (trackGroup.channels[channelsKey] || 0) + 1; | |
| trackGroup.hasDefault = trackGroup.hasDefault || track.default; | |
| trackGroup.hasAutoSelect = trackGroup.hasAutoSelect || track.autoselect; | |
| if (trackGroup.hasDefault) { | |
| audioTracksByGroup.hasDefaultAudio = true; | |
| } | |
| if (trackGroup.hasAutoSelect) { | |
| audioTracksByGroup.hasAutoSelectAudio = true; | |
| } | |
| return audioTracksByGroup; | |
| }, | |
| { | |
| hasDefaultAudio: false, | |
| hasAutoSelectAudio: false, | |
| groups: {}, | |
| }, | |
| ); | |
| } | |
| export function getCodecTiers( | |
| levels: Level[], | |
| audioTracksByGroup: AudioTracksByGroup, | |
| minAutoLevel: number, | |
| maxAutoLevel: number, | |
| ): Record<string, CodecSetTier> { | |
| return levels | |
| .slice(minAutoLevel, maxAutoLevel + 1) | |
| .reduce((tiers: Record<string, CodecSetTier>, level, index) => { | |
| if (!level.codecSet) { | |
| return tiers; | |
| } | |
| const audioGroups = level.audioGroups; | |
| let tier = tiers[level.codecSet]; | |
| if (!tier) { | |
| tiers[level.codecSet] = tier = { | |
| minBitrate: Infinity, | |
| minHeight: Infinity, | |
| minFramerate: Infinity, | |
| minIndex: index, | |
| maxScore: 0, | |
| videoRanges: { SDR: 0 }, | |
| channels: { '2': 0 }, | |
| hasDefaultAudio: !audioGroups, | |
| fragmentError: 0, | |
| }; | |
| } | |
| tier.minBitrate = Math.min(tier.minBitrate, level.bitrate); | |
| const lesserWidthOrHeight = Math.min(level.height, level.width); | |
| tier.minHeight = Math.min(tier.minHeight, lesserWidthOrHeight); | |
| tier.minFramerate = Math.min(tier.minFramerate, level.frameRate); | |
| tier.minIndex = Math.min(tier.minIndex, index); | |
| tier.maxScore = Math.max(tier.maxScore, level.score); | |
| tier.fragmentError += level.fragmentError; | |
| tier.videoRanges[level.videoRange] = | |
| (tier.videoRanges[level.videoRange] || 0) + 1; | |
| if (__USE_ALT_AUDIO__ && audioGroups) { | |
| audioGroups.forEach((audioGroupId) => { | |
| if (!audioGroupId) { | |
| return; | |
| } | |
| const audioGroup = audioTracksByGroup.groups[audioGroupId]; | |
| if (!audioGroup) { | |
| return; | |
| } | |
| // Default audio is any group with DEFAULT=YES, or if missing then any group with AUTOSELECT=YES, or all variants | |
| tier.hasDefaultAudio = | |
| tier.hasDefaultAudio || audioTracksByGroup.hasDefaultAudio | |
| ? audioGroup.hasDefault | |
| : audioGroup.hasAutoSelect || | |
| (!audioTracksByGroup.hasDefaultAudio && | |
| !audioTracksByGroup.hasAutoSelectAudio); | |
| Object.keys(audioGroup.channels).forEach((channels) => { | |
| tier.channels[channels] = | |
| (tier.channels[channels] || 0) + audioGroup.channels[channels]; | |
| }); | |
| }); | |
| } | |
| return tiers; | |
| }, {}); | |
| } | |
| export function getBasicSelectionOption( | |
| option: | |
| | MediaPlaylist | |
| | AudioSelectionOption | |
| | SubtitleSelectionOption | |
| | undefined, | |
| ): Partial<AudioSelectionOption | SubtitleSelectionOption> | undefined { | |
| if (!option) { | |
| return option; | |
| } | |
| const { lang, assocLang, characteristics, channels, audioCodec } = | |
| option as AudioSelectionOption; | |
| return { lang, assocLang, characteristics, channels, audioCodec }; | |
| } | |
| export function findMatchingOption( | |
| option: MediaPlaylist | AudioSelectionOption | SubtitleSelectionOption, | |
| tracks: MediaPlaylist[], | |
| matchPredicate?: ( | |
| option: MediaPlaylist | AudioSelectionOption | SubtitleSelectionOption, | |
| track: MediaPlaylist, | |
| ) => boolean, | |
| ): number { | |
| if ('attrs' in option) { | |
| const index = tracks.indexOf(option); | |
| if (index !== -1) { | |
| return index; | |
| } | |
| } | |
| for (let i = 0; i < tracks.length; i++) { | |
| const track = tracks[i]; | |
| if (matchesOption(option, track, matchPredicate)) { | |
| return i; | |
| } | |
| } | |
| return -1; | |
| } | |
| export function matchesOption( | |
| option: MediaPlaylist | AudioSelectionOption | SubtitleSelectionOption, | |
| track: MediaPlaylist, | |
| matchPredicate?: ( | |
| option: MediaPlaylist | AudioSelectionOption | SubtitleSelectionOption, | |
| track: MediaPlaylist, | |
| ) => boolean, | |
| ): boolean { | |
| const { groupId, name, lang, assocLang, default: isDefault } = option; | |
| const forced = (option as SubtitleSelectionOption).forced; | |
| return ( | |
| (groupId === undefined || track.groupId === groupId) && | |
| (name === undefined || track.name === name) && | |
| (lang === undefined || languagesMatch(lang, track.lang)) && | |
| (lang === undefined || track.assocLang === assocLang) && | |
| (isDefault === undefined || track.default === isDefault) && | |
| (forced === undefined || track.forced === forced) && | |
| (!('characteristics' in option) || | |
| characteristicsMatch( | |
| option.characteristics || '', | |
| track.characteristics, | |
| )) && | |
| (matchPredicate === undefined || matchPredicate(option, track)) | |
| ); | |
| } | |
| function languagesMatch(languageA: string, languageB: string = '--'): boolean { | |
| if (languageA.length === languageB.length) { | |
| return languageA === languageB; | |
| } | |
| return languageA.startsWith(languageB) || languageB.startsWith(languageA); | |
| } | |
| function characteristicsMatch( | |
| characteristicsA: string, | |
| characteristicsB: string = '', | |
| ): boolean { | |
| const arrA = characteristicsA.split(','); | |
| const arrB = characteristicsB.split(','); | |
| // Expects each item to be unique: | |
| return ( | |
| arrA.length === arrB.length && !arrA.some((el) => arrB.indexOf(el) === -1) | |
| ); | |
| } | |
| export function audioMatchPredicate( | |
| option: MediaPlaylist | AudioSelectionOption, | |
| track: MediaPlaylist, | |
| ) { | |
| const { audioCodec, channels } = option; | |
| return ( | |
| (audioCodec === undefined || | |
| (track.audioCodec || '').substring(0, 4) === | |
| audioCodec.substring(0, 4)) && | |
| (channels === undefined || channels === (track.channels || '2')) | |
| ); | |
| } | |
| export function findClosestLevelWithAudioGroup( | |
| option: MediaPlaylist | AudioSelectionOption, | |
| levels: Level[], | |
| allAudioTracks: MediaPlaylist[], | |
| searchIndex: number, | |
| matchPredicate: ( | |
| option: MediaPlaylist | AudioSelectionOption, | |
| track: MediaPlaylist, | |
| ) => boolean, | |
| ): number { | |
| const currentLevel = levels[searchIndex]; | |
| // Are there variants with same URI as current level? | |
| // If so, find a match that does not require any level URI change | |
| const variants = levels.reduce( | |
| (variantMap: { [uri: string]: number[] }, level, index) => { | |
| const uri = level.uri; | |
| const renditions = variantMap[uri] || (variantMap[uri] = []); | |
| renditions.push(index); | |
| return variantMap; | |
| }, | |
| {}, | |
| ); | |
| const renditions = variants[currentLevel.uri]; | |
| if (renditions.length > 1) { | |
| searchIndex = Math.max.apply(Math, renditions); | |
| } | |
| // Find best match | |
| const currentVideoRange = currentLevel.videoRange; | |
| const currentFrameRate = currentLevel.frameRate; | |
| const currentVideoCodec = currentLevel.codecSet.substring(0, 4); | |
| const matchingVideo = searchDownAndUpList( | |
| levels, | |
| searchIndex, | |
| (level: Level) => { | |
| if ( | |
| level.videoRange !== currentVideoRange || | |
| level.frameRate !== currentFrameRate || | |
| level.codecSet.substring(0, 4) !== currentVideoCodec | |
| ) { | |
| return false; | |
| } | |
| const audioGroups = level.audioGroups; | |
| const tracks = allAudioTracks.filter( | |
| (track): boolean => | |
| !audioGroups || audioGroups.indexOf(track.groupId) !== -1, | |
| ); | |
| return findMatchingOption(option, tracks, matchPredicate) > -1; | |
| }, | |
| ); | |
| if (matchingVideo > -1) { | |
| return matchingVideo; | |
| } | |
| return searchDownAndUpList(levels, searchIndex, (level: Level) => { | |
| const audioGroups = level.audioGroups; | |
| const tracks = allAudioTracks.filter( | |
| (track): boolean => | |
| !audioGroups || audioGroups.indexOf(track.groupId) !== -1, | |
| ); | |
| return findMatchingOption(option, tracks, matchPredicate) > -1; | |
| }); | |
| } | |
| function searchDownAndUpList( | |
| arr: any[], | |
| searchIndex: number, | |
| predicate: (item: any) => boolean, | |
| ): number { | |
| for (let i = searchIndex; i > -1; i--) { | |
| if (predicate(arr[i])) { | |
| return i; | |
| } | |
| } | |
| for (let i = searchIndex + 1; i < arr.length; i++) { | |
| if (predicate(arr[i])) { | |
| return i; | |
| } | |
| } | |
| return -1; | |
| } | |
| export function useAlternateAudio( | |
| audioTrackUrl: string | undefined, | |
| hls: Hls, | |
| ): boolean { | |
| return !!audioTrackUrl && audioTrackUrl !== hls.loadLevelObj?.uri; | |
| } | |
Xet Storage Details
- Size:
- 16.4 kB
- Xet hash:
- 97fc47c3798cd94df882104542671c1153c1f94784994e8a8c7f25a805aea36c
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.