mikutterをいじりたおす 第7回

動画を再生します。第1回はこちら

動画再生実装

現状では、動画のあるツイートの該当URLをクリックしても、そのサムネイルが表示されるだけである。でも、せっかく動画を投稿できるなら、やっぱりその動画を見たい。動くチノちゃんを見たい。チノちゃんが見たい。

実装フロウ

  1. 現状実装されている画像表示がどういう仕組なのかを理解する
  2. mikutter内で動画のURLを取得する
  3. 動画を再生する

まずtwitterの仕様としてExtended Entitiesという概念があることを知っていなければならない。ここには画像やビデオのデータ、サムネイル、その大きさやバイト数、様々な情報が含まれていて、これがentityの含まれる(つまり画像や動画が含まれる)ほとんどのツイートに付属している。僕は動画をアップロードした時のレスポンスを見て気づいたが、知らない人は知らないままなので、常識としてまず紹介しておく。

画像をいかに表示しているのか

まずはリンクをポチッとすると出てくる画像の仕組みを知ろう。おそらくこれはプラグイン実装なので、プラグインのフォルダを眺める。すると「openimg」というフォルダがあることがわかる。中の.mikutter.ymlを見ると、「画像だけを専用のウィンドウに表示する」という説明があって、まさにこれっぽい。

中を見てみよう。いろいろな処理をしていてよくわからないが、199行目に以下のような記述がある。

::Gtk::TimeLine.addopenway(->_{
                             openers = Plugin.filtering(:openimg_image_openers, Set.new).first
                             openers.any?{ |opener| opener.condition === _ }
                           }) do |shrinked_url, cancel|
  Thread.new do
    url = (Plugin.filtering(:expand_url, [shrinked_url]).first.first rescue shrinked_url)
    notice url
    Plugin.call(:openimg_open, url) end end

つまり、addopenwayしてあげることで、新しいリンクを開く方法を提供することができるのではないか、というわけだ。どうやら見た限り、「::Gtk::TimeLine.addopenway(-> _ { bool値を返す関数 }) do |url, cancel| 処理 end」とすることで登録できそうだ。bool値を返す関数は、どうやら_という変数が使えるらしい。それ以外に判断する方法がなさそうだ。おそらく ->_ がそれを示しているのだろう。いわゆる「アロー演算子」というやつだ。一度この_をputsしてみるとよい。なにが送られてくるのかわかりやすいだろう。任意のリンクをクリックした際にそのURLが出力される。

つまり、URLをmp4にどこかで書き換えて、それを別プラグインで受け取るようにして、addopenwayすればいいのでは、ということになる。

URLの書き換え

URLは最初t.coで書かれた状態で送られてくる。これはすべてのリンクに関してそうで、たとえ公式pic.twitter.comでもこうなる。まずはこれを書き換えている主を探す必要がある。

間違いなくextended_entitiesを読んでいることは確かなので、これで全grepをかけてみよう。するとすべてcore/entity.rbにまとめられていることがわかる。これも300行弱あるので読むのがつらいが、少し我慢して読んでいこう。

45行目辺りにrefreshという関数が定義されており、ここにfilterが並んでいる。filterということは書き換えている可能性が高い。以下のコードで:faceと:urlに表示URLと実際URLを代入しているのだろう。実際にsegment[:url]を出力すると、書き換えられているのがわかるはずだ。

filter(:media){ |segment|
  segment[:face] = segment[:display_url]
  segment[:url] = segment[:media_url]
  segment }

ガバッとsegment自体がどうなっているのかを調べてみよう。ppで出力すると、いかのようなデータが得られる。

