rtmpdumpとffmpegをつかったライブトランスコーディング

rtmpdumpからffmpegへパイプを通す

rtmpdumpは出力ファイルを指定しないと標準出力にそのまま出力します。

 --flv	-o	output
Specify the output file name. If the name is − or is omitted, the stream is written to stdout.
RTMPDUMP(1):

そして以下の設定をすることで、ffmpegは標準入力を入力ソースに出来ます。

標準入力をつかったエンコード

cat test.mpeg | /usr/local/bin/ffmpeg -i pipe:0 out.mp4 

これで、FFMPEGが、標準入力のパケットをそのままエンコードして、out.mp4に書き出します。
pipe0 :0 が標準入力
pipe1 :1 が標準出力
pipe2 :2 がエラー出力
pipe :- 指定なしは自動判別
です。
ffmpegを応用する。ファイル転送しながら、エンコード - それマグで!

つまり、「-o output」を指定しないrtmpdumpから、「-i pipe:0」を指定したffmpegへパイプを通すと、ライブトランスコーディングが出来ます

実例

ではやってみましょう。
ソースとしてはNHKらじる★らじる大阪ラジオ第1(エンコードはmp4)を使います。
参考:仙台・名古屋・東京・大阪のらじる★らじるをrtmpdumpで録音 - 別館 子子子子子子(ねこのここねこ)

$ rtmpdump -r 'rtmpe://netradio-bkr1-flash.nhk.jp' \
  -a 'live' -y 'NetRadio_BKR1_flash@108232' \
  -W 'http://www3.nhk.or.jp/netradio/files/swf/rtmpe.swf' \
  -v -B 10 | ffmpeg -i pipe:0 -ab 64k 'out.mp3'

-Bのあとに録音時間を秒で指定します。
音質は-abのあとのビットレートで(もちろん指定しなくてもいいです)。
フォーマットはmp3ですが、もちろん他のフォーマットもOK。

rtmpdumpの標準エラー出力のログを取る

rtmpdumpのログは標準エラー出力に出ます。上記のように、「-o output」を指定しないとソースを標準出力へ流します。
ffmpegもログは標準エラー出力に出ます。なので、上記のシェルスクリプト標準エラー出力 2> を取ると、rtmpdumpとffmpegが入り混じります。
これら2つのログを分けましょう。
(2015/12/23追記)勘違いしていました、ffmpeg標準エラー出力だけを/dev/nullに捨てればいいのです…。なのでパイプからは| ffmpeg -i pipe:0 -ab 64k 'out.mp3' 2>/dev/nullみたいな書き方をしておけばOK。

rtmpdumpの標準エラー出力のログでプログレスバーを表示(2015/12/23追記)

NHK最新ニュース動画 Pickup NEWSの前日20時分の動画を取得してみます。rtmpdumpコマンドの引数の見つけ方は次の記事を参考にしてください。

また、プログレスバーの表示メソッドはRuby - 進行状況をプログレスバーで表示 - Qiitaを参照してください。

require 'io/console/size'
$stdout.sync = true

def counter_to_percent(counter)
  counter[0].to_i + counter[1].to_i / 10.0
end

def progress_bar(i, max = 100)
  height, width = IO.console_size
  i = max if i > max
  rest_size = 1 + 5 + 1 # space + progress_num + %
  bar_width = (width - 1) - rest_size
  percent = i * 100.0 / max
  bar_length = i * bar_width.to_f / max
  bar_str = ('#' * bar_length).ljust(bar_width)
  progress_num = '%3.1f' % percent
  print "\r#{bar_str} #{'%5s' % progress_num}%"
end

def yesterday_ddmm
  now = Time.now
  [:month, :day].inject('') do |str, method| 
    str << (now - 86400).__send__(method).to_s
    str
  end
end

