Angular Reel Player

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

ライブデモを見る →

機能

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

インストール

bash
npm install @reelkit/angular-reel-player @reelkit/angular lucide-angular

スタイルシートを一度だけ登録します:

typescript
// Anywhere that runs at startup — main.ts, app.config.ts, a component file.
import '@reelkit/angular-reel-player/styles.css';

// Or, if you keep CSS out of TypeScript, from your global stylesheet:
//   @import '@reelkit/angular-reel-player/styles.css';
// Or register it in angular.json:
//   "styles": ["node_modules/@reelkit/angular-reel-player/styles.css"]
アイコン

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

クイックスタート

スタイルシートとスタンドアロンの RkReelPlayerOverlayComponent を、コンポーネントの imports 配列にインポートします。

reel-feed.component.ts
import { Component, signal } from '@angular/core';
import {
  RkReelPlayerOverlayComponent,
  type ContentItem,
} from '@reelkit/angular-reel-player';
import '@reelkit/angular-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 content',
  },
  {
    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',
  },
];

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RkReelPlayerOverlayComponent],
  template: `
    <!-- Grid thumbnail view -->
    <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px">
      @for (item of content; track item.id; let i = $index) {
        <button
          (click)="openAt(i)"
          style="aspect-ratio:9/16;cursor:pointer;overflow:hidden"
        >
          <img
            [src]="item.media[0].poster || item.media[0].src"
            style="width:100%;height:100%;object-fit:cover"
          />
        </button>
      }
    </div>

    <rk-reel-player-overlay
      [isOpen]="isOpen()"
      [content]="content"
      [initialIndex]="startIndex()"
      (closed)="isOpen.set(false)"
    />
  `,
})
export class AppComponent {
  readonly content = content;
  readonly isOpen = signal(false);
  readonly startIndex = signal(0);

  openAt(index: number): void {
    this.startIndex.set(index);
    this.isOpen.set(true);
  }
}
オーバーレイが描画される場所

オーバーレイを document.body の末尾へ移す React と Vue のプレーヤーと違い、rk-reel-player-overlay は置いた場所に描画され、position: fixed で画面を覆います。transform、perspective、filter、will-change: transform を持つ祖先の中には置かないでください。その祖先がオーバーレイの包含ブロックになり、プレーヤーはビューポートではなくその祖先の大きさになり、祖先の overflow で切り取られます。独自のスタッキングコンテキストを作る祖先も、--rk-reel-overlay-z でプレーヤーをどこまで持ち上げられるかを制限します。その代わり、どの祖先に設定した --rk-reel-* トークンもプレーヤーに届きます。

テンプレートスロット

6 つのテンプレートスロットのディレクティブで、プレイヤーの UI のあらゆる部分をカスタマイズできます。それぞれが厳密に型付けされたコンテキストのオブジェクトを受け取ります。上書きしたいスロットだけを渡してください。残りはデフォルトが使われます。

ディレクティブコンテキストの型説明
[rkPlayerControls]PlayerControlsContext<T>独自のグローバルなコントロールバー(閉じる、サウンド切り替えなど)。ディレクティブのクラスは RkPlayerControlsDirective です
[rkPlayerError]PlayerErrorContext独自のエラー表示のテンプレートスロット。ディレクティブのクラスは RkPlayerErrorDirective です
[rkPlayerLoading]PlayerLoadingContext独自の読み込み表示のテンプレートスロット。ディレクティブのクラスは RkPlayerLoadingDirective です
[rkPlayerNavigation]PlayerNavigationContext独自の前後のナビゲーション矢印。ディレクティブのクラスは RkPlayerNavigationDirective です
[rkPlayerNestedNavigation]PlayerNestedNavigationContext内側の横方向のスライダー用の独自のナビゲーション矢印。ディレクティブのクラスは RkPlayerNestedNavigationDirective です
[rkPlayerNestedSlide]PlayerNestedSlideContext内側の横方向のスライダーの各スライドの独自の内容。ディレクティブのクラスは RkPlayerNestedSlideDirective です
[rkPlayerSlide]PlayerSlideContext<T>デフォルトのメディアスライドを置き換える、完全に独自のスライドの内容。ディレクティブのクラスは RkPlayerSlideDirective です
[rkPlayerSlideOverlay]PlayerSlideOverlayContext<T>スライドごとのオーバーレイ(作者の情報、いいね、説明など)。ディレクティブのクラスは RkPlayerSlideOverlayDirective です
[rkPlayerTimeline]PlayerTimelineContext<T>独自の再生タイムラインバー。条件(タイムラインのモードと最小の長さ)がデフォルトのバーを描画する場合だけ描画されます(同じ auto/always/never の判定)。ディレクティブのクラスは RkPlayerTimelineDirective です

