0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails初学者】Rails × ActiveStorage × ffmpegで録音ファイルの再生時間を取得するまで

Last updated at Posted at 2025-08-17

Rails × ActiveStorage × ffmpegで録音ファイルの再生時間を取得するまで

現在、学習のためにRailsアプリを作っています。
本記事は備忘録ですが、同じところでハマった人の参考になれば嬉しいです。
初学者のため、記事内容に誤りがあるかもしれません。
ご了承ください。

環境

  • Rails 8.0.2
  • Ruby 3.4.2
  • ffmpeg 7.1.1 (Homebrew)
  • Chrome 131 (MediaRecorder)
  • Active Storage

困っていた状況

録音機能を保存完了画面で録音時間を表示したかったのですが、

期待していた表示:

・ 録音保存完了しました
・ 保存情報は以下
・ ファイルサイズ: 47.8KB
・ 録音時間: 0分9秒  ← ここを表示したい

実際の表示:

・ 録音保存完了  
・ 保存情報は以下
・ ファイルサイズ: 47.8KB
・ 録音時間: --分--秒  ← 取得できない

Railsのログを見てみました。

# metadata に duration: 0 が保存されている
UPDATE "active_storage_blobs" SET "metadata" = '{"identified":true,"duration":0}' 
WHERE "active_storage_blobs"."id" = 43

ffprobeを直接実行します。

$ ffprobe -i recording.webm -show_entries format=duration -v quiet -of csv=p=0
N/A  #取得ができませんでした。

なぜMediaRecorderで録音したファイルの再生時間が取得できないのかという状況から、取得できるまでをまとめておきます。

背景

実装したかった要件

  • JavaScript(MediaRecorder)で音声録音
  • ActiveStorageで録音ファイル(.webm)を保存
  • 保存完了画面で録音時間を「○分○秒」で表示

最初の実装

フロント:MediaRecorderで録音

// MediaRecorderでwebm形式で録音
const mediaRecorder = new MediaRecorder(stream);
const audioChunks = [];

mediaRecorder.ondataavailable = e => {
  audioChunks.push(e.data);
};

mediaRecorder.onstop = () => {
  const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
  // Railsに送信
  const formData = new FormData();
  formData.append('audio', audioBlob, 'recording.webm');
  fetch('/recordings', { method: 'POST', body: formData });
};

バック:Recordingモデル

class Recording < ApplicationRecord
  belongs_to :user
  belongs_to :visit

  has_one_attached :audio_file

  validates :audio_file,
            attached: true,
            content_type: ["audio/webm"],
            size: { less_than: 2.megabytes }

  after_commit :add_duration, on: :create

  private

  def add_duration
    return unless audio_file.attached?

    path = ActiveStorage::Blob.service.send(:path_for, audio_file.key)
    duration = `ffprobe -i "#{path}" -show_entries format=duration -v quiet -of csv=p=0`.to_f.round
    
    audio_file.blob.update(metadata: audio_file.blob.metadata.merge(duration: duration))
  end
end

ビュー:録音時間の表示をします。

<div class="detail-item">
  <div class="detail-label">録音時間</div>
  <div class="detail-value">
    <% if @recording.audio_file.blob.metadata["duration"].present? %>
      <% d = @recording.audio_file.blob.metadata["duration"].to_i %>
      <%= "#{d / 60}#{d % 60}秒" %>
    <% else %>
      --分--秒
    <% end %>
  </div>
</div>

問題:DurationがN/A

ビューでは常に --分--秒 が表示される状況となってしまいます。

ログを見ると、blob.metadataduration: 0 が保存されていました。

原因は何か

実際に保存されたファイルに対して ffprobe を直接実行する。

$ ffprobe -i /path/to/recording.webm -show_entries format=duration -v quiet -of csv=p=0
N/A

$ ffprobe -i /path/to/recording.webm
# 出力抜粋
Input #0, matroska,webm, from '/path/to/recording.webm':
  Metadata:
    encoder         : Chrome
  Duration: N/A, start: 0.000000, bitrate: N/A
  Stream #0:0(eng): Audio: opus, 48000 Hz, mono, fltp (default)

Duration: N/A になってしまっています。

根本原因

ChromeのMediaRecorderで録音したwebmファイルには、録音時間の情報(duration)が入っていないことがあります。
特に「Opus」という形式で録音すると、その情報が抜けやすいようです。

解決:ffmpegを使用して直すやり方

検証:ffmpeg -c copy

試しに ffmpeg で「コピー変換」(音声を作り直す再エンコードなし、メタデータだけ書き直し)を行います。

$ ffmpeg -i input.webm -c copy fixed.webm
# 出力抜粋
size=130KiB time=00:00:08.70 bitrate=122.9kbits/s speed=5.75e+03x

$ ffprobe -i fixed.webm -show_entries format=duration -v quiet -of csv=p=0
8.700000

8.7秒が取得できました。

仕組み

ffmpeg -c copyを使うと、音声データそのものは変えずに、ファイルの入れ物(箱みたいなもの)だけ作り直す。
そのときにffmpegが中身の音声を読んで「録音時間」を計算し、ちゃんとファイルに書いてくれる。
だから、その新しいファイルをffprobeで調べると、録音時間が正しく出てくるようになる。

Railsに組み込んでみます。

修正版:Recording モデル

class Recording < ApplicationRecord
  belongs_to :user
  belongs_to :visit

  has_one_attached :audio_file

  validates :audio_file,
            attached: true,
            content_type: ["audio/webm"],
            size: { less_than: 2.megabytes }

  after_commit :add_duration, on: :create

  private

  def add_duration
    return unless audio_file.attached?

    path = ActiveStorage::Blob.service.send(:path_for, audio_file.key)

    # 一旦 ffmpegで直してからffprobe
    fixed_path = "#{path}.fixed.webm"
    
    # ffmpeg -c copy でduration情報を埋め込む
    system("ffmpeg -i #{Shellwords.escape(path)} -c copy #{Shellwords.escape(fixed_path)} -y -loglevel quiet")

    # 直した後のファイルからdurationを取得
    duration = `ffprobe -i #{Shellwords.escape(fixed_path)} -show_entries format=duration -v quiet -of csv=p=0`.to_f.round(2)

    # 一時ファイルを削除
    File.delete(fixed_path) if File.exist?(fixed_path)

    # ActiveStorageのmetadataに保存
    blob = audio_file.blob
    blob.metadata = blob.metadata.merge(duration: duration)
    blob.save

    Rails.logger.info "Recording duration saved: #{duration} seconds"
  end
end

動作確認

録音 → 保存後、ログで以下が確認できた。

Recording duration saved: 8.7 seconds

ビューでも正しく表示されるようになりました。
(to_f.round(2)は小数点第3位を見て四捨五入して第2位まで残す四捨五入)

録音時間: 0分9秒

まとめ

  • ChromeのMediaRecorderで録音した.webmは、duration情報がないことがあるようです。
  • ffprobe 単体では Duration: N/A で取得できないみたいです。
  • ffmpeg -c copy で直することで、durationが正しく埋め込まれるようです
  • Rails環境では after_commit やActiveJobで変換 → duration取得 → metadata保存の流れで実装可能でした。

初学者のため、間違えていたらすいません。

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?