Skip to content

Commit 9541bba

Browse files
committed
Release v0.3.2: add advanced codec options and worker reliability fixes
1 parent 00c9df3 commit 9541bba

15 files changed

Lines changed: 759 additions & 239 deletions

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.3.2] - 2026-02-07
9+
10+
### ✨ Added
11+
- Added advanced codec option support for WebCodecs:
12+
- `video.codecString` / `audio.codecString`
13+
- `video.quantizer`
14+
- `video.avc.format` / `video.hevc.format`
15+
- `audio.aac.format`
16+
- Exported codec option helper types for those advanced fields from the public API.
17+
18+
### 🛠️ Improved
19+
- `canEncode()` now probes codec-specific configuration details (codec strings and codec-specific option blocks) for more accurate capability checks in Chrome/WebCodecs environments.
20+
- Worker initialization now applies and sanitizes codec-specific encoder overrides so only options valid for the selected codec are passed through.
21+
- Added configurable external worker URL support via `WEBCODECS_WORKER_URL` and `window.__WEBCODECS_WORKER_URL__`.
22+
23+
### 🐛 Fixed
24+
- Fixed audio-only initialization to skip unnecessary video encoder setup and capability checks.
25+
- Fixed `AudioData` lifecycle handling to ensure internally created or provided audio frames are always closed after encode attempts.
26+
- Improved worker error forwarding so error handlers receive worker errors consistently even when handler registration is delayed.
27+
28+
### 🧪 Tests
29+
- Expanded coverage for audio-only worker initialization, codec override parsing, capability probe option forwarding, and worker error propagation behavior.
30+
31+
---
32+
833
## [0.3.1] - 2025-09-22
934

1035
### 🎧 Added

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Inline worker controls:
4747
| `WEBCODECS_USE_INLINE_WORKER=true` or `window.__WEBCODECS_USE_INLINE_WORKER__ = true` | Force the inline mock (useful for Storybook, unit tests, etc.). |
4848
| `WEBCODECS_DISABLE_INLINE_WORKER=true` or `window.__WEBCODECS_DISABLE_INLINE_WORKER__ = true` | Always require the external worker. |
4949
| `WEBCODECS_ALLOW_INLINE_IN_PROD=true` or `window.__WEBCODECS_ALLOW_INLINE_IN_PROD__ = true` | Explicitly permit the inline mock on production builds (not recommended). |
50+
| `WEBCODECS_WORKER_URL=/assets/webcodecs-worker.js` or `window.__WEBCODECS_WORKER_URL__ = '/assets/webcodecs-worker.js'` | Override the external worker URL when your app is served from a sub-path/CDN. |
5051

