|
8 | 8 | EncodeError, |
9 | 9 | Frame, |
10 | 10 | ProgressInfo, |
| 11 | + VideoFile, |
11 | 12 | } from "../types"; |
12 | 13 | import { inferAndBuildConfig } from "../utils/config-parser"; |
13 | 14 | import { WorkerCommunicator } from "../worker/worker-communicator"; |
@@ -163,11 +164,8 @@ async function processVideoSource( |
163 | 164 | // AsyncIterableの処理 |
164 | 165 | await processAsyncIterable(communicator, source); |
165 | 166 | } 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); |
171 | 169 | } |
172 | 170 | } |
173 | 171 |
|
@@ -422,3 +420,88 @@ async function convertToVideoFrame( |
422 | 420 | `Unsupported frame type: ${typeof frame}. Frame must be VideoFrame, HTMLCanvasElement, OffscreenCanvas, ImageBitmap, or ImageData.`, |
423 | 421 | ); |
424 | 422 | } |
| 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 | +} |
0 commit comments