0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vueでffmpegを扱うときの注意

Last updated at Posted at 2025-07-09

インストールコマンド

npm install @ffmpeg/core

コピーコマンド

npm install @ffmpeg/ffmpeg @ffmpeg/core

PowerShell でコピー:

cp -Recurse node_modules/@ffmpeg/core/dist public/ffmpeg-core

Vue + @ffmpeg/ffmpeg の適切なインスタンス管理と load タイミング

Vue コンポーネントで @ffmpeg/ffmpeg を使用する際、createFFmpeg()load() のタイミング・使い方によってエラーや読み込み失敗が起きることがあります。

以下に正しい実装例と注意点をまとめます。


❗ 問題点

1. mount 前に load() を呼ぶと失敗する

  • Vue のライフサイクルで DOM や依存ライブラリが準備できていない状態で呼ぶと失敗する。

2. 同じインスタンスで複数回 load() を呼ぶと失敗する

  • load() は非同期処理で内部的に wasm ファイルを読み込むため、何度も呼ぶと競合してエラーになる可能性がある。

✅ 対処方法

✔️ load() は 1 回だけ実行する

✔️ グローバルにインスタンスを管理する

// ffmpeg.js
import { createFFmpeg } from '@ffmpeg/ffmpeg';

export const ffmpeg = createFFmpeg({ log: true });

✔️ Vue コンポーネントでの使用例

// MyComponent.vue (script setup または methods内)
import { ffmpeg } from './ffmpeg.js';

async function setupFFmpeg() {
  if (!ffmpeg.isLoaded()) {
    await ffmpeg.load(); // 1回だけ実行されるようにする
  }
  // あとは run() などで使用可能
}

🔄 補足:ffmpeg を使ったファイル変換の一例

await ffmpeg.FS('writeFile', 'input.wav', await fetchFile(file));
await ffmpeg.run('-i', 'input.wav', 'output.mp3');
const data = ffmpeg.FS('readFile', 'output.mp3');
const blob = new Blob([data.buffer], { type: 'audio/mpeg' });

📝 まとめ

注意点 対処法
load() を複数回呼ばない isLoaded() で確認してから呼ぶ
グローバルに管理する createFFmpeg() を1回だけ使う
mount 前の呼び出し setup() または mounted フックで

2024年時点の本プロジェクトでの実装例(src/services/ffmpeg-service.ts より抜粋)

グローバルインスタンス管理・ロードの流れ

  • let globalFFmpeg でグローバルインスタンスを1つだけ生成・管理
  • initializeGlobalFFmpeg() で未ロード時のみ初期化・loadを実行
  • isGlobalFFmpegLoaded フラグで多重ロードを防止
  • load()はPromiseで多重呼び出し時も1回だけ実行
  • WASMファイルの存在確認やタイムアウト制御も実装
// グローバル変数
let globalFFmpeg: any = null
let isGlobalFFmpegLoaded = false
let loadPromise: Promise<void> | null = null

// 初期化関数
const initializeGlobalFFmpeg = async (): Promise<void> => {
  if (globalFFmpeg && isGlobalFFmpegLoaded) return;
  if (loadPromise) { await loadPromise; return; }
  // ...FFmpegのimportとインスタンス生成...
  loadPromise = performGlobalFFmpegLoad();
  await loadPromise;
  isGlobalFFmpegLoaded = true;
}

サービスクラスでのラップ

  • FFmpegServiceクラスでisLoaded/isGlobalFFmpegLoadedを判定し、load()は1回だけ実行
  • 利用前にawait ffmpegService.load()でロード保証
  • processAudio等のメソッドでthis.isFFmpegLoaded()を必ずチェック
export class FFmpegService {
  private isLoaded = false;
  private loadAttempted = false;
  get ffmpeg() { return globalFFmpeg; }
  isFFmpegLoaded(): boolean { return this.isLoaded || isGlobalFFmpegLoaded; }
  async load(): Promise<void> {
    if (this.isLoaded || isGlobalFFmpegLoaded) return;
    await initializeGlobalFFmpeg();
    this.isLoaded = true;
  }
  async processAudio(file: File, ...) {
    if (!this.isFFmpegLoaded()) throw new Error('FFmpeg is not loaded');
    // ...処理本体...
  }
}
export const ffmpegService = new FFmpegService();