{:slug=>:media,
 :filter_id=>nil,
 :regexp=>nil,
 :callback=>
  #<Proc:0x007f6abbf5b4a0@/home/ishotihadus/Documents/mikutter_dev/core/mui/cairo_timeline.rb:31>,
 :message=>
  {:created_at=>"Tue Nov 03 10:09:57 +0000 2015", :id=>661485458241679360, :id_str=>"661485458241679360", :message=>"@Ishotihadus なら頑張りなさい。応援してあげるから。 https://t.co/zTdTRvLUsf", :source=>"ウィズレー魔法学院", :truncated=>false, :replyto=>661485311684247552, :in_reply_to_status_id_str=>"661485311684247552", :receiver=>123516332, :in_reply_to_user_id_str=>"123516332", :in_reply_to_screen_name=>"Ishotihadus", :user=>User(@orietta_bot), :geo=>nil, :coordinates=>nil, :place=>nil, :contributors=>nil, :is_quote_status=>false, :retweet_count=>0, :favorite_count=>0, :entities=>{:hashtags=>[], :symbols=>[], :user_mentions=>[{:screen_name=>"Ishotihadus", :name=>"イショティハドゥス", :id=>123516332, :id_str=>"123516332", :indices=>[0, 12]}], :urls=>[], :media=>[{:id=>345663766987407362, :id_str=>"345663766987407362", :indices=>[33, 56], :media_url=>"http://pbs.twimg.com/media/BMwLXVsCAAILqd6.jpg", :media_url_https=>"https://pbs.twimg.com/media/BMwLXVsCAAILqd6.jpg", :url=>"https://t.co/zTdTRvLUsf", :display_url=>"pic.twitter.com/zTdTRvLUsf", :expanded_url=>"http://twitter.com/orietta_bot/status/345663766983213056/photo/1", :type=>"photo", :sizes=>{:thumb=>{:w=>150, :h=>150, :resize=>"crop"}, :small=>{:w=>340, :h=>191, :resize=>"fit"}, :medium=>{:w=>600, :h=>337, :resize=>"fit"}, :large=>{:w=>1024, :h=>576, :resize=>"fit"}}, :source_status_id=>345663766983213056, :source_status_id_str=>"345663766983213056", :source_user_id=>1517545424, :source_user_id_str=>"1517545424"}]}, :extended_entities=>{:media=>[{:id=>345663766987407362, :id_str=>"345663766987407362", :indices=>[33, 56], :media_url=>"http://pbs.twimg.com/media/BMwLXVsCAAILqd6.jpg", :media_url_https=>"https://pbs.twimg.com/media/BMwLXVsCAAILqd6.jpg", :url=>"https://t.co/zTdTRvLUsf", :display_url=>"pic.twitter.com/zTdTRvLUsf", :expanded_url=>"http://twitter.com/orietta_bot/status/345663766983213056/photo/1", :type=>"photo", :sizes=>{:thumb=>{:w=>150, :h=>150, :resize=>"crop"}, :small=>{:w=>340, :h=>191, :resize=>"fit"}, :medium=>{:w=>600, :h=>337, :resize=>"fit"}, :large=>{:w=>1024, :h=>576, :resize=>"fit"}}, :source_status_id=>345663766983213056, :source_status_id_str=>"345663766983213056", :source_user_id=>1517545424, :source_user_id_str=>"1517545424"}]}, :favorited=>false, :retweeted=>false, :possibly_sensitive=>false, :lang=>"ja", :created=>2015-11-03 19:09:57 +0900, :exact=>false},
 :from=>:message_entities,
 :id=>345663766987407362,
 :id_str=>"345663766987407362",
 :indices=>[33, 34],
 :media_url=>"http://pbs.twimg.com/media/BMwLXVsCAAILqd6.jpg",
 :media_url_https=>"https://pbs.twimg.com/media/BMwLXVsCAAILqd6.jpg",
 :url=>"https://t.co/zTdTRvLUsf",
 :display_url=>"http://pbs.twimg.com/media/BMwLXVsCAAILqd6.jpg",
 :expanded_url=>
  "http://twitter.com/orietta_bot/status/345663766983213056/photo/1",
 :type=>"photo",
 :sizes=>
  {:thumb=>{:w=>150, :h=>150, :resize=>"crop"},
   :small=>{:w=>340, :h=>191, :resize=>"fit"},
   :medium=>{:w=>600, :h=>337, :resize=>"fit"},
   :large=>{:w=>1024, :h=>576, :resize=>"fit"}},
 :source_status_id=>345663766983213056,
 :source_status_id_str=>"345663766983213056",
 :source_user_id=>1517545424,
 :source_user_id_str=>"1517545424"}

かなり長くてアレだが、普通に:extended_entitiesの中身があることがわかる。つまりコレ自体が1つのextended_entityなのだ。ためしにvideoが入っているツイートを見てみよう。これは:typeが’video’なツイートを見れば良い。これのsegmentを見てみよう。

