動画を再生します。第1回はこちら。
もくじ
動画再生実装
現状では、動画のあるツイートの該当URLをクリックしても、そのサムネイルが表示されるだけである。でも、せっかく動画を投稿できるなら、やっぱりその動画を見たい。動くチノちゃんを見たい。チノちゃんが見たい。
実装フロウ
- 現状実装されている画像表示がどういう仕組なのかを理解する
- mikutter内で動画のURLを取得する
- 動画を再生する
まず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回でサムネイルを作る際に使った)を設定から変えられるようにした。これはそんなに難しくないので実装は省略。
参考文献
- mikutter公式 – http://mikutter.hachune.net/
- Writing mikutter plugin – http://toshia.github.io/writing-mikutter-plugin/
- mikutter RDoc – http://mikutter.hachune.net/rdoc/index.html
- mikutter開発ブログ – http://mikutter.blogspot.com/
- Ruby-GNOME2 Project Website – http://ruby-gnome2.osdn.jp/ja/
- Twitter API Documentation – https://dev.twitter.com/overview/documentation
- 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
- avconv Documentation – https://libav.org/avconv.html
- 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/
- TreeView で データを表示させてみたと思う – 想像力の欠如は深刻な欠点の一つである。 – http://noqisofon.hatenablog.com/entry/20110913/1315917614
- Stack Overflowなど