玩式草子─ソフトウェアとたわむれる日々

第13回TotemのYouTubeプラグイン再び

今年の夏は各地で記録的な猛暑の日々が続いていますが、皆さまいかがお過しでしょうか?

データセンターなどが利用できる大手ディストリビューションとは異なり、Plamoのような零細ディストリではFTPサーバ用のマシンも自宅に置いているため、夏の暑さには余計敏感になってしまいます。

今年の暑さは特に厳しくて、去年までは何とか我慢して居室で同居していたサーバ用マシンを、今年はついに隣りの物置用部屋に追い出してしまいました。サーバ機用に空調を管理できるデータセンターとは異なり、お家サーバではサーバ機よりも住人の健康優先はいたしかたないところでしょう。

さて、そのような日々でもディストリビューションの開発作業は続きます。Plamoの場合、筆者の仕事の都合で3月の後半から6月くらいまではほとんど開発時間を取れないので、7月以降の暑い時期に、春ごろから貯まった宿題を片づけるはめになりがちです。今年も、3月にリリースしたPlamo-4.72以降のアップデートを集めた次のメンテナンスリリース(Plamo-4.73)を近々に公開するために現在鋭意作業中です。

今回はその作業中に遭遇した、TotemのYouTubeプラグイン問題を紹介してみましょう。

TotemとYouTube plugin

この連載の3回目あたりでも紹介したように、TotemはGNOME環境のデフォルトのマルチメディアプレイヤーです。Totemでは、動画や音声のデータをデコードするバックエンド部をGStreamerに委ねて多様なコーデックに対応すると共に、フロントエンド部はプラグインを使ってインターネット上のさまざまなサービスに対応する柔軟性を有しています。

このTotemの各種機能の中で、筆者が気に入っていたのは動画投稿サイトYouTubeの動画を見るためのYouTubeプラグインです。この機能(プラグイン)を使えば、firefox等のWebブラウザを起動しなくてもTotemからYouTubeの動画を再生できるので、作業時のBGM代わりに愛用していました。

このプラグインは便利なものの、YouTube側の処理に強く依存しているため、動画ファイルの指定方法などが少しでも変更されれば使えなくなります。事実、かつて紹介したパッチの後にもYouTube側で動画URIの指定方法が変更されたことがあり、それに対応するための新しいパッチが必要となったこともありました。

ところが、7月の中旬くらいから、この新パッチを適用したプラグインでもYouTubeが利用できなくなっていました。正確に言うと、キーワードから動画を検索はできるものの、検索された結果を再生しようとすると、指定されたURLが見つからない旨のエラーになります。

図1 Totem のエラー
図1 Totem のエラー

このエラーは前回も経験したので、またまたYouTube側がAPIを変えたんだろうなぁ…、とあれこれ調べていたところ、修正コードがTotemの開発版用に登録(コミット)された旨の情報をPlamo-MLで教えてもらいました。

さっそくその修正コードを眺めてみましたが、最近のTotem(2.28以降)では、YouTube用のプラグインはより本体と密接に連携できるようにCで書かれたコードになっており、そのままではPlamo-4.7系が採用しているTotem-2.26のPython版プラグインで利用することはできません。一方、Totem-2.26はかなり古くなったバージョンなので、待っていてもPython版のYouTubeプラグインが修正されることは無さそうです。仕方ないので少し手元で調べてみることにしました。

Totem-2.30用YouTubeプラグインパッチ

まずはTotem-2.30用のYouTubeプラグインに施された修正を調べてみましょう。この修正はパッチファイルで公開されており、抜粋して紹介します。

