Vue Reel Player

@reelkit/vue-reel-player で作られた、Vue 3 用の Instagram/TikTok 風のフルスクリーン縦型メディアプレイヤーです。

ライブデモを見る →

機能

縦方向のスワイプ
タッチ、ドラッグ、キーボード、ホイール
動画の自動再生
表示されたら再生
サウンド切り替え
iOS でも途切れない
複数メディア
横方向にネストしたカルーセル
位置の記憶
止めたところから再開
フレームキャプチャー
ポスターから動画へのクロスフェード
仮想化
DOM にはスライド 3 枚だけ
アスペクト比
デスクトップは 9:16、モバイルは全画面
デスクトップの操作
矢印ボタン
ジェネリック型
独自のコンテンツのデータモデル
スコープ付きスロット
あらゆる UI 要素をカスタマイズ
v-model:is-open
表示の双方向バインディング
URL の状態
共有できるリンク、戻るボタンで閉じる

インストール

bash
npm install @reelkit/vue-reel-player @reelkit/vue lucide-vue-next

アプリのエントリー(または任意のコンポーネント)でスタイルシートを 1 回インポートします:

typescript
import '@reelkit/vue-reel-player/styles.css';
アイコン

デフォルトのコントロールはアイコン(閉じる、サウンド、ナビゲーションの矢印)に lucide-vue-next を使います。別のアイコンライブラリを使いたい場合は、スコープ付きスロットの #controls と #navigation で独自のものを渡してください。

Vue 3.3 以降

オーバーレイはジェネリックなコンポーネントで、Vue は 3.3 からこれをサポートしています。古い Vue ではランタイムが props とイベントを捨ててしまうため、このパッケージは peer dependency として vue 3.3 以降を必要とします。

クイックスタート

サムネイルのグリッドを描画し、クリックしたインデックスでオーバーレイを開きます。v-model:is-open をバインドすると、ユーザーがボタン、ジェスチャー、Escape でプレイヤーを閉じたときにも、親の ref が同期されます。

App.vue
<script setup lang="ts">
import { ref } from 'vue';
import { ReelPlayerOverlay, type ContentItem } from '@reelkit/vue-reel-player';
import '@reelkit/vue-reel-player/styles.css';

const content: ContentItem[] = [
  {
    id: '1',
    media: [{
      id: 'v1',
      type: 'video',
      src: '/cdn/samples/videos/video-01.mp4',
      poster: '/cdn/samples/videos/video-poster-01.jpg',
      aspectRatio: 16 / 9,
    }],
    author: { name: 'Alex Johnson', avatar: '/cdn/samples/avatars/avatar-01.jpg' },
    likes: 1234,
    description: 'Amazing sunset vibes',
  },
  {
    id: '2',
    media: [{
      id: 'img1',
      type: 'image',
      src: '/cdn/samples/images/image-01.jpg',
      aspectRatio: 2 / 3,
    }],
    author: { name: 'Sarah Miller', avatar: '/cdn/samples/avatars/avatar-02.jpg' },
    likes: 5678,
    description: 'Nature at its finest',
  },
  {
    id: '3',
    media: [{
      id: 'v2',
      type: 'video',
      src: '/cdn/samples/videos/video-02.mp4',
      poster: '/cdn/samples/videos/video-poster-02.jpg',
      aspectRatio: 16 / 9,
    }],
    author: { name: 'Mike Chen', avatar: '/cdn/samples/avatars/avatar-03.jpg' },
    likes: 3456,
    description: 'Adventure awaits',
  },
];

const isOpen = ref(false);
const startIndex = ref(0);

function openAt(i: number) {
  startIndex.value = i;
  isOpen.value = true;
}
</script>

<template>
  <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px">
    <button
      v-for="(item, i) in content"
      :key="item.id"
      @click="openAt(i)"
      style="aspect-ratio:9/16;cursor:pointer;overflow:hidden;padding:0;border:0"
    >
      <img
        :src="item.media[0].poster || item.media[0].src"
        style="width:100%;height:100%;object-fit:cover"
      />
    </button>
  </div>

  <ReelPlayerOverlay
    v-model:is-open="isOpen"
    :content="content"
    :initial-index="startIndex"
  />
</template>

スコープ付きスロット

8 つのスコープ付きスロットで、プレイヤーの UI のどの部分でも置き換えられます。それぞれが厳密に型付けされたスコープのオブジェクトを受け取ります。渡さなかったスロットはデフォルトが使われます。

スロットスコープ説明
#controls{ item, soundState, activeIndex, content, onClose }独自のグローバルなコントロールバー(閉じる、サウンド、共有など)
#error{ item, activeIndex, innerActiveIndex }独自のエラー表示(デフォルトのアイコンを置き換え)
#loading{ item, activeIndex, innerActiveIndex }独自の読み込み表示(デフォルトの波のローダーを置き換え)
#navigation{ item, activeIndex, count, onPrev, onNext }独自の前後のナビゲーション矢印(デスクトップ)
#nestedNavigation{ media, activeIndex, count, onPrev, onNext }内側の横方向のスライダー用の独自の矢印
#nestedSlide{ item, media, index, size, isActive, isInnerActive, slideKey, defaultContent, onReady, onWaiting, onError }内側の横方向のスライダーの中の独自のスライドの内容
#slide{ item, index, size, isActive, slideKey, defaultContent, onReady, onWaiting, onError }完全に独自のスライドの内容(省略するとデフォルト)
#slideOverlay{ item, index, isActive }スライドごとのオーバーレイ(作者の情報、いいね、説明など)
#timeline{ item, activeIndex, timelineState, defaultContent }独自の再生タイムラインバー。組み込みの条件(タイムラインのモードと最小の長さ)がデフォルトのバーを描画する場合だけ呼ばれ、同じ auto/always/never の判定を使います。組み込みの <TimelineBar /> を包むには defaultContent() を使います。
vue
<script setup lang="ts">
import { ref } from 'vue';
import {
  ReelPlayerOverlay,
  CloseButton,
  SoundButton,
  type ContentItem,
  type SlideOverlaySlotScope,
  type ControlsSlotScope,
} from '@reelkit/vue-reel-player';

