Angular Reel Player

Vertikaler Medienplayer im Vollbild und im Stil von Instagram/TikTok für Angular, gebaut auf @reelkit/angular-reel-player.

Live-Demo ansehen →

Funktionen

Vertikales Wischen
Touch, Ziehen, Tastatur, Mausrad
Video-Autoplay
Spielt, sobald sichtbar
Ton an/aus
Durchgehend auf iOS
Mehrere Medien
Verschachtelte horizontale Karussells
Merkt sich die Position
Macht dort weiter, wo du warst
Frame-Erfassung
Überblendung vom Poster zum Video
Virtualisiert
Nur 3 Slides im DOM
Seitenverhältnis
9:16 am Desktop, voll auf dem Handy
Navigation am Desktop
Pfeil-Buttons
Generische Typen
Eigene Datenmodelle für Inhalte
Anpassbar
Template-Slots für alles
Fehlerbehandlung
Erkennt kaputte Medien mit LRU-Cache
URL-State
Teilbare Links, Schließen mit Zurück

Installation

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

Registriere das Stylesheet einmal:

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"]
Icons

Die Standard-Bedienelemente nutzen lucide-angular für die Icons (Schließen, Ton, Navigationspfeile). Wenn du eine andere Icon-Bibliothek bevorzugst, liefere über die Template-Slots rkPlayerControls und rkPlayerNavigation deine eigenen.

Schnellstart

Importiere das Stylesheet und die Standalone-Komponente RkReelPlayerOverlayComponent in das Array imports deiner Komponente.

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);
  }
}
Wo das Overlay gerendert wird

Anders als die Player für React und Vue, die ihr Overlay an das Ende von document.body verschieben, rendert rk-reel-player-overlay dort, wo du es platzierst, und deckt den Bildschirm mit position: fixed ab. Halte es aus jedem Vorfahren mit transform, perspective, filter oder will-change: transform heraus: Dieser Vorfahre wird zum umgebenden Block des Overlays, der Player bekommt also dessen Größe statt der des Viewports und wird von dessen overflow beschnitten. Ein Vorfahre mit eigenem Stacking-Kontext begrenzt außerdem, wie weit --rk-reel-overlay-z den Player anheben kann. Im Gegenzug erreichen --rk-reel-*-Tokens, die auf einem beliebigen Vorfahren gesetzt sind, den Player.

Template-Slots

Mit sechs Direktiven für Template-Slots passt du jeden Teil der Oberfläche des Players an. Jede bekommt ein streng typisiertes Kontext-Objekt. Gib nur die Slots an, die du ersetzen willst — für den Rest gelten die Standards.

DirektiveKontext-TypBeschreibung
[rkPlayerControls]PlayerControlsContext<T>Eigene globale Leiste mit Bedienelementen (Schließen, Ton an/aus und so weiter); die Klasse der Direktive ist RkPlayerControlsDirective
[rkPlayerError]PlayerErrorContextTemplate-Slot für eine eigene Fehleranzeige; die Klasse der Direktive ist RkPlayerErrorDirective
[rkPlayerLoading]PlayerLoadingContextTemplate-Slot für eine eigene Ladeanzeige; die Klasse der Direktive ist RkPlayerLoadingDirective
[rkPlayerNavigation]PlayerNavigationContextEigene Pfeile für zurück/weiter; die Klasse der Direktive ist RkPlayerNavigationDirective
[rkPlayerNestedNavigation]PlayerNestedNavigationContextEigene Navigationspfeile für den inneren horizontalen Slider; die Klasse der Direktive ist RkPlayerNestedNavigationDirective
[rkPlayerNestedSlide]PlayerNestedSlideContextEigener Inhalt für jede Slide im inneren horizontalen Slider; die Klasse der Direktive ist RkPlayerNestedSlideDirective
[rkPlayerSlide]PlayerSlideContext<T>Vollständig eigener Slide-Inhalt, der die Standard-Medien-Slide ersetzt; die Klasse der Direktive ist RkPlayerSlideDirective
[rkPlayerSlideOverlay]PlayerSlideOverlayContext<T>Overlay pro Slide (Infos zum Autor, Likes, Beschreibung und so weiter); die Klasse der Direktive ist RkPlayerSlideOverlayDirective
[rkPlayerTimeline]PlayerTimelineContext<T>Eigene Zeitleiste für die Wiedergabe. Wird nur gerendert, wenn die Bedingung (Modus der Zeitleiste + Mindestdauer) auch die Standardleiste rendern würde (dieselbe Logik mit auto/always/never). Die Klasse der Direktive ist RkPlayerTimelineDirective

