PyAV と PIL.ImageGrab でデスクトップ録画(Windows、かつ音はなしね)

是非ともやりたかった、てわけではなくて。

PyAV の検証 しながら、「作れちゃうよな」と思っただけ。今10分くらいで作った:

 1 import signal
 2 import time
 3 import av
 4 from PIL import ImageGrab
 5 
 6 class _Interrupt(Exception):
 7     pass
 8 
 9 def _IntHandler(signum, frame):
10     raise _Interrupt()
11 
12 signal.signal(signal.SIGINT, _IntHandler)
13 # ---
14 time.sleep(3)
15 
16 ocont = av.open("dr.mp4", "w")
17 vstream = None
18 vrate = 24  #
19 while True:
20     try:
21         dimg = ImageGrab.grab()
22         #dimg = dimg.resize((dimg.width // 4 * 3, dimg.height // 4 * 3))
23         if vstream is None:
24             vstream = ocont.add_stream('h264', rate=vrate)
25             vstream.width = dimg.width
26             vstream.height = dimg.height
27             vstream.pix_fmt = 'yuv420p'
28 
29         vframe = av.VideoFrame.from_image(dimg)
30         for p in vstream.encode(vframe):
31             ocont.mux(p)
32         time.sleep(1.0 // vrate)
33     except (_Interrupt, KeyboardInterrupt):
34         print("done.")
35         break
36 ocont.close()  # MUST!

録画終了は Ctrl-C、のつもりなので INT ハンドラってるが、こんなんで良かったっけか? で、今録画したヤツ:

なお、最初

1             vstream = ocont.add_stream('mpeg4', rate=vrate)

としててあまりの画質の悪さに参った。サンプルコード猿真似してた間は黙殺してたが、よく考えたら「エンコーダが mpeg4」てのもあるんだな。とりあえず見慣れた:

1             vstream = ocont.add_stream('h264', rate=vrate)

に変えて、挙げた動画になった。

にしてもまともな分解能の動画を作りには、細かくするほど(ソースコードの vrate)プログラムの性能が問題になるかもしれん(multiprocessing のお世話になるかも)、と想像して始めたが、アニメ並みの 24 fps でご覧のとおりの実用になるものになったし、性能も全然問題ないのね、意外だった。

普段はスクリーンキャストには Hangouts on Air を使ってるけど、ところどころ不満はあるのね、やっぱり。操作が面倒でもあるし。けどこの ImageGrab + PyAV は楽だし自由度もあって、案外日常使いにもいいかもなぁ。(無論そのためにはもちっと便利にせんといけんけれど。)


21日追記:
INT ハンドラの振る舞いが不定で使いづらいんで、ちょっとだけマジメなものに書き直してみた:

 1 #! /bin/env python
 2 import time
 3 import signal
 4 import argparse
 5 from multiprocessing import Process, Queue
 6 from Queue import Empty
 7 
 8 import av
 9 from PIL import ImageGrab
10 
11 def _run_capture(outfile, q):
12     def _IntHandler(signum, frame):
13         q.put("done")
14 
15     signal.signal(signal.SIGINT, _IntHandler)
16 
17     ocont = av.open(outfile, "w")
18     vstream = None
19     vrate = 24  #
20 
21     print("start capturing.")
22     while True:
23         dimg = ImageGrab.grab()
24         #dimg = dimg.resize((dimg.width // 4 * 3, dimg.height // 4 * 3))
25         if vstream is None:
26             vstream = ocont.add_stream('h264', rate=vrate)
27             vstream.width = dimg.width
28             vstream.height = dimg.height
29             vstream.pix_fmt = 'yuv420p'
30 
31         vframe = av.VideoFrame.from_image(dimg)
32         for p in vstream.encode(vframe):
33             ocont.mux(p)
34         try:
35             r = q.get(block=False, timeout=1.0 // vrate)
36             if r:
37                 break
38         except Empty as e:
39             pass
40 
41     print("done.")
42     ocont.close()  # MUST!
43 
44 if __name__ == '__main__':
45     parser = argparse.ArgumentParser()
46     parser.add_argument("--recordfile", default="recorded.mp4")
47     parser.add_argument("--countdown", help="countdown for starting, in secs.", type=float, default=0.5)
48     args = parser.parse_args()
49 
50     time.sleep(args.countdown)
51 
52     q = Queue()
53     p = Process(target=_run_capture, args=(args.recordfile, q,))
54     p.start()
55     p.join()

multiprocessing な意味があるのかはわからんけれど、このやり方が安定して Ctrl-C を扱える、と思う。ちとダルいけど。