Skip to content

Commit af3b2bf

Browse files
authored
Merge pull request #3 from FastPix/feature/audio-subtitle-tracks-headless-functionality-support
added tracks functionality support
2 parents 1d2ed7f + 1a26c72 commit af3b2bf

File tree

11 files changed

+1596
-84
lines changed

11 files changed

+1596
-84
lines changed

AUDIO_SUBTITLE_TRACKS_API.md

Lines changed: 585 additions & 0 deletions
Large diffs are not rendered by default.

CHANGELOG.md

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

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

5+
## [1.0.14]
6+
7+
### Audio & Subtitle Tracks
8+
9+
- **Switch by name (label)**`setAudioTrack(languageName)` / `setSubtitleTrack(languageName | null)` switch tracks by **label/name** (no numeric ids required).
10+
- **Set defaults by name** – New attributes:
11+
- `default-audio-track="French"`
12+
- `default-subtitle-track="English"`
13+
- **Cleaner track lists**`getAudioTracks()` / `getSubtitleTracks()` now avoid duplicate entries when multiple tracks share the same label.
14+
- **Better events for integrations**
15+
- `fastpixtracksready` includes the **full current track objects** (`currentAudioTrackLoaded`, `currentSubtitleLoaded`) in addition to the track lists.
16+
- `fastpixaudiochange` / `fastpixsubtitlechange` include the **current track object** (`currentTrack`) so you can log/update UI easily.
17+
518
## [1.0.13]
619

720
### Readme.md
@@ -147,4 +160,4 @@ All notable changes to this project will be documented in this file.
147160
- **Placeholder**: Added placeholder support for loading states.
148161
- **offline/online control**: Provided control mechanisms for offline/online scenarios.
149162
- **Title Display**: Implemented title display options for videos.
150-
- **Overriding Default Behaviors**: Allowed users to override default player behaviors.
163+
- **Overriding Default Behaviors**: Allowed users to override default player behaviors.

README.md

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,70 @@ This SDK simplifies HLS video playback by offering a wide range of customization
4444

4545
- Users can switch between available subtitles and audio tracks during playback, offering a personalized viewing experience. This feature allows viewers to choose their preferred language or audio option easily.
4646

47+
- ## Audio & Subtitle Tracks (integration guide)
48+
49+
This section documents **how to read tracks, set defaults, switch tracks, and consume events**.
50+
For the full API reference, see **`AUDIO_SUBTITLE_TRACKS_API.md`** (in this folder).
51+
52+
- **Integration steps (recommended)**:
53+
- Include the player script (`dist/player.js`) and add a `<fastpix-player>` element with a `playback-id`.
54+
- Optionally set defaults by **name/label** using:
55+
- `default-audio-track="French"`
56+
- `default-subtitle-track="English"`
57+
- Attach listeners for:
58+
- `fastpixtracksready` (initial track snapshot; may re-emit once subtitle `textTracks` attach)
59+
- `fastpixaudiochange` / `fastpixsubtitlechange` (only for explicit changes)
60+
- Build your UI from `getAudioTracks()` / `getSubtitleTracks()` and call `setAudioTrack(...)` / `setSubtitleTrack(...)` to switch.
61+
62+
- **Important behavior**:
63+
- **Track switching is label-only**: no numeric ids are accepted by `setAudioTrack` / `setSubtitleTrack`.
64+
- **Duplicate labels are de-duped** (case-insensitive): if multiple tracks share the same label/name, the player keeps one entry (prefers the currently active one).
65+
- **`fastpixtracksready` timing**: audio tracks are known at HLS `MANIFEST_PARSED`, but subtitle `textTracks` can attach slightly later, so the player may emit `fastpixtracksready` again with populated subtitle tracks.
66+
67+
- **Attributes**:
68+
69+
| Attribute | Type | Meaning |
70+
|---|---:|---|
71+
| `default-audio-track` | string | Default **audio** track by label/name (case-insensitive) |
72+
| `default-subtitle-track` | string | Default **subtitle** track by label/name (case-insensitive) |
73+
| `disable-hidden-captions` | boolean | Disables subtitles/captions automatically on load |
74+
75+
- **Methods**:
76+
77+
| Method | Purpose |
78+
|---|---|
79+
| `getAudioTracks()` | Returns de-duped audio track list (each track has `label`, `language`, `isCurrent`) |
80+
| `getSubtitleTracks()` | Returns de-duped subtitle list (each track has `label`, `language`, `isCurrent`) |
81+
| `setAudioTrack(languageName)` | Switch audio by **label/name** |
82+
| `setSubtitleTrack(languageName \| null)` | Switch subtitles by **label/name**, or `null` to turn Off |
83+
| `disableSubtitles()` | Turns subtitles Off (equivalent to UI “Off”) |
84+
85+
- **Events**:
86+
87+
| Event | When it fires | `event.detail` (key fields) |
88+
|---|---|---|
89+
| `fastpixtracksready` | After manifest parse; may re-emit when subtitle `textTracks` attach | `audioTracks`, `subtitleTracks`, `currentAudioTrackLoaded`, `currentSubtitleLoaded` (plus legacy ids) |
90+
| `fastpixaudiochange` | Only when audio is explicitly changed (menu click or `setAudioTrack`) | `tracks`, `currentId`, `currentTrack` |
91+
| `fastpixsubtitlechange` | Only when subtitles are explicitly changed (menu click / Off / programmatic) | `tracks`, `currentId`, `currentTrack` |
92+
| `fastpixsubtitlecue` | Whenever a cue changes for the active subtitle track | `{ text, language, startTime, endTime }` |
93+
94+
- **Demo explained (`test/index.html`)**:
95+
- **Markup**:
96+
- `<fastpix-player ... default-audio-track="French" default-subtitle-track="English">` sets initial tracks by **name**.
97+
- Each `.player-container` includes a `<div class="custom-subtitle" data-role="custom-subtitle"></div>` overlay for custom-rendered subtitles.
98+
- **Custom subtitle overlay (per player/session)**:
99+
- The demo attaches a `fastpixsubtitlecue` listener to **every** `fastpix-player` on the page.
100+
- It scopes rendering to the player’s own container using `closest('.player-container')`, so multiple players don’t overwrite each other.
101+
- The overlay is `display: none` by default and only shown when a subtitle is enabled and a non-empty cue arrives.
102+
- **Track UI**:
103+
- On `fastpixtracksready`, the demo calls `getAudioTracks()` and renders buttons.
104+
- Subtitles can appear later, so it **polls** `getSubtitleTracks()` briefly and renders subtitle buttons once available.
105+
- **Logging current track details**:
106+
- `fastpixaudiochange` / `fastpixsubtitlechange` listeners log the **current track object** (`detail.currentTrack`), regardless of whether the change came from the built-in menu or the programmatic API.
107+
108+
- **Full reference**:
109+
- See **`AUDIO_SUBTITLE_TRACKS_API.md`** for the complete API, examples, and best practices.
110+
47111
- ## Styling and color customization:
48112