PLAYER_TEMPLATE_SLOT_DIRECTIVES sammelt alle diese Direktiven, sodass eine Komponente sie in einem Schritt in ihre imports übernehmen kann.

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);
  }
}

Standardkomponenten für Slides

Die Teile, aus denen der Standard-Slide besteht. Jedes ist eine Standalone-Komponente; nutze eine in deinem eigenen Template, um einen Teil des Standardaussehens zu behalten.

KomponenteSelektorBeschreibung
RkMediaSlideComponentrk-media-slideDer Standard-Slide. Rendert rk-image-slide, rk-video-slide oder rk-nested-slider anhand von media des Inhaltselements; nutze ihn in einem Template rkPlayerSlide, um die Standardmedien zu behalten und eine eigene Ebene darüberzulegen.
RkImageSlideComponentrk-image-slideEin Bild mit object-fit: cover, das bis zum Laden sein poster zeigt und onReady, onWaiting oder onError aufruft.
RkVideoSlideComponentrk-video-slideEin Video auf dem gemeinsamen <video>-Element. Es braucht den SoundStateService, den das Overlay bereitstellt, also rendere es nur innerhalb des Players.
RkNestedSliderComponentrk-nested-sliderDer horizontale Slider mit Punkten als Indikator, für ein Inhaltselement mit mehreren Einträgen in media.
RkSlideOverlayComponentrk-slide-overlayDas Standard-Overlay unten: Avatar und Name des Autors, Beschreibung und Likes aus seinen Inputs author, description und likes. Nutze es in einem Template rkPlayerSlideOverlay, um das Standardaussehen zu behalten.

Eigene Zeitleiste

Der Template-Slot rkPlayerTimeline wird nur aufgerufen, wenn die Regeln des Overlays auch die Standardleiste rendern würden (derselbe Modus timeline + timelineMinDurationSeconds). Du musst diese Logik also nicht nachbauen. Nutze die Klasse .rk-reel-timeline auf deinem Wurzelelement, um die Position bündig am unteren Rand, den Abstand für die Safe Area und den Freiraum auf Touch-Geräten zu übernehmen. Rufe state.bindInteractions(el) auf deiner Spur zum Spulen auf, um das Spulen mit Zeiger und Tastatur anzubinden.

Verschachtelter Slider (Elemente mit mehreren Medien)

Enthält ein ContentItem mehrere Einträge in media, rendert der Player sie in einem verschachtelten horizontalen Slider (im Stil eines Instagram-Karussells). Mit dem Slot rkPlayerNestedSlide passt du den Inhalt der inneren Slides an.

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',
};

Laden von Inhalten und Fehlerbehandlung

Der Player verfolgt Lade- und Fehlerzustände pro Slide. Ein Wellen-Loader erscheint, während Inhalte laden; ein Fehler-Icon erscheint bei kaputten Medien. Fehlerhafte URLs werden zwischengespeichert, so dass ein erneuter Besuch den Fehler sofort zeigt, ohne es noch einmal zu versuchen.

Lifecycle-Callbacks

Nutzt du den Template-Slot rkPlayerSlide, steuerst du die Ladeanzeige über die Callbacks im Kontext:

CallbackWann aufrufen
onReadyDas Bild ist geladen oder das Video spielt. Setzt Lade- und Fehlerzustand zurück.
onWaitingDas Video puffert mitten in der Wiedergabe. Zeigt die Ladeanzeige.
onErrorDer Inhalt konnte nicht geladen werden. Zeigt das Fehler-Overlay und merkt sich die URL als kaputt.
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>

Eigene Oberfläche für Laden und Fehler

Ersetze den Standard-Wellen-Loader und das Fehler-Icon durch eigene Templates:

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>

Zeitleiste