5152
> ⚠️ The inline worker is a **test stub** that returns placeholder bytes. Use it only for wiring/UI development. Real MP4/WebM output requires the external worker bundle.
5253
@@ -213,18 +214,24 @@ interface EncodeOptions {
213214
/** Set to `false` for audio-only encoding. */
214215
video?: {
215216
codec?: 'avc' | 'hevc' | 'vp9' | 'vp8' | 'av1';
217+
codecString?: string; // e.g. 'avc1.640028'
216218
bitrate?: number;
219+
quantizer?: number;
220+
avc?: { format?: 'annexb' | 'avc' };
221+
hevc?: { format?: 'annexb' | 'hevc' };
217222
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
218223
keyFrameInterval?: number;
219224
} | false;
220225
221226
/** Set to `false` to disable audio. */
222227
audio?: {
223228
codec?: 'aac' | 'mp3' | 'opus' | 'vorbis' | 'flac';
229+
codecString?: string; // e.g. 'mp4a.40.2'
224230
bitrate?: number;
225231
sampleRate?: number;
226232
channels?: number;
227233
bitrateMode?: 'constant' | 'variable';
234+
aac?: { format?: 'aac' | 'adts' };
228235
} | false;
229236
230237
container?: 'mp4' | 'webm';

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "webcodecs-encoder",
3-
"version": "0.3.1",
3+
"version": "0.3.2",
44
"description": "A TypeScript library for browser environments to encode video (H.264/AVC, VP9, VP8) and audio (AAC, Opus) using the WebCodecs API and mux them into MP4 or WebM containers with real-time streaming support. New function-first API design.",
55
"homepage": "https://github.com/romot-co/webcodecs-encoder",
66
"repository": {

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export type {
1717
QualityPreset,
1818
VideoConfig,
1919
AudioConfig,
20+
AvcBitstreamFormatOption,
21+
HevcBitstreamFormatOption,
22+
AacBitstreamFormatOption,
2023
ProgressInfo,
2124
EncodeErrorType,
2225
VideoFile,

src/types.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,29 @@ export type VideoSource =
1919
// Quality presets
2020
export type QualityPreset = 'low' | 'medium' | 'high' | 'lossless';
2121

22+
export type AvcBitstreamFormatOption = "annexb" | "avc";
23+
export type HevcBitstreamFormatOption = "annexb" | "hevc";
24+
export type AacBitstreamFormatOption = "aac" | "adts";
25+
2226
// Video configuration
2327
export interface VideoConfig {
2428
codec?: 'avc' | 'hevc' | 'vp9' | 'vp8' | 'av1';
29+
/** Override codec string passed to VideoEncoder (e.g. "avc1.640028"). */
30+
codecString?: string;
2531
bitrate?: number;
32+
/**
33+
* Optional quantizer hint. Browser support varies by codec/platform.
34+
* When set, it is forwarded to VideoEncoderConfig.
35+
*/
36+
quantizer?: number;
37+
/** AVC-specific options. */
38+
avc?: {
39+
format?: AvcBitstreamFormatOption;
40+
};
41+
/** HEVC-specific options. */
42+
hevc?: {
43+
format?: HevcBitstreamFormatOption;
44+
};
2645
hardwareAcceleration?: 'no-preference' | 'prefer-hardware' | 'prefer-software';
2746
latencyMode?: 'quality' | 'realtime';
2847
keyFrameInterval?: number;
@@ -41,10 +60,16 @@ export type AudioCodec =
4160

4261
export interface AudioConfig {
4362
codec?: AudioCodec;
63+
/** Override codec string passed to AudioEncoder (e.g. "mp4a.40.2"). */
64+
codecString?: string;
4465
bitrate?: number;
4566
sampleRate?: number;
4667
channels?: number;
4768
bitrateMode?: 'constant' | 'variable';
69+
/** AAC-specific options. */
70+
aac?: {
71+
format?: AacBitstreamFormatOption;
72+
};
4873
}
4974

5075
// Progress information

src/utils/can-encode.ts

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,28 @@ export async function canEncode(options?: EncodeOptions): Promise<boolean> {
3636
}
3737
}
3838

39-
// Check audio configuration (only if audio is explicitly specified)
39+
// Check audio configuration
4040
const hasAudioConfig = options.audio && typeof options.audio === "object";
41-
if (hasAudioConfig) {
42-
const audioCodec = (options.audio as AudioConfig).codec || "aac";
43-
const audioSupported = await testAudioCodecSupport(audioCodec, options);
44-
if (!audioSupported) {
45-
return false;
46-
}
47-
} else if (options.audio === undefined && !hasVideoConfig) {
48-
// Only check audio for default configuration
49-
const audioSupported = await testAudioCodecSupport("aac", options);
50-
if (!audioSupported) {
51-
return false;
41+
const audioEnabled = options.audio !== false;
42+
if (audioEnabled) {
43+
if (hasAudioConfig) {
44+
const audioCodec = (options.audio as AudioConfig).codec || "aac";
45+
const audioSupported = await testAudioCodecSupport(audioCodec, options);
46+
if (!audioSupported) {
47+
return false;
48+
}
49+
} else {
50+
const fallbackCodecs = getDefaultAudioProbeOrder(options.container);
51+
let foundSupportedAudioCodec = false;
52+
for (const codec of fallbackCodecs) {
53+
if (await testAudioCodecSupport(codec, options)) {
54+
foundSupportedAudioCodec = true;
55+
break;
56+
}
57+
}
58+
if (!foundSupportedAudioCodec) {
59+
return false;
60+
}
5261
}
5362
}
5463

@@ -128,12 +137,14 @@ async function testVideoCodecSupport(
128137
try {
129138
const videoOptions =
130139
options?.video && typeof options.video === "object" ? options.video : {};
131-
const codecString = getVideoCodecString(
132-
codec,
133-
options?.width || 640,
134-
options?.height || 480,
135-
options?.frameRate || 30,
136-
);
140+
const codecString =
141+
videoOptions.codecString ||
142+
getVideoCodecString(
143+
codec,
144+
options?.width || 640,
145+
options?.height || 480,
146+
options?.frameRate || 30,
147+
);
137148
const config: VideoEncoderConfig = {
138149
codec: codecString,
139150
width: options?.width || 640,
@@ -151,6 +162,16 @@ async function testVideoCodecSupport(
151162
config.latencyMode = videoOptions.latencyMode;
152163
}
153164

165+
if (typeof videoOptions.quantizer === "number") {
166+
(config as any).quantizer = videoOptions.quantizer;
167+
}
168+
if (codec === "avc" && videoOptions.avc?.format) {
169+
(config as any).avc = { format: videoOptions.avc.format };
170+
}
171+
if (codec === "hevc" && videoOptions.hevc?.format) {
172+
(config as any).hevc = { format: videoOptions.hevc.format };
173+
}
174+
154175
const support = await VideoEncoder.isConfigSupported(config);
155176
return support.supported || false;
156177
} catch {
@@ -166,9 +187,9 @@ async function testAudioCodecSupport(
166187
options?: EncodeOptions,
167188
): Promise<boolean> {
168189
try {
169-
const codecString = getAudioCodecString(codec);
170190
const audioOptions =
171191
typeof options?.audio === "object" ? options.audio : {};
192+
const codecString = audioOptions.codecString || getAudioCodecString(codec);
172193

173194
const isTelephonyCodec = codec === "ulaw" || codec === "alaw";
174195
const isPcmCodec = codec === "pcm";
@@ -204,6 +225,9 @@ async function testAudioCodecSupport(
204225
if (codec === "aac" && audioOptions.bitrateMode) {
205226
(config as any).bitrateMode = audioOptions.bitrateMode;
206227
}
228+
if (codec === "aac" && audioOptions.aac?.format) {
229+
(config as any).aac = { format: audioOptions.aac.format };
230+
}
207231

208232
const support = await AudioEncoder.isConfigSupported(config);
209233
return support.supported || false;
@@ -341,6 +365,15 @@ function getAudioCodecString(codec: string): string {
341365
}
342366
}
343367

368+
function getDefaultAudioProbeOrder(
369+
container?: EncodeOptions["container"],
370+
): string[] {
371+
if (container === "webm") {
372+
return ["opus", "vorbis", "flac"];
373+
}
374+
return ["aac", "mp3"];
375+
}
376+
344377
/**
345378
* Check support for specific codec and profile (for advanced users)
346379
*/

src/utils/config-parser.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -214,46 +214,63 @@ function applyQualityPreset(
214214
* EncodeOptionsから内部のEncoderConfigに変換
215215
*/
216216
function convertToEncoderConfig(options: EncodeOptions): EncoderConfig {
217+
const videoOptions =
218+
options.video && typeof options.video === "object" ? options.video : null;
219+
217220
const config: EncoderConfig = {
218221
width: options.video === false ? 0 : options.width || 640,
219222
height: options.video === false ? 0 : options.height || 480,
220223
frameRate: options.frameRate || 30,
221224
videoBitrate:
222-
options.video === false
223-
? 0
224-
: (options.video as any)?.bitrate || 1_000_000,
225+
options.video === false ? 0 : videoOptions?.bitrate || 1_000_000,
225226
audioBitrate: 0,
226227
sampleRate: 0,
227228
channels: 0,
228229
container: options.container || "mp4",
229230
codec: {
230-
video:
231-
options.video === false
232-
? undefined
233-
: (options.video as any)?.codec || "avc",
231+
video: options.video === false ? undefined : videoOptions?.codec || "avc",
234232
audio: undefined,
235233
},
236234
latencyMode:
237235
options.video === false
238236
? "quality"
239-
: options.latencyMode ||
240-
(options.video as any)?.latencyMode ||
241-
"quality",
237+
: options.latencyMode || videoOptions?.latencyMode || "quality",
242238
hardwareAcceleration:
243239
options.video === false
244240
? "no-preference"
245-
: (options.video as any)?.hardwareAcceleration || "no-preference",
241+
: videoOptions?.hardwareAcceleration || "no-preference",
246242
keyFrameInterval:
247-
options.video === false
248-
? undefined
249-
: (options.video as any)?.keyFrameInterval,
243+
options.video === false ? undefined : videoOptions?.keyFrameInterval,
250244
audioBitrateMode: undefined,
251245
firstTimestampBehavior: options.firstTimestampBehavior || "offset",
252246
maxVideoQueueSize: options.maxVideoQueueSize || 30,
253247
maxAudioQueueSize: options.maxAudioQueueSize || 30,
254248
backpressureStrategy: options.backpressureStrategy || "drop",
255249
};
256250

251+
if (options.video !== false && videoOptions?.codecString) {
252+
config.codecString = {
253+
...(config.codecString ?? {}),
254+
video: videoOptions.codecString,
255+
};
256+
}
257+
258+
if (options.video !== false && videoOptions) {
259+
const videoEncoderConfig: Partial<VideoEncoderConfig> = {};
260+
if (typeof videoOptions.quantizer === "number") {
261+
(videoEncoderConfig as any).quantizer = videoOptions.quantizer;
262+
}
263+
if (config.codec?.video === "avc" && videoOptions.avc?.format) {
264+
(videoEncoderConfig as any).avc = { format: videoOptions.avc.format };
265+
}
266+
if (config.codec?.video === "hevc" && videoOptions.hevc?.format) {
267+
(videoEncoderConfig as any).hevc = { format: videoOptions.hevc.format };
268+
}
269+
if (Object.keys(videoEncoderConfig).length > 0) {
270+
config.videoEncoderConfig = videoEncoderConfig;
271+
}
272+
}
273+
257274
if (options.audio !== false) {
258275
const audioOptions = (options.audio as any) || {};
259276
const requestedCodec = (audioOptions.codec || "aac") as any;
@@ -292,6 +309,21 @@ function convertToEncoderConfig(options: EncodeOptions): EncoderConfig {
292309
config.audioBitrateMode =
293310
audioOptions.bitrateMode ||
294311
(requestedCodec === "aac" ? "variable" : "constant");
312+
313+
if (audioOptions.codecString) {
314+
config.codecString = {
315+
...(config.codecString ?? {}),
316+
audio: audioOptions.codecString,
317+
};
318+
}
319+
320+
const audioEncoderConfig: Partial<AudioEncoderConfig> = {};
321+
if (requestedCodec === "aac" && audioOptions.aac?.format) {
322+
(audioEncoderConfig as any).aac = { format: audioOptions.aac.format };
323+
}
324+
if (Object.keys(audioEncoderConfig).length > 0) {
325+
config.audioEncoderConfig = audioEncoderConfig;
326+
}
295327
}
296328

297329
if (options.audio === false) {

0 commit comments

Comments
 (0)