const isOpen = ref(false);
const content = ref<ContentItem[]>([/* ... */]);
</script>

<template>
  <ReelPlayerOverlay v-model:is-open="isOpen" :content="content">
    <!-- Custom per-slide overlay: branded caption -->
    <template #slideOverlay="{ item, isActive }: SlideOverlaySlotScope">
      <div v-if="isActive" style="position:absolute;bottom:80px;left:16px;color:#fff">
        <div style="display:flex;align-items:center;gap:8px">
          <img :src="item.author.avatar" style="width:40px;height:40px;border-radius:50%" />
          <span style="font-weight:600">{{ item.author.name }}</span>
        </div>
        <p style="margin-top:8px">{{ item.description }}</p>
      </div>
    </template>

    <!-- Custom global controls -->
    <template #controls="{ onClose }: ControlsSlotScope">
      <div style="position:absolute;top:16px;right:16px;display:flex;gap:8px">
        <SoundButton />
        <CloseButton :on-click="onClose" />
      </div>
    </template>

    <!-- Custom playback timeline -->
    <template #timeline="{ timelineState }: TimelineSlotScope">
      <CustomTimelineBar :state="timelineState" />
    </template>
  </ReelPlayerOverlay>
</template>

独自のタイムライン

#timeline スロットで、組み込みの再生バーを独自のシーク UI に置き換えます。スロットが呼ばれるのは、オーバーレイの表示条件がデフォルトのバーを描画する場合(同じ timeline モードと timelineMinDurationSeconds)だけなので、それを実装し直す必要はありません。ルート要素で .rk-reel-timeline クラスを使えば、下端に沿った配置、セーフエリアの余白、タッチ端末での間隔を引き継げます。

vue
<script setup lang="ts">
import { shallowRef, onMounted, onBeforeUnmount } from 'vue';
import {
  ReelPlayerOverlay,
  type TimelineSlotScope,
} from '@reelkit/vue-reel-player';
import { toVueRef, type TimelineController } from '@reelkit/vue';

const trackRef = shallowRef<HTMLDivElement | null>(null);
let dispose: (() => void) | null = null;
const bind = (state: TimelineController) => {
  if (trackRef.value) dispose = state.bindInteractions(trackRef.value);
};
onBeforeUnmount(() => dispose?.());
</script>

<template>
  <ReelPlayerOverlay :is-open="open" :content="items" timeline="always">
    <template #timeline="{ timelineState }: TimelineSlotScope">
      <div class="rk-reel-timeline" style="padding: 0 16px" @vue:mounted="bind(timelineState)">
        <div ref="trackRef" role="slider" style="height:6px;background:rgba(255,255,255,0.2)">
          <div :style="{
            width: (timelineState.progress.value * 100) + '%',
            height: '100%',
            background: 'linear-gradient(90deg, #6366f1, #ec4899)',
          }" />
        </div>
      </div>
    </template>
  </ReelPlayerOverlay>
</template>

独自のコンテンツ型

ReelPlayerOverlay と ReelPlayerUrlOverlay は、React のプレーヤーと同じく、コンテンツのアイテムの形についてジェネリックです。BaseContentItem を拡張すれば任意のデータモデルを使えます。アイテムの型は content から推論されるため、BaseContentItem に合わないフィードは型チェックで弾かれます。Vue ではスロットがこの型を運べないので、スロットのスコープは型を付けるまで型なしのままです。対応するスロットのスコープの型をインポートして分割代入に型を付ければ、テンプレートの中でも追加フィールドが型付けされたままになります。この型注釈は content と照合されないため、両者を揃えておいてください:

vue
<script setup lang="ts">
import { ref } from 'vue';
import {
  ReelPlayerOverlay,
  type BaseContentItem,
  type SlideOverlaySlotScope,
} from '@reelkit/vue-reel-player';

interface MyItem extends BaseContentItem {
  title: string;
  category: 'video' | 'photo';
}

const open = ref(false);
const items: MyItem[] = [/* ... */];
</script>

<template>
  <ReelPlayerOverlay v-model:is-open="open" :content="items">
    <template #slideOverlay="{ item }: SlideOverlaySlotScope<MyItem>">
      <div class="my-overlay">
        <h2>{{ item.title }}</h2>
        <span>{{ item.category }}</span>
      </div>
    </template>
  </ReelPlayerOverlay>
</template>

ほかのすべてのスロットでも同じパターンです。対応するスコープの型(SlideSlotScope、ControlsSlotScope、NavigationSlotScope、NestedSlideSlotScope、LoadingSlotScope)をインポートし、分割代入に型を付けてください。