{:slug=>:media,
 :filter_id=>nil,
 :regexp=>nil,
 :callback=>
  #<Proc:0x007f8fcd5d2410@/home/ishotihadus/Documents/mikutter_dev/core/mui/cairo_timeline.rb:31>,
 :message=>省略,
 :from=>:message_entities,
 :id=>661709148283736064,
 :id_str=>"661709148283736064",
 :indices=>[4, 5],
 :media_url=>
  "http://pbs.twimg.com/ext_tw_video_thumb/661709148283736064/pr/img/Z2ALimGm0UDFVXbA.jpg",
 :media_url_https=>
  "https://pbs.twimg.com/ext_tw_video_thumb/661709148283736064/pr/img/Z2ALimGm0UDFVXbA.jpg",
 :url=>"https://t.co/YpuxlQ16qk",
 :display_url=>
  "http://pbs.twimg.com/ext_tw_video_thumb/661709148283736064/pr/img/Z2ALimGm0UDFVXbA.jpg",
 :expanded_url=>
  "http://twitter.com/Ishotihadus_E/status/661709215690457088/video/1",
 :type=>"video",
 :sizes=>
  {:thumb=>{:w=>150, :h=>150, :resize=>"crop"},
   :medium=>{:w=>600, :h=>338, :resize=>"fit"},
   :small=>{:w=>340, :h=>191, :resize=>"fit"},
   :large=>{:w=>1024, :h=>576, :resize=>"fit"}},
 :video_info=>
  {:aspect_ratio=>[16, 9],
   :duration_millis=>7883,
   :variants=>
    [{:content_type=>"application/dash+xml",
      :url=>
       "https://video.twimg.com/ext_tw_video/661709148283736064/pr/pl/ZWn3wXH25b9VEZLn.mpd"},
     {:content_type=>"application/x-mpegURL",
      :url=>
       "https://video.twimg.com/ext_tw_video/661709148283736064/pr/pl/ZWn3wXH25b9VEZLn.m3u8"},
     {:bitrate=>832000,
      :content_type=>"video/mp4",
      :url=>
       "https://video.twimg.com/ext_tw_video/661709148283736064/pr/vid/640x360/ZFR9NL3lcl8R8W_e.mp4"},
     {:bitrate=>832000,
      :content_type=>"video/webm",
      :url=>
       "https://video.twimg.com/ext_tw_video/661709148283736064/pr/vid/640x360/ZFR9NL3lcl8R8W_e.webm"},
     {:bitrate=>320000,
      :content_type=>"video/mp4",
      :url=>
       "https://video.twimg.com/ext_tw_video/661709148283736064/pr/vid/320x180/K74c7iiRM0fl9nC9.mp4"}]}}

なので、:typeがvideoのときは、:faceと:urlを:video_info->:variants->[どれか]->:urlにか聞けてしまえば良い。統一するために、:content_typeがvideo/mp4であるもの、さらにこの中でビットレートがもっとも高いものを選ぶことにしよう。これを使ってフィルターを実装しよう。

filter(:media){ |segment|
  if segment[:type] == 'video' || segment[:type] == 'animated_gif'
    sel = segment[:video_info][:variants].select{|v| v[:content_type] == 'video/mp4'}.max_by{|v| v[:bitrate]}
    if sel
      segment[:url] = sel[:url]
    else
      segment[:url] = segment[:expanded_url]
    end
    segment[:face] = segment[:expanded_url]
  else
    segment[:face] = segment[:display_url]
    segment[:url] = segment[:media_url]
  end
  segment }

こんな風に実装すれば動画のときにうまく書き換えられるだろう。:faceは/photo/1みたいなアレのURLになるのでmp4のURLよりは好き。まあ好みの問題なのでsegment[:url]にしてもよいだろう。

これで動画付きのツイートを開くと、mp4の動画への直リンが開くはずだ。

補足

:typeがanimated_gifのときもこれが適用される。

