読者です 読者をやめる 読者になる 読者になる

複雑な動画の処理を記述するためのmediakitを作り始めた

最近業務で複雑気味な動画の処理をプログラミングする機会があって、ffmpegをよく使うのだけどこれを扱うための既存ソリューションが気に食わなかったので、 自分が満足するやつを一から書き始めて、v0.0.1をリリースした。

https://github.com/ainame/mediakit

mediakitというのは単にffmpegラッパーであるだけでなく、最終的にSoxとかImageMagickとかも取り扱えたら便利だなという意味でつけた名前。 (〜〜kitの部分は家の住人がServerkitというものを作っていたり、iOSSDK公開するときにXXXKitとつける慣習があるっぽいのにインスパイアされてる。)

モチベーション

Rubyからffmpegをさわろうとするといろいろ困ることがある。

  • ファイルのメタ情報どうする?
  • どうやってユニットテストする?
  • FFmpegのバージョンごとの差異はどうやって吸収する・・・?
  • そもそもコマンドラインオプションむずかしくね・・・?

などなど他にもありそうだけど、とにかく面倒なのでエンコードするたびにいちいちsystem("ffmpeg -i ....")みたいなことを毎回やってられない。 なのでCLIはラッパーが欲しい。もともと業務では ruby-av/paperclip-av-transcoder · GitHub の前身の paperclip-ffmpeg と、 streamio/streamio-ffmpeg · GitHub を活用していたのだけど、どちらも使っていて不満が出てきてしまった。

まず前者は、動画アップロード時のエンコードに利用していたのだが、最悪なことにpaperclipと密結合しているので、何か困って手を入れようとしたらgemをforkしてコードに手を入れないと行けなかったし、ガラケー時代のAMRなどのmp4に対応していないコーデックを使用した動画を上げるとエンコードに失敗する(ffmpeg自体は対応しているのでライブラリ側のオプションの渡し方が問題)のでもうさっさとライブラリの使用を辞めたい。開発者の人がマージした他人のPRによって元の挙動がどんどん壊れていったりしてとにかくもう使いたい気持ちがなくなった。

次に後者のstreamio-ffmpegは悪くない出来で、FFMPEG::Movie.new('test.mp4')などとオブジェクトを作るとメタ情報をオブジェクトに保持してくれて扱いやすく、動画のエンコードも自分で好きなオプションを渡せるようなインターフェイスになっていた。

こんなかんじで動画を取り扱える。ActiveRecordのような使い心地かと思った。

movie = FFMEPG::Movie.new(path)
movie.height #=> 300
movie.transcode(resolution: "100x200")

ただこれにも問題があってtranscodeメソッドに渡すオプションが実際にffmpegに渡すべき物をマッピングしたものなので、コードを読むときには意味がわかりやすいけど、コードを書くときにいちいちstreamio-ffmpegのコードを読みに行ってマッピング内容を調べる必要があった。また、ユニットテストを書こう!と思ってもテストのための機能が用意されていないので、テストの中で呼ぶと実際にコマンドを発行してしまう状態になっていた。しかも1年以上コードが放置されているので、これも最終的にはやめたい。 

mediakitの実装

本当にシンプルでまだ全然機能が足りてないのだけどffmpegのコマンドを発行するのをこんなかんじで記述できる。

driver = Mediakit::Drivers::FFmpeg.new
ffmpeg = Mediakit::FFmpeg.new(driver)

options = Mediakit::FFmpeg::Options.new(
  Mediakit::FFmpeg::Options::GlobalOption.new(
    'y' => true,
  ),
  Mediakit::FFmpeg::Options::InputFileOption.new(
    options: nil,
    path:    'in.mp4',
  ),
  Mediakit::FFmpeg::Options::InputFileOption.new(
    options: nil,
    path:    'out.mp4',
  ),
  Mediakit::FFmpeg::Options::OutputFileOption.new(
    options: {
      'vf' => 'crop=320:320:0:0',
      'ar' => '44100',
      'ab' => '128k',
    },
    path:    output,
  ),
)

puts "$ #{ffmpeg.command(options)}"
puts ffmpeg.run(options)

↑のコードでは実際に↓のコマンドが発行される。

$ ffmpeg -y -i in.mp4 -vf crop\=320:320:0:0 -ar 44100 -ab 128k out.mp4

この実装だと上述の不満点だった、オプションはffmpegが実際に使う生の文字列で渡せるし、driverが指定できるのでコードをほとんど書き換えずに実際にはコマンドを発行しないなどの切り替えができるようになってたりしている。

オプションの指定とかは若干野暮ったいインターフェイスだけど、RubyとかPerlにありがちなArrayとHashの組合わせで何でも実現するみたいな設計に嫌気がさしたのでいちいちクラスを定義してコンストラクタにオブジェクトを渡すなどの実装をしている。まだ作ってないけどこのインターフェイスをさらに抽象化するレイヤーのコードをこの上に作っていけばその問題もある程度緩和されるはず。

今回v0.0.1したものの中にはメタ情報を取得するところとか、動画ファイルをオブジェクトとして扱うような実装とか、ffmpeg自体の対応コーデックをチェックする機能とかは入れていないけど、良いインターフェイス思いついたらどんどん実装していく次第。ffmpegはとにかく複雑なのでスッキリしたインターフェイスを考えるのが難しい。

ちなみに今回このコードを書くのRubyMineを使い始めたけど悪くない感じでしばらく使っていきたい。 コードを書いてるとたまにキーの入力を受け付けなくなる時があったり、モジュールのネストの構成を変えたい時のリファクタリング機能が欲しいとか思った。