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

これは，[RStudio Advent Calendar 2017](https://qiita.com/advent-calendar/2017/rstudio) 14日目の記事です。

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

## tubeplayRパッケージ

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

![93845287.png](https://qiita-image-store.s3.amazonaws.com/0/35118/8d9e5978-ad34-f01e-1dc6-b0b16a736be5.png)

現在CRANには登録しておらず，[GitHubのリポジトリ](https://github.com/kazutan/tubeplayR)で公開しています。また，このパッケージの使い方については，以下のスライドを参照してください:

[YouTube on RStudio - Hijiyama.R #6](https://kazutan.github.io/HijiyamaR6/intro_tubeplayR.html#/)

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

- YouTube上に公開されているR関連のWebinerを参照しながらできる
- 大好きな動画を楽しみつつ作業できる

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

## tubeplayRの仕組み

実際のコードは[GitHub上のコード](https://github.com/kazutan/tubeplayR/blob/master/R/tubeplay.R)を参照してください。ざっくり言うと，以下の処理を行っています:

- 受け取ったurlから動画(もしくはプレイリスト)IDを取り出す
- 表示するhtmlコンテンツを生成
    - `miniUI::miniPage()`でダイアログボックス向けhtmlページを生成
        - `htmltools`パッケージを駆使してstyleを作成
        - 動画IDを利用して作成した埋め込み用iframeコンテンツを作成
- 作成したhtmlコンテンツを`htmltools::html_print()`でviewerへ送る

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

### htmlコンテンツ生成

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

```r
# 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`オブジェクトです。これについては[既に別途記事を書いている](https://qiita.com/kazutan/items/897a5f78846ebe185485)のでそちらを参照してください。

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

### RStudioのViewerに表示させる

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

```r
htmltools::html_print(ui)
```

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

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

```r
> 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]。というわけで，これで解決ですね。アイデア次第でいろんなことができそうですね。

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

## RStudioのViewerについて

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

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

### htmltools::html_printの中身について

この関数のコードは[Githubにあり](https://github.com/rstudio/htmltools/blob/master/R/html_print.R#L41-L58)，以下のようになっています:

```r
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]。よくよく考えると，たしかにそういう仕様の方が通常利用として適切かなという気はします。

[^2]: あとローカルの(Shinyなどの)Webアプリケーションを動かせます。この辺りについては，この[サポートページ](https://support.rstudio.com/hc/en-us/articles/202133558-Extending-RStudio-with-the-Viewer-Pane)を参照してください。

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

```r
> 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ユーザーも周りに多いため辛いところです。

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

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

[upgrade to Qt 5.9.2 + Qt WebEngine by kevinushey · Pull Request #1720 · rstudio/rstudio](https://github.com/rstudio/rstudio/pull/1720)

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

## さいごに

誰が読むんだこの記事。

**Enjoy!**