Das Overlay rendert über dem aktiven Video eine eingebaute Zeitleiste für die Wiedergabe. Du steuerst sie über den Input timeline: 'auto' (Standard) rendert sie, sobald das aktive Medium ein Video ist, das länger als timelineMinDurationSeconds (Standard 30) dauert, 'always' bei jedem aktiven Video, 'never' schaltet sie ab. Für eine vollständig eigene Spulleiste nutzt du die Template-Direktive rkPlayerTimeline; ihr Kontext stellt einen timelineState bereit, der auf dem zugrunde liegenden TimelineController aufbaut.

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

Das Theming läuft über die CSS Custom Properties --rk-reel-timeline-*. Für die direkte Steuerung in eigenen Komponenten injizierst du TimelineStateService.

RkTimelineBarComponent

Die Standardkomponente für die Spulleiste. Sie nutzt TimelineStateService (bereitgestellt von RkReelPlayerOverlayComponent) und rendert die Spur, die gepufferten Bereiche, die Fortschrittsfüllung und den Griff zum Spulen. Selektor: rk-timeline-bar. Inputs: class?: string, style?: Record<string, string>. Nutze sie in einem Template rkPlayerTimeline, um die Standardleiste zu umhüllen oder zu erweitern; allein nur in einer Komponente, die den Service bereitstellt.

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

Wird auf der Ebene von RkReelPlayerOverlayComponent bereitgestellt. Der Standard-Ton-Button injiziert ihn, und der Kontext des Template-Slots für die Bedienelemente stellt ihn bereit. Eigene Bedienelemente, die Kinder des Overlays sind, können ihn für den direkten Zugriff injizieren.

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()"
}
MitgliedTypBeschreibung
muted()Signal<boolean>Ob der Player gerade stumm ist
disabled()Signal<boolean>Wahr, wenn die aktive Slide kein Video hat oder gerade wechselt
toggle()() => voidSchaltet den Stummzustand um

URL-State

Live-Demo ansehen →

RkReelPlayerUrlOverlayComponent ist eine eigene Komponente, deren Öffnungszustand in der Adressleiste liegt. Baue in einem Injection-Kontext einen Controller mit createOverlayUrlState und übergib ihn als [controller]: Der Player öffnet sich, wenn der Parameter eine Slide nennt, und schließt sich, wenn er verschwindet. Links lassen sich teilen, und der Zurück-Button schließt den Player. RkReelPlayerOverlayComponent bleibt über [isOpen] gesteuert, so hat jede Komponente genau eine Quelle dafür, ob sie offen ist.

Eingebaute Schlüssel

Du kannst Slides mit einem eingebauten Schlüssel ansprechen — setze urlIndexKey (nach Position) oder urlStableIdKey (nach einer stabilen id) in den Controller ein; beide werden von @reelkit/angular erneut exportiert. Siehe den Leitfaden zum URL-State und die Core-API.

Eine App mit Router übergibt einen Adapter, der den Router nutzt, damit der Router die einzige Quelle der Wahrheit für die Navigation bleibt — wer den Verlauf an ihm vorbei schreibt, lässt seine Location veralten und verliert den Parameter bei der nächsten Navigation. createRouterUrlAdapter aus @reelkit/angular/ng-router-url-adapter ist der fertige Adapter.

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),
  });
}
  • Das Öffnen legt einen Eintrag im Verlauf an. Wischen durch den Feed ersetzt ihn, N-mal Wischen fügt also keine Einträge hinzu, und ein Schritt zurück verlässt den Player immer. Zurück schließt; es blättert nicht durch die Slides.
  • Zurück schließt nur, wenn der Player aus der App heraus geöffnet wurde — der Link hat einen Eintrag angelegt. Ein geteilter Link, der direkt in einem neuen Tab geöffnet wird, hat keinen Verlauf dahinter, also verlässt Zurück im Browser die Seite; der Button ✕ oder Escape entfernt den Parameter an Ort und Stelle und bleibt auf der Seite.
  • Ein Deep Link ?reel=3 öffnet den Player beim Laden auf dieser Slide.
  • Ein Parameter, der keine Slide nennt — ein veraltetes Lesezeichen, ein von Hand geänderter Wert —, wird aus der URL entfernt, statt die Adressleiste eine Slide behaupten zu lassen, die sich nicht öffnen lässt.
  • Die Tiefe der URL folgt dem Schlüssel des Controllers: eine Achse nur für den Beitrag, oder zwei Achsen (urlIndexTwoAxisKey), um zusätzlich den inneren Bildindex eines Beitrags mit mehreren Medien zu tragen. Wähle einen Schlüssel pro App; die Formen lassen sich nicht gegenseitig dekodieren.

