PyAV の(生半可な)紹介

「生半可」なのは、「やっと評価開始出来る状態に持っていけた」程度だからさ。

PyAV の(生半可な)紹介

マエオキング

とにかくここんとこずーっと日本人が書いた情報を黙殺(というよりはヒットしないように検索)してるので、お仲間情報が存在してるのかどうかは知らない。けどワタシが探さないだけで、ほかにもっといい記事はあると思うぞ、おそらく。お口に合わないようならとっとと離脱しなはれ、その方が幸せと思う。

餅べーしょん

Python の wave モジュール例の殴り書きFFT なスペクトラム可視化に音階をくっつけて(4)’までの流れでは、「とにかく (PCM の)データ処理をすることだけが目的なんだから、ひとまずは凝ったものはいらない」というノリで、まぁ NumPy、SciPy、MatplotLib はともかくとして、「動画ファイルや音声ファイル」の扱いは Python の標準ライブラリの世界から出ないでやってた。

だけれども、まぁ「リアルタイム処理」が目的ではないにしたって、例えば mp4 や mp3 を最初に手にしていたとして、予め ffmpeg を使って「WAV に変換しておく」ってのもやはり時として億劫だったりもするわけだ。ダイレクトに扱えないだろうか、と。

となれば当然「ffmpeg / libav」と直接お話出来る Python ラッパー、ありませんかー、ってことになる。

あんまり多くないみたいね。「ffmpeg python wrapper」でヒットするほとんどは、「ffmpeg コマンドの呼び出しをラップするもの」であって、まぁそれも使えば結構便利だろうけど、もちろん期待のものではなく、Cython でお喋りしてるのは PyAV しか見当たらなかった。ctypes 使ってるのならあったけど。

で、Basic DemoExamplesAPI リファレンスの品揃えをざっと眺めてみるに、少なくとも「とっても理想なんではないのかしら」という気だけはする。どうか?

先に「examples」が動作してるの図、見せちゃうわ

この先の話がちと長くなるんで、「構築出来たとしたら」どうなるか、の正解を先に動画で:

無論動画だけでは何一つ伝わらない。真ん中のスペクトラム表示しているっぽい黒い画面はこれは PyAV 関係なくて、ffplay の画面。PyAV の examples/audio.py はオプション -p を渡すと、ffplay の標準入力にデータを流し込む、ということをする:

examples/audio.py
83         if frames and args.play:
84             if not ffplay:
85                 cmd = ['ffplay',
86                     '-f', frames[0].format.packed.container_name,
87                     '-ar', str(args.rate or stream.rate),
88                     '-ac', str(len(resampler.layout.channels if resampler else stream.layout.channels)),
89                     '-vn', '-',
90                 ]
91                 print('PLAY', ' '.join(cmd))
92                 ffplay = subprocess.Popen(cmd, stdin=subprocess.PIPE)
93             try:
94                 for frame in frames:
95                     ffplay.stdin.write(frame.planes[0].to_bytes())
96             except IOError as e:
97                 print(e)
98                 exit()

この例で、およそ自分が欲しいものがあるということがわかる。つまりフレームごとにデータを取り出せて、リサンプリングも出来て、等々、ということ。そして ffplay が PyAV を経由したデータを「ちゃんと」再生出来ているようであれば、PyAV のデータ処理に問題がないのであろう、ということもわかる。

正直このデモプログラムの「-c」(–count)にハメられて「うまくいってない」と思い込んでしまってた、最初。無論この手のデモプログラムがイキナリ「全データを猛烈に処理」しようとすると初めて触るユーザにとって迷惑なことなので、これは「良い配慮」だけれど、にしたってデフォルトで 5 は少な過ぎるよ…。

PyAV のセットアップは長い道のりか? そうでもないよ、実際のとこは

といいつつも、個人的には「これで評価を始められるな」に至るまでに無駄に時間を費やした。

PyAV の公式の説明は conda 使わないと死ぬぜ と言うけれど、いやー、んなことない、んなことない。依存性が超絶複雑怪奇なのは ffmpeg (libav) のほうであって、PyAV は ffmpeg (libav) をちゃんと構築できてさえいりゃぁ、かぁなりスンナリ行く。「答えがわかってしまえば」。正解にたどり着くまでには「ワタシは」時間かかっちったけれど、後追いの「あなた」は楽に行けるはずよ。

linux