PLAYER_TEMPLATE_SLOT_DIRECTIVES はこれらのディレクティブをすべてまとめたものなので、コンポーネントの imports に一度に展開できます。

typescript
import {
  RkReelPlayerOverlayComponent,
  RkPlayerSlideOverlayDirective,
  RkPlayerControlsDirective,
  RkPlayerNavigationDirective,
  RkCloseButtonComponent,
  RkSoundButtonComponent,
  type ContentItem,
  type PlayerSlideOverlayContext,
  type PlayerControlsContext,
  type PlayerNavigationContext,
} from '@reelkit/angular-reel-player';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    RkReelPlayerOverlayComponent,
    RkPlayerSlideOverlayDirective,
    RkPlayerControlsDirective,
    RkPlayerNavigationDirective,
    RkCloseButtonComponent,
    RkSoundButtonComponent,
  ],
  template: `
    <rk-reel-player-overlay
      [isOpen]="isOpen"
      [content]="content"
      (closed)="isOpen = false"
    >
      <!-- Custom per-slide overlay: author + likes -->
      <ng-template rkPlayerSlideOverlay let-item let-isActive="isActive">
        @if (isActive) {
          <div 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>
        }
      </ng-template>

      <!-- Custom global controls -->
      <ng-template rkPlayerControls
                   let-onClose
                   let-soundState="soundState">
        <div style="position:absolute;top:16px;right:16px;display:flex;gap:8px">
          <rk-sound-button [soundState]="soundState" />
          <rk-close-button (click)="onClose()" />
        </div>
      </ng-template>

      <!-- Custom navigation -->
      <ng-template rkPlayerNavigation
                   let-onPrev
                   let-onNext="onNext"
                   let-activeIndex="activeIndex"
                   let-count="count">
        <div style="position:absolute;right:16px;top:50%;transform:translateY(-50%)">
          <button (click)="onPrev()" [disabled]="activeIndex === 0">&#9650;</button>
          <button (click)="onNext()" [disabled]="activeIndex === count - 1">&#9660;</button>
        </div>
      </ng-template>

      <!-- Custom playback timeline -->
      <ng-template rkPlayerTimeline let-state="timelineState">
        <div class="rk-reel-timeline" style="padding:0 16px"
             (pointerdown)="bindTrack(track, state); track.focus()">
          <div #track
               role="slider"
               [attr.aria-valuenow]="state.currentTime()"
               style="height:6px;background:rgba(255,255,255,0.2);border-radius:999px">
            <div [style.width.%]="state.progress() * 100"
                 style="height:100%;background:linear-gradient(90deg,#6366f1,#ec4899);border-radius:999px"></div>
          </div>
        </div>
      </ng-template>
    </rk-reel-player-overlay>
  `,
})
export class AppComponent {
  isOpen = false;
  content: ContentItem[] = [];

  private _trackDispose: (() => void) | null = null;
  /** Wire pointer + keyboard scrub onto your custom track element. */
  bindTrack(el: HTMLElement, state: PlayerTimelineState) {
    this._trackDispose?.();
    this._trackDispose = state.bindInteractions(el);
  }
}

デフォルトのスライドコンポーネント

デフォルトのスライドを組み立てている部品です。どれもスタンドアロンのコンポーネントなので、自分のテンプレートの中で使えばデフォルトの見た目の一部を保てます。

