From e48ca4a69000c549ec01042ea8711eb6a78dc219 Mon Sep 17 00:00:00 2001 From: Alvaro Velad Galvan Date: Thu, 24 Nov 2022 12:15:21 +0100 Subject: [PATCH 1/6] fix(cea): Fix not rendering CEA-608 on encrypted mp4 segments --- lib/cea/mp4_cea_parser.js | 32 ++++++++++++++++++++++++++++-- lib/media/closed_caption_parser.js | 2 +- test/cea/mp4_cea_parser_unit.js | 4 ++-- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/lib/cea/mp4_cea_parser.js b/lib/cea/mp4_cea_parser.js index 4869500da7..7d6d4ddb1a 100644 --- a/lib/cea/mp4_cea_parser.js +++ b/lib/cea/mp4_cea_parser.js @@ -20,8 +20,15 @@ goog.require('shaka.util.Mp4BoxParsers'); * @implements {shaka.cea.ICeaParser} */ shaka.cea.Mp4CeaParser = class { - /** */ - constructor() { + /** + * @param {string} mimeType + */ + constructor(mimeType) { + /** + * @private {string} + */ + this.mimeType_ = mimeType; + /** * SEI data processor. * @private @@ -75,6 +82,8 @@ shaka.cea.Mp4CeaParser = class { }; }; + let encrypted = false; + new Mp4Parser() .box('moov', Mp4Parser.children) .box('mvex', Mp4Parser.children) @@ -107,6 +116,10 @@ shaka.cea.Mp4CeaParser = class { .box('stbl', Mp4Parser.children) .fullBox('stsd', Mp4Parser.sampleDescription) + .box('encv', () => { + encrypted = true; + }) + // These are the various boxes that signal a codec. // Use these to set bistreamFormat_. .box('avc1', bitstreamFormatSetter(BitstreamFormat.H264)) @@ -129,6 +142,21 @@ shaka.cea.Mp4CeaParser = class { shaka.util.Error.Code.INVALID_MP4_CEA); } + // If the content is encrypted we have no way of knowing the + // bitstreamformat, so we try to get it from the mimetype. + if (encrypted && this.bitstreamFormat_ == BitstreamFormat.UNKNOWN) { + if (this.mimeType_.includes('avc1.') || + this.mimeType_.includes('avc3.')) { + this.bitstreamFormat_ = BitstreamFormat.H264; + } + if (this.mimeType_.includes('hev1.') || + this.mimeType_.includes('hvc1.') || + this.mimeType_.includes('dvh1.') || + this.mimeType_.includes('dvhe.')) { + this.bitstreamFormat_ = BitstreamFormat.H265; + } + } + if (this.bitstreamFormat_ == BitstreamFormat.UNKNOWN) { shaka.log.alwaysWarn( 'Unable to determine bitstream format for CEA parsing!'); diff --git a/lib/media/closed_caption_parser.js b/lib/media/closed_caption_parser.js index 79e82d5d9e..731828629f 100644 --- a/lib/media/closed_caption_parser.js +++ b/lib/media/closed_caption_parser.js @@ -61,7 +61,7 @@ shaka.media.ClosedCaptionParser = class { if (mimeType.includes('video/mp4')) { // MP4 Parser to extract closed caption packets from H.264 video. - this.ceaParser_ = new shaka.cea.Mp4CeaParser(); + this.ceaParser_ = new shaka.cea.Mp4CeaParser(mimeType); } /** diff --git a/test/cea/mp4_cea_parser_unit.js b/test/cea/mp4_cea_parser_unit.js index 3f93c65bc8..282c7f7a18 100644 --- a/test/cea/mp4_cea_parser_unit.js +++ b/test/cea/mp4_cea_parser_unit.js @@ -50,7 +50,7 @@ describe('Mp4CeaParser', () => { }); it('parses cea data from mp4 stream', () => { - const cea708Parser = new shaka.cea.Mp4CeaParser(); + const cea708Parser = new shaka.cea.Mp4CeaParser('video/mp4'); const expectedCea708Packet = new Uint8Array([ 0xb5, 0x00, 0x31, 0x47, 0x41, 0x39, 0x34, 0x03, @@ -71,7 +71,7 @@ describe('Mp4CeaParser', () => { }); it('parses an invalid init segment', () => { - const cea708Parser = new shaka.cea.Mp4CeaParser(); + const cea708Parser = new shaka.cea.Mp4CeaParser('video/mp4'); const expected = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, From cc8c047d6a086902479a23176f30b4dd986e35cd Mon Sep 17 00:00:00 2001 From: Alvaro Velad Galvan Date: Thu, 1 Dec 2022 12:29:12 +0100 Subject: [PATCH 2/6] fix(cea): Fix not rendering CEA-608 on encrypted mp4 segments --- lib/cea/mp4_cea_parser.js | 43 +++++++++--------------------- lib/media/closed_caption_parser.js | 2 +- test/cea/mp4_cea_parser_unit.js | 4 +-- 3 files changed, 16 insertions(+), 33 deletions(-) diff --git a/lib/cea/mp4_cea_parser.js b/lib/cea/mp4_cea_parser.js index 7d6d4ddb1a..813a31e2b7 100644 --- a/lib/cea/mp4_cea_parser.js +++ b/lib/cea/mp4_cea_parser.js @@ -20,15 +20,8 @@ goog.require('shaka.util.Mp4BoxParsers'); * @implements {shaka.cea.ICeaParser} */ shaka.cea.Mp4CeaParser = class { - /** - * @param {string} mimeType - */ - constructor(mimeType) { - /** - * @private {string} - */ - this.mimeType_ = mimeType; - + /** */ + constructor() { /** * SEI data processor. * @private @@ -82,8 +75,6 @@ shaka.cea.Mp4CeaParser = class { }; }; - let encrypted = false; - new Mp4Parser() .box('moov', Mp4Parser.children) .box('mvex', Mp4Parser.children) @@ -116,10 +107,6 @@ shaka.cea.Mp4CeaParser = class { .box('stbl', Mp4Parser.children) .fullBox('stsd', Mp4Parser.sampleDescription) - .box('encv', () => { - encrypted = true; - }) - // These are the various boxes that signal a codec. // Use these to set bistreamFormat_. .box('avc1', bitstreamFormatSetter(BitstreamFormat.H264)) @@ -130,6 +117,17 @@ shaka.cea.Mp4CeaParser = class { .box('dvh1', bitstreamFormatSetter(BitstreamFormat.H265)) .box('dvhe', bitstreamFormatSetter(BitstreamFormat.H265)) + .box('encv', Mp4Parser.children) + // These are the various boxes that signal a codec in + // encrypted segments + // Use these to set bistreamFormat_. + .box('avcC', bitstreamFormatSetter(BitstreamFormat.H264)) + .box('hvcE', bitstreamFormatSetter(BitstreamFormat.H265)) + .box('hvcC', bitstreamFormatSetter(BitstreamFormat.H265)) + // Dobly vision is also H265. + .box('dvcC', bitstreamFormatSetter(BitstreamFormat.H265)) + .box('dvvC', bitstreamFormatSetter(BitstreamFormat.H265)) + .parse(initSegment, /* partialOkay= */ true); // At least one track should exist, and each track should have a @@ -142,21 +140,6 @@ shaka.cea.Mp4CeaParser = class { shaka.util.Error.Code.INVALID_MP4_CEA); } - // If the content is encrypted we have no way of knowing the - // bitstreamformat, so we try to get it from the mimetype. - if (encrypted && this.bitstreamFormat_ == BitstreamFormat.UNKNOWN) { - if (this.mimeType_.includes('avc1.') || - this.mimeType_.includes('avc3.')) { - this.bitstreamFormat_ = BitstreamFormat.H264; - } - if (this.mimeType_.includes('hev1.') || - this.mimeType_.includes('hvc1.') || - this.mimeType_.includes('dvh1.') || - this.mimeType_.includes('dvhe.')) { - this.bitstreamFormat_ = BitstreamFormat.H265; - } - } - if (this.bitstreamFormat_ == BitstreamFormat.UNKNOWN) { shaka.log.alwaysWarn( 'Unable to determine bitstream format for CEA parsing!'); diff --git a/lib/media/closed_caption_parser.js b/lib/media/closed_caption_parser.js index 29d5cc2802..a657bb2d92 100644 --- a/lib/media/closed_caption_parser.js +++ b/lib/media/closed_caption_parser.js @@ -62,7 +62,7 @@ shaka.media.ClosedCaptionParser = class { if (mimeType.toLowerCase().includes('video/mp4')) { // MP4 Parser to extract closed caption packets from H.264/H.265 video. - this.ceaParser_ = new shaka.cea.Mp4CeaParser(mimeType); + this.ceaParser_ = new shaka.cea.Mp4CeaParser(); } if (mimeType.toLowerCase().includes('video/mp2t')) { // TS Parser to extract closed caption packets from H.264 video. diff --git a/test/cea/mp4_cea_parser_unit.js b/test/cea/mp4_cea_parser_unit.js index 282c7f7a18..3f93c65bc8 100644 --- a/test/cea/mp4_cea_parser_unit.js +++ b/test/cea/mp4_cea_parser_unit.js @@ -50,7 +50,7 @@ describe('Mp4CeaParser', () => { }); it('parses cea data from mp4 stream', () => { - const cea708Parser = new shaka.cea.Mp4CeaParser('video/mp4'); + const cea708Parser = new shaka.cea.Mp4CeaParser(); const expectedCea708Packet = new Uint8Array([ 0xb5, 0x00, 0x31, 0x47, 0x41, 0x39, 0x34, 0x03, @@ -71,7 +71,7 @@ describe('Mp4CeaParser', () => { }); it('parses an invalid init segment', () => { - const cea708Parser = new shaka.cea.Mp4CeaParser('video/mp4'); + const cea708Parser = new shaka.cea.Mp4CeaParser(); const expected = Util.jasmineError(new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, From b4f0441f131576251b4e0227f4d5d7805e989ebf Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Thu, 1 Dec 2022 11:19:24 -0800 Subject: [PATCH 3/6] Parse encv, sinf, and frma boxes to get encrypted bitstream format --- lib/cea/mp4_cea_parser.js | 65 ++++++++++++++++++++++--------------- lib/util/mp4_box_parsers.js | 23 +++++++++++++ lib/util/mp4_parser.js | 34 +++++++++++++++++++ 3 files changed, 95 insertions(+), 27 deletions(-) diff --git a/lib/cea/mp4_cea_parser.js b/lib/cea/mp4_cea_parser.js index 813a31e2b7..8f519bcc4a 100644 --- a/lib/cea/mp4_cea_parser.js +++ b/lib/cea/mp4_cea_parser.js @@ -66,14 +66,7 @@ shaka.cea.Mp4CeaParser = class { const trackIds = []; const timescales = []; - // This generates a box-parser callback that simply sets a particular - // bitstream format. It makes the list of codec-signalling boxes below - // more compact and readable. - const bitstreamFormatSetter = (format) => { - return (box) => { - this.bitstreamFormat_ = format; - }; - }; + const codecBoxParser = (box) => this.setBitstreamFormat_(box.type); new Mp4Parser() .box('moov', Mp4Parser.children) @@ -108,25 +101,22 @@ shaka.cea.Mp4CeaParser = class { .fullBox('stsd', Mp4Parser.sampleDescription) // These are the various boxes that signal a codec. - // Use these to set bistreamFormat_. - .box('avc1', bitstreamFormatSetter(BitstreamFormat.H264)) - .box('avc3', bitstreamFormatSetter(BitstreamFormat.H264)) - .box('hev1', bitstreamFormatSetter(BitstreamFormat.H265)) - .box('hvc1', bitstreamFormatSetter(BitstreamFormat.H265)) - // Dobly vision is also H265. - .box('dvh1', bitstreamFormatSetter(BitstreamFormat.H265)) - .box('dvhe', bitstreamFormatSetter(BitstreamFormat.H265)) - - .box('encv', Mp4Parser.children) - // These are the various boxes that signal a codec in - // encrypted segments - // Use these to set bistreamFormat_. - .box('avcC', bitstreamFormatSetter(BitstreamFormat.H264)) - .box('hvcE', bitstreamFormatSetter(BitstreamFormat.H265)) - .box('hvcC', bitstreamFormatSetter(BitstreamFormat.H265)) - // Dobly vision is also H265. - .box('dvcC', bitstreamFormatSetter(BitstreamFormat.H265)) - .box('dvvC', bitstreamFormatSetter(BitstreamFormat.H265)) + .box('avc1', codecBoxParser) + .box('avc3', codecBoxParser) + .box('hev1', codecBoxParser) + .box('hvc1', codecBoxParser) + .box('dvh1', codecBoxParser) + .box('dvhe', codecBoxParser) + + // This signals an encrypted sample, which we can go inside of to find + // the codec used. + .box('encv', Mp4Parser.visualSampleEntry) + .box('sinf', Mp4Parser.children) + .box('frma', (box) => { + const {codec} = shaka.util.Mp4BoxParsers.parseFRMA( + box.reader); + this.setBitstreamFormat_(codec); + }) .parse(initSegment, /* partialOkay= */ true); @@ -349,6 +339,16 @@ shaka.cea.Mp4CeaParser = class { } } } + + /** + * @param {string} codec A fourcc for a codec. + * @private + */ + setBitstreamFormat_(codec) { + if (codec in shaka.cea.Mp4CeaParser.CodecBitstreamMap_) { + this.bitstreamFormat_ = shaka.cea.Mp4CeaParser.CodecBitstreamMap_[codec]; + } + } }; /** @enum {number} */ @@ -357,3 +357,14 @@ shaka.cea.Mp4CeaParser.BitstreamFormat = { H264: 1, H265: 2, }; + +/** @private {Object.} */ +shaka.cea.Mp4CeaParser.CodecBitstreamMap_ = { + 'avc1': shaka.cea.Mp4CeaParser.BitstreamFormat.H264, + 'avc3': shaka.cea.Mp4CeaParser.BitstreamFormat.H264, + 'hev1': shaka.cea.Mp4CeaParser.BitstreamFormat.H265, + 'hvc1': shaka.cea.Mp4CeaParser.BitstreamFormat.H265, + // Dobly vision is also H265. + 'dvh1': shaka.cea.Mp4CeaParser.BitstreamFormat.H265, + 'dvhe': shaka.cea.Mp4CeaParser.BitstreamFormat.H265, +}; diff --git a/lib/util/mp4_box_parsers.js b/lib/util/mp4_box_parsers.js index 204393c129..e94da13fe4 100644 --- a/lib/util/mp4_box_parsers.js +++ b/lib/util/mp4_box_parsers.js @@ -188,6 +188,17 @@ shaka.util.Mp4BoxParsers = class { trackId, }; } + + /** + * Parses a FRMA box. + * @param {!shaka.util.DataViewReader} reader + * @return {!shaka.util.ParsedFRMABox} + */ + static parseFRMA(reader) { + const fourcc = reader.readUint32(); + const codec = shaka.util.Mp4Parser.typeToString(fourcc); + return {codec}; + } }; @@ -306,3 +317,15 @@ shaka.util.ParsedTRUNSample; * @exportDoc */ shaka.util.ParsedTKHDBox; + +/** + * @typedef {{ + * codec: string + * }} + * + * @property {string} codec + * A fourcc for a codec + * + * @exportDoc + */ +shaka.util.ParsedFRMABox; diff --git a/lib/util/mp4_parser.js b/lib/util/mp4_parser.js index 7d0ebf0818..089278863f 100644 --- a/lib/util/mp4_parser.js +++ b/lib/util/mp4_parser.js @@ -236,6 +236,40 @@ shaka.util.Mp4Parser = class { } + /** + * A callback that tells the Mp4 parser to treat the body of a box as a visual + * sample entry. A visual sample entry has some fixed-sized fields + * describing the video codec parameters, followed by an arbitrary number of + * appended children. Each child is a box. + * + * @param {!shaka.extern.ParsedBox} box + * @export + */ + static visualSampleEntry(box) { + // The "reader" starts at the payload, so we need to add the header to the + // start position. The header size varies. + const headerSize = shaka.util.Mp4Parser.headerSize(box); + + // Skip 6 reserved bytes. + // Skip 2-byte data reference index. + // Skip 16 more reserved bytes. + // Skip 4 bytes for width/height. + // Skip 8 bytes for horizontal/vertical resolution. + // Skip 4 more reserved bytes (0) + // Skip 2-byte frame count. + // Skip 32-byte compressor name (length byte, then name, then 0-padding). + // Skip 2-byte depth. + // Skip 2 more reserved bytes (0xff) + // 78 bytes total. + // See also https://github.com/shaka-project/shaka-packager/blob/d5ca6e84/packager/media/formats/mp4/box_definitions.cc#L1544 + box.reader.skip(78); + + while (box.reader.hasMoreData() && !box.parser.done_) { + box.parser.parseNext(box.start + headerSize, box.reader, box.partialOkay); + } + } + + /** * Create a callback that tells the Mp4 parser to treat the body of a box as a * binary blob and to parse the body's contents using the provided callback. From d86793eb9489d8e82556d0bb2b1f00ec38640fd6 Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Thu, 1 Dec 2022 11:20:37 -0800 Subject: [PATCH 4/6] style nit --- lib/cea/mp4_cea_parser.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/cea/mp4_cea_parser.js b/lib/cea/mp4_cea_parser.js index 8f519bcc4a..96e82cdc6d 100644 --- a/lib/cea/mp4_cea_parser.js +++ b/lib/cea/mp4_cea_parser.js @@ -113,8 +113,7 @@ shaka.cea.Mp4CeaParser = class { .box('encv', Mp4Parser.visualSampleEntry) .box('sinf', Mp4Parser.children) .box('frma', (box) => { - const {codec} = shaka.util.Mp4BoxParsers.parseFRMA( - box.reader); + const {codec} = shaka.util.Mp4BoxParsers.parseFRMA(box.reader); this.setBitstreamFormat_(codec); }) From 7e3edce2e15c3c72abeeab14b91d177cfae563e9 Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Thu, 1 Dec 2022 11:32:06 -0800 Subject: [PATCH 5/6] Add missing require --- lib/util/mp4_box_parsers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/util/mp4_box_parsers.js b/lib/util/mp4_box_parsers.js index e94da13fe4..6fc41db21f 100644 --- a/lib/util/mp4_box_parsers.js +++ b/lib/util/mp4_box_parsers.js @@ -7,6 +7,7 @@ goog.provide('shaka.util.Mp4BoxParsers'); goog.require('shaka.util.DataViewReader'); +goog.require('shaka.util.Mp4Parser'); shaka.util.Mp4BoxParsers = class { /** From 93b6075e77c1ce5f267529029917e1bb9c94660a Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Thu, 1 Dec 2022 12:31:52 -0800 Subject: [PATCH 6/6] Fix box name extraction --- externs/shaka/mp4_parser.js | 3 +++ lib/cea/mp4_cea_parser.js | 2 +- lib/util/mp4_parser.js | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/externs/shaka/mp4_parser.js b/externs/shaka/mp4_parser.js index 3545bcdf18..ecc300b899 100644 --- a/externs/shaka/mp4_parser.js +++ b/externs/shaka/mp4_parser.js @@ -11,6 +11,7 @@ /** * @typedef {{ + * name: string, * parser: !shaka.util.Mp4Parser, * partialOkay: boolean, * start: number, @@ -21,6 +22,8 @@ * has64BitSize: boolean * }} * + * @property {string} name + * The box name, a 4-character string (fourcc). * @property {!shaka.util.Mp4Parser} parser * The parser that parsed this box. The parser can be used to parse child * boxes where the configuration of the current parser is needed to parsed diff --git a/lib/cea/mp4_cea_parser.js b/lib/cea/mp4_cea_parser.js index 96e82cdc6d..553407813a 100644 --- a/lib/cea/mp4_cea_parser.js +++ b/lib/cea/mp4_cea_parser.js @@ -66,7 +66,7 @@ shaka.cea.Mp4CeaParser = class { const trackIds = []; const timescales = []; - const codecBoxParser = (box) => this.setBitstreamFormat_(box.type); + const codecBoxParser = (box) => this.setBitstreamFormat_(box.name); new Mp4Parser() .box('moov', Mp4Parser.children) diff --git a/lib/util/mp4_parser.js b/lib/util/mp4_parser.js index 089278863f..b21221de3b 100644 --- a/lib/util/mp4_parser.js +++ b/lib/util/mp4_parser.js @@ -172,6 +172,7 @@ shaka.util.Mp4Parser = class { /** @type {shaka.extern.ParsedBox} */ const box = { + name, parser: this, partialOkay: partialOkay || false, version,