Skip to content

Commit 8e916a7

Browse files
committed
Added enhanced playlist support and overlays
1 parent 6796dee commit 8e916a7

File tree

12 files changed

+1216
-28
lines changed

12 files changed

+1216
-28
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
All notable changes to this project will be documented in this file.
44

5-
## [1.0.8] - 2025-09-18
5+
## [1.0.9]
6+
- Added `skip-intro-start` and `skip-intro-end` and `next-episode-button-overlay` buttons support.
7+
- Fixed shoppable video hotspot issue
8+
- Added `player-buttons` slot
9+
10+
## [1.0.8]
611

712
### New & Improved: Playlist
813
- Added playlist APIs: `addPlaylist(playlist)`, `next()`, `previous()`, `selectEpisodeByPlaybackId(playbackId)`

PLAYLIST_DEVELOPER_GUIDE.md

Lines changed: 322 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,324 @@
1-
## FastPix Player - Playlist Developer Guide
1+
# FastPix Player Playlist Developer Guide
22

3-
For the complete and up-to-date playlist documentation (APIs, attributes, events, and examples), please refer to the Player Documentation.
3+
This guide explains how to build, control, and customize Playlists using FastPix Player. You'll learn how to use the built-in playlist UI, replace it with your own panel, and integrate with playlist events for advanced behaviors.
44