コンポーネントセレクター説明
RkMediaSlideComponentrk-media-slideデフォルトのスライドです。コンテンツの要素の media に応じて rk-image-slide、rk-video-slide、rk-nested-slider を描画します。rkPlayerSlide テンプレートで使えば、デフォルトのメディアを保ったまま独自のレイヤーを上に重ねられます。
RkImageSlideComponentrk-image-slideobject-fit: cover の画像 1 枚です。読み込むまでは poster を表示し、onReady、onWaiting、onError を呼びます。
RkVideoSlideComponentrk-video-slide共有の <video> 要素で再生する動画 1 本です。オーバーレイが提供する SoundStateService が必要なので、プレイヤーの中でだけ描画してください。
RkNestedSliderComponentrk-nested-slider複数の media を持つコンテンツの要素のための、インジケーターのドット付きの横方向のスライダーです。
RkSlideOverlayComponentrk-slide-overlayデフォルトの下部のオーバーレイです。author、description、likes の入力から、作者のアバターと名前、説明、いいねを表示します。rkPlayerSlideOverlay テンプレートで使えばデフォルトの見た目を保てます。

独自のタイムライン

rkPlayerTimeline のテンプレートスロットが呼ばれるのは、オーバーレイの表示条件がデフォルトのバーを描画する場合(同じ timeline モードと timelineMinDurationSeconds)だけなので、それを実装し直す必要はありません。ルート要素で .rk-reel-timeline クラスを使えば、下端に沿った配置、セーフエリアの余白、タッチ端末での間隔を引き継げます。シークのトラックで state.bindInteractions(el) を呼ぶと、ポインターとキーボードによるシークがつながります。

ネストしたスライダー(複数メディアのアイテム)

ContentItem が複数の media を持つ場合、プレイヤーはそれらを横方向にネストしたスライダー(Instagram のカルーセル風)で描画します。内側のスライドの内容をカスタマイズするには rkPlayerNestedSlide スロットを使います。

typescript
const carouselItem: ContentItem = {
  id: '3',
  media: [
    { id: 'img-a', type: 'image', src: '/photo-a.jpg', aspectRatio: 2 / 3 },
    { id: 'img-b', type: 'image', src: '/photo-b.jpg', aspectRatio: 3 / 4 },
    { id: 'img-c', type: 'image', src: '/photo-c.jpg', aspectRatio: 1 },
  ],
  author: { name: 'Emma Davis', avatar: '/avatar3.jpg' },
  likes: 8901,
  description: 'Travel moments',
};

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

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

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

rkPlayerSlide のテンプレートスロットを使う場合は、コンテキストのコールバックで読み込み表示を制御します:

コールバック呼ぶタイミング
onReady画像が読み込まれたか、動画の再生が始まったとき。読み込み中とエラーの状態をクリアします。
onWaiting再生の途中で動画がバッファリングしているとき。読み込み表示を出します。
onErrorコンテンツの読み込みに失敗したとき。エラーのオーバーレイを出し、その URL を壊れたものとしてキャッシュします。
html
<!-- Wire lifecycle callbacks in a custom slide template -->
<rk-reel-player-overlay [isOpen]="isOpen" [content]="items" (closed)="isOpen = false">
  <ng-template rkPlayerSlide
    let-item
    let-size="size"
    let-isActive="isActive"
    let-onReady="onReady"
    let-onWaiting="onWaiting"
    let-onError="onError"
  >
    @if (item.media[0].type === 'image') {
      <img
        [src]="item.media[0].src"
        (load)="onReady()"
        (error)="onError()"
        [style.width.px]="size[0]"
        [style.height.px]="size[1]"
        style="object-fit: cover"
      />
    } @else {
      <video
        [src]="item.media[0].src"
        [autoplay]="isActive"
        (canplay)="onReady()"
        (waiting)="onWaiting()"
        (error)="onError()"
        [style.width.px]="size[0]"
        [style.height.px]="size[1]"
        style="object-fit: cover"
      />
    }
  </ng-template>