49113
- Customize the player’s visual elements using the `accent-color`, `primary-color`, and `secondary-color` attributes:
@@ -1576,4 +1640,4 @@ For a full **Shorts-style feed** in React 19 (multiple vertical shorts, scroll s
15761640
15771641
- **[FastPix/fastpix-web-player-react-shorts-demo](https://github.com/FastPix/fastpix-web-player-react-shorts-demo)**
15781642
1579-
You can reuse the HTML/CSS/script above in your own page or adapt the pattern from the React demo to get your own seekbar design while keeping FastPix thumbnail hover previews and seeking behavior.
1643+
You can reuse the HTML/CSS/script above in your own page or adapt the pattern from the React demo to get your own seekbar design while keeping FastPix thumbnail hover previews and seeking behavior.

demo/audio_subtitle_tracks.html

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Audio Track Switching Demo</title>
7+
<script src="../dist/player.js"></script>
8+
<style>
9+
body {
10+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11+
max-width: 800px;
12+
margin: 20px auto;
13+
padding: 0 20px;
14+
}
15+
fastpix-player {
16+
width: 100%;
17+
--aspect-ratio: 21/9;
18+
}
19+
.player-container {
20+
position: relative;
21+
width: 100%;
22+
aspect-ratio: 16/9;
23+
}
24+
.custom-subtitle {
25+
position: absolute;
26+
left: 50%;
27+
bottom: 10%;
28+
transform: translateX(-50%);
29+
max-width: 90%;
30+
padding: 6px 12px;
31+
background: rgba(0, 0, 0, 0.7);
32+
color: #fff;
33+
display: none;
34+
border-radius: 4px;
35+
text-align: center;
36+
font-size: 16px;
37+
line-height: 1.4;
38+
pointer-events: none;
39+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
40+
}
41+
.track-controls {
42+
margin-top: 20px;
43+
padding: 15px;
44+
background: #f5f5f5;
45+
border-radius: 8px;
46+
}
47+
.track-controls h3 {
48+
margin: 0 0 10px 0;
49+
}
50+
.track-buttons {
51+
display: flex;
52+
gap: 8px;
53+
flex-wrap: wrap;
54+
}
55+
.track-btn {
56+
padding: 8px 16px;
57+
border: 2px solid #ddd;
58+
border-radius: 6px;
59+
background: white;
60+
cursor: pointer;
61+
font-size: 14px;
62+
}
63+
.track-btn:hover {
64+
border-color: #007bff;
65+
}
66+
.track-btn.active {
67+
background: #007bff;
68+
color: white;
69+
border-color: #007bff;
70+
}
71+
.current-track {
72+
margin-top: 10px;
73+
font-size: 14px;
74+
color: #666;
75+
}
76+
</style>
77+
</head>
78+
<body>
79+
<h1>Audio Track Switching Demo</h1>
80+
81+
<div class="player-container">
82+
<fastpix-player
83+
playback-id="your-playback-id"
84+
loop
85+
auto-play
86+
muted
87+
preload="auto"
88+
default-audio-track="French"
89+
default-subtitle-track="English"
90+
>
91+
</fastpix-player>
92+
<div class="custom-subtitle" data-role="custom-subtitle"></div>
93+
</div>
94+
95+
<div class="track-controls">
96+
<h3>Audio Tracks</h3>
97+
<div id="audioButtons" class="track-buttons">
98+
<em>Loading tracks...</em>
99+
</div>
100+
<div id="currentAudio" class="current-track"></div>
101+
</div>
102+
103+
<div class="track-controls">
104+
<h3>Subtitle Tracks</h3>
105+
<div id="subtitleButtons" class="track-buttons">
106+
<em>Loading tracks...</em>
107+
</div>
108+
<div id="currentSubtitle" class="current-track"></div>
109+
</div>
110+
111+
<script>
112+
// Controls below target the first player on the page,
113+
// but custom subtitles are scoped per-player container.
114+
const player = document.querySelector('fastpix-player');
115+
const audioButtonsContainer = document.getElementById('audioButtons');
116+
const subtitleButtonsContainer = document.getElementById('subtitleButtons');
117+
const currentAudioDisplay = document.getElementById('currentAudio');
118+
const currentSubtitleDisplay = document.getElementById('currentSubtitle');
119+
function getCustomSubtitleDiv(forPlayer) {
120+
const container = forPlayer.closest('.player-container');
121+
return container ? container.querySelector('[data-role="custom-subtitle"]') : null;
122+
}
123+
let subtitlePollId = null;
124+
125+
// Wait for tracks to be ready
126+
player.addEventListener('fastpixtracksready', (e) => {
127+
console.log("Tracks ready!", e.detail);
128+
129+
// Get audio tracks
130+
const audioTracks = player.getAudioTracks();
131+
console.log("Audio tracks:", audioTracks);
132+
133+
// Render audio track buttons
134+
renderAudioButtons(audioTracks);
135+
136+
// Subtitle tracks often become available slightly after MANIFEST_PARSED.
137+
// Poll getSubtitleTracks() for a short window so we render
138+
// all detected subtitle tracks automatically, without needing to click Off.
139+
if (subtitlePollId) {
140+
clearInterval(subtitlePollId);
141+
}
142+
const startTime = Date.now();
143+
subtitlePollId = setInterval(() => {
144+
const subtitleTracks = player.getSubtitleTracks();
145+
console.log("Polled subtitle tracks:", subtitleTracks);
146+
if (subtitleTracks && subtitleTracks.length > 0) {
147+
clearInterval(subtitlePollId);
148+
subtitlePollId = null;
149+
// Render and highlight whichever subtitle track is currently "showing"
150+
// so that the actively playing subtitle is highlighted by default.
151+
renderSubtitleButtons(subtitleTracks);
152+
} else if (Date.now() - startTime > 10000) { // safety timeout 10s
153+
clearInterval(subtitlePollId);
154+
subtitlePollId = null;
155+
}
156+
}, 500);
157+
});
158+
159+
// Custom subtitle overlay: attach to ALL players so each renders separately.
160+
document.querySelectorAll('fastpix-player').forEach((p) => {
161+
p.addEventListener('fastpixsubtitlecue', (e) => {
162+
const { text, language, startTime, endTime } = /** @type {CustomEvent} */ (e).detail;
163+
console.log('Current subtitle cue:', text, language, startTime, endTime);
164+
165+
// Only show cues when a subtitle track is actually enabled (not Off).
166+
const subtitleTracks = p.getSubtitleTracks();
167+
const hasActiveSubtitle = Array.isArray(subtitleTracks) && subtitleTracks.some(t => t.isCurrent);
168+
169+
const customSubtitleDiv = getCustomSubtitleDiv(p);
170+
if (customSubtitleDiv) {
171+
if (hasActiveSubtitle && text) {
172+
customSubtitleDiv.textContent = text;
173+
customSubtitleDiv.style.display = 'block';
174+
} else {
175+
customSubtitleDiv.textContent = '';
176+
customSubtitleDiv.style.display = 'none';
177+
}
178+
}
179+
});
180+
});
181+
182+
// Listen for audio track changes
183+
player.addEventListener('fastpixaudiochange', (e) => {
184+
const { tracks, currentTrack } = /** @type {CustomEvent} */ (e).detail || {};
185+
const resolvedCurrent = currentTrack || (Array.isArray(tracks) ? tracks.find(t => t.isCurrent) : null);
186+
console.log("Audio changed. Current track:", resolvedCurrent);
187+
renderAudioButtons(tracks || player.getAudioTracks());
188+
});
189+
190+
// Listen for subtitle track changes
191+
player.addEventListener('fastpixsubtitlechange', (e) => {
192+
const { tracks, currentTrack } = /** @type {CustomEvent} */ (e).detail || {};
193+
const resolvedCurrent = currentTrack || (Array.isArray(tracks) ? tracks.find(t => t.isCurrent) : null);
194+
console.log("Subtitle changed. Current track:", resolvedCurrent);
195+
renderSubtitleButtons(tracks || player.getSubtitleTracks());
196+
});
197+
198+
function renderAudioButtons(tracks) {
199+
if (!tracks || tracks.length === 0) {
200+
audioButtonsContainer.innerHTML = '<em>No audio tracks available</em>';
201+
currentAudioDisplay.textContent = '';
202+
return;
203+
}
204+
205+
audioButtonsContainer.innerHTML = '';
206+
207+
tracks.forEach(track => {
208+
const btn = document.createElement('button');
209+
btn.className = 'track-btn' + (track.isCurrent ? ' active' : '');
210+
btn.textContent = `${track.label} (${track.language || 'unknown'})`;
211+
btn.onclick = () => {
212+
console.log(`Switching to audio track: ${track.label} (${track.language || 'unknown'})`);
213+
// Public API is label-driven (no numeric ids / language codes)
214+
player.setAudioTrack(track.label);
215+
};
216+
audioButtonsContainer.appendChild(btn);
217+
});
218+
219+
const current = tracks.find(t => t.isCurrent);
220+
currentAudioDisplay.textContent = current
221+
? `Current: ${current.label} (id: ${current.id})`
222+
: '';
223+
}
224+
225+
function renderSubtitleButtons(tracks) {
226+
subtitleButtonsContainer.innerHTML = '';
227+
228+
// Add "Off" button
229+
const offBtn = document.createElement('button');
230+
offBtn.className = 'track-btn' + (!tracks.some(t => t.isCurrent) ? ' active' : '');
231+
offBtn.textContent = 'Off';
232+
offBtn.onclick = () => {
233+
console.log('Turning subtitles off');
234+
// Prefer the new public API if available
235+
if (typeof player.disableSubtitles === 'function') {
236+
player.disableSubtitles();
237+
} else {
238+
// Fallback for older builds
239+
player.setSubtitleTrack(null);
240+
}
241+
// Also immediately clear any currently displayed custom subtitle text
242+
const customSubtitleDiv = getCustomSubtitleDiv(player);
243+
if (customSubtitleDiv) {
244+
customSubtitleDiv.textContent = '';
245+
customSubtitleDiv.style.display = 'none';
246+
}
247+
// Update status text
248+
currentSubtitleDisplay.textContent = 'Current: Off';
249+
};
250+
subtitleButtonsContainer.appendChild(offBtn);
251+
252+
if (!tracks || tracks.length === 0) {
253+
currentSubtitleDisplay.textContent = 'No subtitle tracks available';
254+
return;
255+
}
256+
257+
tracks.forEach(track => {
258+
const btn = document.createElement('button');
259+
btn.className = 'track-btn' + (track.isCurrent ? ' active' : '');
260+
btn.textContent = `${track.label} (${track.language || 'unknown'})`;
261+
btn.onclick = () => {
262+
console.log(`Switching to subtitle track: ${track.label} (${track.language || 'unknown'})`);
263+
// Public API is label-driven (no numeric ids / language codes)
264+
player.setSubtitleTrack(track.label);
265+
};
266+
subtitleButtonsContainer.appendChild(btn);
267+
});
268+
269+
const current = tracks.find(t => t.isCurrent);
270+
currentSubtitleDisplay.textContent = current
271+
? `Current: ${current.label} (id: ${current.id})`
272+
: 'Current: Off';
273+
}
274+
</script>
275+
</body>
276+
</html>

0 commit comments

Comments
 (0)