{:slug=>:media,
 :filter_id=>nil,
 :regexp=>nil,
 :callback=>
  #<Proc:0x007f26b00da178@/home/ishotihadus/Documents/mikutter_dev/core/mui/cairo_timeline.rb:31>,
 :message=>(省略),
 :from=>:message_entities,
 :id=>661708404650455040,
 :id_str=>"661708404650455040",
 :indices=>[4, 5],
 :media_url=>"http://pbs.twimg.com/tweet_video_thumb/CS7cScQUsAAwaU-.png",
 :media_url_https=>
  "https://pbs.twimg.com/tweet_video_thumb/CS7cScQUsAAwaU-.png",
 :url=>"https://t.co/AborJuSl8l",
 :display_url=>"http://pbs.twimg.com/tweet_video_thumb/CS7cScQUsAAwaU-.png",
 :expanded_url=>
  "http://twitter.com/Ishotihadus/status/661708406844100608/photo/1",
 :type=>"animated_gif",
 :sizes=>
  {:small=>{:w=>100, :h=>100, :resize=>"fit"},
   :medium=>{:w=>100, :h=>100, :resize=>"fit"},
   :large=>{:w=>100, :h=>100, :resize=>"fit"},
   :thumb=>{:w=>100, :h=>100, :resize=>"crop"}},
 :video_info=>
  {:aspect_ratio=>[1, 1],
   :variants=>
    [{:bitrate=>0,
      :content_type=>"video/mp4",
      :url=>"https://pbs.twimg.com/tweet_video/CS7cScQUsAAwaU-.mp4"}]}}

animated_gifでも同じ処理をすれば済むのでうえで実装した。実験では気づかなかったので実装していない。

動画の再生

今回はavplayを使って再生する。gemにあるかなーと思って探したのだが見つからなかった。誰かgem作れ。

まずはcore/plugin/avplayディレクトリを作って、core/plugin/openimg/の中の.mikutter.ymlとopenimg.rbをコピーする。

まずは.mikutter.ymlの編集。適当に編集すればよいだろう。

---
slug: :avplay
depends:
  mikutter: '3.2'
version: '2.0'
author: ishotihadus
name: 動画プレビュー
description: 動画を不思議な力で再生する

つぎにopenimg.rbをavplay.rbに書き換えて、下まで削除。

# -*- coding: utf-8 -*-                                                                                                                          

Plugin.create :avplay do
  ::Gtk::TimeLine.addopenway(->_{
                               openers = Plugin.filtering(:openimg_image_openers, Set.new).first
                               openers.any?{ |opener| opener.condition === _ }
                             }) do |shrinked_url, cancel|
    Thread.new do
      url = (Plugin.filtering(:expand_url, [shrinked_url]).first.first rescue shrinked_url)
      notice url
      Plugin.call(:openimg_open, url) end end
end

次に条件と動作を改良する。といってもめんどくさいので以下のようにすればよいだろう。雑の鬼。

# -*- coding: utf-8 -*-                                                                                                                          

Plugin.create :avplay do
  ::Gtk::TimeLine.addopenway(->_{!!(_ =~ /\.mp4$/)}) do |url, cancel|
    Thread.new do
      `avplay -window_title mikutter -loop 0 "#{url}"`
    end
  end
end

これで動画をクリックするとavplayの画面が開いてチノちゃんが踊る。

補足

実験ではavplay・avconv(第6回でサムネイルを作る際に使った)を設定から変えられるようにした。これはそんなに難しくないので実装は省略。

参考文献

  1. mikutter公式 – http://mikutter.hachune.net/
  2. Writing mikutter plugin – http://toshia.github.io/writing-mikutter-plugin/
  3. mikutter RDoc – http://mikutter.hachune.net/rdoc/index.html
  4. mikutter開発ブログ – http://mikutter.blogspot.com/
  5. Ruby-GNOME2 Project Website – http://ruby-gnome2.osdn.jp/ja/
  6. Twitter API Documentation – https://dev.twitter.com/overview/documentation
  7. GTK/GNOMEによるGUIプログラミング – http://www.iim.cs.tut.ac.jp/~sugaya/wiki/wiki/index.php?GTK%2FGNOME%A4%CB%A4%E8%A4%EBGUI%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0
  8. avconv Documentation – https://libav.org/avconv.html
  9. Ubuntu Server 12.04で動画のサムネイルを作成~avconv ffmpeg | Scimpr Blog – http://blog.scimpr.com/2012/09/24/ubuntu-server-12-04%E3%81%A7%E5%8B%95%E7%94%BB%E3%81%AE%E3%82%B5%E3%83%A0%E3%83%8D%E3%82%A4%E3%83%AB%E3%82%92%E4%BD%9C%E6%88%90%EF%BD%9Eavconv-ffmpeg/
  10. TreeView で データを表示させてみたと思う – 想像力の欠如は深刻な欠点の一つである。 – http://noqisofon.hatenablog.com/entry/20110913/1315917614
  11. Stack Overflowなど

コメントを残す

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