URL の状態

ライブデモを見る →

@reelkit/vue の useOverlayUrlState でコントローラーを作り、ReelPlayerUrlOverlay に controller として渡します。プレイヤーを持つのはアドレスバーなので、パラメーターがスライドを指すと開き、パラメーターがなくなると閉じます。開くと履歴エントリーを 1 つ積み、スライドが変わるたびにそれを置き換えるので、フィードを送ってもエントリーは増えず、1 回戻れば必ず出られます。URL の深さはコントローラーのキーで決まります。一軸の urlIndexKey は投稿だけを指し(?reel=3)、二軸の urlIndexTwoAxisKey は複数メディアの投稿の内側のメディアのインデックスも運びます(?reel=3.2)。アプリごとにキーを 1 つ選んでください。2 つの通信形式は互いに読み替えられません。これは ReelPlayerOverlay とは別のコンポーネントなので、開閉状態を決めるものはそれぞれ 1 つだけです。is-open のモデルか、URL の controller のどちらかで、両方ではありません。

組み込みのキー

スライドは組み込みのキーで指せます。urlIndexKey(位置で指す)か urlStableIdKey(安定した id で指す)をコントローラーに展開してください。どちらも @reelkit/vue から再エクスポートされています。URL の状態のガイド と Core API を参照してください。

ルーターを使うアプリでは、ルーターに連動したアダプターを渡して、ナビゲーションの信頼できる情報源をルーターだけに保つべきです。ルーターの裏で履歴を書くと、その位置情報が古くなり、次のナビゲーションでパラメーターが消えます。@reelkit/vue/vue-router-url-adapter の useVueRouterUrlAdapter が、Vue Router 用に用意されたアダプターです。

vue
<script setup lang="ts">
import { ReelPlayerUrlOverlay, type ContentItem } from '@reelkit/vue-reel-player';
import { useOverlayUrlState, urlIndexKey, urlStableIdKey } from '@reelkit/vue';
import { useVueRouterUrlAdapter } from '@reelkit/vue/vue-router-url-adapter';
import '@reelkit/vue-reel-player/styles.css';

const props = defineProps<{ content: ContentItem[] }>();

const reel = useOverlayUrlState({
  param: 'reel',
  adapter: useVueRouterUrlAdapter(),
  ...urlIndexKey(() => props.content.length),
});
</script>

<template>
  <!-- Opening is a link — the overlay reads the URL and opens itself. -->
  <RouterLink v-for="(post, i) in props.content" :key="post.id" :to="`?reel=${i}`">
    <img :src="post.media[0].src" />
  </RouterLink>

  <ReelPlayerUrlOverlay :controller="reel" :content="props.content" />
</template>

useOverlayUrlState のすべてのオプションは Vue API リファレンス にあります。

  • 開くと履歴エントリーを 1 つ 積みます。フィードをスワイプするとそれを 置き換え るので、N 回スワイプしてもエントリーは増えず、1 回戻れば必ずプレイヤーから出られます。戻るとプレイヤーが閉じ、スライドは戻りません。
  • 戻るで閉じるのは、アプリ内から開いた場合だけです(リンクがエントリーを積んだ場合)。新しいタブで直接開いた共有リンクには前の履歴がないので、ブラウザーの戻るではサイトを離れます。✕ ボタンか Escape なら、その場でパラメーターを取り除いてページにとどまります。
  • ディープリンク ?reel=3 は、読み込み時にそのスライドでプレイヤーを開きます。
  • どのスライドも指さないパラメーター(古いブックマークや手で書き換えた値)は、開けないスライドをアドレスバーが指したままにならないよう、URL から取り除かれます。
  • URL の深さはコントローラーのキーで決まります。投稿だけなら一軸、複数メディアの投稿の内側の画像のインデックスも運ぶなら二軸(urlIndexTwoAxisKey)です。アプリごとにキーを 1 つ選んでください。形は互いに読み替えられません。

キーは 1 つか 2 つか — URL の深さを選ぶ

同じ ReelPlayerOverlay がどちらの形も扱います。実行時にコントローラーの位置から判別するので、モードを指定するプロップはありません。コントローラーを作るときにキーを選んでください:

キー通信形式運ぶもの
urlIndexKey(…)?reel=3縦方向の投稿だけ。
urlIndexTwoAxisKey(…)?reel=3.2投稿 と カルーセルの内側のメディアのインデックス。

2 つの通信形式は意図的に区別されています。二軸のキーは厳密にドットで区切る(3.0 で、3 だけにはしない)ので、一軸のリンクが二軸として読まれることはありません。そのため、アプリでキーを切り替えると、それまでに共有されたリンクは無効になります。形を 1 つ選んで使い続けてください。

typescript
import { useOverlayUrlState, urlIndexTwoAxisKey } from '@reelkit/vue';

const reel = useOverlayUrlState({
  param: 'reel',
  ...urlIndexTwoAxisKey({
    outerCount: () => content.value.length,
    innerCounts: () => content.value.map((post) => post.media.length),
  }),
});

// A link now names both axes: post 3, inner media 2 — ?reel=3.2

