@sarakusha/ebml 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +43 -0
- package/build/CancelError.cjs +14 -0
- package/build/CancelError.cjs.map +1 -0
- package/build/CancelError.d.cts +1 -0
- package/build/CancelError.d.mts +7 -0
- package/build/CancelError.d.mts.map +1 -0
- package/build/CancelError.mjs +10 -0
- package/build/CancelError.mjs.map +1 -0
- package/build/Deferred-7Cu0KIje.mjs +28 -0
- package/build/Deferred-7Cu0KIje.mjs.map +1 -0
- package/build/Deferred-CB69mtbF.cjs +33 -0
- package/build/Deferred-CB69mtbF.cjs.map +1 -0
- package/build/Element-8JyOKTYb.cjs +73 -0
- package/build/Element-8JyOKTYb.cjs.map +1 -0
- package/build/Element-CC4E6wMR.d.mts +134 -0
- package/build/Element-CC4E6wMR.d.mts.map +1 -0
- package/build/Element-ChEar-fo.mjs +20 -0
- package/build/Element-ChEar-fo.mjs.map +1 -0
- package/build/FadeTransform.cjs +90 -0
- package/build/FadeTransform.cjs.map +1 -0
- package/build/FadeTransform.d.cts +1 -0
- package/build/FadeTransform.d.mts +17 -0
- package/build/FadeTransform.d.mts.map +1 -0
- package/build/FadeTransform.mjs +86 -0
- package/build/FadeTransform.mjs.map +1 -0
- package/build/RangeFetcher.cjs +63 -0
- package/build/RangeFetcher.cjs.map +1 -0
- package/build/RangeFetcher.d.cts +1 -0
- package/build/RangeFetcher.d.mts +14 -0
- package/build/RangeFetcher.d.mts.map +1 -0
- package/build/RangeFetcher.mjs +59 -0
- package/build/RangeFetcher.mjs.map +1 -0
- package/build/ReducingValve.cjs +129 -0
- package/build/ReducingValve.cjs.map +1 -0
- package/build/ReducingValve.d.cts +1 -0
- package/build/ReducingValve.d.mts +17 -0
- package/build/ReducingValve.d.mts.map +1 -0
- package/build/ReducingValve.mjs +125 -0
- package/build/ReducingValve.mjs.map +1 -0
- package/build/VideoChunkGenerator.cjs +131 -0
- package/build/VideoChunkGenerator.cjs.map +1 -0
- package/build/VideoChunkGenerator.d.cts +1 -0
- package/build/VideoChunkGenerator.d.mts +13 -0
- package/build/VideoChunkGenerator.d.mts.map +1 -0
- package/build/VideoChunkGenerator.mjs +127 -0
- package/build/VideoChunkGenerator.mjs.map +1 -0
- package/build/VideoFrameGenerator.cjs +133 -0
- package/build/VideoFrameGenerator.cjs.map +1 -0
- package/build/VideoFrameGenerator.d.cts +1 -0
- package/build/VideoFrameGenerator.d.mts +10 -0
- package/build/VideoFrameGenerator.d.mts.map +1 -0
- package/build/VideoFrameGenerator.mjs +129 -0
- package/build/VideoFrameGenerator.mjs.map +1 -0
- package/build/index.cjs +6315 -0
- package/build/index.cjs.map +1 -0
- package/build/index.d.cts +1 -0
- package/build/index.d.mts +31 -0
- package/build/index.d.mts.map +1 -0
- package/build/index.mjs +6292 -0
- package/build/index.mjs.map +1 -0
- package/build/tools-CYLlbo9J.mjs +79 -0
- package/build/tools-CYLlbo9J.mjs.map +1 -0
- package/build/tools-DQrZvGBN.cjs +138 -0
- package/build/tools-DQrZvGBN.cjs.map +1 -0
- package/package.json +213 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { a as isDuration, c as isTimestamp, i as isContentElement, l as isTrackType, n as isBlockElement, o as isMasterElement, r as isBlockGroupElement, s as isSimpleBlockElement } from "./Element-ChEar-fo.mjs";
|
|
2
|
+
import { o as readHexString } from "./tools-CYLlbo9J.mjs";
|
|
3
|
+
import { t as Deferred } from "./Deferred-7Cu0KIje.mjs";
|
|
4
|
+
//#region src/VideoChunkGenerator.ts
|
|
5
|
+
const VIDEO = 1;
|
|
6
|
+
const getCodec = (element) => {
|
|
7
|
+
switch (element.value) {
|
|
8
|
+
case "V_MPEG4/ISO/AVC": return "avc1.";
|
|
9
|
+
case "V_VP9": return "vp09.00.50.08";
|
|
10
|
+
case "V_VP8": return "vp8";
|
|
11
|
+
default: return;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var VideoChunkGenerator = class extends TransformStream {
|
|
15
|
+
#config = new Deferred();
|
|
16
|
+
clusters = [];
|
|
17
|
+
get config() {
|
|
18
|
+
return this.#config.promise;
|
|
19
|
+
}
|
|
20
|
+
id = Date.now().toString(16).slice(-6);
|
|
21
|
+
constructor() {
|
|
22
|
+
let trackType;
|
|
23
|
+
let offset = 0;
|
|
24
|
+
let block;
|
|
25
|
+
let hasReference = false;
|
|
26
|
+
let total = 0;
|
|
27
|
+
const config = {};
|
|
28
|
+
const elements = {};
|
|
29
|
+
const required = {
|
|
30
|
+
PixelWidth: "codedWidth",
|
|
31
|
+
PixelHeight: "codedHeight",
|
|
32
|
+
CodecPrivate: "codec",
|
|
33
|
+
CodecID: "codec"
|
|
34
|
+
};
|
|
35
|
+
const parseConfig = (meta) => {
|
|
36
|
+
switch (meta.name) {
|
|
37
|
+
case "CodecID": {
|
|
38
|
+
const codec = getCodec(meta);
|
|
39
|
+
if (codec) {
|
|
40
|
+
config.codec = codec;
|
|
41
|
+
if (!codec.endsWith(".")) delete required.CodecPrivate;
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
case "CodecPrivate":
|
|
46
|
+
if (config.codec === "avc1.") {
|
|
47
|
+
const codecPrivate = meta;
|
|
48
|
+
config.codec += readHexString(codecPrivate.data.subarray(1, 4));
|
|
49
|
+
config.description = codecPrivate.data.slice();
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
default: {
|
|
53
|
+
const name = required[meta.name];
|
|
54
|
+
if (name) config[name] = meta.value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
super({
|
|
59
|
+
transform: async (element, controller) => {
|
|
60
|
+
elements[element.name] = (elements[element.name] ?? 0) + 1;
|
|
61
|
+
try {
|
|
62
|
+
if (isTrackType(element)) {
|
|
63
|
+
trackType = element.value;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (isContentElement(element) && required[element.name] && (trackType == null || trackType === VIDEO)) {
|
|
67
|
+
parseConfig(element);
|
|
68
|
+
delete required[element.name];
|
|
69
|
+
if (Object.keys(required).length === 0) this.#config.resolve(config);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (isTimestamp(element)) {
|
|
73
|
+
offset = element.value;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (isDuration(element)) {
|
|
77
|
+
postMessage({ duration: element.value / 1e3 });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (isSimpleBlockElement(element)) {
|
|
81
|
+
if (trackType === VIDEO) {
|
|
82
|
+
controller.enqueue(new EncodedVideoChunk({
|
|
83
|
+
type: element.keyframe ? "key" : "delta",
|
|
84
|
+
timestamp: (offset + element.value) * 1e3,
|
|
85
|
+
data: element.payload
|
|
86
|
+
}));
|
|
87
|
+
total += 1;
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (isBlockElement(element)) {
|
|
92
|
+
block = element;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (element.name === "ReferenceBlock") {
|
|
96
|
+
hasReference = true;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (isBlockGroupElement(element) && element.isClosing) {
|
|
100
|
+
if (block) {
|
|
101
|
+
controller.enqueue(new EncodedVideoChunk({
|
|
102
|
+
type: hasReference ? "delta" : "key",
|
|
103
|
+
timestamp: (block.value + offset) * 1e3,
|
|
104
|
+
data: block.payload
|
|
105
|
+
}));
|
|
106
|
+
total += 1;
|
|
107
|
+
}
|
|
108
|
+
block = void 0;
|
|
109
|
+
hasReference = false;
|
|
110
|
+
}
|
|
111
|
+
if (isMasterElement(element) && element.name === "Cluster" && !element.isClosing) this.clusters.push(total);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error(`VideoChunkGenerator#${this.id} error while ELEMENT: ${err}`);
|
|
114
|
+
controller.error(err);
|
|
115
|
+
this.#config.reject(err);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
flush: (controller) => {
|
|
119
|
+
controller.terminate();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
//#endregion
|
|
125
|
+
export { VideoChunkGenerator as default };
|
|
126
|
+
|
|
127
|
+
//# sourceMappingURL=VideoChunkGenerator.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VideoChunkGenerator.mjs","names":["#config"],"sources":["../src/VideoChunkGenerator.ts"],"sourcesContent":["import Deferred from './Deferred';\nimport {\n isBlockElement,\n isBlockGroupElement,\n isContentElement,\n isDuration,\n isMasterElement,\n isSimpleBlockElement,\n isTimestamp,\n isTrackType,\n} from './Element';\nimport type { BlockElement, ContentElement, Element } from './Element';\nimport { readHexString } from './tools';\n\nconst VIDEO = 1;\n\nconst getCodec = (element: ContentElement<'string' | 'utf-8'>): string | undefined => {\n switch (element.value) {\n case 'V_MPEG4/ISO/AVC':\n return 'avc1.';\n case 'V_VP9':\n return 'vp09.00.50.08';\n case 'V_VP8':\n return 'vp8'; // 'vp08.00.41.08';\n default:\n return undefined;\n }\n};\n\nexport default class VideoChunkGenerator extends TransformStream<Element, EncodedVideoChunk> {\n #config = new Deferred<VideoDecoderConfig>();\n\n readonly clusters: number[] = [];\n\n get config(): Promise<VideoDecoderConfig> {\n return this.#config.promise;\n }\n\n readonly id: string = Date.now().toString(16).slice(-6);\n\n constructor() {\n let trackType: number | undefined;\n let offset = 0;\n let block: BlockElement | undefined;\n let hasReference = false;\n let total = 0;\n const config: Partial<VideoDecoderConfig> = {};\n const elements: Record<string, number> = {};\n const required: Record<string, keyof VideoDecoderConfig> = {\n PixelWidth: 'codedWidth',\n PixelHeight: 'codedHeight',\n CodecPrivate: 'codec',\n CodecID: 'codec',\n };\n\n const parseConfig = (meta: ContentElement): void => {\n switch (meta.name) {\n case 'CodecID': {\n const codec = getCodec(meta as ContentElement<'string'>);\n if (codec) {\n config.codec = codec;\n if (!codec.endsWith('.')) {\n delete required.CodecPrivate;\n }\n }\n break;\n }\n case 'CodecPrivate':\n if (config.codec === 'avc1.') {\n const codecPrivate = meta as ContentElement<'binary'>;\n config.codec += readHexString(codecPrivate.data.subarray(1, 4));\n config.description = codecPrivate.data.slice();\n }\n break;\n default: {\n const name = required[meta.name];\n if (name) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n config[name] = meta.value as any;\n }\n }\n }\n };\n super({\n transform: async (element, controller) => {\n elements[element.name] = (elements[element.name] ?? 0) + 1;\n try {\n if (isTrackType(element)) {\n trackType = element.value as number;\n return;\n }\n if (\n isContentElement(element) &&\n required[element.name] &&\n (trackType == null || trackType === VIDEO)\n ) {\n parseConfig(element);\n delete required[element.name];\n if (Object.keys(required).length === 0) {\n // console.info(`VideoChunkGenerator#${this.id}:config ${JSON.stringify(config)}`);\n this.#config.resolve(config as VideoDecoderConfig);\n }\n return;\n }\n\n if (isTimestamp(element)) {\n offset = element.value as number;\n return;\n }\n\n if (isDuration(element)) {\n postMessage({ duration: element.value / 1000 });\n return;\n }\n\n if (isSimpleBlockElement(element)) {\n if (trackType === VIDEO) {\n controller.enqueue(\n new EncodedVideoChunk({\n type: element.keyframe ? 'key' : 'delta',\n timestamp: (offset + element.value) * 1000,\n data: element.payload,\n }),\n );\n total += 1;\n }\n return;\n }\n\n if (isBlockElement(element)) {\n block = element;\n return;\n }\n\n if (element.name === 'ReferenceBlock') {\n hasReference = true;\n return;\n }\n\n if (isBlockGroupElement(element) && element.isClosing) {\n if (block) {\n controller.enqueue(\n new EncodedVideoChunk({\n type: hasReference ? 'delta' : 'key',\n timestamp: (block.value + offset) * 1000,\n data: block.payload,\n }),\n );\n total += 1;\n }\n block = undefined;\n hasReference = false;\n }\n if (isMasterElement(element) && element.name === 'Cluster' && !element.isClosing)\n this.clusters.push(total);\n } catch (err) {\n console.error(`VideoChunkGenerator#${this.id} error while ELEMENT: ${err}`);\n controller.error(err);\n this.#config.reject(err as Error);\n }\n },\n flush: (controller) => {\n // console.info(\n // `VideoChunkGenerator#${this.id}:flush total: ${total}, clusters: ${this.clusters.join()}`\n // );\n controller.terminate();\n },\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
5
|
+
//#region src/Semaphore.ts
|
|
6
|
+
var Semaphore = class {
|
|
7
|
+
max;
|
|
8
|
+
#counter = 0;
|
|
9
|
+
#waiting = [];
|
|
10
|
+
constructor(max = 1) {
|
|
11
|
+
this.max = max;
|
|
12
|
+
}
|
|
13
|
+
take() {
|
|
14
|
+
const promise = this.#waiting.shift();
|
|
15
|
+
if (promise) {
|
|
16
|
+
promise.resolve();
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
acquire() {
|
|
22
|
+
if (this.#counter < this.max) {
|
|
23
|
+
this.#counter += 1;
|
|
24
|
+
return Promise.resolve();
|
|
25
|
+
}
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
this.#waiting.push({
|
|
28
|
+
resolve,
|
|
29
|
+
reject
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
release() {
|
|
34
|
+
if (this.take()) this.#counter -= 1;
|
|
35
|
+
}
|
|
36
|
+
purge() {
|
|
37
|
+
const unresolved = this.#waiting.splice(0);
|
|
38
|
+
unresolved.forEach(({ reject }) => {
|
|
39
|
+
reject(/* @__PURE__ */ new Error("Task has been purged."));
|
|
40
|
+
});
|
|
41
|
+
this.#counter = 0;
|
|
42
|
+
return unresolved.length;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/VideoFrameGenerator.ts
|
|
47
|
+
const MAX_PRELOAD_FRAMES = 10;
|
|
48
|
+
var VideoFrameGenerator = class {
|
|
49
|
+
config;
|
|
50
|
+
readable;
|
|
51
|
+
writable;
|
|
52
|
+
constructor(config, maxPreloadFrames = MAX_PRELOAD_FRAMES) {
|
|
53
|
+
this.config = config;
|
|
54
|
+
const pendingFrames = [];
|
|
55
|
+
const push = new Semaphore(maxPreloadFrames);
|
|
56
|
+
const pull = new Semaphore(0);
|
|
57
|
+
let decoder;
|
|
58
|
+
let cancel;
|
|
59
|
+
let abort;
|
|
60
|
+
let finished = false;
|
|
61
|
+
let clearing = false;
|
|
62
|
+
const clear = () => {
|
|
63
|
+
clearing = true;
|
|
64
|
+
pendingFrames.splice(0).forEach((frame) => frame.close());
|
|
65
|
+
push.purge();
|
|
66
|
+
pull.purge();
|
|
67
|
+
};
|
|
68
|
+
this.readable = new ReadableStream({
|
|
69
|
+
pull: async (controller) => {
|
|
70
|
+
if (abort || finished && pendingFrames.length === 0) controller.close();
|
|
71
|
+
else try {
|
|
72
|
+
await pull.acquire();
|
|
73
|
+
const frame = pendingFrames.shift();
|
|
74
|
+
if (frame) controller.enqueue(frame);
|
|
75
|
+
push.release();
|
|
76
|
+
} catch (e) {
|
|
77
|
+
controller.error(e);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
cancel: (reason) => {
|
|
81
|
+
cancel = reason;
|
|
82
|
+
clear();
|
|
83
|
+
}
|
|
84
|
+
}, new CountQueuingStrategy({ highWaterMark: 1 }));
|
|
85
|
+
this.writable = new WritableStream({
|
|
86
|
+
start: async (controller) => {
|
|
87
|
+
decoder = new VideoDecoder({
|
|
88
|
+
output: async (frame) => {
|
|
89
|
+
pendingFrames.push(frame);
|
|
90
|
+
pull.release();
|
|
91
|
+
},
|
|
92
|
+
error: (err) => {
|
|
93
|
+
console.error("error while decode", err);
|
|
94
|
+
controller.error(err);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
try {
|
|
98
|
+
decoder.configure(await this.config);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
controller.error(err);
|
|
101
|
+
console.error("error while configure", err);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
write: async (chunk, controller) => {
|
|
105
|
+
if (cancel) controller.error(cancel);
|
|
106
|
+
else try {
|
|
107
|
+
await push.acquire();
|
|
108
|
+
decoder.decode(chunk);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
if (!clearing) {
|
|
111
|
+
console.error("error while decode chunk", e);
|
|
112
|
+
controller.error(e);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
close: async () => {
|
|
117
|
+
await decoder.flush();
|
|
118
|
+
decoder.close();
|
|
119
|
+
finished = true;
|
|
120
|
+
clear();
|
|
121
|
+
},
|
|
122
|
+
abort: (reason) => {
|
|
123
|
+
abort = reason;
|
|
124
|
+
decoder.close();
|
|
125
|
+
clear();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
//#endregion
|
|
131
|
+
exports.default = VideoFrameGenerator;
|
|
132
|
+
|
|
133
|
+
//# sourceMappingURL=VideoFrameGenerator.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VideoFrameGenerator.cjs","names":["#waiting","#counter"],"sources":["../src/Semaphore.ts","../src/VideoFrameGenerator.ts"],"sourcesContent":["type WaitingPromise = { resolve: () => void; reject: (err: Error) => void };\n\nexport default class Semaphore {\n #counter = 0;\n\n #waiting: WaitingPromise[] = [];\n\n constructor(readonly max = 1) {}\n\n protected take(): boolean {\n const promise = this.#waiting.shift();\n if (promise) {\n promise.resolve();\n return false;\n }\n return true;\n }\n\n acquire(): Promise<void> {\n if (this.#counter < this.max) {\n this.#counter += 1;\n return Promise.resolve();\n }\n return new Promise<void>((resolve, reject) => {\n this.#waiting.push({\n resolve,\n reject,\n });\n });\n }\n\n release(): void {\n if (this.take()) this.#counter -= 1;\n }\n\n purge() {\n const unresolved = this.#waiting.splice(0);\n unresolved.forEach(({ reject }) => {\n reject(new Error('Task has been purged.'));\n });\n this.#counter = 0;\n return unresolved.length;\n }\n}\n","import Semaphore from './Semaphore';\n\nconst MAX_PRELOAD_FRAMES = 10;\n\nexport default class VideoFrameGenerator implements TransformStream<EncodedVideoChunk, VideoFrame> {\n readonly readable: ReadableStream<VideoFrame>;\n\n readonly writable: WritableStream<EncodedVideoChunk>;\n\n constructor(\n readonly config: Promise<VideoDecoderConfig>,\n maxPreloadFrames: number = MAX_PRELOAD_FRAMES,\n ) {\n const pendingFrames: VideoFrame[] = [];\n const push = new Semaphore(maxPreloadFrames);\n const pull = new Semaphore(0);\n let decoder: VideoDecoder;\n let cancel: unknown;\n let abort: unknown;\n let finished = false;\n let clearing = false;\n const clear = () => {\n clearing = true;\n pendingFrames.splice(0).forEach((frame) => frame.close());\n push.purge();\n pull.purge();\n };\n this.readable = new ReadableStream<VideoFrame>(\n {\n pull: async (controller) => {\n if (abort || (finished && pendingFrames.length === 0)) controller.close();\n else {\n try {\n await pull.acquire();\n const frame = pendingFrames.shift();\n if (frame) {\n controller.enqueue(frame);\n }\n push.release();\n } catch (e) {\n controller.error(e);\n }\n }\n },\n cancel: (reason) => {\n cancel = reason;\n clear();\n },\n },\n new CountQueuingStrategy({ highWaterMark: 1 }),\n );\n this.writable = new WritableStream({\n start: async (controller) => {\n decoder = new VideoDecoder({\n output: async (frame) => {\n pendingFrames.push(frame);\n pull.release();\n },\n error: (err) => {\n console.error('error while decode', err);\n controller.error(err);\n },\n });\n try {\n decoder.configure(await this.config);\n } catch (err) {\n controller.error(err);\n console.error('error while configure', err);\n }\n },\n write: async (chunk, controller) => {\n if (cancel) controller.error(cancel);\n else {\n try {\n await push.acquire();\n decoder.decode(chunk);\n } catch (e) {\n if (!clearing) {\n console.error('error while decode chunk', e);\n controller.error(e);\n }\n }\n }\n },\n close: async () => {\n await decoder.flush();\n decoder.close();\n finished = true;\n clear();\n },\n abort: (reason) => {\n abort = reason;\n decoder.close();\n clear();\n },\n });\n }\n}\n"],"mappings":";;;;;AAEA,IAAqB,YAArB,MAA+B;CAKR;CAJrB,WAAW;CAEX,WAA6B,CAAC;CAE9B,YAAY,MAAe,GAAG;EAAT,KAAA,MAAA;CAAU;CAE/B,OAA0B;EACxB,MAAM,UAAU,KAAKA,SAAS,MAAM;EACpC,IAAI,SAAS;GACX,QAAQ,QAAQ;GAChB,OAAO;EACT;EACA,OAAO;CACT;CAEA,UAAyB;EACvB,IAAI,KAAKC,WAAW,KAAK,KAAK;GAC5B,KAAKA,YAAY;GACjB,OAAO,QAAQ,QAAQ;EACzB;EACA,OAAO,IAAI,SAAe,SAAS,WAAW;GAC5C,KAAKD,SAAS,KAAK;IACjB;IACA;GACF,CAAC;EACH,CAAC;CACH;CAEA,UAAgB;EACd,IAAI,KAAK,KAAK,GAAG,KAAKC,YAAY;CACpC;CAEA,QAAQ;EACN,MAAM,aAAa,KAAKD,SAAS,OAAO,CAAC;EACzC,WAAW,SAAS,EAAE,aAAa;GACjC,uBAAO,IAAI,MAAM,uBAAuB,CAAC;EAC3C,CAAC;EACD,KAAKC,WAAW;EAChB,OAAO,WAAW;CACpB;AACF;;;ACzCA,MAAM,qBAAqB;AAE3B,IAAqB,sBAArB,MAAmG;CAMtF;CALX;CAEA;CAEA,YACE,QACA,mBAA2B,oBAC3B;EAFS,KAAA,SAAA;EAGT,MAAM,gBAA8B,CAAC;EACrC,MAAM,OAAO,IAAI,UAAU,gBAAgB;EAC3C,MAAM,OAAO,IAAI,UAAU,CAAC;EAC5B,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI,WAAW;EACf,IAAI,WAAW;EACf,MAAM,cAAc;GAClB,WAAW;GACX,cAAc,OAAO,CAAC,EAAE,SAAS,UAAU,MAAM,MAAM,CAAC;GACxD,KAAK,MAAM;GACX,KAAK,MAAM;EACb;EACA,KAAK,WAAW,IAAI,eAClB;GACE,MAAM,OAAO,eAAe;IAC1B,IAAI,SAAU,YAAY,cAAc,WAAW,GAAI,WAAW,MAAM;SAEtE,IAAI;KACF,MAAM,KAAK,QAAQ;KACnB,MAAM,QAAQ,cAAc,MAAM;KAClC,IAAI,OACF,WAAW,QAAQ,KAAK;KAE1B,KAAK,QAAQ;IACf,SAAS,GAAG;KACV,WAAW,MAAM,CAAC;IACpB;GAEJ;GACA,SAAS,WAAW;IAClB,SAAS;IACT,MAAM;GACR;EACF,GACA,IAAI,qBAAqB,EAAE,eAAe,EAAE,CAAC,CAC/C;EACA,KAAK,WAAW,IAAI,eAAe;GACjC,OAAO,OAAO,eAAe;IAC3B,UAAU,IAAI,aAAa;KACzB,QAAQ,OAAO,UAAU;MACvB,cAAc,KAAK,KAAK;MACxB,KAAK,QAAQ;KACf;KACA,QAAQ,QAAQ;MACd,QAAQ,MAAM,sBAAsB,GAAG;MACvC,WAAW,MAAM,GAAG;KACtB;IACF,CAAC;IACD,IAAI;KACF,QAAQ,UAAU,MAAM,KAAK,MAAM;IACrC,SAAS,KAAK;KACZ,WAAW,MAAM,GAAG;KACpB,QAAQ,MAAM,yBAAyB,GAAG;IAC5C;GACF;GACA,OAAO,OAAO,OAAO,eAAe;IAClC,IAAI,QAAQ,WAAW,MAAM,MAAM;SAEjC,IAAI;KACF,MAAM,KAAK,QAAQ;KACnB,QAAQ,OAAO,KAAK;IACtB,SAAS,GAAG;KACV,IAAI,CAAC,UAAU;MACb,QAAQ,MAAM,4BAA4B,CAAC;MAC3C,WAAW,MAAM,CAAC;KACpB;IACF;GAEJ;GACA,OAAO,YAAY;IACjB,MAAM,QAAQ,MAAM;IACpB,QAAQ,MAAM;IACd,WAAW;IACX,MAAM;GACR;GACA,QAAQ,WAAW;IACjB,QAAQ;IACR,QAAQ,MAAM;IACd,MAAM;GACR;EACF,CAAC;CACH;AACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type * from './VideoFrameGenerator.d.mts'
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/VideoFrameGenerator.d.ts
|
|
2
|
+
declare class VideoFrameGenerator implements TransformStream<EncodedVideoChunk, VideoFrame> {
|
|
3
|
+
readonly config: Promise<VideoDecoderConfig>;
|
|
4
|
+
readonly readable: ReadableStream<VideoFrame>;
|
|
5
|
+
readonly writable: WritableStream<EncodedVideoChunk>;
|
|
6
|
+
constructor(config: Promise<VideoDecoderConfig>, maxPreloadFrames?: number);
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { VideoFrameGenerator as default };
|
|
10
|
+
//# sourceMappingURL=VideoFrameGenerator.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VideoFrameGenerator.d.mts","names":[],"sources":["../src/VideoFrameGenerator.ts"],"mappings":";cAIqB,mBAAA,YAA+B,eAAA,CAAgB,iBAAA,EAAmB,UAAA;EAAA,SAM1E,MAAA,EAAQ,OAAA,CAAQ,kBAAA;EAAA,SALlB,QAAA,EAAU,cAAA,CAAe,UAAA;EAAA,SAEzB,QAAA,EAAU,cAAA,CAAe,iBAAA;cAGvB,MAAA,EAAQ,OAAA,CAAQ,kBAAA,GACzB,gBAAA;AAAA"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
//#region src/Semaphore.ts
|
|
2
|
+
var Semaphore = class {
|
|
3
|
+
max;
|
|
4
|
+
#counter = 0;
|
|
5
|
+
#waiting = [];
|
|
6
|
+
constructor(max = 1) {
|
|
7
|
+
this.max = max;
|
|
8
|
+
}
|
|
9
|
+
take() {
|
|
10
|
+
const promise = this.#waiting.shift();
|
|
11
|
+
if (promise) {
|
|
12
|
+
promise.resolve();
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
acquire() {
|
|
18
|
+
if (this.#counter < this.max) {
|
|
19
|
+
this.#counter += 1;
|
|
20
|
+
return Promise.resolve();
|
|
21
|
+
}
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
this.#waiting.push({
|
|
24
|
+
resolve,
|
|
25
|
+
reject
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
release() {
|
|
30
|
+
if (this.take()) this.#counter -= 1;
|
|
31
|
+
}
|
|
32
|
+
purge() {
|
|
33
|
+
const unresolved = this.#waiting.splice(0);
|
|
34
|
+
unresolved.forEach(({ reject }) => {
|
|
35
|
+
reject(/* @__PURE__ */ new Error("Task has been purged."));
|
|
36
|
+
});
|
|
37
|
+
this.#counter = 0;
|
|
38
|
+
return unresolved.length;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/VideoFrameGenerator.ts
|
|
43
|
+
const MAX_PRELOAD_FRAMES = 10;
|
|
44
|
+
var VideoFrameGenerator = class {
|
|
45
|
+
config;
|
|
46
|
+
readable;
|
|
47
|
+
writable;
|
|
48
|
+
constructor(config, maxPreloadFrames = MAX_PRELOAD_FRAMES) {
|
|
49
|
+
this.config = config;
|
|
50
|
+
const pendingFrames = [];
|
|
51
|
+
const push = new Semaphore(maxPreloadFrames);
|
|
52
|
+
const pull = new Semaphore(0);
|
|
53
|
+
let decoder;
|
|
54
|
+
let cancel;
|
|
55
|
+
let abort;
|
|
56
|
+
let finished = false;
|
|
57
|
+
let clearing = false;
|
|
58
|
+
const clear = () => {
|
|
59
|
+
clearing = true;
|
|
60
|
+
pendingFrames.splice(0).forEach((frame) => frame.close());
|
|
61
|
+
push.purge();
|
|
62
|
+
pull.purge();
|
|
63
|
+
};
|
|
64
|
+
this.readable = new ReadableStream({
|
|
65
|
+
pull: async (controller) => {
|
|
66
|
+
if (abort || finished && pendingFrames.length === 0) controller.close();
|
|
67
|
+
else try {
|
|
68
|
+
await pull.acquire();
|
|
69
|
+
const frame = pendingFrames.shift();
|
|
70
|
+
if (frame) controller.enqueue(frame);
|
|
71
|
+
push.release();
|
|
72
|
+
} catch (e) {
|
|
73
|
+
controller.error(e);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
cancel: (reason) => {
|
|
77
|
+
cancel = reason;
|
|
78
|
+
clear();
|
|
79
|
+
}
|
|
80
|
+
}, new CountQueuingStrategy({ highWaterMark: 1 }));
|
|
81
|
+
this.writable = new WritableStream({
|
|
82
|
+
start: async (controller) => {
|
|
83
|
+
decoder = new VideoDecoder({
|
|
84
|
+
output: async (frame) => {
|
|
85
|
+
pendingFrames.push(frame);
|
|
86
|
+
pull.release();
|
|
87
|
+
},
|
|
88
|
+
error: (err) => {
|
|
89
|
+
console.error("error while decode", err);
|
|
90
|
+
controller.error(err);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
try {
|
|
94
|
+
decoder.configure(await this.config);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
controller.error(err);
|
|
97
|
+
console.error("error while configure", err);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
write: async (chunk, controller) => {
|
|
101
|
+
if (cancel) controller.error(cancel);
|
|
102
|
+
else try {
|
|
103
|
+
await push.acquire();
|
|
104
|
+
decoder.decode(chunk);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
if (!clearing) {
|
|
107
|
+
console.error("error while decode chunk", e);
|
|
108
|
+
controller.error(e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
close: async () => {
|
|
113
|
+
await decoder.flush();
|
|
114
|
+
decoder.close();
|
|
115
|
+
finished = true;
|
|
116
|
+
clear();
|
|
117
|
+
},
|
|
118
|
+
abort: (reason) => {
|
|
119
|
+
abort = reason;
|
|
120
|
+
decoder.close();
|
|
121
|
+
clear();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
//#endregion
|
|
127
|
+
export { VideoFrameGenerator as default };
|
|
128
|
+
|
|
129
|
+
//# sourceMappingURL=VideoFrameGenerator.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VideoFrameGenerator.mjs","names":["#waiting","#counter"],"sources":["../src/Semaphore.ts","../src/VideoFrameGenerator.ts"],"sourcesContent":["type WaitingPromise = { resolve: () => void; reject: (err: Error) => void };\n\nexport default class Semaphore {\n #counter = 0;\n\n #waiting: WaitingPromise[] = [];\n\n constructor(readonly max = 1) {}\n\n protected take(): boolean {\n const promise = this.#waiting.shift();\n if (promise) {\n promise.resolve();\n return false;\n }\n return true;\n }\n\n acquire(): Promise<void> {\n if (this.#counter < this.max) {\n this.#counter += 1;\n return Promise.resolve();\n }\n return new Promise<void>((resolve, reject) => {\n this.#waiting.push({\n resolve,\n reject,\n });\n });\n }\n\n release(): void {\n if (this.take()) this.#counter -= 1;\n }\n\n purge() {\n const unresolved = this.#waiting.splice(0);\n unresolved.forEach(({ reject }) => {\n reject(new Error('Task has been purged.'));\n });\n this.#counter = 0;\n return unresolved.length;\n }\n}\n","import Semaphore from './Semaphore';\n\nconst MAX_PRELOAD_FRAMES = 10;\n\nexport default class VideoFrameGenerator implements TransformStream<EncodedVideoChunk, VideoFrame> {\n readonly readable: ReadableStream<VideoFrame>;\n\n readonly writable: WritableStream<EncodedVideoChunk>;\n\n constructor(\n readonly config: Promise<VideoDecoderConfig>,\n maxPreloadFrames: number = MAX_PRELOAD_FRAMES,\n ) {\n const pendingFrames: VideoFrame[] = [];\n const push = new Semaphore(maxPreloadFrames);\n const pull = new Semaphore(0);\n let decoder: VideoDecoder;\n let cancel: unknown;\n let abort: unknown;\n let finished = false;\n let clearing = false;\n const clear = () => {\n clearing = true;\n pendingFrames.splice(0).forEach((frame) => frame.close());\n push.purge();\n pull.purge();\n };\n this.readable = new ReadableStream<VideoFrame>(\n {\n pull: async (controller) => {\n if (abort || (finished && pendingFrames.length === 0)) controller.close();\n else {\n try {\n await pull.acquire();\n const frame = pendingFrames.shift();\n if (frame) {\n controller.enqueue(frame);\n }\n push.release();\n } catch (e) {\n controller.error(e);\n }\n }\n },\n cancel: (reason) => {\n cancel = reason;\n clear();\n },\n },\n new CountQueuingStrategy({ highWaterMark: 1 }),\n );\n this.writable = new WritableStream({\n start: async (controller) => {\n decoder = new VideoDecoder({\n output: async (frame) => {\n pendingFrames.push(frame);\n pull.release();\n },\n error: (err) => {\n console.error('error while decode', err);\n controller.error(err);\n },\n });\n try {\n decoder.configure(await this.config);\n } catch (err) {\n controller.error(err);\n console.error('error while configure', err);\n }\n },\n write: async (chunk, controller) => {\n if (cancel) controller.error(cancel);\n else {\n try {\n await push.acquire();\n decoder.decode(chunk);\n } catch (e) {\n if (!clearing) {\n console.error('error while decode chunk', e);\n controller.error(e);\n }\n }\n }\n },\n close: async () => {\n await decoder.flush();\n decoder.close();\n finished = true;\n clear();\n },\n abort: (reason) => {\n abort = reason;\n decoder.close();\n clear();\n },\n });\n }\n}\n"],"mappings":";AAEA,IAAqB,YAArB,MAA+B;CAKR;CAJrB,WAAW;CAEX,WAA6B,CAAC;CAE9B,YAAY,MAAe,GAAG;EAAT,KAAA,MAAA;CAAU;CAE/B,OAA0B;EACxB,MAAM,UAAU,KAAKA,SAAS,MAAM;EACpC,IAAI,SAAS;GACX,QAAQ,QAAQ;GAChB,OAAO;EACT;EACA,OAAO;CACT;CAEA,UAAyB;EACvB,IAAI,KAAKC,WAAW,KAAK,KAAK;GAC5B,KAAKA,YAAY;GACjB,OAAO,QAAQ,QAAQ;EACzB;EACA,OAAO,IAAI,SAAe,SAAS,WAAW;GAC5C,KAAKD,SAAS,KAAK;IACjB;IACA;GACF,CAAC;EACH,CAAC;CACH;CAEA,UAAgB;EACd,IAAI,KAAK,KAAK,GAAG,KAAKC,YAAY;CACpC;CAEA,QAAQ;EACN,MAAM,aAAa,KAAKD,SAAS,OAAO,CAAC;EACzC,WAAW,SAAS,EAAE,aAAa;GACjC,uBAAO,IAAI,MAAM,uBAAuB,CAAC;EAC3C,CAAC;EACD,KAAKC,WAAW;EAChB,OAAO,WAAW;CACpB;AACF;;;ACzCA,MAAM,qBAAqB;AAE3B,IAAqB,sBAArB,MAAmG;CAMtF;CALX;CAEA;CAEA,YACE,QACA,mBAA2B,oBAC3B;EAFS,KAAA,SAAA;EAGT,MAAM,gBAA8B,CAAC;EACrC,MAAM,OAAO,IAAI,UAAU,gBAAgB;EAC3C,MAAM,OAAO,IAAI,UAAU,CAAC;EAC5B,IAAI;EACJ,IAAI;EACJ,IAAI;EACJ,IAAI,WAAW;EACf,IAAI,WAAW;EACf,MAAM,cAAc;GAClB,WAAW;GACX,cAAc,OAAO,CAAC,EAAE,SAAS,UAAU,MAAM,MAAM,CAAC;GACxD,KAAK,MAAM;GACX,KAAK,MAAM;EACb;EACA,KAAK,WAAW,IAAI,eAClB;GACE,MAAM,OAAO,eAAe;IAC1B,IAAI,SAAU,YAAY,cAAc,WAAW,GAAI,WAAW,MAAM;SAEtE,IAAI;KACF,MAAM,KAAK,QAAQ;KACnB,MAAM,QAAQ,cAAc,MAAM;KAClC,IAAI,OACF,WAAW,QAAQ,KAAK;KAE1B,KAAK,QAAQ;IACf,SAAS,GAAG;KACV,WAAW,MAAM,CAAC;IACpB;GAEJ;GACA,SAAS,WAAW;IAClB,SAAS;IACT,MAAM;GACR;EACF,GACA,IAAI,qBAAqB,EAAE,eAAe,EAAE,CAAC,CAC/C;EACA,KAAK,WAAW,IAAI,eAAe;GACjC,OAAO,OAAO,eAAe;IAC3B,UAAU,IAAI,aAAa;KACzB,QAAQ,OAAO,UAAU;MACvB,cAAc,KAAK,KAAK;MACxB,KAAK,QAAQ;KACf;KACA,QAAQ,QAAQ;MACd,QAAQ,MAAM,sBAAsB,GAAG;MACvC,WAAW,MAAM,GAAG;KACtB;IACF,CAAC;IACD,IAAI;KACF,QAAQ,UAAU,MAAM,KAAK,MAAM;IACrC,SAAS,KAAK;KACZ,WAAW,MAAM,GAAG;KACpB,QAAQ,MAAM,yBAAyB,GAAG;IAC5C;GACF;GACA,OAAO,OAAO,OAAO,eAAe;IAClC,IAAI,QAAQ,WAAW,MAAM,MAAM;SAEjC,IAAI;KACF,MAAM,KAAK,QAAQ;KACnB,QAAQ,OAAO,KAAK;IACtB,SAAS,GAAG;KACV,IAAI,CAAC,UAAU;MACb,QAAQ,MAAM,4BAA4B,CAAC;MAC3C,WAAW,MAAM,CAAC;KACpB;IACF;GAEJ;GACA,OAAO,YAAY;IACjB,MAAM,QAAQ,MAAM;IACpB,QAAQ,MAAM;IACd,WAAW;IACX,MAAM;GACR;GACA,QAAQ,WAAW;IACjB,QAAQ;IACR,QAAQ,MAAM;IACd,MAAM;GACR;EACF,CAAC;CACH;AACF"}
|