Vueコンポーネントでの利用例

import { ffmpegService } from '@/services/ffmpeg-service';

onMounted(async () => {
  try {
    await ffmpegService.load(); // 1回だけロード
    // ...以降はffmpegService.processAudio等を利用可能
  } catch (e) {
    // エラー処理
  }
});

本実装の特徴まとめ

ポイント 実装内容
load()多重呼び出し防止 Promise/フラグで1回だけロード
グローバル管理 globalFFmpegで1インスタンス
WASMファイル存在確認 ファイルアクセス/タイムアウト制御あり
利用前のロード保証 サービスクラスでisLoaded判定
フォールバック処理 WASM失敗時はAudioContext等で代替

🔧 FFmpeg.wasm読み込み方法の詳細

1. 基本的な読み込みパターン

パターンA: 直接import + createFFmpeg

import { createFFmpeg } from '@ffmpeg/ffmpeg';

const ffmpeg = createFFmpeg({ 
  log: true,
  corePath: '/ffmpeg/ffmpeg-core.js' // カスタムパス
});

// 読み込み
await ffmpeg.load();

パターンB: 動的import + グローバル管理

// グローバル変数で管理
let globalFFmpeg: any = null;
let isLoaded = false;

const loadFFmpeg = async () => {
  if (isLoaded) return globalFFmpeg;
  
  const { createFFmpeg } = await import('@ffmpeg/ffmpeg');
  globalFFmpeg = createFFmpeg({ log: true });
  await globalFFmpeg.load();
  isLoaded = true;
  
  return globalFFmpeg;
};

2. 本プロジェクトでの実装詳細

2.1 サービスクラス構造

// src/services/ffmpeg-service.ts
export class FFmpegService {
  private isLoaded = false;
  private loadAttempted = false;
  
  // グローバルインスタンスへのアクセス
  get ffmpeg() { 
    return globalFFmpeg; 
  }
  
  // 読み込み状態チェック
  isFFmpegLoaded(): boolean { 
    return this.isLoaded || isGlobalFFmpegLoaded; 
  }
  
  // 読み込み実行(1回のみ)
  async load(): Promise<void> {
    if (this.isLoaded || isGlobalFFmpegLoaded) return;
    if (this.loadAttempted) {
      // 既に読み込み試行中の場合、完了を待つ
      while (!this.isLoaded && !isGlobalFFmpegLoaded) {
        await new Promise(resolve => setTimeout(resolve, 100));
      }
      return;
    }
    
    this.loadAttempted = true;
    await initializeGlobalFFmpeg();
    this.isLoaded = true;
  }
}

2.2 グローバル初期化関数

const initializeGlobalFFmpeg = async (): Promise<void> => {
  // 既にロード済みの場合はスキップ
  if (globalFFmpeg && isGlobalFFmpegLoaded) return;
  
  // 読み込み中の場合は待機
  if (loadPromise) { 
    await loadPromise; 
    return; 
  }
  
  // 新しい読み込みを開始
  loadPromise = performGlobalFFmpegLoad();
  await loadPromise;
  isGlobalFFmpegLoaded = true;
};

const performGlobalFFmpegLoad = async (): Promise<void> => {
  try {
    // 動的importでFFmpegを読み込み
    const { createFFmpeg } = await import('@ffmpeg/ffmpeg');
    
    // インスタンス作成
    globalFFmpeg = createFFmpeg({ 
      log: true,
      corePath: '/ffmpeg/ffmpeg-core.js'
    });
    
    // WASMファイルの存在確認
    await checkWasmFilesExist();
    
    // 読み込み実行
    await globalFFmpeg.load();
    
  } catch (error) {
    console.error('FFmpeg.wasm読み込みエラー:', error);
    throw error;
  }
};

3. Vueコンポーネントでの使用方法

3.1 App.vueでの初期化

// App.vue
import { ffmpegService } from './services/ffmpeg-service';

