diff --git a/README.md b/README.md index 0c67142f28..2da6f11eed 100644 --- a/README.md +++ b/README.md @@ -283,10 +283,12 @@ Shaka Player supports: - Raw MP3 to MP3 in MP4 - Raw AC-3 to AC-3 in MP4 - Raw EC-3 to EC-3 in MP4 - - AAC in MPEG-2 TS to AAC in MP4, - with help from [mux.js][] v6.2.0+ - - H.264 in MPEG-2 TS to H.264 in MP4, - with help from [mux.js][] v6.2.0+ + - AAC in MPEG-2 TS to AAC in MP4 + - AC-3 in MPEG-2 TS to AC-3 in MP4 + - EC-3 in MPEG-2 TS to EC-3 in MP4 + - MP3 in MPEG-2 TS to MP3 in MP4 + - MP3 in MPEG-2 TS to raw MP3 + - H.264 in MPEG-2 TS to H.264 in MP4 - Muxed AAC and H.264 in MPEG-2 TS to AAC and H.264 in MP4, with help from [mux.js][] v6.2.0+ diff --git a/build/types/transmuxer b/build/types/transmuxer index 2010dcb323..727a521045 100644 --- a/build/types/transmuxer +++ b/build/types/transmuxer @@ -6,6 +6,9 @@ +../../lib/transmuxer/adts.js +../../lib/transmuxer/ec3.js +../../lib/transmuxer/ec3_transmuxer.js ++../../lib/transmuxer/h264.js +../../lib/transmuxer/mp3_transmuxer.js +../../lib/transmuxer/mpeg_audio.js ++../../lib/transmuxer/mpeg_ts_transmuxer.js +../../lib/transmuxer/muxjs_transmuxer.js ++../../lib/transmuxer/ts_transmuxer.js diff --git a/externs/shaka/codecs.js b/externs/shaka/codecs.js new file mode 100644 index 0000000000..71bf6d964a --- /dev/null +++ b/externs/shaka/codecs.js @@ -0,0 +1,32 @@ +/** + * @typedef {{ + * data: Uint8Array, + * packetLength: number, + * pts: ?number, + * dts: ?number + * }} + * + * @summary MPEG_PES. + * @property {Uint8Array} data + * @property {number} packetLength + * @property {?number} pts + * @property {?number} dts + */ +shaka.extern.MPEG_PES; + + +/** + * @typedef {{ + * data: !Uint8Array, + * fullData: !Uint8Array, + * type: number, + * time: ?number + * }} + * + * @summary VideoNalu. + * @property {!Uint8Array} data + * @property {!Uint8Array} fullData + * @property {number} type + * @property {?number} time + */ +shaka.extern.VideoNalu; diff --git a/karma.conf.js b/karma.conf.js index 50c9da8d88..28e2fe050e 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -242,7 +242,10 @@ module.exports = (config) => { {pattern: 'test/test/assets/hls-raw-ec3/*', included: false}, {pattern: 'test/test/assets/hls-raw-mp3/*', included: false}, {pattern: 'test/test/assets/hls-ts-aac/*', included: false}, + {pattern: 'test/test/assets/hls-ts-ac3/*', included: false}, + {pattern: 'test/test/assets/hls-ts-ec3/*', included: false}, {pattern: 'test/test/assets/hls-ts-h264/*', included: false}, + {pattern: 'test/test/assets/hls-ts-mp3/*', included: false}, {pattern: 'test/test/assets/hls-ts-muxed-aac-h264/*', included: false}, {pattern: 'dist/shaka-player.ui.js', included: false}, {pattern: 'dist/locales.js', included: false}, diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index 9e1182fed1..6f31e8ae12 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -426,8 +426,7 @@ shaka.media.MediaSourceEngine = class { shaka.util.MimeUtils.getFullTypeWithAllCodecs( stream.mimeType, stream.codecs); const TransmuxerEngine = shaka.transmuxer.TransmuxerEngine; - if (needTransmux && - TransmuxerEngine.isSupported(mimeTypeWithAllCodecs, contentType)) { + if (needTransmux) { const transmuxerPlugin = TransmuxerEngine.findTransmuxer(mimeTypeWithAllCodecs); if (transmuxerPlugin) { @@ -1656,8 +1655,7 @@ shaka.media.MediaSourceEngine = class { shaka.util.MimeUtils.getFullTypeWithAllCodecs( stream.mimeType, stream.codecs); const TransmuxerEngine = shaka.transmuxer.TransmuxerEngine; - if (needTransmux && - TransmuxerEngine.isSupported(newMimeTypeWithAllCodecs, contentType)) { + if (needTransmux) { const transmuxerPlugin = TransmuxerEngine.findTransmuxer(newMimeTypeWithAllCodecs); if (transmuxerPlugin) { diff --git a/lib/mss/mss_parser.js b/lib/mss/mss_parser.js index e1e1bac047..5fd1a7aea5 100644 --- a/lib/mss/mss_parser.js +++ b/lib/mss/mss_parser.js @@ -633,10 +633,12 @@ shaka.mss.MssParser = class { } /** @type {shaka.util.Mp4Generator.StreamInfo} */ const streamInfo = { + encrypted: stream.encrypted, timescale: stream.mssPrivateData.timescale, duration: stream.mssPrivateData.duration, videoNalus: videoNalus, audioConfig: new Uint8Array([]), + videoConfig: new Uint8Array([]), data: null, // Data is not necessary for init segement. stream: stream, }; diff --git a/lib/transmuxer/aac_transmuxer.js b/lib/transmuxer/aac_transmuxer.js index 5e11e77145..30e2164bb2 100644 --- a/lib/transmuxer/aac_transmuxer.js +++ b/lib/transmuxer/aac_transmuxer.js @@ -188,10 +188,12 @@ shaka.transmuxer.AacTransmuxer = class { /** @type {shaka.util.Mp4Generator.StreamInfo} */ const streamInfo = { + encrypted: stream.encrypted && stream.drmInfos.length > 0, timescale: sampleRate, duration: duration, videoNalus: [], audioConfig: new Uint8Array([]), + videoConfig: new Uint8Array([]), data: { sequenceNumber: this.frameIndex_, baseMediaDecodeTime: baseMediaDecodeTime, diff --git a/lib/transmuxer/ac3_transmuxer.js b/lib/transmuxer/ac3_transmuxer.js index e8bc106eb4..2728eb020f 100644 --- a/lib/transmuxer/ac3_transmuxer.js +++ b/lib/transmuxer/ac3_transmuxer.js @@ -175,10 +175,12 @@ shaka.transmuxer.Ac3Transmuxer = class { /** @type {shaka.util.Mp4Generator.StreamInfo} */ const streamInfo = { + encrypted: stream.encrypted && stream.drmInfos.length > 0, timescale: sampleRate, duration: duration, videoNalus: [], audioConfig: audioConfig, + videoConfig: new Uint8Array([]), data: { sequenceNumber: this.frameIndex_, baseMediaDecodeTime: baseMediaDecodeTime, diff --git a/lib/transmuxer/ec3_transmuxer.js b/lib/transmuxer/ec3_transmuxer.js index 6e90758ddc..8f99bd107e 100644 --- a/lib/transmuxer/ec3_transmuxer.js +++ b/lib/transmuxer/ec3_transmuxer.js @@ -175,10 +175,12 @@ shaka.transmuxer.Ec3Transmuxer = class { /** @type {shaka.util.Mp4Generator.StreamInfo} */ const streamInfo = { + encrypted: stream.encrypted && stream.drmInfos.length > 0, timescale: sampleRate, duration: duration, videoNalus: [], audioConfig: audioConfig, + videoConfig: new Uint8Array([]), data: { sequenceNumber: this.frameIndex_, baseMediaDecodeTime: baseMediaDecodeTime, diff --git a/lib/transmuxer/h264.js b/lib/transmuxer/h264.js new file mode 100644 index 0000000000..ee772dd681 --- /dev/null +++ b/lib/transmuxer/h264.js @@ -0,0 +1,324 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.transmuxer.H264'); + +goog.require('shaka.util.ExpGolomb'); +goog.require('shaka.util.Uint8ArrayUtils'); + + +/** + * H.264 utils + */ +shaka.transmuxer.H264 = class { + /** + * Read a sequence parameter set and return some interesting video + * properties. A sequence parameter set is the H264 metadata that + * describes the properties of upcoming video frames. + * + * @param {!Array.} nalus + * @return {?{height: number, width: number, videoConfig: !Uint8Array}} + */ + static parseInfo(nalus) { + const H264 = shaka.transmuxer.H264; + if (!nalus.length) { + return null; + } + const spsNalu = nalus.find((nalu) => { + return nalu.type == H264.NALU_TYPE_SPS_; + }); + const ppsNalu = nalus.find((nalu) => { + return nalu.type == H264.NALU_TYPE_PPS_; + }); + if (!spsNalu || !ppsNalu) { + return null; + } + + const expGolombDecoder = new shaka.util.ExpGolomb(spsNalu.data); + // profile_idc + const profileIdc = expGolombDecoder.readUnsignedByte(); + // constraint_set[0-5]_flag + expGolombDecoder.readUnsignedByte(); + // level_idc u(8) + expGolombDecoder.readUnsignedByte(); + // seq_parameter_set_id + expGolombDecoder.skipExpGolomb(); + + // some profiles have more optional data we don't need + if (H264.PROFILES_WITH_OPTIONAL_SPS_DATA_.includes(profileIdc)) { + const chromaFormatIdc = expGolombDecoder.readUnsignedExpGolomb(); + if (chromaFormatIdc === 3) { + // separate_colour_plane_flag + expGolombDecoder.skipBits(1); + } + // bit_depth_luma_minus8 + expGolombDecoder.skipExpGolomb(); + // bit_depth_chroma_minus8 + expGolombDecoder.skipExpGolomb(); + // qpprime_y_zero_transform_bypass_flag + expGolombDecoder.skipBits(1); + // seq_scaling_matrix_present_flag + if (expGolombDecoder.readBoolean()) { + const scalingListCount = (chromaFormatIdc !== 3) ? 8 : 12; + for (let i = 0; i < scalingListCount; i++) { + // seq_scaling_list_present_flag[ i ] + if (expGolombDecoder.readBoolean()) { + if (i < 6) { + expGolombDecoder.skipScalingList(16); + } else { + expGolombDecoder.skipScalingList(64); + } + } + } + } + } + + // log2_max_frame_num_minus4 + expGolombDecoder.skipExpGolomb(); + const picOrderCntType = expGolombDecoder.readUnsignedExpGolomb(); + + if (picOrderCntType === 0) { + // log2_max_pic_order_cnt_lsb_minus4 + expGolombDecoder.readUnsignedExpGolomb(); + } else if (picOrderCntType === 1) { + // delta_pic_order_always_zero_flag + expGolombDecoder.skipBits(1); + // offset_for_non_ref_pic + expGolombDecoder.skipExpGolomb(); + // offset_for_top_to_bottom_field + expGolombDecoder.skipExpGolomb(); + const numRefFramesInPicOrderCntCycle = + expGolombDecoder.readUnsignedExpGolomb(); + for (let i = 0; i < numRefFramesInPicOrderCntCycle; i++) { + // offset_for_ref_frame[ i ] + expGolombDecoder.skipExpGolomb(); + } + } + + // max_num_ref_frames + expGolombDecoder.skipExpGolomb(); + // gaps_in_frame_num_value_allowed_flag + expGolombDecoder.skipBits(1); + + const picWidthInMbsMinus1 = + expGolombDecoder.readUnsignedExpGolomb(); + const picHeightInMapUnitsMinus1 = + expGolombDecoder.readUnsignedExpGolomb(); + + const frameMbsOnlyFlag = expGolombDecoder.readBits(1); + if (frameMbsOnlyFlag === 0) { + // mb_adaptive_frame_field_flag + expGolombDecoder.skipBits(1); + } + // direct_8x8_inference_flag + expGolombDecoder.skipBits(1); + + let frameCropLeftOffset = 0; + let frameCropRightOffset = 0; + let frameCropTopOffset = 0; + let frameCropBottomOffset = 0; + + // frame_cropping_flag + if (expGolombDecoder.readBoolean()) { + frameCropLeftOffset = expGolombDecoder.readUnsignedExpGolomb(); + frameCropRightOffset = expGolombDecoder.readUnsignedExpGolomb(); + frameCropTopOffset = expGolombDecoder.readUnsignedExpGolomb(); + frameCropBottomOffset = expGolombDecoder.readUnsignedExpGolomb(); + } + + const height = ((2 - frameMbsOnlyFlag) * + (picHeightInMapUnitsMinus1 + 1) * 16) - (frameCropTopOffset * 2) - + (frameCropBottomOffset * 2); + const width = ((picWidthInMbsMinus1 + 1) * 16) - + frameCropLeftOffset * 2 - frameCropRightOffset * 2; + + // assemble the SPSs + let sps = []; + const spsData = spsNalu.fullData; + sps.push((spsData.byteLength >>> 8) & 0xff); + sps.push(spsData.byteLength & 0xff); + sps = sps.concat(...spsData); + + // assemble the PPSs + let pps = []; + const ppsData = ppsNalu.fullData; + pps.push((ppsData.byteLength >>> 8) & 0xff); + pps.push(ppsData.byteLength & 0xff); + pps = pps.concat(...ppsData); + + const videoConfig = new Uint8Array( + [ + 0x01, // version + sps[3], // profile + sps[4], // profile compat + sps[5], // level + 0xfc | 3, // lengthSizeMinusOne, hard-coded to 4 bytes + 0xe0 | 1, // 3bit reserved (111) + numOfSequenceParameterSets + ].concat(sps).concat([ + 1, // numOfPictureParameterSets + ]).concat(pps)); + + return { + height, + width, + videoConfig, + }; + } + + /** + * @param {!Array.} nalus + * @return {?{data: !Uint8Array, isKeyframe: boolean}} + */ + static parseFrame(nalus) { + const H264 = shaka.transmuxer.H264; + let isKeyframe = false; + let data = new Uint8Array([]); + const spsNalu = nalus.find((nalu) => { + return nalu.type == H264.NALU_TYPE_SPS_; + }); + let avcSample = false; + for (const nalu of nalus) { + let push = false; + switch (nalu.type) { + case H264.NALU_TYPE_NDR_: { + avcSample = true; + push = true; + const data = nalu.data; + // Only check slice type to detect KF in case SPS found in same packet + // (any keyframe is preceded by SPS ...) + if (spsNalu && data.length > 4) { + // retrieve slice type by parsing beginning of NAL unit (follow + // H264 spec,slice_header definition) to detect keyframe embedded + // in NDR + const sliceType = new shaka.util.ExpGolomb(data).readSliceType(); + // 2 : I slice, 4 : SI slice, 7 : I slice, 9: SI slice + // SI slice : A slice that is coded using intra prediction only and + // using quantisation of the prediction samples. + // An SI slice can be coded such that its decoded samples can be + // constructed identically to an SP slice. + // I slice: A slice that is not an SI slice that is decoded using + // intra prediction only. + if (sliceType === 2 || sliceType === 4 || + sliceType === 7 || sliceType === 9) { + isKeyframe = true; + } + } + break; + } + case H264.NALU_TYPE_IDR_: + avcSample = true; + push = true; + isKeyframe = true; + break; + case H264.NALU_TYPE_SEI_: + push = true; + break; + case H264.NALU_TYPE_SPS_: + push = true; + break; + case H264.NALU_TYPE_PPS_: + push = true; + break; + case H264.NALU_TYPE_AUD_: + push = true; + avcSample = true; + break; + case H264.NALU_TYPE_FILLER_DATA_: + push = true; + break; + default: + push = false; + break; + } + if (avcSample && push) { + const size = nalu.fullData.byteLength; + const naluLength = new Uint8Array(4); + naluLength[0] = (size >> 24) & 0xff; + naluLength[1] = (size >> 16) & 0xff; + naluLength[2] = (size >> 8) & 0xff; + naluLength[3] = size & 0xff; + data = shaka.util.Uint8ArrayUtils.concat( + data, naluLength, nalu.fullData); + } + } + if (!data.byteLength) { + return null; + } + return { + data, + isKeyframe, + }; + } +}; + + +/** + * NALU type for NDR for H.264. + * @const {number} + * @private + */ +shaka.transmuxer.H264.NALU_TYPE_NDR_ = 0x01; + + +/** + * NALU type for Instantaneous Decoder Refresh (IDR) for H.264. + * @const {number} + * @private + */ +shaka.transmuxer.H264.NALU_TYPE_IDR_ = 0x05; + + +/** + * NALU type for Supplemental Enhancement Information (SEI) for H.264. + * @const {number} + * @private + */ +shaka.transmuxer.H264.NALU_TYPE_SEI_ = 0x06; + + +/** + * NALU type for Sequence Parameter Set (SPS) for H.264. + * @const {number} + * @private + */ +shaka.transmuxer.H264.NALU_TYPE_SPS_ = 0x07; + + +/** + * NALU type for Picture Parameter Set (PPS) for H.264. + * @const {number} + * @private + */ +shaka.transmuxer.H264.NALU_TYPE_PPS_ = 0x08; + + +/** + * NALU type for Access Unit Delimiter (AUD) for H.264. + * @const {number} + * @private + */ +shaka.transmuxer.H264.NALU_TYPE_AUD_ = 0x09; + + +/** + * NALU type for Filler Data for H.264. + * @const {number} + * @private + */ +shaka.transmuxer.H264.NALU_TYPE_FILLER_DATA_ = 0x0c; + + +/** + * Values of profile_idc that indicate additional fields are included in the + * SPS. + * see Recommendation ITU-T H.264 (4/2013) + * 7.3.2.1.1 Sequence parameter set data syntax + * + * @const {!Array.} + * @private + */ +shaka.transmuxer.H264.PROFILES_WITH_OPTIONAL_SPS_DATA_ = + [100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134]; diff --git a/lib/transmuxer/mp3_transmuxer.js b/lib/transmuxer/mp3_transmuxer.js index 4771876f8d..913afa0864 100644 --- a/lib/transmuxer/mp3_transmuxer.js +++ b/lib/transmuxer/mp3_transmuxer.js @@ -167,10 +167,12 @@ shaka.transmuxer.Mp3Transmuxer = class { /** @type {shaka.util.Mp4Generator.StreamInfo} */ const streamInfo = { + encrypted: stream.encrypted && stream.drmInfos.length > 0, timescale: sampleRate, duration: duration, videoNalus: [], audioConfig: new Uint8Array([]), + videoConfig: new Uint8Array([]), data: { sequenceNumber: this.frameIndex_, baseMediaDecodeTime: baseMediaDecodeTime, diff --git a/lib/transmuxer/mpeg_ts_transmuxer.js b/lib/transmuxer/mpeg_ts_transmuxer.js new file mode 100644 index 0000000000..bd90b05b6b --- /dev/null +++ b/lib/transmuxer/mpeg_ts_transmuxer.js @@ -0,0 +1,182 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.transmuxer.MpegTsTransmuxer'); + +goog.require('shaka.media.Capabilities'); +goog.require('shaka.transmuxer.MpegAudio'); +goog.require('shaka.transmuxer.TransmuxerEngine'); +goog.require('shaka.util.BufferUtils'); +goog.require('shaka.util.Error'); +goog.require('shaka.util.ManifestParserUtils'); +goog.require('shaka.util.MimeUtils'); +goog.require('shaka.util.TsParser'); +goog.require('shaka.util.Uint8ArrayUtils'); + +/** + * @fileoverview + * + * This transmuxer takes an audio-only TS with MP3, and converts it to + * raw MP3(audio/mpeg). We don't do it in ts_transmuxer.js because the + * output of it is always MP4. This transmuxer is necessary because the only + * browser that supports MP3 in MP4 is Firefox(audio/mp4; codecs="mp3"), + * other browsers don't support it. + */ + +/** + * @implements {shaka.extern.Transmuxer} + * @export + */ +shaka.transmuxer.MpegTsTransmuxer = class { + /** + * @param {string} mimeType + */ + constructor(mimeType) { + /** @private {string} */ + this.originalMimeType_ = mimeType; + + /** @private {?shaka.util.TsParser} */ + this.tsParser_ = null; + } + + + /** + * @override + * @export + */ + destroy() { + // Nothing + } + + + /** + * Check if the mime type and the content type is supported. + * @param {string} mimeType + * @param {string=} contentType + * @return {boolean} + * @override + * @export + */ + isSupported(mimeType, contentType) { + const Capabilities = shaka.media.Capabilities; + + if (!this.isTsContainer_(mimeType)) { + return false; + } + + const ContentType = shaka.util.ManifestParserUtils.ContentType; + const MimeUtils = shaka.util.MimeUtils; + + const codecs = MimeUtils.getCodecs(mimeType); + const allCodecs = MimeUtils.splitCodecs(codecs); + + const audioCodec = shaka.util.ManifestParserUtils.guessCodecsSafe( + ContentType.AUDIO, allCodecs); + const videoCodec = shaka.util.ManifestParserUtils.guessCodecsSafe( + ContentType.VIDEO, allCodecs); + + if (!audioCodec || videoCodec) { + return false; + } + const normalizedCodec = MimeUtils.getNormalizedCodec(audioCodec); + + if (normalizedCodec != 'mp3') { + return false; + } + + return Capabilities.isTypeSupported( + this.convertCodecs(ContentType.AUDIO, mimeType)); + } + + + /** + * Check if the mimetype is 'video/mp2t'. + * @param {string} mimeType + * @return {boolean} + * @private + */ + isTsContainer_(mimeType) { + return mimeType.toLowerCase().split(';')[0] == 'video/mp2t'; + } + + + /** + * @override + * @export + */ + convertCodecs(contentType, mimeType) { + if (this.isTsContainer_(mimeType)) { + return 'audio/mpeg'; + } + return mimeType; + } + + + /** + * @override + * @export + */ + getOrginalMimeType() { + return this.originalMimeType_; + } + + + /** + * @override + * @export + */ + transmux(data) { + const MpegAudio = shaka.transmuxer.MpegAudio; + const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; + + if (!this.tsParser_) { + this.tsParser_ = new shaka.util.TsParser(); + } else { + this.tsParser_.clearData(); + } + + const uint8ArrayData = shaka.util.BufferUtils.toUint8(data); + + const tsParser = this.tsParser_.parse(uint8ArrayData); + const codecs = tsParser.getCodecs(); + if (codecs.audio != 'mp3' || codecs.video) { + return Promise.reject(new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MEDIA, + shaka.util.Error.Code.TRANSMUXING_FAILED)); + } + + let transmuxData = new Uint8Array([]); + + for (const audioData of tsParser.getAudioData()) { + const data = audioData.data; + if (!data) { + continue; + } + let offset = 0; + while (offset < data.length) { + const header = MpegAudio.parseHeader(data, offset); + if (!header) { + offset++; + continue; + } + if (offset + header.frameLength <= data.length) { + transmuxData = Uint8ArrayUtils.concat(transmuxData, + data.subarray(offset, offset + header.frameLength)); + } + offset += header.frameLength; + } + } + + return Promise.resolve(transmuxData); + } +}; + + +shaka.transmuxer.TransmuxerEngine.registerTransmuxer( + 'video/mp2t', + () => new shaka.transmuxer.MpegTsTransmuxer('video/mp2t'), + shaka.transmuxer.TransmuxerEngine.PluginPriority.PREFERRED_SECONDARY); diff --git a/lib/transmuxer/transmuxer_engine.js b/lib/transmuxer/transmuxer_engine.js index 876043e44b..16d2a00e62 100644 --- a/lib/transmuxer/transmuxer_engine.js +++ b/lib/transmuxer/transmuxer_engine.js @@ -6,7 +6,6 @@ goog.provide('shaka.transmuxer.TransmuxerEngine'); -goog.require('goog.asserts'); goog.require('shaka.util.IDestroyable'); @@ -27,41 +26,59 @@ shaka.transmuxer.TransmuxerEngine = class { /** * @param {string} mimeType * @param {!shaka.extern.TransmuxerPlugin} plugin + * @param {number} priority * @export */ static registerTransmuxer(mimeType, plugin, priority) { const TransmuxerEngine = shaka.transmuxer.TransmuxerEngine; - goog.asserts.assert(priority == undefined || priority > 0, - 'explicit priority must be > 0'); - const mimeTypeNormalizated = TransmuxerEngine.normalizeMimeType_(mimeType); - const existing = TransmuxerEngine.transmuxerMap_[mimeTypeNormalizated]; - if (!existing || priority >= existing.priority) { - TransmuxerEngine.transmuxerMap_[mimeTypeNormalizated] = { - priority: priority, - plugin: plugin, - }; - } + const normalizedMimetype = TransmuxerEngine.normalizeMimeType_(mimeType); + const key = normalizedMimetype + '-' + priority; + TransmuxerEngine.transmuxerMap_[key] = { + priority: priority, + plugin: plugin, + }; } /** * @param {string} mimeType + * @param {number} priority * @export */ - static unregisterTransmuxer(mimeType) { + static unregisterTransmuxer(mimeType, priority) { const TransmuxerEngine = shaka.transmuxer.TransmuxerEngine; - const mimeTypeNormalizated = TransmuxerEngine.normalizeMimeType_(mimeType); - delete TransmuxerEngine.transmuxerMap_[mimeTypeNormalizated]; + const normalizedMimetype = TransmuxerEngine.normalizeMimeType_(mimeType); + const key = normalizedMimetype + '-' + priority; + delete TransmuxerEngine.transmuxerMap_[key]; } /** + * @param {string} mimeType + * @param {string=} contentType * @return {?shaka.extern.TransmuxerPlugin} * @export */ - static findTransmuxer(mimeType) { + static findTransmuxer(mimeType, contentType) { const TransmuxerEngine = shaka.transmuxer.TransmuxerEngine; - const mimeTypeNormalizated = TransmuxerEngine.normalizeMimeType_(mimeType); - const object = TransmuxerEngine.transmuxerMap_[mimeTypeNormalizated]; - return object ? object.plugin : null; + const normalizedMimetype = TransmuxerEngine.normalizeMimeType_(mimeType); + const priorities = [ + TransmuxerEngine.PluginPriority.APPLICATION, + TransmuxerEngine.PluginPriority.PREFERRED, + TransmuxerEngine.PluginPriority.PREFERRED_SECONDARY, + TransmuxerEngine.PluginPriority.FALLBACK, + ]; + for (const priority of priorities) { + const key = normalizedMimetype + '-' + priority; + const object = TransmuxerEngine.transmuxerMap_[key]; + if (object) { + const transmuxer = object.plugin(); + const isSupported = transmuxer.isSupported(mimeType, contentType); + transmuxer.destroy(); + if (isSupported) { + return object.plugin; + } + } + } + return null; } /** @@ -85,10 +102,7 @@ shaka.transmuxer.TransmuxerEngine = class { if (!transmuxerPlugin) { return false; } - const transmuxer = transmuxerPlugin(); - const isSupported = transmuxer.isSupported(mimeType, contentType); - transmuxer.destroy(); - return isSupported; + return true; } /** @@ -140,7 +154,8 @@ shaka.transmuxer.TransmuxerEngine.transmuxerMap_ = {}; */ shaka.transmuxer.TransmuxerEngine.PluginPriority = { 'FALLBACK': 1, - 'PREFERRED': 2, - 'APPLICATION': 3, + 'PREFERRED_SECONDARY': 2, + 'PREFERRED': 3, + 'APPLICATION': 4, }; diff --git a/lib/transmuxer/ts_transmuxer.js b/lib/transmuxer/ts_transmuxer.js new file mode 100644 index 0000000000..6963f7fdee --- /dev/null +++ b/lib/transmuxer/ts_transmuxer.js @@ -0,0 +1,689 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.transmuxer.TsTransmuxer'); + +goog.require('shaka.media.Capabilities'); +goog.require('shaka.transmuxer.Ac3'); +goog.require('shaka.transmuxer.ADTS'); +goog.require('shaka.transmuxer.Ec3'); +goog.require('shaka.transmuxer.H264'); +goog.require('shaka.transmuxer.MpegAudio'); +goog.require('shaka.transmuxer.TransmuxerEngine'); +goog.require('shaka.util.BufferUtils'); +goog.require('shaka.util.Error'); +goog.require('shaka.util.ManifestParserUtils'); +goog.require('shaka.util.MimeUtils'); +goog.require('shaka.util.Mp4Generator'); +goog.require('shaka.util.TsParser'); +goog.require('shaka.util.Uint8ArrayUtils'); + + +/** + * @implements {shaka.extern.Transmuxer} + * @export + */ +shaka.transmuxer.TsTransmuxer = class { + /** + * @param {string} mimeType + */ + constructor(mimeType) { + /** @private {string} */ + this.originalMimeType_ = mimeType; + + /** @private {number} */ + this.frameIndex_ = 0; + + /** @private {!Map.} */ + this.initSegments = new Map(); + + /** @private {?shaka.util.TsParser} */ + this.tsParser_ = null; + } + + + /** + * @override + * @export + */ + destroy() { + this.initSegments.clear(); + } + + + /** + * Check if the mime type and the content type is supported. + * @param {string} mimeType + * @param {string=} contentType + * @return {boolean} + * @override + * @export + */ + isSupported(mimeType, contentType) { + const Capabilities = shaka.media.Capabilities; + + if (!this.isTsContainer_(mimeType)) { + return false; + } + + const ContentType = shaka.util.ManifestParserUtils.ContentType; + const MimeUtils = shaka.util.MimeUtils; + + const codecs = MimeUtils.getCodecs(mimeType); + const allCodecs = MimeUtils.splitCodecs(codecs); + + const audioCodec = shaka.util.ManifestParserUtils.guessCodecsSafe( + ContentType.AUDIO, allCodecs); + const videoCodec = shaka.util.ManifestParserUtils.guessCodecsSafe( + ContentType.VIDEO, allCodecs); + + // We don't support muxed content yet. + if (audioCodec && videoCodec) { + return false; + } + + const TsTransmuxer = shaka.transmuxer.TsTransmuxer; + + if (audioCodec) { + const normalizedCodec = MimeUtils.getNormalizedCodec(audioCodec); + if (!TsTransmuxer.SUPPORTED_AUDIO_CODECS_.includes(normalizedCodec)) { + return false; + } + } + + if (videoCodec) { + const normalizedCodec = MimeUtils.getNormalizedCodec(videoCodec); + if (!TsTransmuxer.SUPPORTED_VIDEO_CODECS_.includes(normalizedCodec)) { + return false; + } + } + + if (contentType) { + return Capabilities.isTypeSupported( + this.convertCodecs(contentType, mimeType)); + } + + const audioMime = this.convertCodecs(ContentType.AUDIO, mimeType); + const videoMime = this.convertCodecs(ContentType.VIDEO, mimeType); + return Capabilities.isTypeSupported(audioMime) || + Capabilities.isTypeSupported(videoMime); + } + + + /** + * Check if the mimetype is 'video/mp2t'. + * @param {string} mimeType + * @return {boolean} + * @private + */ + isTsContainer_(mimeType) { + return mimeType.toLowerCase().split(';')[0] == 'video/mp2t'; + } + + + /** + * @override + * @export + */ + convertCodecs(contentType, mimeType) { + if (this.isTsContainer_(mimeType)) { + const ContentType = shaka.util.ManifestParserUtils.ContentType; + // The replace it's necessary because Firefox(the only browser that + // supports MP3 in MP4) only support the MP3 codec with the mp3 string. + // MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.34"') -> false + // MediaSource.isTypeSupported('audio/mp4; codecs="mp3"') -> true + const codecs = shaka.util.MimeUtils.getCodecs(mimeType) + .replace('mp4a.40.34', 'mp3'); + if (contentType == ContentType.AUDIO) { + return `audio/mp4; codecs="${codecs}"`; + } + return `video/mp4; codecs="${codecs}"`; + } + return mimeType; + } + + + /** + * @override + * @export + */ + getOrginalMimeType() { + return this.originalMimeType_; + } + + + /** + * @override + * @export + */ + transmux(data, stream, reference, duration) { + if (!this.tsParser_) { + this.tsParser_ = new shaka.util.TsParser(); + } else { + this.tsParser_.clearData(); + } + const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; + + const uint8ArrayData = shaka.util.BufferUtils.toUint8(data); + + let timestamp = reference.endTime * 1000; + + const tsParser = this.tsParser_.parse(uint8ArrayData); + const startTime = tsParser.getStartTime(); + + if (startTime.audio != null) { + timestamp = startTime.audio; + } + if (startTime.video != null) { + timestamp = startTime.video; + } + let streamInfo; + const codecs = tsParser.getCodecs(); + try { + switch (codecs.audio) { + case 'aac': + streamInfo = + this.getAacStreamInfo_(tsParser, timestamp, stream, duration); + break; + case 'ac3': + streamInfo = + this.getAc3StreamInfo_(tsParser, timestamp, stream, duration); + break; + case 'ec3': + streamInfo = + this.getEc3StreamInfo_(tsParser, timestamp, stream, duration); + break; + case 'mp3': + streamInfo = + this.getMp3StreamInfo_(tsParser, timestamp, stream, duration); + break; + } + switch (codecs.video) { + case 'avc': + streamInfo = + this.getAvcStreamInfo_(tsParser, timestamp, stream, duration); + break; + } + } catch (e) { + return Promise.reject(e); + } + + if (!streamInfo) { + return Promise.reject(new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MEDIA, + shaka.util.Error.Code.TRANSMUXING_FAILED)); + } + + const mp4Generator = new shaka.util.Mp4Generator(streamInfo); + let initSegment; + if (!this.initSegments.has(stream.id)) { + initSegment = mp4Generator.initSegment(); + this.initSegments.set(stream.id, initSegment); + } else { + initSegment = this.initSegments.get(stream.id); + } + const segmentData = mp4Generator.segmentData(); + + this.frameIndex_++; + const transmuxData = Uint8ArrayUtils.concat(initSegment, segmentData); + return Promise.resolve(transmuxData); + } + + + /** + * @param {shaka.util.TsParser} tsParser + * @param {number} timestamp + * @param {shaka.extern.Stream} stream + * @param {number} duration + * @return {shaka.util.Mp4Generator.StreamInfo} + * @private + */ + getAacStreamInfo_(tsParser, timestamp, stream, duration) { + const ADTS = shaka.transmuxer.ADTS; + + /** @type {!Array.} */ + const samples = []; + + let info; + + for (const audioData of tsParser.getAudioData()) { + const data = audioData.data; + if (!data) { + continue; + } + let offset = 0; + info = ADTS.parseInfo(data, offset); + if (!info) { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MEDIA, + shaka.util.Error.Code.TRANSMUXING_FAILED); + } + stream.audioSamplingRate = info.sampleRate; + stream.channelsCount = info.channelCount; + stream.codecs = info.codec; + stream.type = 'audio'; + + while (offset < data.length) { + const header = ADTS.parseHeader(data, offset); + if (!header) { + // We will increment one byte each time until we find the header. + offset++; + continue; + } + const length = header.headerLength + header.frameLength; + if (offset + length <= data.length) { + const frameData = data.subarray( + offset + header.headerLength, offset + length); + + samples.push({ + data: frameData, + size: header.frameLength, + duration: ADTS.AAC_SAMPLES_PER_FRAME, + cts: 0, + flags: { + isLeading: 0, + isDependedOn: 0, + hasRedundancy: 0, + degradPrio: 0, + dependsOn: 2, + isNonSync: 0, + }, + }); + } + offset += length; + } + } + + if (!info) { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MEDIA, + shaka.util.Error.Code.TRANSMUXING_FAILED); + } + + /** @type {number} */ + const sampleRate = info.sampleRate; + /** @type {number} */ + const baseMediaDecodeTime = Math.floor(timestamp * sampleRate / 1000); + + return { + encrypted: stream.encrypted && stream.drmInfos.length > 0, + timescale: sampleRate, + duration: duration, + videoNalus: [], + audioConfig: new Uint8Array([]), + videoConfig: new Uint8Array([]), + data: { + sequenceNumber: this.frameIndex_, + baseMediaDecodeTime: baseMediaDecodeTime, + samples: samples, + }, + stream: stream, + }; + } + + + /** + * @param {shaka.util.TsParser} tsParser + * @param {number} timestamp + * @param {shaka.extern.Stream} stream + * @param {number} duration + * @return {shaka.util.Mp4Generator.StreamInfo} + * @private + */ + getAc3StreamInfo_(tsParser, timestamp, stream, duration) { + const Ac3 = shaka.transmuxer.Ac3; + + /** @type {!Array.} */ + const samples = []; + + /** @type {number} */ + let sampleRate = 0; + + /** @type {!Uint8Array} */ + let audioConfig = new Uint8Array([]); + + for (const audioData of tsParser.getAudioData()) { + const data = audioData.data; + let offset = 0; + while (offset < data.length) { + const frame = Ac3.parseFrame(data, offset); + if (!frame) { + offset++; + continue; + } + stream.audioSamplingRate = frame.sampleRate; + stream.channelsCount = frame.channelCount; + sampleRate = frame.sampleRate; + audioConfig = frame.audioConfig; + + const frameData = data.subarray( + offset, offset + frame.frameLength); + + samples.push({ + data: frameData, + size: frame.frameLength, + duration: Ac3.AC3_SAMPLES_PER_FRAME, + cts: 0, + flags: { + isLeading: 0, + isDependedOn: 0, + hasRedundancy: 0, + degradPrio: 0, + dependsOn: 2, + isNonSync: 0, + }, + }); + offset += frame.frameLength; + } + } + + if (sampleRate == 0 || audioConfig.byteLength == 0) { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MEDIA, + shaka.util.Error.Code.TRANSMUXING_FAILED); + } + + + /** @type {number} */ + const baseMediaDecodeTime = Math.floor(timestamp * sampleRate / 1000); + + return { + encrypted: stream.encrypted && stream.drmInfos.length > 0, + timescale: sampleRate, + duration: duration, + videoNalus: [], + audioConfig: audioConfig, + videoConfig: new Uint8Array([]), + data: { + sequenceNumber: this.frameIndex_, + baseMediaDecodeTime: baseMediaDecodeTime, + samples: samples, + }, + stream: stream, + }; + } + + + /** + * @param {shaka.util.TsParser} tsParser + * @param {number} timestamp + * @param {shaka.extern.Stream} stream + * @param {number} duration + * @return {shaka.util.Mp4Generator.StreamInfo} + * @private + */ + getEc3StreamInfo_(tsParser, timestamp, stream, duration) { + const Ec3 = shaka.transmuxer.Ec3; + + /** @type {!Array.} */ + const samples = []; + + /** @type {number} */ + let sampleRate = 0; + + /** @type {!Uint8Array} */ + let audioConfig = new Uint8Array([]); + + for (const audioData of tsParser.getAudioData()) { + const data = audioData.data; + let offset = 0; + while (offset < data.length) { + const frame = Ec3.parseFrame(data, offset); + if (!frame) { + offset++; + continue; + } + stream.audioSamplingRate = frame.sampleRate; + stream.channelsCount = frame.channelCount; + sampleRate = frame.sampleRate; + audioConfig = frame.audioConfig; + + const frameData = data.subarray( + offset, offset + frame.frameLength); + + samples.push({ + data: frameData, + size: frame.frameLength, + duration: Ec3.EC3_SAMPLES_PER_FRAME, + cts: 0, + flags: { + isLeading: 0, + isDependedOn: 0, + hasRedundancy: 0, + degradPrio: 0, + dependsOn: 2, + isNonSync: 0, + }, + }); + offset += frame.frameLength; + } + } + + if (sampleRate == 0 || audioConfig.byteLength == 0) { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MEDIA, + shaka.util.Error.Code.TRANSMUXING_FAILED); + } + + + /** @type {number} */ + const baseMediaDecodeTime = Math.floor(timestamp * sampleRate / 1000); + + return { + encrypted: stream.encrypted && stream.drmInfos.length > 0, + timescale: sampleRate, + duration: duration, + videoNalus: [], + audioConfig: audioConfig, + videoConfig: new Uint8Array([]), + data: { + sequenceNumber: this.frameIndex_, + baseMediaDecodeTime: baseMediaDecodeTime, + samples: samples, + }, + stream: stream, + }; + } + + + /** + * @param {shaka.util.TsParser} tsParser + * @param {number} timestamp + * @param {shaka.extern.Stream} stream + * @param {number} duration + * @return {shaka.util.Mp4Generator.StreamInfo} + * @private + */ + getMp3StreamInfo_(tsParser, timestamp, stream, duration) { + const MpegAudio = shaka.transmuxer.MpegAudio; + + /** @type {!Array.} */ + const samples = []; + + let firstHeader; + + for (const audioData of tsParser.getAudioData()) { + const data = audioData.data; + if (!data) { + continue; + } + let offset = 0; + while (offset < data.length) { + const header = MpegAudio.parseHeader(data, offset); + if (!header) { + offset++; + continue; + } + if (!firstHeader) { + firstHeader = header; + } + if (offset + header.frameLength <= data.length) { + samples.push({ + data: data.subarray(offset, offset + header.frameLength), + size: header.frameLength, + duration: MpegAudio.MPEG_AUDIO_SAMPLE_PER_FRAME, + cts: 0, + flags: { + isLeading: 0, + isDependedOn: 0, + hasRedundancy: 0, + degradPrio: 0, + dependsOn: 2, + isNonSync: 0, + }, + }); + } + offset += header.frameLength; + } + } + if (!firstHeader) { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MEDIA, + shaka.util.Error.Code.TRANSMUXING_FAILED); + } + /** @type {number} */ + const sampleRate = firstHeader.sampleRate; + /** @type {number} */ + const baseMediaDecodeTime = Math.floor(timestamp * sampleRate / 1000); + + return { + encrypted: stream.encrypted && stream.drmInfos.length > 0, + timescale: sampleRate, + duration: duration, + videoNalus: [], + audioConfig: new Uint8Array([]), + videoConfig: new Uint8Array([]), + data: { + sequenceNumber: this.frameIndex_, + baseMediaDecodeTime: baseMediaDecodeTime, + samples: samples, + }, + stream: stream, + }; + } + + + /** + * @param {shaka.util.TsParser} tsParser + * @param {number} timestamp + * @param {shaka.extern.Stream} stream + * @param {number} duration + * @return {shaka.util.Mp4Generator.StreamInfo} + * @private + */ + getAvcStreamInfo_(tsParser, timestamp, stream, duration) { + const H264 = shaka.transmuxer.H264; + const timescale = shaka.util.TsParser.Timescale; + + /** @type {!Array.} */ + const samples = []; + + /** @type {?number} */ + let baseMediaDecodeTime = null; + + let nalus = []; + const videoData = tsParser.getVideoData().filter((pes) => { + return pes.pts != null && pes.dts != null; + }); + for (let i = 0; i < videoData.length; i++) { + const pes = videoData[i]; + let nextPes; + if (i + 1 < videoData.length) { + nextPes = videoData[i + 1]; + } + const dataNalus = tsParser.parseAvcNalus(pes, nextPes); + nalus = nalus.concat(dataNalus); + const frame = H264.parseFrame(dataNalus); + if (!frame) { + continue; + } + if (baseMediaDecodeTime == null && pes.dts != null) { + baseMediaDecodeTime = pes.dts; + } + let duration; + if (i + 1 < videoData.length) { + duration = (videoData[i + 1].dts || 0) - (pes.dts || 0); + } else { + duration = (pes.dts || 0) - (videoData[i - 1].dts || 0); + } + samples.push({ + data: frame.data, + size: frame.data.byteLength, + duration: duration, + cts: Math.round((pes.pts || 0) - (pes.dts || 0)), + flags: { + isLeading: 0, + isDependedOn: 0, + hasRedundancy: 0, + degradPrio: 0, + dependsOn: frame.isKeyframe ? 2 : 1, + isNonSync: frame.isKeyframe ? 0 : 1, + }, + }); + } + + const info = H264.parseInfo(nalus); + + if (!info || baseMediaDecodeTime == null) { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MEDIA, + shaka.util.Error.Code.TRANSMUXING_FAILED); + } + stream.height = info.height; + stream.width = info.width; + + return { + encrypted: stream.encrypted && stream.drmInfos.length > 0, + timescale: timescale, + duration: duration, + videoNalus: [], + audioConfig: new Uint8Array([]), + videoConfig: info.videoConfig, + data: { + sequenceNumber: this.frameIndex_, + baseMediaDecodeTime: baseMediaDecodeTime, + samples: samples, + }, + stream: stream, + }; + } +}; + +/** + * Supported audio codecs. + * + * @private + * @const {!Array.} + */ +shaka.transmuxer.TsTransmuxer.SUPPORTED_AUDIO_CODECS_ = [ + 'aac', + 'ac-3', + 'ec-3', + 'mp3', +]; + +/** + * Supported audio codecs. + * + * @private + * @const {!Array.} + */ +shaka.transmuxer.TsTransmuxer.SUPPORTED_VIDEO_CODECS_ = [ + 'avc', +]; + + +shaka.transmuxer.TransmuxerEngine.registerTransmuxer( + 'video/mp2t', + () => new shaka.transmuxer.TsTransmuxer('video/mp2t'), + shaka.transmuxer.TransmuxerEngine.PluginPriority.PREFERRED); diff --git a/lib/util/exp_golomb.js b/lib/util/exp_golomb.js index 0fad5ad761..016b7908bf 100644 --- a/lib/util/exp_golomb.js +++ b/lib/util/exp_golomb.js @@ -203,4 +203,18 @@ shaka.util.ExpGolomb = class { lastScale = (nextScale === 0) ? lastScale : nextScale; } } + + /** + * Return the slice type + * + * @return {number} + */ + readSliceType() { + // skip Nalu type + this.readUnsignedByte(); + // discard first_mb_in_slice + this.readUnsignedExpGolomb(); + // return slice_type + return this.readUnsignedExpGolomb(); + } }; diff --git a/lib/util/mime_utils.js b/lib/util/mime_utils.js index edafb8914a..b1e4df472a 100644 --- a/lib/util/mime_utils.js +++ b/lib/util/mime_utils.js @@ -141,6 +141,7 @@ shaka.util.MimeUtils = class { switch (true) { case base === 'mp4a' && profile === '69': case base === 'mp4a' && profile === '6b': + case base === 'mp4a' && profile === '40.34': return 'mp3'; case base === 'mp4a' && profile === '66': case base === 'mp4a' && profile === '67': diff --git a/lib/util/mp4_generator.js b/lib/util/mp4_generator.js index b441558cdc..99ac0a33a9 100644 --- a/lib/util/mp4_generator.js +++ b/lib/util/mp4_generator.js @@ -21,6 +21,9 @@ shaka.util.Mp4Generator = class { /** @private {!shaka.extern.Stream} */ this.stream_ = streamInfo.stream; + /** @private {boolean} */ + this.encrypted_ = streamInfo.encrypted; + /** @private {number} */ this.timescale_ = streamInfo.timescale; @@ -36,6 +39,9 @@ shaka.util.Mp4Generator = class { /** @private {!Uint8Array} */ this.audioConfig_ = streamInfo.audioConfig; + /** @private {!Uint8Array} */ + this.videoConfig_ = streamInfo.videoConfig; + /** @private {number} */ this.sequenceNumber_ = 0; @@ -336,12 +342,66 @@ shaka.util.Mp4Generator = class { avc1_() { const Mp4Generator = shaka.util.Mp4Generator; - const NALUTYPE_SPS = 7; - const NALUTYPE_PPS = 8; - const width = this.stream_.width || 0; const height = this.stream_.height || 0; + let avcCBox; + if (this.videoConfig_.byteLength > 0) { + avcCBox = Mp4Generator.box('avcC', this.videoConfig_); + } else { + avcCBox = Mp4Generator.box('avcC', this.avcC_()); + } + + const avc1Bytes = new Uint8Array([ + 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // data_reference_index + 0x00, 0x00, // pre_defined + 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // pre_defined + ...this.breakNumberIntoBytes_(width, 2), // width + ...this.breakNumberIntoBytes_(height, 2), // height + 0x00, 0x48, 0x00, 0x00, // horizresolution + 0x00, 0x48, 0x00, 0x00, // vertresolution + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // frame_count + 0x13, + 0x76, 0x69, 0x64, 0x65, + 0x6f, 0x6a, 0x73, 0x2d, + 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x69, 0x62, 0x2d, + 0x68, 0x6c, 0x73, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, // compressorname + 0x00, 0x18, // depth = 24 + 0x11, 0x11, // pre_defined = -1 + ]); + + let sinfBox = new Uint8Array([]); + if (this.encrypted_) { + sinfBox = this.sinf_(); + } + + let boxName = 'avc1'; + if (this.encrypted_) { + boxName = 'encv'; + } + return Mp4Generator.box(boxName, avc1Bytes, avcCBox, sinfBox); + } + + /** + * Generate a AVCC box + * + * @return {!Uint8Array} + * @private + */ + avcC_() { + const NALUTYPE_SPS = 7; + const NALUTYPE_PPS = 8; + // length = 7 by default (0 SPS and 0 PPS) let avcCLength = 7; @@ -402,46 +462,7 @@ shaka.util.Mp4Generator = class { avcCBytes.set(pps[n], i); i += pps[n].length; } - const avcCBox = Mp4Generator.box('avcC', avcCBytes); - - const avc1Bytes = new Uint8Array([ - 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, // reserved - 0x00, 0x01, // data_reference_index - 0x00, 0x00, // pre_defined - 0x00, 0x00, // reserved - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, // pre_defined - ...this.breakNumberIntoBytes_(width, 2), // width - ...this.breakNumberIntoBytes_(height, 2), // height - 0x00, 0x48, 0x00, 0x00, // horizresolution - 0x00, 0x48, 0x00, 0x00, // vertresolution - 0x00, 0x00, 0x00, 0x00, // reserved - 0x00, 0x01, // frame_count - 0x13, - 0x76, 0x69, 0x64, 0x65, - 0x6f, 0x6a, 0x73, 0x2d, - 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x69, 0x62, 0x2d, - 0x68, 0x6c, 0x73, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, // compressorname - 0x00, 0x18, // depth = 24 - 0x11, 0x11, // pre_defined = -1 - ]); - - let sinfBox = new Uint8Array([]); - if (this.stream_.encrypted) { - sinfBox = this.sinf_(); - } - - let boxName = 'avc1'; - if (this.stream_.encrypted) { - boxName = 'encv'; - } - return Mp4Generator.box(boxName, avc1Bytes, avcCBox, sinfBox); + return avcCBytes; } /** @@ -491,12 +512,12 @@ shaka.util.Mp4Generator = class { const dac3Box = Mp4Generator.box('dac3', this.audioConfig_); let sinfBox = new Uint8Array([]); - if (this.stream_.encrypted) { + if (this.encrypted_) { sinfBox = this.sinf_(); } let boxName = 'ac-3'; - if (this.stream_.encrypted) { + if (this.encrypted_) { boxName = 'enca'; } return Mp4Generator.box(boxName, this.audioStsd_(), dac3Box, sinfBox); @@ -513,12 +534,12 @@ shaka.util.Mp4Generator = class { const dec3Box = Mp4Generator.box('dec3', this.audioConfig_); let sinfBox = new Uint8Array([]); - if (this.stream_.encrypted) { + if (this.encrypted_) { sinfBox = this.sinf_(); } let boxName = 'ec-3'; - if (this.stream_.encrypted) { + if (this.encrypted_) { boxName = 'enca'; } return Mp4Generator.box(boxName, this.audioStsd_(), dec3Box, sinfBox); @@ -540,12 +561,12 @@ shaka.util.Mp4Generator = class { } let sinfBox = new Uint8Array([]); - if (this.stream_.encrypted) { + if (this.encrypted_) { sinfBox = this.sinf_(); } let boxName = 'mp4a'; - if (this.stream_.encrypted) { + if (this.encrypted_) { boxName = 'enca'; } return Mp4Generator.box(boxName, this.audioStsd_(), esdsBox, sinfBox); @@ -662,7 +683,7 @@ shaka.util.Mp4Generator = class { */ pssh_() { let boxes = new Uint8Array([]); - if (!this.stream_.encrypted) { + if (!this.encrypted_) { return boxes; } @@ -1199,14 +1220,18 @@ shaka.util.Mp4Generator.DINF_ = new Uint8Array([]); /** * @typedef {{ + * encrypted: boolean, * timescale: number, * duration: number, * videoNalus: !Array., * audioConfig: !Uint8Array, + * videoConfig: !Uint8Array, * data: ?shaka.util.Mp4Generator.Data, * stream: !shaka.extern.Stream * }} * + * @property {boolean} encrypted + * Indicate if the stream is encrypted. * @property {number} timescale * The Stream's timescale. * @property {number} duration @@ -1215,6 +1240,8 @@ shaka.util.Mp4Generator.DINF_ = new Uint8Array([]); * The stream's video nalus. * @property {!Uint8Array} audioConfig * The stream's audio config. + * @property {!Uint8Array} videoConfig + * The stream's video config. * @property {?shaka.util.Mp4Generator.Data} data * The stream's data. * @property {!shaka.extern.Stream} stream diff --git a/lib/util/ts_parser.js b/lib/util/ts_parser.js index b64e69a7aa..1f42442104 100644 --- a/lib/util/ts_parser.js +++ b/lib/util/ts_parser.js @@ -57,6 +57,19 @@ shaka.util.TsParser = class { this.id3Data_ = []; } + /** + * Clear previous data + * + * @export + */ + clearData() { + this.videoStartTime_ = null; + this.videoData_ = []; + this.audioStartTime_ = null; + this.audioData_ = []; + this.id3Data_ = []; + } + /** * Parse the given data * @@ -65,7 +78,7 @@ shaka.util.TsParser = class { * @export */ parse(data) { - const timescale = shaka.util.TsParser.Timescale_; + const timescale = shaka.util.TsParser.Timescale; const packetLength = shaka.util.TsParser.PacketLength_; // A TS fragment should contain at least 3 TS packets, a PAT, a PMT, and @@ -148,7 +161,7 @@ shaka.util.TsParser = class { } case this.videoPid_: { const videoData = data.subarray(offset, start + packetLength); - const pes = this.parsePES_(videoData); + const pes = this.parsePES_(videoData, /* isVideo= */ true); if (pes && pes.pts != null) { const startTime = Math.min(pes.dts, pes.pts) / timescale; if (this.videoStartTime_ == null || @@ -327,10 +340,11 @@ shaka.util.TsParser = class { * Parse PES * * @param {Uint8Array} data - * @return {?shaka.util.TsParser.PES} + * @param {boolean=} isVideo + * @return {?shaka.extern.MPEG_PES} * @private */ - parsePES_(data) { + parsePES_(data, isVideo = false) { const startPrefix = (data[0] << 16) | (data[1] << 8) | data[2]; // In certain live streams, the start of a TS fragment has ts packets // that are frame data that is continuing from the previous fragment. This @@ -338,15 +352,21 @@ shaka.util.TsParser = class { if (startPrefix !== 1) { return null; } - /** @type {shaka.util.TsParser.PES} */ + /** @type {shaka.extern.MPEG_PES} */ const pes = { data: new Uint8Array(0), // get the packet length, this will be 0 for video - packetLength: 6 + ((data[4] << 8) | data[5]), + packetLength: ((data[4] << 8) | data[5]), pts: null, dts: null, }; + // If it is video, we expect the packet length to be 0. When the PES packet + // length is set to zero, the PES packet can be any length. + if (isVideo && pes.packetLength != 0) { + return null; + } + // PES packets may be annotated with a PTS value, or a PTS value // and a DTS value. Determine what combination of values is // available to work with. @@ -395,15 +415,16 @@ shaka.util.TsParser = class { * The code is based on hls.js * Credit to https://github.com/video-dev/hls.js/blob/master/src/demux/tsdemuxer.ts * - * @param {shaka.util.TsParser.PES} pes - * @return {!Array.} - * @private + * @param {shaka.extern.MPEG_PES} pes + * @param {?shaka.extern.MPEG_PES=} nextPes + * @return {!Array.} + * @export */ - parseAvcNalus_(pes) { - const timescale = shaka.util.TsParser.Timescale_; + parseAvcNalus(pes, nextPes) { + const timescale = shaka.util.TsParser.Timescale; const time = pes.pts ? pes.pts / timescale : null; - const data = pes.data; - const len = data.byteLength; + let data = pes.data; + let len = data.byteLength; // A NALU does not contain is its size. // The Annex B specification solves this by requiring ‘Start Codes’ to @@ -412,7 +433,7 @@ shaka.util.TsParser = class { // More info in: https://stackoverflow.com/questions/24884827/possible-locations-for-sequence-picture-parameter-sets-for-h-264-stream/24890903#24890903 let numZeros = 0; - /** @type {!Array.} */ + /** @type {!Array.} */ const nalus = []; // Start position includes the first byte where we read the type. @@ -421,10 +442,26 @@ shaka.util.TsParser = class { // Extracted from the first byte. let lastNaluType = 0; + let tryToFinishLastNalu = false; + + /** @type {?shaka.extern.VideoNalu} */ + let infoOfLastNalu; + for (let i = 0; i < len; ++i) { const value = data[i]; if (!value) { numZeros++; + } else if (numZeros >= 2 && value == 1 && tryToFinishLastNalu) { + // If we are scanning the next PES, we need append the data to the + // previous Nalu and don't scan for more nalus. + if (i <= 2) { + break; + } + infoOfLastNalu.data = shaka.util.Uint8ArrayUtils.concat( + infoOfLastNalu.data, data.subarray(0, i - 2)); + infoOfLastNalu.fullData = shaka.util.Uint8ArrayUtils.concat( + infoOfLastNalu.fullData, data.subarray(0, i - 2)); + break; } else if (numZeros >= 2 && value == 1) { // We just read a start code. Consume the NALU we passed, if any. if (lastNaluStart >= 0) { @@ -436,10 +473,11 @@ shaka.util.TsParser = class { const startCodeSize = (numZeros > 3 ? 3 : numZeros) + 1; const lastByteToKeep = i - startCodeSize; - /** @type {shaka.util.TsParser.AvcNalu} */ + /** @type {shaka.extern.VideoNalu} */ const nalu = { // subarray's end position is exclusive, so add one. data: data.subarray(firstByteToKeep, lastByteToKeep + 1), + fullData: data.subarray(lastNaluStart, lastByteToKeep + 1), type: lastNaluType, time: time, }; @@ -461,19 +499,37 @@ shaka.util.TsParser = class { } else { numZeros = 0; } + // If we have gone through all the data from the PES and we have an + // unfinished Nalu, we will try to use the next PES to complete the + // unfinished Nalu. + if (i >= (len - 1) && lastNaluStart >= 0 && numZeros >= 0) { + if (tryToFinishLastNalu) { + infoOfLastNalu.data = shaka.util.Uint8ArrayUtils.concat( + infoOfLastNalu.data, data); + infoOfLastNalu.fullData = shaka.util.Uint8ArrayUtils.concat( + infoOfLastNalu.fullData, data); + } else { + tryToFinishLastNalu = true; + // The rest of the buffer was a NALU. + // Because the start position includes the type, skip the first byte. + const firstByteToKeep = lastNaluStart + 1; + infoOfLastNalu = { + data: data.subarray(firstByteToKeep, len), + fullData: data.subarray(lastNaluStart, len), + type: lastNaluType, + time: time, + }; + if (nextPes) { + data = nextPes.data; + len = data.byteLength; + i = -1; + } + } + } } - if (lastNaluStart >= 0 && numZeros >= 0) { - // The rest of the buffer was a NALU. - // Because the start position includes the type, skip the first byte. - const firstByteToKeep = lastNaluStart + 1; - /** @type {shaka.util.TsParser.AvcNalu} */ - const nalu = { - data: data.subarray(firstByteToKeep, len), - type: lastNaluType, - time: time, - }; - nalus.push(nalu); + if (infoOfLastNalu) { + nalus.push(infoOfLastNalu); } return nalus; } @@ -485,7 +541,7 @@ shaka.util.TsParser = class { * @export */ getMetadata() { - const timescale = shaka.util.TsParser.Timescale_; + const timescale = shaka.util.TsParser.Timescale; const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; const metadata = []; let prevId3Data = new Uint8Array(0); @@ -515,6 +571,66 @@ shaka.util.TsParser = class { return metadata; } + /** + * Return the audio data + * + * @return {!Array.} + * @export + */ + getAudioData() { + const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; + const audio = []; + let prevAudioData = new Uint8Array(0); + // parsePES_() only works if the data begins on a PES boundary. + // Try the last data blob first, and if it doesn't begin on a + // PES boundary, prepend the previous blob and try again. + // This way, a successful parse will always begin and end on + // the correct boundary, and no data will be skipped. + for (let i = this.audioData_.length - 1; i >= 0; i--) { + const data = this.audioData_[i]; + goog.asserts.assert(data, 'We should have a data'); + const audioData = Uint8ArrayUtils.concat(data, prevAudioData); + const pes = this.parsePES_(audioData); + if (pes) { + audio.unshift(pes); + prevAudioData = new Uint8Array(0); + } else { + prevAudioData = audioData; + } + } + return audio; + } + + /** + * Return the audio data + * + * @return {!Array.} + * @export + */ + getVideoData() { + const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; + const video = []; + let prevVideoData = new Uint8Array(0); + // parsePES_() only works if the data begins on a PES boundary. + // Try the last data blob first, and if it doesn't begin on a + // PES boundary, prepend the previous blob and try again. + // This way, a successful parse will always begin and end on + // the correct boundary, and no data will be skipped. + for (let i = this.videoData_.length - 1; i >= 0; i--) { + const data = this.videoData_[i]; + goog.asserts.assert(data, 'We should have a data'); + const videoData = Uint8ArrayUtils.concat(data, prevVideoData); + const pes = this.parsePES_(videoData, /* isVideo= */ true); + if (pes) { + video.unshift(pes); + prevVideoData = new Uint8Array(0); + } else { + prevVideoData = videoData; + } + } + return video; + } + /** * Return the start time for the audio and video * @@ -544,7 +660,8 @@ shaka.util.TsParser = class { /** * Return the video data * - * @return {!Array.} + * @return {!Array.} + * @export */ getVideoNalus() { const Uint8ArrayUtils = shaka.util.Uint8ArrayUtils; @@ -562,7 +679,7 @@ shaka.util.TsParser = class { const pes = this.parsePES_(videoData); if (pes) { if (this.videoCodec_ == 'avc') { - nalus = nalus.concat(this.parseAvcNalus_(pes)); + nalus = nalus.concat(this.parseAvcNalus(pes)); } prevVideoData = new Uint8Array(0); } else { @@ -747,16 +864,16 @@ shaka.util.TsParser = class { /** * @const {number} - * @private + * @export */ -shaka.util.TsParser.PacketLength_ = 188; +shaka.util.TsParser.Timescale = 90000; /** * @const {number} * @private */ -shaka.util.TsParser.Timescale_ = 90000; +shaka.util.TsParser.PacketLength_ = 188; /** @@ -803,35 +920,3 @@ shaka.util.TsParser.PROFILES_WITH_OPTIONAL_SPS_DATA_ = */ shaka.util.TsParser.PMT; - -/** - * @typedef {{ - * data: Uint8Array, - * packetLength: number, - * pts: ?number, - * dts: ?number - * }} - * - * @summary PES. - * @property {Uint8Array} data - * @property {number} packetLength - * @property {?number} pts - * @property {?number} dts - */ -shaka.util.TsParser.PES; - - -/** - * @typedef {{ - * data: !Uint8Array, - * type: number, - * time: ?number - * }} - * - * @summary AvcNalu. - * @property {!Uint8Array} data - * @property {number} type - * @property {?number} time - */ -shaka.util.TsParser.AvcNalu; - diff --git a/shaka-player.uncompiled.js b/shaka-player.uncompiled.js index 29b207e1fc..5c4d72f98f 100644 --- a/shaka-player.uncompiled.js +++ b/shaka-player.uncompiled.js @@ -71,11 +71,14 @@ goog.require('shaka.transmuxer.Ac3Transmuxer'); goog.require('shaka.transmuxer.ADTS'); goog.require('shaka.transmuxer.Ec3'); goog.require('shaka.transmuxer.Ec3Transmuxer'); +goog.require('shaka.transmuxer.H264'); goog.require('shaka.transmuxer.Mp3Transmuxer'); goog.require('shaka.transmuxer.MpegAudio'); +goog.require('shaka.transmuxer.MpegTsTransmuxer'); goog.require('shaka.transmuxer.TransmuxerEngine'); goog.require('shaka.transmuxer.MssTransmuxer'); goog.require('shaka.transmuxer.MuxjsTransmuxer'); +goog.require('shaka.transmuxer.TsTransmuxer'); goog.require('shaka.ui.Controls'); goog.require('shaka.ui.PlayButton'); goog.require('shaka.ui.SettingsMenu'); diff --git a/test/media/drm_engine_unit.js b/test/media/drm_engine_unit.js index a5355eea07..4eafb67c5d 100644 --- a/test/media/drm_engine_unit.js +++ b/test/media/drm_engine_unit.js @@ -717,6 +717,8 @@ describe('DrmEngine', () => { it('maps TS MIME types through the transmuxer', async () => { const originalIsSupported = shaka.transmuxer.TransmuxerEngine.isSupported; + const originalConvertCodecs = + shaka.transmuxer.TransmuxerEngine.convertCodecs; try { // Mock out isSupported on Transmuxer so that we don't have to care @@ -726,6 +728,14 @@ describe('DrmEngine', () => { (mimeType, contentType) => { return mimeType.startsWith('video/mp2t'); }; + shaka.transmuxer.TransmuxerEngine.convertCodecs = + (contentType, mimeType) => { + let newMimeType = mimeType.replace('mp2t', 'mp4'); + if (contentType == 'audio') { + newMimeType = newMimeType.replace('video', 'audio'); + } + return newMimeType; + }; // The default mock for this is so unrealistic, some of our test // conditions would always fail. Make it realistic enough for this @@ -763,6 +773,8 @@ describe('DrmEngine', () => { } finally { // Restore the mock. shaka.transmuxer.TransmuxerEngine.isSupported = originalIsSupported; + shaka.transmuxer.TransmuxerEngine.convertCodecs = + originalConvertCodecs; } }); }); // describe('init') diff --git a/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0000.ts b/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0000.ts new file mode 100644 index 0000000000..e99bdbf1a5 Binary files /dev/null and b/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0000.ts differ diff --git a/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0001.ts b/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0001.ts new file mode 100644 index 0000000000..0d8a7b28d6 Binary files /dev/null and b/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0001.ts differ diff --git a/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0002.ts b/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0002.ts new file mode 100644 index 0000000000..f43a74caaa Binary files /dev/null and b/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0002.ts differ diff --git a/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0003.ts b/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0003.ts new file mode 100644 index 0000000000..51aadb4566 Binary files /dev/null and b/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0003.ts differ diff --git a/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0004.ts b/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0004.ts new file mode 100644 index 0000000000..1480c5d484 Binary files /dev/null and b/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0004.ts differ diff --git a/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0005.ts b/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0005.ts new file mode 100644 index 0000000000..a9efb35dd0 Binary files /dev/null and b/test/test/assets/hls-ts-ac3/ChID_voices_6ch_640kbps_dd_sub_0005.ts differ diff --git a/test/test/assets/hls-ts-ac3/prog_index.m3u8 b/test/test/assets/hls-ts-ac3/prog_index.m3u8 new file mode 100644 index 0000000000..597bc351d5 --- /dev/null +++ b/test/test/assets/hls-ts-ac3/prog_index.m3u8 @@ -0,0 +1,19 @@ +#EXTM3U +#EXT-X-VERSION:4 +#EXT-X-TARGETDURATION:6 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:6.016, +ChID_voices_6ch_640kbps_dd_sub_0000.ts +#EXTINF:5.984, +ChID_voices_6ch_640kbps_dd_sub_0001.ts +#EXTINF:6.016, +ChID_voices_6ch_640kbps_dd_sub_0002.ts +#EXTINF:5.984, +ChID_voices_6ch_640kbps_dd_sub_0003.ts +#EXTINF:6.016, +ChID_voices_6ch_640kbps_dd_sub_0004.ts +#EXTINF:5.984, +ChID_voices_6ch_640kbps_dd_sub_0005.ts +#EXT-X-ENDLIST diff --git a/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0000.ts b/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0000.ts new file mode 100644 index 0000000000..6685bf0144 Binary files /dev/null and b/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0000.ts differ diff --git a/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0001.ts b/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0001.ts new file mode 100644 index 0000000000..1e1b6b30b7 Binary files /dev/null and b/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0001.ts differ diff --git a/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0002.ts b/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0002.ts new file mode 100644 index 0000000000..c1caaeb445 Binary files /dev/null and b/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0002.ts differ diff --git a/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0003.ts b/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0003.ts new file mode 100644 index 0000000000..22db27c184 Binary files /dev/null and b/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0003.ts differ diff --git a/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0004.ts b/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0004.ts new file mode 100644 index 0000000000..12c6f1a674 Binary files /dev/null and b/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0004.ts differ diff --git a/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0005.ts b/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0005.ts new file mode 100644 index 0000000000..7821f8bf27 Binary files /dev/null and b/test/test/assets/hls-ts-ec3/ChID_voices_6ch_256kbps_ddp_sub_0005.ts differ diff --git a/test/test/assets/hls-ts-ec3/prog_index.m3u8 b/test/test/assets/hls-ts-ec3/prog_index.m3u8 new file mode 100644 index 0000000000..22435eeba9 --- /dev/null +++ b/test/test/assets/hls-ts-ec3/prog_index.m3u8 @@ -0,0 +1,19 @@ +#EXTM3U +#EXT-X-VERSION:4 +#EXT-X-TARGETDURATION:6 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:6.016, +ChID_voices_6ch_256kbps_ddp_sub_0000.ts +#EXTINF:5.984, +ChID_voices_6ch_256kbps_ddp_sub_0001.ts +#EXTINF:6.016, +ChID_voices_6ch_256kbps_ddp_sub_0002.ts +#EXTINF:5.984, +ChID_voices_6ch_256kbps_ddp_sub_0003.ts +#EXTINF:6.016, +ChID_voices_6ch_256kbps_ddp_sub_0004.ts +#EXTINF:5.984, +ChID_voices_6ch_256kbps_ddp_sub_0005.ts +#EXT-X-ENDLIST diff --git a/test/test/assets/hls-ts-mp3/manifest.m3u8 b/test/test/assets/hls-ts-mp3/manifest.m3u8 new file mode 100644 index 0000000000..4c8def6b40 --- /dev/null +++ b/test/test/assets/hls-ts-mp3/manifest.m3u8 @@ -0,0 +1,13 @@ +#EXTM3U +#EXT-X-VERSION:3 +#EXT-X-TARGETDURATION:10 +#EXT-X-MEDIA-SEQUENCE:0 +#EXTINF:10.0, +media_w239891843_0.ts +#EXTINF:10.0, +media_w239891843_1.ts +#EXTINF:10.0, +media_w239891843_2.ts +#EXTINF:10.0, +media_w239891843_3.ts +#EXT-X-ENDLIST diff --git a/test/test/assets/hls-ts-mp3/media_w239891843_0.ts b/test/test/assets/hls-ts-mp3/media_w239891843_0.ts new file mode 100644 index 0000000000..a431f4317f Binary files /dev/null and b/test/test/assets/hls-ts-mp3/media_w239891843_0.ts differ diff --git a/test/test/assets/hls-ts-mp3/media_w239891843_1.ts b/test/test/assets/hls-ts-mp3/media_w239891843_1.ts new file mode 100644 index 0000000000..ff804428f4 Binary files /dev/null and b/test/test/assets/hls-ts-mp3/media_w239891843_1.ts differ diff --git a/test/test/assets/hls-ts-mp3/media_w239891843_2.ts b/test/test/assets/hls-ts-mp3/media_w239891843_2.ts new file mode 100644 index 0000000000..e43ee1a19b Binary files /dev/null and b/test/test/assets/hls-ts-mp3/media_w239891843_2.ts differ diff --git a/test/test/assets/hls-ts-mp3/media_w239891843_3.ts b/test/test/assets/hls-ts-mp3/media_w239891843_3.ts new file mode 100644 index 0000000000..54b61848d0 Binary files /dev/null and b/test/test/assets/hls-ts-mp3/media_w239891843_3.ts differ diff --git a/test/transmuxer/transmuxer_engine_integration.js b/test/transmuxer/transmuxer_engine_integration.js index bc0c519cc1..c7d2f89b21 100644 --- a/test/transmuxer/transmuxer_engine_integration.js +++ b/test/transmuxer/transmuxer_engine_integration.js @@ -50,19 +50,6 @@ describe('TransmuxerEngine', () => { expect(convertedAacCodecs).toBe(expectedAacCodecs); }); - it('converts legacy avc1 codec strings', () => { - expect( - convertCodecs( - ContentType.VIDEO, 'video/mp2t; codecs="avc1.100.42"')) - .toBe('video/mp4; codecs="avc1.64002a"'); - expect( - convertCodecs(ContentType.VIDEO, 'video/mp2t; codecs="avc1.77.80"')) - .toBe('video/mp4; codecs="avc1.4d0050"'); - expect( - convertCodecs(ContentType.VIDEO, 'video/mp2t; codecs="avc1.66.1"')) - .toBe('video/mp4; codecs="avc1.420001"'); - }); - // Issue #1991 it('handles upper-case MIME types', () => { expect(convertCodecs( diff --git a/test/transmuxer/transmuxer_integration.js b/test/transmuxer/transmuxer_integration.js index f7e7316c08..71159c0b9b 100644 --- a/test/transmuxer/transmuxer_integration.js +++ b/test/transmuxer/transmuxer_integration.js @@ -131,6 +131,10 @@ describe('Transmuxer Player', () => { if (!MediaSource.isTypeSupported('audio/mp4; codecs="ec-3"')) { return; } + // This tests is flaky in some Tizen devices, so we need omit it for now. + if (shaka.util.Platform.isTizen()) { + return; + } // It seems that AC3 on Edge Windows from github actions is not working // (in the lab AC3 is working). The AC3 detection is currently hard-coded // to true, which leads to a failure in GitHub's environment. @@ -205,4 +209,96 @@ describe('Transmuxer Player', () => { await player.unload(); }); + + it('MP3 in TS', async () => { + if (!MediaSource.isTypeSupported('audio/mp4; codecs="mp3"') && + !MediaSource.isTypeSupported('audio/mpeg')) { + return; + } + // This tests is flaky in some Tizen devices, so we need omit it for now. + if (shaka.util.Platform.isTizen()) { + return; + } + await player.load('/base/test/test/assets/hls-ts-mp3/manifest.m3u8'); + await video.play(); + expect(player.isLive()).toBe(false); + + // Wait for the video to start playback. If it takes longer than 10 + // seconds, fail the test. + await waiter.waitForMovementOrFailOnTimeout(video, 10); + + // Play for 15 seconds, but stop early if the video ends. If it takes + // longer than 45 seconds, fail the test. + await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 15, 45); + + await player.unload(); + }); + + it('AC3 in TS', async () => { + if (!MediaSource.isTypeSupported('audio/mp4; codecs="ac-3"')) { + return; + } + // This tests is flaky in some Tizen devices, so we need omit it for now. + if (shaka.util.Platform.isTizen()) { + return; + } + // It seems that AC3 on Edge Windows from github actions is not working + // (in the lab AC3 is working). The AC3 detection is currently hard-coded + // to true, which leads to a failure in GitHub's environment. + // We must enable this, once it is resolved: + // https://bugs.chromium.org/p/chromium/issues/detail?id=1450313 + const chromeVersion = shaka.util.Platform.chromeVersion(); + if (shaka.util.Platform.isEdge() && + chromeVersion && chromeVersion <= 116) { + return; + } + + await player.load('/base/test/test/assets/hls-ts-ac3/prog_index.m3u8'); + await video.play(); + expect(player.isLive()).toBe(false); + + // Wait for the video to start playback. If it takes longer than 10 + // seconds, fail the test. + await waiter.waitForMovementOrFailOnTimeout(video, 10); + + // Play for 15 seconds, but stop early if the video ends. If it takes + // longer than 45 seconds, fail the test. + await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 15, 45); + + await player.unload(); + }); + + it('EC3 in TS', async () => { + if (!MediaSource.isTypeSupported('audio/mp4; codecs="ec-3"')) { + return; + } + // This tests is flaky in some Tizen devices, so we need omit it for now. + if (shaka.util.Platform.isTizen()) { + return; + } + // It seems that AC3 on Edge Windows from github actions is not working + // (in the lab AC3 is working). The AC3 detection is currently hard-coded + // to true, which leads to a failure in GitHub's environment. + // We must enable this, once it is resolved: + // https://bugs.chromium.org/p/chromium/issues/detail?id=1450313 + const chromeVersion = shaka.util.Platform.chromeVersion(); + if (shaka.util.Platform.isEdge() && + chromeVersion && chromeVersion <= 116) { + return; + } + + await player.load('/base/test/test/assets/hls-ts-ec3/prog_index.m3u8'); + await video.play(); + expect(player.isLive()).toBe(false); + + // Wait for the video to start playback. If it takes longer than 10 + // seconds, fail the test. + await waiter.waitForMovementOrFailOnTimeout(video, 10); + + // Play for 15 seconds, but stop early if the video ends. If it takes + // longer than 45 seconds, fail the test. + await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 15, 45); + + await player.unload(); + }); });