@sarakusha/ebml 0.0.5 → 0.0.7
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 +16 -0
- package/build/VideoChunkGenerator.cjs +42 -14
- package/build/VideoChunkGenerator.cjs.map +1 -1
- package/build/VideoChunkGenerator.d.mts.map +1 -1
- package/build/VideoChunkGenerator.mjs +43 -15
- package/build/VideoChunkGenerator.mjs.map +1 -1
- package/build/VideoFrameGenerator.cjs +22 -8
- package/build/VideoFrameGenerator.cjs.map +1 -1
- package/build/VideoFrameGenerator.mjs +22 -8
- package/build/VideoFrameGenerator.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [0.0.7](https://github.com/sarakusha/ebml/compare/v0.0.6...v0.0.7) (2026-06-26)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* log recoverable video decode errors ([e1e5e85](https://github.com/sarakusha/ebml/commit/e1e5e8512a537ec8f2774aaaff4ed58aea11369a))
|
|
11
|
+
* tolerate decoder frame failures ([9b1f65c](https://github.com/sarakusha/ebml/commit/9b1f65c2d0df88f5c5a004a5e94994e4a6618fb5))
|
|
12
|
+
|
|
13
|
+
### [0.0.6](https://github.com/sarakusha/ebml/compare/v0.0.5...v0.0.6) (2026-06-25)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* не работал на смешанных (аудио+видео) данных ([c85d24f](https://github.com/sarakusha/ebml/commit/c85d24fa0424785af35849c430a57c238f2fd07e))
|
|
19
|
+
* parse video track by TrackNumber in VideoChunkGenerator ([ba83550](https://github.com/sarakusha/ebml/commit/ba8355026467ed8b3e1a2d5d29b68ff32bfc8b16))
|
|
20
|
+
|
|
5
21
|
### [0.0.5](https://github.com/sarakusha/ebml/compare/v0.0.4...v0.0.5) (2026-05-26)
|
|
6
22
|
|
|
7
23
|
|
|
@@ -23,20 +23,23 @@ var VideoChunkGenerator = class extends TransformStream {
|
|
|
23
23
|
}
|
|
24
24
|
id = Date.now().toString(16).slice(-6);
|
|
25
25
|
constructor() {
|
|
26
|
+
let inTrackEntry = false;
|
|
27
|
+
let trackNumber;
|
|
26
28
|
let trackType;
|
|
29
|
+
let videoTrackNumber;
|
|
27
30
|
let offset = 0;
|
|
28
31
|
let block;
|
|
29
32
|
let hasReference = false;
|
|
30
33
|
let total = 0;
|
|
31
|
-
const
|
|
32
|
-
const elements = {};
|
|
33
|
-
const required = {
|
|
34
|
+
const createRequired = () => ({
|
|
34
35
|
PixelWidth: "codedWidth",
|
|
35
36
|
PixelHeight: "codedHeight",
|
|
36
37
|
CodecPrivate: "codec",
|
|
37
38
|
CodecID: "codec"
|
|
38
|
-
};
|
|
39
|
-
|
|
39
|
+
});
|
|
40
|
+
let trackConfig = {};
|
|
41
|
+
let trackRequired = createRequired();
|
|
42
|
+
const parseConfig = (meta, config, required) => {
|
|
40
43
|
switch (meta.name) {
|
|
41
44
|
case "CodecID": {
|
|
42
45
|
const codec = getCodec(meta);
|
|
@@ -58,19 +61,44 @@ var VideoChunkGenerator = class extends TransformStream {
|
|
|
58
61
|
if (name) config[name] = meta.value;
|
|
59
62
|
}
|
|
60
63
|
}
|
|
64
|
+
delete required[meta.name];
|
|
61
65
|
};
|
|
66
|
+
const isTrackNumber = (element) => element.name === "TrackNumber" && require_Element.isContentElement(element);
|
|
67
|
+
const isTrackType = (element) => element.name === "TrackType" && require_Element.isContentElement(element);
|
|
68
|
+
const isVideoBlock = (candidate) => videoTrackNumber != null && candidate.track === videoTrackNumber;
|
|
62
69
|
super({
|
|
63
70
|
transform: async (element, controller) => {
|
|
64
|
-
elements[element.name] = (elements[element.name] ?? 0) + 1;
|
|
65
71
|
try {
|
|
66
|
-
if (require_Element.
|
|
67
|
-
|
|
72
|
+
if (require_Element.isMasterElement(element) && element.name === "TrackEntry") {
|
|
73
|
+
if (element.isClosing) {
|
|
74
|
+
if (trackType === VIDEO && trackNumber != null) {
|
|
75
|
+
videoTrackNumber = trackNumber;
|
|
76
|
+
if (Object.keys(trackRequired).length === 0) this.#config.resolve(trackConfig);
|
|
77
|
+
}
|
|
78
|
+
inTrackEntry = false;
|
|
79
|
+
trackNumber = void 0;
|
|
80
|
+
trackType = void 0;
|
|
81
|
+
trackConfig = {};
|
|
82
|
+
trackRequired = createRequired();
|
|
83
|
+
} else {
|
|
84
|
+
inTrackEntry = true;
|
|
85
|
+
trackNumber = void 0;
|
|
86
|
+
trackType = void 0;
|
|
87
|
+
trackConfig = {};
|
|
88
|
+
trackRequired = createRequired();
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (inTrackEntry && isTrackNumber(element)) {
|
|
93
|
+
trackNumber = Number(element.value);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (inTrackEntry && isTrackType(element)) {
|
|
97
|
+
trackType = Number(element.value);
|
|
68
98
|
return;
|
|
69
99
|
}
|
|
70
|
-
if (require_Element.isContentElement(element) &&
|
|
71
|
-
parseConfig(element);
|
|
72
|
-
delete required[element.name];
|
|
73
|
-
if (Object.keys(required).length === 0) this.#config.resolve(config);
|
|
100
|
+
if (inTrackEntry && require_Element.isContentElement(element) && trackRequired[element.name]) {
|
|
101
|
+
parseConfig(element, trackConfig, trackRequired);
|
|
74
102
|
return;
|
|
75
103
|
}
|
|
76
104
|
if (require_Element.isTimestamp(element)) {
|
|
@@ -82,7 +110,7 @@ var VideoChunkGenerator = class extends TransformStream {
|
|
|
82
110
|
return;
|
|
83
111
|
}
|
|
84
112
|
if (require_Element.isSimpleBlockElement(element)) {
|
|
85
|
-
if (
|
|
113
|
+
if (isVideoBlock(element)) {
|
|
86
114
|
controller.enqueue(new EncodedVideoChunk({
|
|
87
115
|
type: element.keyframe ? "key" : "delta",
|
|
88
116
|
timestamp: (offset + element.value) * 1e3,
|
|
@@ -101,7 +129,7 @@ var VideoChunkGenerator = class extends TransformStream {
|
|
|
101
129
|
return;
|
|
102
130
|
}
|
|
103
131
|
if (require_Element.isBlockGroupElement(element) && element.isClosing) {
|
|
104
|
-
if (block) {
|
|
132
|
+
if (block && isVideoBlock(block)) {
|
|
105
133
|
controller.enqueue(new EncodedVideoChunk({
|
|
106
134
|
type: hasReference ? "delta" : "key",
|
|
107
135
|
timestamp: (block.value + offset) * 1e3,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoChunkGenerator.cjs","names":["Deferred","#config","readHexString","
|
|
1
|
+
{"version":3,"file":"VideoChunkGenerator.cjs","names":["Deferred","#config","readHexString","isContentElement","isMasterElement","isTimestamp","isDuration","isSimpleBlockElement","isBlockElement","isBlockGroupElement"],"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} 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 inTrackEntry = false;\n let trackNumber: number | undefined;\n let trackType: number | undefined;\n let videoTrackNumber: number | undefined;\n let offset = 0;\n let block: BlockElement | undefined;\n let hasReference = false;\n let total = 0;\n const createRequired = (): Record<string, keyof VideoDecoderConfig> => ({\n PixelWidth: 'codedWidth',\n PixelHeight: 'codedHeight',\n CodecPrivate: 'codec',\n CodecID: 'codec',\n });\n let trackConfig: Partial<VideoDecoderConfig> = {};\n let trackRequired = createRequired();\n\n const parseConfig = (\n meta: ContentElement,\n config: Partial<VideoDecoderConfig>,\n required: Record<string, keyof VideoDecoderConfig>,\n ): 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 delete required[meta.name];\n };\n const isTrackNumber = (element: Element): element is ContentElement<'uinteger'> =>\n element.name === 'TrackNumber' && isContentElement(element);\n\n const isTrackType = (element: Element): element is ContentElement<'uinteger'> =>\n element.name === 'TrackType' && isContentElement(element);\n\n const isVideoBlock = (candidate: { track: number }): boolean =>\n videoTrackNumber != null && candidate.track === videoTrackNumber;\n\n super({\n transform: async (element, controller) => {\n try {\n if (isMasterElement(element) && element.name === 'TrackEntry') {\n if (element.isClosing) {\n if (trackType === VIDEO && trackNumber != null) {\n videoTrackNumber = trackNumber;\n if (Object.keys(trackRequired).length === 0) {\n // console.info(`VideoChunkGenerator#${this.id}:config ${JSON.stringify(trackConfig)}`);\n this.#config.resolve(trackConfig as VideoDecoderConfig);\n }\n }\n inTrackEntry = false;\n trackNumber = undefined;\n trackType = undefined;\n trackConfig = {};\n trackRequired = createRequired();\n } else {\n inTrackEntry = true;\n trackNumber = undefined;\n trackType = undefined;\n trackConfig = {};\n trackRequired = createRequired();\n }\n return;\n }\n\n if (inTrackEntry && isTrackNumber(element)) {\n trackNumber = Number(element.value);\n return;\n }\n\n if (inTrackEntry && isTrackType(element)) {\n trackType = Number(element.value);\n return;\n }\n if (\n inTrackEntry &&\n isContentElement(element) &&\n trackRequired[element.name]\n ) {\n parseConfig(element, trackConfig, trackRequired);\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 (isVideoBlock(element)) {\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 && isVideoBlock(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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VideoChunkGenerator.d.mts","names":[],"sources":["../src/VideoChunkGenerator.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"VideoChunkGenerator.d.mts","names":[],"sources":["../src/VideoChunkGenerator.ts"],"mappings":";;;cA4BqB,mBAAA,SAA4B,eAAA,CAAgB,OAAA,EAAS,iBAAA;EAAA;WAG/D,QAAA;EAAA,IAEL,MAAA,CAAA,GAAU,OAAA,CAAQ,kBAAA;EAAA,SAIb,EAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as isDuration, c as isTimestamp, i as isContentElement,
|
|
1
|
+
import { a as isDuration, c as isTimestamp, i as isContentElement, n as isBlockElement, o as isMasterElement, r as isBlockGroupElement, s as isSimpleBlockElement } from "./Element-ChEar-fo.mjs";
|
|
2
2
|
import { o as readHexString } from "./tools-CYLlbo9J.mjs";
|
|
3
3
|
import { t as Deferred } from "./Deferred-7Cu0KIje.mjs";
|
|
4
4
|
//#region src/VideoChunkGenerator.ts
|
|
@@ -19,20 +19,23 @@ var VideoChunkGenerator = class extends TransformStream {
|
|
|
19
19
|
}
|
|
20
20
|
id = Date.now().toString(16).slice(-6);
|
|
21
21
|
constructor() {
|
|
22
|
+
let inTrackEntry = false;
|
|
23
|
+
let trackNumber;
|
|
22
24
|
let trackType;
|
|
25
|
+
let videoTrackNumber;
|
|
23
26
|
let offset = 0;
|
|
24
27
|
let block;
|
|
25
28
|
let hasReference = false;
|
|
26
29
|
let total = 0;
|
|
27
|
-
const
|
|
28
|
-
const elements = {};
|
|
29
|
-
const required = {
|
|
30
|
+
const createRequired = () => ({
|
|
30
31
|
PixelWidth: "codedWidth",
|
|
31
32
|
PixelHeight: "codedHeight",
|
|
32
33
|
CodecPrivate: "codec",
|
|
33
34
|
CodecID: "codec"
|
|
34
|
-
};
|
|
35
|
-
|
|
35
|
+
});
|
|
36
|
+
let trackConfig = {};
|
|
37
|
+
let trackRequired = createRequired();
|
|
38
|
+
const parseConfig = (meta, config, required) => {
|
|
36
39
|
switch (meta.name) {
|
|
37
40
|
case "CodecID": {
|
|
38
41
|
const codec = getCodec(meta);
|
|
@@ -54,19 +57,44 @@ var VideoChunkGenerator = class extends TransformStream {
|
|
|
54
57
|
if (name) config[name] = meta.value;
|
|
55
58
|
}
|
|
56
59
|
}
|
|
60
|
+
delete required[meta.name];
|
|
57
61
|
};
|
|
62
|
+
const isTrackNumber = (element) => element.name === "TrackNumber" && isContentElement(element);
|
|
63
|
+
const isTrackType = (element) => element.name === "TrackType" && isContentElement(element);
|
|
64
|
+
const isVideoBlock = (candidate) => videoTrackNumber != null && candidate.track === videoTrackNumber;
|
|
58
65
|
super({
|
|
59
66
|
transform: async (element, controller) => {
|
|
60
|
-
elements[element.name] = (elements[element.name] ?? 0) + 1;
|
|
61
67
|
try {
|
|
62
|
-
if (
|
|
63
|
-
|
|
68
|
+
if (isMasterElement(element) && element.name === "TrackEntry") {
|
|
69
|
+
if (element.isClosing) {
|
|
70
|
+
if (trackType === VIDEO && trackNumber != null) {
|
|
71
|
+
videoTrackNumber = trackNumber;
|
|
72
|
+
if (Object.keys(trackRequired).length === 0) this.#config.resolve(trackConfig);
|
|
73
|
+
}
|
|
74
|
+
inTrackEntry = false;
|
|
75
|
+
trackNumber = void 0;
|
|
76
|
+
trackType = void 0;
|
|
77
|
+
trackConfig = {};
|
|
78
|
+
trackRequired = createRequired();
|
|
79
|
+
} else {
|
|
80
|
+
inTrackEntry = true;
|
|
81
|
+
trackNumber = void 0;
|
|
82
|
+
trackType = void 0;
|
|
83
|
+
trackConfig = {};
|
|
84
|
+
trackRequired = createRequired();
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (inTrackEntry && isTrackNumber(element)) {
|
|
89
|
+
trackNumber = Number(element.value);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (inTrackEntry && isTrackType(element)) {
|
|
93
|
+
trackType = Number(element.value);
|
|
64
94
|
return;
|
|
65
95
|
}
|
|
66
|
-
if (isContentElement(element) &&
|
|
67
|
-
parseConfig(element);
|
|
68
|
-
delete required[element.name];
|
|
69
|
-
if (Object.keys(required).length === 0) this.#config.resolve(config);
|
|
96
|
+
if (inTrackEntry && isContentElement(element) && trackRequired[element.name]) {
|
|
97
|
+
parseConfig(element, trackConfig, trackRequired);
|
|
70
98
|
return;
|
|
71
99
|
}
|
|
72
100
|
if (isTimestamp(element)) {
|
|
@@ -78,7 +106,7 @@ var VideoChunkGenerator = class extends TransformStream {
|
|
|
78
106
|
return;
|
|
79
107
|
}
|
|
80
108
|
if (isSimpleBlockElement(element)) {
|
|
81
|
-
if (
|
|
109
|
+
if (isVideoBlock(element)) {
|
|
82
110
|
controller.enqueue(new EncodedVideoChunk({
|
|
83
111
|
type: element.keyframe ? "key" : "delta",
|
|
84
112
|
timestamp: (offset + element.value) * 1e3,
|
|
@@ -97,7 +125,7 @@ var VideoChunkGenerator = class extends TransformStream {
|
|
|
97
125
|
return;
|
|
98
126
|
}
|
|
99
127
|
if (isBlockGroupElement(element) && element.isClosing) {
|
|
100
|
-
if (block) {
|
|
128
|
+
if (block && isVideoBlock(block)) {
|
|
101
129
|
controller.enqueue(new EncodedVideoChunk({
|
|
102
130
|
type: hasReference ? "delta" : "key",
|
|
103
131
|
timestamp: (block.value + offset) * 1e3,
|
|
@@ -1 +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
|
|
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} 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 inTrackEntry = false;\n let trackNumber: number | undefined;\n let trackType: number | undefined;\n let videoTrackNumber: number | undefined;\n let offset = 0;\n let block: BlockElement | undefined;\n let hasReference = false;\n let total = 0;\n const createRequired = (): Record<string, keyof VideoDecoderConfig> => ({\n PixelWidth: 'codedWidth',\n PixelHeight: 'codedHeight',\n CodecPrivate: 'codec',\n CodecID: 'codec',\n });\n let trackConfig: Partial<VideoDecoderConfig> = {};\n let trackRequired = createRequired();\n\n const parseConfig = (\n meta: ContentElement,\n config: Partial<VideoDecoderConfig>,\n required: Record<string, keyof VideoDecoderConfig>,\n ): 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 delete required[meta.name];\n };\n const isTrackNumber = (element: Element): element is ContentElement<'uinteger'> =>\n element.name === 'TrackNumber' && isContentElement(element);\n\n const isTrackType = (element: Element): element is ContentElement<'uinteger'> =>\n element.name === 'TrackType' && isContentElement(element);\n\n const isVideoBlock = (candidate: { track: number }): boolean =>\n videoTrackNumber != null && candidate.track === videoTrackNumber;\n\n super({\n transform: async (element, controller) => {\n try {\n if (isMasterElement(element) && element.name === 'TrackEntry') {\n if (element.isClosing) {\n if (trackType === VIDEO && trackNumber != null) {\n videoTrackNumber = trackNumber;\n if (Object.keys(trackRequired).length === 0) {\n // console.info(`VideoChunkGenerator#${this.id}:config ${JSON.stringify(trackConfig)}`);\n this.#config.resolve(trackConfig as VideoDecoderConfig);\n }\n }\n inTrackEntry = false;\n trackNumber = undefined;\n trackType = undefined;\n trackConfig = {};\n trackRequired = createRequired();\n } else {\n inTrackEntry = true;\n trackNumber = undefined;\n trackType = undefined;\n trackConfig = {};\n trackRequired = createRequired();\n }\n return;\n }\n\n if (inTrackEntry && isTrackNumber(element)) {\n trackNumber = Number(element.value);\n return;\n }\n\n if (inTrackEntry && isTrackType(element)) {\n trackType = Number(element.value);\n return;\n }\n if (\n inTrackEntry &&\n isContentElement(element) &&\n trackRequired[element.name]\n ) {\n parseConfig(element, trackConfig, trackRequired);\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 (isVideoBlock(element)) {\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 && isVideoBlock(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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"}
|
|
@@ -56,6 +56,11 @@ var VideoFrameGenerator = class {
|
|
|
56
56
|
let readableController;
|
|
57
57
|
let decoder;
|
|
58
58
|
let finished = false;
|
|
59
|
+
let acceptChunks = true;
|
|
60
|
+
const getErrorMessage = (err) => err instanceof Error ? err.message : String(err);
|
|
61
|
+
const reportRecoverableError = (message, err) => {
|
|
62
|
+
postMessage({ debug: `${message}: ${getErrorMessage(err)}` });
|
|
63
|
+
};
|
|
59
64
|
const closeDecoder = () => {
|
|
60
65
|
if (decoder && decoder.state !== "closed") decoder.close();
|
|
61
66
|
};
|
|
@@ -77,6 +82,12 @@ var VideoFrameGenerator = class {
|
|
|
77
82
|
readableController = void 0;
|
|
78
83
|
}
|
|
79
84
|
};
|
|
85
|
+
const finish = () => {
|
|
86
|
+
acceptChunks = false;
|
|
87
|
+
closeDecoder();
|
|
88
|
+
finished = true;
|
|
89
|
+
drain();
|
|
90
|
+
};
|
|
80
91
|
const fail = (reason) => {
|
|
81
92
|
clear();
|
|
82
93
|
closeDecoder();
|
|
@@ -103,8 +114,9 @@ var VideoFrameGenerator = class {
|
|
|
103
114
|
},
|
|
104
115
|
error: (err) => {
|
|
105
116
|
console.error("error while decode", err);
|
|
117
|
+
reportRecoverableError("recoverable decoder error, ending video source", err);
|
|
106
118
|
controller.error(err);
|
|
107
|
-
|
|
119
|
+
finish();
|
|
108
120
|
}
|
|
109
121
|
});
|
|
110
122
|
try {
|
|
@@ -115,29 +127,31 @@ var VideoFrameGenerator = class {
|
|
|
115
127
|
console.error("error while configure", err);
|
|
116
128
|
}
|
|
117
129
|
},
|
|
118
|
-
write: async (chunk
|
|
130
|
+
write: async (chunk) => {
|
|
119
131
|
try {
|
|
132
|
+
if (!acceptChunks) return;
|
|
120
133
|
await capacity.acquire();
|
|
121
134
|
if (!decoder || decoder.state === "closed") {
|
|
122
|
-
|
|
135
|
+
capacity.release();
|
|
123
136
|
return;
|
|
124
137
|
}
|
|
125
138
|
decoder.decode(chunk);
|
|
126
139
|
} catch (e) {
|
|
127
140
|
if (decoder?.state !== "closed") {
|
|
128
141
|
console.error("error while decode chunk", e);
|
|
129
|
-
|
|
142
|
+
reportRecoverableError("dropping encoded video chunk after decode error", e);
|
|
143
|
+
capacity.release();
|
|
130
144
|
}
|
|
131
145
|
}
|
|
132
146
|
},
|
|
133
147
|
close: async () => {
|
|
134
148
|
try {
|
|
135
149
|
await decoder?.flush();
|
|
136
|
-
|
|
137
|
-
finished = true;
|
|
138
|
-
drain();
|
|
150
|
+
finish();
|
|
139
151
|
} catch (err) {
|
|
140
|
-
|
|
152
|
+
console.error("error while flush decoder", err);
|
|
153
|
+
reportRecoverableError("recoverable decoder flush error, ending video source", err);
|
|
154
|
+
finish();
|
|
141
155
|
}
|
|
142
156
|
},
|
|
143
157
|
abort: (reason) => {
|
|
@@ -1 +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 capacity = new Semaphore(maxPreloadFrames);\n let readableController: ReadableStreamDefaultController<VideoFrame> | undefined;\n let decoder: VideoDecoder | undefined;\n let finished = false;\n\n const closeDecoder = () => {\n if (decoder && decoder.state !== 'closed') decoder.close();\n };\n\n const clear = () => {\n pendingFrames.splice(0).forEach((frame) => frame.close());\n capacity.purge();\n };\n\n const drain = () => {\n if (!readableController) return;\n while (pendingFrames.length > 0 && (readableController.desiredSize ?? 0) > 0) {\n const frame = pendingFrames.shift();\n if (frame) {\n readableController.enqueue(frame);\n capacity.release();\n }\n }\n if (finished && pendingFrames.length === 0) {\n readableController.close();\n readableController = undefined;\n }\n };\n\n const fail = (reason: unknown) => {\n clear();\n closeDecoder();\n readableController?.error(reason);\n readableController = undefined;\n };\n\n this.readable = new ReadableStream<VideoFrame>(\n {\n start: (controller) => {\n readableController = controller;\n },\n pull: () => {\n drain();\n },\n cancel: (reason) => {\n fail(reason);\n },\n },\n new CountQueuingStrategy({ highWaterMark: 1 }),\n );\n this.writable = new WritableStream({\n start: async (controller) => {\n decoder = new VideoDecoder({\n output: (frame) => {\n pendingFrames.push(frame);\n drain();\n },\n error: (err) => {\n console.error('error while decode', err);\n controller.error(err);\n
|
|
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 capacity = new Semaphore(maxPreloadFrames);\n let readableController: ReadableStreamDefaultController<VideoFrame> | undefined;\n let decoder: VideoDecoder | undefined;\n let finished = false;\n let acceptChunks = true;\n\n const getErrorMessage = (err: unknown): string =>\n err instanceof Error ? err.message : String(err);\n\n const reportRecoverableError = (message: string, err: unknown) => {\n postMessage({ debug: `${message}: ${getErrorMessage(err)}` });\n };\n\n const closeDecoder = () => {\n if (decoder && decoder.state !== 'closed') decoder.close();\n };\n\n const clear = () => {\n pendingFrames.splice(0).forEach((frame) => frame.close());\n capacity.purge();\n };\n\n const drain = () => {\n if (!readableController) return;\n while (pendingFrames.length > 0 && (readableController.desiredSize ?? 0) > 0) {\n const frame = pendingFrames.shift();\n if (frame) {\n readableController.enqueue(frame);\n capacity.release();\n }\n }\n if (finished && pendingFrames.length === 0) {\n readableController.close();\n readableController = undefined;\n }\n };\n\n const finish = () => {\n acceptChunks = false;\n closeDecoder();\n finished = true;\n drain();\n };\n\n const fail = (reason: unknown) => {\n clear();\n closeDecoder();\n readableController?.error(reason);\n readableController = undefined;\n };\n\n this.readable = new ReadableStream<VideoFrame>(\n {\n start: (controller) => {\n readableController = controller;\n },\n pull: () => {\n drain();\n },\n cancel: (reason) => {\n fail(reason);\n },\n },\n new CountQueuingStrategy({ highWaterMark: 1 }),\n );\n this.writable = new WritableStream({\n start: async (controller) => {\n decoder = new VideoDecoder({\n output: (frame) => {\n pendingFrames.push(frame);\n drain();\n },\n error: (err) => {\n console.error('error while decode', err);\n reportRecoverableError('recoverable decoder error, ending video source', err);\n controller.error(err);\n finish();\n },\n });\n try {\n decoder.configure(await this.config);\n } catch (err) {\n controller.error(err);\n fail(err);\n console.error('error while configure', err);\n }\n },\n write: async (chunk) => {\n try {\n if (!acceptChunks) return;\n await capacity.acquire();\n if (!decoder || decoder.state === 'closed') {\n capacity.release();\n return;\n }\n decoder.decode(chunk);\n } catch (e) {\n if (decoder?.state !== 'closed') {\n console.error('error while decode chunk', e);\n reportRecoverableError('dropping encoded video chunk after decode error', e);\n capacity.release();\n }\n }\n },\n close: async () => {\n try {\n await decoder?.flush();\n finish();\n } catch (err) {\n console.error('error while flush decoder', err);\n reportRecoverableError('recoverable decoder flush error, ending video source', err);\n finish();\n }\n },\n abort: (reason) => {\n fail(reason);\n },\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -52,6 +52,11 @@ var VideoFrameGenerator = class {
|
|
|
52
52
|
let readableController;
|
|
53
53
|
let decoder;
|
|
54
54
|
let finished = false;
|
|
55
|
+
let acceptChunks = true;
|
|
56
|
+
const getErrorMessage = (err) => err instanceof Error ? err.message : String(err);
|
|
57
|
+
const reportRecoverableError = (message, err) => {
|
|
58
|
+
postMessage({ debug: `${message}: ${getErrorMessage(err)}` });
|
|
59
|
+
};
|
|
55
60
|
const closeDecoder = () => {
|
|
56
61
|
if (decoder && decoder.state !== "closed") decoder.close();
|
|
57
62
|
};
|
|
@@ -73,6 +78,12 @@ var VideoFrameGenerator = class {
|
|
|
73
78
|
readableController = void 0;
|
|
74
79
|
}
|
|
75
80
|
};
|
|
81
|
+
const finish = () => {
|
|
82
|
+
acceptChunks = false;
|
|
83
|
+
closeDecoder();
|
|
84
|
+
finished = true;
|
|
85
|
+
drain();
|
|
86
|
+
};
|
|
76
87
|
const fail = (reason) => {
|
|
77
88
|
clear();
|
|
78
89
|
closeDecoder();
|
|
@@ -99,8 +110,9 @@ var VideoFrameGenerator = class {
|
|
|
99
110
|
},
|
|
100
111
|
error: (err) => {
|
|
101
112
|
console.error("error while decode", err);
|
|
113
|
+
reportRecoverableError("recoverable decoder error, ending video source", err);
|
|
102
114
|
controller.error(err);
|
|
103
|
-
|
|
115
|
+
finish();
|
|
104
116
|
}
|
|
105
117
|
});
|
|
106
118
|
try {
|
|
@@ -111,29 +123,31 @@ var VideoFrameGenerator = class {
|
|
|
111
123
|
console.error("error while configure", err);
|
|
112
124
|
}
|
|
113
125
|
},
|
|
114
|
-
write: async (chunk
|
|
126
|
+
write: async (chunk) => {
|
|
115
127
|
try {
|
|
128
|
+
if (!acceptChunks) return;
|
|
116
129
|
await capacity.acquire();
|
|
117
130
|
if (!decoder || decoder.state === "closed") {
|
|
118
|
-
|
|
131
|
+
capacity.release();
|
|
119
132
|
return;
|
|
120
133
|
}
|
|
121
134
|
decoder.decode(chunk);
|
|
122
135
|
} catch (e) {
|
|
123
136
|
if (decoder?.state !== "closed") {
|
|
124
137
|
console.error("error while decode chunk", e);
|
|
125
|
-
|
|
138
|
+
reportRecoverableError("dropping encoded video chunk after decode error", e);
|
|
139
|
+
capacity.release();
|
|
126
140
|
}
|
|
127
141
|
}
|
|
128
142
|
},
|
|
129
143
|
close: async () => {
|
|
130
144
|
try {
|
|
131
145
|
await decoder?.flush();
|
|
132
|
-
|
|
133
|
-
finished = true;
|
|
134
|
-
drain();
|
|
146
|
+
finish();
|
|
135
147
|
} catch (err) {
|
|
136
|
-
|
|
148
|
+
console.error("error while flush decoder", err);
|
|
149
|
+
reportRecoverableError("recoverable decoder flush error, ending video source", err);
|
|
150
|
+
finish();
|
|
137
151
|
}
|
|
138
152
|
},
|
|
139
153
|
abort: (reason) => {
|
|
@@ -1 +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 capacity = new Semaphore(maxPreloadFrames);\n let readableController: ReadableStreamDefaultController<VideoFrame> | undefined;\n let decoder: VideoDecoder | undefined;\n let finished = false;\n\n const closeDecoder = () => {\n if (decoder && decoder.state !== 'closed') decoder.close();\n };\n\n const clear = () => {\n pendingFrames.splice(0).forEach((frame) => frame.close());\n capacity.purge();\n };\n\n const drain = () => {\n if (!readableController) return;\n while (pendingFrames.length > 0 && (readableController.desiredSize ?? 0) > 0) {\n const frame = pendingFrames.shift();\n if (frame) {\n readableController.enqueue(frame);\n capacity.release();\n }\n }\n if (finished && pendingFrames.length === 0) {\n readableController.close();\n readableController = undefined;\n }\n };\n\n const fail = (reason: unknown) => {\n clear();\n closeDecoder();\n readableController?.error(reason);\n readableController = undefined;\n };\n\n this.readable = new ReadableStream<VideoFrame>(\n {\n start: (controller) => {\n readableController = controller;\n },\n pull: () => {\n drain();\n },\n cancel: (reason) => {\n fail(reason);\n },\n },\n new CountQueuingStrategy({ highWaterMark: 1 }),\n );\n this.writable = new WritableStream({\n start: async (controller) => {\n decoder = new VideoDecoder({\n output: (frame) => {\n pendingFrames.push(frame);\n drain();\n },\n error: (err) => {\n console.error('error while decode', err);\n controller.error(err);\n
|
|
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 capacity = new Semaphore(maxPreloadFrames);\n let readableController: ReadableStreamDefaultController<VideoFrame> | undefined;\n let decoder: VideoDecoder | undefined;\n let finished = false;\n let acceptChunks = true;\n\n const getErrorMessage = (err: unknown): string =>\n err instanceof Error ? err.message : String(err);\n\n const reportRecoverableError = (message: string, err: unknown) => {\n postMessage({ debug: `${message}: ${getErrorMessage(err)}` });\n };\n\n const closeDecoder = () => {\n if (decoder && decoder.state !== 'closed') decoder.close();\n };\n\n const clear = () => {\n pendingFrames.splice(0).forEach((frame) => frame.close());\n capacity.purge();\n };\n\n const drain = () => {\n if (!readableController) return;\n while (pendingFrames.length > 0 && (readableController.desiredSize ?? 0) > 0) {\n const frame = pendingFrames.shift();\n if (frame) {\n readableController.enqueue(frame);\n capacity.release();\n }\n }\n if (finished && pendingFrames.length === 0) {\n readableController.close();\n readableController = undefined;\n }\n };\n\n const finish = () => {\n acceptChunks = false;\n closeDecoder();\n finished = true;\n drain();\n };\n\n const fail = (reason: unknown) => {\n clear();\n closeDecoder();\n readableController?.error(reason);\n readableController = undefined;\n };\n\n this.readable = new ReadableStream<VideoFrame>(\n {\n start: (controller) => {\n readableController = controller;\n },\n pull: () => {\n drain();\n },\n cancel: (reason) => {\n fail(reason);\n },\n },\n new CountQueuingStrategy({ highWaterMark: 1 }),\n );\n this.writable = new WritableStream({\n start: async (controller) => {\n decoder = new VideoDecoder({\n output: (frame) => {\n pendingFrames.push(frame);\n drain();\n },\n error: (err) => {\n console.error('error while decode', err);\n reportRecoverableError('recoverable decoder error, ending video source', err);\n controller.error(err);\n finish();\n },\n });\n try {\n decoder.configure(await this.config);\n } catch (err) {\n controller.error(err);\n fail(err);\n console.error('error while configure', err);\n }\n },\n write: async (chunk) => {\n try {\n if (!acceptChunks) return;\n await capacity.acquire();\n if (!decoder || decoder.state === 'closed') {\n capacity.release();\n return;\n }\n decoder.decode(chunk);\n } catch (e) {\n if (decoder?.state !== 'closed') {\n console.error('error while decode chunk', e);\n reportRecoverableError('dropping encoded video chunk after decode error', e);\n capacity.release();\n }\n }\n },\n close: async () => {\n try {\n await decoder?.flush();\n finish();\n } catch (err) {\n console.error('error while flush decoder', err);\n reportRecoverableError('recoverable decoder flush error, ending video source', err);\n finish();\n }\n },\n abort: (reason) => {\n fail(reason);\n },\n });\n }\n}\n"],"mappings":"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"}
|