mikutterをいじりたおす 第6回

動画を投稿します。第1回はこちら

動画投稿実装

第5回までで、画像を投稿することができるようになった。ついでだから、動画も投稿したい。冷静に考えて公式クライアントじゃないと動画投稿できないのは不便だし、他に動画投稿できるクライアントを知らない。何でだ。

実装フロウ

  1. 動画投稿をとにかくやってみる
  2. サムネイルを作る

画像投稿の際に作ったものをたくさん流用するので、今回は実装少なめ。

動画を投稿する

いきなりゴールみたいなタイトル。とりあえずファイル名をソースコードに直打ちして動画の投稿自体ができるかどうかだけ試してみよう。

画像投稿の時は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関数にしよう。流れとしては

  1. INIT送る
  2. APPENDを2MBごとに送る
  3. FINALIZE送る
  4. 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を出力してやると動きがわかりやすいと思います。

次回予告

次回は動画を再生します。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です