Angular Reel Player
面向 Angular 的全屏 Instagram / TikTok 风格竖向媒体播放器,基于 @reelkit/angular-reel-player.
特性
安装
npm install @reelkit/angular-reel-player @reelkit/angular lucide-angular注册一次样式表:
// 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 数组。
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);
}
}覆盖层渲染在哪里
React 和 Vue 播放器会把覆盖层移到 document.body 末尾,而 rk-reel-player-overlay 就渲染在你放置它的位置,并用 position: fixed 铺满屏幕。不要把它放在带有 transform、perspective、filter 或 will-change: transform 的祖先元素里:这样的祖先会成为覆盖层的包含块,播放器的尺寸会跟随它而不是视口,并被它的 overflow 裁剪。创建了自身层叠上下文的祖先,也会限制 --rk-reel-overlay-z 能把播放器抬到多高。反过来,设置在任意祖先上的 --rk-reel-* 令牌都能作用到播放器。
模板插槽
六个模板插槽指令让你定制播放器界面的每个部分。每个都会收到带完整类型的上下文对象。只需提供你想覆盖的插槽 —— 其余的沿用默认实现。
| 指令 | 上下文类型 | 说明 |
|---|---|---|
| [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> | 自定义播放时间轴条。只有在门控(timeline 模式 + 最小时长)会渲染默认条时才渲染(同样是 auto/always/never 的逻辑)。指令类为 RkPlayerTimelineDirective |
PLAYER_TEMPLATE_SLOT_DIRECTIVES 汇集了以上所有指令,组件可以一次性把它们展开到 imports 中。
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);
}
}默认幻灯片组件
默认幻灯片由这些部件构成。每个都是独立组件;在你自己的模板里使用其中一个,即可保留部分默认外观。
| 组件 | 选择器 | 说明 |
|---|---|---|
RkMediaSlideComponent | rk-media-slide | 默认幻灯片。根据内容条目的 media 渲染 rk-image-slide、rk-video-slide 或 rk-nested-slider;在 rkPlayerSlide 模板里使用它,可以保留默认媒体并在上面叠加你自己的图层。 |
RkImageSlideComponent | rk-image-slide | 单张图片,使用 object-fit: cover,加载完成前显示其 poster,并调用 onReady、onWaiting 或 onError。 |
RkVideoSlideComponent | rk-video-slide | 在共享 <video> 元素上播放的单个视频。它需要浮层提供的 SoundStateService,所以只能在播放器内部渲染。 |
RkNestedSliderComponent | rk-nested-slider | 带指示圆点的横向滑动器,用于包含多个 media 条目的内容条目。 |
RkSlideOverlayComponent | rk-slide-overlay | 默认的底部浮层:根据 author、description 和 likes 输入显示作者头像和名字、描述以及点赞数。在 rkPlayerSlideOverlay 模板里使用它,可保留默认外观。 |
自定义时间轴
rkPlayerTimeline 模板插槽只有在浮层的门控规则会渲染默认条时才会调用(同样是 timeline 模式 + timelineMinDurationSeconds),所以不必自己重写一遍。在你的根元素上复用 .rk-reel-timeline 类,即可继承贴底定位、安全区内边距和触摸设备上的留白。在你的拖动轨道上调用 state.bindInteractions(el) 即可接上指针 + 键盘拖动。
嵌套滑动器(多媒体条目)
When a ContentItem 包含多个 media 条目时,播放器会把它们渲染成一个横向的嵌套滑动器(Instagram 轮播风格)。用 rkPlayerNestedSlide 插槽即可自定义内层幻灯片的内容。
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 标记为损坏缓存起来。 |
<!-- 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>自定义加载与错误界面
用自定义模板替换默认的波浪加载动画和错误图标:
<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 模板指令;它的上下文会暴露一个 timelineState ,其数据来自底层的 TimelineController.
<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 模板内部使用它来包裹或增强默认条;只有在提供了该服务的使用方内部才可以单独使用。
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 级提供。默认的声音按钮会注入它,控件模板插槽的上下文里也会暴露它。作为浮层 children 的自定义控件也可以注入它来直接访问。
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]控制,因此每个组件都只有一个打开状态的驱动源。
带路由的应用会传入基于 Router 的适配器,让 Router 始终是导航的唯一真相来源 —— 绕过它直接写历史会让 location 过期,下一次导航就会把参数丢掉。 createRouterUrlAdapter from @reelkit/angular/ng-router-url-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),
});
}- 打开时压入 一条 历史记录。滑动信息流则是 替换 它,因此滑 N 次也不会多出记录,退一步永远就是离开播放器。返回键关闭播放器,不会逐张后退。
- 只有在应用内部打开播放器时返回键才会关闭它 —— 因为那次链接压入了一条记录。在新标签页里直接打开的分享链接背后没有历史,浏览器返回会离开站点;这时用 ✕ 按钮或 Escape 就地移除参数并留在页面上。
- 深链
?reel=3会在加载时直接把播放器打开到那一张。 - 指向不存在幻灯片的参数 —— 过期的书签、手改的值 —— 会从 URL 中移除,而不是让地址栏继续声称一张打不开的幻灯片。
- URL 的深度取决于控制器用的 key:单轴只表示帖子,双轴(
urlIndexTwoAxisKey)还会携带多媒体帖子的内层图片索引。每个应用只选一种;两种形态不会互相解码。
完整的 createOverlayUrlState 选项见 Angular API 参考.
一条轴还是两条 —— 自己决定 URL 的深度
同一个 RkReelPlayerUrlOverlayComponent 两种形态都能驱动;它在运行时根据控制器的 position 自行判别,所以没有 mode 输入。在构建控制器时选好 key 即可:
| Key | URL 形态 | 携带内容 |
|---|---|---|
urlIndexKey(…) | ?reel=3 | 只有竖向的帖子。 |
urlIndexTwoAxisKey(…) | ?reel=3.2 | 帖子 和 轮播内层媒体索引。 |
两种形态是刻意区分开的 —— 双轴 key 严格使用点分隔(3.0,绝不会是裸的 3),因此单轴链接不会被错误解码。也正因如此,应用在两种 key 之间切换会让此前分享出去的链接全部失效。选定一种形态就别再改。
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来寻址,扫描当前的信息流 —— 一次调用就覆盖了常见场景。
protected readonly reel = createOverlayUrlState({
param: 'reel',
...urlStableIdKey({ items: () => this.loaded() }),
});传入 hashCodec: base64UrlCodec 即可把 URL 中的 id 做 base64url 编码 —— 这是可逆的混淆,不是加密哈希。
想按别的字段(比如 slug)来寻址,或者用 locateAsync给无限信息流翻页,就自己构建 codec/locator 。这是两件事: codec 把身份写进 URL, locator 则负责找到这个身份在哪。
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来寻址?那就不必手写编解码器和定位器 —— 直接把 locateAsync 传给 urlStableIdKey({ items, locateAsync }) (未命中时它会去拉取,然后返回索引)。下面更完整的写法是给按别的字段寻址、或者需要完全掌控的场景准备的。
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或请求被拒绝则会移除参数。 - 如果结果在 URL 已经变化、播放器已关闭或组件已卸载之后才到达,就会被丢弃 —— 慢请求不能打开一张没人要的幻灯片。
- 等待期间什么都不渲染;加载状态本来就归页面自己管,所以请渲染你自己的骨架屏。
- 没有超时机制 —— 播放器无从得知信息流有多长。分页用尽时请以
null结束,否则浮层会一直关着。
自定义数据类型
扩展 BaseContentItem 即可使用你自己的领域模型。这个组件是泛型的: 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[] = [];
}RkReelPlayerOverlayComponent 输入
| 输入 | 类型 | 默认值 | 说明 |
|---|---|---|---|
ariaLabel | string | 'Video player' | 对话框区域的无障碍标签 |
aspectRatio | number | undefined | undefined | 桌面端容器的宽高比。默认 9/16,为零、为负或非有限的值也会被替换为它。宽度在 768px 及以下时,播放器占满整个视口。 |
content | T[] (extends BaseContentItem) | 必填 | 播放器中要展示的内容条目数组 |
enableNavKeys | boolean | true | 启用键盘方向键导航 |
enableWheel | boolean | true | 启用鼠标滚轮导航 |
initialIndex | number | 0 | 初始可见条目的索引(从 0 开始) |
initialInnerIndex | number | 0 | 打开时定位的内层媒体索引,只对最初可见的那条帖子生效 —— 让双轴 URL 能直达多 媒体帖子里的某一张图。用户一开始导航就会忽略它。 |
isOpen | boolean | 必填 | 控制浮层显示;为 false 时浮层会从 DOM 中移除 |
loop | boolean | false | 在幻灯片之间启用无限循环 |
swipeDistanceFactor | number | 0.12 | 触发切换所需的最小滑动距离占比 |
timeline | TimelineMode ('auto' | 'always' | 'never') | 'auto' | 内置播放进度条的显示策略。'auto' 只在视频时长超过 timelineMinDurationSeconds 时渲染;'always' 只要当前幻灯片有视频就渲染;'never' 关闭内置进度条(需要完全自定义时使用 rkPlayerTimeline 模板插槽)。 |
timelineMinDurationSeconds | number | 30 | timeline='auto' 时渲染内置进度条所需的最短视频时长(秒)。低于该阈值的循环短片不显示进度条。 |
transitionDuration | number | 300 | 幻灯片动画时长(毫秒) |
wheelDebounceMs | number | 200 | 滚轮事件的防抖时长(毫秒) |
RkReelPlayerOverlayComponent 输出
| 输出 | 类型 | 说明 |
|---|---|---|
apiReady | EventEmitter<ReelApi> | 滑动器就绪时发出一次,同时暴露命令式 API |
closed | EventEmitter<void> | 播放器关闭时发出 |
slideChange | EventEmitter<number> | 当前幻灯片索引变化时发出 |
innerSlideChange | EventEmitter<{ outer: number; inner: number }> | 当前帖子的内层媒体索引变化时发出 —— 包括内层导航和外层激活,报告被激活帖子当前的内层索引(单媒体帖子为 0)。 |
RkReelPlayerUrlOverlayComponent 输入
接受上面所有输入,除了 isOpen 和 initialIndex,它被 controller ,由它的 position 决定打开哪一张。输出 closed 和 slideChange.
| 输入 | 类型 | 默认值 | 说明 |
|---|---|---|---|
controller | UrlStateController | 必填 | 来自 createOverlayUrlState的控制器。它的 position 决定播放器是否打开、显示哪一张;浮层会在切换幻灯片和关闭时通过它写回。 |
MediaItem 接口
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 媒体条目的唯一标识 |
type | MediaType | 媒体类型:'image' | 'video' |
src | string | 媒体资源的 URL |
poster | string? | 视频条目的封面缩略图 URL |
aspectRatio | number | 宽高比。小于 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() 中的一项 |
PlayerLoadingContext | { $implicit: activeIndex, item, innerActiveIndex } —— rkPlayerLoading 模板收到的上下文;不在内层滑动器中时 innerActiveIndex 为 null |
PlayerErrorContext | { $implicit: activeIndex, item, innerActiveIndex } —— rkPlayerError 模板收到的上下文 |
CSS 类名
所有 CSS 类名都是普通类名(没有 scoped),因此可以在 @reelkit/angular-reel-player/styles.css之后加载的样式表里用更高优先级的选择器覆盖它们。若只是改颜色、尺寸和 z-index,请优先使用下面 主题定制 一节记录的 CSS 自定义属性 —— 它们正是为此设计的。
| 类名 | 组件 | 说明 |
|---|---|---|
.rk-reel-overlay | Overlay | 固定的全屏背景层(背景、z-index) |
.rk-reel-container | Overlay | 播放器容器(定位、溢出) |
.rk-reel-loader | Overlay | 波浪加载动画浮层 |
.rk-reel-media-error | Overlay | 错误状态浮层(居中图标 + 文字) |
.rk-reel-media-error-text | Overlay | 错误信息文字 |
.rk-reel-button | 控制内容 | 共用的圆形图标按钮(关闭、声音、导航箭头) |
.rk-reel-close-btn | 控制内容 | 关闭按钮 |
.rk-reel-sound-btn | 控制内容 | 声音开关按钮 |
.rk-reel-nav-arrows | 导航 | 仅桌面端的箭头容器(宽度在 768px 及以下时隐藏) |
.rk-reel-nav-btn | 导航 | 单个上一张 / 下一张导航箭头 |
.rk-reel-slide-wrapper | Slide | 媒体 + 浮层的包装层 |
.rk-reel-slide-overlay | SlideOverlay | 渐变浮层容器 |
.rk-reel-slide-overlay-author | SlideOverlay | 作者行(头像 + 名称) |
.rk-reel-slide-overlay-avatar | SlideOverlay | 作者头像图片 |
.rk-reel-slide-overlay-name | SlideOverlay | 作者名称文字 |
.rk-reel-slide-overlay-description | SlideOverlay | 描述文字 |
.rk-reel-slide-overlay-likes | SlideOverlay | 点赞行(爱心 + 数量) |
.rk-reel-video-container | VideoSlide | 视频包装层(背景、溢出) |
.rk-reel-video-element | VideoSlide | <video> 元素 |
.rk-reel-video-poster | VideoSlide | 封面图(播放时淡出) |
.rk-reel-video-loader | VideoSlide | 波浪加载动画 |
.rk-reel-video-poster.rk-visible | VideoSlide | 视频暂停 / 加载时施加在封面图上的状态修饰类 |
.rk-reel-nested-indicator | NestedSlider | 多媒体幻灯片下方的圆点分页(桌面端与触摸端位置不同) |
.rk-reel-nested-nav | NestedSlider | 横向轮播箭头(宽度在 768px 及以下时隐藏) |
.rk-reel-nested-nav-next | NestedSlider | 嵌套的下一张箭头位置 |
.rk-reel-nested-nav-prev | NestedSlider | 嵌套的上一张箭头位置 |
.rk-reel-nested-slider-inner | NestedSlider | 嵌套横向滑动器的根节点 |
.rk-reel-timeline | TimelineBar | 拖动条包装层。在自定义的 `rkPlayerTimeline` 模板根元素上复用它,即可继承贴底定位、安全区内边距,以及触摸设备上为幻灯片浮层预留的空间。 |
.rk-reel-timeline-track | TimelineBar | 轨道(未播放区域) |
.rk-reel-timeline-buffered | TimelineBar | 缓冲分段层 |
.rk-reel-timeline-fill | TimelineBar | 已播放进度填充 |
.rk-reel-timeline-cursor | TimelineBar | 拖动手柄(浮在轨道上方) |
主题定制
每一个颜色、尺寸、z-index 和过渡都放在 CSS 自定义属性里。在 :root (或浮层的任意祖先元素)上覆盖,即可在不改组件源码的情况下换主题。这些变量与 React 和 Vue 包保持一致,因此覆盖样式可以在不同框架绑定之间通用。
| 变量 | 默认值 | 控制内容 |
|---|---|---|
--rk-reel-overlay-bg | #000 | 全屏背景颜色 |
--rk-reel-overlay-z | 1000 | 浮层 z-index |
--rk-reel-button-bg | rgba(0, 0, 0, 0.5) | 圆形按钮的默认背景 |
--rk-reel-button-bg-hover | rgba(255, 255, 255, 0.1) | 导航箭头背景(以及基础悬停状态) |
--rk-reel-button-bg-hover-strong | rgba(255, 255, 255, 0.2) | 导航箭头悬停背景 |
--rk-reel-button-fg | #fff | 按钮图标颜色 |
--rk-reel-button-size | 44px | 按钮宽高 |
--rk-reel-button-radius | 50% | 按钮圆角半径 |
--rk-reel-ui-z | 10 | 关闭、声音和导航按钮的 z-index |
--rk-reel-edge-padding | 16px | 关闭、声音和导航箭头距边缘的内缩 |
--rk-reel-nav-gap | 8px | 堆叠导航箭头之间的间距 |
--rk-reel-transition | 0.2s | 悬停过渡时长 |
--rk-reel-loader-color | rgba(255, 255, 255, 0.12) | 波浪加载渐变颜色 |
--rk-reel-loader-duration | 1.8s | 波浪加载动画时长 |
--rk-reel-error-fg | rgba(255, 255, 255, 0.4) | 错误图标和文字颜色 |
--rk-reel-slide-overlay-bg | linear-gradient(transparent, rgba(0, 0, 0, 0.7)) | 说明文字遮罩渐变 |
--rk-reel-slide-overlay-padding | 48px 16px 16px | 说明文字内边距 |
--rk-reel-slide-overlay-name-color | #fff | 作者名称颜色 |
--rk-reel-video-bg | #000 | <video> 后方的留黑背景 |
--rk-reel-video-loader-color | rgba(255, 255, 255, 0.15) | 视频缓冲闪烁效果颜色 |
--rk-reel-nested-button-bg | rgba(0, 0, 0, 0.5) | 内层箭头背景 |
--rk-reel-nested-button-size | 36px | 内层箭头尺寸 |
--rk-reel-nested-edge-padding | 12px | 内层箭头距边缘的内缩 |
--rk-reel-timeline-track | rgba(255, 255, 255, 0.22) | 轨道背景(未播放区域) |
--rk-reel-timeline-buffered | rgba(255, 255, 255, 0.4) | 已缓冲片段颜色 |
--rk-reel-timeline-fill | #fff | 已播放进度的填充颜色 |
--rk-reel-timeline-cursor | #fff | 拖动手柄胶囊颜色 |
--rk-reel-timeline-height | 3px | 轨道静止时的高度 |
--rk-reel-timeline-height-active | 6px | 悬停、聚焦或拖动时的轨道高度 |
--rk-reel-timeline-cursor-width | 10px | 拖动胶囊静止时的宽度 |
--rk-reel-timeline-cursor-width-active | 14px | 拖动时胶囊的宽度 |
--rk-reel-timeline-cursor-height | 24px | 拖动胶囊静止时的高度 |
--rk-reel-timeline-cursor-height-active | 32px | 拖动时胶囊的高度 |
--rk-reel-timeline-transition | 0.15s ease-out | 轨道与胶囊的伸缩动画 |
把下面这段放进在 @reelkit/angular-reel-player/styles.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"). Set the ariaLabel 输入可以改变屏幕阅读器的播报内容,默认是 “Video player”。每张幻灯片都带有 role="group", aria-roledescription="slide"、 aria-label="第 N 张,共 M 张".
浮层打开时捕获焦点,关闭时把焦点还给触发元素。Tab 和 Shift+Tab 在内部的可聚焦元素之间循环;跑出去的焦点(点击外部、程序化聚焦)会被拉回来。实现基于 captureFocusForReturn 和 createFocusTrap from @reelkit/core.
键盘快捷键
| Key | 作用 |
|---|---|
ArrowUp | 上一张幻灯片 |
ArrowDown | 下一张幻灯片 |
ArrowLeft | 上一个媒体(内层滑动器中) |
ArrowRight | 下一个媒体(内层滑动器中) |
Escape | 关闭播放器 |