LoginSignup
0

More than 5 years have passed since last update.

YouTube IFrame Player APIをElmから利用する

Last updated at Posted at 2017-12-21

はじめに

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タグを設置します。

src/Main.elm
 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が動くようになりました。

src/index.js
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です。

src/index.js
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インスタンスを取得できるようになったのでした。

src/index.js
var ytPlayer;
window.onYouTubeIframeAPIReady = function () {
    ytPlayer = new YT.Player("hoge", {});
}

portからYouTube Playerを制御する

src/index.jsで取得したYouTube Playerインスタンスは、上記の変数ytPlayerに保持してもらえ、ElmのPortsでアクセスされる関数からも利用できるので、Elmからは全てのメソッドが利用できます。例えば、playVideoであれば

src/index.js
app.ports.playVideo.subscribe(function() {
    ytPlayer.playVideo();
});
YTPlayer.elm
port playVideo : () -> Cmd msg

とすることで、update関数でのAPI利用が可能になります。

YouTube Playerの情報を取得

今回の実装では、「単純にgetCurrentTime()の値を返す」という実装はしてないですが、仮にするとしたら、portとsubscriptionの組み合わせになるでしょう。

src/index.js
app.ports.getCurrentTime.subscribe(function() {
   app.ports.currentTime.send(ytPlayer.getCurrentTime());
});
src/YTPlayer.elm
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側にメッセージを投げる、とします。

src/index.js
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);
}
src/YTPlayer.elm
--- 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
src/Main.elm
main : Program String Model Msg
main =
    Html.programWithFlags
        { view = view
        , init = init
        , update = update
        , subscriptions = \_ -> Sub.map MsgYTPlayer YP.subscriptions
        }

リポジトリ

今回作成した百人一首の動作制御アプリをGithubにあげていますので、より詳細については、コードも参考にしてください

https://github.com/cyclone-t/karuta-elm

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0