const initializeFFmpeg = async () => {
  try {
    console.log('App.vue: FFmpeg.wasm初期化開始...');
    
    // サービスクラスで読み込み
    if (!ffmpegService.isFFmpegLoaded()) {
      await ffmpegService.load();
    }
    
    // グローバルインスタンスとして公開
    if (typeof window !== 'undefined') {
      (window as any).FFmpegService = ffmpegService;
    }
    
    console.log('App.vue: FFmpeg.wasm初期化成功');
    
  } catch (error) {
    console.error('App.vue: FFmpeg.wasm初期化エラー:', error);
  }
};

onMounted(() => {
  initializeFFmpeg();
});

3.2 個別コンポーネントでの使用

// RecordingView.vue
import { ffmpegService } from '../services/ffmpeg-service';

const convertWebmToMp3WithMetadata = async (webmBlob: Blob, title: string, duration: number) => {
  try {
    // 読み込み状態チェック
    if (!ffmpegService.isFFmpegLoaded()) {
      console.log('FFmpeg.wasmを読み込み中...');
      await ffmpegService.load();
    }
    
    const ffmpeg = ffmpegService.ffmpeg;
    
    // ファイル変換処理
    const webmData = await webmBlob.arrayBuffer();
    ffmpeg.writeFile('input.webm', new Uint8Array(webmData));
    
    await ffmpeg.exec([
      '-i', 'input.webm',
      '-c:a', 'libmp3lame',
      '-b:a', '128k',
      'output.mp3'
    ]);
    
    const mp3Data = ffmpeg.readFile('output.mp3');
    return new Blob([mp3Data], { type: 'audio/mp3' });
    
  } catch (error) {
    console.error('FFmpeg.wasm処理エラー:', error);
    throw error;
  }
};

4. エラーハンドリングとフォールバック

4.1 読み込み失敗時の処理

const convertWithFallback = async (webmBlob: Blob) => {
  try {
    // FFmpeg.wasmで変換を試行
    return await convertWebmToMp3WithMetadata(webmBlob, filename, duration);
  } catch (error) {
    console.warn('FFmpeg.wasm変換失敗、WebMファイルのまま保存:', error);
    
    // フォールバック: WebMファイルのまま返す
    const webmFilename = filename.endsWith('.webm') ? filename : filename + '.webm';
    return { blob: webmBlob, filename: webmFilename };
  }
};

4.2 読み込み状態の監視

// 読み込み状態を監視するイベントシステム
const ffmpegLoadingStatus = ref<'loading' | 'success' | 'error' | null>(null);

const checkFFmpegStatus = () => {
  const event = new CustomEvent('check-ffmpeg-status');
  window.dispatchEvent(event);
};

// 定期的に状態をチェック
const statusInterval = setInterval(checkFFmpegStatus, 1000);

// 状態変更イベントのリスナー
const handleFFmpegStatusChange = (event: CustomEvent) => {
  ffmpegLoadingStatus.value = event.detail.status;
};

window.addEventListener('ffmpeg-status-change', handleFFmpegStatusChange);

5. パフォーマンス最適化

5.1 遅延読み込み

// 必要になるまで読み込みを遅延
const loadFFmpegOnDemand = async () => {
  if (!ffmpegService.isFFmpegLoaded()) {
    // ユーザーに読み込み中であることを通知
    showLoadingMessage('音声処理エンジンを読み込み中...');
    await ffmpegService.load();
    hideLoadingMessage();
  }
};

5.2 メモリ管理

// 使用後のクリーンアップ
const cleanupFFmpeg = () => {
  if (ffmpegService.ffmpeg) {
    // ファイルシステムのクリア
    ffmpegService.ffmpeg.FS('unlink', 'input.webm');
    ffmpegService.ffmpeg.FS('unlink', 'output.mp3');
  }
};

6. デバッグとトラブルシューティング

6.1 読み込み状態の確認

const debugFFmpegStatus = () => {
  console.log('=== FFmpeg.wasm状態確認 ===');
  console.log('グローバルインスタンス:', (window as any).FFmpeg);
  console.log('FFmpegService:', (window as any).FFmpegService);
  console.log('isFFmpegLoaded:', ffmpegService.isFFmpegLoaded());
  console.log('ffmpegインスタンス:', ffmpegService.ffmpeg);
  console.log('ロード状態:', ffmpegService.ffmpeg?.loaded);
  console.log('エラー:', ffmpegService.ffmpeg?.loadError);
};

6.2 よくある問題と解決策