結局耐え切れなくなって linux での正解をとっとと知りたかったのだ。すぐに linux でのしかるべき姿をみれるようにしとかないと、Windows 版でおかしなことになったときに手も足も出ないことになるのだ。

Fedora 26 では特別難しいことはなかった。パッケージマネージャ任せ(RPM Fusion を使って)なら。Fedora 21 だとおそらくキツい。

なお、Fedora 26 と RPM Fusion の組み合わせでは ffmpeg は最新の 3.3.2 を持ってくる。PyAV は 3.2 を前提にしているようである。でこれだとマズいんだろうか、と不安には思うんだけれど、3.3.2 でも少なくとも上の examples/audio.py は動作した。

API (ABI) が同じとも思えないんで、さらに深く潜っていくと何かあるかもなぁ、とは思う。そういうわけで、ffmpeg 3.2 を自力でビルドを試みてはみた。これは PyAV ソースツリーの PyAV/scripts/build-deps を実行すればいい。ただしこれの configure では --prefix を操作してしまって、/usr/local にインストールする向けのビルドにはならないので、build-deps スクリプトで --prefix してる部分を消して。

自力ビルドだと「フルインストール」が難しいことになったりもするので、どっちみち一般の方にはオススメは出来ないんだけれど、個人的にインターフェイスのミスマッチがあったらやだな、てのがあったので一応やってみた、てことね。これは「ほぼ問題ない」んだけれど、ffplay のビルドだけは全然ダメだった。SDL のバージョン違い問題、かなぁ、多分。(実際それが理由で、configure--enable-ffplay しても、ビルドはさせてくれない。config.mak をマニュアルで編集しない限り。)

なお、「(dnf で /usr にインストールした) ffmpeg 3.3.2 と (野良ビルドして /usr/local にインストールした) ffmpeg 3.2 を共存」状態だと少しだけ面倒だけど、わかるよね? 環境変数では PKG_CONFIG_PATHLD_LIBRARY_PATH の2つ(PATH も入れれば3つ)で誤魔化せる。かつ、「ffplay」だけはもう一つ別の誤魔化し:

/usr/local/bin/ffplay (ビルド出来なかった 3.2 の ffplay の代わり)
1 #! /bin/sh
2 export LD_LIBRARY_PATH=/usr/lib64:$LD_LIBRARY_PATH
3 
4 /usr/bin/ffplay "$@"

ffplay は PyAV が相手にする libav ではないので 3.3.2 でも全然関係ない。

ffmpeg 3.2 でないとダメかどうかはまだわかんないけど、まぁ 3.3.2 と両方いつでも使える状態の方がきっと理想よね。

で ffmpeg (libav) が適切にインストール出来ていれば、PyAV のビルド・インストールは、まったくもって簡単。いつも通り「python setup.py build」「sudo python setup.py install」とかね。virtualenv したいヒトは…まぁ知ってるでしょ、そうしたい人なら。それより「野良ビルド版 /usr/local な libav」の方を使いたい人は前述の環境変数を忘れずにな。

Windows

まず「非公式な Windows バイナリ」が転がっているが、64bit 版で動作しないので、現代の多数派は利用出来ないと思う。し、「推奨しないぞ」言うておる、ご本人が。

だので自力でビルドする必要はある。Cython (と当然「Microsoft Visual C++ Compiler」)が必要である。非常にややこしい話、「じっつに簡単だ」というところもあれば、「あぁあ、やっぱり Windwos だな」てところもある。

Windows で PyAV をビルドしたい場合の注意点はひとつ。「ffmpeg の Windows 版公式バイナリ」をダウンロードする際、「shared と dev」の両方を入手する必要がある、ということ。(static リンクはおそらく出来んと思う。ので shared な。) shared には必要な DLL が入っているが PyAV ビルドに必要なヘッダファイルとインポートライブラリが含まれず、これが dev に入ってる。

この公式バイナリなのだが、幸い「特定の VC バージョンへの依存」がないことがわかった。実際 gcc でビルドされているようなんだけれど、依存 VC ランタイムとしては MSVCRT 一つだけ。これが非常に助かる。これのおかげで、CPython 2.7 公式、CPython 3.5 公式の MSVC で作る C 拡張と問題なくリンク出来る。

PyAV のビルドは簡単だが、PyAV の setup.py が「ややバカ」で、一度の失敗を「永続化」してしまう(jsonで)ため、「措置後」でも「やーい、失敗しやがっただろ、今度だって失敗だよヴァーカヴァーカ」とのたまうので注意。setup.py を書き換えて json のチェックをやめさせるなり、毎度 build を消すなり。

