LoginSignup
61
65

More than 5 years have passed since last update.

SPAで動画を使って大ヤケドした話

Posted at

tl;dr

気軽に removeChild するもんじゃない。

フレームワーク(Riot.js)は悪くない

概要

動画メインの小規模サイト(厳密にはSPAと言わないかも)

スペック

  • 動画は YouTube IFrame API と一部に video要素 を使用
  • フロントのフレームワークは Riot.js
  • URL制御はRiot.jsのルータを使う
  • バックエンドはSinatra(この記事には関係ない)

YouTube IFrame APIで起こった問題

iframeをDOMツリーから切り離すとYouTubeのエラー頻発

youtube.tag
<youtube>
<iframe if={ page == 'hoge' } id="player" src="~" />
<script>
routing (path) {
    this.page = path
}
riot.route(this.routing)

onReady () {
    this.player = new YT.Player('player', option)
}
window.onYouTubeIframeAPIReady = this.onReady
</script>
</youtube>

Riot.jsのテンプレートエンジンの便利機能のひとつ 条件属性 をつかって、ページを判定して表示の切り替える設計にしていた。
if属性はCSSのdisplay: none;ではなくremoveChildでDOMツリーからの削除を行う。
なのでページによってはiframeがDOMツリー上にない状態ができるのだが、紐付いているthis.playerYT.Playerインスタンス)の postMessage のやり取りに失敗しエラーを吐いて、その スコープ内の処理が完全に死ぬ
postMessage の使用なのかよくわかってないのだけど)try/catchで何故かエラーをキャッチできない。postMessageが行う通信自体をどうにか止める必要がある。

解決策: ちゃんとdestroyする

this.player<iframe>の関係を断ち切る唯一の方法は this.player.destroy()メソッドを呼び出すこと。
destroyメソッドは実行した時点で<iframe>を自動的にDOMツリーから削除する。また、<iframe>がDOMツリー上にない状態で実行すると、即座にエラーを吐いて死ぬので、<iframe>は自分で管理しないといけない。
つまり残念ながら <iframe>要素自体をRiotに任せるわけにはいかなく、愚直にバニラcreateElement して要素をつくり、適宜 appendChildでDOMに追加し、DOMから削除はthis.player.destroy()するという方法をとった。

結構色々試して念には念を入れたソースになってしまった。もしかしたら無駄な処理があるかもしれない。

youtube.tag
// this.iframe = document.createElement('iframe') とする

// DOMツリーから削除する場合
this.player.stopVideo() // 一応止める
var iframe = this.player.getIFrame()
if (iframe) { // 紐付いているiframeがあるかどうか念のため確認
    this.player.destroy()
}
iframe = document.getElementById('player')
if (iframe) {
    this.root.removeChild(iframe);
}
if (this.iframe && this.iframe.parentNode) {
    this.root.removeChild(this.iframe)
}
this.player = null
this.iframe = null

video要素で起こった問題

1. video要素をDOMツリーから切り離しても音が鳴ったまま

同じくRiotにまかせて <video if={ page === 'hoge' }> のような書き方をしていたんだけど、どうも再生中にページを切り替えても 音が消えない。デベロッパーツールからDOMツリーを参照したり、querySelectorで探しても、どう考えても<video>が存在しないのに動画の音声だけ流れて続けている。

解決策: ちゃんと pause する

最初はこの仕様に驚きを隠せなかったけど、まあ…そういう仕様なら仕方ない。やってないけどきっとAudioもそうなんだろう。覚えておこう。
そしてつまり やっぱり Riotには任せることができないので、createElementで生成したvideo要素をしっかり管理して、動画を停止させてからremoveChildするようにした。

x-video.tag
// this.video = document.createElement('video') とする

this.video.pause()
this.root.removeChild(this.video)
this.video.removeEventListener('xxx', this.onxxx)
this.video = null

2. 字幕が重複する

動画を他のソースに更新する際、video要素ならそのまま srcにパスを代入しなおせば動画が切り替わる。
しかし字幕を扱うtrack要素はそうはいかなかった。

x-video.tag
this.video.src = '/path/filename.mp4'
this.track.src = '/path/filename.vtt'

こうしてしまうと、前回代入した字幕がそのまま出るので、更新する度、二重・三重に字幕が出てしまう。

解決策: 都度track要素を生成する

x-video.tag
// 消す
if (this.track) {
    this.video.removeChild(this.track)
    this.track = null
}
// 新しく要素を生成する
this.track = document.createElement('track')
this.video.appendChild(this.track)

this.video.src = '/path/filename.mp4'
this.track.src = '/path/filename.vtt'

なにこれ面倒くさッ。でもこれが一番早い解決方法だった。

まとめ

JSのメモリ上に存在する要素のインスタンスと、DOMツリーの関係をいまいち理解していなかったのが最大の要因だったと今更思う。
removeChildしても要素はメモリーに残って仕事をし続けるので、きちんとした手順で仕事を終わらせてあげてから削除しないといけない。

今回はRiot.jsだったけど、それに限らずテンプレートエンジンや仮想DOMを採用しているフレームワークは、そのへんが意識されて removeChild(Riotの場合は一部innerHTML)はしていないと思うので、iframeとvideo、あとたぶんaudio要素は気をつけないといけないと思った。
それを 考慮したコンポーネント を見つけたり、開発したりすることも今後大切かなとも思う。

61
65
2

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
61
65