|
1 | | -## FastPix Player - Playlist Developer Guide |
| 1 | +# FastPix Player – Playlist Developer Guide |
2 | 2 |
|
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. |
4 | 4 |
|
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