安定したリンク。 インデックスは位置なので、ブックマークした ?reel=3 はフィードが並べ替えられると別の投稿を開きます。フィードではそれが例外ではなく普通のことです。urlStableIdKey は各投稿の安定した id で指し、現在のフィードを走査します。よくあるケースは 1 回の呼び出しで済みます。

typescript
const reel = useOverlayUrlState({
  param: 'reel',
  ...urlStableIdKey({ items: () => content.value }),
});

hashCodec: base64UrlCodec を渡すと、URL の id を base64url でエンコードします。元に戻せる難読化で、暗号学的ハッシュではありません。

別のフィールド(slug)で指したい場合や、locateAsync で無限フィードをページングしたい場合は、codec/locator を自分で作ります。2 つは別の役割です。codec は識別子を URL に書き、locator はその識別子がどこにあるかを探します。

typescript
const reel = useOverlayUrlState({
  param: 'reel',
  codec: { decode: (raw) => raw, encode: (id) => id },
  locator: {
    locate: (id) => content.value.findIndex((x) => x.id === id),
    identify: (index) => content.value[index].id,
  },
});

無限フィード。 locate は同期なので、読み込み済みの投稿にしか答えられません。20 件を読み込んだフィードの 400 番目の投稿への共有リンクは見つかりません。locateAsync はフォールバックで、locate が見つけられなかったときだけ呼ばれます。

近道

アイテムの id で指すなら、codec と locator を手書きする必要はありません。locateAsync をそのまま urlStableIdKey({ items, locateAsync }) に渡してください(見つからなければ取得してからインデックスを返します)。下のより完全な例は、別のフィールドで指す場合や、すべてを自分で制御したい場合のものです。

typescript
const reel = useOverlayUrlState({
  param: 'reel',
  codec: { decode: (raw) => raw, encode: (id) => id },
  locator: {
    locate: (id) => content.value.findIndex((x) => x.id === id),
    identify: (index) => content.value[index].id,
    locateAsync: async (id) => {
      const loaded = await loadById(id); // or loadUntil(id) — fetch just that one, or page up to it
      if (!loaded) return null; // exhausted — link names no post
      content.value = loaded; // commit — the overlay renders from this state
      return loaded.findIndex((x) => x.id === id); // wherever it landed
    },
  },
});
  • locateAsync が保留中のあいだ、プレイヤーは閉じたままで、パラメーターはそのまま残るので、ディープリンクは取得のあいだも失われません。null か reject でパラメーターは取り除かれます。
  • URL が変わったあと、閉じたあと、アンマウントしたあとに届いた答えは破棄されます。遅い取得が、誰も求めていないスライドを開くことはありません。
  • 保留中は何も描画されません。その読み込み状態はすでにページが持っているので、独自のスケルトンを描画してください。
  • タイムアウトはありません。プレイヤーにはフィードの長さがわからないからです。ページングが尽きたら null で決着をつけてください。そうしないとオーバーレイは閉じたままになります。

API リファレンス

ReelPlayerOverlay の Props

ReelPlayerOverlayProps<T>

