Edited at
RStudioDay 14

tubeplayRパッケージはなぜRStudio上でYouTubeを再生できるのか

More than 1 year has passed since last update.

これは,RStudio Advent Calendar 2017 14日目の記事です。

かなりマニアックな話となります。知ってても知らなくても今後のRStudioライフには影響しないと思います。でもこういう記事こそ燃えます。


tubeplayRパッケージ

私が作成したRのパッケージで,RStudioのMac向けデスクトップ版,あるいはServer版で動作する,RStudio上でYouTubeを再生するパッケージです。実行結果は以下のような感じです:

93845287.png

現在CRANには登録しておらず,GitHubのリポジトリで公開しています。また,このパッケージの使い方については,以下のスライドを参照してください:

YouTube on RStudio - Hijiyama.R #6

RStudio上でYouTubeを再生することで,以下のメリットがあります:


  • YouTube上に公開されているR関連のWebinerを参照しながらできる

  • 大好きな動画を楽しみつつ作業できる

もちろん個人によっては不要だと感じるかもしれませんが,興味がある方は利用してみてください。さて,今日の本題は「なんでRStudioでこんなことができるの?」です。


tubeplayRの仕組み

実際のコードはGitHub上のコードを参照してください。ざっくり言うと,以下の処理を行っています:


  • 受け取ったurlから動画(もしくはプレイリスト)IDを取り出す

  • 表示するhtmlコンテンツを生成



    • miniUI::miniPage()でダイアログボックス向けhtmlページを生成



      • htmltoolsパッケージを駆使してstyleを作成

      • 動画IDを利用して作成した埋め込み用iframeコンテンツを作成





  • 作成したhtmlコンテンツをhtmltools::html_print()でviewerへ送る

これだけです。結構シンプルです。ポイントを説明していきます。


htmlコンテンツ生成

該当するコードが以下となります:

# make_ui

ui <- miniUI::miniPage(
htmltools::tags$style(type = "text/css",
paste(sep = "\n",
"<!--",
"div.iframeWrap {",
htmltools::css(
position = "relative",
width = "100%",
`padding-top` = "56.25%"
),
"}",
"div.iframeWrap iframe {",
htmltools::css(
position = "absolute",
top = 0,
left = 0,
width = "100%",
height = "100%"
),
"}",
"-->"
)),
htmltools::tags$div(class = "iframeWrap",
htmltools::tags$iframe(src = paste("https://www.youtube.com/embed/", target, sep = ""),
frameborder="0")
)
)

このパッケージではminiUI::miniPage()を利用しています。これは主にRStudio AddinのUIを作成することを念頭に準備されたものです。本当はshiny::fillPage()でもいいんでしょうけど,開発の経緯としてRStudio Addin形式を利用する方法も考えていたので,その名残です。当面はこのままでいきます。

このminiUI::miniPage()内部にネストする感じでhtml要素を作成していきます。ただ,これを普通のテキスト形式で書き出してもうまくいきませんし,何より効率が悪くなります。そこで出番がhtmltools::tagsオブジェクトです。これについては既に別途記事を書いているのでそちらを参照してください。

というわけでこれで生成されたuiですが,このオブジェクトはhtmlオブジェクト(正確にはshinyクラスも付与さてる)という扱いになります。


RStudioのViewerに表示させる

これは以下の一行でOKです:

htmltools::html_print(ui)

このhtmltools::html_print()は,上述のようなhtmlコンテツをviewerにて表示させる,HTMLコンテンツのprintと思ってくれてOKです。楽ちんです。

なお,Rでの可視化としてhtmlwidgets系パッケージがありますが,これらのパッケージでの生成物はViewerに表示されますが,これはhtmlwidgets:::print.htmlwidgetメソッドが呼び出されるためです:

> htmlwidgets:::print.htmlwidget

function (x, ..., view = interactive())
{
viewer <- getOption("viewer")
if (!is.null(viewer)) {
viewerFunc <- function(url) {
paneHeight <- x$sizingPolicy$viewer$paneHeight
if (identical(paneHeight, "maximize"))
paneHeight <- -1
viewer(url, height = paneHeight)
}
}
else {
viewerFunc <- utils::browseURL
}
html_print(htmltools::as.tags(x, standalone = TRUE), viewer = if (view)
viewerFunc)
invisible(x)
}
<environment: namespace:htmlwidgets>

この最後のほうで,やはりhtml_print()によってviewerへ送られています1。というわけで,これで解決ですね。アイデア次第でいろんなことができそうですね。


