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.metadata
に duration: 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保存の流れで実装可能でした。
初学者のため、間違えていたらすいません。