Alle Optionen von createOverlayUrlState stehen in der Angular-API-Referenz.

Ein Schlüssel oder zwei — wähle die Tiefe der URL

Dieselbe RkReelPlayerUrlOverlayComponent steuert beide Formen; sie unterscheidet sie zur Laufzeit an der Position des Controllers, es gibt also keinen Input für den Modus. Den Schlüssel wählst du, wenn du den Controller baust:

SchlüsselFormatTrägt
urlIndexKey(…)?reel=3Nur den vertikalen Beitrag.
urlIndexTwoAxisKey(…)?reel=3.2Den Beitrag und den inneren Medienindex eines Karussells.

Die beiden Formate sind bewusst verschieden — ein Schlüssel mit zwei Achsen hat immer einen Punkt (3.0, nie ein bloßes 3), ein bloßer Link mit einer Achse wird also nicht falsch dekodiert. Wechselt eine App zwischen den Schlüsseln, werden deshalb alle früher geteilten Links ungültig. Wähle eine Form und bleib dabei.

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

Stabile Links. Der Index hängt an der Position, ein gespeichertes ?reel=3 öffnet also einen anderen Beitrag, sobald der Feed neu sortiert ist — bei einem Feed ist das der Normalfall. urlStableIdKey nutzt die stabile id jedes Beitrags als Schlüssel und durchsucht den aktuellen Feed — ein Aufruf deckt den üblichen Fall ab.

typescript
protected readonly reel = createOverlayUrlState({
  param: 'reel',
  ...urlStableIdKey({ items: () => this.loaded() }),
});

Übergib hashCodec: base64UrlCodec, um die id in der URL als base64url zu kodieren — eine umkehrbare Verschleierung, kein kryptografischer Hash.

Nutzt du ein anderes Feld als Schlüssel (einen slug) oder lädst einen endlosen Feed seitenweise mit locateAsync, baust du codec/locator selbst. Zwei getrennte Aufgaben: codec schreibt die Identität in die URL, locator findet, wo diese Identität steht.

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,
  },
});

Endlose Feeds. locate ist synchron und kann daher nur für Beiträge antworten, die schon geladen sind — ein geteilter Link auf Beitrag 400 eines Feeds, der 20 geladen hat, findet nichts. locateAsync ist der Ausweg und wird nur aufgerufen, wenn locate nichts findet: Lade die nötigen Seiten und gib dann den Index zurück, den die Identität tatsächlich hat.

Abkürzung

Ist die id des Elements dein Schlüssel? Spare dir den selbst gebauten codec und locator — übergib locateAsync direkt an urlStableIdKey({ items, locateAsync }) (es lädt bei einem Fehlschlag nach und gibt dann den Index zurück). Die ausführlichere Version unten ist für einen Schlüssel aus einem anderen Feld oder für die volle Kontrolle.

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);
    },
  },
});
  • Solange locateAsync läuft, bleibt der Player geschlossen und der Parameter unberührt, der Deep Link übersteht also das Laden. null oder eine Ablehnung entfernt den Parameter.
  • Eine Antwort, die erst ankommt, nachdem sich die URL geändert hat, nach einem Schließen oder nach dem Aushängen, wird verworfen — ein langsames Laden kann keine Slide öffnen, nach der niemand gefragt hat.
  • Während des Wartens wird nichts gerendert; die Seite besitzt diesen Ladezustand bereits, also renderst du dein eigenes Skeleton.
  • Es gibt kein Timeout — der Player kann nicht wissen, wie lang der Feed ist. Antworte mit null, wenn die Seiten erschöpft sind, sonst bleibt das Overlay auf Dauer geschlossen.

Eigene Datentypen

Erweitere BaseContentItem, um dein eigenes Domänenmodell zu nutzen. Die Komponente ist generisch: 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[] = [];
}

Inputs von RkReelPlayerOverlayComponent

