ts-ebml-esm
Version:
ebml decoder and encoder
469 lines (427 loc) • 14.4 kB
text/typescript
// /// <reference types="qunit"/>
import {
EBMLElementDetail,
EBMLElementValue,
MasterElement,
ChildElementsValue,
Decoder,
Encoder,
Reader,
tools
} from "./index.js";
// const Buffer = tools.Buffer;
import QUnit = require("qunit");
import empower = require("empower");
import formatter = require("power-assert-formatter");
const qunitTap = require("qunit-tap");
QUnit.config.autostart = true;
empower(QUnit.assert, formatter(), { destructive: true });
qunitTap(
QUnit,
function qunitTapCallback() {
/* eslint prefer-rest-params: off */
console.log.apply(console, arguments as any);
},
{ showSourceOnFailure: false }
);
const WEBM_FILE_LIST = [
"../matroska-test-files/test_files/test1.mkv",
"../matroska-test-files/test_files/test2.mkv",
"../matroska-test-files/test_files/test3.mkv",
// "../matroska-test-files/test_files/test4.mkv", this file is broken so not pass encoder_decoder_test
"../matroska-test-files/test_files/test5.mkv",
"../matroska-test-files/test_files/test6.mkv",
// "../matroska-test-files/test_files/test7.mkv", this file has unknown tag so cannot write file
"../matroska-test-files/test_files/test8.mkv",
"./chrome52.webm",
"./chrome59.webm",
"./chrome57.webm"
];
QUnit.module("ts-EBML");
QUnit.test("encoder-decoder", async (assert: Assert) => {
const file = "../matroska-test-files/test_files/test1.mkv";
const res = await fetch(file);
const buf = await res.arrayBuffer();
const elms = new Decoder().decode(buf);
const buf2 = new Encoder().encode(elms);
const elms2 = new Decoder().decode(buf2);
type D = EBMLElementDetail;
const tests = [
{
index: 0,
test: (elm: D) => {
assert.ok(elm.name === "EBML" && elm.type === "m" && !elm.isEnd);
}
},
{
index: 4,
test: (elm: D) => {
assert.ok(elm.name === "EBML" && elm.type === "m" && elm.isEnd);
}
},
{
index: 5,
test: (elm: D) => {
assert.ok(elm.name === "Segment" && elm.type === "m" && !elm.isEnd);
}
},
{
index: 24,
test: (elm: D) => {
assert.ok(elm.name === "Info" && elm.type === "m" && !elm.isEnd);
}
},
{
index: 25,
test: (elm: D) => {
assert.ok(
elm.name === "Duration" && elm.type === "f" && elm.value === 87336
);
}
},
{
index: 26,
test: (elm: D) => {
assert.ok(
elm.name === "MuxingApp" &&
elm.type === "8" &&
elm.value === "libebml2 v0.10.0 + libmatroska2 v0.10.1"
);
}
},
{
index: 28,
test: (elm: D) => {
assert.ok(
elm.name === "DateUTC" &&
elm.type === "d" &&
elm.value instanceof Date
);
// toISOString
assert.ok(
elm.type === "d" &&
tools.convertEBMLDateToJSDate(elm.value).getTime() ===
new Date("2010-08-21T07:23:03.000Z").getTime()
);
}
},
{
index: 29,
test: (elm: D) => {
assert.ok(elm.name === "SegmentUUID" && elm.type === "b");
if (elm.type === "b") {
const buf = new Uint8Array(
Buffer.from([
0x92, 0x2d, 0x19, 0x32, 0x0f, 0x1e, 0x13, 0xc5, 0xb5, 0x05, 0x63,
0x0a, 0xaf, 0xd8, 0x53, 0x36
])
);
const buf2 = new Uint8Array(elm.value);
assert.ok(buf.every((val, i) => buf2[i] === val));
}
}
}
];
for (const test of tests) {
test.test(elms2[test.index]);
}
});
for (const file of WEBM_FILE_LIST) {
QUnit.test("encoder-decoder:" + file, create_encoder_decoder_test(file));
}
function create_encoder_decoder_test(file: string) {
return async (assert: Assert) => {
const res = await fetch(file);
const buf = await res.arrayBuffer();
const elms = new Decoder().decode(buf);
const buf2 = new Encoder().encode(elms);
const elms2 = new Decoder().decode(buf2);
//assert.ok(buf.byteLength === buf2.byteLength, "This problem is caused by JS being unable to handle Int64.");
assert.ok(elms.length === elms2.length);
for (let i = 0; i < elms.length; i++) {
const elm = elms[i];
const elm2 = elms2[i];
assert.ok(elm.name === elm2.name);
assert.ok(elm.type === elm2.type);
if (elm.type === "m" || elm2.type === "m") {
return;
}
if (elm.type === "b" && elm2.type === "b") {
assert.ok(elm.value.length === elm2.value.length);
return;
}
assert.ok(elm.value === elm2.value);
await sleep(1);
}
};
}
QUnit.test("handwrite-encoder", (assert: Assert) => {
const tagStream: EBMLElementValue[] = [
{ name: "EBML", type: "m", isEnd: false },
{ name: "EBMLVersion", type: "u", value: 1 },
{ name: "EBMLReadVersion", type: "u", value: 1 },
{ name: "EBMLMaxIDLength", type: "u", value: 4 },
{ name: "EBMLMaxSizeLength", type: "u", value: 8 },
{ name: "DocType", type: "s", value: "webm" },
{ name: "DocTypeVersion", type: "u", value: 4 },
{ name: "DocTypeReadVersion", type: "u", value: 2 },
{ name: "EBML", type: "m", isEnd: true },
{ name: "Segment", type: "m", unknownSize: true, isEnd: false },
{ name: "SeekHead", type: "m", isEnd: false },
{ name: "SeekHead", type: "m", isEnd: true },
{ name: "Info", type: "m", isEnd: false },
{ name: "TimestampScale", type: "u", value: 1000000 },
{ name: "Info", type: "m", isEnd: true },
{ name: "Duration", type: "f", value: 0.0 },
{ name: "Cluster", type: "m", unknownSize: true, isEnd: false },
{ name: "Timestamp", type: "u", value: 1 },
{
name: "SimpleBlock",
type: "b",
value: Buffer.alloc(1024)
}
];
const binarized = tagStream.map((o: MasterElement | ChildElementsValue) =>
tools.encodeValueToBuffer(o as any)
);
const buf = new Encoder().encode(binarized);
const elms = new Decoder().decode(buf);
for (const [i, elm] of elms.entries()) {
const origin = tagStream[i];
assert.ok(elm.name === origin.name, "compare tag name");
assert.ok(elm.type === origin.type, "compare tag type");
if (elm.type === "m" || origin.type === "m") {
return;
}
if (elm.type === "b" && origin.type === "b") {
assert.ok(elm.value.length === origin.value.length, "compare tag value");
return;
}
assert.ok(elm.value === origin.value, "compare tag value");
}
});
QUnit.module("Reader");
const MEDIA_RECORDER_WEBM_FILE_LIST: Array<[string, number]> = [
["./chrome52.webm", 50],
["./chrome59.webm", 60],
["./chrome57.webm", 7],
// last2timestamp(video, audio): ((7.493s, 7.552s), (7.493s, 7.552s))
// Chrome57: 7.612s ~= 7.611s = 7.552s + (7.552s - 7.493s) // ???
// Firefox53: 7.552s = 7.552s + (7.552s - 7.552s) // shit!
// Reader: 7.611s = 7.552s + (7.552s - 7.493s)
["./firefox55nightly.webm", 8],
// last2timestamp(video, audio): ((8.567s, 8.590s), (8.626s, 8.646s)), CodecDelay(audio): 6.500ms
// Chrome57: 8.659s ~= 8.6595s = 8.646s + (8.646s - 8.626s) - 6.500ms
// Firefox53: 8.666s = 8.646s + (8.646s - 8.626s)
// Reader: 8.6595s = 8.646s + (8.646s - 8.626s) - 6.500ms
["./firefox53.webm", 10]
// Chrome57: 10.019s, Firefox53: 10.026s, Reader: 9.967s
// last2timestamp(video, audio): ((9.932s, 9.967s), (9.986s, 10.006s)), CodecDelay(audio): 6.500ms
// Chrome57: 10.019s ~= 10.0195s = 10.006s + (10.006s - 9.986s) - 6.500ms
// Firefox53: 10.026s = 10.006s + (10.006s - 9.986s)
// Reader: 10.0195s = 10.006s + (10.006s - 9.986s) - 6.500ms
];
for (const [file, _expectDur] of MEDIA_RECORDER_WEBM_FILE_LIST) {
QUnit.test("create_webp_test:" + file, create_webp_test(file));
}
function create_webp_test(file: string) {
return async (assert: Assert) => {
const res = await fetch(file);
const webm_buf = await res.arrayBuffer();
const elms = new Decoder().decode(webm_buf);
const WebPs = tools.WebPFrameFilter(elms);
for (const WebP of WebPs) {
const src = URL.createObjectURL(WebP);
try {
const img = await fetchImage(src);
assert.ok(
img.width > 0 && img.height > 0,
"size:" + img.width + "x" + img.height
);
} catch (err) {
assert.notOk(err, "webp load failre");
}
URL.revokeObjectURL(src);
}
};
}
for (const [file, expectDur] of MEDIA_RECORDER_WEBM_FILE_LIST) {
QUnit.test(
"create_convert_to_seekable_test:" + file,
create_convert_to_seekable_test(file, expectDur)
);
}
function create_convert_to_seekable_test(file: string, expectDur: number) {
return async (assert: Assert) => {
const decoder = new Decoder();
const reader = new Reader();
//reader.logging = true;
const res = await fetch(file);
const webm_buf = await res.arrayBuffer();
console.info("analasis unseekable original ebml tree");
const elms = decoder.decode(webm_buf);
for (const elm of elms) {
reader.read(elm);
}
reader.stop();
console.info("convert to seekable file");
assert.ok(reader.metadatas[0].name === "EBML");
assert.ok(reader.metadatas.length > 0);
const sec = (reader.duration * reader.timestampScale) / 1000 / 1000 / 1000;
assert.ok(expectDur - 1 < sec && sec < expectDur + 1);
const refinedMetadataBuf = tools.makeMetadataSeekable(
reader.metadatas,
reader.duration,
reader.cues
);
const body = webm_buf.slice(reader.metadataSize);
assert.ok(refinedMetadataBuf.byteLength - reader.metadataSize > 0);
assert.ok(webm_buf.byteLength === reader.metadataSize + body.byteLength);
console.info("check duration");
const raw_webM = new Blob([webm_buf], { type: "video/webm" });
const refinedWebM = new Blob([refinedMetadataBuf, body], {
type: "video/webm"
});
try {
const raw_video = await fetchVideo(URL.createObjectURL(raw_webM));
const refined_video = await fetchVideo(URL.createObjectURL(refinedWebM));
if (!navigator.userAgent.includes("Firefox")) {
assert.ok(
!Number.isFinite(raw_video.duration),
"media recorder webm duration is not finite"
);
}
assert.ok(
Number.isFinite(refined_video.duration),
"refined webm duration is finite"
);
await sleep(100);
const wait = new Promise((resolve, reject) => {
raw_video.onseeked = resolve;
raw_video.onerror = reject;
});
/* eslint require-atomic-updates: off */
raw_video.currentTime = 7 * 24 * 60 * 60;
await wait;
// duration sec is different each browsers
assert.ok(Math.abs(raw_video.duration - refined_video.duration) < 0.25);
} catch (err) {
assert.notOk(err);
}
if (reader.logging) {
// for debug
console.info("put seekable ebml tree");
const refinedBuf = await readAsArrayBuffer(refinedWebM);
const refinedElms = new Decoder().decode(refinedBuf);
const _reader = new Reader();
_reader.logging = true;
for (const elm of refinedElms) {
_reader.read(elm);
}
_reader.stop();
}
};
}
for (const [file, _expectDur] of MEDIA_RECORDER_WEBM_FILE_LIST) {
QUnit.test(
"create_recorder_helper_test:" + file,
create_recorder_helper_test(file)
);
}
function create_recorder_helper_test(file: string) {
return async (assert: Assert) => {
const decoder = new Decoder();
const reader = new Reader();
let last_sec = 0;
reader.addListener("duration", ({ timestampScale, duration }) => {
const sec = (duration * timestampScale) / 1000 / 1000 / 1000;
assert.ok(Number.isFinite(sec), "duration:" + sec + "sec");
assert.ok(sec > last_sec);
last_sec = sec;
});
let metadata_loaded = false;
reader.addListener("metadata", ({ metadataSize, data }) => {
assert.ok(metadataSize > 0);
assert.ok(data.length > 0);
assert.ok(data[0].name === "EBML");
metadata_loaded = true;
});
let cluster_num = 0;
let last_timestamp = -1;
reader.addListener("cluster", (ev) => {
// cluster chunk test
const { data, timestamp } = ev;
assert.ok(Number.isFinite(timestamp), "cluster.timestamp:" + timestamp);
assert.ok(data.length > 0, "cluster.length:" + data.length);
const assertion = data.every(
(elm) =>
elm.name === "Cluster" ||
elm.name === "Timestamp" ||
elm.name === "SimpleBlock"
);
assert.ok(assertion, "element check");
assert.ok(timestamp > last_timestamp);
cluster_num += 1;
last_timestamp = timestamp;
});
const res = await fetch(file);
const webm_buf = await res.arrayBuffer();
const elms = decoder.decode(webm_buf);
for (const elm of elms) {
reader.read(elm);
}
reader.stop();
assert.ok(last_sec > 0);
assert.ok(metadata_loaded);
assert.ok(cluster_num > 0);
assert.ok(last_timestamp > 0);
};
}
function sleep(ms: number): Promise<any> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function fetchVideo(src: string): Promise<HTMLVideoElement> {
return new Promise((resolve, reject) => {
const video = document.createElement("video");
video.src = src;
video.controls = true;
video.onloadeddata = () => {
video.onloadeddata = null as any;
resolve(video);
};
video.onerror = (err) => {
video.onerror = null as any;
reject(typeof err === "string" ? new Error(err) : err);
};
});
}
function fetchImage(src: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = src;
img.onload = () => {
resolve(img);
};
img.onerror = (err) => {
reject(typeof err === "string" ? new Error(err) : err);
};
});
}
function readAsArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onloadend = () => {
resolve(reader.result as ArrayBuffer);
};
reader.onerror = (ev) => {
reject(ev);
};
});
}
// @types/qunit doesn't define QUnit.on yet
// hack for the playwright testing in run_test.ts
(QUnit as any).on("runEnd", (runEnd: any) => {
console.log("runEnd", runEnd);
(global as any).runEnd = runEnd;
});