From 1b10dee06ca3acad45bf34a4933079318b100b0e Mon Sep 17 00:00:00 2001 From: jrivera Date: Mon, 8 Aug 2016 20:02:11 -0400 Subject: [PATCH] First code-baring commit for thumbcoil! --- .editorconfig | 13 + .gitignore | 56 +- .npmignore | 3 + .travis.yml | 13 + CHANGELOG.md | 8 + LICENSE | 13 + README.md | 26 + debug/css/main.css | 385 ++++++ debug/css/normalize.css | 527 ++++++++ debug/css/normalize.min.css | 1 + debug/index.html | 152 +++ index.html | 26 + package.json | 97 ++ scripts/banner.ejs | 6 + scripts/build-test.js | 15 + scripts/postversion.js | 33 + scripts/server.js | 140 +++ scripts/version.js | 69 + src/bit-streams/h264/access-unit-delimiter.js | 12 + src/bit-streams/h264/hdr-parameters.js | 29 + src/bit-streams/h264/index.js | 15 + src/bit-streams/h264/lib/combinators.js | 168 +++ src/bit-streams/h264/lib/conditionals.js | 135 ++ src/bit-streams/h264/lib/data-types.js | 86 ++ .../h264/lib/discard-emulation-prevention.js | 44 + src/bit-streams/h264/lib/exp-golomb-string.js | 102 ++ src/bit-streams/h264/lib/rbsp-utils.js | 43 + src/bit-streams/h264/pic-parameter-set.js | 85 ++ src/bit-streams/h264/scaling-list.js | 63 + src/bit-streams/h264/seq-parameter-set.js | 110 ++ src/bit-streams/h264/slice-header.js | 264 ++++ .../h264/slice-layer-without-partitioning.js | 13 + src/bit-streams/h264/vui-parameters.js | 188 +++ src/index.js | 13 + src/inspectors/index.js | 9 + src/inspectors/mp4.js | 1112 +++++++++++++++++ 36 files changed, 4045 insertions(+), 29 deletions(-) create mode 100644 .editorconfig create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 debug/css/main.css create mode 100644 debug/css/normalize.css create mode 100644 debug/css/normalize.min.css create mode 100644 debug/index.html create mode 100644 index.html create mode 100644 package.json create mode 100644 scripts/banner.ejs create mode 100644 scripts/build-test.js create mode 100644 scripts/postversion.js create mode 100644 scripts/server.js create mode 100644 scripts/version.js create mode 100644 src/bit-streams/h264/access-unit-delimiter.js create mode 100644 src/bit-streams/h264/hdr-parameters.js create mode 100644 src/bit-streams/h264/index.js create mode 100644 src/bit-streams/h264/lib/combinators.js create mode 100644 src/bit-streams/h264/lib/conditionals.js create mode 100644 src/bit-streams/h264/lib/data-types.js create mode 100644 src/bit-streams/h264/lib/discard-emulation-prevention.js create mode 100644 src/bit-streams/h264/lib/exp-golomb-string.js create mode 100644 src/bit-streams/h264/lib/rbsp-utils.js create mode 100644 src/bit-streams/h264/pic-parameter-set.js create mode 100644 src/bit-streams/h264/scaling-list.js create mode 100644 src/bit-streams/h264/seq-parameter-set.js create mode 100644 src/bit-streams/h264/slice-header.js create mode 100644 src/bit-streams/h264/slice-layer-without-partitioning.js create mode 100644 src/bit-streams/h264/vui-parameters.js create mode 100644 src/index.js create mode 100644 src/inspectors/index.js create mode 100644 src/inspectors/mp4.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fc6f5de --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore index 5148e52..651c316 100644 --- a/.gitignore +++ b/.gitignore @@ -1,37 +1,35 @@ +# OS +Thumbs.db +ehthumbs.db +Desktop.ini +.DS_Store +._* + +# Editors +*~ +*.swp +*.tmproj +*.tmproject +*.sublime-* +.idea/ +.project/ +.settings/ +.vscode/ + # Logs logs *.log npm-debug.log* -# Runtime data -pids -*.pid -*.seed - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - # Dependency directories -node_modules -jspm_packages +bower_components/ +node_modules/ -# Optional npm cache directory -.npm +# Yeoman meta-data +.yo-rc.json -# Optional REPL history -.node_repl_history +# Build-related directories +dist/ +docs/api/ +es5/ +test/dist/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..ea8c75d --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +# Intentionally left blank, so that npm does not ignore anything by default, +# but relies on the package.json "files" array to explicitly define what ends +# up in the package. diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5ef4768 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +sudo: false +language: node_js +node_js: + - 'node' + - '4.2' + - '0.12' + - '0.10' + +before_script: + + # Set up a virtual screen for Firefox. + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c4e4551 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +CHANGELOG +========= + +## HEAD (Unreleased) +_(none)_ + +-------------------- + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e4e41de --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2016 Brightcove, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e2b06b2 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Thumbcoil + +Tools for inspecting MPEG2TS and MP4 files and the codec bitstreams therein + +## Table of Contents + + + +## Installation + +- [Installation](#installation) +- [License](#license) + + +## Installation + +```sh +npm install --save thumbcoil +``` + +## License + +Apache-2.0. Copyright (c) Jon-Carlos Rivera + + +[videojs]: http://videojs.com/ diff --git a/debug/css/main.css b/debug/css/main.css new file mode 100644 index 0000000..9389084 --- /dev/null +++ b/debug/css/main.css @@ -0,0 +1,385 @@ +/* ========================================================================== + HTML5 Boilerplate styles - h5bp.com (generated via initializr.com) + ========================================================================== */ + +html, +button, +input, +select, +textarea { + color: #222; +} + +body { + font-size: 1em; + line-height: 1.4; +} + +::-moz-selection { + background: #b3d4fc; + text-shadow: none; +} + +::selection { + background: #b3d4fc; + text-shadow: none; +} + +hr { + display: block; + height: 1px; + border: 0; + border-top: 1px solid #ccc; + margin: 1em 0; + padding: 0; +} + +img { + vertical-align: middle; +} + +fieldset { + border: 0; + margin: 0; + padding: 0; +} + +textarea { + resize: vertical; +} + +.chromeframe { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0; +} + + +/* ===== Initializr Styles ================================================== + Author: Jonathan Verrecchia - verekia.com/initializr/responsive-template + ========================================================================== */ + +body { + font: 16px/26px Helvetica, Helvetica Neue, Arial; +} + +.wrapper { + width: 90%; + margin: 0 5%; +} + +/* =================== + ALL: Orange Theme + =================== */ + +.header-container { + border-bottom: 20px solid #e44d26; +} + +.footer-container, +.main aside { + border-top: 20px solid #e44d26; +} + +.header-container, +.footer-container, +.main aside { + background: #f16529; +} + +.title { + color: white; +} + +/* ============== + MOBILE: Menu + ============== */ + +nav ul { + margin: 0; + padding: 0; +} + +nav a { + display: block; + margin-bottom: 10px; + padding: 15px 0; + + text-align: center; + text-decoration: none; + font-weight: bold; + + color: white; + background: #e44d26; +} + +nav a:hover, +nav a:visited { + color: white; +} + +nav a:hover { + text-decoration: underline; +} + +/* ============== + MOBILE: Main + ============== */ + +.main { + padding: 30px 0; +} + +.main article h1 { + font-size: 2em; +} + +.main aside { + color: white; + padding: 0px 5% 10px; +} + +.footer-container footer { + color: white; + padding: 20px 0; +} + +/* =============== + ALL: IE Fixes + =============== */ + +.ie7 .title { + padding-top: 20px; +} + +/* ========================================================================== + Author's custom styles + ========================================================================== */ + +section { + clear: both; +} +fieldset { + margin: 4px; +} + +/* ========================================================================== + Media Queries + ========================================================================== */ + +@media only screen and (min-width: 480px) { + +/* ==================== + INTERMEDIATE: Menu + ==================== */ + + nav a { + float: left; + width: 27%; + margin: 0 1.7%; + padding: 25px 2%; + margin-bottom: 0; + } + + nav li:first-child a { + margin-left: 0; + } + + nav li:last-child a { + margin-right: 0; + } + +/* ======================== + INTERMEDIATE: IE Fixes + ======================== */ + + nav ul li { + display: inline; + } + + .oldie nav a { + margin: 0 0.7%; + } +} + +@media only screen and (min-width: 768px) { + +/* ==================== + WIDE: CSS3 Effects + ==================== */ + + .header-container, + .main aside { + -webkit-box-shadow: 0 5px 10px #aaa; + -moz-box-shadow: 0 5px 10px #aaa; + box-shadow: 0 5px 10px #aaa; + } + +/* ============ + WIDE: Menu + ============ */ + + .title { + float: left; + } + + nav { + float: right; + width: 38%; + } + +/* ============ + WIDE: Main + ============ */ + + .main article { + float: left; + width: 100%; + } +} + +@media only screen and (min-width: 1140px) { + +/* =============== + Maximal Width + =============== */ + + .wrapper { + width: 1026px; /* 1140px - 10% for margins */ + margin: 0 auto; + } +} + +/* ========================================================================== + Helper classes + ========================================================================== */ + +.ir { + background-color: transparent; + border: 0; + overflow: hidden; + *text-indent: -9999px; +} + +.ir:before { + content: ""; + display: block; + width: 0; + height: 150%; +} + +.hidden { + display: none !important; + visibility: hidden; +} + +.visuallyhidden { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} + +.visuallyhidden.focusable:active, +.visuallyhidden.focusable:focus { + clip: auto; + height: auto; + margin: 0; + overflow: visible; + position: static; + width: auto; +} + +.invisible { + visibility: hidden; +} + +.clearfix:before, +.clearfix:after { + content: " "; + display: table; +} + +.clearfix:after { + clear: both; +} + +.clearfix { + *zoom: 1; +} + +/* ========================================================================== + Print styles + ========================================================================== */ + +@media print { + * { + background: transparent !important; + color: #000 !important; /* Black prints faster: h5bp.com/s */ + box-shadow: none !important; + text-shadow: none !important; + } + + a, + a:visited { + text-decoration: underline; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + /* + * Don't show links for images, or javascript/internal links + */ + + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + + thead { + display: table-header-group; /* h5bp.com/t */ + } + + tr, + img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + @page { + margin: 0.5cm; + } + + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + + h2, + h3 { + page-break-after: avoid; + } +} diff --git a/debug/css/normalize.css b/debug/css/normalize.css new file mode 100644 index 0000000..6e8e42d --- /dev/null +++ b/debug/css/normalize.css @@ -0,0 +1,527 @@ +/*! normalize.css v1.1.2 | MIT License | git.io/normalize */ + +/* ========================================================================== + HTML5 display definitions + ========================================================================== */ + +/** + * Correct `block` display not defined in IE 6/7/8/9 and Firefox 3. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block; +} + +/** + * Correct `inline-block` display not defined in IE 6/7/8/9 and Firefox 3. + */ + +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +/** + * Prevent modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/** + * Address styling not present in IE 7/8/9, Firefox 3, and Safari 4. + * Known issue: no IE 6 support. + */ + +[hidden] { + display: none; +} + +/* ========================================================================== + Base + ========================================================================== */ + +/** + * 1. Correct text resizing oddly in IE 6/7 when body `font-size` is set using + * `em` units. + * 2. Prevent iOS text size adjust after orientation change, without disabling + * user zoom. + */ + +html { + font-size: 100%; /* 1 */ + -ms-text-size-adjust: 100%; /* 2 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/** + * Address `font-family` inconsistency between `textarea` and other form + * elements. + */ + +html, +button, +input, +select, +textarea { + font-family: sans-serif; +} + +/** + * Address margins handled incorrectly in IE 6/7. + */ + +body { + margin: 0; +} + +/* ========================================================================== + Links + ========================================================================== */ + +/** + * Address `outline` inconsistency between Chrome and other browsers. + */ + +a:focus { + outline: thin dotted; +} + +/** + * Improve readability when focused and also mouse hovered in all browsers. + */ + +a:active, +a:hover { + outline: 0; +} + +/* ========================================================================== + Typography + ========================================================================== */ + +/** + * Address font sizes and margins set differently in IE 6/7. + * Address font sizes within `section` and `article` in Firefox 4+, Safari 5, + * and Chrome. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +h2 { + font-size: 1.5em; + margin: 0.83em 0; +} + +h3 { + font-size: 1.17em; + margin: 1em 0; +} + +h4 { + font-size: 1em; + margin: 1.33em 0; +} + +h5 { + font-size: 0.83em; + margin: 1.67em 0; +} + +h6 { + font-size: 0.67em; + margin: 2.33em 0; +} + +/** + * Address styling not present in IE 7/8/9, Safari 5, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/** + * Address style set to `bolder` in Firefox 3+, Safari 4/5, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +blockquote { + margin: 1em 40px; +} + +/** + * Address styling not present in Safari 5 and Chrome. + */ + +dfn { + font-style: italic; +} + +/** + * Address differences between Firefox and other browsers. + * Known issue: no IE 6/7 normalization. + */ + +hr { + -moz-box-sizing: content-box; + box-sizing: content-box; + height: 0; +} + +/** + * Address styling not present in IE 6/7/8/9. + */ + +mark { + background: #ff0; + color: #000; +} + +/** + * Address margins set differently in IE 6/7. + */ + +p, +pre { + margin: 1em 0; +} + +/** + * Correct font family set oddly in IE 6, Safari 4/5, and Chrome. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, serif; + _font-family: 'courier new', monospace; + font-size: 1em; +} + +/** + * Improve readability of pre-formatted text in all browsers. + */ + +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +/** + * Address CSS quotes not supported in IE 6/7. + */ + +q { + quotes: none; +} + +/** + * Address `quotes` property not supported in Safari 4. + */ + +q:before, +q:after { + content: ''; + content: none; +} + +/** + * Address inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* ========================================================================== + Lists + ========================================================================== */ + +/** + * Address margins set differently in IE 6/7. + */ + +dl, +menu, +ol, +ul { + margin: 1em 0; +} + +dd { + margin: 0 0 0 40px; +} + +/** + * Address paddings set differently in IE 6/7. + */ + +menu, +ol, +ul { + padding: 0 0 0 40px; +} + +/** + * Correct list images handled incorrectly in IE 7. + */ + +nav ul, +nav ol { + list-style: none; + list-style-image: none; +} + +/* ========================================================================== + Embedded content + ========================================================================== */ + +/** + * 1. Remove border when inside `a` element in IE 6/7/8/9 and Firefox 3. + * 2. Improve image quality when scaled in IE 7. + */ + +img { + border: 0; /* 1 */ + -ms-interpolation-mode: bicubic; /* 2 */ +} + +/** + * Correct overflow displayed oddly in IE 9. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* ========================================================================== + Figures + ========================================================================== */ + +/** + * Address margin not present in IE 6/7/8/9, Safari 5, and Opera 11. + */ + +figure { + margin: 0; +} + +/* ========================================================================== + Forms + ========================================================================== */ + +/** + * Correct margin displayed oddly in IE 6/7. + */ + +form { + margin: 0; +} + +/** + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/** + * 1. Correct color not being inherited in IE 6/7/8/9. + * 2. Correct text not wrapping in Firefox 3. + * 3. Correct alignment displayed oddly in IE 6/7. + */ + +legend { + border: 0; /* 1 */ + padding: 0; + white-space: normal; /* 2 */ + *margin-left: -7px; /* 3 */ +} + +/** + * 1. Correct font size not being inherited in all browsers. + * 2. Address margins set differently in IE 6/7, Firefox 3+, Safari 5, + * and Chrome. + * 3. Improve appearance and consistency in all browsers. + */ + +button, +input, +select, +textarea { + font-size: 100%; /* 1 */ + margin: 0; /* 2 */ + vertical-align: baseline; /* 3 */ + *vertical-align: middle; /* 3 */ +} + +/** + * Address Firefox 3+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +button, +input { + line-height: normal; +} + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Chrome, Safari 5+, and IE 6+. + * Correct `select` style inheritance in Firefox 4+ and Opera. + */ + +button, +select { + text-transform: none; +} + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + * 4. Remove inner spacing in IE 7 without affecting normal text inputs. + * Known issue: inner spacing remains in IE 6. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ + *overflow: visible; /* 4 */ +} + +/** + * Re-set default cursor for disabled elements. + */ + +button[disabled], +html input[disabled] { + cursor: default; +} + +/** + * 1. Address box sizing set to content-box in IE 8/9. + * 2. Remove excess padding in IE 8/9. + * 3. Remove excess padding in IE 7. + * Known issue: excess padding remains in IE 6. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ + *height: 13px; /* 3 */ + *width: 13px; /* 3 */ +} + +/** + * 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome + * (include `-moz` to future-proof). + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; +} + +/** + * Remove inner padding and search cancel button in Safari 5 and Chrome + * on OS X. + */ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * Remove inner padding and border in Firefox 3+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/** + * 1. Remove default vertical scrollbar in IE 6/7/8/9. + * 2. Improve readability and alignment in all browsers. + */ + +textarea { + overflow: auto; /* 1 */ + vertical-align: top; /* 2 */ +} + +/* ========================================================================== + Tables + ========================================================================== */ + +/** + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/debug/css/normalize.min.css b/debug/css/normalize.min.css new file mode 100644 index 0000000..378226f --- /dev/null +++ b/debug/css/normalize.min.css @@ -0,0 +1 @@ +/*! normalize.css v1.1.2 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-size:100%;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}html,button,input,select,textarea{font-family:sans-serif}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{font-size:2em;margin:.67em 0}h2{font-size:1.5em;margin:.83em 0}h3{font-size:1.17em;margin:1em 0}h4{font-size:1em;margin:1.33em 0}h5{font-size:.83em;margin:1.67em 0}h6{font-size:.67em;margin:2.33em 0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}blockquote{margin:1em 40px}dfn{font-style:italic}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}mark{background:#ff0;color:#000}p,pre{margin:1em 0}code,kbd,pre,samp{font-family:monospace,serif;_font-family:'courier new',monospace;font-size:1em}pre{white-space:pre;white-space:pre-wrap;word-wrap:break-word}q{quotes:none}q:before,q:after{content:'';content:none}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}dl,menu,ol,ul{margin:1em 0}dd{margin:0 0 0 40px}menu,ol,ul{padding:0 0 0 40px}nav ul,nav ol{list-style:none;list-style-image:none}img{border:0;-ms-interpolation-mode:bicubic}svg:not(:root){overflow:hidden}figure{margin:0}form{margin:0}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0;white-space:normal;*margin-left:-7px}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;*overflow:visible}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;*height:13px;*width:13px}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0} \ No newline at end of file diff --git a/debug/index.html b/debug/index.html new file mode 100644 index 0000000..9066741 --- /dev/null +++ b/debug/index.html @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + +
+
+