InputTypStandardBeschreibung
ariaLabelstring'Video player'Barrierefreie Bezeichnung für die Region des Dialogs
aspectRationumber | undefinedundefinedVerhältnis von Breite zu Höhe für den Container am Desktop. Standard ist 9/16, das auch einen Wert von null, einen negativen oder einen nicht endlichen Wert ersetzt. Bei 768px Breite und schmaler nutzt der Player den ganzen Viewport.
contentT[] (extends BaseContentItem)PflichtArray der Inhalte, die der Player anzeigt
enableNavKeysbooleantrueSchaltet die Navigation mit den Pfeiltasten ein
enableWheelbooleantrueSchaltet die Navigation mit dem Mausrad ein
initialIndexnumber0Nullbasierter Index des zuerst sichtbaren Elements
initialInnerIndexnumber0Innerer Medienindex, mit dem geöffnet wird, nur für den zuerst sichtbaren Beitrag — so führt eine URL mit zwei Achsen direkt zu einem bestimmten Bild eines Beitrags mit mehreren Medien. Wird ignoriert, sobald der Nutzer navigiert.
isOpenbooleanPflichtSteuert die Sichtbarkeit des Overlays; bei false wird das Overlay aus dem DOM entfernt
loopbooleanfalseSchaltet die Endlosschleife zwischen den Slides ein
swipeDistanceFactornumber0.12Mindestanteil der Wischstrecke, der einen Slide-Wechsel auslöst
timelineTimelineMode ('auto' | 'always' | 'never')'auto'Strategie für die eingebaute Zeitleiste der Wiedergabe. 'auto' rendert sie nur für Videos, die länger als timelineMinDurationSeconds sind; 'always' rendert sie, sobald die aktive Slide ein Video hat; 'never' schaltet die eingebaute Leiste ab (nutze den Template-Slot rkPlayerTimeline für einen vollständig eigenen Ersatz).
timelineMinDurationSecondsnumber30Mindestdauer des Videos (in Sekunden), ab der timeline='auto' die eingebaute Leiste rendert. Kurze Clips in Schleife unter dieser Schwelle bekommen keine.
transitionDurationnumber300Dauer der Slide-Animation in ms
wheelDebounceMsnumber200Debounce für Mausrad-Ereignisse in ms

Outputs von RkReelPlayerOverlayComponent

OutputTypBeschreibung
apiReadyEventEmitter<ReelApi>Wird ausgelöst, sobald der Slider bereit ist, und stellt die imperative API bereit
closedEventEmitter<void>Wird ausgelöst, wenn der Player geschlossen wird
slideChangeEventEmitter<number>Wird ausgelöst, wenn sich der Index der aktiven Slide ändert
innerSlideChangeEventEmitter<{ outer: number; inner: number }>Wird ausgelöst, wenn sich der innere Medienindex des aktiven Beitrags ändert — bei der inneren Navigation und beim Aktivieren eines äußeren Beitrags, dann mit dessen aktuellem innerem Index (0 bei einem Beitrag mit nur einem Medium).

Inputs von RkReelPlayerUrlOverlayComponent

Nimmt alle Inputs von oben außer isOpen und initialIndex; an ihrer Stelle steht ein controller, dessen Position die Slide wählt. Outputs sind closed und slideChange.

InputTypStandardBeschreibung
controllerUrlStateControllerPflichtController aus createOverlayUrlState. Seine position entscheidet, ob der Player offen ist und welche Slide er zeigt; das Overlay schreibt beim Slide-Wechsel und beim Schließen über ihn zurück.

Das Interface MediaItem

FeldTypBeschreibung
idstringEindeutige Kennung des Medienelements
typeMediaTypeArt des Mediums: 'image' | 'video'
srcstringURL der Mediendatei
posterstring?URL des Vorschaubilds für Videos
aspectRationumberVerhältnis von Breite zu Höhe. Werte < 1 bedeuten vertikal (cover), > 1 horizontal (contain)

Kontext-Typen der Template-Slots

TypFelder
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 } in Sekunden — ein Eintrag von bufferedRanges()
PlayerLoadingContext{ $implicit: activeIndex, item, innerActiveIndex } — was das Template rkPlayerLoading bekommt; innerActiveIndex ist außerhalb eines verschachtelten Sliders null
PlayerErrorContext{ $implicit: activeIndex, item, innerActiveIndex } — was das Template rkPlayerError bekommt

CSS-Klassen