</rk-reel-player-overlay>

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

デフォルトの波のローダーとエラーアイコンを、独自のテンプレートに置き換えます:

html
<rk-reel-player-overlay [isOpen]="isOpen" [content]="items" (closed)="isOpen = false">
  <ng-template rkPlayerLoading let-index let-item="item">
    <div style="
      position: absolute; inset: 0; z-index: 10;
      display: flex; align-items: center; justify-content: center;
      color: #fff; font-size: 14px;
    ">
      Loading slide {{ index + 1 }}...
    </div>
  </ng-template>

  <ng-template rkPlayerError let-index let-item="item">
    <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>Failed to load media</span>
    </div>
  </ng-template>
</rk-reel-player-overlay>

タイムライン

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

html
<rk-reel-player-overlay
  [isOpen]="isOpen()"
  [content]="items"
  timeline="auto"
  [timelineMinDurationSeconds]="30"
  (closed)="isOpen.set(false)"
/>

テーマは --rk-reel-timeline-* の CSS カスタムプロパティで変えます。独自のコンポーネントから直接制御するには、TimelineStateService をインジェクトしてください。

RkTimelineBarComponent

デフォルトの再生シークバーのコンポーネントです。TimelineStateService(RkReelPlayerOverlayComponent が提供します)を使い、トラック、バッファー済みの範囲、進捗の塗り、シーク用のつまみを描画します。セレクター:rk-timeline-bar。入力:class?: string、style?: Record<string, string>。デフォルトのバーを包んだり拡張したりするには rkPlayerTimeline のテンプレートの中で使います。単独で使うのは、そのサービスを提供するコンポーネントの中だけにしてください。

typescript
import { RkTimelineBarComponent } from '@reelkit/angular-reel-player';

@Component({
  standalone: true,
  imports: [RkReelPlayerOverlayComponent, RkTimelineBarComponent],
  template: `
    <rk-reel-player-overlay [isOpen]="isOpen()" [content]="items">
      <!-- Wrap or augment the default bar: -->
      <ng-template rkPlayerTimeline>
        <my-timecode />
        <rk-timeline-bar />
      </ng-template>
    </rk-reel-player-overlay>
  `,
})
export class AppComponent {}

SoundStateService

RkReelPlayerOverlayComponent のレベルで提供されます。デフォルトのサウンドボタンがインジェクトし、コントロールのテンプレートスロットのコンテキストでも公開されます。オーバーレイの 子 である独自のコントロールでインジェクトすれば、直接アクセスできます。

typescript
import { inject } from '@angular/core';
import { SoundStateService } from '@reelkit/angular-reel-player';

@Component({ ... })
export class AppComponent {
  readonly soundState = inject(SoundStateService);

  // Use in template:
  // [class.muted]="soundState.muted()"
  // [disabled]="soundState.disabled()"
  // (click)="soundState.toggle()"
}
メンバー型説明
muted()Signal<boolean>プレイヤーが現在ミュートかどうか
disabled()Signal<boolean>アクティブなスライドに動画がないか、トランジション中なら true
toggle()() => voidミュートの状態を切り替えます

URL の状態

ライブデモを見る →

RkReelPlayerUrlOverlayComponent は、開閉状態をアドレスバーに持つ別のコンポーネントです。インジェクションコンテキストで createOverlayUrlState を使ってコントローラーを作り、[controller] として渡します。パラメーターがスライドを指すとプレイヤーは開き、なくなると閉じます。リンクは共有でき、戻るボタンでプレイヤーが閉じます。RkReelPlayerOverlayComponent は [isOpen] で制御するままなので、開閉状態を決めるものはそれぞれのコンポーネントで 1 つだけです。

組み込みのキー

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

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

typescript
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import {
  RkReelPlayerUrlOverlayComponent,
  type ContentItem,
} from '@reelkit/angular-reel-player';
import { createOverlayUrlState, urlIndexKey, urlStableIdKey } from '@reelkit/angular';
import { createRouterUrlAdapter } from '@reelkit/angular/ng-router-url-adapter';
import '@reelkit/angular-reel-player/styles.css';

