UNPKG

@tianfeng98/hls.js

Version:

HLS.js is a JavaScript library that supports playing MPEG-TS and HEVC encoded HLS streams in browsers with support for MSE.

180 lines (167 loc) 5.83 kB
import { mimeTypeForCodec } from './codecs'; import type { Level, VideoRange } from '../types/level'; import type { AudioTracksByGroup } from './rendition-helper'; export type MediaDecodingInfo = { supported: boolean; configurations: readonly MediaDecodingConfiguration[]; decodingInfoResults: readonly MediaCapabilitiesDecodingInfo[]; error?: Error; }; type BaseVideoConfiguration = Omit<VideoConfiguration, 'contentType'>; export const SUPPORTED_INFO_DEFAULT: MediaDecodingInfo = { supported: true, configurations: [] as MediaDecodingConfiguration[], decodingInfoResults: [ { supported: true, powerEfficient: true, smooth: true, }, ], } as const; export const SUPPORTED_INFO_CACHE: Record< string, Promise<MediaCapabilitiesDecodingInfo> > = {}; export function requiresMediaCapabilitiesDecodingInfo( level: Level, audioTracksByGroup: AudioTracksByGroup, mediaCapabilities: MediaCapabilities | undefined, currentVideoRange: VideoRange | undefined, currentFrameRate: number, currentBw: number, ): boolean { // Only test support when configuration is exceeds minimum options const audioGroups = level.audioCodec ? level.audioGroups : null; let audioChannels: Record<string, number> | null = null; if (audioGroups?.length) { try { if (audioGroups.length === 1 && audioGroups[0]) { audioChannels = audioTracksByGroup.groups[audioGroups[0]].channels; } else { audioChannels = audioGroups.reduce( (acc, groupId) => { if (groupId) { const audioTrackGroup = audioTracksByGroup.groups[groupId]; if (!audioTrackGroup) { throw new Error(`Audio track group ${groupId} not found`); } // Sum all channel key values Object.keys(audioTrackGroup.channels).forEach((key) => { acc[key] = (acc[key] || 0) + audioTrackGroup.channels[key]; }); } return acc; }, { 2: 0 }, ); } } catch (error) { return true; } } return ( (typeof mediaCapabilities?.decodingInfo == 'function' && level.videoCodec !== undefined && ((level.width > 1920 && level.height > 1088) || (level.height > 1920 && level.width > 1088) || level.frameRate > Math.max(currentFrameRate, 30) || (level.videoRange !== 'SDR' && level.videoRange !== currentVideoRange) || level.bitrate > Math.max(currentBw, 8e6))) || (!!audioChannels && Object.keys(audioChannels).length > 1) ); } export function getMediaDecodingInfoPromise( level: Level, audioTracksByGroup: AudioTracksByGroup, mediaCapabilities: MediaCapabilities, ): Promise<MediaDecodingInfo> { const videoCodecs = level.videoCodec; const audioCodecs = level.audioCodec; if (!videoCodecs || !audioCodecs) { return Promise.resolve(SUPPORTED_INFO_DEFAULT); } const baseVideoConfiguration: BaseVideoConfiguration = { width: level.width, height: level.height, bitrate: Math.ceil(Math.max(level.bitrate * 0.9, level.averageBitrate)), // Assume a framerate of 30fps since MediaCapabilities will not accept Level default of 0. framerate: level.frameRate || 30, }; const videoRange = level.videoRange; if (videoRange !== 'SDR') { baseVideoConfiguration.transferFunction = videoRange.toLowerCase() as TransferFunction; } const configurations: MediaDecodingConfiguration[] = videoCodecs .split(',') .map((videoCodec) => ({ type: 'media-source', video: { ...baseVideoConfiguration, contentType: mimeTypeForCodec(videoCodec, 'video'), }, })); const audioGroupId = level.audioGroupId; if (audioCodecs && audioGroupId) { audioTracksByGroup.groups[audioGroupId]?.tracks.forEach((audioTrack) => { if (audioTrack.groupId === audioGroupId) { const channels = audioTrack.channels || ''; const channelsNumber = parseFloat(channels); if (Number.isFinite(channelsNumber) && channelsNumber > 2) { configurations.push.apply( configurations, audioCodecs.split(',').map((audioCodec) => ({ type: 'media-source', audio: { contentType: mimeTypeForCodec(audioCodec, 'audio'), channels: '' + channelsNumber, // spatialRendering: // audioCodec === 'ec-3' && channels.indexOf('JOC'), }, })), ); } } }); } return Promise.all( configurations.map((configuration) => { // Cache MediaCapabilities promises const decodingInfoKey = getMediaDecodingInfoKey(configuration); return ( SUPPORTED_INFO_CACHE[decodingInfoKey] || (SUPPORTED_INFO_CACHE[decodingInfoKey] = mediaCapabilities.decodingInfo(configuration)) ); }), ) .then((decodingInfoResults) => ({ supported: !decodingInfoResults.some((info) => !info.supported), configurations, decodingInfoResults, })) .catch((error) => ({ supported: false, configurations, decodingInfoResults: [] as MediaCapabilitiesDecodingInfo[], error, })); } function getMediaDecodingInfoKey(config: MediaDecodingConfiguration): string { const { audio, video } = config; const mediaConfig = video || audio; if (mediaConfig) { const codec = mediaConfig.contentType.split('"')[1]; if (video) { return `r${video.height}x${video.width}f${Math.ceil(video.framerate)}${ video.transferFunction || 'sd' }_${codec}_${Math.ceil(video.bitrate / 1e5)}`; } if (audio) { return `c${audio.channels}${audio.spatialRendering ? 's' : 'n'}_${codec}`; } } return ''; }