動画を投稿します。第1回はこちら。
もくじ
動画投稿実装
第5回までで、画像を投稿することができるようになった。ついでだから、動画も投稿したい。冷静に考えて公式クライアントじゃないと動画投稿できないのは不便だし、他に動画投稿できるクライアントを知らない。何でだ。
実装フロウ
- 動画投稿をとにかくやってみる
- サムネイルを作る
画像投稿の際に作ったものをたくさん流用するので、今回は実装少なめ。
動画を投稿する
いきなりゴールみたいなタイトル。とりあえずファイル名をソースコードに直打ちして動画の投稿自体ができるかどうかだけ試してみよう。
画像投稿の時はmikutterコンソールからやったのだが、動画の場合はサクッとできるシロモノではないので、ソースにコードをゴリゴリ書いてからテストすることになる。
動画投稿のAPI自体は画像投稿と同じなのだが、今回はmedia/upload chunkedを使う。動画を全部まるごと投稿するのは無理なので、ちょびちょびアップロードする、という感じだ。どうやるかはUploading Mediaを見ればイトヒキイワシでもわかる。
そのまえに、media/uploadをたくさん使うのでちょっと分割した。何度も同じことを書くのは美しくないので以下のようにしておいた。post_with_picturesの中ではこれを呼び出すようにするとよい。
def media_upload(options) options[:host] = 'upload.twitter.com/1.1' options[:https] = true Service.primary.query!('media/upload', options) end
さて、前回はpost_with_pictures関数を作ったので、今回はpost_with_video関数にしよう。流れとしては
- INIT送る
- APPENDを2MBごとに送る
- FINALIZE送る
- statuses/update送る
という形。それぞれをDeferredのnextで分割したことでわかりやすくなった。今までやってきたことを全部あわせただけなので難しくない。2MBは適当に決めた。3MBよりは小さいほうがいいんだと思う。
def post_with_video(text, &callback) callback.call(:start, nil) if callback filename = '/home/ishotihadus/Desktop/test.mp4' Thread.new { # INIT res = media_upload(:command => 'INIT', :media_type => 'video/mp4', :total_bytes => File.size(filename)) raise 'INIT failed' unless res.code.start_with?('2') id = JSON.parse(res.body)['media_id_string'] # APPEND data = File.binread(filename) chunk_length = 2048*1024 for i in 0..(data.length - 1)/chunk_length begin_pos = i * chunk_length end_pos = (i+1) * chunk_length end_pos = data.length if end_pos > data.length media_data = Base64.encode64(data[begin_pos...end_pos]) res = media_upload(:command => 'APPEND', :media_id => id, :media_data => media_data, :segment_index => i) raise 'APPEND failed' unless res.code.start_with?('2') end # FINALIZE res = media_upload(:command => 'FINALIZE', :media_id => id) raise 'FINALIZE failed' unless res.code.start_with?('2') id # POST service.post(:message => text, :media_ids => id){|event, msg| case event when :success callback.call(:success, nil) if callback when :fail raise 'post with video failed' end } }.trap{ |exception| p exception callback.call(:fail, exception) if callback } end
つぎに、投稿の関数を書き換えて様子を見る。
@posting = service.post(:message => text){ |event, msg|
を
@posting = post_with_video(text){ |event, msg|
に書き換える。post_with_videoをDeferredで実装したからこそなせる技だ。これで画像なしツイートをすると動画付きツイートが投稿されるはず。重い動画だと時間が掛かるがご愛嬌。
GUIの実装
一旦うえの実装は戻してGUIの実装をしよう。といっても画像と同じように動画が追加できるようになればいい。ただ、「動画を投稿するときは同時に画像が投稿できない」という制限を守るような実装をしなければいけない。あとは@picturesをパット見で動画であることがわかるようにHashのなかに:is_videoというキーを入れておきたい。この辺を実装していこう。
まずは条件で弾くところから。
def add_picture(filename, hash = nil) # 5つ以上画像は投稿できない return if @pictures.count >= 4 # ディレクトリはアカン return if FileTest.directory?(filename) type = MIME::Types.type_for(filename) return if type.length == 0 is_video = type[0].content_type.include?('mp4') is_image = type[0].content_type.include?('image') # 動画が画像のどちらかでないといけない return unless is_video || is_image # 動画を追加する場合は@picturesは空でないといけない return unless @pictures.empty? if is_video # すでに画像が追加されている場合は動画が追加できない return if @pictures[0][:is_video] unless @pictures.empty? # 画像は3MB未満 return if File::stat(filename).size >= 3*1024*1024 if is_image
次は@picturesの配列に入れるHashにis_videoのキーを入れておく。
if hash hash[:instance] = preview_button hash[:is_video] = is_video else @pictures << {:path => filename, :instance => preview_button, :is_video => is_video} end
さて次はサムネイルを作る。調べてみると、avconvというコマンドを使えばよさそうだとわかる(参考)。とりあえずapt-get install libav-toolsをしてavconvをインストールし、サムネイルが出力されるかやってみよう(ffmpegが使えるならそちらでもよい)。
avconv -v quiet -r 1 -i '/home/ishotihadus/Desktop/test.mp4' -f image2 -vframes 1 - > test.jpeg
ここで標準出力を使っているが、標準出力であればファイルに書き出すことなくプログラムからサムネイルが作れるのでこれを使った。出力ファイル名を-にすると標準出力に出るというのは暗黙のルール。
さて、test.jpegに正しいJPEGファイルが作られたことを確認したらこれを実装しよう。このへんを見ると、PixbufLoaderを使えばメモリからPixbufが作れるらしい。PixbufLoader.new→last_write→pixbuf取得の流れでよいか。
このままPixbufを作ると実サイズで画像がボタンの上に表示されてしまうので、あとでPixbufのscale関数でPixbufを小さくする。この辺をすべて実装するとこんな感じ。
pixbuf = nil if is_video jpg = `avconv -v quiet -r 1 -i "#{filename}" -f image2 -vframes 1 -` pixbufloader = Gdk::PixbufLoader.new pixbufloader.last_write(jpg) pixbuf = pixbufloader.pixbuf pixbuf = pixbuf.scale(80, 80 * pixbuf.height / pixbuf.width) else pixbuf = Gdk::Pixbuf.new(filename, 80, 80) end preview = Gtk::Image.new preview.set_pixbuf(pixbuf) preview_button = Gtk::Button.new preview_button.add(preview)
post_itの実装
これでもう動画は正しく@picturesに追加できたので、あとはpost_itで振り分けるだけだ。@picturesに要素があって、1つめの要素の:is_videoがtrueなら動画投稿に回すようにしよう。
post_with_videoの中でさきほどファイル名を適当に直打ちしていたので、ここを@pictures[0][:path]に書き換える。
さらにpost_itを以下のようにif文で分けることで動画が投稿できるようになるはずだ。
if @pictures.empty? @posting = service.post(:message => text){ |event, msg| case event when :start Delayer.new{ start_post } when :fail Delayer.new{ end_post } when :success Delayer.new{ destroy } end } elsif @pictures[0][:is_video] @posting = post_with_video(text){ |event, msg| case event when :start Delayer.new{ start_post } when :fail Delayer.new{ end_post } when :success Delayer.new{ destroy } end } else @posting = post_with_pictures(text){ |event, msg| case event when :start Delayer.new{ start_post } when :fail Delayer.new{ end_post } when :success Delayer.new{ destroy } end } end
動画投稿、完成!
補足
Thread.new{…}.next{…}と実行した時、nextブロックはメインスレッドで動くようです。よくできてますね。知りませんでした。Thread::currentを出力してやると動きがわかりやすいと思います。
次回予告
次回は動画を再生します。