問題 原因 解決策
load()が複数回呼ばれる コンポーネントの再マウント グローバルフラグで制御
WASMファイルが見つからない パス設定ミス corePathを正しく設定
メモリ不足エラー 大きなファイル処理 ファイルサイズ制限を設定
読み込みタイムアウト ネットワーク遅延 タイムアウト時間を延長

7. 本プロジェクトでの実装の利点

  1. グローバル管理: アプリ全体で1つのインスタンスを共有
  2. 多重読み込み防止: Promiseとフラグで確実に1回のみ読み込み
  3. エラーハンドリング: 読み込み失敗時の適切なフォールバック
  4. 状態監視: 読み込み状態をリアルタイムで監視
  5. メモリ効率: 不要なインスタンス生成を防止
  6. デバッグ支援: 詳細なログとデバッグ機能

この実装により、FFmpeg.wasmの安定した読み込みと利用が可能になります。


📝 2025年1月の修正内容

🔧 録音方式の変更(WebM継続録音)

変更前: チャンク録音方式

  • 5分間隔でチャンク分割
  • IndexedDBを使用したチャンク保存
  • 複雑なメモリ管理機能

変更後: WebM継続録音方式

  • WebM形式で1つのファイルに継続録音
  • 録音停止時にMP3変換とメタデータ追加
  • シンプルで安定した録音方式

技術仕様

// RecordRTC設定
recorder.value = new RecordRTC(mediaStream, {
  type: 'video', // WebM形式で録音
  mimeType: 'video/webm',
  sampleRate: 44100,
  desiredSampRate: 44100,
  recorderType: RecordRTC.MediaStreamRecorder,
  numberOfAudioChannels: 1,
  bufferSize: 4096,
  audioBitsPerSecond: 128000,
  timeSlice: 1000, // 1秒ごとにデータを取得
  ondataavailable: (blob: Blob) => {
    console.log('録音データ受信:', blob.size)
    currentRecordingSize.value += blob.size
  }
})

🔧 FFmpegServiceのグローバル公開

App.vueでの修正

// FFmpeg.wasm初期化成功時にグローバル公開
ffmpegLoadingStatus.value = 'success'
console.log('App.vue: FFmpeg.wasm初期化成功')

// FFmpegServiceをグローバルに公開
if (typeof window !== 'undefined') {
  (window as any).FFmpegService = ffmpegService
  console.log('App.vue: FFmpegServiceをグローバルに公開しました')
}

RecordingView.vueでの使用

// FFmpegServiceを使用してFFmpeg.wasmにアクセス
const ffmpegService = (window as any).FFmpegService

console.log('FFmpegService状態確認:', {
  ffmpegService: !!ffmpegService,
  isLoaded: ffmpegService?.isFFmpegLoaded(),
  ffmpeg: !!ffmpegService?.ffmpeg,
  windowKeys: Object.keys(window).filter(key => key.includes('FFmpeg'))
})

if (!ffmpegService) {
  console.warn('FFmpegServiceがグローバルに公開されていません。WebM形式のまま保存します。')
  return webmBlob
}

if (!ffmpegService.isFFmpegLoaded()) {
  console.warn('FFmpegServiceが読み込みされていません。WebM形式のまま保存します。')
  return webmBlob
}

🔧 読み込み待機機能の追加

MP3ダウンロード時の待機処理

// FFmpegServiceの読み込みを待つ
let ffmpegService = (window as any).FFmpegService
let retryCount = 0
const maxRetries = 10

while (!ffmpegService && retryCount < maxRetries) {
  console.log(`FFmpegService読み込み待機中... (${retryCount + 1}/${maxRetries})`)
  await new Promise(resolve => setTimeout(resolve, 500))
  ffmpegService = (window as any).FFmpegService
  retryCount++
}

if (!ffmpegService) {
  console.warn('FFmpegServiceの読み込みがタイムアウトしました。WebM形式でダウンロードします。')
  downloadAudioFile(webmBlob, filename)
  return
}

🔧 WebM→MP3変換とメタデータ追加

変換処理