Alle CSS-Klassen sind einfache Klassen (nicht gekapselt), du kannst sie also mit spezifischeren Selektoren in einem Stylesheet ansprechen, das nach @reelkit/angular-reel-player/styles.css geladen wird. Für Änderungen an Farbe, Größe und z-index nimm lieber die CSS Custom Properties aus dem Abschnitt Theming unten — sie sind genau dafür gedacht.

KlasseKomponenteBeschreibung
.rk-reel-overlayOverlayFester Hintergrund im Vollbild (background, z-index)
.rk-reel-containerOverlayContainer des Players (position, overflow)
.rk-reel-loaderOverlayOverlay mit der Wellen-Animation beim Laden
.rk-reel-media-errorOverlayOverlay für den Fehlerzustand (zentriertes Icon + Text)
.rk-reel-media-error-textOverlayText der Fehlermeldung
.rk-reel-buttonControlsGemeinsamer runder Icon-Button (Schließen, Ton, Navigationspfeile)
.rk-reel-close-btnControlsButton zum Schließen
.rk-reel-sound-btnControlsButton für Ton an/aus
.rk-reel-nav-arrowsNavigationContainer der Pfeile nur am Desktop (bei 768px Breite und schmaler ausgeblendet)
.rk-reel-nav-btnNavigationEinzelner Pfeil für zurück/weiter
.rk-reel-slide-wrapperSlideHülle um Medium + Overlay
.rk-reel-slide-overlaySlideOverlayContainer des Overlays mit Verlauf
.rk-reel-slide-overlay-authorSlideOverlayZeile des Autors (Avatar + Name)
.rk-reel-slide-overlay-avatarSlideOverlayAvatarbild des Autors
.rk-reel-slide-overlay-nameSlideOverlayName des Autors
.rk-reel-slide-overlay-descriptionSlideOverlayText der Beschreibung
.rk-reel-slide-overlay-likesSlideOverlayZeile der Likes (Herz + Anzahl)
.rk-reel-video-containerVideoSlideHülle des Videos (background, overflow)
.rk-reel-video-elementVideoSlideDas Element <video>
.rk-reel-video-posterVideoSlidePosterbild (blendet beim Abspielen aus)
.rk-reel-video-loaderVideoSlideWellen-Animation beim Laden
.rk-reel-video-poster.rk-visibleVideoSlideZustands-Modifikator auf dem Poster, solange das Video pausiert ist oder lädt
.rk-reel-nested-indicatorNestedSliderPunkte zum Blättern unter Slides mit mehreren Medien (Position am Desktop und auf Touch-Geräten verschieden)
.rk-reel-nested-navNestedSliderPfeile des horizontalen Karussells (bei 768px Breite und schmaler ausgeblendet)
.rk-reel-nested-nav-nextNestedSliderPosition des inneren Pfeils für weiter
.rk-reel-nested-nav-prevNestedSliderPosition des inneren Pfeils für zurück
.rk-reel-nested-slider-innerNestedSliderWurzel des verschachtelten horizontalen Sliders
.rk-reel-timelineTimelineBarHülle der Spulleiste. Nutze die Klasse auf den Wurzeln eigener `rkPlayerTimeline`-Templates, um die Position bündig am unteren Rand, den Abstand für die Safe Area und den Freiraum zum Slide-Overlay auf Touch-Geräten zu übernehmen.
.rk-reel-timeline-trackTimelineBarSpur (noch nicht abgespielter Bereich)
.rk-reel-timeline-bufferedTimelineBarEbene der gepufferten Abschnitte
.rk-reel-timeline-fillTimelineBarFüllung für den abgespielten Fortschritt
.rk-reel-timeline-cursorTimelineBarGriff zum Spulen (schwebt über der Spur)

Theming

Jede Farbe, Größe, jeder z-index und jeder Übergang liegt in einer CSS Custom Property. Überschreibe eine oder mehrere auf :root (oder einem beliebigen Vorfahren des Overlays), um das Aussehen zu ändern, ohne den Quellcode der Komponente anzufassen. Die Tokens entsprechen denen der Pakete für React und Vue, Überschreibungen lassen sich also zwischen den Bindings übertragen.

