はじめに
Qiitaデビューということで、まずはライトめ(?)なネタを書いてみることにしました。
もうすぐお正月ですが、お正月といえば百人一首。昔は誰かが読み手をしないといけなかったのですが、今はYouTubeに読み上げの動画があったりします。便利な世の中になったものです。
ところが、この動画は手軽でいいのですが、いざこれでプレイしようとすると「1プレイ毎に止めないといけない」「毎回同じ順番になってしまう」というのが困ったところです。
YouTubeからは、Javascriptで制御できる「YouTube Iframe API」があったりしまして、この程度の軽い話なら、生JavaScriptで作っても作れちゃいますが、Elmでやってみたくなったのでした。
YouTube Iframe APIとは
iframe内で動作する埋め込みYouTubeをJavaScriptで制御できるAPIです。公式サイトはこちら。使い方をざっくり書くと、こんなあたりでしょうか
- HTMLに
<div id="hoge">
を配置し、JavaScriptでidやビデオIDを引数にセットして、インスタンスを作成 - HTMLに
<iframe id="hoge" src="https://www.youtube.com/embed/video_id?enablejsapi=1">
を配置し、JavaScriptでidを引数にセットしてインスタンスを作成
divを配置するやり方でも、Javascript側でdivがiframeに置き換えられるそうなので、結局どの方法でも「iframe内のYouTubeプレーヤーを操作する」となります。
前提&アプローチ
今回の記事は、下記の前提とします
- craete-elm-appで作成したプロジェクトをベースに実装する。create-elm-appバージョンは1.10.0
- Elmはバージョン0.18
- Youtube IFrame APIは2017年12月現在の仕様に基づく
今回は、src/index.jsをいろいろといじることになるわけですが、「Elmでもindex.jsでもどちらでもできる場合は、なるべくElm側に実装する」とします。Elmにした方が型チェックの恩恵がありますからね。
YouTube Iframe APIを利用するインターフェース部分の実装は、JavaScript側はsrc/index.jsに追加、Elm側は新規にsrc/YTPlayer.elmを作成して、そこに載せます。
ElmからYouTube APIを利用する(失敗編)
ElmでYouTube Iframe APIを使ってみるべく、まずはiframeタグを設置します。
iframe [id "hoge"
, src "https://www.youtube.com/embed/video_id?enablejsapi=1"
, width 560
, height 315
] []
次に、src/index.jsに公式サイトのコードにあるようなYouTube Playerインスタンスを作ってみるものの、制御がうまくいきません。いろいろ調べてみると、このAPIはIframe間でのPostMessageを利用しているようです。
そこで、ElmのPortsを通して、このPostMessageを投げてみると、playVideoやstopVideoが動くようになりました。
app.ports.playVideo.subscribe(function(args) {
var target_id = args[0];
var player = document.getElementById(target_id).contentWindow;
var reqbody = JSON.stringify({event: "command", func: "playVideo", args: {}});
player.postMessage(reqbody, '*');
});
今回やりたいことは「動画を所定のタイミングから所定の時間だけ再生する」なのですが、今のYouTube Iframe APIでは、再生開始時刻は設定できますが、再生終了時刻は指定できません。ですので、「所定の時間だけ再生する(=所定の時刻で再生を止める)」ためには、再生中の時刻を取得できる必要があります。
ところが、このPostMessageを直接使う方法だと、APIで紹介されているgetCurrentTime()にあるような、再生時刻を取得することができませんでした。APIの機能を使い切るには、公式サイトに書いてあるスタイルをElmに適用しないといけないようです。
ElmでYouTube Playerインスタンスを生成する
ElmでYouTube Playerインスタンスを生成するためには、最初に、YouTube Iframe APIのモジュールをロードします。これはsrc/index.jsに記述してOKです。
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
さて、公式サイトにあるコードのうち、一番ハードルが高かったのが、function onYouTubeIframeAPIReady()
を定義しておいて、これをAPIモジュールから呼んでもらう、というところでした。この部分のコードをsrc/index.jsに直接書いても、APIからは認識してもらえません。原因は、「src/index.jsにfunction onYouTubeIframeAPIReady()
と実装しても、それはグローバル(正確にはwindow配下)の名前空間ではない」というところでした。
ElmもJavaScriptもまだまだ不慣れなのでいろいろと試行錯誤をした結果、公式サイトのこの部分を以下のようにアレンジすることで、ようやくYouTube Playerインスタンスを取得できるようになったのでした。
var ytPlayer;
window.onYouTubeIframeAPIReady = function () {
ytPlayer = new YT.Player("hoge", {});
}
portからYouTube Playerを制御する
src/index.jsで取得したYouTube Playerインスタンスは、上記の変数ytPlayerに保持してもらえ、ElmのPortsでアクセスされる関数からも利用できるので、Elmからは全てのメソッドが利用できます。例えば、playVideoであれば
app.ports.playVideo.subscribe(function() {
ytPlayer.playVideo();
});
port playVideo : () -> Cmd msg
とすることで、update関数でのAPI利用が可能になります。
YouTube Playerの情報を取得
今回の実装では、「単純にgetCurrentTime()の値を返す」という実装はしてないですが、仮にするとしたら、portとsubscriptionの組み合わせになるでしょう。
app.ports.getCurrentTime.subscribe(function() {
app.ports.currentTime.send(ytPlayer.getCurrentTime());
});
type Msg = CurrentTime Float
port getCurrentTime : () -> Cmd msg -- 取得用コマンド
port currentTime : (Float -> msg) -> Sub msg -- 戻り値用サブスクリプション
subscriptions : Sub Msg
subscriptions = currentTime CurrentTime
としておいて、Main側でこのsubscriptionを登録する、というような流れになります。
あるいは、YouTube Player側のイベント(API ready, Player readyなど)を捕捉したい場合には、それもsubscription経由での捕捉になります。そのためには、YouTube Playerインスタンス生成時に、イベントのコールバックを定義してやって、そこでsubscription側にメッセージを投げる、とします。
window.onYouTubeIframeAPIReady = function () {
ytPlayer = new YT.Player(playerId, {
events: {
'onReady' : onPlayerReady
, 'onStateChange': onPlayerStateChange
}
});
app.ports.apiReady.send(null);
}
function onPlayerReady(event) {
app.ports.playerReady.send(null);
}
function onPlayerStateChange(event) {
app.ports.playerStateChange.send(event.data);
}
--- Msg defs ---
type Msg
= ApiReady
| PlayerReady
| PlayerStateChange PlayerState
type PlayerState
= PlayerStateUnstarted
| PlayerStateEnded
| PlayerStatePlaying
| PlayerStatePaused
| PlayerStateBuffering
| PlayerStateCued
| PlayerStateUnknown Int
--- Sub msg ports ---
port apiReady : (() -> msg) -> Sub msg
port playerReady : (() -> msg) -> Sub msg
port playerStateChange : (Int -> msg) -> Sub msg
subscriptions : Sub Msg
subscriptions = Sub.batch
[ apiReady (\_ -> ApiReady)
, playerReady (\_ -> PlayerReady)
, playerStateChange (PlayerStateChange << playerStateDecode)
]
--- decoder --
playerStateDecode : Int -> PlayerState
playerStateDecode n = case n of
-1 -> PlayerStateUnstarted
0 -> PlayerStateEnded
1 -> PlayerStatePlaying
2 -> PlayerStatePaused
3 -> PlayerStateBuffering
5 -> PlayerStateCued
others -> PlayerStateUnknown others
main : Program String Model Msg
main =
Html.programWithFlags
{ view = view
, init = init
, update = update
, subscriptions = \_ -> Sub.map MsgYTPlayer YP.subscriptions
}
リポジトリ
今回作成した百人一首の動作制御アプリをGithubにあげていますので、より詳細については、コードも参考にしてください