でその「一度の失敗」だが、何種類かやる、と思う。一つは CPython 2.7 + Microsoft Visual C++ Compiler for Python 2.7 固有で、こちらは msinttypes の対策が必要。予め措置しておくこと。CPython 3.5 + Microsoft Visual C++ 14.0 standalone: Build Tools for Visual Studio 2017 でも共通なのは --ffmpeg-dir の指定間違い、だろう、きっと。たとえばカレントなんぞに手で置いたんであればきっとこんな:

1 me@host: PyAV$ python setup.py build --ffmpeg-dir='ffmpeg-3.2-win64-dev'

まぁ setup.py のバカさはここについては非 windows とも共通なんだけれど、linux でのビルドはもっとスムースなので目立たないんだよね。ともあれ「幸い」CPython 2.7/CPython 3.5 ともに問題なくビルド出来る。(なお、動かす際には ffmpeg (というかこの場合 dll) にちゃんとパスを通しとくことを忘れずに。ビルド出来たのに動かないならきっとソレよ。)

で、さっきの examples/audio.py は、CPython 2.7 + Microsoft Visual C++ Compiler for Python 2.7 でも、CPython 3.5 + Microsoft Visual C++ 14.0 standalone: Build Tools for Visual Studio 2017 のどちらでもどうやら正しく動作した。

linux 版の方で触れた通り、PyAV が前提にしているそのものズバリ版は ffmpeg 3.2 なので、不安なら 3.2 が安心だと思う。けど examples/audio.py が 3.3.2 でも問題なさげに動作するのは linux の方と同じ。

Windows、での蛇足

ヘッダとインポートライブラリがないんだよなぁ、と思ったその日、「Windows でも ffmpeg ビルド出来んかなぁ?」と思った、という話。最初 dev に気付かなかったのである、ワタシ。

結論からは、「CPython 3.5 + Microsoft Visual C++ 14.0 standalone: Build Tools for Visual Studio 2017 では悪くなさそう? たぶん」で、「CPython 2.7 + Microsoft Visual C++ Compiler for Python 2.7 は絶望的にダメ」。

前者の「よさげ」も深く調べれば怪しい可能性も否定出来なくて、公式の gcc でのビルドのものが使えるんだから、あえてそうする理由はないと思う。問題は後者で、そもそもビルド自体完遂するのは「C を知らなきゃ絶対無理」なのは当然としても、ちょいと知ってても苦労したのはそう、c99wrap のせいだ。そしてビルド成功しても、「全く正しく動作しなかった」。この先をやろうとは思いませんて、うまくいくやり方知ってるのにあえて茨の道を進むことはあるめぇ。(どうしてもやりたい人は、リンク先に書いた c99 関係のほかに、direct.h と io.h の問題、snprintf の問題の二つがあったと記憶してる。これはどうとでもなるでしょ?)

いよいよ少しちゃんと使ってみようと思ったが

さすが「生半可」だけある。まずはドキュメントがまったく整備されておらず、例も少ないために、絶賛苦労中。

第一印象としてはだから「なんとなく思った通りのことが出来そうだ」というものではあるものの、いかんせん、「ドキュメントが欠けててさっぱりわからん」がために、「超絶に簡単な例以外は今のところ全部むつかしい」くて困っている。具体的には、まだ「パケット」「フレーム」「プレーン」の考え方が掴めず、なのでオーディオデータを右・左チャンネルに分解して取るにはどうしたらいいんだろう、みたいな基礎的なことも全然見えてきてない。多分 libav の考え方に倣っているんだろうから、libav への理解からが早いならその方がいいのかもなぁ…。

「超絶に簡単な例」は examples/save_frames.pyね:

examples/save_frames.py
1 import sys
2 
3 import av
4 
5 container = av.open(sys.argv[1])
6 for i, frame in enumerate(container.decode(video=0)):
7     frame.to_image().save('sandbox/%04d.jpg' % i)
8     if i > 5:
9         break

誰でもわかるわよな、こんなん。そしてこのサンプルだけからは何の発展もしない。(ただ「動画から静止画を抜き出す」というタスク自体は「あるあるタスク」なので、これだけでも嬉しいことがないわけではないけれども。)

どうも映像だけ扱うぶんにはシンプルなようではあるんだけれど、ただ、映像、音声、サブタイトル、データの4つが混在になってる「普通の動画」の扱いの、「読み」の方の基本もわからんし、「書き」の方の基本もわからん状態。