@Component({
  standalone: true,
  imports: [RkReelPlayerUrlOverlayComponent, RouterLink],
  template: `
    @for (post of content; track post.id; let i = $index) {
      <a [routerLink]="[]" [queryParams]="{ reel: i }">{{ post.id }}</a>
    }
    <rk-reel-player-url-overlay [controller]="reel" [content]="content" />
  `,
})
export class FeedComponent {
  content: ContentItem[] = [/* ... */];
  protected readonly reel = createOverlayUrlState({
    param: 'reel',
    adapter: createRouterUrlAdapter(),
    ...urlIndexKey(() => this.content.length),
  });
}
  • 開くと履歴エントリーを 1 つ 積みます。フィードをスワイプするとそれを 置き換え るので、N 回スワイプしてもエントリーは増えず、1 回戻れば必ずプレイヤーから出られます。戻るとプレイヤーが閉じ、スライドは戻りません。
  • 戻るで閉じるのは、アプリ内から開いた場合だけです(リンクがエントリーを積んだ場合)。新しいタブで直接開いた共有リンクには前の履歴がないので、ブラウザーの戻るではサイトを離れます。✕ ボタンか Escape なら、その場でパラメーターを取り除いてページにとどまります。
  • ディープリンク ?reel=3 は、読み込み時にそのスライドでプレイヤーを開きます。
  • どのスライドも指さないパラメーター(古いブックマークや手で書き換えた値)は、開けないスライドをアドレスバーが指したままにならないよう、URL から取り除かれます。
  • URL の深さはコントローラーのキーで決まります。投稿だけなら一軸、複数メディアの投稿の内側の画像のインデックスも運ぶなら二軸(urlIndexTwoAxisKey)です。アプリごとにキーを 1 つ選んでください。形は互いに読み替えられません。

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

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

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

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

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

typescript
import { createOverlayUrlState, urlIndexTwoAxisKey } from '@reelkit/angular';