RStudioのViewerについて

ここから先は暇な人だけ読んでください

さて,上述のようにhtmltools::html_print()でViewerにhtmlコンテンツを送り出すことができるのですが,一体どうやっているのでしょうか。


htmltools::html_printの中身について

この関数のコードはGithubにあり,以下のようになっています:

html_print <- function(html, background = "white", viewer = getOption("viewer", utils::browseURL)) {

# define temporary directory for output
www_dir <- tempfile("viewhtml")
dir.create(www_dir)

# define output file
index_html <- file.path(www_dir, "index.html")

# save file
save_html(html, file = index_html, background = background, libdir = "lib")

# show it
if (!is.null(viewer))
viewer(index_html)

invisible(index_html)
}

やっていることを書き出すと,以下のとおりです:


  • output用に一時ディレクトリを準備

  • 一時ファイル(***index.html)を準備

  • 引数htmlで受け取ったhtmlコンテンツをhtmlファイルとして一時ファイルに保存

  • viewerで表示させる

この通り一時ファイルに書き出してから,それをviewerで表示させようとしています。実は,Viewerは一時ディレクトリの一時ファイルしか表示しません2。よくよく考えると,たしかにそういう仕様の方が通常利用として適切かなという気はします。

Viewerで示すために,viewerという関数で一時ファイルへのpathを引き渡しているのですが,このviewerという関数は引数viewerで指定した関数で,デフォルトでviewer = getOption("viewer", utils::browseURL)と指定されています。この中身は実行環境によって変化しますが,私の手元のマシン(Ubuntu16.04, RStudio Desktop)では以下のようになっています:

> getOption("viewer", utils::browseURL)

function (url, height = NULL)
{
if (!is.character(url) || (length(url) != 1))
stop("url must be a single element character vector.",
call. = FALSE)
if (identical(height, "maximize"))
height <- -1
if (!is.null(height) && (!is.numeric(height) || (length(height) !=
1)))
stop("height must be a single element numeric vector or 'maximize'.",
call. = FALSE)
invisible(.Call("rs_viewer", url, height))
}
<environment: 0x340ecf8>

私の環境ではoptions("viewer")がNULLではなかったので,その中にあった関数が呼び出されています。実質呼び出しをしているのはinvisible(.Call("rs_viewer", url, height))です。これはRStudioのAPIを呼び出してurlを送ってます。この中身はGitHubの以下にあります:

https://github.com/rstudio/rstudio/blob/master/src/cpp/session/modules/viewer/SessionViewer.cpp#L271-L351

長いですし自分ではcpp全くわからんおじさんなのでコメントと雰囲気で読みましたが,nav igateViewerへと送ってイベントを発火させて表示させるようにしています。なおここのコード内にtemp dirでないとダメな処理が書いてありますので探してみてください。


結局Viewerとは何なのか?

つまりは,上記の処理を掻い潜って到達した一時ファイルのpath(url)にあるhtmlファイルを表示するRStudio組み込みブラウザです。RStudioのUIはQtをベースに作成されていています。なのでこのブラウザもRStudio内に組み込まれているQtに依存します。

なお,この記事の最初の方にtubeplayRの実行可能環境として「RStudioServer版,そしてMac向けデスクトップ版」と紹介しましたが,それもまたここが影響しています。Windows/Linux向けデスクトップ版では,このQtのバージョンが古かったりQtブラウザプラグインの問題で,YouTube埋め込み動画が再生できないのです3。そのため,tubeplayR::tubeplay()を実行してもうまく動いてくれないのです。私のメインPCがUbuntuであり,またWindowsユーザーも周りに多いため辛いところです。

ただ,最近この辺りについて色々手を加えているようです:

upgrade to Qt 5.9.2 + Qt WebEngine by kevinushey · Pull Request #1720 · rstudio/rstudio

Qtをアップグレードするのに加え,Qt WebkitからQt WebEngineに切り替えるようです。個人的にすごく気になっているので,ちょいちょいチェックしてます。これで変わってくれるといいなぁ…。


さいごに

誰が読むんだこの記事。

Enjoy!





  1. というかtubeplayRはここからViewerに送る着想を得ました。同じhtmlコンテンツだしこの辺でいけるはずと考えて。 



  2. あとローカルの(Shinyなどの)Webアプリケーションを動かせます。この辺りについては,このサポートページを参照してください。 



  3. なぜMac向けデスクトップは大丈夫なのかについてはなんとなく掴んでいるのですが,私自身がMacを使わなくなったのであまり検証してないです。すみません。