5-
Quick notes:
6-
- Use `addPlaylist([...])` to load items (each requires `playbackId`).
7-
- Optional: `default-playback-id`, `loop-next`, `hide-default-playlist-panel`.
8-
- Events: `playbackidchange`, `playlisttoggle`.
9-
- Lightweight teardown before switching sources. Usually not required for standard navigation (handled internally), but useful if you implement custom source-switching flows.
10-
- Advanced usage (custom panel via `slot="playlist-panel"`, `customNext`, `customPrev`) is covered in the Player Documentation.
5+
## What You Can Build
6+
7+
- Load and play videos with `addPlaylist([...])`.
8+
- Navigate with `next()` / `previous()` or jump with `selectEpisodeByPlaybackId(id)`.
9+
- Start from a default item via `default-playback-id`.
10+
- Hide the built-in UI and build your own menu with the `playlist-panel` slot.
11+
- Listen to `playbackidchange` and `playlisttoggle` to sync UI, route, or send analytics.
12+
- Inject custom navigation logic with `customNext(ctx)` / `customPrev(ctx)`.
13+
14+
## Core Concepts
15+
16+
- **Playlist Item**: JSON describing one video (requires `playbackId`).
17+
- **Default Playlist Panel**: The built-in playlist UI inside the player.
18+
- **Custom Playlist Panel**: Your own UI rendered with the `playlist-panel` slot.
19+
- **Events**: Emitted by the player to reflect state changes and coordinate UI.
20+
21+
## API Reference (Player Methods)
22+
23+
| Method | Description | Parameters |
24+
|--------|-------------|------------|
25+
| `addPlaylist(playlist: Array<Item>)` | Load a playlist (each item needs playbackId). | `playlist: Array<Item>` |
26+
| `next()` | Navigate to the next item. ||
27+
| `previous()` | Navigate to the previous item. ||
28+
| `selectEpisodeByPlaybackId(playbackId: string)` | Jump to a specific item. | `playbackId: string` |
29+
| `destroy()` | Teardown before custom source switching (advanced use only). ||
30+
| `customNext(ctx)` | Run custom code before continuing to next. | `ctx.next()` must be called |
31+
| `customPrev(ctx)` | Run custom code before continuing to previous. | `ctx.previous()` must be called |
32+
33+
## Playlist Item Shape
34+
35+
```typescript
36+
interface PlaylistItem {
37+
playbackId: string; // required
38+
title?: string;
39+
description?: string;
40+
thumbnail?: string;
41+
token?: string; // optional per-item token
42+
drmToken?: string; // optional per-item DRM token
43+
customDomain?: string; // optional per-item custom domain
44+
duration?: number | string;
45+
}
46+
```
47+
48+
## Attributes
49+
50+
| Attribute | Description |
51+
|-----------|-------------|
52+
| `default-playback-id` | Pre-select starting item. |
53+
| `hide-default-playlist-panel` | Hide built-in panel; use `playlist-panel` slot. |
54+
| `loop-next` | Auto-advance behavior when current item ends. |
55+
| Common attributes: `custom-domain`, `auto-play`, `aspect-ratio`. | |
56+
57+
## Events
58+
59+
| Event | Description | Detail Object |
60+
|-------|-------------|--------------|
61+
| `playbackidchange` | Fired when active playback changes. | `{ playbackId, isFromPlaylist, currentIndex, totalItems, status }` |
62+
| `playlisttoggle` | Fired to open/close playlist panel (slot mode). | `{ open, hasPlaylist, currentIndex, totalItems, playbackId }` |
63+
64+
## Quick Start
65+
66+
### Default Panel
67+
68+
```html
69+
<fastpix-player id="player">
70+
<!-- Built-in playlist panel will show automatically -->
71+
</fastpix-player>
72+
73+
<script>
74+
const player = document.getElementById('player');
75+
const episodes = [
76+
{
77+
playbackId: "episode-1-id",
78+
title: "Episode 1: The Beginning",
79+
description: "Our story starts with a mysterious event.",
80+
thumbnail: "https://example.com/thumb1.jpg"
81+
},
82+
{
83+
playbackId: "episode-2-id",
84+
title: "Episode 2: The Journey Continues",
85+
description: "The heroes set out on their journey.",
86+
thumbnail: "https://example.com/thumb2.jpg"
87+
}
88+
];
89+
90+
// Load playlist
91+
player.addPlaylist(episodes);
92+
93+
// Listen for changes
94+
player.addEventListener('playbackidchange', (e) => {
95+
console.log('Now playing:', e.detail.playbackId);
96+
});
97+
</script>
98+
```
99+
100+
### Custom Panel
101+
102+
```html
103+
<fastpix-player id="player" hide-default-playlist-panel>
104+
<div slot="playlist-panel" id="myPlaylistPanel" hidden>
105+
<style>
106+
/* Panel container (centered overlay, OTT sizing) */
107+
#myPlaylistPanel {
108+
position: absolute;
109+
top: 50%; left: 50%; transform: translate(-50%, -50%);
110+
width: clamp(320px, 42vw, 520px);
111+
max-height: min(78vh, 760px);
112+
background: #fff; color: #100023;
113+
border: 1px solid rgba(16,0,35,0.12);
114+
border-radius: 16px;
115+
box-shadow: 0 20px 60px rgba(0,0,0,0.35);
116+
padding: 12px 12px 8px;
117+
overflow: hidden;
118+
}
119+
120+
/* Header */
121+
.playlistMenuHeader {
122+
position: sticky; top: 0;
123+
background: #fff;
124+
padding: 12px 8px 10px;
125+
font-weight: 700; font-size: 16px;
126+
border-bottom: 1px solid rgba(16,0,35,0.08);
127+
z-index: 1;
128+
}
129+
130+
/* List */
131+
#myPlaylistItems {
132+
overflow: auto;
133+
max-height: calc(78vh - 64px);
134+
padding: 8px 4px 8px 8px;
135+
}
136+
137+
/* Item */
138+
#myPlaylistItems .playlistItem {
139+
display: grid;
140+
grid-template-columns: 128px 1fr;
141+
gap: 14px;
142+
align-items: center;
143+
min-height: 76px;
144+
background: #fff;
145+
color: #100023;
146+
border: 1px solid rgba(16,0,35,0.12);
147+
border-radius: 12px;
148+
padding: 10px;
149+
margin: 10px 4px;
150+
cursor: pointer;
151+
transition: background 0.2s, border-color 0.2s, transform 0.15s;
152+
}
153+
154+
/* Hover: accent border */
155+
#myPlaylistItems .playlistItem:hover {
156+
border-color: var(--accent-color, #5D09C7);
157+
transform: translateY(-1px);
158+
}
159+
160+
/* Selected: accent background + white text */
161+
#myPlaylistItems .playlistItem.selected {
162+
background: var(--accent-color, #5D09C7);
163+
border-color: var(--accent-color, #5D09C7);
164+
color: #fff;
165+
}
166+
167+
/* Texts */
168+
#myPlaylistItems .playlistItem .title {
169+
font-weight: 700; font-size: 15px; line-height: 1.25;
170+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
171+
color: inherit;
172+
}
173+
#myPlaylistItems .playlistItem .meta {
174+
font-size: 12px; color: rgba(16,0,35,0.7);
175+
}
176+
#myPlaylistItems .playlistItem.selected .meta { color: rgba(255,255,255,0.9); }
177+
178+
/* Thumb */
179+
#myPlaylistItems .thumb {
180+
width: 128px; height: 72px;
181+
background-size: cover; background-position: center;
182+
border-radius: 10px;
183+
box-shadow: 0 3px 10px rgba(0,0,0,0.18);
184+
}
185+
186+
/* Mobile */
187+
@media (max-width: 600px) {
188+
#myPlaylistPanel { width: min(92vw, 520px); }
189+
#myPlaylistItems .playlistItem {
190+
grid-template-columns: 96px 1fr;
191+
min-height: 64px;
192+
}
193+
#myPlaylistItems .thumb { width: 96px; height: 54px; }
194+
}
195+
</style>
196+
<div class="playlistMenuHeader">Episode List</div>
197+
<div id="myPlaylistItems"></div>
198+
</div>
199+
</fastpix-player>
200+
201+
<script>
202+
const episodes = [
203+
{
204+
playbackId: "8f7920ec-fcb4-4490-85f7-d73798b20a8a",
205+
title: "Episode 1: The Beginning",
206+
description: "Our story starts with a mysterious event that changes everything.",
207+
thumbnail: "https://placehold.co/320x180?text=Ep+1"
208+
},
209+
{
210+
playbackId: "6b4150d6-4aff-424d-8918-10d4c048892b",
211+
title: "Episode 2: The Journey Continues",
212+
description: "The heroes set out on their journey, facing new challenges.",
213+
thumbnail: "https://placehold.co/320x180?text=Ep+2"
214+
},
215+
{
216+
playbackId: "8ed1318c-5e19-46c9-ac4b-955cdfa2f3a7",
217+
title: "Episode 3: The Revelation",
218+
description: "Secrets are revealed and the stakes are raised for everyone involved.",
219+
thumbnail: "https://placehold.co/320x180?text=Ep+3"
220+
}
221+
];
222+
const player = document.getElementById('player');
223+
224+
// Initial setup
225+
document.addEventListener('DOMContentLoaded', () => {
226+
player.addPlaylist(episodes);
227+
});
228+
229+
// PlaybackId change event
230+
player.addEventListener("playbackidchange", (e) => {
231+
const { playbackId, status, isFromPlaylist, currentIndex, totalItems } = e.detail;
232+
console.log(
233+
`PlaybackId change - ID: ${playbackId}, Status: ${status}, From Playlist: ${isFromPlaylist}, Index: ${currentIndex}/${totalItems}`
234+
);
235+
236+
if (status === "loading") {
237+
console.log("Playback-id is being loaded...");
238+
} else if (status === "ready") {
239+
console.log("Playback-id is ready to play!");
240+
} else if (status === "error") {
241+
console.error("Error loading playback-id:", e.detail.error);
242+
}
243+
});
244+
245+
const myPanel = document.getElementById('myPlaylistPanel');
246+
const items = document.getElementById('myPlaylistItems');
247+
248+
// Handle playlist panel toggle
249+
player.addEventListener('playlisttoggle', (e) => {
250+
console.log("playlisttoggle", e.detail);
251+
if ((e.detail?.hasPlaylist ?? false) === false) return;
252+
if (e.detail.open) myPanel.removeAttribute('hidden');
253+
else myPanel.setAttribute('hidden','');
254+
});
255+
256+
// Update selection highlighting
257+
function updateSelection(activeId) {
258+
items.querySelectorAll('.playlistItem').forEach(d => {
259+
d.classList.toggle('selected', d.dataset.playbackId === activeId);
260+
});
261+
}
262+
263+
// Format metadata
264+
function formatMeta(ep) {
265+
return ep.duration ? ep.duration : (ep.description || "");
266+
}
267+
268+
// Build playlist items
269+
items.innerHTML = "";
270+
episodes.forEach(ep => {
271+
const el = document.createElement('div');
272+
el.className = 'playlistItem';
273+
el.dataset.playbackId = ep.playbackId;
274+
275+
const thumb = document.createElement('div');
276+
thumb.className = 'thumb';
277+
if (ep.thumbnail) thumb.style.backgroundImage = `url('${ep.thumbnail}')`;
278+
279+
const info = document.createElement('div');
280+
info.className = 'info';
281+
282+
const title = document.createElement('div');
283+
title.className = 'title';
284+
title.textContent = ep.title || ep.playbackId;
285+
286+
const meta = document.createElement('div');
287+
meta.className = 'meta';
288+
meta.textContent = formatMeta(ep);
289+
290+
info.appendChild(title);
291+
if (meta.textContent) info.appendChild(meta);
292+
293+
el.appendChild(thumb);
294+
el.appendChild(info);
295+
296+
// Click handler for episode selection
297+
el.onclick = () => player.selectEpisodeByPlaybackId(ep.playbackId);
298+
items.appendChild(el);
299+
});
300+
301+
// Initial selection
302+
const initialId = player.getAttribute('default-playback-id') || episodes[0].playbackId;
303+
updateSelection(initialId);
304+
305+
// Update selection on player change
306+
player.addEventListener('playbackidchange', (e) => {
307+
const id = e.detail?.playbackId;
308+
if (id) updateSelection(id);
309+
});
310+
311+
// Ensure selection updates after internal player loads
312+
player.addEventListener('canplay', () => {
313+
const active = player.getAttribute('playback-id') || player.playbackId;
314+
if (active) updateSelection(active);
315+
});
316+
</script>
317+
```
318+
319+
## Troubleshooting
320+
321+
- **Nothing plays**: Ensure each playlist item has a valid `playbackId`.
322+
- **Custom panel doesn't show**: Use `hide-default-playlist-panel` and render your UI in a child with `slot="playlist-panel"`.
323+
- **customNext/customPrev don't work**: Always call `ctx.next()` / `ctx.previous()` after your custom logic.
324+
- **Wrong item highlighted**: Update the selection in the `playbackidchange` handler using the `playbackId` from `e.detail`.

0 commit comments

Comments
 (0)