puts "fetch nhk_pickup_news_video_#{yesterday_ddmm}_20.mp4"
command = %Q[exec 3>&1; rtmpdump --app ondemand --flashVer 'MAC 20,0,0,228' --swfUrl http://www3.nhk.or.jp/news/pickup_news/swf/player.swf --rtmp rtmp://flv.nhk.or.jp:1935/ondemand --pageUrl http://www3.nhk.or.jp/news/pickup_news/index.html --playpath mp4:flv/news/pickup_news/#{yesterday_ddmm}_20 2>&3 | ffmpeg -i pipe:0 nhk_pickup_news_video_#{yesterday_ddmm}_20.mp4 2>/dev/null]

IO.popen(command) do |pipe|
  duration = nil
  progress = 0
  message = ""
  pipe.each_char do |b|
    if b == "\n" || b == "\r"
      counter_progress_match = message.scan(/\((\d{1,2})\.(\d{1,2})%\)/)
      progress = counter_to_percent(counter_progress_match.first) unless counter_progress_match.empty?
      progress = 100 if progress > 100
      progress_bar(progress)
      message = ""
    else
      message << b
    end
  end
  progress = 100 if progress >= 99
  progress_bar(progress)
  print "\n"
end
rtmpdumpのログ出力である標準エラー出力をIO.popenで受けるために

上記のスクリプトではプログレスバーを表示させるためにIO.popenを使ってrtmpdumpのログを受けています。
しかし、IO.popenは標準出力を受けます。逆に言えば、標準エラー出力を取り込むことが出来ません。そのため、単純に

command = %Q[rtmpdump …(snip)… | ffmpeg -i pipe:0 nhk_pickup_news_video_#{yesterday_ddmm}_20.mp4 2>/dev/null]

としただけでは、rtmpdumpのログ出力をIO.popenで受ける事が出来ません。
このため次のようにrtmpdumpの標準エラー出力が標準出力側へ出るよう振り分ける必要があります。

command = %Q[exec 3>&1; rtmpdump …(snip)… 2>&3 | ffmpeg -i pipe:0 nhk_pickup_news_video_#{yesterday_ddmm}_20.mp4 2>/dev/null]

つまり

  • (exec 3>&1)execで前準備:3番を1番(標準出力)に結びつける
  • (2>&3)rtmpdumpの2番(標準エラー出力=ログ出力)を3番(前準備で標準出力に結びついているがffmpegへのパイプには結びついていない)に結びつける

と工夫することで、IO.popenへログを送ることを可能にしています。

おまけ(2015/12/23構成変更)

パイプの前のコマンドの標準エラー出力と、パイプの後のコマンドの標準エラー出力とが、入り混じってしまうと勘違いしていたので、「前提知識」以下の項目を引用していたのですが、この二つの標準エラー出力は以下の図のように別々に通るので実は問題ないのでした…。

いちおう以下の引用も便利な内容なので、このまま残しておきます。

前提知識

まず、

  • 標準出力は 1 番
  • 標準エラー出力は 2 番

ということを覚えてほしい (ちなみに標準入力は 0 番)。

標準出力と標準エラー出力を両方まとめて他のコマンドに渡すには

% command 2>&1 | less

とし、標準出力と標準エラー出力をまとめて file に書き出す場合は

% command >file 2>&1

とする。ここで順番を逆にして

% command 2>&1 >file (誤り!)

としてはうまくいかないことに注意。

より複雑な例を紹介しよう。
標準エラー出力のみをパイプに出力する。

% command 2>&1 >/dev/null | command2

標準エラー出力のみをパイプに (出力を閉じるので、command が出力結果をチェックしているならエラーになる)。

% command 2>&1 >&- | command2

標準出力と標準エラー出力を交換する。

% command 3>&1 1>&2 2>&3

標準出力を捨て、標準エラー出力をページャで参照する。

% command 3>&1 >/dev/null 2>&3 | less

「2>&1」という書き方は「2 の出力先を 1 にマージする」というイメージで捉えている人が多いのではないだろうか。「2>&1」の本当の意味は「2 の出力先を、1 の出力先と同じものに設定する」である。

まず、リダイレクトを指定しない場合は、

% command1 の出力先 … 画面
⇒ 2 の出力先 … 画面

となっている。標準出力のみをファイルにリダイレクトする場合は

% command 1>file
⇒ 1 の出力先 … file
⇒ 2 の出力先 … 画面

となる。では標準出力と標準エラー出力をファイル file に出力する

% command 1>file 2>&1

だが、これはまず「1>file」が処理されて、

1 の出力先 … file
⇒ 2 の出力先 … 画面

となり、その後に「2>&1」が処理されて、

1 の出力先 … file
⇒ 2 の出力先 … file (なぜなら「2 の出力先を、1 の出力先と同じものに設定した」から)

となって、めでたく標準出力と標準エラー出力が file に出力される。

なお、「n>&m」は「n の出力先を、m の出力先と同じものに設定する」と説明してきたが、UNIX 的には「dup2(m,n)」しているにすぎない。
用語集:リダイレクト: UNIX/Linuxの部屋

前提知識を頭に入れた上で次へ

tee というコマンドがあります。
「標準入力から読んだ内容を標準出力とファイルに書きだす」というものです。
 :
そこで思ったのは、tee コマンドの標準エラー版に相当するものを実現できないかと思いました。
 :

#!/bin/sh
exec 3>&1
foo 2>&1 >&3 3>&- | tee logfile >&2

fooコマンドの標準エラー出力を logfile に書き出します。
なおかつ、標準出力は標準出力として、標準エラー出力は標準エラー出力として出力されます。
m>&n は 「n番の出力先と同じものをm番へコピーする (dup2する)」という意味です。
m>&- は 「m番を閉じる」という意味です。
また、exec は引数にコマンドが与えられていない場合、リダイレクト処理はカレントシェルで効果を表します。
つまり、上記のスクリプトは、 foo コマンドの標準出力と標準エラー出力を入れ替えて、 パイプに流し、さらに tee コマンドの標準出力(= foo コマンドの標準エラー出力) を標準エラー出力に戻す、ということをしています。
とあるエンジニアの備忘log: 標準エラー出力をファイルに落としたい (teeの標準エラー出力版)

(以下は2015/12/23追記)
さらに分解して説明します。

  • (exec 3>&1)前準備として3番を1番(標準出力)に結びつける(これはパイプを通らないが最終的に標準出力から出る)
  • (2>&1)fooコマンドの2番(標準エラー出力)を1番に結びつける(これはパイプを通る)
  • (>&3)fooコマンドの1番(標準出力)を3番に結びつける(これはパイプを通らない
  • (3>&-)fooコマンドの3番を閉じる*1
  • (tee logfile)パイプを通ってきたfooコマンドの標準エラー出力をlogfileへ出力
  • (>&2)teeコマンドの1番(標準出力=fooコマンドの標準エラー出力)を2番(標準エラー出力)に結びつける

最終的にはfooコマンドの標準出力及び標準エラー出力がそれぞれstdout及びstderrに出力されます。つまり、元に戻っている。でもteeコマンドで出力されるのはfooコマンドの標準エラー出力になる、というわけです。

3番出力が混じる場合についての解説は、次の記事での図説が分かりやすかったので引用させて頂きます。
ただし3>&2 2>&1 1>&3なので「標準出力と標準エラー出力の入れ替え」になります。

3番に2番(標準エラー出力)を仮にメモしておき、2番を1番(標準出力)へ、そして1番を3番(メモされた標準エラー出力)へ振り分ける、ということですね。
3番というメモがないと標準エラー出力への振り分けが行えないのです。

その他出力のリダイレクションなどについての詳しい情報(2015/12/23追記)

Advanced Bash-Scripting Guideにとても詳しく載っています。

*1:これは特に要らない気がする