Vue アプリに動画再生機能を追加する方法
VueアプリにHTML5 video、再利用可能なコンポーネント、またはVideo.jsで動画再生を追加。HLS、字幕、autoplay、後片付けまで解説。
Vue アプリに動画再生機能を追加する方法は 3 つあり、どれを選ぶべきかは必要な制御の度合いによって決まります。シンプルな埋め込みクリップならネイティブの HTML5 <video> 要素、アダプティブストリーミングや字幕、幅広いフォーマット対応が必要なら Video.js、そしてプレイヤーを自分で組み立てずに Video.js の機能を使いたいなら @videojs-player/vue のような既製コンポーネントを導入する、という具合です。
動画は、開発したマシン上ではたいてい問題なく再生されます。ところが、テスターが iPhone でページを開くと全画面表示に切り替わってしまったり、Chrome で自動再生が何も言わずに動作せず、誰もその理由が分からなかったりします。そこで本ガイドでは、コピー&ペーストで使える Vue 3 の <script setup> コードとともに 3 つのアプローチをすべて解説し、他人のデバイスでしか表面化しない不具合についても取り上げます。
要点
- デフォルトのコントロールを備えたシンプルな埋め込みクリップであれば、
:src、controls、posterをバインドしたネイティブの HTML5<video>要素だけで十分で、ライブラリは一切不要です。 - Vue 3 の
<script setup>では、テンプレートref()で要素を取得し、onMounted内でvideoRef.valueに対してネイティブのメディア API を呼び出します。document.querySelectorは決して使いません。 - Video.js は
video.js/dist/video-js.cssをインポートしないとスタイルが当たらない状態でレンダリングされます。これは Vue 上の Video.js プレイヤーが壊れて見える最大の原因です。 - Video.js を初期化したら、必ず
onBeforeUnmount(Vue 3)でplayer.dispose()を呼び出してください。これを怠ると、アンマウントのたびにプレイヤーとその DOM がリークします。 - ブラウザは音声付きの自動再生をブロックします。確実に自動再生させるには
mutedとautoplayの両方を指定し、さらに iOS Safari が全画面に切り替えずインライン再生するようplaysinlineを追加する必要があります。
Vue の動画プレイヤーのアプローチはどう選ぶか?
カスタム UI もアダプティブストリーミングも不要ならネイティブ <video>、独自のコントロールを作り込みつつ完全に自分で管理したいならカスタムラッパー、HLS や字幕、多様なフォーマットが必要なら Video.js またはそのコンポーネントラッパーを選びます。判断基準は次の 3 点です。カスタム UI が必要か、アダプティブストリーミング(HLS/DASH)が必要か、そして依存関係をどこまで許容できるか。
| アプローチ | 追加依存 | カスタム UI の手間 | HLS / 字幕 | 選ぶべき状況 |
|---|---|---|---|---|
ネイティブ <video> | なし | すべてのコントロールを自作 | ネイティブ HLS のみ(Safari) | シンプルなクリップ、バンドル最小化 |
| カスタムラッパーコンポーネント | なし | スロットで完全に制御 | ネイティブ HLS のみ | 独自 UI、アプリ全体で再利用 |
Video.js / @videojs-player/vue | video.js(+ ラッパー) | スキン適用またはオーバーライド | あり(標準搭載) | フォーマットの幅、ストリーミング、トラック |
Discover how at OpenReplay.com.
Vue でのネイティブ HTML5 <video>(まずはここから)
Vue に動画再生機能を追加する最速の方法は、:src、poster、preload でプロパティをバインドしたネイティブの <video> 要素に、HTMLMediaElement API の play()、pause()、.muted を呼び出すためのテンプレート ref() を組み合わせることです。Vue 3 の <script setup> では、timeupdate、loadedmetadata、ended といったネイティブのメディアイベントをテンプレート内で直接バインドすれば、Vue がリスナーの登録と解除を代わりに行ってくれます。
<script setup>
import { ref } from 'vue'
const videoRef = ref(null)
const playing = ref(false)
const muted = ref(false)
const current = ref(0)
const duration = ref(0)
function togglePlay() {
const el = videoRef.value
el.paused ? el.play() : el.pause()
}
function toggleMute() {
const el = videoRef.value
el.muted = !el.muted
muted.value = el.muted
}
</script>
<template>
<video
ref="videoRef"
src="/media/clip.mp4"
poster="/media/poster.jpg"
preload="metadata"
playsinline
@play="playing = true"
@pause="playing = false"
@ended="playing = false"
@loadedmetadata="duration = $event.target.duration"
@timeupdate="current = $event.target.currentTime"
/>
<div>
<button @click="togglePlay">{{ playing ? 'Pause' : 'Play' }}</button>
<button @click="toggleMute">{{ muted ? 'Unmute' : 'Mute' }}</button>
<span>{{ current.toFixed(0) }}s / {{ duration.toFixed(0) }}s</span>
</div>
</template>
テンプレートの ref="videoRef" は videoRef.value 上で DOM 要素として解決されます。アクセスできるのはハンドラ内または onMounted 内であり、コンポーネントがマウントされる前には利用できません。Options API では、それぞれ this.$refs.videoRef と mounted フックが対応します。addEventListener で手動でリスナーを登録した場合は、onUnmounted で解除してください。テンプレート内で @event を使ってバインドすれば、この種のリークは完全に回避できます。
再利用可能なカスタムプレイヤーコンポーネント
アプリ全体でプレイヤーのロジックを再利用するには、ネイティブの <video> をコンポーネントでラップし、そのコントロールと状態をスコープ付きスロット経由で公開します。こうすることで、使用箇所ごとに再生処理の配線を重複させることなく、独自のボタンやシークバーを構成できます。
<!-- VideoPlayer.vue -->
<script setup>
import { ref } from 'vue'
defineProps({ src: String, poster: String })
const emit = defineEmits(['timeupdate', 'ended'])
const videoRef = ref(null)
const playing = ref(false)
function togglePlay() {
const el = videoRef.value
el.paused ? el.play() : el.pause()
}
</script>
<template>
<video
ref="videoRef"
:src="src"
:poster="poster"
playsinline
@play="playing = true"
@pause="playing = false"
@timeupdate="emit('timeupdate', $event.target.currentTime)"
@ended="emit('ended')"
/>
<slot name="controls" :playing="playing" :toggle-play="togglePlay" />
</template>
利用側はスロットプロパティから playing と togglePlay を取り出し、必要な UI を自由にレンダリングします。1 つのベースプレイヤーで、再生ボタンだけの最小限の埋め込みと、プログレスバー付きのフルプレイヤーの両方をまかなえ、メディアロジックは 1 か所に集約されたままになります。
<VideoPlayer src="/media/clip.mp4" @ended="onEnded">
<template #controls="{ playing, togglePlay }">
<button @click="togglePlay">{{ playing ? 'Pause' : 'Play' }}</button>
</template>
</VideoPlayer>
Video.js はどんなときに使うべきか?
HLS/アダプティブストリーミング、字幕やテキストトラック、統一されたスキン、あるいはネイティブ <video> が保証する範囲を超えたフォーマット対応が必要なときは Video.js を使います。video.js をインストールし、<video class="video-js"> をレンダリングして onMounted 内で videojs(ref, options) を初期化します。加えて、スタイルシートをインポートし、アンマウント時にプレイヤーを dispose してください。
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import videojs from 'video.js'
import 'video.js/dist/video-js.css' // required, or the player renders unstyled
const videoRef = ref(null)
let player = null
const options = {
autoplay: false,
controls: true,
preload: 'auto',
fluid: true,
sources: [{ src: '/media/clip.mp4', type: 'video/mp4' }]
}
onMounted(() => {
player = videojs(videoRef.value, options)
})
onBeforeUnmount(() => {
if (player) player.dispose()
})
</script>
<template>
<video ref="videoRef" class="video-js" playsinline />
</template>
ここで間違えやすい点が 2 つあります。1 つは、テンプレートの ref 名が onMounted 内で読み取る名前と一致していなければならないこと(videoJsPlayer と videoPlayer のように食い違うとエラーになります)。もう 1 つは、Vue 3 では dispose() を onBeforeUnmount に置くことです。beforeDestroy は Vue 2 のフックであり、Vue 2 は 2023 年 12 月 31 日にサポート終了を迎えています。現在の安定版系列は Video.js 8.x で、8.24.0 は 2026 年 8 月にリリースされました。モジュラー構成の Video.js 10 はベータ段階で、まだ一般提供されていません。v10 リポジトリのタイムラインでは一般提供は依然として作業中とされており、Video.js コアと contrib の機能パリティは 2026 年末を目標としています。本番環境では 8.x に留まり、インストール時はバージョンを固定しないでおきましょう: npm install video.js。
既製コンポーネント: @videojs-player/vue
Vue 3 で Video.js を使う最もコード量の少ない方法が @videojs-player/vue です。これはそのまま組み込める <video-player> コンポーネントで、リアクティブなプロパティ(src、sources、poster、controls、loop、volume、fluid、playsinline、tracks)と、カスタムコントロール用の { player, state } スコープ付きスロットを備えています。初期化と後始末は内部で処理されるため、HLS クリップへの切り替えはソースを 1 行変えるだけで済みます。
<script setup>
import { VideoPlayer } from '@videojs-player/vue'
import 'video.js/dist/video-js.css'
</script>
<template>
<video-player
src="https://example.com/stream/playlist.m3u8"
poster="/media/poster.jpg"
controls
fluid
playsinline
:volume="0.6"
@ready="(payload) => console.log(payload.state)"
>
<template #default="{ player, state }">
<button @click="state.playing ? player.pause() : player.play()">
{{ state.playing ? 'Pause' : 'Play' }}
</button>
</template>
</video-player>
</template>
採用前に知っておくべき注意点が 1 つあります。これは Vue 3 向けの標準的なラッパーではあるものの、npm でのリリースは 2022 年公開の v1.0.0 で止まっており、依存関係スキャナーはメンテナンスが低調であるとフラグを立てます。公開されている peer dependency の範囲は依然として video.js 7.x を要求しているため、Video.js 8 と組み合わせるとインストール時に peer dependency の警告が出ます。Vue 2 を使っている場合は、リポジトリのレガシーセクションからリンクされている旧来の vue-video-player ビルドを利用してください。@videojs-player/vue の現行リリースは Vue 3 のみを対象としています。
Vue 特有の落とし穴チェックリスト
Vue における「動画が再生されない」バグの大半は、コードレビューは通るものの実機で失敗する、いくつかのプラットフォームルールに起因します。とりわけ自動再生と playsinline の不具合は、ユーザーが遭遇するまで見えません。これはまさに、セッションリプレイが実際のセッションから浮かび上がらせるタイプのデバイス依存の問題です。
- アンマウント時にクリーンアップする。 Video.js では
player.dispose()を呼び出し、手動で登録したaddEventListenerのハンドラはonUnmountedで解除します。そうしないとマウント/アンマウントのたびにリークします。 - CSS をインポートする。
import 'video.js/dist/video-js.css'を忘れるとプレイヤーにスタイルが当たりません。 - ミュート付き自動再生は必須。 ブラウザは音声付きの自動再生をブロックします。Chrome の自動再生ポリシーに従い、
mutedとautoplayの両方を指定してください。 - iOS には
playsinlineを。playsinline属性がないと、iPhone の iOS Safari はたいてい動画を全画面で開きます。iPad は挙動が異なります。 - DOM クエリではなくテンプレート ref を使う。 要素へのアクセスは
ref()経由で行い、document.querySelectorは決して使わないでください。
まずはネイティブから始めましょう。バインドした <video> なら、依存関係ゼロで数分のうちに再生機能を出荷できます。HLS、字幕、あるいはブラウザ間で統一された UI が必要になった時点で Video.js や @videojs-player/vue にステップアップし、後から追いかけるリークにならないよう、最初のコミットから dispose() によるクリーンアップを組み込んでおきましょう。
FAQ
Vue アプリで HLS(.m3u8)ストリームを再生するには?
ネイティブの HTML5 video が HLS を再生できるのは Safari だけです。そのため、クロスブラウザで HLS を扱うには Video.js か、そのラッパーである @videojs-player/vue を使います。これらは videojs-http-streaming エンジンを通じて HLS サポートを同梱しています。@videojs-player/vue では src に .m3u8 プレイリストの URL を指定すれば、コンポーネントが内部でストリーミングエンジンを処理します。素の Chrome や Firefox では、ネイティブの video に HLS のサポートは組み込まれていません。これが Video.js を採用する主な理由です。
Vue の動画プレイヤーで自動再生が動作しないのはなぜ?
ミュート状態での自動再生は一般に許可されていますが、音声付きの自動再生にはユーザーが事前にサイトを操作していることが必要です。また Chrome では、サイトの Media Engagement Index スコアが高い場合にも許可されます。Safari も独自の同様のポリシーを適用しています。確実に自動再生させるには、要素または Video.js のオプションで muted と autoplay の両方を設定しなければなりません。この不具合はマークアップではなくブラウザのポリシーとデバイスに依存するため、コードレビューでは見えず、実際のユーザーがページを読み込んだときにはじめて表面化します。
@videojs-player/vue と Video.js を直接使うことの違いは?
@videojs-player/vue は Video.js をラップした、そのまま組み込める Vue 3 コンポーネントで、初期化と後始末を内部で処理し、リアクティブなプロパティと player・state のスコープ付きスロットを公開します。一方、Video.js を直接使う場合は、自分で video 要素をレンダリングし、onMounted で videojs() を呼び出し、onBeforeUnmount で自ら dispose する必要があります。ラッパーの方がコード量は少なくて済みますが、npm でのリリースは 2022 年の v1.0.0 で止まっているため、Video.js を直接使う方がバージョンとライフサイクルをより細かく制御できます。
Vue 2 でも @videojs-player/vue を使える?
いいえ。@videojs-player/vue の現行リリースは Vue 3 のみを対象としています。このパッケージは React サポートの追加時に現在の名前になり、その切り替えは Vue ユーザーにとって破壊的変更でした。Vue 2 では、リポジトリのレガシーセクションから今もリンクされている旧来の vue-video-player 5.x ビルドが必要です。Vue 2 自体も 2023 年 12 月 31 日にサポート終了を迎えているため、新規の開発では Vue 3 を対象とすべきです。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k