MP4 Inspector

+
+
+ +
+
+ +
+
+

This page allows you to inspect the structure of an MP4.

+
+
+

Inputs

+
+
+ MP4 Input + +
+
+
+
+

Structure

+
+

+          
+
+
+ +
+
+ + + + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..805e920 --- /dev/null +++ b/index.html @@ -0,0 +1,26 @@ + + + + + videojs-thumb-coil Demo + + + + + + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..9954c4b --- /dev/null +++ b/package.json @@ -0,0 +1,97 @@ +{ + "name": "thumb-coil", + "version": "1.0.0", + "description": "Tools for inspecting MPEG2TS and MP4 files and the codec bitstreams therein", + "main": "es5/index.js", + "scripts": { + "prebuild": "npm run clean", + "build": "npm-run-all -p build:*", + "build:js": "npm-run-all build:js:babel build:js:browserify build:js:bannerize build:js:uglify", + "build:js:babel": "babel src -d es5", + "build:js:bannerize": "bannerize dist/thumbcoil.js --banner=scripts/banner.ejs", + "build:js:browserify": "browserify . -s thumbcoil -o dist/thumbcoil.js", + "build:js:uglify": "uglifyjs dist/thumbcoil.js --comments --mangle --compress -o dist/thumbcoil.min.js", + "change": "chg add", + "clean": "rimraf dist es5 && mkdirp dist es5", + "docs": "npm-run-all docs:*", + "docs:api": "jsdoc src -r -d docs/api", + "docs:toc": "doctoc README.md", + "lint": "vjsstandard", + "start": "babel-node scripts/server.js", + "version": "babel-node scripts/version.js", + "watch": "npm-run-all -p watch:*", + "watch:doc": "nodemon --watch src/ --exec npm run docs", + "watch:js": "npm-run-all -p watch:js:babel watch:js:browserify", + "watch:js:babel": "babel src --watch -d es5", + "watch:js:browserify": "watchify . -t browserify-shim -v -s thumbcoil -o dist/thumbcoil.js", + "postversion": "babel-node scripts/postversion.js", + "prepublish": "npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/imbcmdth/thumb-coil.git" + }, + "author" : "Brightcove, Inc.", + "contributors": [ + { + "name": "Jon-Carlos Rivera", + "email": "jon.carlos.rivera@gmail.com", + "url": "http://jon-carlos.com" + } + ], + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/imbcmdth/thumb-coil/issues" + }, + "homepage": "https://github.com/imbcmdth/thumb-coil#readme", + "keywords": [ + ], + "browserify": { + "transform": [ + "browserify-shim", + "browserify-versionify" + ] + }, + "browserify-shim": { + }, + "vjsstandard": { + "ignore": [ + "dist", + "docs", + "es5" + ] + }, + "files": [ + "CONTRIBUTING.md", + "dist/", + "docs/", + "es5/", + "scripts/", + "src/" + ], + "dependencies": { + "mux.js": "^2.3.1" + }, + "devDependencies": { + "babel": "^5.8.35", + "babelify": "^6.4.0", + "bannerize": "^1.0.2", + "bluebird": "^3.2.2", + "browserify": "^12.0.2", + "browserify-shim": "^3.8.12", + "browserify-versionify": "^1.0.6", + "budo": "^8.0.4", + "chg": "^0.3.2", + "doctoc": "^0.15.0", + "glob": "^6.0.3", + "global": "^4.3.0", + "jsdoc": "^3.4.0", + "mkdirp": "^0.5.1", + "nodemon": "^1.9.1", + "npm-run-all": "^1.5.1", + "rimraf": "^2.5.1", + "uglify-js": "^2.6.1", + "videojs-standard": "^4.0.0", + "watchify": "^3.6.0" + } +} diff --git a/scripts/banner.ejs b/scripts/banner.ejs new file mode 100644 index 0000000..4966491 --- /dev/null +++ b/scripts/banner.ejs @@ -0,0 +1,6 @@ +/** + * <%- pkg.name %> + * @version <%- pkg.version %> + * @copyright <%- date.getFullYear() %> <%- pkg.author %> + * @license <%- pkg.license %> + */ diff --git a/scripts/build-test.js b/scripts/build-test.js new file mode 100644 index 0000000..6d25859 --- /dev/null +++ b/scripts/build-test.js @@ -0,0 +1,15 @@ +import browserify from 'browserify'; +import fs from 'fs'; +import glob from 'glob'; + +/* eslint no-console: 0 */ + +glob('test/**/*.test.js', (err, files) => { + if (err) { + throw err; + } + browserify(files) + .transform('babelify') + .bundle() + .pipe(fs.createWriteStream('test/dist/bundle.js')); +}); diff --git a/scripts/postversion.js b/scripts/postversion.js new file mode 100644 index 0000000..203fbba --- /dev/null +++ b/scripts/postversion.js @@ -0,0 +1,33 @@ +import {exec} from 'child_process'; +import fs from 'fs'; +import path from 'path'; + +/* eslint no-console: 0 */ + +/** + * Determines whether or not the project has the Bower setup by checking for + * the presence of a bower.json file. + * + * @return {Boolean} + */ +const hasBower = () => { + try { + fs.statSync(path.join(__dirname, '../bower.json')); + return true; + } catch (x) { + return false; + } +}; + +// If the project supports Bower, roll HEAD back one commit to avoid having +// the tagged commit - with `dist/` - in the main history. +if (hasBower()) { + exec('git reset --hard HEAD~1', (err, stdout, stderr) => { + if (err) { + process.stdout.write(err.stack); + process.exit(err.status || 1); + } else { + process.stdout.write(stdout); + } + }); +} diff --git a/scripts/server.js b/scripts/server.js new file mode 100644 index 0000000..1dfcd92 --- /dev/null +++ b/scripts/server.js @@ -0,0 +1,140 @@ +import Promise from 'bluebird'; +import browserify from 'browserify'; +import budo from 'budo'; +import fs from 'fs'; +import glob from 'glob'; +import mkdirp from 'mkdirp'; +import path from 'path'; + +/* eslint no-console: 0 */ + +const pkg = require(path.join(__dirname, '../package.json')); + +// Replace "%s" tokens with the plugin name in a string. +const nameify = (str) => + str.replace(/%s/g, pkg.name.split('/').reverse()[0]); + +const srces = { + js: 'src/plugin.js', + tests: glob.sync('test/**/*.test.js') +}; + +const dests = { + js: nameify('dist/%s.js'), + tests: 'test/dist/bundle.js' +}; + +const bundlers = { + + js: browserify({ + debug: true, + entries: [srces.js], + standalone: nameify('%s'), + transform: [ + 'babelify', + 'browserify-shim', + 'browserify-versionify' + ] + }), + + tests: browserify({ + debug: true, + entries: srces.tests, + transform: [ + 'babelify', + 'browserify-shim', + 'browserify-versionify' + ] + }) +}; + +const bundle = (name) => { + return new Promise((resolve, reject) => { + bundlers[name] + .bundle() + .pipe(fs.createWriteStream(dests[name])) + .on('finish', resolve) + .on('error', reject); + }); +}; + +mkdirp.sync('dist'); + +// Start the server _after_ the initial bundling is done. +Promise.all([bundle('js'), bundle('tests')]).then(() => { + const server = budo({ + port: 9999, + stream: process.stdout + }).on('reload', (f) => console.log('reloading %s', f || 'everything')); + + /** + * A collection of functions which are mapped to strings that are used to + * generate RegExp objects. If a filepath matches the RegExp, the function + * will be used to handle that watched file. + * + * @type {Object} + */ + const handlers = { + + /** + * Handler for JavaScript source. + * + * @param {String} event + * @param {String} file + */ + '^src/.+\.js$'(event, file) { + console.log('re-bundling javascript and tests'); + Promise.all([bundle('js'), bundle('tests')]).then(() => server.reload()); + }, + + /** + * Handler for JavaScript tests. + * + * @param {String} event + * @param {String} file + */ + '^test/.+\.test\.js$'(event, file) { + console.log('re-bundling tests'); + bundle('tests').then(() => server.reload()); + } + }; + + /** + * Finds the first handler function for the file that matches a RegExp + * derived from the keys. + * + * @param {String} file + * @return {Function|Undefined} + */ + const findHandler = (file) => { + const keys = Object.keys(handlers); + + for (let i = 0; i < keys.length; i++) { + let regex = new RegExp(keys[i]); + + if (regex.test(file)) { + return handlers[keys[i]]; + } + } + }; + + server + .live() + .watch([ + 'index.html', + 'src/**/*.js', + 'test/**/*.test.js', + 'test/index.html' + ]) + .on('watch', (event, file) => { + const handler = findHandler(file); + + console.log(`detected a "${event}" event in "${file}"`); + + if (handler) { + handler(event, file); + } else { + server.reload(); + } + }); +}); diff --git a/scripts/version.js b/scripts/version.js new file mode 100644 index 0000000..dfabfdd --- /dev/null +++ b/scripts/version.js @@ -0,0 +1,69 @@ +import {exec} from 'child_process'; +import fs from 'fs'; +import path from 'path'; + +/* eslint no-console: 0 */ + +const pkg = require(path.join(__dirname, '../package.json')); + +/** + * Determines whether or not the project has the CHANGELOG setup by checking + * for the presence of a CHANGELOG.md file and the necessary dependency and + * npm script. + * + * @return {Boolean} + */ +const hasChangelog = () => { + try { + fs.statSync(path.join(__dirname, '../CHANGELOG.md')); + } catch (x) { + return false; + } + return pkg.devDependencies.hasOwnProperty('chg') && + pkg.scripts.hasOwnProperty('change'); +}; + +/** + * Determines whether or not the project has the Bower setup by checking for + * the presence of a bower.json file. + * + * @return {Boolean} + */ +const hasBower = () => { + try { + fs.statSync(path.join(__dirname, '../bower.json')); + return true; + } catch (x) { + return false; + } +}; + +const commands = []; + +// If the project has a CHANGELOG, update it for the new release. +if (hasChangelog()) { + commands.push(`chg release "${pkg.version}"`); + commands.push('git add CHANGELOG.md'); +} + +// If the project supports Bower, perform special extra versioning step. +if (hasBower()) { + commands.push('git add package.json'); + commands.push(`git commit -m "${pkg.version}"`); + + // We only need a build in the Bower-supported case because of the + // temporary addition of the dist/ directory. + commands.push('npm run build'); + commands.push('git add -f dist'); +} + +if (commands.length) { + exec(commands.join(' && '), (err, stdout, stderr) => { + if (err) { + process.stdout.write(err.stack); + process.exit(err.status || 1); + } else { + process.stdout.write(stdout); + } + }); +} diff --git a/src/bit-streams/h264/access-unit-delimiter.js b/src/bit-streams/h264/access-unit-delimiter.js new file mode 100644 index 0000000..458131b --- /dev/null +++ b/src/bit-streams/h264/access-unit-delimiter.js @@ -0,0 +1,12 @@ +'use strict'; + +import {start, data, list, verify} from './lib/combinators'; +import {u} from './lib/data-types'; + +const audCodec = start('access_unit_delimiter', + list([ + data('primary_pic_type', u(3)), + verify('access_unit_delimiter') + ])); + +export default audCodec; diff --git a/src/bit-streams/h264/hdr-parameters.js b/src/bit-streams/h264/hdr-parameters.js new file mode 100644 index 0000000..86f0246 --- /dev/null +++ b/src/bit-streams/h264/hdr-parameters.js @@ -0,0 +1,29 @@ +'use strict'; + +import {list, data} from './lib/combinators'; +import {u, ue} from './lib/data-types'; +import {each} from './lib/conditionals'; + +import scalingList from './scaling-list'; + +let v = null; + +const hdrParameters = list([ + data('cpb_cnt_minus1', ue(v)), + data('bit_rate_scale', u(4)), + data('cpb_size_scale', u(4)), + each((index, output) => { + return index <= output.cpb_cnt_minus1; + }, + list([ + data('bit_rate_value_minus1[]', ue(v)), + data('cpb_size_value_minus1[]', ue(v)), + data('cbr_flag[]', u(1)) + ])), + data('initial_cpb_removal_delay_length_minus1', u(5)), + data('cpb_removal_delay_length_minus1', u(5)), + data('dpb_output_delay_length_minus1', u(5)), + data('time_offset_length', u(5)) +]); + +export default hdrParameters; diff --git a/src/bit-streams/h264/index.js b/src/bit-streams/h264/index.js new file mode 100644 index 0000000..54cce71 --- /dev/null +++ b/src/bit-streams/h264/index.js @@ -0,0 +1,15 @@ +import accessUnitDelimiter from './access-unit-delimiter'; +import seqParameterSet from './seq-parameter-set'; +import picParameterSet from './pic-parameter-set'; +import sliceLayerWithoutPartitioning from './slice-layer-without-partitioning'; +import discardEmulationPrevention from './lib/discard-emulation-prevention'; + +const h264Codecs = { + accessUnitDelimiter, + seqParameterSet, + picParameterSet, + sliceLayerWithoutPartitioning, + discardEmulationPrevention +}; + +export default h264Codecs; diff --git a/src/bit-streams/h264/lib/combinators.js b/src/bit-streams/h264/lib/combinators.js new file mode 100644 index 0000000..4c43c32 --- /dev/null +++ b/src/bit-streams/h264/lib/combinators.js @@ -0,0 +1,168 @@ +'use strict'; + +import {ExpGolombEncoder, ExpGolombDecoder} from './exp-golomb-string'; +import { + typedArrayToBitString, + bitStringToTypedArray, + removeRBSPTrailingBits, + appendRBSPTrailingBits +} from './rbsp-utils'; + +/** + * General ExpGolomb-Encoded-Structure Parse Functions + */ +export const start = function (name, parseFn) { + return { + decode: (input, options) => { + let rawBitString = typedArrayToBitString(input); + let bitString = removeRBSPTrailingBits(rawBitString); + let expGolombDecoder = new ExpGolombDecoder(bitString); + let output = {}; + + options = options || {}; + + return parseFn.decode(expGolombDecoder, output, options); + }, + encode: (input, options) => { + let expGolombEncoder = new ExpGolombEncoder(); + + options = options || {}; + + parseFn.encode(expGolombEncoder, input, options); + + let output = expGolombEncoder.bitReservoir; + let bitString = appendRBSPTrailingBits(output); + let data = bitStringToTypedArray(bitString); + + return data; + } + }; +}; + +export const list = function (parseFns) { + return { + decode: (expGolomb, output, options, index) => { + parseFns.forEach((fn) => { + output = fn.decode(expGolomb, output, options, index) || output; + }); + + return output; + }, + encode: (expGolomb, input, options, index) => { + parseFns.forEach((fn) => { + fn.encode(expGolomb, input, options, index); + }); + } + }; +}; + +export const data = function (name, dataType) { + let nameSplit = name.split(/\[(\d*)\]/); + let property = nameSplit[0]; + let indexOverride; + let nameArray; + + // The `nameSplit` array can either be 1 or 3 long + if (nameSplit && nameSplit[0] !== '') { + if (nameSplit.length > 1) { + nameArray = true; + indexOverride = parseFloat(nameSplit[1]); + + if (isNaN(indexOverride)) { + indexOverride = undefined; + } + } + } else { + throw new Error('ExpGolombError: Invalid name "' + name + '".'); + } + + return { + name: name, + decode: (expGolomb, output, options, index) => { + let value; + + if (typeof indexOverride === 'number') { + index = indexOverride; + } + + value = dataType.read(expGolomb, output, options, index); + + if (!nameArray) { + output[property] = value; + } else { + if (!Array.isArray(output[property])) { + output[property] = []; + } + + if (index !== undefined) { + output[property][index] = value; + } else { + output[property].push(value); + } + } + + return output; + }, + encode: (expGolomb, input, options, index) => { + let value; + + if (typeof indexOverride === 'number') { + index = indexOverride; + } + + if (!nameArray) { + value = input[property]; + } else if (Array.isArray(output[property])) { + if (index !== undefined) { + value = input[property][index]; + } else { + value = input[property].shift(); + } + } + + if (typeof value !== 'number') { + return; + } + + value = dataType.write(expGolomb, input, options, index, value); + } + }; +}; + +export const debug = function (prefix) { + return { + decode: (expGolomb, output, options, index) => { + console.log(prefix, expGolomb.bitReservoir, output, options, index); + }, + encode: (expGolomb, input, options, index) => { + console.log(prefix, expGolomb.bitReservoir, input, options, index); + } + }; +}; + +export const verify = function (name) { + return { + decode: (expGolomb, output, options, index) => { + let len = expGolomb.bitReservoir.length; + if (len !== 0) { + console.trace('ERROR: ' + name + ' was not completely parsed. There were (' + len + ') bits remaining!'); + } + }, + encode: (expGolomb, input, options, index) => {} + }; +}; + +export const pickOptions = function (property, value) { + return { + decode: (expGolomb, output, options, index) => { + if (typeof options[property] !== undefined) { + // options[property][value]; + } + }, + encode: (expGolomb, input, options, index) => { + if (typeof options[property] !== undefined) { + // options.values options[property][value]; + } + } + }; +}; diff --git a/src/bit-streams/h264/lib/conditionals.js b/src/bit-streams/h264/lib/conditionals.js new file mode 100644 index 0000000..82b1126 --- /dev/null +++ b/src/bit-streams/h264/lib/conditionals.js @@ -0,0 +1,135 @@ +'use strict'; + +export const when = function (conditionFn, parseFn) { + return { + decode: (expGolomb, output, options, index) => { + if (conditionFn(output, options, index)) { + return parseFn.decode(expGolomb, output, options, index); + } + + return output; + }, + encode: (expGolomb, input, options, index) => { + if (conditionFn(input, options, index)) { + parseFn.encode(expGolomb, input, options, index); + } + } + }; +}; + +export const each = function (conditionFn, parseFn) { + return { + decode: (expGolomb, output, options) => { + let index = 0; + + while (conditionFn(index, output, options)) { + parseFn.decode(expGolomb, output, options, index); + index++; + } + + return output; + }, + encode: (expGolomb, input, options) => { + let index = 0; + + while (conditionFn(index, input, options)) { + parseFn.encode(expGolomb, input, options, index); + index++; + } + } + }; +}; + +export const inArray = function (name, array) { + let nameSplit = name.split(/\[(\d*)\]/); + let property = nameSplit[0]; + let indexOverride; + let nameArray; + + // The `nameSplit` array can either be 1 or 3 long + if (nameSplit && nameSplit[0] !== '') { + if (nameSplit.length > 1) { + nameArray = true; + indexOverride = parseFloat(nameSplit[1]); + + if (isNaN(indexOverride)) { + indexOverride = undefined; + } + } + } else { + throw new Error('ExpGolombError: Invalid name "' + name + '".'); + } + + return (obj, options, index) => { + if (nameArray) { + return (obj[property] && array.indexOf(obj[property][index]) !== -1) || + (options[property] && array.indexOf(options[property][index]) !== -1); + } else { + return array.indexOf(obj[property]) !== -1 || + array.indexOf(options[property]) !== -1; + } + }; +}; + +export const equals = function (name, value) { + let nameSplit = name.split(/\[(\d*)\]/); + let property = nameSplit[0]; + let indexOverride; + let nameArray; + + // The `nameSplit` array can either be 1 or 3 long + if (nameSplit && nameSplit[0] !== '') { + if (nameSplit.length > 1) { + nameArray = true; + indexOverride = parseFloat(nameSplit[1]); + + if (isNaN(indexOverride)) { + indexOverride = undefined; + } + } + } else { + throw new Error('ExpGolombError: Invalid name "' + name + '".'); + } + + return (obj, options, index) => { + if (nameArray) { + return (obj[property] && obj[property][index] === value) || + (options[property] && options[property][index] === value); + } else { + return obj[property] === value || + options[property] === value; + } + }; +}; + +export const not = function (fn) { + return (obj, options, index) => { + return !fn(obj, options, index); + }; +}; + +export const some = function (conditionFns) { + return (obj, options, index) => { + return conditionFns.some((fn)=>fn(obj, options, index)); + }; +}; + +export const every = function (conditionFns) { + return (obj, options, index) => { + return conditionFns.every((fn)=>fn(obj, options, index)); + }; +}; + +export const whenMoreData = function (parseFn) { + return { + decode: (expGolomb, output, options, index) => { + if (expGolomb.bitReservoir.length) { + return parseFn.decode(expGolomb, output, options, index); + } + return output; + }, + encode: (expGolomb, input, options, index) => { + parseFn.encode(expGolomb, input, options, index); + } + }; +}; diff --git a/src/bit-streams/h264/lib/data-types.js b/src/bit-streams/h264/lib/data-types.js new file mode 100644 index 0000000..76acd63 --- /dev/null +++ b/src/bit-streams/h264/lib/data-types.js @@ -0,0 +1,86 @@ +'use strict'; + +const getNumBits = (numBits, expGolomb, data, options, index) => { + if (typeof numBits === 'function') { + return numBits(expGolomb, data, options, index); + } + return numBits; +}; + +const dataTypes = { + u: (numBits) => { + return { + read: (expGolomb, output, options, index) => { + let bitsToRead = getNumBits(numBits, expGolomb, output, options, index); + + return expGolomb.readBits(bitsToRead); + }, + write: (expGolomb, input, options, index, value) => { + let bitsToWrite = getNumBits(numBits, expGolomb, input, options, index); + + expGolomb.writeBits(value, bitsToWrite); + } + }; + }, + f: (numBits) => { + return { + read: (expGolomb, output, options, index) => { + let bitsToRead = getNumBits(numBits, expGolomb, output, options, index); + + return expGolomb.readBits(bitsToRead); + }, + write: (expGolomb, input, options, index, value) => { + let bitsToWrite = getNumBits(numBits, expGolomb, input, options, index); + + expGolomb.writeBits(value, bitsToWrite); + } + }; + }, + ue: () => { + return { + read: (expGolomb, output, options, index) => { + return expGolomb.readUnsignedExpGolomb(); + }, + write: (expGolomb, input, options, index, value) => { + expGolomb.writeUnsignedExpGolomb(value); + } + }; + }, + se: () => { + return { + read: (expGolomb, output, options, index) => { + return expGolomb.readExpGolomb(); + }, + write: (expGolomb, input, options, index, value) => { + expGolomb.writeExpGolomb(value); + } + }; + }, + b: () => { + return { + read: (expGolomb, output, options, index) => { + return expGolomb.readUnsignedByte(); + }, + write: (expGolomb, input, options, index, value) => { + expGolomb.writeUnsignedByte(value); + } + }; + }, + val: (val) => { + return { + read: (expGolomb, output, options, index) => { + if (typeof val === 'function') { + return val(expGolomb, output, options, index); + } + return val; + }, + write: (expGolomb, input, options, index, value) => { + if (typeof val === 'function') { + val(ExpGolomb, output, options, index); + } + } + }; + } +}; + +export default dataTypes; diff --git a/src/bit-streams/h264/lib/discard-emulation-prevention.js b/src/bit-streams/h264/lib/discard-emulation-prevention.js new file mode 100644 index 0000000..feb9679 --- /dev/null +++ b/src/bit-streams/h264/lib/discard-emulation-prevention.js @@ -0,0 +1,44 @@ +'use strict'; + +const discardEmulationPreventionBytes = (data) => { + let length = data.length; + let emulationPreventionBytesPositions = []; + let i = 1; + let newLength; + let newData; + + // Find all `Emulation Prevention Bytes` + while (i < length - 2) { + if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) { + emulationPreventionBytesPositions.push(i + 2); + i += 2; + } else { + i++; + } + } + + // If no Emulation Prevention Bytes were found just return the original + // array + if (emulationPreventionBytesPositions.length === 0) { + return data; + } + + // Create a new array to hold the NAL unit data + newLength = length - emulationPreventionBytesPositions.length; + newData = new Uint8Array(newLength); + var sourceIndex = 0; + + for (i = 0; i < newLength; sourceIndex++, i++) { + if (sourceIndex === emulationPreventionBytesPositions[0]) { + // Skip this byte + sourceIndex++; + // Remove this position index + emulationPreventionBytesPositions.shift(); + } + newData[i] = data[sourceIndex]; + } + + return newData; +}; + +export default discardEmulationPreventionBytes; diff --git a/src/bit-streams/h264/lib/exp-golomb-string.js b/src/bit-streams/h264/lib/exp-golomb-string.js new file mode 100644 index 0000000..1f22d69 --- /dev/null +++ b/src/bit-streams/h264/lib/exp-golomb-string.js @@ -0,0 +1,102 @@ +/** + * Tools for encoding and decoding ExpGolomb data from a bit-string + */ +'use strict'; + +export const ExpGolombDecoder = function (bitString) { + this.bitReservoir = bitString; +}; + +ExpGolombDecoder.prototype.countLeadingZeros = function () { + let i = 0; + + for (let i = 0; i < this.bitReservoir.length; i++) { + if (this.bitReservoir[i] === '1') { + return i; + } + } + + return -1; +}; + +ExpGolombDecoder.prototype.readUnsignedExpGolomb = function () { + let zeros = this.countLeadingZeros(); + let bitCount = zeros * 2 + 1; + + let val = parseInt(this.bitReservoir.slice(zeros, bitCount), 2); + + val -= 1; + + this.bitReservoir = this.bitReservoir.slice(bitCount); + + return val; +}; + +ExpGolombDecoder.prototype.readExpGolomb = function () { + let val = this.readUnsignedExpGolomb(); + + if (val !== 0) { + if (val & 0x1) { + val = (val + 1) / 2; + } else { + val = -(val / 2); + } + } + + return val; +}; + +ExpGolombDecoder.prototype.readBits = function (bitCount) { + let val = parseInt(this.bitReservoir.slice(0, bitCount), 2); + + this.bitReservoir = this.bitReservoir.slice(bitCount); + + return val; +}; + + +ExpGolombDecoder.prototype.readUnsignedByte = function () { + return this.writeBits(8); +}; + +export const ExpGolombEncoder = function (bitString) { + this.bitReservoir = bitString || ''; +}; + +ExpGolombEncoder.prototype.writeUnsignedExpGolomb = function (value) { + let tempStr = ''; + let bitValue = (value + 1).toString(2); + let numBits = bitValue.length - 1; + + for (let i = 0; i < numBits; i++) { + tempStr += '0'; + } + + this.bitReservoir += tempStr + bitValue; +}; + +ExpGolombEncoder.prototype.writeExpGolomb = function (value) { + if (value <= 0) { + value = -value * 2; + } else { + value = value * 2 - 1; + } + + this.writeUnsignedExpGolomb(value); +}; + +ExpGolombEncoder.prototype.writeBits = function (bitWidth, value) { + let tempStr = ''; + let bitValue = (value & ((1 << bitWidth)-1)).toString(2); + let numBits = bitWidth - bitValue.length; + + for (let i = 0; i < numBits; i++) { + tempStr += '0'; + } + + this.bitReservoir += tempStr + bitValue; +}; + +ExpGolombEncoder.prototype.writeUnsignedByte = function (value) { + this.writeBits(8, value); +}; diff --git a/src/bit-streams/h264/lib/rbsp-utils.js b/src/bit-streams/h264/lib/rbsp-utils.js new file mode 100644 index 0000000..8f33ce1 --- /dev/null +++ b/src/bit-streams/h264/lib/rbsp-utils.js @@ -0,0 +1,43 @@ +'use strict'; + +export const typedArrayToBitString = (data) => { + var array = []; + var bytesPerElement = data.BYTES_PER_ELEMENT || 1; + var prefixZeros = ''; + + for (let i = 0; i < data.length; i++) { + array.push(data[i]); + } + + for (let i = 0; i < bytesPerElement; i++) { + prefixZeros += '00000000'; + } + + return array + .map((n) => (prefixZeros + n.toString(2)).slice(-bytesPerElement * 8)) + .join(''); +}; + +export const bitStringToTypedArray = (bitString) => { + let bitsNeeded = 8 - (bitString.length % 8); + + // Pad with zeros to make length a multiple of 8 + for (let i = 0; bitsNeeded !==8 && i < bitsNeeded; i++) { + bitString += '0'; + } + + let outputArray = bitString.match(/(.{8})/g); + let numberArray = outputArray.map((n) => parseInt(n, 2)); + + return new Uint8Array(numberArray); +}; + +export const removeRBSPTrailingBits = (bits) => { + return bits.split(/10+$/)[0]; +}; + +export const appendRBSPTrailingBits = (bits) => { + let bitString = bits + '10000000'; + + return bitString.slice(0, -(bitString.length % 8)); +}; diff --git a/src/bit-streams/h264/pic-parameter-set.js b/src/bit-streams/h264/pic-parameter-set.js new file mode 100644 index 0000000..9a99ef9 --- /dev/null +++ b/src/bit-streams/h264/pic-parameter-set.js @@ -0,0 +1,85 @@ +'use strict'; + +import {start, list, data, debug, verify} from './lib/combinators'; +import { + when, + each, + inArray, + equals, + some, + every, + not, + whenMoreData +} from './lib/conditionals'; +import {ue, u, se, val} from './lib/data-types'; + +import scalingList from './scaling-list'; + +let v = null; + +const ppsCodec = start('pic_parameter_set', + list([ + data('pic_parameter_set_id', ue(v)), + data('seq_parameter_set_id', ue(v)), +// pickOptions('sps', 'seq_parameter_set_id'), + data('entropy_coding_mode_flag', u(1)), + data('bottom_field_pic_order_in_frame_present_flag', u(1)), + data('num_slice_groups_minus1', ue(v)), + when(not(equals('num_slice_groups_minus1', 0)), + list([ + data('slice_group_map_type', ue(v)), + when(equals('slice_group_map_type', 0), + each((index, output) => { + return index <= output.num_slice_groups_minus1; + }, + data('run_length_minus1[]', ue(v)))), + when(equals('slice_group_map_type', 2), + each((index, output) => { + return index <= output.num_slice_groups_minus1; + }, + list([ + data('top_left[]', ue(v)), + data('bottom_right[]', ue(v)), + ]))), + when(inArray('slice_group_map_type', [3, 4, 5]), + list([ + data('slice_group_change_direction_flag', u(1)), + data('slice_group_change_rate_minus1', ue(v)) + ])), + when(equals('slice_group_map_type', 6), + list([ + data('pic_size_in_map_units_minus1', ue(v)), + each((index, output) => { + return index <= output.pic_size_in_map_units_minus1; + }, + data('slice_group_id[]', ue(v))) + ])) + ])), + data('num_ref_idx_l0_default_active_minus1', ue(v)), + data('num_ref_idx_l1_default_active_minus1', ue(v)), + data('weighted_pred_flag', u(1)), + data('weighted_bipred_idc', u(2)), + data('pic_init_qp_minus26', se(v)), + data('pic_init_qs_minus26', se(v)), + data('chroma_qp_index_offset', se(v)), + data('deblocking_filter_control_present_flag', u(1)), + data('constrained_intra_pred_flag', u(1)), + data('redundant_pic_cnt_present_flag', u(1)), + whenMoreData(list([ + data('transform_8x8_mode_flag', u(1)), + data('pic_scaling_matrix_present_flag', u(1)), + when(equals('pic_scaling_matrix_present_flag', 1), + each((index, output) => { + return index < 6 + ((output.chroma_format_Idc !== 3) ? 2 : 6) * output.transform_8x8_mode_flag; + }, + list([ + data('pic_scaling_list_present_flag[]', u(1)), + when(equals('pic_scaling_list_present_flag[]', 1), + scalingList) + ]))), + data('second_chroma_qp_index_offset', se(v)) + ])), + verify('pic_parameter_set') + ])); + +export default ppsCodec; diff --git a/src/bit-streams/h264/scaling-list.js b/src/bit-streams/h264/scaling-list.js new file mode 100644 index 0000000..a7f2cf8 --- /dev/null +++ b/src/bit-streams/h264/scaling-list.js @@ -0,0 +1,63 @@ +'use strict'; + +const scalingList = { + decode: function (expGolomb, output, options, index) { + let lastScale = 8; + let nextScale = 8; + let deltaScale; + let count = 16; + let scalingArr = []; + + if (!Array.isArray(output.scalingList)) { + output.scalingList = []; + } + + if (index >= 6) { + count = 64; + } + + for (let j = 0; j < count; j++) { + if (nextScale !== 0) { + deltaScale = expGolomb.readExpGolomb(); + nextScale = (lastScale + deltaScale + 256) % 256; + } + + scalingArr[j] = (nextScale === 0) ? lastScale : nextScale; + lastScale = scalingArr[j]; + } + + output.scalingList[index] = scalingArr; + + return output; + }, + encode: function (expGolomb, input, options, index) { + let lastScale = 8; + let nextScale = 8; + let deltaScale; + let count = 16; + let output = ''; + + if (!Array.isArray(input.scalingList)) { + return ''; + } + + if (index >= 6) { + count = 64; + } + + let scalingArr = output.scalingList[index]; + + for (let j = 0; j < count; j++) { + if (scalingArr[j] === lastScale) { + output += expGolomb.writeExpGolomb(-lastScale); + break; + } + nextScale = scalingArr[j] - lastScale; + output += expGolomb.writeExpGolomb(nextScale); + lastScale = scalingArr[j]; + } + return output; + } +}; + +export default scalingList; diff --git a/src/bit-streams/h264/seq-parameter-set.js b/src/bit-streams/h264/seq-parameter-set.js new file mode 100644 index 0000000..6fbe5da --- /dev/null +++ b/src/bit-streams/h264/seq-parameter-set.js @@ -0,0 +1,110 @@ +'use strict'; + +import {start, list, data, debug, verify} from './lib/combinators'; +import {when, each, inArray, equals, some, every, not} from './lib/conditionals'; +import {ue, u, se, val} from './lib/data-types'; + +import scalingList from './scaling-list'; +import vuiParamters from './vui-parameters'; + +let v = null; + +let PROFILES_WITH_OPTIONAL_SPS_DATA = [ + 44, 83, 86, 100, 110, 118, 122, 128, + 134, 138, 139, 244 +]; + +let getChromaFormatIdcValue = { + read: (expGolomb, output, options, index) => { + return output.chroma_format_idc || options.chroma_format_idc; + }, + write:()=>{} +}; + +/** + * NOW we are ready to build an SPS parser! + */ +const spsCodec = start('seq_parameter_set', + list([ + // defaults + data('chroma_format_idc', val(1)), + data('video_format', val(5)), + data('color_primaries', val(2)), + data('transfer_characteristics', val(2)), + data('sample_ratio', val(1.0)), + + data('profile_idc', u(8)), + data('constraint_set0_flag', u(1)), + data('constraint_set1_flag', u(1)), + data('constraint_set2_flag', u(1)), + data('constraint_set3_flag', u(1)), + data('constraint_set4_flag', u(1)), + data('constraint_set5_flag', u(1)), + data('constraint_set6_flag', u(1)), + data('constraint_set7_flag', u(1)), + data('level_idc', u(8)), + data('seq_parameter_set_id', ue(v)), + when(inArray('profile_idc', PROFILES_WITH_OPTIONAL_SPS_DATA), + list([ + data('chroma_format_idc', ue(v)), + when(equals('chroma_format_idc', 3), + data('separate_colour_plane_flag', u(1))), + when(not(equals('chroma_format_idc', 3)), + data('separate_colour_plane_flag', val(0))), + data('bit_depth_luma_minus8', ue(v)), + data('bit_depth_chroma_minus8', ue(v)), + data('qpprime_y_zero_transform_bypass_flag', u(1)), + data('seq_scaling_matrix_present_flag', u(1)), + when(equals('seq_scaling_matrix_present_flag', 1), + each((index, output) => { + return index < ((output.chroma_format_idc !== 3) ? 8 : 12); + }, + list([ + data('seq_scaling_list_present_flag[]', u(1)), + when(equals('seq_scaling_list_present_flag[]', 1), + scalingList) + ]))) + ])), + data('log2_max_frame_num_minus4', ue(v)), + data('pic_order_cnt_type', ue(v)), + when(equals('pic_order_cnt_type', 0), + data('log2_max_pic_order_cnt_lsb_minus4', ue(v))), + when(equals('pic_order_cnt_type', 1), + list([ + data('delta_pic_order_always_zero_flag', u(1)), + data('offset_for_non_ref_pic', se(v)), + data('offset_for_top_to_bottom_field', se(v)), + data('num_ref_frames_in_pic_order_cnt_cycle', ue(v)), + each((index, output) => { + return index < output.num_ref_frames_in_pic_order_cnt_cycle; + }, + data('offset_for_ref_frame[]', se(v))) + ])), + data('max_num_ref_frames', ue(v)), + data('gaps_in_frame_num_value_allowed_flag', u(1)), + data('pic_width_in_mbs_minus1', ue(v)), + data('pic_height_in_map_units_minus1', ue(v)), + data('frame_mbs_only_flag', u(1)), + when(equals('frame_mbs_only_flag', 0), + data('mb_adaptive_frame_field_flag', u(1))), + data('direct_8x8_inference_flag', u(1)), + data('frame_cropping_flag', u(1)), + when(equals('frame_cropping_flag', 1), + list([ + data('frame_crop_left_offset', ue(v)), + data('frame_crop_right_offset', ue(v)), + data('frame_crop_top_offset', ue(v)), + data('frame_crop_bottom_offset', ue(v)) + ])), + data('vui_parameters_present_flag', u(1)), + when(equals('vui_parameters_present_flag', 1), vuiParamters), + // The following field is a derived value that is used for parsing + // slice headers + when(equals('separate_colour_plane_flag', 1), + data('ChromaArrayType', val(0))), + when(equals('separate_colour_plane_flag', 0), + data('ChromaArrayType', getChromaFormatIdcValue)), + verify('seq_parameter_set') + ])); + +export default spsCodec; diff --git a/src/bit-streams/h264/slice-header.js b/src/bit-streams/h264/slice-header.js new file mode 100644 index 0000000..7cc3944 --- /dev/null +++ b/src/bit-streams/h264/slice-header.js @@ -0,0 +1,264 @@ +'use strict'; + +import {start, list, data, debug, verify} from './lib/combinators'; +import {when, each, inArray, equals, some, every, not} from './lib/conditionals'; +import {ue, u, se, val} from './lib/data-types'; + +let v = null; + +let sliceType = { + P: [0, 5], + B: [1, 6], + I: [2, 7], + SP: [3, 8], + SI: [4, 9] +}; + +/** + * Functions for calculating the number of bits to read for certain + * properties based on the values in other properties (usually specified + * in the SPS) + */ +let frameNumBits = (expGolomb, data, options, index) => { + return options.log2_max_frame_num_minus4 + 4; +}; + +let picOrderCntBits = (expGolomb, data, options, index) => { + return options.log2_max_pic_order_cnt_lsb_minus4 + 4; +}; + +let sliceGroupChangeCycleBits = (expGolomb, data, options, index) => { + let picHeightInMapUnits = options.pic_height_in_map_units_minus1 + 1; + let picWidthInMbs = options.pic_width_in_mbs_minus1 + 1; + let sliceGroupChangeRate = options.slice_group_change_rate_minus1 + 1; + let picSizeInMapUnits = picWidthInMbs * picHeightInMapUnits; + + return Math.ceil(Math.log(picSizeInMapUnits / sliceGroupChangeRate + 1) / Math.LN2); +}; + +let useWeightedPredictionTable = some([ + every([ + equals('weighted_pred_flag', 1), + some([ + inArray('slice_type', sliceType.P), + inArray('slice_type', sliceType.SP) + ]) + ]), + every([ + equals('weighted_bipred_idc', 1), + inArray('slice_type', sliceType.B), + ]) +]); + +let refPicListModification = list([ + when(every([ + not(inArray('slice_type', sliceType.I)), + not(inArray('slice_type', sliceType.SI)) + ]), list([ + data('ref_pic_list_modification_flag_l0', u(1)), + when(equals('ref_pic_list_modification_flag_l0', 1), + each((index, output) => { + return index === 0 || output.modification_of_pic_nums_idc_l0[index - 1] !== 3; + }, + list([ + data('modification_of_pic_nums_idc_l0[]', ue(v)), + when(inArray('modification_of_pic_nums_idc_l0[]', [0, 1]), + data('abs_diff_pic_num_minus1_l0[]', ue(v))), + when(equals('modification_of_pic_nums_idc_l0[]', 2), + data('long_term_pic_num_l0[]', ue(v))) + ]))) + ])), + when(inArray('slice_type', sliceType.B), + list([ + data('ref_pic_list_modification_flag_l1', u(1)), + when(equals('ref_pic_list_modification_flag_l1', 1), + each((index, output) => { + return index === 0 || output.modification_of_pic_nums_idc_l1[index - 1] !== 3; + }, + list([ + data('modification_of_pic_nums_idc_l1[]', ue(v)), + when(inArray('modification_of_pic_nums_idc_l1[]', [0, 1]), + data('abs_diff_pic_num_minus1_l1[]', ue(v))), + when(equals('modification_of_pic_nums_idc_l1[]', 2), + data('long_term_pic_num_l1[]', ue(v))) + ]))) + ])) +]); + +let refPicListMvcModification = { + encode: () => { throw new Error('ref_pic_list_mvc_modification: NOT IMPLEMENTED!')}, + decode: () => {throw new Error('ref_pic_list_mvc_modification: NOT IMPLEMENTED!')} +}; + +let predWeightTable = list([ + data('luma_log2_weight_denom', ue(v)), + when(not(equals('ChromaArrayType', 0)), + data('chroma_log2_weight_denom', ue(v))), + each((index, output) => { + return index <= output.num_ref_idx_l0_active_minus1; + }, + list([ + data('luma_weight_l0_flag', u(1)), + when(equals('luma_weight_l0_flag', 1), + list([ + data('luma_weight_l0[]', se(v)), + data('luma_offset_l0[]', se(v)), + when(not(equals('ChromaArrayType', 0)), + list([ + data('chroma_weight_l0_flag', u(1)), + when(equals('chroma_weight_l0_flag', 1), + list([ + data('chroma_weight_l0_Cr[]', se(v)), + data('chroma_offset_l0_Cr[]', se(v)), + data('chroma_weight_l0_Cb[]', se(v)), + data('chroma_offset_l0_Cb[]', se(v)) + ])) + ])) + ])) + ])), + when(inArray('slice_type', sliceType.B), + each((index, output) => { + return index <= output.num_ref_idx_l1_active_minus1; + }, + list([ + data('luma_weight_l1_flag', u(1)), + when(equals('luma_weight_l1_flag', 1), + list([ + data('luma_weight_l1[]', se(v)), + data('luma_offset_l1[]', se(v)), + when(not(equals('ChromaArrayType', 0)), + list([ + data('chroma_weight_l1_flag', u(1)), + when(equals('chroma_weight_l1_flag', 1), + list([ + data('chroma_weight_l1_Cr[]', se(v)), + data('chroma_offset_l1_Cr[]', se(v)), + data('chroma_weight_l1_Cb[]', se(v)), + data('chroma_offset_l1_Cb[]', se(v)) + ])) + ])) + ])) + ]))) +]); + +let decRefPicMarking = list([ + when(equals('nal_unit_type', 5), + list([ + data('no_output_of_prior_pics_flag', u(1)), + data('long_term_reference_flag', u(1)) + ])), + when(not(equals('nal_unit_type', 5)), + list([ + data('adaptive_ref_pic_marking_mode_flag', u(1)), + when(equals('adaptive_ref_pic_marking_mode_flag', 1), + each((index, output) => { + return index === 0 || output.memory_management_control_operation[index - 1] !== 0; + }, + list([ + data('memory_management_control_operation[]', ue(v)), + when(inArray('memory_management_control_operation[]', [1, 3]), + data('difference_of_pic_nums_minus1[]', ue(v))), + when(inArray('memory_management_control_operation[]', [2]), + data('long_term_pic_num[]', ue(v))), + when(inArray('memory_management_control_operation[]', [3, 6]), + data('long_term_frame_idx[]', ue(v))), + when(inArray('memory_management_control_operation[]', [4]), + data('max_long_term_frame_idx_plus1[]', ue(v))) + ]))) + ])) +]); + +const sliceHeader = list([ + data('first_mb_in_slice', ue(v)), + data('slice_type', ue(v)), + data('pic_parameter_set_id', ue(v)), + when(equals('separate_colour_plane_flag', 1), + data('colour_plane_id', u(2))), + data('frame_num', u(frameNumBits)), + when(equals('frame_mbs_only_flag', 0), + list([ + data('field_pic_flag', u(1)), + when(equals('field_pic_flag', 1), + data('bottom_field_flag', u(1))), + ])), + when(equals('idrPicFlag', 1), + data('idr_pic_id', ue(v))), + when(equals('pic_order_cnt_type', 0), + list([ + data('pic_order_cnt_lsb', u(picOrderCntBits)), + when(every([ + equals('bottom_field_pic_order_in_frame_present_flag', 1), + not(equals('field_pic_flag', 1)) + ]), data('delta_pic_order_cnt_bottom', se(v))) + ])), + when(every([ + equals('pic_order_cnt_type', 1), + not(equals('delta_pic_order_always_zero_flag', 1)) + ]), + list([ + data('delta_pic_order_cnt[0]', se(v)), + when(every([ + equals('bottom_field_pic_order_in_frame_present_flag', 1), + not(equals('field_pic_flag', 1)) + ]), data('delta_pic_order_cnt[1]', se(v))) + ])), + when(equals('redundant_pic_cnt_present_flag', 1), + data('redundant_pic_cnt', ue(v))), + when(inArray('slice_type', sliceType.B), + data('direct_spatial_mv_pred_flag', u(1))), + when(some([ + inArray('slice_type', sliceType.P), + inArray('slice_type', sliceType.SP), + inArray('slice_type', sliceType.B) + ]), list([ + data('num_ref_idx_active_override_flag', u(1)), + when(equals('num_ref_idx_active_override_flag', 1), + list([ + data('num_ref_idx_l0_active_minus1', ue(v)), + when(inArray('slice_type', sliceType.B), + data('num_ref_idx_l1_active_minus1', ue(v))) + ])) + ])), + when(some([ + equals('nal_unit_type', 20), + equals('nal_unit_type', 21) + ]), refPicListMvcModification), + when(every([ + not(equals('nal_unit_type', 20)), + not(equals('nal_unit_type', 21)) + ]), refPicListModification), + when(useWeightedPredictionTable, predWeightTable), + when(not(equals('nal_ref_idc', 0)), decRefPicMarking), + when(every([ + equals('entropy_coding_mode_flag', 1), + not(inArray('slice_type', sliceType.I)), + not(inArray('slice_type', sliceType.SI)) + ]), data('cabac_init_idc', ue(v))), + data('slice_qp_delta', se(v)), + when(inArray('slice_type', sliceType.SP), + data('sp_for_switch_flag', u(1))), + when(some([ + inArray('slice_type', sliceType.SP), + inArray('slice_type', sliceType.SI), + ]), data('slice_qs_delta', se(v))), + when(equals('deblocking_filter_control_present_flag', 1), + list([ + data('disable_deblocking_filter_idc', ue(v)), + when(not(equals('disable_deblocking_filter_idc', 1)), + list([ + data('slice_alpha_c0_offset_div2', se(v)), + data('slice_beta_offset_div2', se(v)), + ])) + ])), + when(every([ + not(equals('num_slice_groups_minus1', 0)), + some([ + equals('slice_group_map_type', 3), + equals('slice_group_map_type', 4), + equals('slice_group_map_type', 5), + ]) + ]), + data('slice_group_change_cycle', u(sliceGroupChangeCycleBits))) +]); + +export default sliceHeader; diff --git a/src/bit-streams/h264/slice-layer-without-partitioning.js b/src/bit-streams/h264/slice-layer-without-partitioning.js new file mode 100644 index 0000000..d8e567a --- /dev/null +++ b/src/bit-streams/h264/slice-layer-without-partitioning.js @@ -0,0 +1,13 @@ +'use strict'; + +import sliceHeader from './slice-header'; +import {start, list} from './lib/combinators'; + +const sliceLayerWithoutPartitioningCodec = start('slice_layer_without_partitioning', + list([ + sliceHeader + // TODO: slice_data + ])); + + +export default sliceLayerWithoutPartitioningCodec; diff --git a/src/bit-streams/h264/vui-parameters.js b/src/bit-streams/h264/vui-parameters.js new file mode 100644 index 0000000..fc21887 --- /dev/null +++ b/src/bit-streams/h264/vui-parameters.js @@ -0,0 +1,188 @@ +'use strict'; + +import {start, list, data} from './lib/combinators'; +import {when, equals, some} from './lib/conditionals'; +import {ue, u, val} from './lib/data-types'; + +import hdrParameters from './hdr-parameters'; + +let v = null; + +let sampleRatioCalc = list([ + /* + 1:1 + 7680x4320 16:9 frame without horizontal overscan + 3840x2160 16:9 frame without horizontal overscan + 1280x720 16:9 frame without horizontal overscan + 1920x1080 16:9 frame without horizontal overscan (cropped from 1920x1088) + 640x480 4:3 frame without horizontal overscan + */ + when(equals('aspect_ratio_idc', 1), + data('sample_ratio', val(1))), + /* + 12:11 + 720x576 4:3 frame with horizontal overscan + 352x288 4:3 frame without horizontal overscan + */ + when(equals('aspect_ratio_idc', 2), + data('sample_ratio', val(12 / 11))), + /* + 10:11 + 720x480 4:3 frame with horizontal overscan + 352x240 4:3 frame without horizontal overscan + */ + when(equals('aspect_ratio_idc', 3), + data('sample_ratio', val(10 / 11))), + /* + 16:11 + 720x576 16:9 frame with horizontal overscan + 528x576 4:3 frame without horizontal overscan + */ + when(equals('aspect_ratio_idc', 4), + data('sample_ratio', val(16 / 11))), + /* + 40:33 + 720x480 16:9 frame with horizontal overscan + 528x480 4:3 frame without horizontal overscan + */ + when(equals('aspect_ratio_idc', 5), + data('sample_ratio', val(40 / 33))), + /* + 24:11 + 352x576 4:3 frame without horizontal overscan + 480x576 16:9 frame with horizontal overscan + */ + when(equals('aspect_ratio_idc', 6), + data('sample_ratio', val(24 / 11))), + /* + 20:11 + 352x480 4:3 frame without horizontal overscan + 480x480 16:9 frame with horizontal overscan + */ + when(equals('aspect_ratio_idc', 7), + data('sample_ratio', val(20 / 11))), + /* + 32:11 + 352x576 16:9 frame without horizontal overscan + */ + when(equals('aspect_ratio_idc', 8), + data('sample_ratio', val(32 / 11))), + /* + 80:33 + 352x480 16:9 frame without horizontal overscan + */ + when(equals('aspect_ratio_idc', 9), + data('sample_ratio', val(80 / 33))), + /* + 18:11 + 480x576 4:3 frame with horizontal overscan + */ + when(equals('aspect_ratio_idc', 10), + data('sample_ratio', val(18 / 11))), + /* + 15:11 + 480x480 4:3 frame with horizontal overscan + */ + when(equals('aspect_ratio_idc', 11), + data('sample_ratio', val(15 / 11))), + /* + 64:33 + 528x576 16:9 frame with horizontal overscan + */ + when(equals('aspect_ratio_idc', 12), + data('sample_ratio', val(64 / 33))), + /* + 160:99 + 528x480 16:9 frame without horizontal overscan + */ + when(equals('aspect_ratio_idc', 13), + data('sample_ratio', val(160 / 99))), + /* + 4:3 + 1440x1080 16:9 frame without horizontal overscan + */ + when(equals('aspect_ratio_idc', 14), + data('sample_ratio', val(4 / 3))), + /* + 3:2 + 1280x1080 16:9 frame without horizontal overscan + */ + when(equals('aspect_ratio_idc', 15), + data('sample_ratio', val(3 / 2))), + /* + 2:1 + 960x1080 16:9 frame without horizontal overscan + */ + when(equals('aspect_ratio_idc', 16), + data('sample_ratio', val(2 / 1))), + /* Extended_SAR */ + when(equals('aspect_ratio_idc', 255), + list([ + data('sar_width', u(16)), + data('sar_height', u(16)), + data('sample_ratio', + val((expGolomb, output, options) => output.sar_width / output.sar_height)) + ])) +]); + +const vuiParamters = list([ + data('aspect_ratio_info_present_flag', u(1)), + when(equals('aspect_ratio_info_present_flag', 1), + list([ + data('aspect_ratio_idc', u(8)), + sampleRatioCalc, + ])), + data('overscan_info_present_flag', u(1)), + when(equals('overscan_info_present_flag', 1), + data('overscan_appropriate_flag', u(1))), + data('video_signal_type_present_flag', u(1)), + when(equals('video_signal_type_present_flag', 1), + list([ + data('video_format', u(3)), + data('video_full_range_flag', u(1)), + data('colour_description_present_flag', u(1)), + when(equals('colour_description_present_flag', 1), + list([ + data('colour_primaries', u(8)), + data('transfer_characteristics', u(8)), + data('matrix_coefficients', u(8)) + ])) + ])), + data('chroma_loc_info_present_flag', u(1)), + when(equals('chroma_loc_info_present_flag', 1), + list([ + data('chroma_sample_loc_type_top_field', ue(v)), + data('chroma_sample_loc_type_bottom_field', ue(v)) + ])), + data('timing_info_present_flag', u(1)), + when(equals('timing_info_present_flag', 1), + list([ + data('num_units_in_tick', u(32)), + data('time_scale', u(32)), + data('fixed_frame_rate_flag', u(1)) + ])), + data('nal_hrd_parameters_present_flag', u(1)), + when(equals('nal_hrd_parameters_present_flag', 1), hdrParameters), + data('vcl_hrd_parameters_present_flag', u(1)), + when(equals('vcl_hrd_parameters_present_flag', 1), hdrParameters), + when( + some([ + equals('nal_hrd_parameters_present_flag', 1), + equals('vcl_hrd_parameters_present_flag', 1) + ]), + data('low_delay_hrd_flag', u(1))), + data('pic_struct_present_flag', u(1)), + data('bitstream_restriction_flag', u(1)), + when(equals('bitstream_restriction_flag', 1), + list([ + data('motion_vectors_over_pic_boundaries_flag', u(1)), + data('max_bytes_per_pic_denom', ue(v)), + data('max_bits_per_mb_denom', ue(v)), + data('log2_max_mv_length_horizontal', ue(v)), + data('log2_max_mv_length_vertical', ue(v)), + data('max_num_reorder_frames', ue(v)), + data('max_dec_frame_buffering', ue(v)) + ])) +]); + +export default vuiParamters; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..72dbc41 --- /dev/null +++ b/src/index.js @@ -0,0 +1,13 @@ +import h264Codecs from './bit-streams/h264'; + +import mp4Inspector from './inspectors'; + +const thumbCoil = { + h264Codecs, + mp4Inspector +}; + +// Include the version number. +thumbCoil.VERSION = '__VERSION__'; + +export default thumbCoil; diff --git a/src/inspectors/index.js b/src/inspectors/index.js new file mode 100644 index 0000000..68860e1 --- /dev/null +++ b/src/inspectors/index.js @@ -0,0 +1,9 @@ +import {inspect, textify, domify} from './mp4'; + +const mp4Inspector = { + inspect, + textify, + domify +}; + +export default mp4Inspector; diff --git a/src/inspectors/mp4.js b/src/inspectors/mp4.js new file mode 100644 index 0000000..e77e62d --- /dev/null +++ b/src/inspectors/mp4.js @@ -0,0 +1,1112 @@ +'use strict'; + +import { + discardEmulationPrevention, + picParameterSet, + seqParameterSet, + sliceLayerWithoutPartitioning, + accessUnitDelimiter +} from '../bit-streams/h264'; + + /** + * Returns the string representation of an ASCII encoded four byte buffer. + * @param buffer {Uint8Array} a four-byte buffer to translate + * @return {string} the corresponding string + */ +const parseType = function(buffer) { + var result = ''; + result += String.fromCharCode(buffer[0]); + result += String.fromCharCode(buffer[1]); + result += String.fromCharCode(buffer[2]); + result += String.fromCharCode(buffer[3]); + return result; +}; + +const parseMp4Date = function(seconds) { + return new Date(seconds * 1000 - 2082844800000); +}; + +const parseSampleFlags = function(flags) { + return { + isLeading: (flags[0] & 0x0c) >>> 2, + dependsOn: flags[0] & 0x03, + isDependedOn: (flags[1] & 0xc0) >>> 6, + hasRedundancy: (flags[1] & 0x30) >>> 4, + paddingValue: (flags[1] & 0x0e) >>> 1, + isNonSyncSample: flags[1] & 0x01, + degradationPriority: (flags[2] << 8) | flags[3] + }; +}; + +let lastSPS; +let lastPPS; +let lastOptions; + +const mergePS = function (a, b) { + var newObj = {}; + + if (a) { + Object.keys(a).forEach(function (key) { + newObj[key] = a[key]; + }); + } + + if (b) { + Object.keys(b).forEach(function (key) { + newObj[key] = b[key]; + }); + } + + return newObj; +}; + +const nalParse = function(avcStream) { + var + avcView = new DataView(avcStream.buffer, avcStream.byteOffset, avcStream.byteLength), + result = [], + nalData, + i, + length; + + for (i = 0; i + 4 < avcStream.length; i += length) { + length = avcView.getUint32(i); + i += 4; + + // bail if this doesn't appear to be an H264 stream + if (length <= 0) { + result.push({ + type: 'MALFORMED-DATA' + }); + continue; + } + if (length > avcStream.length) { + result.push({ + type: 'UNKNOWN MDAT DATA' + }); + return; + } + + if (length > 1) { + nalData = discardEmulationPrevention(avcStream.subarray(i + 1, i + length)); + } + var nalUnitType = (avcStream[i] & 0x1F); + var nalRefIdc = (avcStream[i] & 0x60) >>> 5; + + if (lastOptions) { + lastOptions.nal_unit_type = nalUnitType; + lastOptions.nal_ref_idc = nalRefIdc; + } + + switch (nalUnitType) { + case 0x01: + var nalObject = sliceLayerWithoutPartitioning.decode(nalData, lastOptions); + nalObject.type = 'slice_layer_without_partitioning_rbsp'; + nalObject.nal_ref_idc = nalRefIdc; + nalObject.size = nalData.length; + result.push(nalObject); + break; + case 0x02: + result.push({ + type: 'slice_data_partition_a_layer_rbsp', + size: nalData.length + }); + break; + case 0x03: + result.push({ + type: 'slice_data_partition_b_layer_rbsp', + size: nalData.length}); + break; + case 0x04: + result.push({ + type: 'slice_data_partition_c_layer_rbsp', + size: nalData.length + }); + break; + case 0x05: + var newOptions = mergePS(lastOptions, {idrPicFlag: 1}); + var nalObject = sliceLayerWithoutPartitioning.decode(nalData, newOptions); + nalObject.type = 'slice_layer_without_partitioning_rbsp_idr'; + nalObject.nal_ref_idc = nalRefIdc; + nalObject.size = nalData.length; + result.push(nalObject); + break; + case 0x06: + result.push({ + type: 'sei_rbsp', + size: nalData.length + }); + break; + case 0x07: + lastSPS = seqParameterSet.decode(nalData); + lastOptions = mergePS(lastPPS, lastSPS); + lastSPS.type = 'seq_parameter_set_rbsp'; + lastSPS.size = nalData.length; + result.push(lastSPS); + break; + case 0x08: + lastPPS = picParameterSet.decode(nalData); + lastOptions = mergePS(lastPPS, lastSPS); + lastPPS.type = 'pic_parameter_set_rbsp'; + lastPPS.size = nalData.length; + result.push(lastPPS); + break; + case 0x09: + var nalObject = accessUnitDelimiter.decode(nalData); + nalObject.type = 'access_unit_delimiter_rbsp'; + nalObject.size = nalData.length; + result.push(nalObject); + break; + case 0x0A: + result.push({ + type: 'end_of_seq_rbsp', + size: nalData.length}); + break; + case 0x0B: + result.push({ + type: 'end_of_stream_rbsp', + size: nalData.length}); + break; + case 0x0C: + result.push({ + type: 'filler_data_rbsp', + size: nalData.length + }); + break; + case 0x0D: + result.push({ + type: 'seq_parameter_set_extension_rbsp', + size: nalData.length + }); + break; + case 0x0E: + result.push({ + type: 'prefix_nal_unit_rbsp', + size: nalData.length + }); + break; + case 0x0F: + result.push({ + type: 'subset_seq_parameter_set_rbsp', + size: nalData.length + }); + break; + case 0x10: + result.push({ + type: 'depth_parameter_set_rbsp', + size: nalData.length + }); + break; + case 0x13: + result.push({ + type: 'slice_layer_without_partitioning_rbsp_aux', + size: nalData.length + }); + break; + case 0x14: + case 0x15: + result.push({ + type: 'slice_layer_extension_rbsp', + size: nalData.length + }); + break; + default: + result.push({ + type: 'INVALID NAL-UNIT-TYPE - ' + nalUnitType, + size: nalData.length + }); + break; + } + } + return result; +}; + +// registry of handlers for individual mp4 box types +const parse = { + // codingname, not a first-class box type. stsd entries share the + // same format as real boxes so the parsing infrastructure can be + // shared + avc1: function(data) { + var view = new DataView(data.buffer, data.byteOffset, data.byteLength); + return { + dataReferenceIndex: view.getUint16(6), + width: view.getUint16(24), + height: view.getUint16(26), + horizresolution: view.getUint16(28) + (view.getUint16(30) / 16), + vertresolution: view.getUint16(32) + (view.getUint16(34) / 16), + frameCount: view.getUint16(40), + depth: view.getUint16(74), + config: inspectMp4(data.subarray(78, data.byteLength)) + }; + }, + avcC: function(data) { + var + view = new DataView(data.buffer, data.byteOffset, data.byteLength), + result = { + configurationVersion: data[0], + avcProfileIndication: data[1], + profileCompatibility: data[2], + avcLevelIndication: data[3], + lengthSizeMinusOne: data[4] & 0x03, + sps: [], + pps: [] + }, + numOfSequenceParameterSets = data[5] & 0x1f, + numOfPictureParameterSets, + nalSize, + offset, + i; + + // iterate past any SPSs + offset = 6; + for (i = 0; i < numOfSequenceParameterSets; i++) { + nalSize = view.getUint16(offset); + offset += 2; + var nalData = discardEmulationPrevention(new Uint8Array(data.subarray(offset + 1, offset + nalSize))); + console.log(nalData); + lastSPS = seqParameterSet.decode(nalData); + lastOptions = mergePS(lastPPS, lastSPS); + result.sps.push(lastSPS); + offset += nalSize; + } + // iterate past any PPSs + numOfPictureParameterSets = data[offset]; + offset++; + for (i = 0; i < numOfPictureParameterSets; i++) { + nalSize = view.getUint16(offset); + offset += 2; + var nalData = discardEmulationPrevention(new Uint8Array(data.subarray(offset + 1, offset + nalSize))); + console.log(nalData); + lastPPS = picParameterSet.decode(nalData); + lastOptions = mergePS(lastPPS, lastSPS); + result.pps.push(lastPPS); + offset += nalSize; + } + return result; + }, + btrt: function(data) { + var view = new DataView(data.buffer, data.byteOffset, data.byteLength); + return { + bufferSizeDB: view.getUint32(0), + maxBitrate: view.getUint32(4), + avgBitrate: view.getUint32(8) + }; + }, + esds: function(data) { + return { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)), + esId: (data[6] << 8) | data[7], + streamPriority: data[8] & 0x1f, + decoderConfig: { + objectProfileIndication: data[11], + streamType: (data[12] >>> 2) & 0x3f, + bufferSize: (data[13] << 16) | (data[14] << 8) | data[15], + maxBitrate: (data[16] << 24) | + (data[17] << 16) | + (data[18] << 8) | + data[19], + avgBitrate: (data[20] << 24) | + (data[21] << 16) | + (data[22] << 8) | + data[23], + decoderConfigDescriptor: { + tag: data[24], + length: data[25], + audioObjectType: (data[26] >>> 3) & 0x1f, + samplingFrequencyIndex: ((data[26] & 0x07) << 1) | + ((data[27] >>> 7) & 0x01), + channelConfiguration: (data[27] >>> 3) & 0x0f + } + } + }; + }, + ftyp: function(data) { + var + view = new DataView(data.buffer, data.byteOffset, data.byteLength), + result = { + majorBrand: parseType(data.subarray(0, 4)), + minorVersion: view.getUint32(4), + compatibleBrands: [] + }, + i = 8; + while (i < data.byteLength) { + result.compatibleBrands.push(parseType(data.subarray(i, i + 4))); + i += 4; + } + return result; + }, + dinf: function(data) { + return { + boxes: inspectMp4(data) + }; + }, + dref: function(data) { + return { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)), + dataReferences: inspectMp4(data.subarray(8)) + }; + }, + hdlr: function(data) { + var + view = new DataView(data.buffer, data.byteOffset, data.byteLength), + result = { + version: view.getUint8(0), + flags: new Uint8Array(data.subarray(1, 4)), + handlerType: parseType(data.subarray(8, 12)), + name: '' + }, + i = 8; + + // parse out the name field + for (i = 24; i < data.byteLength; i++) { + if (data[i] === 0x00) { + // the name field is null-terminated + i++; + break; + } + result.name += String.fromCharCode(data[i]); + } + // decode UTF-8 to javascript's internal representation + // see http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html + result.name = decodeURIComponent(global.escape(result.name)); + + return result; + }, + mdat: function(data) { + return { + byteLength: data.byteLength, + nals: nalParse(data) + }; + }, + mdhd: function(data) { + var + view = new DataView(data.buffer, data.byteOffset, data.byteLength), + i = 4, + language, + result = { + version: view.getUint8(0), + flags: new Uint8Array(data.subarray(1, 4)), + language: '' + }; + if (result.version === 1) { + i += 4; + result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes + i += 8; + result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes + i += 4; + result.timescale = view.getUint32(i); + i += 8; + result.duration = view.getUint32(i); // truncating top 4 bytes + } else { + result.creationTime = parseMp4Date(view.getUint32(i)); + i += 4; + result.modificationTime = parseMp4Date(view.getUint32(i)); + i += 4; + result.timescale = view.getUint32(i); + i += 4; + result.duration = view.getUint32(i); + } + i += 4; + // language is stored as an ISO-639-2/T code in an array of three 5-bit fields + // each field is the packed difference between its ASCII value and 0x60 + language = view.getUint16(i); + result.language += String.fromCharCode((language >> 10) + 0x60); + result.language += String.fromCharCode(((language & 0x03c0) >> 5) + 0x60); + result.language += String.fromCharCode((language & 0x1f) + 0x60); + + return result; + }, + mdia: function(data) { + return { + boxes: inspectMp4(data) + }; + }, + mfhd: function(data) { + return { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)), + sequenceNumber: (data[4] << 24) | + (data[5] << 16) | + (data[6] << 8) | + (data[7]) + }; + }, + minf: function(data) { + return { + boxes: inspectMp4(data) + }; + }, + // codingname, not a first-class box type. stsd entries share the + // same format as real boxes so the parsing infrastructure can be + // shared + mp4a: function(data) { + var + view = new DataView(data.buffer, data.byteOffset, data.byteLength), + result = { + // 6 bytes reserved + dataReferenceIndex: view.getUint16(6), + // 4 + 4 bytes reserved + channelcount: view.getUint16(16), + samplesize: view.getUint16(18), + // 2 bytes pre_defined + // 2 bytes reserved + samplerate: view.getUint16(24) + (view.getUint16(26) / 65536) + }; + + // if there are more bytes to process, assume this is an ISO/IEC + // 14496-14 MP4AudioSampleEntry and parse the ESDBox + if (data.byteLength > 28) { + result.streamDescriptor = inspectMp4(data.subarray(28))[0]; + } + return result; + }, + moof: function(data) { + return { + boxes: inspectMp4(data) + }; + }, + moov: function(data) { + return { + boxes: inspectMp4(data) + }; + }, + mvex: function(data) { + return { + boxes: inspectMp4(data) + }; + }, + mvhd: function(data) { + var + view = new DataView(data.buffer, data.byteOffset, data.byteLength), + i = 4, + result = { + version: view.getUint8(0), + flags: new Uint8Array(data.subarray(1, 4)) + }; + + if (result.version === 1) { + i += 4; + result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes + i += 8; + result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes + i += 4; + result.timescale = view.getUint32(i); + i += 8; + result.duration = view.getUint32(i); // truncating top 4 bytes + } else { + result.creationTime = parseMp4Date(view.getUint32(i)); + i += 4; + result.modificationTime = parseMp4Date(view.getUint32(i)); + i += 4; + result.timescale = view.getUint32(i); + i += 4; + result.duration = view.getUint32(i); + } + i += 4; + + // convert fixed-point, base 16 back to a number + result.rate = view.getUint16(i) + (view.getUint16(i + 2) / 16); + i += 4; + result.volume = view.getUint8(i) + (view.getUint8(i + 1) / 8); + i += 2; + i += 2; + i += 2 * 4; + result.matrix = new Uint32Array(data.subarray(i, i + (9 * 4))); + i += 9 * 4; + i += 6 * 4; + result.nextTrackId = view.getUint32(i); + return result; + }, + pdin: function(data) { + var view = new DataView(data.buffer, data.byteOffset, data.byteLength); + return { + version: view.getUint8(0), + flags: new Uint8Array(data.subarray(1, 4)), + rate: view.getUint32(4), + initialDelay: view.getUint32(8) + }; + }, + sdtp: function(data) { + var + result = { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)), + samples: [] + }, i; + + for (i = 4; i < data.byteLength; i++) { + result.samples.push({ + dependsOn: (data[i] & 0x30) >> 4, + isDependedOn: (data[i] & 0x0c) >> 2, + hasRedundancy: data[i] & 0x03 + }); + } + return result; + }, + sidx: function(data) { + var view = new DataView(data.buffer, data.byteOffset, data.byteLength), + result = { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)), + references: [], + referenceId: view.getUint32(4), + timescale: view.getUint32(8), + earliestPresentationTime: view.getUint32(12), + firstOffset: view.getUint32(16) + }, + referenceCount = view.getUint16(22), + i; + + for (i = 24; referenceCount; i += 12, referenceCount--) { + result.references.push({ + referenceType: (data[i] & 0x80) >>> 7, + referencedSize: view.getUint32(i) & 0x7FFFFFFF, + subsegmentDuration: view.getUint32(i + 4), + startsWithSap: !!(data[i + 8] & 0x80), + sapType: (data[i + 8] & 0x70) >>> 4, + sapDeltaTime: view.getUint32(i + 8) & 0x0FFFFFFF + }); + } + + return result; + }, + smhd: function(data) { + return { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)), + balance: data[4] + (data[5] / 256) + }; + }, + stbl: function(data) { + return { + boxes: inspectMp4(data) + }; + }, + stco: function(data) { + var + view = new DataView(data.buffer, data.byteOffset, data.byteLength), + result = { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)), + chunkOffsets: [] + }, + entryCount = view.getUint32(4), + i; + for (i = 8; entryCount; i += 4, entryCount--) { + result.chunkOffsets.push(view.getUint32(i)); + } + return result; + }, + stsc: function(data) { + var + view = new DataView(data.buffer, data.byteOffset, data.byteLength), + entryCount = view.getUint32(4), + result = { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)), + sampleToChunks: [] + }, + i; + for (i = 8; entryCount; i += 12, entryCount--) { + result.sampleToChunks.push({ + firstChunk: view.getUint32(i), + samplesPerChunk: view.getUint32(i + 4), + sampleDescriptionIndex: view.getUint32(i + 8) + }); + } + return result; + }, + stsd: function(data) { + return { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)), + sampleDescriptions: inspectMp4(data.subarray(8)) + }; + }, + stsz: function(data) { + var + view = new DataView(data.buffer, data.byteOffset, data.byteLength), + result = { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)), + sampleSize: view.getUint32(4), + entries: [] + }, + i; + for (i = 12; i < data.byteLength; i += 4) { + result.entries.push(view.getUint32(i)); + } + return result; + }, + stts: function(data) { + var + view = new DataView(data.buffer, data.byteOffset, data.byteLength), + result = { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)), + timeToSamples: [] + }, + entryCount = view.getUint32(4), + i; + + for (i = 8; entryCount; i += 8, entryCount--) { + result.timeToSamples.push({ + sampleCount: view.getUint32(i), + sampleDelta: view.getUint32(i + 4) + }); + } + return result; + }, + styp: function(data) { + return parse.ftyp(data); + }, + tfdt: function(data) { + var result = { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)), + baseMediaDecodeTime: data[4] << 24 | data[5] << 16 | data[6] << 8 | data[7] + }; + if (result.version === 1) { + result.baseMediaDecodeTime *= Math.pow(2, 32); + result.baseMediaDecodeTime += data[8] << 24 | data[9] << 16 | data[10] << 8 | data[11]; + } + return result; + }, + tfhd: function(data) { + var + view = new DataView(data.buffer, data.byteOffset, data.byteLength), + result = { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)), + trackId: view.getUint32(4) + }, + baseDataOffsetPresent = result.flags[2] & 0x01, + sampleDescriptionIndexPresent = result.flags[2] & 0x02, + defaultSampleDurationPresent = result.flags[2] & 0x08, + defaultSampleSizePresent = result.flags[2] & 0x10, + defaultSampleFlagsPresent = result.flags[2] & 0x20, + i; + + i = 8; + if (baseDataOffsetPresent) { + i += 4; // truncate top 4 bytes + result.baseDataOffset = view.getUint32(12); + i += 4; + } + if (sampleDescriptionIndexPresent) { + result.sampleDescriptionIndex = view.getUint32(i); + i += 4; + } + if (defaultSampleDurationPresent) { + result.defaultSampleDuration = view.getUint32(i); + i += 4; + } + if (defaultSampleSizePresent) { + result.defaultSampleSize = view.getUint32(i); + i += 4; + } + if (defaultSampleFlagsPresent) { + result.defaultSampleFlags = view.getUint32(i); + } + return result; + }, + tkhd: function(data) { + var + view = new DataView(data.buffer, data.byteOffset, data.byteLength), + i = 4, + result = { + version: view.getUint8(0), + flags: new Uint8Array(data.subarray(1, 4)) + }; + if (result.version === 1) { + i += 4; + result.creationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes + i += 8; + result.modificationTime = parseMp4Date(view.getUint32(i)); // truncating top 4 bytes + i += 4; + result.trackId = view.getUint32(i); + i += 4; + i += 8; + result.duration = view.getUint32(i); // truncating top 4 bytes + } else { + result.creationTime = parseMp4Date(view.getUint32(i)); + i += 4; + result.modificationTime = parseMp4Date(view.getUint32(i)); + i += 4; + result.trackId = view.getUint32(i); + i += 4; + i += 4; + result.duration = view.getUint32(i); + } + i += 4; + i += 2 * 4; + result.layer = view.getUint16(i); + i += 2; + result.alternateGroup = view.getUint16(i); + i += 2; + // convert fixed-point, base 16 back to a number + result.volume = view.getUint8(i) + (view.getUint8(i + 1) / 8); + i += 2; + i += 2; + result.matrix = new Uint32Array(data.subarray(i, i + (9 * 4))); + i += 9 * 4; + result.width = view.getUint16(i) + (view.getUint16(i + 2) / 16); + i += 4; + result.height = view.getUint16(i) + (view.getUint16(i + 2) / 16); + return result; + }, + traf: function(data) { + return { + boxes: inspectMp4(data) + }; + }, + trak: function(data) { + return { + boxes: inspectMp4(data) + }; + }, + trex: function(data) { + var view = new DataView(data.buffer, data.byteOffset, data.byteLength); + return { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)), + trackId: view.getUint32(4), + defaultSampleDescriptionIndex: view.getUint32(8), + defaultSampleDuration: view.getUint32(12), + defaultSampleSize: view.getUint32(16), + sampleDependsOn: data[20] & 0x03, + sampleIsDependedOn: (data[21] & 0xc0) >> 6, + sampleHasRedundancy: (data[21] & 0x30) >> 4, + samplePaddingValue: (data[21] & 0x0e) >> 1, + sampleIsDifferenceSample: !!(data[21] & 0x01), + sampleDegradationPriority: view.getUint16(22) + }; + }, + trun: function(data) { + var + result = { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)), + samples: [] + }, + view = new DataView(data.buffer, data.byteOffset, data.byteLength), + dataOffsetPresent = result.flags[2] & 0x01, + firstSampleFlagsPresent = result.flags[2] & 0x04, + sampleDurationPresent = result.flags[1] & 0x01, + sampleSizePresent = result.flags[1] & 0x02, + sampleFlagsPresent = result.flags[1] & 0x04, + sampleCompositionTimeOffsetPresent = result.flags[1] & 0x08, + sampleCount = view.getUint32(4), + offset = 8, + sample; + + if (dataOffsetPresent) { + result.dataOffset = view.getUint32(offset); + offset += 4; + } + + if (firstSampleFlagsPresent && sampleCount) { + sample = { + flags: parseSampleFlags(data.subarray(offset, offset + 4)) + }; + offset += 4; + if (sampleDurationPresent) { + sample.duration = view.getUint32(offset); + offset += 4; + } + if (sampleSizePresent) { + sample.size = view.getUint32(offset); + offset += 4; + } + if (sampleCompositionTimeOffsetPresent) { + sample.compositionTimeOffset = view.getUint32(offset); + offset += 4; + } + result.samples.push(sample); + sampleCount--; + } + + while (sampleCount--) { + sample = {}; + if (sampleDurationPresent) { + sample.duration = view.getUint32(offset); + offset += 4; + } + if (sampleSizePresent) { + sample.size = view.getUint32(offset); + offset += 4; + } + if (sampleFlagsPresent) { + sample.flags = parseSampleFlags(data.subarray(offset, offset + 4)); + offset += 4; + } + if (sampleCompositionTimeOffsetPresent) { + sample.compositionTimeOffset = view.getUint32(offset); + offset += 4; + } + result.samples.push(sample); + } + return result; + }, + 'url ': function(data) { + return { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)) + }; + }, + vmhd: function(data) { + var view = new DataView(data.buffer, data.byteOffset, data.byteLength); + return { + version: data[0], + flags: new Uint8Array(data.subarray(1, 4)), + graphicsmode: view.getUint16(4), + opcolor: new Uint16Array([view.getUint16(6), + view.getUint16(8), + view.getUint16(10)]) + }; + } +}; + + +/** + * Return a javascript array of box objects parsed from an ISO base + * media file. + * @param data {Uint8Array} the binary data of the media to be inspected + * @return {array} a javascript array of potentially nested box objects + */ +const inspectMp4 = function(data) { + var + i = 0, + result = [], + view, + size, + type, + end, + box, + seenMOOV = false, + pendingMDAT = null; + + // Convert data from Uint8Array to ArrayBuffer, to follow Dataview API + var ab = new ArrayBuffer(data.length); + var v = new Uint8Array(ab); + for (var z = 0; z < data.length; ++z) { + v[z] = data[z]; + } + view = new DataView(ab); + + + while (i < data.byteLength) { + // parse box data + size = view.getUint32(i); + type = parseType(data.subarray(i + 4, i + 8)); + end = size > 1 ? i + size : data.byteLength; + + if (type === 'moov') { + seenMOOV = true; + } + + if (type === 'mdat' && !seenMOOV) { + pendingMDAT = data.subarray(i + 8, end); + } else { + // parse type-specific data + box = (parse[type] || function(data) { + return { + data: data + }; + })(data.subarray(i + 8, end)); + box.size = size; + box.type = type; + // store this box and move to the next + result.push(box); + } + + if (pendingMDAT && seenMOOV) { + box = parse['mdat'](pendingMDAT); + box.size = pendingMDAT.byteLength; + box.type = 'mdat'; + // store this box and move to the next + result.push(box); + pendingMDAT = null; + } + + i = end; + } + return result; +}; + +/** + * Returns a textual representation of the javascript represtentation + * of an MP4 file. You can use it as an alternative to + * JSON.stringify() to compare inspected MP4s. + * @param inspectedMp4 {array} the parsed array of boxes in an MP4 + * file + * @param depth {number} (optional) the number of ancestor boxes of + * the elements of inspectedMp4. Assumed to be zero if unspecified. + * @return {string} a text representation of the parsed MP4 + */ +const textifyMp4 = function(inspectedMp4, depth) { + var indent; + depth = depth || 0; + indent = new Array(depth * 2 + 1).join(' '); + + // iterate over all the boxes + return inspectedMp4.map(function(box, index) { + + // list the box type first at the current indentation level + return indent + box.type + '\n' + + + // the type is already included and handle child boxes separately + Object.keys(box).filter(function(key) { + return key !== 'type' && key !== 'boxes'; + + // output all the box properties + }).map(function(key) { + var prefix = indent + ' ' + key + ': ', + value = box[key]; + + // print out raw bytes as hexademical + if (value instanceof Uint8Array || value instanceof Uint32Array) { + var bytes = Array.prototype.slice.call(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)) + .map(function(byte) { + return ' ' + ('00' + byte.toString(16)).slice(-2); + }).join('').match(/.{1,24}/g); + if (!bytes) { + return prefix + '<>'; + } + if (bytes.length === 1) { + return prefix + '<' + bytes.join('').slice(1) + '>'; + } + return prefix + '<\n' + bytes.map(function(line) { + return indent + ' ' + line; + }).join('\n') + '\n' + indent + ' >'; + } + + // stringify generic objects + return prefix + + JSON.stringify(value, null, 2) + .split('\n').map(function(line, index) { + if (index === 0) { + return line; + } + return indent + ' ' + line; + }).join('\n'); + }).join('\n') + + + // recursively textify the child boxes + (box.boxes ? '\n' + textifyMp4(box.boxes, depth + 1) : ''); + }).join('\n'); +}; + +const domifyMp4 = function (inspectedMp4) { + var topLevelObject = { + type: 'mp4', + boxes: inspectedMp4, + size: inspectedMp4.reduce((sum, box) => sum + box.size, 0) + }; + + var container = document.createElement('div'); + + domifyBox(topLevelObject, container, 1); + + return container; +}; + +/* + + + + + + + + +*/ + +const domifyBox = function (box, parentNode, depth) { + var isObject = (o) => Object.prototype.toString.call(o) === '[object Object]'; + var attributes = ['size', 'flags', 'type', 'version']; + var specialProperties = ['boxes', 'nals', 'samples']; + var objectProperties = Object.keys(box).filter((key) => { + return isObject(box[key]) || + (Array.isArray(box[key]) && isObject(box[key][0])); + }); + var propertyExclusions = + attributes + .concat(specialProperties) + .concat(objectProperties); + var subProperties = Object.keys(box).filter((key) => { + return propertyExclusions.indexOf(key) === -1; + }); + + var boxNode = document.createElement('mp4-box'); + var propertyNode = document.createElement('mp4-properties'); + var subBoxesNode = document.createElement('mp4-boxes'); + var boxTypeNode = document.createElement('mp4-box-type'); + + if (box.type) { + boxTypeNode.textContent = box.type; + + if (depth > 1) { + boxTypeNode.classList.add('collapsed'); + } + + boxNode.appendChild(boxTypeNode); + } + + attributes.forEach((key) => { + if (typeof box[key] !== 'undefined') { + boxNode.setAttribute(key, box[key]); + } + }); + + if (subProperties.length) { + subProperties.forEach((key) => { + makeProperty(key, box[key], propertyNode); + }); + boxNode.appendChild(propertyNode); + } + + if (box.boxes && box.boxes.length) { + box.boxes.forEach((subBox) => domifyBox(subBox, subBoxesNode, depth + 1)); + boxNode.appendChild(subBoxesNode); + } else if (objectProperties.length) { + objectProperties.forEach((key) => { + if (Array.isArray(box[key])) { + domifyBox({ + type: key, + boxes: box[key] + }, + subBoxesNode, + depth + 1); + } else { + domifyBox(box[key], subBoxesNode, depth + 1); + } + }); + boxNode.appendChild(subBoxesNode); + } + + parentNode.appendChild(boxNode); +}; + +const makeProperty = function (name, value, parentNode) { + var nameNode = document.createElement('mp4-name'); + var valueNode = document.createElement('mp4-value'); + var propertyNode = document.createElement('mp4-property'); + + nameNode.setAttribute('name', name); + nameNode.textContent = name; + valueNode.setAttribute('value', value); + valueNode.textContent = value; + + propertyNode.appendChild(nameNode); + propertyNode.appendChild(valueNode); + + parentNode.appendChild(propertyNode); +}; + +export default { + inspect: inspectMp4, + textify: textifyMp4, + domify: domifyMp4 +};