Prop型デフォルト説明
ariaLabelstring'Video player'ダイアログ領域のアクセシブルなラベル。オーバーレイが開くとスクリーンリーダーが読み上げます
aspectRationumber9 / 16デスクトップのコンテナーの幅と高さの比。幅 768px 以下ではプレイヤーが全画面を使います。0、負の値、有限でない値は 9/16 になります。
contentT[] (extends BaseContentItem)必須プレイヤーに表示するコンテンツアイテムの配列
enableNavKeysbooleantrueキーボードの矢印キーによる操作を有効にします
enableWheelbooleantrueマウスホイールでの操作を有効にします
initialIndexnumber0最初に表示するアイテムの 0 始まりのインデックス
initialInnerIndexnumber0最初に表示する投稿でだけ使う、内側のメディアのインデックス。二軸の URL で、複数メディアの投稿の特定の画像へ直接リンクできます。ユーザーが移動したあとは無視されます。
isOpenboolean必須オーバーレイの表示を制御します。false のときオーバーレイは DOM から取り除かれます
loopbooleanfalseスライド間の無限ループを有効にします
swipeDistanceFactornumber0.12スライドを切り替えるのに必要なスワイプ距離の最小の割合
timeline'auto' | 'always' | 'never''auto'組み込みの再生タイムラインバーの表示条件。'auto' は timelineMinDurationSeconds より長い動画でだけ表示し、'always' はアクティブなスライドに動画があれば常に表示し、'never' は組み込みのバーを無効にします(完全に置き換えるには #timeline スロットを使います)。
timelineMinDurationSecondsnumber30timeline='auto' で組み込みのバーを表示する動画の最小の長さ(秒)。これより短いループ動画では表示されません。
transitionDurationnumber300スライドのアニメーションの長さ(ms)
wheelDebounceMsnumber200ホイールイベントのデバウンス時間(ms)

ReelPlayerUrlOverlay の Props

ReelPlayerUrlOverlayProps<T>

上の props をすべて受け取りますが、is-open の代わりに controller を使います。initial-index は無視されます。スライドはコントローラーの位置で決まるので、一緒に渡した値は開くたびに上書きされます。

Prop型デフォルト説明
controllerUrlStateController必須useOverlayUrlState のコントローラー。その position で、オーバーレイが開いているかどうかと、どのスライドを表示するかが決まります。オーバーレイはスライドの切り替え時と閉じるときに、これを通じて書き戻します。

イベント

イベントペイロード説明
@api-readyReelPlayerApiスライダーの準備ができたときに発行され、命令的な API を公開します
@closevoidプレイヤーが閉じたときに発行されます
@slide-changenumber切り替え後の新しいアクティブなスライドのインデックスとともに発行されます
@inner-slide-changeouter: number, inner: numberアクティブな投稿の内側のメディアのインデックスが変わったときに発行されます。内側で移動したときと、外側でアクティブになったとき(アクティブになった投稿の現在の内側のインデックス、単一メディアなら 0)です。
@update:is-openboolean閉じるときに発行され、`v-model:is-open` を可能にします

v-model:is-open

v-model:is-open を使うと、1 つのバインディングでオーバーレイを操作できます。明示的なイベントが必要なら、従来の :is-open と @close の組み合わせもそのまま使えます。

vue
<template>
  <button @click="open = true">Open</button>
  <ReelPlayerOverlay v-model:is-open="open" :content="content" />
</template>

型

ContentItem

フィールド型説明
idstring一意の識別子
mediaMediaItem[]1 つ以上のメディア(画像または動画)
author{ name: string; avatar?: string }デフォルトのスライドオーバーレイに表示する作者
descriptionstring?キャプションのテキスト
likesnumber?いいねの件数

TimelineBarProps

typescript
interface TimelineBarProps {
  class?: string;
  style?: CSSProperties;
}

TimelineSlotScope<T>

typescript
interface TimelineSlotScope<T extends BaseContentItem> {
  item: T;
  activeIndex: number;
  timelineState: TimelineController;
  defaultContent: () => VNode | VNode[];
}

MediaItem

フィールド型説明
idstring一意の識別子
type'image' | 'video'メディアの種類
srcstringメディアの URL
posterstring?動画アイテムのポスターのサムネイルの URL
aspectRationumber幅と高さの比。< 1 は縦長(cover)、≥ 1 は横長(contain)。

ユニオン型

型値説明
MediaType'image' | 'video'MediaItem の種類。スライドが ImageSlide と VideoSlide のどちらを描画するかを決めます。
TimelineMode'auto' | 'always' | 'never'timeline プロップの値。'auto' は timelineMinDurationSeconds より長い動画でバーを表示し、'always' はすべての動画で表示し、'never' は隠します。

サブコンポーネント

独自の #controls、#slide、#slideOverlay のテンプレートに入れて使います。自動再生、ポスターのキャプチャー、サウンドの同期が動き続けるよう、寸法とコールバックはスロットのスコープからそのまま渡してください。

CloseButton

デフォルトのプレイヤーのスタイルを持つ、単独の円形の閉じるボタンです。#controls の中で使います。

vue
<script setup lang="ts">
import { CloseButton } from '@reelkit/vue-reel-player';
</script>

<template>
  <CloseButton :on-click="onClose" />
  <CloseButton :on-click="onClose" class-name="my-close-btn" :style="{ top: '24px', right: '24px' }" />
</template>

SoundButton

ミュートの切り替えボタンです。SoundProvider の中に描画してください(ReelPlayerOverlay が用意します)。アクティブなスライドに動画がないときは非表示です。

vue
<script setup lang="ts">
import { SoundButton } from '@reelkit/vue-reel-player';
</script>

<template>
  <SoundButton />
  <SoundButton disabled class-name="my-sound-btn" />
</template>

PlayerControls

#controls スロットがないときにオーバーレイが描画するデフォルトのコントロールです。CloseButton と、show-sound のときは SoundButton を表示します。Props は showSound、soundDisabled(サウンドボタンを薄くします)、onClose です。追加のボタンの横で組み込みの見た目を保つには、独自の #controls スロットの中に置きます。

LoadingIndicator と ErrorIndicator

LoadingIndicator はメディアのバッファー中に表示するスライド全体の波のローダー、ErrorIndicator はメディアの読み込みに失敗したときに表示するデフォルトのメッセージです。どちらも props を取りません。追加の中身の周りで組み込みの見た目を保つには、独自の #loading や #error スロットから描画します。

TimelineBar

デフォルトの再生シークバーです。いちばん近い TimelineProvider(ReelPlayerOverlay の中に自動でマウントされます)から読み取り、トラック、バッファー済みの範囲、進捗の塗り、シーク用のつまみを描画します。--rk-reel-timeline-* のカスタムプロパティでテーマを変えるか、#timeline スロットで置き換えます。

vue
<script setup lang="ts">
import type { TimelineSlotScope } from '@reelkit/vue-reel-player';
</script>

<template>
  <!-- Wrap or augment the default bar from #timeline: -->
  <ReelPlayerOverlay>
    <template #timeline="{ defaultContent }: TimelineSlotScope">
      <MyTimecode />
      <component :is="defaultContent" />
    </template>
  </ReelPlayerOverlay>
</template>

SlideOverlay

作者、説明、いいねを表示するデフォルトのグラデーションのオーバーレイです。コンテンツにそれらのフィールドがあると描画されます。置き換えるか非表示にするには #slideOverlay スロットを使います。

vue
<script setup lang="ts">
import { SlideOverlay } from '@reelkit/vue-reel-player';
</script>

<template>
  <SlideOverlay
    :author="{ name: 'John', avatar: '/avatar.jpg' }"
    description="Amazing content"
    :likes="12500"
  />
</template>

ImageSlide

遅延読み込みとデフォルトの object-fit: cover を備えた画像スライドです。#slide スロットの中で組み合わせれば、組み込みの挙動を保ったまま画像の描画をカスタマイズできます。

vue
<script setup lang="ts">
import { ImageSlide } from '@reelkit/vue-reel-player';
</script>

<template>
  <ImageSlide :src="media.src" :size="size" />

  <ImageSlide
    :src="media.src"
    :size="size"
    class-name="my-image-slide"
    :style="{ backgroundColor: '#1a1a1a', borderRadius: '12px' }"
    :img-style="{ objectFit: 'contain' }"
  />
</template>

VideoSlide

共有の <video> 要素に支えられた動画スライドです。iOS で音声を途切れさせない処理、ポスターのフレーム、位置の記憶を扱います。SoundProvider の中に描画してください(ReelPlayerOverlay が用意します)。

vue
<script setup lang="ts">
import { VideoSlide } from '@reelkit/vue-reel-player';
</script>

<template>
  <VideoSlide
    :src="media.src"
    :poster="media.poster"
    :aspect-ratio="9 / 16"
    :size="size"
    :is-active="isActive"
    :slide-key="slideKey"
    :style="{ borderRadius: '12px' }"
  />
</template>
独自のスライドを組み立てる

#slide と ImageSlide/VideoSlide を組み合わせれば、組み込みの挙動(自動再生、ポスターのキャプチャー、サウンドの同期)をすべて保ったまま、メディアの描画をカスタマイズできます。

vue
<script setup lang="ts">
import {
  ReelPlayerOverlay,
  ImageSlide,
  VideoSlide,
} from '@reelkit/vue-reel-player';
</script>

<template>
  <ReelPlayerOverlay v-model:is-open="isOpen" :content="content">
    <template #slide="{ item, size, isActive, slideKey }">
      <ImageSlide
        v-if="item.media[0].type === 'image'"
        :src="item.media[0].src"
        :size="size"
        :style="{ backgroundColor: '#111' }"
        :img-style="{ objectFit: 'contain' }"
      />
      <VideoSlide
        v-else
        :src="item.media[0].src"
        :poster="item.media[0].poster"
        :aspect-ratio="item.media[0].aspectRatio"
        :size="size"
        :is-active="isActive"
        :slide-key="slideKey"
        :style="{ borderRadius: '16px' }"
      />
    </template>
  </ReelPlayerOverlay>
</template>

コンテンツの読み込みとエラー処理

プレイヤーはスライドごとの読み込み中とエラーの状態を追跡します。読み込み中は波のローダーを表示し、壊れたメディアにはエラーアイコンを表示します。失敗した URL はキャッシュされるので、壊れたスライドを開き直しても再試行はしません。

ライフサイクルのコールバック

#slide スロットを使う場合は、スロットのスコープにあるこれらのコールバックを呼んで読み込み表示を制御します:

コールバック呼ぶタイミング
onReady画像が読み込まれたか、動画の再生が始まったとき。読み込み中とエラーの状態をクリアします。
onWaiting再生の途中で動画がバッファリングしているとき。読み込み表示を出します。
onErrorコンテンツの読み込みに失敗したとき。エラーのオーバーレイを出し、その URL を壊れたものとしてキャッシュします。
vue
<!-- Inside #slide — wire callbacks to your custom media -->
<template #slide="{ item, size, isActive, onReady, onWaiting, onError }">
  <div :style="{ width: size[0] + 'px', height: size[1] + 'px' }">
    <img
      v-if="item.media[0].type === 'image'"
      :src="item.media[0].src"
      @load="onReady"
      @error="onError"
      style="width:100%;height:100%;object-fit:cover"
    />
    <video
      v-else
      :src="item.media[0].src"
      :autoplay="isActive"
      @canplay="onReady"
      @waiting="onWaiting"
      @error="onError"
      style="width:100%;height:100%;object-fit:cover"
    />
  </div>
</template>

独自の読み込みとエラーの UI

#loading と #error のスロットで、デフォルトの波のローダーとエラーアイコンを置き換えます:

vue
<ReelPlayerOverlay v-model:is-open="isOpen" :content="content">
  <template #loading="{ activeIndex }">
    <div
      style="position:absolute;inset:0;z-index:10;display:flex;
             align-items:center;justify-content:center;color:#fff;font-size:14px"
    >
      Loading slide {{ activeIndex + 1 }}...
    </div>
  </template>

  <template #error="{ activeIndex }">
    <div
      style="position:absolute;inset:0;z-index:10;display:flex;
             flex-direction:column;align-items:center;justify-content:center;
             gap:12px;color:rgba(255,255,255,0.5)"
    >
      <span style="font-size:48px">!</span>
      <span>Slide {{ activeIndex + 1 }} failed to load</span>
    </div>
  </template>
</ReelPlayerOverlay>

タイムライン

オーバーレイは、アクティブな動画の上に組み込みの再生タイムラインバーを描画します。timeline プロップで表示条件を決めます。'auto'(デフォルト)はアクティブなメディアが timelineMinDurationSeconds(デフォルト 30)より長い動画のときに表示し、'always' は動画がアクティブなら常に表示し、'never' で無効にします。完全に独自のシークバーには #timeline スロットを使います。そのスコープは、内部の TimelineController に支えられた timelineState を公開します。

vue
<ReelPlayerOverlay
  :is-open="isOpen"
  :content="items"
  timeline="auto"
  :timeline-min-duration-seconds="30"
  @close="isOpen = false"
/>

テーマは --rk-reel-timeline-* の CSS カスタムプロパティで変えます。

スロットの外では、コンポーザブルで同じコントローラーを読めます:

Export説明
useTimelineState()最も近い TimelineController を返します。TimelineProvider(オーバーレイがマウントします)の外では例外を投げます。
useTimelineStateOptional()useTimelineState と同じですが、TimelineProvider の外では例外を投げる代わりに null を返します。オーバーレイの外でも描画されることがあるコンポーネント向けです。
RK_TIMELINE_KEY両方のコンポーザブルが読むインジェクションキーです。自分のコントローラーで TimelineBar を動かすには、自分で provide します。
useViewportSize(aspectRatio)オーバーレイが自分の大きさを決めるリアクティブな [width, height] です。デスクトップでは指定したアスペクト比、幅 768px 以下ではビューポート全体になります。マウントまでは [0, 0] を返すので、SSR でも安全です。

サウンドコンテキスト

ReelPlayerOverlay はルートに SoundProvider をマウントするので、中に描画されたどのコンポーネントでも useSoundState でミュートの状態を読んだり切り替えたりできます。このコンポーザブルは @reelkit/vue-reel-player から再エクスポートされているので、@reelkit/vue を別にインポートする必要はありません。

vue
<script setup lang="ts">
import { useSoundState, toVueRef } from '@reelkit/vue';

// Inside a custom control rendered from the #controls slot:
const soundState = useSoundState();
const muted = toVueRef(soundState.muted);
</script>

<template>
  <button @click="soundState.toggle()">
    {{ muted ? 'Unmute' : 'Mute' }}
  </button>
</template>

プレイヤーの中では、#controls スロットのスコープでも soundState が公開されています。コントロールのテンプレートの中でだけ必要なら、そちらを使ってください。

CSS クラス

CSS クラスは通常のクラス(スコープなし)です。@reelkit/vue-reel-player/styles.css のあとに読み込むスタイルシートで、詳細度の高いセレクターを使えばどれでも上書きできます。色、サイズ、z-index を変えるなら、下の テーマ設定 の CSS カスタムプロパティを使ってください。

クラスコンポーネント説明
.rk-reel-overlayOverlay固定配置のフルスクリーンの背景(背景色、z-index)
.rk-reel-containerOverlayプレイヤーのコンテナー(配置、はみ出し)
.rk-reel-loaderOverlay波の読み込みアニメーションのオーバーレイ
.rk-reel-media-errorOverlayエラー状態のオーバーレイ(中央のアイコンとテキスト)
.rk-reel-media-error-textOverlayエラーメッセージのテキスト
.rk-reel-buttonControls共通の円形アイコンボタン(閉じる、サウンド、ナビゲーションの矢印)
.rk-reel-close-btnControls閉じるボタン
.rk-reel-sound-btnControlsサウンド切り替えボタン
.rk-reel-nav-arrowsNavigationデスクトップ専用の矢印のコンテナー(幅 768px 以下では非表示)
.rk-reel-nav-buttonNavigation個々の前後のナビゲーション矢印
.rk-reel-slide-wrapperSlideメディアとオーバーレイを包むラッパー
.rk-reel-slide-overlaySlideOverlayグラデーションのオーバーレイのコンテナー
.rk-reel-slide-overlay-authorSlideOverlay作者の行(アバターと名前)
.rk-reel-slide-overlay-avatarSlideOverlay作者のアバター画像
.rk-reel-slide-overlay-nameSlideOverlay作者の名前のテキスト
.rk-reel-slide-overlay-descriptionSlideOverlay説明のテキスト
.rk-reel-slide-overlay-likesSlideOverlayいいねの行(ハートと件数)
.rk-reel-video-containerVideoSlide動画のラッパー(背景色、はみ出し)
.rk-reel-video-elementVideoSlide<video> 要素
.rk-reel-video-posterVideoSlideポスター画像(再生でフェードアウト)
.rk-reel-video-poster.rk-visibleVideoSlide動画が一時停止中か読み込み中のあいだ、ポスターに付く状態のモディファイア
.rk-reel-nested-indicatorNestedSlider複数メディアのスライドの下のドットのページネーション(デスクトップとタッチで位置が変わります)
.rk-reel-nested-navNestedSlider横方向のカルーセルの矢印(幅 768px 以下では非表示)
.rk-reel-nested-nav-nextNestedSliderネストした「次へ」の矢印の位置
.rk-reel-nested-nav-prevNestedSliderネストした「前へ」の矢印の位置
.rk-reel-timelineTimelineBarシークバーのラッパー。独自の `#timeline` スロットのルートで使えば、下端に沿った配置、セーフエリアの余白、タッチ端末でのスライドオーバーレイとの間隔を引き継げます。
.rk-reel-timeline-trackTimelineBarトラック(未再生の範囲)
.rk-reel-timeline-bufferedTimelineBarバッファー済みの区間のレイヤー
.rk-reel-timeline-fillTimelineBar再生済みの進捗の塗り
.rk-reel-timeline-cursorTimelineBarシーク用のつまみ(トラックの上に浮かびます)

テーマ設定

色、サイズ、z-index、トランジションはすべて CSS カスタムプロパティにあります。:root(またはオーバーレイの任意の祖先)で 1 つでも複数でも上書きすれば、コンポーネントのソースに触れずにテーマを変えられます。トークンは @reelkit/react-reel-player と共通なので、上書きはバインディング間でそのまま使えます。

トークンデフォルト制御するもの
--rk-reel-overlay-bg#000フルスクリーンの背景色
--rk-reel-overlay-z1000オーバーレイの z-index
--rk-reel-button-bgrgba(0, 0, 0, 0.5)円形ボタンのデフォルトの背景
--rk-reel-button-bg-hoverrgba(255, 255, 255, 0.1)ナビゲーション矢印の背景(とホバーの基本状態)
--rk-reel-button-bg-hover-strongrgba(255, 255, 255, 0.2)ナビゲーション矢印のホバー時の背景
--rk-reel-button-fg#fffボタンのアイコンの色
--rk-reel-button-size44pxボタンの幅と高さ
--rk-reel-button-radius50%ボタンの角丸
--rk-reel-ui-z10閉じる、サウンド、ナビゲーションの z-index
--rk-reel-edge-padding16px閉じる、サウンド、ナビゲーション矢印の端からの距離
--rk-reel-nav-gap8px縦に並んだナビゲーション矢印の間隔
--rk-reel-transition0.2sホバーのトランジションの長さ
--rk-reel-loader-colorrgba(255, 255, 255, 0.12)波のローダーのグラデーションの色
--rk-reel-loader-duration1.8s波のローダーのアニメーションの長さ
--rk-reel-error-fgrgba(255, 255, 255, 0.4)エラーアイコンとテキストの色
--rk-reel-slide-overlay-bglinear-gradient(transparent, rgba(0, 0, 0, 0.7))キャプションの背景のグラデーション
--rk-reel-slide-overlay-padding48px 16px 16pxキャプションの内側の余白
--rk-reel-slide-overlay-name-color#fff作者の名前の色
--rk-reel-video-bg#000<video> の後ろのレターボックスの背景
--rk-reel-nested-button-bgrgba(0, 0, 0, 0.5)ネストした矢印の背景
--rk-reel-nested-button-size36pxネストした矢印の大きさ
--rk-reel-timeline-trackrgba(255, 255, 255, 0.22)トラックの背景(未再生の範囲)
--rk-reel-timeline-bufferedrgba(255, 255, 255, 0.4)バッファー済みの区間の色
--rk-reel-timeline-fill#fff再生済みの進捗の塗りの色
--rk-reel-timeline-cursor#fffシーク用のつまみの色
--rk-reel-timeline-height3px通常時のトラックの高さ
--rk-reel-timeline-height-active6pxホバー、フォーカス、シーク中のトラックの高さ
--rk-reel-timeline-cursor-width10px通常時のつまみの幅
--rk-reel-timeline-cursor-width-active14pxシーク中のつまみの幅
--rk-reel-timeline-cursor-height24px通常時のつまみの高さ
--rk-reel-timeline-cursor-height-active32pxシーク中のつまみの高さ
--rk-reel-timeline-transition0.15s ease-outトラックとつまみが伸び縮みするアニメーション

下のスニペットを、@reelkit/vue-reel-player/styles.css のあとに読み込むスタイルシートに入れてください。

css
/* Brand the reel-player overlay */
:root {
  --rk-reel-overlay-bg: #0f172a;
  --rk-reel-button-bg: rgba(99, 102, 241, 0.65);
  --rk-reel-button-bg-hover-strong: rgba(168, 85, 247, 0.85);
  --rk-reel-edge-padding: 24px;
  --rk-reel-button-size: 52px;

  /* Timeline bar: brand-matched, beefier on desktop */
  --rk-reel-timeline-track: rgba(99, 102, 241, 0.25);
  --rk-reel-timeline-buffered: rgba(168, 85, 247, 0.45);
  --rk-reel-timeline-fill: #a855f7;
  --rk-reel-timeline-cursor: #a855f7;
  --rk-reel-timeline-height: 4px;
  --rk-reel-timeline-height-active: 8px;
  --rk-reel-timeline-cursor-width-active: 18px;
  --rk-reel-timeline-transition: 0.2s ease-out;
}

アクセシビリティ

オーバーレイのルートはモーダルダイアログ(role="dialog"、aria-modal="true")です。aria-label プロップを指定すると、スクリーンリーダーの読み上げが変わります。デフォルトは「Video player」です。各スライドは role="group"、aria-roledescription="slide"、aria-label="Slide N of M" を持ちます。

オーバーレイは開くとフォーカスを取り込み、閉じるとトリガーに戻します。Tab と Shift+Tab は中のフォーカス可能な要素を循環し、外へ出たフォーカス(外側のクリック、プログラムからのフォーカス)は引き戻されます。@reelkit/core の captureFocusForReturn と createFocusTrap で実装されています。

キーボードショートカット

キー動作
ArrowUp前のスライド
ArrowDown次のスライド
ArrowLeft前のメディア(ネストしたカルーセル)
ArrowRight次のメディア(ネストしたカルーセル)
Escapeプレイヤーを閉じる