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 - + -