diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..07e6e47 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..798adba --- /dev/null +++ b/LICENSE @@ -0,0 +1,10 @@ +Copyright (c) 2014, CableLabs, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ba518e --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# MPEG-TS Sections in JavaScript + +Author: Brendan Long + +This library decodes [MPEG-TS program-specific information][mpegts-psi] into JSON. It's intended to be used with [HTML5 DataCues][datacue], but will work in any case where you have an `ArrayBuffer`. + +## Using this library + +To use, copy mpegtssections.js into your application's script directory, then add something like this to the `` of your HTML page(s): + + + +You can then use `MpegTs` methods in your JavaScript code. + +## API Documentation + +### Data Structures + +The following uses [Web IDL][webidl] syntax to describe the resulting data structures. The attribute names are taken directly from the MPEG-TS spec. + +#### Generic MPEG-TS Section + + interface MpegTsSyntaxSection { + attribute unsigned short table_id_extension; + attribute octet version_number; + attribute boolean current_next_indicator; + attribute octet section_number; + attribute octet last_section_number; + attribute unsigned long crc32; + } + + interface MpegTsSection { + attribute octet table_id; + attribute unsigned short section_length; + attribute MpegTsSyntaxSection? syntax_section; + } + + interface MpegTsDescriptor { + attribute octet tag; + attribute octet length; // in bytes + attribute ArrayBuffer data; + } + +#### Program Association Section + +See Table 2-25 - Program association section + + interface MpegTsPatProgramInfo { + attribute unsigned short program_number; + + attribute unsigned short? network_PID; // if program_number == 0 + attribute unsigned short? program_map_PID; // if program_number != 0 + } + + interface MpegTsPat implements MpegTsSection { + attribute unsigned short transport_stream_id; + attribute MpegTsPatProgramInfo[] program_info; + } + +#### Conditional Access Table + +See Table 2-27 - Conditional access section. + + interface MpegTsCat implements MpegTsSection { + attribute MpegTsDescriptor[] descriptors; + } + +#### Program Map Table + +See Table 2-28 - Transport Stream program map section. + + interface MpegTsElementaryStream { + attribute octet stream_type; + attribute unsigned short elementary_PID; + attribute unsigned short ES_info_length; + attribute MpegTsDescriptor[] descriptors; + } + + interface MpegTsPmt implements MpegTsSection { + attribute unsigned short? PCR_PID; // 8191 maps to null + attribute octet program_info_length; + attribute MpegTsDescriptor[] descriptors; + attribute MpegTsElementaryStreamData[] stream_info; + } + +#### Private Section + +See Table 2-30 - Private Section + + interface MpegTsPrivateSection implements MpegTsSection { + attribute boolean private_indicator; + attribute unsigned short private_section_length; + attribute MpegTsSyntaxSection? syntax_section; + ArrayBuffer private_data; + } + +#### Transport Stream Description + +See Table 2-30-1 - The Transport Stream Description Table + + interface MpegTsDescriptionSection implements MpegTsSection { + attribute MpegTsDescriptor[] descriptors; + } + +#### Functions + +`String MpegTs.decodeTable(ArrayBuffer buf, boolean checkReservedBits)` + +If `buf` is a PSI table (starting with the `table_id`), it will be decoded into the most appropriate type, using the following algorithm: + + 1. If there are any serious problems with the data (lengths are wrong), throw an exception. + 2. If the `table_id` is 0, return an `MpegTsPat`. + 3. If the `table_id` is 1, return an `MpegTsCat`. + 4. If the `table_id` is 2, return an `MpegTsPmt`. + 5. If the `table_id` is 3, return an `MpegTsDescriptionSection`. + 6. If the `table_id` is >= 128, return an `MpegTsPrivateSection`. + 7. If the `sectionSyntaxIndicator` is `true`, return an `MpegTsTableWithSyntaxSection`. + 8. Return an `MpegTsTable`. + +[datacue]: http://www.w3.org/html/wg/drafts/html/CR/embedded-content-0.html#datacue +[mpegts-psi]: http://en.wikipedia.org/wiki/Program-specific_information +[webidl]: http://www.w3.org/TR/WebIDL/ diff --git a/lib/mpegtssections.js b/lib/mpegtssections.js new file mode 100644 index 0000000..8639c36 --- /dev/null +++ b/lib/mpegtssections.js @@ -0,0 +1,149 @@ +/* Copyright (c) 2014, CableLabs, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +/*jslint browser: true, node: true, bitwise: true, plusplus: true, vars: true, + indent: 4, maxlen: 80 */ +(function (exports) { + "use strict"; + + /* Constants */ + var TableIds = { + PROGRAM_ASSOCIATION_SECTION: 0, + CONDITIONAL_ACCESS_SECTION: 1, + TS_PROGRAM_MAP_SECTION: 2, + TS_DESCRIPTION_SECTION: 3, + ISO_IEC_14496_SCENE_DESCRIPTION_SECTION: 4, + ISO_IEC_14496_OBJECT_DESCRIPTION_SECTION: 5 + }; + + var ReservedPids = { + PROGRAM_ASSOCIATION: 0, + CONDITIONAL_ACCESS: 1, + TRANSPORT_STREAM_DESCRIPTION: 2 + }; + + /* Errors */ + function defineError(name, defaultMessage) { + function NewError(message) { + this.name = name; + this.message = message; + } + NewError.prototype = new Error(); + NewError.prototype.constructor = NewError; + return NewError; + } + var BadSizeError = defineError("BadSizeError"); + var MissingSyntaxSectionError = defineError("MissingSyntaxSectionError"); + + /* Functions */ + function decodeSection(buf) { + if (!buf || !(buf instanceof ArrayBuffer)) { + throw new TypeError("Expected an ArrayBuffer as the first " + + "argument but got " + typeof buf + ": " + buf); + } + + if (buf.byteLength < 3) { + throw new BadSizeError("MPEG-TS sections must be at least 3 " + + "bytes long, but got buffer with length " + buf.byteLength); + } + + var view = new DataView(buf); + // check CRC32? + + var section = { + table_id: view.getUint8(0), + section_length: view.getUint16(1) & 0xFFF + }; + + // if section_syntax_indicator is set, parse the syntax section + if ((view.getUint8(1) & 0x80) >> 7) { + if (buf.byteLength < 7) { + throw new BadSizeError("section_syntax_indicator is 1, but " + + "the buffer is not long enough to contain a valid " + + "syntax section"); + } + + section.syntax_section = { + table_id_extension: view.getUint16(3), + version_number: (view.getUint8(5) & 0x3E) >> 1, + current_next_indicator: view.getUint8(5) & 1, + section_number: view.getUint8(6), + last_section_number: view.getUint8(7), + CRC: view.getUint32(buf.byteLength - 4) // check this + }; + } else { + section.syntax_section = null; + } + + switch (section.table_id) { + case TableIds.PROGRAM_ASSOCIATION_SECTION: + if (!section.syntax_section) { + throw new MissingSyntaxSectionError("Program access section " + + "requires a syntax section, but " + + "section_syntax_indicator is 0"); + } + section.transport_stream_id = view.getUint16(3); + // program info + break; + case TableIds.CONDITIONAL_ACCESS_SECTION: + case TableIds.TS_DESCRIPTION_SECTION: + // descriptors + break; + case TableIds.TS_PROGRAM_MAP_SECTION: + section.program_number = view.getUint16(3); + section.PCR_PID = view.getUint16(8) & 0x1FFF; + section.program_info_length = view.getUint16(10) & 0xFFF; + section.streams = []; + + var stream_start = 12 + section.program_info_length; + var streams_end = buf.byteLength - 4 - 5; + var i = 0; + while (stream_start <= streams_end) { + section.streams[i] = { + stream_type: view.getUint8(stream_start), + elementary_PID: view.getUint16(stream_start + 1) & 0x1FFF, + ES_info_length: view.getUint16(stream_start + 3) & 0xFFF + }; + stream_start += 5 + section.streams[i].ES_info_length; + ++i; + } + break; + default: + if (section.table_id >= 128) { + // private data + } + break; + } + return section; + } + + exports.TableIds = TableIds; + exports.ReservedPids = ReservedPids; + + exports.BadSizeError = BadSizeError; + exports.MissingSyntaxSectionError = MissingSyntaxSectionError; + + exports.decodeSection = decodeSection; +}(typeof exports === 'undefined' ? this.MpegTs = {} : exports)); diff --git a/package.json b/package.json new file mode 100644 index 0000000..59d46cc --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "author": { + "name": "Brendan Long", + "email": "self@brendanlong.com", + "url": "https://www.brendanlong.com" + }, + "bugs": "https://github.com/cablelabs/mpegtssections-js/issues", + "devDependencies": { + "jshint": "2.5.x", + "nodeunit": "0.8.x" + }, + "homepage": "https://github.com/cablelabs/mpegtssections-js", + "license": "BSD-2-Clause", + "main": "./lib/mpegtssections", + "name": "mpegtssections", + "repository": { + "type": "git", + "url": "https://github.com/cablelabs/mpegtssections-js.git" + }, + "scripts": { + "hint": "./node_modules/.bin/jshint --show-non-errors lib/mpegtssections.js test/tests.js", + "test": "./node_modules/.bin/nodeunit test" + }, + "version": "1.0.0" +} diff --git a/test/tests.js b/test/tests.js new file mode 100644 index 0000000..b0c9722 --- /dev/null +++ b/test/tests.js @@ -0,0 +1,64 @@ +/* Copyright (c) 2014, CableLabs, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +/*jslint browser: true, node: true, plusplus: true, vars: true, indent: 4 */ +"use strict"; +var MpegTs = require('../lib/mpegtssections'); + +exports.TestBufArgumentNull = function(test) { + test.throws(function() { + MpegTs.decodeSection(null); + }, TypeError); + test.done(); +}; + +exports.TestBufArgumentUndefined = function(test) { + test.throws(function() { + MpegTs.decodeSection(); + }, TypeError); + test.done(); +}; + +exports.testBufArgumentWrongType = function(test) { + test.throws(function() { + MpegTs.decodeSection([5]); + }, TypeError); + test.done(); +}; + +exports.testBufArgumentTooSmall = function(test) { + test.throws(function() { + var data = new Uint8Array([1, 2]).buffer; + MpegTs.decodeSection(data); + }, MpegTs.BadSizeError); + test.done(); +}; + +exports.TestUserPrivateData = function(test) { + var data = new Uint8Array([227, 64, 136, 251, 251, 0, 59, 176, 126, 0, 1, 193, 0, 0, 17, 3, 16, 2, 128, 0, 0, 1, 255, 0, 0, 105, 0, 0, 0, 1, 3, 216, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 2, 12, 1, 60, 59, 67, 97, 98, 108, 101, 108, 97, 98, 115, 95, 78, 97, 116, 105, 111, 110, 97, 108, 95, 101, 116, 118, 95, 115, 116, 114, 101, 97, 109, 95, 99, 111, 110, 102, 105, 103, 47, 109, 97, 105, 110, 97, 112, 112, 47, 49, 46, 48, 47, 109, 97, 105, 110, 95, 112, 114, 46, 112, 114, 0, 15, 14, 105, 98, 46, 116, 118, 119, 111, 114, 107, 115, 46, 99, 111, 109, 225, 54, 136, 221, 188, 252, 142, 137]).buffer; + var section = MpegTs.decodeSection(data); + test.ok(section, "Section should not be null"); + test.done(); +};