diff --git a/bin/big-buck-bunny.mp4 b/bin/big-buck-bunny.mp4
new file mode 100644
index 0000000..c70466a
Binary files /dev/null and b/bin/big-buck-bunny.mp4 differ
diff --git a/bin/generate-formats.js b/bin/generate-formats.js
new file mode 100755
index 0000000..52f6c2b
--- /dev/null
+++ b/bin/generate-formats.js
@@ -0,0 +1,332 @@
+#!/usr/bin/env node
+/* eslint-disable no-console */
+const shelljs = require('shelljs');
+const childProcess = require('child_process');
+const path = require('path');
+
+const baseDir = path.join(__dirname, '..', 'test', 'fixtures', 'formats');
+const DURATION = '0.01s';
+const INPUT_FILE = path.join(__dirname, 'big-buck-bunny.mp4');
+
+shelljs.rm('-rf', baseDir);
+
+const promiseSpawn = function(bin, args, options = {}) {
+ process.setMaxListeners(1000);
+
+ return new Promise((resolve, reject) => {
+ const child = childProcess.spawn(bin, args, options);
+
+ let stdout = '';
+ let stderr = '';
+ let out = '';
+
+ child.stdout.on('data', function(chunk) {
+ stdout += chunk;
+ out += chunk;
+ });
+
+ child.stderr.on('data', function(chunk) {
+ stderr += chunk;
+ out += chunk;
+ });
+
+ const kill = () => child.kill();
+
+ process.on('SIGINT', kill);
+ process.on('SIGQUIT', kill);
+ process.on('exit', kill);
+
+ child.on('close', (status) => resolve({
+ cmd: [bin].concat(args),
+ status,
+ out: out.toString(),
+ stderr: stderr.toString(),
+ stdout: stdout.toString()
+ }));
+ });
+};
+
+const ffmpeg = (args) => promiseSpawn('ffmpeg', [
+ '-hide_banner',
+ '-loglevel', 'error',
+ '-y',
+ '-i', INPUT_FILE,
+ '-t', DURATION
+].concat(args));
+
+const audioCodecs = [
+ {audioCodec: 'aac', args: ['-c:a', 'aac', '-metadata', 'title="Big Buck Bunny"']},
+ {audioCodec: 'mp4a.40.2', args: ['-c:a', 'aac']},
+ {audioCodec: 'mp4a.40.5', args: ['-c:a', 'aac', '-profile:a', 'aac_he']},
+ {audioCodec: 'mp4a.40.29', args: ['-c:a', 'aac', '-profile:a', 'aac_he_v2']},
+ {audioCodec: 'mp4a.40.34', args: ['-c:a', 'mp3']},
+ {audioCodec: 'mp3', args: ['-c:a', 'mp3', '-metadata', 'title="Big Buck Bunny"']},
+ {audioCodec: 'opus', args: ['-c:a', 'libopus']},
+ {audioCodec: 'ac-3', args: ['-c:a', 'ac3']},
+ {audioCodec: 'ec-3', args: ['-c:a', 'eac3']},
+ {audioCodec: 'vorbis', args: ['-c:a', 'libvorbis']},
+ {audioCodec: 'flac', args: ['-c:a', 'flac']},
+ {audioCodec: 'alac', args: ['-c:a', 'alac']},
+ {audioCodec: 'speex', args: ['-c:a', 'speex']}
+];
+
+const videoCodecs = [
+ // TODO: use another encoder, ffmpeg does not support codecPrivate for vp09
+ // TODO: generate more formats
+ // profile.level.depth.chroma.[color-primary].[transferchar].[matrixco].[blacklevel]
+ {videoCodec: 'vp09.01.00.00.00.00.00.20.00', args: ['-c:v', 'vp9']},
+ {videoCodec: 'vp8', args: ['-c:v', 'vp8']},
+ {videoCodec: 'theora', args: ['-c:v', 'theora']},
+ {videoCodec: 'avc1.42c00d', args: ['-c:v', 'libx264', '-profile:v', 'baseline', '-level', '1.3']},
+ {videoCodec: 'avc1.4d401e', args: ['-c:v', 'libx264', '-profile:v', 'main', '-level', '3.0']},
+ {videoCodec: 'avc1.640028', args: ['-c:v', 'libx264', '-profile:v', 'high', '-level', '4.0']},
+
+ // https://trac.ffmpeg.org/ticket/2901
+ // aka profile is first 4 bits, level is second 4 bits
+ {videoCodec: 'mp4v.20.9', args: ['-c:v', 'mpeg4', '-profile:v', '0', '-level', '9']},
+ {videoCodec: 'mp4v.20.240', args: ['-c:v', 'mpeg4', '-profile:v', '15', '-level', '0']},
+ {videoCodec: 'hvc1.1.6.H120.90', args: ['-c:v', 'libx265', '-tag:v', 'hvc1', '-x265-params', 'profile=main12:level-idc=4.0']},
+ {videoCodec: 'hev1.1.6.H150.90', args: ['-c:v', 'libx265', '-x265-params', 'profile=main12:level-idc=5.0']},
+ {videoCodec: 'hev1.1.6.L60.90', args: ['-c:v', 'libx265', '-x265-params', 'profile=main12:level-idc=4.0:no-high-tier']},
+ {videoCodec: 'hev1.1.6.H120.90', args: ['-c:v', 'libx265', '-x265-params', 'profile=main12:level-idc=4.0']},
+ {videoCodec: 'hev1.4.10.H120.9c.8', args: ['-c:v', 'libx265', '-pix_fmt', 'yuv444p10', '-x265-params', 'profile=main12:level-idc=4.0']},
+
+ // TODO: generate more av1 formats
+ {videoCodec: 'av01.0.00M.08.0.110', args: ['-strict', 'experimental', '-c:v', 'av1', '-cpu-used', '8']}
+];
+
+const buildCodecs = (changeFn) => {
+ const allCodecs = [];
+
+ const find = ({audioCodec, videoCodec}) =>
+ allCodecs.find((c) => c.audioCodec === audioCodec && c.videoCodec === videoCodec);
+
+ videoCodecs.forEach(function({args, videoCodec}) {
+ args = args.slice();
+ args.unshift('-an');
+ const changed = changeFn({args, videoCodec});
+
+ if (changed && !find(changed)) {
+ allCodecs.push(changed);
+ }
+ });
+
+ audioCodecs.forEach(function({args, audioCodec}) {
+ args = args.slice();
+ args.unshift('-vn');
+ const changed = changeFn({args, audioCodec});
+
+ if (changed && !find(changed)) {
+ allCodecs.push(changed);
+ }
+ });
+
+ videoCodecs.forEach(function(video) {
+ audioCodecs.forEach(function(audio) {
+ const changed = changeFn({
+ audioCodec: audio.audioCodec,
+ videoCodec: video.videoCodec,
+ args: video.args.slice().concat(audio.args.slice())
+ });
+
+ if (changed && !find(changed)) {
+ allCodecs.push(changed);
+ }
+ });
+ });
+
+ return allCodecs;
+};
+
+const containerCodecs = {
+ mp4: buildCodecs((c) => {
+ if (c.audioCodec && (/^(alac|flac|opus|speex)/).test(c.audioCodec)) {
+ return null;
+ }
+
+ if (c.videoCodec && (/^(vp8|theora)/).test(c.videoCodec)) {
+ return null;
+ }
+
+ return c;
+ }),
+ mov: buildCodecs((c) => {
+ if (c.audioCodec && (/^(flac|opus)/).test(c.audioCodec)) {
+ return null;
+ }
+
+ if (c.videoCodec && (/^(vp8|vp9|vp09|av01)/.test(c.videoCodec))) {
+ return null;
+ }
+
+ return c;
+ }),
+ mkv: buildCodecs((c) => {
+ // hvc1 is an mp4 only codec designation
+ if (c.videoCodec && (/^hvc1/).test(c.videoCodec)) {
+ return null;
+ }
+
+ // ffmpeg does not support codecPrivate for vp9
+ // so we can only use the base codec
+ if (c.videoCodec && (/^vp09|vp9/).test(c.videoCodec)) {
+ c.videoCodec = 'vp9';
+ }
+
+ return c;
+ }),
+ // TODO: should webm support more content types??
+ webm: buildCodecs((c) => {
+ if (c.videoCodec && !(/^(av01|vp8|vp09|vp9)/).test(c.videoCodec)) {
+ return null;
+ }
+
+ // ffmpeg does not support codecPrivate for vp9
+ // so we can only use the base codec
+ if (c.videoCodec && (/^vp09|vp9/).test(c.videoCodec)) {
+ c.videoCodec = 'vp9';
+ }
+
+ if (c.audioCodec && !(/^(vorbis|opus)/).test(c.audioCodec)) {
+ return null;
+ }
+
+ return c;
+ }),
+ avi: buildCodecs((c) => {
+ if (c.videoCodec && (/^(hvc1)/).test(c.videoCodec)) {
+ return null;
+ }
+
+ // verify that a correctly tagged avi file works
+ // ffmpeg doesn't do this...
+ if (c.videoCodec && c.videoCodec === 'hev1.4.10.H120.9c.8') {
+ c.args.push('-tag:v', 'HEVC');
+ }
+
+ if (c.audioCodec && (/^(opus|alac)/).test(c.audioCodec)) {
+ return null;
+ }
+
+ // avi does not support codec parameters
+ const match = c.videoCodec && c.videoCodec.match(/^(av01|vp09|vp9)/);
+
+ if (match && match[1]) {
+ if (match[1] === 'vp09') {
+ c.videoCodec = 'vp9';
+ } else {
+ c.videoCodec = match[1];
+ }
+ }
+
+ return c;
+ }),
+ ts: buildCodecs((c) => {
+ if (c.videoCodec && (/^(vp8|vp09|vp9|theora|hvc1|av01)/).test(c.videoCodec)) {
+ return null;
+ }
+
+ if (c.audioCodec && (/^(mp4a.40.29|mp4a.40.5|alac|vorbis|flac|speex)/).test(c.audioCodec)) {
+ return null;
+ }
+
+ // ts does not support codec parameters
+ const match = c.videoCodec && c.videoCodec.match(/^(mp4v.20)/);
+
+ if (match && match[1]) {
+ c.videoCodec = match[1];
+ }
+
+ return c;
+ }),
+ ogg: buildCodecs((c) => {
+ // ogg only supports theora/vp8 video
+ if (c.videoCodec && !(/^(vp8|theora)/).test(c.videoCodec)) {
+ return null;
+ }
+
+ // ogg only supports flac, opus, speex, vorbis audio
+ if (c.audioCodec && !(/^(flac|opus|speex|vorbis)/).test(c.audioCodec)) {
+ return null;
+ }
+
+ return c;
+ }),
+ wav: buildCodecs((c) => {
+ // wav does not support video
+ if (c.videoCodec || !c.audioCodec) {
+ return null;
+ }
+
+ if ((/^(alac|opus)/).test(c.audioCodec)) {
+ return null;
+ }
+
+ return c;
+
+ }),
+ aac: [
+ {audioCodec: 'aac', args: ['-vn', '-metadata', 'title="aac"']}
+ ],
+ mp3: [
+ {audioCodec: 'mp3', args: ['-vn', '-metadata', 'title="mp3"']}
+ ],
+ ac3: [
+ {audioCodec: 'ac-3', args: ['-vn', '-c:a', 'ac3', '-metadata', 'title="ac3"']},
+ {audioCodec: 'ec-3', args: ['-vn', '-c:a', 'eac3', '-metadata', 'title="eac3"']}
+ ],
+ flac: [
+ {audioCodec: 'flac', args: ['-vn', '-c:a', 'flac', '-metadata', 'title="flac"']}
+ ],
+ h264: buildCodecs((c) => {
+ // h264 only supports hevc video content
+ if (c.audioCodec || !c.videoCodec) {
+ return null;
+ }
+
+ if (!(/^avc1/).test(c.videoCodec)) {
+ return null;
+ }
+
+ return c;
+ }),
+ h265: buildCodecs((c) => {
+ // h265 only supports hevc video content
+ if (c.audioCodec || !c.videoCodec) {
+ return null;
+ }
+
+ if (!(/^hev1/).test(c.videoCodec)) {
+ return null;
+ }
+
+ return c;
+ })
+};
+
+let total = 0;
+
+const promises = Object.keys(containerCodecs).map((container) => {
+ const codecs = containerCodecs[container];
+ const containerPath = path.join(baseDir, container);
+
+ shelljs.mkdir('-p', containerPath);
+
+ return Promise.all(codecs.map((codec) => new Promise((resolve, reject) => {
+ const fileName = [codec.videoCodec, codec.audioCodec].filter(Boolean).join(',') + '.' + container;
+ const filePath = path.join(containerPath, fileName);
+
+ return resolve(ffmpeg([].concat(codec.args).concat([filePath])).then(function(result) {
+ if (result.status !== 0) {
+ console.log(result.cmd.join(' '));
+ console.log(`FAIL: ${fileName} ${result.out}`);
+ return;
+ }
+ total++;
+ }));
+ })));
+});
+
+Promise.all(promises).then(function(args) {
+ console.log(`Wrote ${total} files!`);
+});
diff --git a/bin/parse-blocks.js b/bin/parse-blocks.js
new file mode 100755
index 0000000..1f97a61
--- /dev/null
+++ b/bin/parse-blocks.js
@@ -0,0 +1,85 @@
+#! /usr/bin/env node
+/* eslint-disable no-console */
+const {version} = require('../package.json');
+const {parseData} = require('../dist/ebml-helpers.js');
+const {concatTypedArrays} = require('../dist/byte-helpers.js');
+const fs = require('fs');
+const path = require('path');
+
+const showHelp = function() {
+ console.log(`
+ parse-blocks [...file.webm|file.mkv]
+
+ parse blocks and output block counts for a ebml format (webm/mkv) files
+ so that we can compare counts against a reference spec.
+
+ Test files can be found at https://github.com/Matroska-Org/matroska-test-files
+
+ -h, --help print help
+ -v, --version print the version
+`);
+};
+
+const parseArgs = function(args) {
+ const options = {};
+
+ for (let i = 0; i < args.length; i++) {
+ const arg = args[i];
+
+ if ((/^--version|-v$/).test(arg)) {
+ console.log(`parse-blocks v${version}`);
+ process.exit(0);
+ } else if ((/^--help|-h$/).test(arg)) {
+ showHelp();
+ process.exit(0);
+ } else {
+ options.files = options.files || [];
+ options.files.push(arg);
+ }
+ }
+
+ return options;
+};
+
+const options = parseArgs(process.argv.slice(2));
+
+console.log();
+
+Promise.all(options.files.map(function(file) {
+ return new Promise(function(resolve, reject) {
+ const stream = fs.createReadStream(path.resolve(file));
+ let allData;
+
+ stream.on('data', (chunk) => {
+ allData = concatTypedArrays(allData, chunk);
+ });
+
+ stream.on('error', reject);
+
+ stream.on('close', () => {
+ const {blocks, tracks} = parseData(allData);
+
+ console.log(`Results for ${file}`);
+ console.log(`Tracks Found ${tracks.length}`);
+ console.log(`Blocks Found ${blocks.length}`);
+ if (blocks.length < 100) {
+ console.warn('WARNING: possible parsing issue. less than 100 blocks in file.');
+ }
+
+ const noFrames = blocks.find((b) => !b.frames.length);
+
+ if (noFrames) {
+ console.warn(`WARNING: possible parsing issue ${noFrames.length} block have no frames!`);
+ }
+ console.log();
+ resolve();
+ });
+ });
+})).then(function() {
+ console.log('All files read!');
+ console.log();
+ process.exit(0);
+}).catch(function(e) {
+ console.error(e);
+ process.exit(1);
+});
diff --git a/bin/parse-format.js b/bin/parse-format.js
new file mode 100755
index 0000000..27c2b4a
--- /dev/null
+++ b/bin/parse-format.js
@@ -0,0 +1,89 @@
+#! /usr/bin/env node
+/* eslint-disable no-console */
+const {version} = require('../package.json');
+const {parseFormatForBytes} = require('../dist/format-parser.js');
+const {concatTypedArrays} = require('../dist/byte-helpers.js');
+const fs = require('fs');
+const path = require('path');
+
+const showHelp = function() {
+ console.log(`
+ parse-format [...media-files]
+ curl -s 'some-media-ulr' | parse-format
+ wget -O - -o /dev/null 'some-media-url' | parse-format
+
+ parse containers and codecs given a media file that contains that information.
+
+ -h, --help print help
+ -v, --version print the version
+`);
+};
+
+const parseArgs = function(args) {
+ const options = {files: []};
+
+ for (let i = 0; i < args.length; i++) {
+ const arg = args[i];
+
+ if ((/^--version|-v$/).test(arg)) {
+ console.log(`parse-format v${version}`);
+ process.exit(0);
+ } else if ((/^--help|-h$/).test(arg)) {
+ showHelp();
+ process.exit(0);
+ } else {
+ options.files.push(arg);
+ }
+ }
+
+ return options;
+};
+
+const cli = function(stdin) {
+ const options = parseArgs(process.argv.slice(2));
+ const streams = [];
+
+ // if stdin was provided
+ if (stdin) {
+ streams.push({stream: process.stdin, file: 'stdin'});
+ }
+
+ options.files.forEach(function(file) {
+ streams.push({stream: fs.createReadStream(path.resolve(file)), file});
+ });
+
+ return Promise.all(streams.map(({file, stream}) => new Promise(function(resolve, reject) {
+ let allData;
+ let lastResult;
+
+ stream.on('data', (chunk) => {
+ allData = concatTypedArrays(allData, chunk);
+ lastResult = parseFormatForBytes(allData);
+
+ if (lastResult && Object.keys(lastResult.codecs).length) {
+ return stream.destroy();
+ }
+ });
+ stream.on('error', reject);
+
+ stream.on('close', () => {
+ console.log(`Results for ${file}`);
+ console.log(lastResult);
+ if (lastResult && !Object.keys(lastResult.codecs).length) {
+ console.warn('WARNING no codecs found');
+ }
+ console.log();
+ resolve();
+ });
+ }))).then(function() {
+ console.log('All files read!');
+ console.log();
+ process.exit(0);
+ }).catch(function(e) {
+ console.error(e);
+ process.exit(1);
+ });
+};
+
+// no stdin if isTTY is set
+cli(!process.stdin.isTTY ? process.stdin : null);
diff --git a/index.html b/index.html
index 242632a..56277b8 100644
--- a/index.html
+++ b/index.html
@@ -4,23 +4,17 @@
videojs-stream Demo
-
+
-