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 の出力先と同じものに設定する」である。
まず、リダイレクトを指定しない場合は、
% command ⇒ 1 の出力先 … 画面 ⇒ 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 >&2fooコマンドの標準エラー出力を 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番というメモがないと標準エラー出力への振り分けが行えないのです。
その他出力のリダイレクションなどについての詳しい情報(2015/12/23追記)
Advanced Bash-Scripting Guideにとても詳しく載っています。
*1:これは特に要らない気がする