Skip to content

Commit 7897a15

Browse files
committed
Release v0.2.1: Add VideoFile support and remove AudioWorklet - Add VideoFile processing support in core/encode.ts - Remove unused AudioWorklet feature (was unimplemented) - Update version to 0.2.1 in package.json and README.md - Simplify MediaStreamRecorder to use only MediaStreamTrackProcessor - Clean up build configuration and exports - Update tests and maintain test coverage
1 parent 6aad064 commit 7897a15

13 files changed

+102
-525
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
A TypeScript library 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 a simple, function-first design.
44

5-
> **🎉 v1.0.0 Release**
6-
> This is the stable release with the new function-first API. The API is now simplified and production-ready with automatic configuration, quality presets, and progressive enhancement.
5+
> **🎉 v0.2.1 Release**
6+
> This release includes VideoFile support and improved API stability. The function-first API is production-ready with automatic configuration, quality presets, and progressive enhancement.
77
88
## Features
99

package.json

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "webcodecs-encoder",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
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": {
@@ -35,12 +35,7 @@
3535
"require": "./dist/utils/can-encode.cjs",
3636
"import": "./dist/utils/can-encode.js"
3737
},
38-
"./worker": "./dist/worker.js",
39-
"./audio-worklet": {
40-
"types": "./dist/audio-worklet.d.ts",
41-
"require": "./dist/audio-worklet.cjs",
42-
"import": "./dist/audio-worklet.js"
43-
}
38+
"./worker": "./dist/worker.js"
4439
},
4540
"scripts": {
4641
"build:main": "tsup",

scripts/postinstall.js

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
1616
// Find the package root (where this script is located)
1717
const packageRoot = path.resolve(__dirname, '..');
1818
const workerSource = path.join(packageRoot, 'dist', 'worker.js');
19-
const audioWorkletSource = path.join(packageRoot, 'dist', 'audio-worklet.js');
2019

2120
// Also check the actual file location for better detection
2221
const scriptPath = fileURLToPath(import.meta.url);
@@ -98,12 +97,10 @@ function installWorker() {
9897
const publicDir = path.join(projectRoot, pattern);
9998
if (fs.existsSync(publicDir)) {
10099
const workerDestination = path.join(publicDir, 'webcodecs-worker.js');
101-
const audioWorkletDestination = path.join(publicDir, 'webcodecs-audio-worklet.js');
102100

103101
const workerCopied = safeCopy(workerSource, workerDestination);
104-
const audioWorkletCopied = safeCopy(audioWorkletSource, audioWorkletDestination);
105102

106-
if (workerCopied || audioWorkletCopied) {
103+
if (workerCopied) {
107104
copied = true;
108105
break; // Only copy to the first available public directory
109106
}
@@ -114,19 +111,17 @@ function installWorker() {
114111
// Try to create a public directory
115112
const publicDir = path.join(projectRoot, 'public');
116113
const workerDestination = path.join(publicDir, 'webcodecs-worker.js');
117-
const audioWorkletDestination = path.join(publicDir, 'webcodecs-audio-worklet.js');
118114

119115
const workerCopied = safeCopy(workerSource, workerDestination);
120-
const audioWorkletCopied = safeCopy(audioWorkletSource, audioWorkletDestination);
121116

122-
if (workerCopied || audioWorkletCopied) {
117+
if (workerCopied) {
123118
copied = true;
124119
}
125120
}
126121

127122
if (copied) {
128123
console.log('\n🎉 WebCodecs Encoder is ready to use!');
129-
console.log('Worker and AudioWorklet files have been automatically copied to your public directory.');
124+
console.log('Worker files have been automatically copied to your public directory.');
130125
console.log('\nUsage:');
131126
console.log(' import { encode, canEncode } from "webcodecs-encoder";');
132127
console.log(' const isSupported = await canEncode();');
@@ -135,7 +130,6 @@ function installWorker() {
135130
console.log('\n⚠️ WebCodecs Encoder setup info:');
136131
console.log('Unable to auto-copy worker files. You may need to copy them manually:');
137132
console.log(' 1. Copy node_modules/webcodecs-encoder/dist/worker.js to your public directory as webcodecs-worker.js');
138-
console.log(' 2. Copy node_modules/webcodecs-encoder/dist/audio-worklet.js to your public directory as webcodecs-audio-worklet.js');
139133
console.log('\nOr specify custom worker URLs in your bundler configuration.');
140134
}
141135
}

src/core/encode.ts

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
EncodeError,
99
Frame,
1010
ProgressInfo,
11+
VideoFile,
1112
} from "../types";
1213
import { inferAndBuildConfig } from "../utils/config-parser";
1314
import { WorkerCommunicator } from "../worker/worker-communicator";
@@ -163,11 +164,8 @@ async function processVideoSource(
163164
// AsyncIterableの処理
164165
await processAsyncIterable(communicator, source);
165166
} else {
166-
// VideoFileの処理(今回は基本実装)
167-
throw new EncodeError(
168-
"invalid-input",
169-
"VideoFile processing not yet implemented",
170-
);
167+
// VideoFileの処理
168+
await processVideoFile(communicator, source as VideoFile, config);
171169
}
172170
}
173171

@@ -422,3 +420,88 @@ async function convertToVideoFrame(
422420
`Unsupported frame type: ${typeof frame}. Frame must be VideoFrame, HTMLCanvasElement, OffscreenCanvas, ImageBitmap, or ImageData.`,
423421
);
424422
}
423+
424+
/**
425+
* VideoFileを処理してフレームを抽出
426+
*/
427+
async function processVideoFile(
428+
communicator: WorkerCommunicator,
429+
videoFile: VideoFile,
430+
config: any,
431+
): Promise<void> {
432+
try {
433+
// HTML5 Video要素を作成してファイルを読み込み
434+
const video = document.createElement("video");
435+
video.muted = true;
436+
video.preload = "metadata";
437+
438+
// ファイルをオブジェクトURLとして設定
439+
const objectUrl = URL.createObjectURL(videoFile.file);
440+
video.src = objectUrl;
441+
442+
await new Promise<void>((resolve, reject) => {
443+
video.onloadedmetadata = () => resolve();
444+
video.onerror = () => reject(new Error("Failed to load video file"));
445+
});
446+
447+
// 動画の情報を取得
448+
const { duration, videoWidth, videoHeight } = video;
449+
const frameRate = config.frameRate || 30;
450+
const totalFrames = Math.floor(duration * frameRate);
451+
452+
// Canvasを作成してフレームを抽出
453+
const canvas = document.createElement("canvas");
454+
canvas.width = videoWidth;
455+
canvas.height = videoHeight;
456+
const ctx = canvas.getContext("2d");
457+
458+
if (!ctx) {
459+
throw new EncodeError(
460+
"initialization-failed",
461+
"Failed to get canvas context",
462+
);
463+
}
464+
465+
// 動画の各フレームを処理
466+
for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
467+
const timestamp = frameIndex / frameRate;
468+
469+
// 動画の指定時間にシーク
470+
video.currentTime = timestamp;
471+
472+
await new Promise<void>((resolve) => {
473+
video.onseeked = () => resolve();
474+
// タイムアウト処理を追加してデッドロックを防止
475+
setTimeout(() => resolve(), 100);
476+
});
477+
478+
// Canvasに現在のフレームを描画
479+
ctx.drawImage(video, 0, 0, videoWidth, videoHeight);
480+
481+
// VideoFrameを作成
482+
const videoFrame = new VideoFrame(canvas, {
483+
timestamp: frameIndex * (1000000 / frameRate), // マイクロ秒
484+
});
485+
486+
// ワーカーに送信
487+
await addFrameToWorker(
488+
communicator,
489+
videoFrame,
490+
frameIndex * (1000000 / frameRate),
491+
);
492+
493+
// フレームをクローズしてメモリリークを防止
494+
videoFrame.close();
495+
}
496+
497+
// リソースをクリーンアップ
498+
URL.revokeObjectURL(objectUrl);
499+
video.remove();
500+
} catch (error) {
501+
throw new EncodeError(
502+
"invalid-input",
503+
`VideoFile processing failed: ${error instanceof Error ? error.message : String(error)}`,
504+
error,
505+
);
506+
}
507+
}

src/mediastream-recorder.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import { inferAndBuildConfig } from "./utils/config-parser";
66
export interface MediaStreamRecorderOptions extends EncodeOptions {
77
/** 最初のタイムスタンプの処理方法 */
88
firstTimestampBehavior?: "offset" | "strict";
9-
/** AudioWorkletを使用するかどうか */
10-
useAudioWorklet?: boolean;
119
}
1210

1311
export class MediaStreamRecorder {
@@ -75,16 +73,12 @@ export class MediaStreamRecorder {
7573

7674
if (aTrack) {
7775
this.audioTrack = aTrack;
78-
if (mergedOptions.useAudioWorklet) {
79-
await this.setupAudioWorklet(stream);
80-
} else {
81-
const processor = new MediaStreamTrackProcessor({
82-
track: aTrack,
83-
});
84-
this.audioReader =
85-
processor.readable.getReader() as ReadableStreamDefaultReader<AudioData>;
86-
this.processAudio();
87-
}
76+
const processor = new MediaStreamTrackProcessor({
77+
track: aTrack,
78+
});
79+
this.audioReader =
80+
processor.readable.getReader() as ReadableStreamDefaultReader<AudioData>;
81+
this.processAudio();
8882
}
8983
} catch (error) {
9084
this.cleanup();
@@ -148,12 +142,6 @@ export class MediaStreamRecorder {
148142
});
149143
}
150144

151-
private async setupAudioWorklet(_stream: MediaStream): Promise<void> {
152-
// AudioWorkletのセットアップは複雑なため、基本実装のみ
153-
// 実際の実装では AudioContext と AudioWorkletNode が必要
154-
throw new EncodeError('not-supported', 'AudioWorklet setup not yet implemented for new API');
155-
}
156-
157145
private async processVideo(): Promise<void> {
158146
if (!this.videoReader || !this.communicator) return;
159147
const reader = this.videoReader;

src/types.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -215,18 +215,12 @@ export interface CancelWorkerMessage {
215215
type: "cancel";
216216
}
217217

218-
export interface ConnectAudioPortMessage {
219-
type: "connectAudioPort";
220-
port: MessagePort;
221-
}
222-
223218
export type WorkerMessage =
224219
| InitializeWorkerMessage
225220
| AddVideoFrameMessage
226221
| AddAudioDataMessage
227222
| FinalizeWorkerMessage
228-
| CancelWorkerMessage
229-
| ConnectAudioPortMessage;
223+
| CancelWorkerMessage;
230224

231225
// Messages FROM the Worker
232226
export interface WorkerInitializedMessage {

src/worker/audio-worklet.ts

Lines changed: 0 additions & 69 deletions
This file was deleted.

src/worker/encoder-worker.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@ class EncoderWorker {
105105
private processedFrames: number = 0;
106106
private videoFrameCount: number = 0;
107107
private isCancelled: boolean = false;
108-
private audioWorkletPort: MessagePort | null = null;
109108

110109
constructor() {
111110
// コンストラクタで依存性を注入することも可能
@@ -1051,11 +1050,6 @@ class EncoderWorker {
10511050
this.totalFramesToProcess = undefined;
10521051
this.processedFrames = 0;
10531052
this.videoFrameCount = 0;
1054-
if (this.audioWorkletPort) {
1055-
this.audioWorkletPort.onmessage = null;
1056-
this.audioWorkletPort.close();
1057-
this.audioWorkletPort = null;
1058-
}
10591053
if (resetCancelled) {
10601054
this.isCancelled = false;
10611055
}
@@ -1080,15 +1074,6 @@ class EncoderWorker {
10801074
this.cleanup();
10811075
await this.initializeEncoders(eventData);
10821076
break;
1083-
case "connectAudioPort":
1084-
this.audioWorkletPort = eventData.port;
1085-
this.audioWorkletPort.onmessage = async (
1086-
e: MessageEvent<AddAudioDataMessage>,
1087-
) => {
1088-
if (this.isCancelled) return;
1089-
await this.handleAddAudioData(e.data);
1090-
};
1091-
break;
10921077
case "addVideoFrame":
10931078
await this.handleAddVideoFrame(eventData);
10941079
break;

0 commit comments

Comments
 (0)