TokenStandardSteuert
--rk-reel-overlay-bg#000Farbe des Hintergrunds im Vollbild
--rk-reel-overlay-z1000z-index des Overlays
--rk-reel-button-bgrgba(0, 0, 0, 0.5)Standard-Hintergrund der runden Buttons
--rk-reel-button-bg-hoverrgba(255, 255, 255, 0.1)Hintergrund der Navigationspfeile (und Grundzustand beim Hovern)
--rk-reel-button-bg-hover-strongrgba(255, 255, 255, 0.2)Hintergrund der Navigationspfeile beim Hovern
--rk-reel-button-fg#fffFarbe der Icons in den Buttons
--rk-reel-button-size44pxBreite / Höhe der Buttons
--rk-reel-button-radius50%border-radius der Buttons
--rk-reel-ui-z10z-index von Schließen / Ton / Navigation
--rk-reel-edge-padding16pxAbstand zum Rand für Schließen / Ton / Navigationspfeile
--rk-reel-nav-gap8pxAbstand zwischen übereinander liegenden Navigationspfeilen
--rk-reel-transition0.2sDauer des Übergangs beim Hovern
--rk-reel-loader-colorrgba(255, 255, 255, 0.12)Farbe des Verlaufs im Wellen-Loader
--rk-reel-loader-duration1.8sDauer der Animation des Wellen-Loaders
--rk-reel-error-fgrgba(255, 255, 255, 0.4)Farbe von Fehler-Icon und Fehlertext
--rk-reel-slide-overlay-bglinear-gradient(transparent, rgba(0, 0, 0, 0.7))Verlauf hinter der Bildunterschrift
--rk-reel-slide-overlay-padding48px 16px 16pxInnenabstand der Bildunterschrift
--rk-reel-slide-overlay-name-color#fffFarbe des Autorennamens
--rk-reel-video-bg#000Hintergrund für den Letterbox-Bereich hinter <video>
--rk-reel-video-loader-colorrgba(255, 255, 255, 0.15)Farbe des Schimmers beim Puffern des Videos
--rk-reel-nested-button-bgrgba(0, 0, 0, 0.5)Hintergrund der inneren Pfeile
--rk-reel-nested-button-size36pxGröße der inneren Pfeile
--rk-reel-nested-edge-padding12pxAbstand der inneren Pfeile zum Rand
--rk-reel-timeline-trackrgba(255, 255, 255, 0.22)Hintergrund der Spur (noch nicht abgespielter Bereich)
--rk-reel-timeline-bufferedrgba(255, 255, 255, 0.4)Farbe der gepufferten Abschnitte
--rk-reel-timeline-fill#fffFarbe der Füllung für den abgespielten Fortschritt
--rk-reel-timeline-cursor#fffFarbe des Griffs zum Spulen
--rk-reel-timeline-height3pxHöhe der Spur in Ruhe
--rk-reel-timeline-height-active6pxHöhe der Spur bei Hover / Fokus / Spulen
--rk-reel-timeline-cursor-width10pxBreite des Griffs in Ruhe
--rk-reel-timeline-cursor-width-active14pxBreite des Griffs beim Spulen
--rk-reel-timeline-cursor-height24pxHöhe des Griffs in Ruhe
--rk-reel-timeline-cursor-height-active32pxHöhe des Griffs beim Spulen
--rk-reel-timeline-transition0.15s ease-outAnimation, mit der Spur und Griff wachsen und schrumpfen

Füge das Snippet unten in ein Stylesheet ein, das nach @reelkit/angular-reel-player/styles.css geladen wird.

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;
}

Barrierefreiheit

Die Wurzel des Overlays ist ein modaler Dialog (role="dialog", aria-modal="true"). Setze den Input ariaLabel, um die Ansage für Screenreader zu ändern; Standard ist „Video player“. Jede Slide trägt role="group", aria-roledescription="slide" und aria-label="Slide N of M".

Das Overlay übernimmt beim Öffnen den Fokus und gibt ihn beim Schließen an den Auslöser zurück. Tab und Shift+Tab wandern durch die fokussierbaren Elemente darin; Fokus, der entwischt (Klick nach außen, Fokus aus dem Code), wird zurückgeholt. Umgesetzt mit captureFocusForReturn und createFocusTrap aus @reelkit/core.

Tastenkürzel

TasteAktion
ArrowUpVorherige Slide
ArrowDownNächste Slide
ArrowLeftVorheriges Medium (im verschachtelten Slider)
ArrowRightNächstes Medium (im verschachtelten Slider)
EscapePlayer schließen