そういうわけで現状「いいものなのかそうでもないのか」の評価はまったく出来てない。「評価を始められるぜ」になっただけ。まぁおいおいでってことで。

17日追記んぐ

まだ断定は出来ないが、「CPython 2.7 + Microsoft Visual C++ Compiler for Python 2.7」での方、何かおかしいかもしれない。こんなシンプルなのが意図したのと違う結果になる:

http://mikeboers.github.io/PyAV/examples.html ほぼそのまま
 1 # -*- coding: utf-8 -*-
 2 import numpy as np
 3 import av
 4 
 5 duration = 10  # Seconds
 6 frames_per_second = 24
 7 total_frames = duration * frames_per_second
 8 
 9 container = av.open('test.mp4', mode='w')
10 
11 stream = container.add_stream('mpeg4', rate=frames_per_second)
12 stream.width = 480
13 stream.height = 320
14 stream.pix_fmt = 'yuv420p'
15 
16 for frame_i in range(total_frames):
17     img = np.empty((480, 320, 3))
18 
19     img[:, :, 0] = 0.5 + 0.5 * np.sin(frame_i / duration * 2 * np.pi)
20     img[:, :, 1] = 0.5 + 0.5 * np.sin(frame_i / duration * 2 * np.pi + 2 / 3 * np.pi)
21     img[:, :, 2] = 0.5 + 0.5 * np.sin(frame_i / duration * 2 * np.pi + 4 / 3 * np.pi)
22 
23     img = np.round(255 * img).astype(np.uint8)
24 
25     frame = av.VideoFrame.from_ndarray(img, format='rgb24')
26     packet = stream.encode(frame)
27     if packet is not None:
28         container.mux(packet)
29 
30 # Finish encoding the stream -> これすると EOF Error 喰らうんだけどそもそも何をしたい?
31 #while True:
32 #    packet = stream.encode()
33 #    if packet is None:
34 #        break
35 #    container.mux(packet)
36 
37 # Close the file
38 container.close()

3.5 の方はちゃんと「色が刻々と変わる(くそつまらない)動画が出来上がる」が、2.7 の方はずっと灰色のままの動画になる。

リンクしている ffmpeg (libav) は全く一緒なので、考えられるのは cython コード内での型の扱いをミスっているのだろうなぁという気がする。あ、いや、、、3.5 と 2.7 ではそういや cython のバージョンが違ってるな、それも可能性ある?

と思って cython をアップグレードしてみるも変わらず。やはり cython コード内で何か移植性に対する配慮が欠けているのかもしれない。(もうひとつは numpy のバージョン違いの可能性もないではないけれど…さすがにそれはないと思うなぁ。)

作者がどんなプラットフォームで開発しているのかどこにも書かれていないのでわからないが、どうも今のところは Windows 版 CPython 2.7 から使うのは避けた方が良さそうだ。


さらに追記。

linux でもやってみたら、「2.7 はダメ」が真相。linux でも 2.7 はおかしく、また、EOF エラーの件も同じ。

17日さらなる追記んぐ: 生半可な「期待通りの」シンプルな例を作れたわい

なんかようやっと要領つかめてきた。

例は「mp4 を読み込んで wav にしてしまいやがる」である。多分 PyAV (というか libav) 自身でも作れちゃうような気もするけれど、ひとまずは「データの読みほどき方の基本」側だけを、「書き込み」には PyAV を使わないことによって、ちゃんと「オレ的に」わかったつもりになってやろう、ということである:

 1 # -*- coding: utf-8 -*-
 2 import numpy as np
 3 import av
 4 from tiny_wave_wrapper import WaveReader, WaveWriter
 5 
 6 #
 7 container = av.open("hoge.mp4")
 8 #
 9 stream = next(s for s in container.streams if s.type == 'audio')
10 resampler = av.AudioResampler(
11     format=av.AudioFormat("s16p"), layout="stereo", rate=44100)
12 #
13 
14 # sampwidth=2
15 with WaveWriter("out.wav", 2, 2, 44100) as wavfo:
16     for packet in container.demux(stream):
17         for frame in packet.decode():
18             rf = resampler.resample(frame)
19             channels = (
20                 np.fromstring(rf.planes[0].to_bytes(), dtype=np.int16),
21                 np.fromstring(rf.planes[1].to_bytes(), dtype=np.int16))
22             wavfo.writechannels(channels)