// MP3変換とメタデータ追加
await ffmpeg.exec([
  '-i', 'input.webm',
  '-c:a', 'libmp3lame', // LAMEエンコーダーを使用
  '-b:a', '128k', // 固定ビットレート128k
  '-ar', '44100', // サンプルレート44.1kHz
  '-ac', '1', // モノラル
  '-write_xing', '1', // Xingヘッダーを有効化(シーク機能)
  '-id3v2_version', '3', // ID3v2.3タグを使用
  '-write_id3v1', '1', // ID3v1タグも追加
  // 基本メタデータ
  '-metadata', `title=${filename.value || '録音ファイル'}`,
  '-metadata', `artist=東広島基幹相談支援センター`,
  '-metadata', `album=相談記録`,
  '-metadata', `date=${recordingDate}`,
  '-metadata', `year=${now.getFullYear()}`,
  '-metadata', `recording_time=${recordingTimeStr}`,
  '-metadata', `duration=${formatTime(recordingTime.value)}`,
  '-metadata', `file_size=${formatFileSize(webmBlob.size)}`,
  '-metadata', `sample_rate=44100`,
  '-metadata', `bitrate=128k`,
  '-metadata', `channels=1`,
  '-metadata', `recording_session_id=${Date.now()}`,
  '-metadata', `processing_timestamp=${now.toISOString()}`,
  // 技術情報
  '-metadata', `encoder=FFmpeg.wasm + LAME`,
  '-metadata', `encoding_settings=MP3 128k CBR Mono 44.1kHz`,
  '-metadata', `audio_format=MPEG-1 Layer 3`,
  '-metadata', `source_format=WebM Opus`,
  // システム情報
  '-metadata', `system_info=東広島基幹相談支援センター 相談記録システム`,
  '-metadata', `software_version=1.0.0`,
  '-metadata', `browser_info=${navigator.userAgent}`,
  '-metadata', `platform=${navigator.platform}`,
  // 説明文
  '-metadata', `comment=WebM形式で録音後、MP3に変換された音声ファイル。シーク機能対応。`,
  '-metadata', `description=相談記録用音声ファイル。モノラル録音、128kbps固定ビットレート。`,
  'output.mp3'
])

🔧 ローカルダウンロード機能の改善

MP3形式ダウンロード

// ローカルダウンロード機能(MP3形式)
const downloadMp3File = async (webmBlob: Blob, filename: string) => {
  try {
    console.log('MP3形式でのダウンロード開始...')
    
    // FFmpegServiceの読み込みを待つ
    let ffmpegService = (window as any).FFmpegService
    let retryCount = 0
    const maxRetries = 10
    
    while (!ffmpegService && retryCount < maxRetries) {
      console.log(`FFmpegService読み込み待機中... (${retryCount + 1}/${maxRetries})`)
      await new Promise(resolve => setTimeout(resolve, 500))
      ffmpegService = (window as any).FFmpegService
      retryCount++
    }
    
    if (!ffmpegService) {
      console.warn('FFmpegServiceの読み込みがタイムアウトしました。WebM形式でダウンロードします。')
      downloadAudioFile(webmBlob, filename)
      return
    }
    
    // WebM→MP3変換
    const mp3Blob = await convertWebmToMp3WithMetadata(webmBlob)
    
    // ダウンロード実行
    const url = URL.createObjectURL(mp3Blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `${filename}.mp3`
    document.body.appendChild(a)
    a.click()
    document.body.removeChild(a)
    URL.revokeObjectURL(url)

    console.log('MP3形式でのローカルダウンロード完了:', filename)
  } catch (error) {
    console.error('MP3形式でのローカルダウンロードエラー:', error)
    alert('MP3形式でのダウンロードに失敗しました。')
  }
}

📊 修正による改善点

項目 改善前 改善後
録音方式 チャンク録音(複雑) WebM継続録音(シンプル)
メモリ使用量 高(チャンク管理) 低(単一ファイル)
FFmpegアクセス 直接インスタンス作成 FFmpegService経由
読み込み待機 なし 最大5秒間待機
ダウンロード形式 WebM形式 MP3形式(変換後)
エラーハンドリング 基本的 詳細なデバッグ情報

🎯 今後の課題

  1. FFmpegService読み込みタイミング: より確実な読み込み保証
  2. 変換処理の最適化: 大きなファイルの処理時間短縮
  3. メタデータの拡張: より詳細な録音情報の追加
  4. エラー回復機能: 変換失敗時の自動リトライ
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?