Angular Reel Player
Vertikaler Medienplayer im Vollbild und im Stil von Instagram/TikTok für Angular, gebaut auf @reelkit/angular-reel-player.
Funktionen
Installation
npm install @reelkit/angular-reel-player @reelkit/angular lucide-angularRegistriere das Stylesheet einmal:
// 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.
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.
| Direktive | Kontext-Typ | Beschreibung |
|---|---|---|
| [rkPlayerControls] | PlayerControlsContext<T> | Eigene globale Leiste mit Bedienelementen (Schließen, Ton an/aus und so weiter); die Klasse der Direktive ist RkPlayerControlsDirective |
| [rkPlayerError] | PlayerErrorContext | Template-Slot für eine eigene Fehleranzeige; die Klasse der Direktive ist RkPlayerErrorDirective |
| [rkPlayerLoading] | PlayerLoadingContext | Template-Slot für eine eigene Ladeanzeige; die Klasse der Direktive ist RkPlayerLoadingDirective |
| [rkPlayerNavigation] | PlayerNavigationContext | Eigene Pfeile für zurück/weiter; die Klasse der Direktive ist RkPlayerNavigationDirective |
| [rkPlayerNestedNavigation] | PlayerNestedNavigationContext | Eigene Navigationspfeile für den inneren horizontalen Slider; die Klasse der Direktive ist RkPlayerNestedNavigationDirective |
| [rkPlayerNestedSlide] | PlayerNestedSlideContext | Eigener 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.
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">▲</button>
<button (click)="onNext()" [disabled]="activeIndex === count - 1">▼</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.
| Komponente | Selektor | Beschreibung |
|---|---|---|
RkMediaSlideComponent | rk-media-slide | Der 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. |
RkImageSlideComponent | rk-image-slide | Ein Bild mit object-fit: cover, das bis zum Laden sein poster zeigt und onReady, onWaiting oder onError aufruft. |
RkVideoSlideComponent | rk-video-slide | Ein Video auf dem gemeinsamen <video>-Element. Es braucht den SoundStateService, den das Overlay bereitstellt, also rendere es nur innerhalb des Players. |
RkNestedSliderComponent | rk-nested-slider | Der horizontale Slider mit Punkten als Indikator, für ein Inhaltselement mit mehreren Einträgen in media. |
RkSlideOverlayComponent | rk-slide-overlay | Das 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.
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:
| Callback | Wann aufrufen |
|---|---|
onReady | Das Bild ist geladen oder das Video spielt. Setzt Lade- und Fehlerzustand zurück. |
onWaiting | Das Video puffert mitten in der Wiedergabe. Zeigt die Ladeanzeige. |
onError | Der Inhalt konnte nicht geladen werden. Zeigt das Fehler-Overlay und merkt sich die URL als kaputt. |
<!-- 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:
<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.
<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.
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.
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()"
}| Mitglied | Typ | Beschreibung |
|---|---|---|
muted() | Signal<boolean> | Ob der Player gerade stumm ist |
disabled() | Signal<boolean> | Wahr, wenn die aktive Slide kein Video hat oder gerade wechselt |
toggle() | () => void | Schaltet 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.
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üssel | Format | Trägt |
|---|---|---|
urlIndexKey(…) | ?reel=3 | Nur den vertikalen Beitrag. |
urlIndexTwoAxisKey(…) | ?reel=3.2 | Den 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.
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.2Stabile 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.
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.
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.
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
locateAsyncläuft, bleibt der Player geschlossen und der Parameter unberührt, der Deep Link übersteht also das Laden.nulloder 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>.
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
| Input | Typ | Standard | Beschreibung |
|---|---|---|---|
ariaLabel | string | 'Video player' | Barrierefreie Bezeichnung für die Region des Dialogs |
aspectRatio | number | undefined | undefined | Verhä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. |
content | T[] (extends BaseContentItem) | Pflicht | Array der Inhalte, die der Player anzeigt |
enableNavKeys | boolean | true | Schaltet die Navigation mit den Pfeiltasten ein |
enableWheel | boolean | true | Schaltet die Navigation mit dem Mausrad ein |
initialIndex | number | 0 | Nullbasierter Index des zuerst sichtbaren Elements |
initialInnerIndex | number | 0 | Innerer 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. |
isOpen | boolean | Pflicht | Steuert die Sichtbarkeit des Overlays; bei false wird das Overlay aus dem DOM entfernt |
loop | boolean | false | Schaltet die Endlosschleife zwischen den Slides ein |
swipeDistanceFactor | number | 0.12 | Mindestanteil der Wischstrecke, der einen Slide-Wechsel auslöst |
timeline | TimelineMode ('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). |
timelineMinDurationSeconds | number | 30 | Mindestdauer des Videos (in Sekunden), ab der timeline='auto' die eingebaute Leiste rendert. Kurze Clips in Schleife unter dieser Schwelle bekommen keine. |
transitionDuration | number | 300 | Dauer der Slide-Animation in ms |
wheelDebounceMs | number | 200 | Debounce für Mausrad-Ereignisse in ms |
Outputs von RkReelPlayerOverlayComponent
| Output | Typ | Beschreibung |
|---|---|---|
apiReady | EventEmitter<ReelApi> | Wird ausgelöst, sobald der Slider bereit ist, und stellt die imperative API bereit |
closed | EventEmitter<void> | Wird ausgelöst, wenn der Player geschlossen wird |
slideChange | EventEmitter<number> | Wird ausgelöst, wenn sich der Index der aktiven Slide ändert |
innerSlideChange | EventEmitter<{ 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.
| Input | Typ | Standard | Beschreibung |
|---|---|---|---|
controller | UrlStateController | Pflicht | Controller 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
| Feld | Typ | Beschreibung |
|---|---|---|
id | string | Eindeutige Kennung des Medienelements |
type | MediaType | Art des Mediums: 'image' | 'video' |
src | string | URL der Mediendatei |
poster | string? | URL des Vorschaubilds für Videos |
aspectRatio | number | Verhältnis von Breite zu Höhe. Werte < 1 bedeuten vertikal (cover), > 1 horizontal (contain) |
Kontext-Typen der Template-Slots
| Typ | Felder |
|---|---|
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.
| Klasse | Komponente | Beschreibung |
|---|---|---|
.rk-reel-overlay | Overlay | Fester Hintergrund im Vollbild (background, z-index) |
.rk-reel-container | Overlay | Container des Players (position, overflow) |
.rk-reel-loader | Overlay | Overlay mit der Wellen-Animation beim Laden |
.rk-reel-media-error | Overlay | Overlay für den Fehlerzustand (zentriertes Icon + Text) |
.rk-reel-media-error-text | Overlay | Text der Fehlermeldung |
.rk-reel-button | Controls | Gemeinsamer runder Icon-Button (Schließen, Ton, Navigationspfeile) |
.rk-reel-close-btn | Controls | Button zum Schließen |
.rk-reel-sound-btn | Controls | Button für Ton an/aus |
.rk-reel-nav-arrows | Navigation | Container der Pfeile nur am Desktop (bei 768px Breite und schmaler ausgeblendet) |
.rk-reel-nav-btn | Navigation | Einzelner Pfeil für zurück/weiter |
.rk-reel-slide-wrapper | Slide | Hülle um Medium + Overlay |
.rk-reel-slide-overlay | SlideOverlay | Container des Overlays mit Verlauf |
.rk-reel-slide-overlay-author | SlideOverlay | Zeile des Autors (Avatar + Name) |
.rk-reel-slide-overlay-avatar | SlideOverlay | Avatarbild des Autors |
.rk-reel-slide-overlay-name | SlideOverlay | Name des Autors |
.rk-reel-slide-overlay-description | SlideOverlay | Text der Beschreibung |
.rk-reel-slide-overlay-likes | SlideOverlay | Zeile der Likes (Herz + Anzahl) |
.rk-reel-video-container | VideoSlide | Hülle des Videos (background, overflow) |
.rk-reel-video-element | VideoSlide | Das Element <video> |
.rk-reel-video-poster | VideoSlide | Posterbild (blendet beim Abspielen aus) |
.rk-reel-video-loader | VideoSlide | Wellen-Animation beim Laden |
.rk-reel-video-poster.rk-visible | VideoSlide | Zustands-Modifikator auf dem Poster, solange das Video pausiert ist oder lädt |
.rk-reel-nested-indicator | NestedSlider | Punkte zum Blättern unter Slides mit mehreren Medien (Position am Desktop und auf Touch-Geräten verschieden) |
.rk-reel-nested-nav | NestedSlider | Pfeile des horizontalen Karussells (bei 768px Breite und schmaler ausgeblendet) |
.rk-reel-nested-nav-next | NestedSlider | Position des inneren Pfeils für weiter |
.rk-reel-nested-nav-prev | NestedSlider | Position des inneren Pfeils für zurück |
.rk-reel-nested-slider-inner | NestedSlider | Wurzel des verschachtelten horizontalen Sliders |
.rk-reel-timeline | TimelineBar | Hü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-track | TimelineBar | Spur (noch nicht abgespielter Bereich) |
.rk-reel-timeline-buffered | TimelineBar | Ebene der gepufferten Abschnitte |
.rk-reel-timeline-fill | TimelineBar | Füllung für den abgespielten Fortschritt |
.rk-reel-timeline-cursor | TimelineBar | Griff 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.
| Token | Standard | Steuert |
|---|---|---|
--rk-reel-overlay-bg | #000 | Farbe des Hintergrunds im Vollbild |
--rk-reel-overlay-z | 1000 | z-index des Overlays |
--rk-reel-button-bg | rgba(0, 0, 0, 0.5) | Standard-Hintergrund der runden Buttons |
--rk-reel-button-bg-hover | rgba(255, 255, 255, 0.1) | Hintergrund der Navigationspfeile (und Grundzustand beim Hovern) |
--rk-reel-button-bg-hover-strong | rgba(255, 255, 255, 0.2) | Hintergrund der Navigationspfeile beim Hovern |
--rk-reel-button-fg | #fff | Farbe der Icons in den Buttons |
--rk-reel-button-size | 44px | Breite / Höhe der Buttons |
--rk-reel-button-radius | 50% | border-radius der Buttons |
--rk-reel-ui-z | 10 | z-index von Schließen / Ton / Navigation |
--rk-reel-edge-padding | 16px | Abstand zum Rand für Schließen / Ton / Navigationspfeile |
--rk-reel-nav-gap | 8px | Abstand zwischen übereinander liegenden Navigationspfeilen |
--rk-reel-transition | 0.2s | Dauer des Übergangs beim Hovern |
--rk-reel-loader-color | rgba(255, 255, 255, 0.12) | Farbe des Verlaufs im Wellen-Loader |
--rk-reel-loader-duration | 1.8s | Dauer der Animation des Wellen-Loaders |
--rk-reel-error-fg | rgba(255, 255, 255, 0.4) | Farbe von Fehler-Icon und Fehlertext |
--rk-reel-slide-overlay-bg | linear-gradient(transparent, rgba(0, 0, 0, 0.7)) | Verlauf hinter der Bildunterschrift |
--rk-reel-slide-overlay-padding | 48px 16px 16px | Innenabstand der Bildunterschrift |
--rk-reel-slide-overlay-name-color | #fff | Farbe des Autorennamens |
--rk-reel-video-bg | #000 | Hintergrund für den Letterbox-Bereich hinter <video> |
--rk-reel-video-loader-color | rgba(255, 255, 255, 0.15) | Farbe des Schimmers beim Puffern des Videos |
--rk-reel-nested-button-bg | rgba(0, 0, 0, 0.5) | Hintergrund der inneren Pfeile |
--rk-reel-nested-button-size | 36px | Größe der inneren Pfeile |
--rk-reel-nested-edge-padding | 12px | Abstand der inneren Pfeile zum Rand |
--rk-reel-timeline-track | rgba(255, 255, 255, 0.22) | Hintergrund der Spur (noch nicht abgespielter Bereich) |
--rk-reel-timeline-buffered | rgba(255, 255, 255, 0.4) | Farbe der gepufferten Abschnitte |
--rk-reel-timeline-fill | #fff | Farbe der Füllung für den abgespielten Fortschritt |
--rk-reel-timeline-cursor | #fff | Farbe des Griffs zum Spulen |
--rk-reel-timeline-height | 3px | Höhe der Spur in Ruhe |
--rk-reel-timeline-height-active | 6px | Höhe der Spur bei Hover / Fokus / Spulen |
--rk-reel-timeline-cursor-width | 10px | Breite des Griffs in Ruhe |
--rk-reel-timeline-cursor-width-active | 14px | Breite des Griffs beim Spulen |
--rk-reel-timeline-cursor-height | 24px | Höhe des Griffs in Ruhe |
--rk-reel-timeline-cursor-height-active | 32px | Höhe des Griffs beim Spulen |
--rk-reel-timeline-transition | 0.15s ease-out | Animation, 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.
/* 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
| Taste | Aktion |
|---|---|
ArrowUp | Vorherige Slide |
ArrowDown | Nächste Slide |
ArrowLeft | Vorheriges Medium (im verschachtelten Slider) |
ArrowRight | Nächstes Medium (im verschachtelten Slider) |
Escape | Player schließen |