23  g_regex_match (self->regex, contents, 0, &match_info);
24  if (g_match_info_matches (match_info) == TRUE) {
25  - gchar *t_param, *s;
26  - const gchar *fmt_param;
27  - GString *video_uri_string;
28  + gchar *fmt_url_map_escaped, *fmt_url_map;
29  + gchar **mappings, **i;
30  /* We have a match */
31  - s = g_match_info_fetch (match_info, 1);
32  - t_param = g_uri_unescape_string (s, NULL);
33  - if (t_param == NULL)
34  - t_param = s;
35  - else
36  - g_free (s);
37  - fmt_param = get_fmt_param (self);
38  -
39  - video_uri_string = g_string_new ("http://www.youtube.com/get_video?video_id=");
40  - g_string_append_uri_escaped (video_uri_string, video_id, NULL, TRUE);
41  - g_string_append (video_uri_string, "&t=");
42  - g_string_append_uri_escaped (video_uri_string, t_param, NULL, TRUE);
43  - g_string_append (video_uri_string, fmt_param);
44  -
45  - video_uri = g_string_free (video_uri_string, FALSE);
46  + fmt_url_map_escaped = g_match_info_fetch (match_info, 1);
47  + fmt_url_map = g_uri_unescape_string (fmt_url_map_escaped, NULL);
48  + g_free (fmt_url_map_escaped);
49  +
50  + /* The fmt_url_map parameter is in the following format:
51  + * fmt1|uri1,fmt2|uri2,fmt3|uri3,...
52  + * where fmtN is an identifier for the audio and video encoding and resolution as described here:
53  + * (http://en.wikipedia.org/wiki/YouTube#Quality_and_codecs) and uriN is the playback URI for that format. */
54  + mappings = g_strsplit (fmt_url_map, ",", 0);
55  +
56  + for (i = mappings; *i != NULL; i++) {
57  + /* For the moment we just take the first format we get */
58  + gchar **mapping = g_strsplit (*i, "|", 2);
59  + video_uri = g_strdup (mapping[1]);
60  + g_strfreev (mapping);
61  + break;
62  + }

パッチファイルに詳しくない人でも、このリストを見ればある程度意味がわかることでしょう。パッチファイルでは、行頭に "-" が付いた行を削除し "+" が付いた行を挿入することで、元のファイルを新しいファイルに修正することができます。

このパッチファイルを見る限り、変更前の従来の手順では、32行目で入手したt_paramを元に、39行目から43行目でサーバのアドレスや動画のID番号(video_id)などの情報を付け加えて動画ファイルのURLを生成しているのに対して、変更後の新しい手順では46~47行目でfmt_url_mapという変数を作り、その変数を切り分けることで動画ファイルのURLを生成しているようです。

具体的には、50行目からのコメントに示されているように、fmt_url_mapにはfmt1|uri1,fmt2|uri2,…という形式で複数のフォーマット情報と対応するURL情報が折り込まれていて、それを54行目のg_strsplit()で "," を区切りに分割し、さらにをその結果を56行目からのループで "|" を区切りに分割し、その結果をvideo_uriと見なす、といった処理のようです。

YouTube用プラグインにどのような修正が必要なのかをざっと把握した上で、Totem-2.26のPython版プラグインの方に目を移します。

Python版のYouTubeプラグイン

上述のように、Plamo-4.7系で採用しているTotem-2.26ではYouTube用プラグインはPythonで書かれています。ざっとソースコード(/usr/lib/totem/plugins/youtube/youtube.py)を眺めた限りでは、全体で415行ほどのファイルで、 DownloadThread, CallbackThread, YouTube という3つのクラスが定義されているだけで、これらのクラスを呼び出す処理らしき部分は見当りません。

どうやってこれらのクラスを使うのだろう、と少し悩みましたが、恐らくそれらはTotemの本体側の機能になっているのだろうと考え、とりあえずYouTubeにアクセスしている部分を手掛かりにしようと、youtube.comをキーワードにソースコードをgrepしてみました。

 % grep -C2 youtube.com totem.py
   128
   129                  """Set up the service"""
   130                  self.service = gdata.service.GDataService (account_type = "HOSTED_OR_GOOGLE", server = "gdata.youtube.com")
   131
   132          def deactivate (self, totem):
--
   204          def resolve_t_param (self, youtube_id):
   205                  """We have to get the t parameter from the actual video page, since Google changed how their URLs work"""
   206                  stream = urllib.urlopen ("http://youtube.com/watch?v=" + urllib.quote (youtube_id))
   207                  regexp1 = re.compile ("swfArgs.*\"t\": \"([^\"]+)\"")
   208                  regexp2 = re.compile ("</head>")
--
   242
   243                  """Open the video in the browser"""
   244                  os.spawnlp (os.P_NOWAIT, "xdg-open", "xdg-open", "http://www.youtube.com/watch?v=" + urllib.quote (youtube_id) + self.get_fmt_string ())
   245
   246          def on_button_press_event (self, widget, event):
--
   335
   336                  if t_param != "":
   337                          mrl = "http://www.youtube.com/get_video?video_id=" + urllib.quote (youtube_id) + "&t=" + t_param + self.get_fmt_string ()
   338
   339                  gobject.idle_add (self._append_to_liststore, treeview_name, pixbuf, entry.title.text, mrl, youtube_id, search_token)

grepに与えた-C2というオプションは、該当行の前後2行も併せて表示せよ、という指示です。この結果を見ると、上記のように "youtube.com" が出てくるのは 130行目、206行目、244行目、337行目の4ヵ所でした。

ざっと見、130行目はGoogleの提供している各種サービスを利用するためにlibgdataに必要な情報を登録している部分、244行目はWebブラウザからプラグインとしてで呼ばれた際の処理のようです。また、337行目は実際にYouTubeから動画データを取り寄せる処理のようですが、現在問題になっているのは、そのための位置を定める部分のはずなので、残りの206行目の前後から見てみることにしました。

この部分のソースコードを調べてみると、206行目は204行目から始まる resolve_t_param() という処理の一部になっていました。

   204          def resolve_t_param (self, youtube_id):
   205                  """We have to get the t parameter from the actual video page, since Google changed how their URLs work"""
   206                  stream = urllib.urlopen ("http://youtube.com/watch?v=" + urllib.quote (youtube_id))
   207                  regexp1 = re.compile ("swfArgs.*\"t\": \"([^\"]+)\"")
   208                  regexp2 = re.compile ("</head>")
   209
   210                  contents = stream.read ()
   211                  if contents != "":
   212                          """Check for the t parameter, which is now in a JavaScript array on the video page"""
   213                          matches = regexp1.search (contents)
   214                          if (matches != None):
   215                                  stream.close ()
   216                                  return matches.group (1)
   217
   218                          """Check to see if we've come to the end of the <head> tag; in which case, we should give up"""
   219                          if (regexp2.search (contents) != None):
   220                                  stream.close ()
   221                                  return ""
   222
   223                  stream.close ()
   224                  return ""
   225

オブジェクト指向のPythonの場合、stream = ...で定義したオブジェクトからstream.read()することでデータを読み出します。また、urllib.urlopen(..)は指定されたURLとオブジェクトを結びつける処理なので、このあたりでYouTubeから動画に関するデータを取ってきている気配が濃厚です。また、207行目のre.compile ("swfArgs...")の部分は少し見覚えがあるな、と思っていたら、以前のパッチでも修正された箇所でした。

デバッグ、デバッグ、デバッグ

修正すべき箇所の手掛りが見えてきたので、実際の動作を確認するためのデバッグ用コードを埋めこみながら調べてゆきます。

まず210行目の stream.read() で読み出した contents の中身を直接見てみることにしました。

   210                  contents = stream.read ()
   211                  print "my_debug:",contents
   212                  if contents != "":

このプラグインはPythonで書かれているので、211行目を/usr/lib/totem/plugin/youtube/youtube.pyに直接書き加えてTotemを再起動すれば、起動したターミナルにcontentsの中身が表示されるようになりました。

図2 デバッグメッセージを出力しているYouTubeプラグイン
図2 デバッグメッセージを出力しているYouTubeプラグイン

この出力をファイルに保存してみると、3万行を超えるUTF-8なHTMLファイルになっていました。

  1	my_debug: <!DOCTYPE html>
  2	<html lang="" dir="ltr">
  3	<!-- machid: ta3Z2bmtLVURHRVBnejN3ODRJTWU3M0F6UTNfck9jWjFCRF9nVEstUllVXzE0aExvWnVLS1ln -->
  4	<head>
  5					<script>
  6				var yt = yt || {};
  7				yt.timing = yt.timing || {};
  8				yt.timing.cookieName = 'VISITOR_INFO1_LIVE';
  9				yt.timing.tick = function(label) %7B
 ....

つらつらとこのログファイルを眺めていると、261行目あたりに何度も折り返されている巨大な行がありました。

261		var swfHTML = (isIE) ? "<object height=\"38" + "5\" width=\"64" + "0\"  classid=\"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\" id=\"movie_player\" ><param name=\"movie\"  value=\"http:\/\/s.ytimg.com\/yt\/swf\/watch_as3-vfl180154.swf\"><param name=\"flashvars\"  ...

どうやらこの行が怪しそうだ、と当りを付けて、詳しく眺めてみると、1行の中に & を区切りにして複数のパラメータが並んでいる気配です。そこでEmacsの中から & を & +[改行]に変換してやると、かなり見通しがよくなりました。

	var swfHTML = (isIE) ? "<object height=\"38" + "5\" width=\"64" + "0\" classid=\"..
  rv.2.thumbnailUrl=http%3A%2F%2Fi3.ytimg.com%2Fvi%2F240Vq6tIxio%2Fdefault.jpg&
  rv.0.url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DxBZOlipfjkQ&
  rv.0.view_count=888291&
  enablecsi=1&
  rv.2.title=Bad+Apple%21%21+-+Stop+Motion+PV&
  rv.6.author=tripflag&
  rv.3.view_count=192392&
  is_doubleclick_tracked=1&
  rv.4.thumbnailUrl=http%3A%2F%2Fi3.ytimg.com%2Fvi%2F2GIMgD_gShU%2Fdefault.jpg&
  fmt_url_map=34%7Chttp%3A%2F%2Fv4.lscache5.c.youtube.com%2Fvideoplayback%3Fip%3D210.0.0.0%26sparams%3Did%252Cexpire..
  csi_page_type=watch&
  keywords=%E6%9D%B1%E6%96%B9%2CTouhou%2CBad%2CApple%21%21%2CPV%2C%E9%9F%B3%E6%A5%BD%2C%E6%9D%B1%E6%96%B9%E6%89%8B%E6%..
  cr=JP&
  cc3_module=http%3A%2F%2Fs.ytimg.com%2Fyt%2Fswf%2Fsubtitles3_module-vfl180154.swf&

この結果を見るとfmt_url_mapの部分がYouTubeのサーバを指している気配です。そう言えば、fmt_url_map に関する処理はTotem-2.30用のパッチにもあったなぁ…と思い出して、C版のパッチファイルを見直してみれば、まさにそれらしい部分がありました。

 71	- self->regex = g_regex_new ("swfHTML = .*&t=([^&]+)&", G_REGEX_OPTIMIZE, 0, NULL);
 72	+ self->regex = g_regex_new ("swfHTML = .*&fmt_url_map=([^&]+)&", G_REGEX_OPTIMIZE, 0, NULL);

このパッチを見る限り、かっては &t=.. 以下で指定されていた部分を取り出していたのに対し、最近では &fmt_url_map=.. で指定された部分を取り出す必要があることを示しています。

改めて、デバッグログのfmt_url_mapの部分を詳しく見ると、確かにYouTubeのサーバ上の特定のファイルを指定しているようですが、%7Cや%3AといったURLエンコードが目障りです。必要な部分をカット&ペーストしてURLデコードしてやるとかなり見やすくなりました。

 fmt_url_map=34|http://v4.lscache5.c.youtube.com/videoplayback?ip=210.0.0.0&sparams=id%2Cexpire.. csi_page_type=watch&..

改めて、先のTotem-2.30用のパッチを見ると、50行目から53行目のコメントで、fmt_url_map はfmt1|uri1,fmt2|uri2,fmt3|uri3,...の形式になっている旨がwikipediaからの引用として指摘されています。ならば、このデータはfmt1=34、url1=http://v4.lscache5.c.youtube.com/videoplayback?ip=210.... と解釈できそうです。この部分をつかまえるように regex1 の部分を直してみましょう。

207                # regexp1 = re.compile ("swfHTML = .*&t=([^&]+)&")
208                regexp1 = re.compile ("fmt_stream_map=([^&]+)&")
209                regexp2 = re.compile ("</head>")

合わせて、このregexp1で捕まえた部分を出力するようにしておきます。

213                          matches = regexp1.search (contents)
214                          print "my_debug_2:", matches.group(1)
215                          if (matches != None):
216                             stream.close ()
217                                     return matches.group (1)

これらの修正を加えて再起動し再度ログを取って調べると、214行目に追加したmy_debug_2 部の出力にURLを含むデータを捕えていることが確認できました。

my_debug_2: 34%7Chttp%3A%2F%2Fv4.lscache5.c.youtube.com%2Fvideoplayback%3Fip%3D210.0.0.0%26sparams%3Did%252Cexpire%252Cip%252Cipbits%252Citag%252Calgorithm%252Cburst%252Cfactor%26algorithm%3Dthrottle-factor%26itag%3D34%26ipbits%3D8%26burst%3D40%26sver%3D3%26expire%3D1280865600%26key%3Dyt1%26signature%3DA1BEA852CE9BCCEA89F4DEB5EE912081BDC79...

ここまで来れば、後はこのデータをURLデコードし、"," で分割し、その最初の部分を "|" で分割してやればよさそうです。その処理は後半の t_param を構成する部分に追加してみましょう。

336                 """Get the video stream MRL"""
337                 quoted_param = self.resolve_t_param(youtube_id)
338
339                 fmt_stream_param = urllib.unquote(quoted_param)
340                 param_list = fmt_stream_param.split(',')
341                 tmp_param = param_list[0]
342                 fmt_list =  tmp_param.split('|')
343                 t_param = fmt_list[1]
344                 print "my_debug_3: quoted_param:", quoted_param
345                 print "my_debug_3: fmt_stream_param:", fmt_stream_param
346                 print "my_debug_3: param_list:", param_list
347                 print "my_debug_3: tmp_param:", tmp_param
348                 print "my_debug_3: fmt_list:",fmt_list

この処理は、まず337行でmatches.group(1)に含まれるURLを含むデータを quoted_param に入れ、そのデータを339行目で urllib.unquote() してから "," で分割し(340行⁠⁠、その最初の部分を tmp_param に入れて(341行)、tmp_paramを "|" で分割して(342行)、fmt1|url1 で示されている url1 の部分を t_param に代入する(343行)という流れになっています。

この変更を追加してログを取り直してみると fmt_list[] にhttp://v4.lscache5.c.youtube.com/.. というURLが取り出せていました。

my_debug_3: quoted_param: 34%7Chttp%3A%2F%2Fv4.lscache5.c.youtube.com%2Fvideoplayback%3Fip%3D210.0.0.0%26sparams%3Did%2 52Cexpire%252Cip%252Cipbits%252Citag%252Calgorithm%252Cburst%252Cfactor%26fexp%3D900505%26algorithm%3Dthrottle-factor%2 ...
my_debug_3: fmt_stream_param: 34|http://v4.lscache5.c.youtube.com/videoplayback?ip=210.0.0.0&sparams=id%2Cexpire%2Cip%2 Cipbits%2Citag%2Calgorithm%2Cburst%2Cfactor&fexp=900505&algorithm=throttle-factor&itag=34&ipbits=8&burst=40&sver=3&expi ...
my_debug_3: param_list: ['34|http://v4.lscache5.c.youtube.com/videoplayback?ip=210.0.0.0&sparams=id%2Cexpire%2Cip%2Cipb its%2Citag%2Calgorithm%2Cburst%2Cfactor&fexp=900505&algorithm=throttle-factor&itag=34&ipbits=8&burst=40&sver=3&expire=1 ...
my_debug_3: fmt_list: ['34', 'http://v4.lscache5.c.youtube.com/videoplayback?ip=210.0.0.0&sparams=id%2Cexpire%2Cip%2Cip bits%2Citag%2Calgorithm%2Cburst%2Cfactor&fexp=900505&algorithm=throttle-factor&itag=34&ipbits=8&burst=40&sver=3&expire= ...

あとはこうして取り出した t_param に含まれたURLを、mrlという変数経由でTotem本体に返してやればよさそうです。

350                 if t_param != "":
351                         # mrl = "http://www.youtube.com/get_video?video_id=" + urllib.quote (youtube_id) + "&t=" + t_param + self.get_fmt_string ()
352                         mrl = t_param

YouTubeプラグインにここまでの修正を施してみると、キーワード検索した動画がTotem-2.26でも再生できるようになりました。

図3 復活したYouTubeプラグイン
図3 復活したYouTubeプラグイン

さて、Totemには現在再生している動画ファイルのコーデック等の情報を表示する「プロパティ」という機能があります。この機能を試してみると、今見ている動画ファイルでは、ビデオコーデックは H.264/AVC、音声コーデックはMPEG-4 AAC audioになっていることがわかりました。

図4 Totemのプロパティ情報
図4 Totemのプロパティ情報

TotemではこれらのコーデックはバックエンドのGStreamerの機能で直接再生できるのに対し、firefox等のブラウザでYouTubeを見る際には、Adobe社からバイナリのみで配布されているlibflashplayerプラグインが必要になります。その意味で、TotemでYouTubeを見る方がOSSフレンドリーという気はしますが、フラッシュプレイヤー経由の広告配信等は機能しなくなるので、YouTube的にはありがたくない観客になりそうです(苦笑⁠⁠。

ただ、このあたりはGoogleがフリーで公開した動画規格であるWebMや現在仕様策定が進められているHTML5などと関係して、これからも大きく変っていく部分なので、今回の修正が有効な期間はそう長くないでしょう。次のYouTube側の修正までには、Totemの最新版に追従して、Plamoローカルなパッチは不要にしたいところです。

おすすめ記事

記事・ニュース一覧