protected readonly reel = createOverlayUrlState({
  param: 'reel',
  ...urlIndexTwoAxisKey({
    outerCount: () => this.content.length,
    innerCounts: () => this.content.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
protected readonly reel = createOverlayUrlState({
  param: 'reel',
  ...urlStableIdKey({ items: () => this.loaded() }),
});

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

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

typescript
protected readonly reel = createOverlayUrlState({
  param: 'reel',
  codec: { decode: (raw) => raw, encode: (id) => id },
  locator: {
    locate: (id) => this.loaded().findIndex((x) => x.id === id),
    identify: (index) => this.loaded()[index].id,
  },
});

無限フィード。 locate は同期なので、読み込み済みの投稿にしか答えられません。20 件を読み込んだフィードの 400 番目の投稿への共有リンクは見つかりません。locateAsync はフォールバックで、locate が見つけられなかったときだけ呼ばれます。必要なページを読み込み、その識別子が最終的に持ったインデックスを返してください。

近道

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

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

独自のデータ型

BaseContentItem を拡張すれば、独自のドメインモデルを使えます。コンポーネントはジェネリックです:RkReelPlayerOverlayComponent<T extends BaseContentItem>。

typescript
import type { BaseContentItem } from '@reelkit/angular-reel-player';

interface MyPost extends BaseContentItem {
  // id: string  — from BaseContentItem
  // media: MediaItem[]  — from BaseContentItem
  title: string;
  tags: string[];
  publishedAt: Date;
}

@Component({
  imports: [RkReelPlayerOverlayComponent],
  template: `
    <rk-reel-player-overlay [isOpen]="isOpen" [content]="posts" (closed)="isOpen = false">
      <ng-template rkPlayerSlideOverlay let-post let-isActive="isActive">
        @if (isActive) {
          <div style="position:absolute;bottom:80px;left:16px;color:#fff">
            <h3>{{ post.title }}</h3>
            @for (tag of post.tags; track tag) {
              <span>#{{ tag }} </span>
            }
          </div>
        }
      </ng-template>
    </rk-reel-player-overlay>
  `,
})
export class AppComponent {
  isOpen = false;
  posts: MyPost[] = [];
}

RkReelPlayerOverlayComponent の入力

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

RkReelPlayerOverlayComponent の出力

出力型説明
apiReadyEventEmitter<ReelApi>スライダーの準備ができたときに発行され、命令的な API を公開します
closedEventEmitter<void>プレイヤーが閉じたときに発行されます
slideChangeEventEmitter<number>アクティブなスライドのインデックスが変わったときに発行されます
innerSlideChangeEventEmitter<{ outer: number; inner: number }>アクティブな投稿の内側のメディアのインデックスが変わったときに発行されます。内側で移動したときと、外側でアクティブになったときで、アクティブになった投稿の現在の内側のインデックス(単一メディアの投稿なら 0)を報告します。

RkReelPlayerUrlOverlayComponent の入力

上の入力をすべて受け取りますが、isOpen と initialIndex の代わりに、位置でスライドを選ぶ controller を使います。出力は closed と slideChange です。

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

MediaItem インターフェース

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

テンプレートスロットのコンテキスト型

型フィールド
PlayerControlsContext<T>{ $implicit: onClose, activeIndex, content: T[], soundState: PlayerSoundState }
PlayerNavigationContext{ $implicit: onPrev, onNext, activeIndex, count }
PlayerNestedNavigationContext{ $implicit: onPrev, onNext, activeIndex, count }
PlayerNestedSlideContext{ $implicit: MediaItem, index, size, isActive, isInnerActive, slideKey }
PlayerSlideContext<T>{ $implicit: T, index, size: [number,number], isActive, slideKey, onReady, onWaiting, onError }
PlayerSlideOverlayContext<T>{ $implicit: T, index, isActive }
PlayerTimelineContext<T>{ $implicit: T, activeIndex, timelineState: PlayerTimelineState }
PlayerTimelineState{ duration(), currentTime(), progress(), bufferedRanges(), isScrubbing(), seek(t), bindInteractions(el) }
PlayerBufferedRange{ start, end }(秒)。bufferedRanges() の 1 つの要素
PlayerLoadingContext{ $implicit: activeIndex, item, innerActiveIndex }。rkPlayerLoading テンプレートが受け取るもの。ネストしたスライダーの外では innerActiveIndex は null
PlayerErrorContext{ $implicit: activeIndex, item, innerActiveIndex }。rkPlayerError テンプレートが受け取るもの

CSS クラス

CSS クラスはすべて通常のクラス(スコープなし)なので、@reelkit/angular-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-btnNavigation個々の前後のナビゲーション矢印
.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-loaderVideoSlide波の読み込みアニメーション
.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-nested-slider-innerNestedSliderネストした横方向のスライダーのルート
.rk-reel-timelineTimelineBarシークバーのラッパー。独自の `rkPlayerTimeline` のテンプレートのルートで使えば、下端に沿った配置、セーフエリアの余白、タッチ端末でのスライドオーバーレイとの間隔を引き継げます。
.rk-reel-timeline-trackTimelineBarトラック(未再生の範囲)
.rk-reel-timeline-bufferedTimelineBarバッファー済みの区間のレイヤー
.rk-reel-timeline-fillTimelineBar再生済みの進捗の塗り
.rk-reel-timeline-cursorTimelineBarシーク用のつまみ(トラックの上に浮かびます)

テーマ設定

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

トークンデフォルト制御するもの
--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-video-loader-colorrgba(255, 255, 255, 0.15)動画のバッファリング中のシマーの色
--rk-reel-nested-button-bgrgba(0, 0, 0, 0.5)ネストした矢印の背景
--rk-reel-nested-button-size36pxネストした矢印の大きさ
--rk-reel-nested-edge-padding12pxネストした矢印の端からの距離
--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/angular-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")です。ariaLabel 入力を指定すると、スクリーンリーダーの読み上げが変わります。デフォルトは「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プレイヤーを閉じる