tiny_wave_wrapperについては ここに書いたのと同じもの。(汎用性はなく、「ワタシの理解のため」に作ったもんなので、ご利用は計画的に。)

17日モア追記んぐ: 音声手作りしてエンコード、は出来ないのかも…

上で「読み」が出来たから「書き」の方を試みようと探っていたが、インターフェイスが見つからず。というか「音声」の方だけインターフェイスが見当たらない。VideoFrame には from_ndarray があるのに AudioFrame はデータの受け口が見当たらない。

ないのか? と、ソースコードを読んでみてもやっぱり見つからないし。なんか止まってる?

少なくとも「読み」は自在なので最悪では「一時ファイルを作ってから av.open で開いて AudioFrame を操る」とすればやりたいことは出来るであろうけれど、そうなると「Python の wave モジュール」から逃れられないことになるな…。

ほんとにそうなのかなぁ…?

AudioFrame にデータを後から fill 出来ないんであればこんなことするしかない

 1 from io import BytesIO
 2 import math
 3 import numpy as np
 4 import av
 5 from pytinymts.scales import onsc2freq
 6 from tiny_wave_wrapper import WaveReader, WaveWriter
 7 
 8 _RATE = 44100
 9 _SAMPW = 2
10 
11 # ---
12 # setup PCM data
13 maxvol = 2**10 - 1.0  #maximum amplitude
14 _score = [
15     [  # left channel
16         # (duration, ((octave number 1, scale 1), ...))
17         (8, ((5, "C"), (5, "E"), (5, "G"))),  # C
18         (8, ((5, "D"), (5, "F"), (5, "A"))),  #
19         ],
20     [  # right channel
21         (1, ((6, "C"),)), (1, ((6, "E"),)), (1, ((6, "G"),)),
22         (1, ((6, "E"),)), (4, ((6, "C"),)),
23         (1, ((6, "D"),)), (1, ((6, "F"),)), (1, ((6, "A"),)),
24         (1, ((6, "F"),)), (4, ((6, "D"),)),
25         ]
26     ]
27 pcm = [[], []]
28 for chn, scorech in enumerate(_score):
29     for dur, chord in scorech:
30         for i in range(0, dur * _RATE):
31             pcm[chn].append(
32                 int(maxvol * sum(
33                     [math.sin(i * math.pi * 2 * onsc2freq(on, sc) / _RATE)
34                      for on, sc in chord]) / len(chord)))
35 
36 tmp = BytesIO()
37 with WaveWriter(tmp, 2, 2, 44100) as wavfo:
38     wavfo.writechannels(pcm)
39 tmp.seek(0)
40 
41 # ---
42 # write as aac with av
43 tcont = av.open(tmp, "r")
44 tstr = next(s for s in tcont.streams if s.type == 'audio')
45 ocont = av.open("test_CEG.aac", "w")
46 resampler = av.AudioResampler(
47     format=av.AudioFormat("fltp"), layout="stereo", rate=_RATE)
48 astream = ocont.add_stream(codec_name='aac', rate=_RATE)
49 for packet in tcont.demux(tstr):
50     for frame in packet.decode():
51         rf = resampler.resample(frame)
52         p = astream.encode(rf)
53         if p is not None:
54             ocont.mux(p)

この例では、手作りで音声を作ったはいいがそれを元に AudioFrane を作れないので、先に wavefile として書き出してから av.open し、それを aac に変換している。

pytinymts.scalestiny_wave_wrapper は例によって ここに書いた のと同じ。

まぁ「一時ファイル」を作らずに BytesIO に包めばいい、ということではあるけれど、いずれにせよまどろっこしい。

18日追記: 音声と映像を両方書き込む、の例

 1 from io import BytesIO
 2 import math
 3 import numpy as np
 4 import matplotlib.pyplot as plt
 5 import av
 6 from pytinymts.scales import onsc2freq
 7 from tiny_wave_wrapper import WaveReader, WaveWriter
 8 
 9 _ARATE = 44100
