Skip to content

Commit

Permalink
feat(HLS): Improve detection of basic info from Media Playlist
Browse files Browse the repository at this point in the history
  • Loading branch information
avelad committed Dec 9, 2022
1 parent 76f96b9 commit bc7d7bc
Show file tree
Hide file tree
Showing 2 changed files with 338 additions and 36 deletions.
325 changes: 294 additions & 31 deletions lib/hls/hls_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ goog.require('shaka.util.Functional');
goog.require('shaka.util.LanguageUtils');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.MimeUtils');
goog.require('shaka.util.Mp4BoxParsers');
goog.require('shaka.util.Mp4Parser');
goog.require('shaka.util.OperationManager');
goog.require('shaka.util.Pssh');
goog.require('shaka.util.Timer');
goog.require('shaka.util.TsParser');
goog.require('shaka.util.Platform');
goog.require('shaka.util.Uint8ArrayUtils');
goog.require('shaka.util.XmlUtils');
Expand Down Expand Up @@ -583,7 +586,6 @@ shaka.hls.HlsParser = class {
* @private
*/
async parseManifest_(data, uri) {
const HlsParser = shaka.hls.HlsParser;
const Utils = shaka.hls.Utils;

goog.asserts.assert(this.masterPlaylistUri_,
Expand All @@ -609,40 +611,13 @@ shaka.hls.HlsParser = class {
// Get necessary info for this stream, from the config. These are things
// we would normally find from the master playlist (e.g. from values on
// EXT-X-MEDIA tags).
let fullMimeType = this.config_.hls.mediaPlaylistFullMimeType;
// Try to infer the full mimetype better.
if (playlist.segments.length) {
const parsedUri = new goog.Uri(playlist.segments[0].absoluteUri);
const extension = parsedUri.getPath().split('.').pop();
let mimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension];
if (mimeType) {
fullMimeType = mimeType;
} else if (extension === 'ts') {
// TODO: Fetch one segment a use the TsParser to analize if there is
// video, audio or both.
} else if (extension === 'mp4') {
// TODO: Fetch one segment a use the Mp4Parser to analize if there is
// video, audio or both.
} else if (HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_[extension]) {
mimeType = HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_[extension];
const defaultAudioCodec = this.config_.hls.defaultAudioCodec;
fullMimeType = `${mimeType}; codecs="${defaultAudioCodec}"`;
} else if (HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_[extension]) {
mimeType = HlsParser.VIDEO_EXTENSIONS_TO_MIME_TYPES_[extension];
const defaultVideoCodec = this.config_.hls.defaultVideoCodec;
fullMimeType = `${mimeType}; codecs="${defaultVideoCodec}"`;
}
}

const mimeType = shaka.util.MimeUtils.getBasicType(fullMimeType);
const type = mimeType.split('/')[0];
const codecs = shaka.util.MimeUtils.getCodecs(fullMimeType);
const {type, mimeType, codecs, language, height, width} =
await this.getMediaPlaylistBasicInfo_(playlist);

// Some values we cannot figure out, and aren't important enough to ask
// the user to provide through config values. A lot of these are only
// relevant to ABR, which isn't necessary if there's only one variant.
// So these unknowns should be set to false or null, largely.
const language = '';
const channelsCount = null;
const spatialAudio = false;
const characteristics = null;
Expand All @@ -658,10 +633,15 @@ shaka.hls.HlsParser = class {
mimeType);
this.uriToStreamInfosMap_.set(uri, streamInfo);

if (type == 'video') {
this.addVideoAttributes_(streamInfo.stream, width, height,
/* frameRate= */ null, /* videoRange= */ null);
}

// Wrap the stream from that stream info with a variant.
variants.push({
id: 0,
language: 'und',
language: language,
disabledUntilTime: 0,
primary: true,
audio: type == 'audio' ? streamInfo.stream : null,
Expand Down Expand Up @@ -765,6 +745,269 @@ shaka.hls.HlsParser = class {
this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
}

/**
* @param {shaka.hls.Playlist} playlist
* @return {!Promise.<shaka.hls.HlsParser.BasicInfo>}
* @private
*/
async getMediaPlaylistBasicInfo_(playlist) {
const HlsParser = shaka.hls.HlsParser;
const defaultFullMimeType = this.config_.hls.mediaPlaylistFullMimeType;
const defaultMimeType =
shaka.util.MimeUtils.getBasicType(defaultFullMimeType);
const defaultType = defaultMimeType.split('/')[0];
const defaultCodecs = shaka.util.MimeUtils.getCodecs(defaultFullMimeType);
const defaultBasicInfo = {
type: defaultType,
mimeType: defaultMimeType,
codecs: defaultCodecs,
language: 'und',
height: null,
width: null,
};
if (!playlist.segments.length) {
return defaultBasicInfo;
}
const firstSegment = playlist.segments[0];
const parsedUri = new goog.Uri(firstSegment.absoluteUri);
const extension = parsedUri.getPath().split('.').pop();
const rawMimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension];
if (rawMimeType) {
return {
type: 'audio',
mimeType: rawMimeType,
codecs: '',
language: 'und',
height: null,
width: null,
};
}

let segmentUris = [firstSegment.absoluteUri];
const initSegmentRef = this.getInitSegmentReference_(
playlist.absoluteUri, firstSegment.tags, new Map());
if (initSegmentRef) {
segmentUris = initSegmentRef.getUris();
}

const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
const segmentRequest = shaka.net.NetworkingEngine.makeRequest(
segmentUris, this.config_.retryParameters);
const response = await this.makeNetworkRequest_(
segmentRequest, requestType);

let contentMimeType = response.headers['content-type'];
if (contentMimeType) {
// Split the MIME type in case the server sent additional parameters.
contentMimeType = contentMimeType.split(';')[0].toLowerCase();
}

if (extension == 'ts' || contentMimeType == 'video/mp2t') {
const basicInfo = this.getBasicInfoFromTs_(response);
if (basicInfo) {
return basicInfo;
}
} else if (extension == 'mp4' ||
contentMimeType == 'video/mp4' || contentMimeType == 'audio/mp4') {
const basicInfo = this.getBasicInfoFromMp4_(response);
if (basicInfo) {
return basicInfo;
}
}
return defaultBasicInfo;
}

/**
* @param {shaka.extern.Response} response
* @return {?shaka.hls.HlsParser.BasicInfo}
* @private
*/
getBasicInfoFromTs_(response) {
const uint8ArrayData = shaka.util.BufferUtils.toUint8(response.data);
const tsParser = new shaka.util.TsParser().parse(uint8ArrayData);
const tsCodecs = tsParser.getCodecs();
const codecs = [];
let hasAudio = false;
let hasVideo = false;
switch (tsCodecs.audio) {
case 'aac':
codecs.push('mp4a.40.2');
hasAudio = true;
break;
case 'mp3':
codecs.push('mp4a.40.34');
hasAudio = true;
break;
}
switch (tsCodecs.video) {
case 'avc':
codecs.push('avc1.42E01E');
hasVideo = true;
break;
case 'hvc':
codecs.push('hvc1.1.6.L93.90');
hasVideo = true;
break;
}
if (!codecs.length) {
return null;
}
const onlyAudio = hasAudio && !hasVideo;
return {
type: onlyAudio ? 'audio' : 'video',
mimeType: 'video/mp2t',
codecs: codecs.join(', '),
language: 'und',
height: null,
width: null,
};
}

/**
* @param {shaka.extern.Response} response
* @return {?shaka.hls.HlsParser.BasicInfo}
* @private
*/
getBasicInfoFromMp4_(response) {
const Mp4Parser = shaka.util.Mp4Parser;

const codecs = [];

let hasAudio = false;
let hasVideo = false;

const addCodec = (codec) => {
const codecLC = codec.toLowerCase();
switch (codecLC) {
case 'avc1':
case 'avc3':
codecs.push(codecLC + '.42E01E');
hasVideo = true;
break;
case 'hev1':
case 'hvc1':
codecs.push(codecLC + '.1.6.L93.90');
hasVideo = true;
break;
case 'dvh1':
case 'dvhe':
codecs.push(codecLC + '.05.04');
hasVideo = true;
break;
case 'vp09':
codecs.push(codecLC + '.00.10.08');
hasVideo = true;
break;
case 'av01':
codecs.push(codecLC + '.0.01M.08');
hasVideo = true;
break;
case 'mp4a':
// We assume AAC, but this can be wrong since mp4a supports
// others codecs
codecs.push('mp4a.40.2');
hasAudio = true;
break;
case 'ac-3':
case 'ec-3':
case 'opus':
case 'flac':
codecs.push(codecLC);
hasAudio = true;
break;
}
};

const codecBoxParser = (box) => addCodec(box.name);

/** @type {string} */
let language = 'und';
/** @type {?string} */
let height = null;
/** @type {?string} */
let width = null;

new Mp4Parser()
.box('moov', Mp4Parser.children)
.box('trak', Mp4Parser.children)
.fullBox('tkhd', (box) => {
goog.asserts.assert(
box.version != null,
'TKHD is a full box and should have a valid version.');
const parsedTKHDBox = shaka.util.Mp4BoxParsers.parseTKHD(
box.reader, box.version);
height = String(parsedTKHDBox.height);
width = String(parsedTKHDBox.width);
})
.box('mdia', Mp4Parser.children)
.fullBox('mdhd', (box) => {
goog.asserts.assert(
box.version != null,
'MDHD is a full box and should have a valid version.');
const parsedMDHDBox = shaka.util.Mp4BoxParsers.parseMDHD(
box.reader, box.version);
language = parsedMDHDBox.language;
})
.box('minf', Mp4Parser.children)
.box('stbl', Mp4Parser.children)
.fullBox('stsd', Mp4Parser.sampleDescription)

// AUDIO
// These are the various boxes that signal a codec.
.box('mp4a', codecBoxParser)
.box('ac-3', codecBoxParser)
.box('ec-3', codecBoxParser)
.box('opus', codecBoxParser)
.box('Opus', codecBoxParser)
.box('fLaC', codecBoxParser)

// VIDEO
// These are the various boxes that signal a codec.
.box('avc1', codecBoxParser)
.box('avc3', codecBoxParser)
.box('hev1', codecBoxParser)
.box('hvc1', codecBoxParser)
.box('dvh1', codecBoxParser)
.box('dvhe', codecBoxParser)
.box('vp09', codecBoxParser)
.box('av01', codecBoxParser)

// This signals an encrypted sample, which we can go inside of to
// find the codec used.
// Note: If encrypted, you can only have audio or video, not both.
.box('enca', Mp4Parser.visualSampleEntry)
.box('encv', Mp4Parser.visualSampleEntry)
.box('sinf', Mp4Parser.children)
.box('frma', (box) => {
const {codec} = shaka.util.Mp4BoxParsers.parseFRMA(box.reader);
addCodec(codec);
})

.parse(response.data, /* partialOkay= */ true);
if (!codecs.length) {
return null;
}
const realCodecs = [];
const seen = new Set();
for (const codec of codecs) {
if (!seen.has(codec)) {
realCodecs.push(codec);
seen.add(codec);
} else {
shaka.log.debug('Ignoring duplicate codec');
}
}
const onlyAudio = hasAudio && !hasVideo;
return {
type: onlyAudio ? 'audio' : 'video',
mimeType: onlyAudio ? 'audio/mp4' : 'video/mp4',
codecs: realCodecs.join(', '),
language: language,
height: height,
width: width,
};
}

/** @private */
determineDuration_() {
goog.asserts.assert(this.presentationTimeline_,
Expand Down Expand Up @@ -3279,6 +3522,26 @@ shaka.hls.HlsParser.StreamInfo;
shaka.hls.HlsParser.StreamInfos;


/**
* @typedef {{
* type: string,
* mimeType: string,
* codecs: string,
* language: string,
* height: ?string,
* width: ?string
* }}
*
* @property {string} type
* @property {string} mimeType
* @property {string} codecs
* @property {string} language
* @property {?string} height
* @property {?string} width
*/
shaka.hls.HlsParser.BasicInfo;


/**
* @const {!Object.<string, string>}
* @private
Expand Down
Loading

0 comments on commit bc7d7bc

Please sign in to comment.