@sarakusha/ebml 0.0.6 → 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
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
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
|
+
|
|
5
13
|
### [0.0.6](https://github.com/sarakusha/ebml/compare/v0.0.5...v0.0.6) (2026-06-25)
|
|
6
14
|
|
|
7
15
|
|
|
@@ -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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"}
|