10 _SAMPW = 2
11 
12 # ---
13 # setup PCM data
14 maxvol = 2**10 - 1.0  #maximum amplitude
15 _score = [
16     [  # left channel
17         # (duration, ((octave number 1, scale 1), ...))
18         (8, ((5, "C"), (5, "E"), (5, "G"))),  # C
19         (8, ((5, "D"), (5, "F"), (5, "A"))),  #
20         ],
21     [  # right channel
22         (1, ((6, "C"),)), (1, ((6, "E"),)), (1, ((6, "G"),)),
23         (1, ((6, "E"),)), (4, ((6, "C"),)),
24         (1, ((6, "D"),)), (1, ((6, "F"),)), (1, ((6, "A"),)),
25         (1, ((6, "F"),)), (4, ((6, "D"),)),
26         ]
27     ]
28 pcm = [[], []]
29 for chn, scorech in enumerate(_score):
30     for dur, chord in scorech:
31         for i in range(0, dur * _ARATE):
32             pcm[chn].append(
33                 int(maxvol * sum(
34                     [math.sin(i * math.pi * 2 * onsc2freq(on, sc) / _ARATE)
35                      for on, sc in chord]) / len(chord)))
36 
37 tmp = BytesIO()
38 with WaveWriter(tmp, 2, 2, 44100) as wavfo:
39     wavfo.writechannels(pcm)
40 tmp.seek(0)
41 
42 # ---
43 # write as mp4 with av
44 tcont = av.open(tmp, "r")
45 tstr = next(s for s in tcont.streams if s.type == 'audio')
46 ocont = av.open("test_av_audiovideomux.mp4", "w")
47 resampler = av.AudioResampler(
48     format=av.AudioFormat("fltp"), layout="stereo", rate=_ARATE)
49 astream = ocont.add_stream(codec_name='aac', rate=_ARATE)
50 vstream = None
51 for packet in tcont.demux(tstr):
52     for frame in packet.decode():
53         #
54         # mux audio packets
55         rf = resampler.resample(frame)
56         p = astream.encode(rf)
57         if p is not None:
58             ocont.mux(p)
59         #
60         # mux video packets
61         fig, ax = plt.subplots()
62         ax2 = plt.subplot(1, 2, 1)
63         left = np.fromstring(rf.planes[0].to_bytes(), dtype=np.float32)
64         ax2.plot(left)
65         ax2.set_ylim((-0.03, 0.03))
66         ax2 = plt.subplot(1, 2, 2)
67         right = np.fromstring(rf.planes[1].to_bytes(), dtype=np.float32)
68         ax2.plot(right)
69         ax2.set_ylim((-0.03, 0.03))
70         fig.canvas.draw()
71         ncols, nrows = fig.canvas.get_width_height()
72         rgb = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8).reshape(nrows, ncols, 3)
73         plt.close(fig)
74         if vstream is None:
75             vrate = int(_ARATE // left.size)
76             vstream = ocont.add_stream('mpeg4', rate=vrate)
77             vstream.width = ncols
78             vstream.height = nrows
79             vstream.pix_fmt = 'yuv420p'
80 
81         vframe = av.VideoFrame.from_ndarray(rgb, format='rgb24')
82         p = vstream.encode(vframe)
83         if p is not None:
84             ocont.mux(p)
85 ocont.close()  # MUST!

まぁなんというか「PyAV を紹介」にふさわしい例ではないかもしんない。ちと長いよね。

pytinymts.scalestiny_wave_wrapper は例によって ここに書いた のと同じ。

まず音声をプログラムで手作りし、それを入力にして、その波形のプロットと入力音声をくっつけてる。フレームレートの計算の都合 vstream を後で作ってるのは…まぁ読めばわかるよね。

実は最後の「close()」をサボってたばっかりに無駄に苦労してた。flush の関係だろうか、close がないと不正な動画が出来上がり、ffplay も、無論 Windows メディアプレイヤーも再生出来ない。それと、本当はやりたかったのとはこれ、違ってて、元は resample 後のフレームではなく元の入力フレームから波形を作りたかったんだけれど、len(frame.planes) == 1。え、なにこれ? こういうとこも理解していかんとなぁ。

結果出来上がった mpeg4:

18日さらに追記: None チェック部分について

stream の encode から戻る packet が None かどうかをチェックしているのは Examples に倣ったから。

で、Examples があんまりにも意味不明だったんで、いくつか質問してみたんだけれど、その回答の中でこんなこと言ってた:

The example is very likely bad given recent changes to the API (in which encode and decode methods both always return lists (so it can never return None). I can confirm this (when I’m not rushing) but I think it should just be:
1 for packet in stream.encode():
2     container.mux(packet)

あーそうか、None を返さなくなったし、リストで返すようになったので、漏らさず mux するためにループしちゃいなよ、てことね。てわけで、最新の PyAV ではこうするのが正しい、てことみたい:

 1 # ... 省略 ...
 2         #
 3         # mux audio packets
 4         rf = resampler.resample(frame)
 5         for p in astream.encode(rf):
 6             ocont.mux(p)
 7 # ... 省略 ...
 8         for p in vstream.encode(vframe):
 9             ocont.mux(p)
10 # ... 省略 ...

19日時点での軽めの「評価」

既にバグ見つけちゃったりはしてるが、そんなであっても、現状 PyAV は「ワタシが欲しいもの」に最も近く、また、アクティビティがあるので、可能性は高い。

ただ、現状色々「騙しながら」使う必要があるのは間違いなさそうだ。

というのも、「左から右へコピー」という、至って単純なものが、まだ書けない。だからそこからの派生「開始時刻を指定してシークしてコピー」(いわゆる「trim」)が書けない。どうもこれと同じ事象に陥ってるらしい。trim くらいならすぐ書けて欲しいなと思い、以下のようにこねくりまわしてみたが、ダメ、出来ない:

 1 import av
 2 import argparse
 3 
 4 if __name__ == '__main__':
 5     parser = argparse.ArgumentParser()
 6     parser.add_argument("inputpath")
 7     parser.add_argument("start_sec", type=float)
 8     args = parser.parse_args()
 9 
10     icont = av.open(args.inputpath)
11     ocont = av.open(args.inputpath + ".out.mp4", "w")
12     in_to_out = {}
13     for i, s in enumerate(icont.streams):
14         if s.type in (b'audio', b'video', b'subtitle', b'data'):
15             in_to_out[s] = ocont.add_stream(template=s)
16 
17     #ivstr = next(s for s in icont.streams if s.type == b'video')
18     #iastr = next(s for s in icont.streams if s.type == b'audio')
19 
20     #seek_pts_v = int(args.start_sec / float(ivstr.time_base) + ivstr.start_time)
21     #seek_pts_a = int(args.start_sec / float(iastr.time_base) + iastr.start_time)
22 
23     #iastr.seek(seek_pts_a, any_frame=True)
24     for i, packet in enumerate(icont.demux(in_to_out.keys())):
25         if packet.dts is None:
26             continue
27         packet.stream = in_to_out[packet.stream]
28         ocont.mux(packet)
29     ocont.close()
30 
31 #    print(seek_pts_v, seek_pts_a)
32 #
33 #    iastr.seek(seek_pts_a, any_frame=True)
34 #    oastr = ocont.add_stream(codec_name='aac', rate=iastr.average_rate)
35 #    seek_pts_a = None
36 #    for packet in icont.demux(iastr):
37 #        #if packet.dts is None:
38 #        #    continue
39 #        for iframe in packet.decode():
40 #            iframe.pts = None
41 #            for p in oastr.encode(iframe):
42 #                ocont.mux(p)
43 #
44 #    ivstr.seek(seek_pts_v, any_frame=True)
45 #    ovstr = ocont.add_stream(codec_name='h264', rate=ivstr.average_rate)
46 #    for packet in icont.demux(ivstr):
47 #        if packet.dts is None:
48 #            continue
49 #        for iframe in packet.decode():
50 #            iframe.pts = None
51 #            for p in ovstr.encode(iframe):
52 #                try:
53 #                    ocont.mux(p)
54 #                except ValueError:
55 #                    pass
56 #
57 #    ocont.close()

後ろの方のコメントアウトしてある方が、最初書いてた方で、前半のオーディオだけならうまくいくが、映像の方を入れるとダメ。で、活きてる方は scratchpad/remux.py の猿真似。これがもうダメ。

issue に挙がってる事象であるならば、まぁ待ってりゃ直りそうだけどね、なんせ昨日今日の issue だし。

てわけで PyAV、オススメでっす。(どこがや。) こんな評価、デジャブね。いつも通り、「自力でごにょごにょ出来る人なら」、先に言った通り「最も理想に近い」のだけは間違いないと思う。現状のこんなでも、かなり出来ることは出来るわけであるからして。

issue#254 が直れば

#254 そのものはなんか複合的な話みたいだけれど、少なくともワタシの問題は「AttributeError: ‘NoneType’ object has no attribute ‘update’」。仮にこれが修正されたとすれば、上で試みようとした trim はこれで良かった:

 1 import av
 2 import argparse
 3 
 4 if __name__ == '__main__':
 5     parser = argparse.ArgumentParser()
 6     parser.add_argument("inputpath")
 7     parser.add_argument("start_sec", type=float)
 8     args = parser.parse_args()
 9 
10     icont = av.open(args.inputpath)
11     ocont = av.open(args.inputpath + ".out.mp4", "w")
12     in_to_out = {}
13     for i, s in enumerate(icont.streams):
14         if s.type in (b'audio', b'video', b'subtitle', b'data'):
15             in_to_out[s] = ocont.add_stream(template=s)
16 
17     ivstr = next(s for s in icont.streams if s.type == b'video')
18     iastr = next(s for s in icont.streams if s.type == b'audio')
19 
20     seek_pts_v = int(args.start_sec / float(ivstr.time_base) + ivstr.start_time)
21     seek_pts_a = int(args.start_sec / float(iastr.time_base) + iastr.start_time)
22 
23     iastr.seek(seek_pts_a)
24     ivstr.seek(seek_pts_v)
25     for i, packet in enumerate(icont.demux(in_to_out.keys())):
26         if packet.dts is None:
27             continue
28         packet.stream = in_to_out[packet.stream]
29         ocont.mux(packet)
30     ocont.close()

subtitle と data がない前提で書いちゃってるけど、普通の音声と映像だけならこれでイケた。

なお seek はわかってる人にはお馴染みの「キーフレーム」問題があるので注意。挙げたコードはライブラリ任せにキーフレームの場所にシークする。これを「キーフレームでない箇所にピンポイントで飛ばす」ことも出来るが、再生出来ない動画が出来る可能性がある。

21日追記: filter について

「理想的ならば」という前提付きで使うならこんな具合:

 1 import argparse
 2 import av
 3 import av.filter
 4 
 5 if __name__ == '__main__':
 6     parser = argparse.ArgumentParser()
 7     parser.add_argument("inputpath")
 8     args = parser.parse_args()
 9 
10     icntnr = av.open(args.inputpath)
11     ocntnr = av.open(args.inputpath + ".out.mp4", "w")
12 
13     ivstrm = next(s for s in icntnr.streams if s.type == b'video')
14     iastrm = next(s for s in icntnr.streams if s.type == b'audio')
15     ostrms = {
16         "audio": ocntnr.add_stream(codec_name="aac", rate=iastrm.sample_rate),
17         "video": ocntnr.add_stream(codec_name="h264", rate=ivstrm.average_rate),
18         }
19 
20     graph = av.filter.Graph()
21     # you can enumerate available filters with av.filter.filters_available.
22     #print(av.filter.filters_available)
23     #
24     fchain = {"video": [], "audio": []}
25     fchain["video"].append(graph.add_buffer(template=ivstrm))
26     fchain["video"].append(graph.add("vflip"))
27     fchain["video"][-2].link_to(fchain["video"][-1])
28     fchain["video"].append(graph.add("buffersink"))
29     fchain["video"][-2].link_to(fchain["video"][-1])
30 
31     fchain["audio"].append(graph.add_abuffer(template=iastrm))
32     #fchain["audio"].append(graph.add("aecho", "0.8:0.9:1000|1800:0.3|0.25"))
33     fchain["audio"].append(graph.add("volume", "3.0"))
34     fchain["audio"][-2].link_to(fchain["audio"][-1])
35     fchain["audio"].append(graph.add("abuffersink"))
36     fchain["audio"][-2].link_to(fchain["audio"][-1])
37 
38     for packet in icntnr.demux():
39         for ifr in packet.decode():
40             typ = packet.stream.type
41             fchain[typ][0].push(ifr)
42             ofr = fchain[typ][-1].pull()
43             ofr.pts = None
44             for p in ostrms[typ].encode(ofr):
45                 ocntnr.mux(p)
46 
47     ocntnr.close()

ただし現時点でのマスターブランチでは2つ問題が。一つはメモリーリーク、もうひとつは、まだ音声に対応してない (add_abuffer なんてのはない)、てこと。後者は手を入れなくても対応出来るのかと思ったが実際はダメで、最低でも av.filter.Graphpush に手を入れる必要がある:

av.filter.graph.pyx
146     def push(self, frame):
147 
148         if isinstance(frame, VideoFrame):
149             contexts = self._context_by_type.get('buffer', [])
150         elif isinstance(frame, AudioFrame):                      # ADD
151             contexts = self._context_by_type.get('abuffer', [])  # ADD
152         else:
153             raise ValueError('can only push VideoFrame or AudioFormat', type(frame))
154        # ...