From e191c755167abd8658e9e3aae35cd99d07d15ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Velad=20Galv=C3=A1n?= Date: Fri, 8 Sep 2023 08:45:47 +0200 Subject: [PATCH] feat(HLS): Get the correct video info for TS segments with H.265 (#5616) --- lib/hls/hls_parser.js | 6 +- lib/util/exp_golomb.js | 32 +++++++- lib/util/ts_parser.js | 164 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 196 insertions(+), 6 deletions(-) diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index b4836eadc8..8391130c38 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -932,7 +932,11 @@ shaka.hls.HlsParser = class { hasVideo = true; break; case 'hvc': - codecs.push('hvc1.1.6.L93.90'); + if (videoInfo.codec) { + codecs.push(videoInfo.codec); + } else { + codecs.push('hvc1.1.6.L93.90'); + } hasVideo = true; break; } diff --git a/lib/util/exp_golomb.js b/lib/util/exp_golomb.js index 016b7908bf..8db9551b7d 100644 --- a/lib/util/exp_golomb.js +++ b/lib/util/exp_golomb.js @@ -17,6 +17,7 @@ goog.provide('shaka.util.ExpGolomb'); +goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.DataViewReader'); @@ -31,13 +32,17 @@ goog.require('shaka.util.DataViewReader'); shaka.util.ExpGolomb = class { /** * @param {!Uint8Array} data + * @param {boolean=} convertEbsp2rbsp */ - constructor(data) { + constructor(data, convertEbsp2rbsp = false) { /** @private {!Uint8Array} */ this.data_ = data; + if (convertEbsp2rbsp) { + this.data_ = this.ebsp2rbsp_(data); + } /** @private {number} */ - this.workingBytesAvailable_ = data.byteLength; + this.workingBytesAvailable_ = this.data_.byteLength; // the current word being examined /** @private {number} */ @@ -48,6 +53,29 @@ shaka.util.ExpGolomb = class { this.workingBitsAvailable_ = 0; } + /** + * @param {!Uint8Array} data + * @return {!Uint8Array} + * @private + */ + ebsp2rbsp_(data) { + const ret = new Uint8Array(data.byteLength); + let retIndex = 0; + + for (let i = 0; i < data.byteLength; i++) { + if (i >= 2) { + // Unescape: Skip 0x03 after 00 00 + if (data[i] == 0x03 && data[i - 1] == 0x00 && data[i - 2] == 0x00) { + continue; + } + } + ret[retIndex] = data[i]; + retIndex++; + } + + return shaka.util.BufferUtils.toUint8(ret, 0, retIndex); + } + /** * Load the next word * diff --git a/lib/util/ts_parser.js b/lib/util/ts_parser.js index 49ae7cde8e..3c89c68d76 100644 --- a/lib/util/ts_parser.js +++ b/lib/util/ts_parser.js @@ -727,15 +727,25 @@ shaka.util.TsParser = class { * @export */ getVideoInfo() { + if (this.videoCodec_ == 'hvc') { + return this.getHvcInfo_(); + } + return this.getAvcInfo_(); + } + + /** + * Return the video information for AVC + * + * @return {{height: ?string, width: ?string, codec: ?string}} + * @private + */ + getAvcInfo_() { const TsParser = shaka.util.TsParser; const videoInfo = { height: null, width: null, codec: null, }; - if (this.videoCodec_ != 'avc') { - return videoInfo; - } const videoNalus = this.getVideoNalus(); if (!videoNalus.length) { return videoInfo; @@ -850,6 +860,146 @@ shaka.util.TsParser = class { return videoInfo; } + /** + * Return the video information for HVC + * + * @return {{height: ?string, width: ?string, codec: ?string}} + * @private + */ + getHvcInfo_() { + const TsParser = shaka.util.TsParser; + const videoInfo = { + height: null, + width: null, + codec: null, + }; + const videoNalus = this.getVideoNalus(); + if (!videoNalus.length) { + return videoInfo; + } + const spsNalu = videoNalus.find((nalu) => { + return nalu.type == TsParser.H265_NALU_TYPE_SPS_; + }); + if (!spsNalu) { + return videoInfo; + } + + const gb = new shaka.util.ExpGolomb( + spsNalu.fullData, /* convertEbsp2rbsp= */ true); + + // remove NALu Header + gb.readUnsignedByte(); + gb.readUnsignedByte(); + + // SPS + gb.readBits(4); // video_paramter_set_id + const maxSubLayersMinus1 = gb.readBits(3); + gb.readBoolean(); // temporal_id_nesting_flag + + // profile_tier_level begin + const generalProfileSpace = gb.readBits(2); + const generalTierFlag = gb.readBits(1); + const generalProfileIdc = gb.readBits(5); + const generalProfileCompatibilityFlags = gb.readBits(32); + const generalConstraintIndicatorFlags1 = gb.readUnsignedByte(); + const generalConstraintIndicatorFlags2 = gb.readUnsignedByte(); + const generalConstraintIndicatorFlags3 = gb.readUnsignedByte(); + const generalConstraintIndicatorFlags4 = gb.readUnsignedByte(); + const generalConstraintIndicatorFlags5 = gb.readUnsignedByte(); + const generalConstraintIndicatorFlags6 = gb.readUnsignedByte(); + const generalLevelIdc = gb.readUnsignedByte(); + const subLayerProfilePresentFlag = []; + const subLayerLevelPresentFlag = []; + for (let i = 0; i < maxSubLayersMinus1; i++) { + subLayerProfilePresentFlag.push(gb.readBoolean()); + subLayerLevelPresentFlag.push(gb.readBoolean()); + } + if (maxSubLayersMinus1 > 0) { + for (let i = maxSubLayersMinus1; i < 8; i++) { + gb.readBits(2); + } + } + for (let i = 0; i < maxSubLayersMinus1; i++) { + if (subLayerProfilePresentFlag[i]) { + gb.readBits(88); + } + if (subLayerLevelPresentFlag[i]) { + gb.readUnsignedByte(); + } + } + // profile_tier_level end + + gb.readUnsignedExpGolomb(); // seq_parameter_set_id + const chromaFormatIdc = gb.readUnsignedExpGolomb(); + if (chromaFormatIdc == 3) { + gb.readBits(1); // separate_colour_plane_flag + } + const picWidthInLumaSamples = gb.readUnsignedExpGolomb(); + const picHeightInLumaSamples = gb.readUnsignedExpGolomb(); + let leftOffset = 0; + let rightOffset = 0; + let topOffset = 0; + let bottomOffset = 0; + const conformanceWindowFlag = gb.readBoolean(); + if (conformanceWindowFlag) { + leftOffset += gb.readUnsignedExpGolomb(); + rightOffset += gb.readUnsignedExpGolomb(); + topOffset += gb.readUnsignedExpGolomb(); + bottomOffset += gb.readUnsignedExpGolomb(); + } + + const subWc = chromaFormatIdc === 1 || chromaFormatIdc === 2 ? 2 : 1; + const subHc = chromaFormatIdc === 1 ? 2 : 1; + videoInfo.width = + String(picWidthInLumaSamples - (leftOffset + rightOffset) * subWc); + videoInfo.height = + String(picHeightInLumaSamples - (topOffset + bottomOffset) * subHc); + + const reverseBits = (integer) => { + let result = 0; + for (let i = 0; i < 32; i++) { + result |= ((integer >> i) & 1) << (31 - i); + } + return result >>> 0; + }; + + const profileSpace = ['', 'A', 'B', 'C'][generalProfileSpace]; + const profileCompatibility = reverseBits(generalProfileCompatibilityFlags); + const tierFlag = generalTierFlag == 1 ? 'H' : 'L'; + + let codec = 'hvc1'; + codec += '.' + profileSpace + generalProfileIdc; + codec += '.' + profileCompatibility.toString(16).toUpperCase(); + codec += '.' + tierFlag + generalLevelIdc; + if (generalConstraintIndicatorFlags6) { + codec += '.' + + generalConstraintIndicatorFlags6.toString(16).toUpperCase(); + } + if (generalConstraintIndicatorFlags5) { + codec += '.' + + generalConstraintIndicatorFlags5.toString(16).toUpperCase(); + } + if (generalConstraintIndicatorFlags4) { + codec += '.' + + generalConstraintIndicatorFlags4.toString(16).toUpperCase(); + } + if (generalConstraintIndicatorFlags3) { + codec += '.' + + generalConstraintIndicatorFlags3.toString(16).toUpperCase(); + } + if (generalConstraintIndicatorFlags2) { + codec += '.' + + generalConstraintIndicatorFlags2.toString(16).toUpperCase(); + } + if (generalConstraintIndicatorFlags1) { + codec += '.' + + generalConstraintIndicatorFlags1.toString(16).toUpperCase(); + } + videoInfo.codec = codec; + + return videoInfo; + } + /** * Convert a byte to 2 digits of hex. (Only handles values 0-255.) * @@ -931,6 +1081,14 @@ shaka.util.TsParser.PacketLength_ = 188; shaka.util.TsParser.H264_NALU_TYPE_SPS_ = 0x07; +/** + * NALU type for Sequence Parameter Set (SPS) for H.265. + * @const {number} + * @private + */ +shaka.util.TsParser.H265_NALU_TYPE_SPS_ = 0x21; + + /** * Values of profile_idc that indicate additional fields are included in the * SPS.