12k
All articles

Vue アプリに動画再生機能を追加する方法

VueアプリにHTML5 video、再利用可能なコンポーネント、またはVideo.jsで動画再生を追加。HLS、字幕、autoplay、後片付けまで解説。

OpenReplay Team
OpenReplay Team
Vue アプリに動画再生機能を追加する方法

Vue アプリに動画再生機能を追加する方法は 3 つあり、どれを選ぶべきかは必要な制御の度合いによって決まります。シンプルな埋め込みクリップならネイティブの HTML5 <video> 要素、アダプティブストリーミングや字幕、幅広いフォーマット対応が必要なら Video.js、そしてプレイヤーを自分で組み立てずに Video.js の機能を使いたいなら @videojs-player/vue のような既製コンポーネントを導入する、という具合です。

動画は、開発したマシン上ではたいてい問題なく再生されます。ところが、テスターが iPhone でページを開くと全画面表示に切り替わってしまったり、Chrome で自動再生が何も言わずに動作せず、誰もその理由が分からなかったりします。そこで本ガイドでは、コピー&ペーストで使える Vue 3 の <script setup> コードとともに 3 つのアプローチをすべて解説し、他人のデバイスでしか表面化しない不具合についても取り上げます。

要点

  • デフォルトのコントロールを備えたシンプルな埋め込みクリップであれば、:srccontrolsposter をバインドしたネイティブの 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 がリークします。
  • ブラウザは音声付きの自動再生をブロックします。確実に自動再生させるには mutedautoplay の両方を指定し、さらに iOS Safari が全画面に切り替えずインライン再生するよう playsinline を追加する必要があります。

Vue の動画プレイヤーのアプローチはどう選ぶか?

カスタム UI もアダプティブストリーミングも不要ならネイティブ <video>、独自のコントロールを作り込みつつ完全に自分で管理したいならカスタムラッパー、HLS や字幕、多様なフォーマットが必要なら Video.js またはそのコンポーネントラッパーを選びます。判断基準は次の 3 点です。カスタム UI が必要か、アダプティブストリーミング(HLS/DASH)が必要か、そして依存関係をどこまで許容できるか。

アプローチ追加依存カスタム UI の手間HLS / 字幕選ぶべき状況
ネイティブ <video>なしすべてのコントロールを自作ネイティブ HLS のみ(Safari)シンプルなクリップ、バンドル最小化
カスタムラッパーコンポーネントなしスロットで完全に制御ネイティブ HLS のみ独自 UI、アプリ全体で再利用
Video.js / @videojs-player/vuevideo.js(+ ラッパー)スキン適用またはオーバーライドあり(標準搭載)フォーマットの幅、ストリーミング、トラック

Vue でのネイティブ HTML5 <video>(まずはここから)

Vue に動画再生機能を追加する最速の方法は、:srcposterpreload でプロパティをバインドしたネイティブの <video> 要素に、HTMLMediaElement APIplay()pause().muted を呼び出すためのテンプレート ref() を組み合わせることです。Vue 3 の <script setup> では、timeupdateloadedmetadataended といったネイティブのメディアイベントをテンプレート内で直接バインドすれば、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.videoRefmounted フックが対応します。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>

利用側はスロットプロパティから playingtogglePlay を取り出し、必要な 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 内で読み取る名前と一致していなければならないこと(videoJsPlayervideoPlayer のように食い違うとエラーになります)。もう 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> コンポーネントで、リアクティブなプロパティ(srcsourcespostercontrolsloopvolumefluidplaysinlinetracks)と、カスタムコントロール用の { 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 の自動再生ポリシーに従い、mutedautoplay の両方を指定してください。
  • 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 を対象とすべきです。

DevTools for the frontend

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

We use cookies to improve your experience. By using our site, you accept cookies.