This react-shorts-app project is a shorts-style demo that shows how to use the web-based FastPix player in a React 19 app.
Player bundle: src/App.tsx imports the built player from the parent repo (../../../dist/player.js). That keeps the demo aligned with your local FastPix build (including track APIs and attributes like hide-native-subtitles). You can swap this for @fastpix/fp-player from npm if you pin a version that supports the same APIs.
package.json still lists @fastpix/fp-player for reference; ensure the runtime bundle you load matches the features you document (audio/subtitle switching, fastpixsubtitlecue, etc.).
To run or adapt this demo you should have:
- Node.js 18+ and npm 9+ installed.
- A React app (this example uses React 19 + Vite).
- Access to FastPix vertical videos (9:16) with:
- A FastPix playback ID for each short.
- Assets encoded for vertical viewing – this demo assumes a portrait/shorts layout only.
The shorts feed is just a JSON array. You can generate or fetch it from your own backend as long as it matches this shape:
[
{
"id": "YOUR_PLAYBACK_ID_1",
"creator": "channel_or_creator_handle",
"title": "Title for the short",
"likes": "12.4K",
"comments": "342",
"shares": "891"
},
{
"id": "YOUR_PLAYBACK_ID_2",
"creator": "another_creator",
"title": "Another vertical short",
"likes": "8.7K",
"comments": "201",
"shares": "543"
}
]In this repo, that JSON is represented by the TypeScript array SHORTS_FEED in src/shorts/types.ts.
Replace every id with your own FastPix playback IDs and update creator, title, and counts as needed. The code does not depend on any other fields, so this format is universal across apps and backends as long as you keep these keys.
All playback in the demo ultimately comes from short.id being passed to:
<ShortItem playbackId={short.id} metadata={short} ... />So once you wire your own JSON feed into SHORTS_FEED (or fetch it and pass it as props), the shorts player will work with your vertical videos.
Clone the repo and run the demo locally:
# from the web-player repo root
cd react-app-publish/fastpix-web-player-react-shorts-demo
npm install
npm run devThen open http://localhost:Your-port-number in your browser.
Before you start, update src/shorts/types.ts and replace all placeholder values like YOUR_PLAYBACK_ID_VERTICAL_VIDEO_1, CREATOR_NAME_1, and TITLE_OF_VERTICAL_VIDEO_1 with your actual FastPix playback IDs and metadata.
To build a production bundle:
npm run buildThis runs tsc -b and vite build, producing a static build in dist/.
It focuses on programmatic control of the player and a shorts feed UI:
- Each short uses a
<fastpix-player>web component. - React drives which short is active, mute state, scroll snapping, and UI controls.
- The player instance exposes convenience methods (
play,pause,mute,unmute) and an underlyingHTMLVideoElementviaplayerRef.current.video.
At a high level:
-
FastPix (player level) – implemented in
@fastpix/fp-playerand configured via attributes + CSS variables:- HLS/DASH playback, manifest fetching, buffering, error handling.
- Track selection (audio, captions), DRM, thumbnails, ads/shoppable integrations.
- Attributes we set in code:
playback-id– which vertical asset to play (1:1 with your feed JSONid).autoplay-shorts– tuned autoplay behavior for shorts for instant playback start.mutedandloop– initial mute + continuous looping at the player level.disable-keyboard-controls– let the app (not the player) own keyboard shortcuts.preload– per‑item preload hint ("auto","metadata","none").
- CSS variables we use in
index.css:- Hide built‑in overlays (middle controls, bottom‑right cluster, left controls) so we can draw our own UI:
- Middle controls –
--middle-controls-mobile,--play-button-initialized,--mobile-play-button-initialized. - Bottom‑right controls –
--bottom-right-controls,--bottom-right-controls-mobile. - Left‑bottom controls –
--left-controls-bottom,--left-controls-bottom-mobile.
- Middle controls –
--seekbar-bottom– pins the FastPix seekbar to the very bottom of the frame; the thumbnail sprites and timestamp tiles automatically follow this position.--progress-bar-invisible: 1– makes the visual bar invisible but keeps the FastPix seekbar logic fully active, so:- Hover thumbnail previews and timestamp pills still work.
- Click‑to‑seek on the underlying bar still works.
- We can build our own progress stripe on top, driven by
video.currentTime, while reusing all of FastPix’s seeking behavior under the hood.
- Hide built‑in overlays (middle controls, bottom‑right cluster, left controls) so we can draw our own UI:
- Internal seekbar behavior, thumbnail seeking, and all low‑level media work remain the player’s responsibility.
-
This React app (application level) – implemented in
src/App.tsx+src/shorts:- Maintains the shorts feed (
SHORTS_FEED) and renders oneShortItemper entry. - Tracks which short is active (
activeIndexRef), global mute state (isMuted), and whether the user has interacted (for autoplay policy). - Uses refs to call player methods (
play(),pause(),mute(),unmute()) at the right time:- Autoplay the active short when scroll/snapping lands on it.
- Pause + mute all non‑active shorts so only one plays at a time.
- Owns all visible UI and layout:
- Play/pause, mute/unmute, fullscreen buttons.
- Like/follow/share buttons, creator chip, HUD counter, vertical dot rail.
- Custom progress stripe that mirrors
video.currentTimebut delegates seek + thumbnails to FastPix.
- Controls navigation and feed UX:
- Scroll snapping (wheel + touch) between shorts.
- Keyboard navigation (ArrowUp / ArrowDown) via app‑level handlers.
- Fullscreen of the scroll container so the shorts column behaves the same in fullscreen and windowed modes.
- Maintains the shorts feed (
Use this repo as a working reference implementation: you can copy the patterns from:
src/App.tsx– feed container, refs, and behavior (playAt,handleMuteToggle, wheel/keyboard handlers).src/shorts/ShortItem.tsx– per‑card player mounting and card UI.src/shorts/types.ts– universal JSON format for your shorts feed.src/index.css– how to hide built‑in controls and restyle the player for a shorts layout.
You are encouraged to re‑use or adapt this code in your own GitHub projects.
FastPix exposes the native HTMLVideoElement on the custom element as a video property. In this demo we type it as:
interface FastPixPlayerElement extends HTMLElement {
video?: HTMLVideoElement;
play?: () => Promise<void> | void;
pause?: () => void;
mute?: () => void;
unmute?: () => void;
destroy?: () => void;
}Inside ShortItem we keep a ref to the player:
const playerRef = useRef<FastPixPlayerElement | null>(null);Once the element is created and mounted, we can safely access the video element:
useEffect(() => {
const player = playerRef.current;
const vid = player?.video;
if (!vid) return;
const onPlay = () => setIsPlaying(true);
const onPause = () => setIsPlaying(false);
setIsPlaying(!vid.paused);
vid.addEventListener("play", onPlay);
vid.addEventListener("pause", onPause);
return () => {
vid.removeEventListener("play", onPlay);
vid.removeEventListener("pause", onPause);
};
}, [playbackId]);We also use playerRef.current.video for:
- Progress bar: a
requestAnimationFrameloop readsvideo.currentTime/video.duration. - Preload behavior: setting
video.preloadbased on whether the short is active / adjacent.
This keeps visual progress and preloading under app control while the streaming logic stays in FastPix.
The shorts demo wires multi-track audio and subtitle / caption switching on top of the FastPix web component. It uses the same ideas as the standalone HTML demo and the project’s AUDIO_SUBTITLE_TRACKS_API.md (methods, events, attributes).
| Attribute | Purpose |
|---|---|
hide-native-subtitles |
Keeps the player’s internal subtitleContainer from showing text. fastpixsubtitlecue still fires so your React overlay can render captions. Use this when you want only your custom subtitle UI. |
disable-hidden-captions (optional) |
On load, subtitles start Off (no automatic showing). After that, the user or your code can enable a track via the built-in CC menu or setSubtitleTrack(...). |
default-audio-track (optional) |
Initial audio track by label (case-insensitive), e.g. "French". Must match a track name from the HLS manifest. |
default-subtitle-track (optional) |
Initial subtitle track by label (case-insensitive). Only applies when subtitles are allowed to show (not when you force everything Off with disable-hidden-captions on load). |
Example (conceptual — in this repo the element is created in useEffect; attributes are the same as JSX):
el.setAttribute("playback-id", playbackId);
el.setAttribute("autoplay-shorts", "");
el.setAttribute("muted", "");
el.setAttribute("loop", "");
el.setAttribute("disable-keyboard-controls", "");
el.setAttribute("preload", preload);
el.setAttribute("hide-native-subtitles", "");
// Optional:
// el.setAttribute("disable-hidden-captions", "");
// el.setAttribute("default-audio-track", "French");
// el.setAttribute("default-subtitle-track", "English");After the manifest / text tracks are ready, you can call:
| Method | Description |
|---|---|
getAudioTracks() |
Snapshot of audio TrackInfo[] (de-duped by label). |
setAudioTrack(label) |
Switch audio by label (string only; not numeric id). |
getSubtitleTracks() |
Snapshot of subtitle/caption tracks. |
setSubtitleTrack(label | null) |
Enable a track by label, or null for Off. |
disableSubtitles() |
Same idea as choosing Off in the built-in menu (when available). |
TrackInfo objects include id, label, optional language, isDefault, isCurrent. Prefer label for setAudioTrack / setSubtitleTrack; use language for persisting user preference across assets.
| Event | When | Typical use |
|---|---|---|
fastpixtracksready |
After HLS manifest is parsed; may fire again when subtitle textTracks attach. |
Populate menus; read event.detail.audioTracks / subtitleTracks. |
fastpixaudiochange |
User or setAudioTrack changed audio. |
Refresh UI highlight. |
fastpixsubtitlechange |
User or setSubtitleTrack / Off changed subtitles. |
Refresh UI highlight. |
fastpixsubtitlecue |
Active cue text updates. | Drive a custom React subtitle overlay (recommended with hide-native-subtitles). |
Active short only: In this demo, fastpixsubtitlecue is listened to only when isActive === true (see below), so the console and React state are not flooded by off-screen shorts.
- Top bar (⋯) – Shown only when the stream exposes more than one audio track and/or at least one subtitle track. The menu lists Audio and Subtitles (with an Off row for captions) and calls
setAudioTrack/setSubtitleTrack. - Custom subtitle pill – Subscribes to
fastpixsubtitlecue, updates local state, and renders a centered pill above the progress bar. Withhide-native-subtitles, that pill is the visible captions layer.
App.tsx passes isActive={i === activeIndex} into each ShortItem so subtitle cue handling and logging apply only to the visible short.
useEffect(() => {
const player = playerRef.current as any;
if (!player || !isActive) return;
const onCue = (e: Event) => {
const d = (e as CustomEvent).detail || {};
console.log("[fastpixsubtitlecue]", d);
setSubtitleText(d.text ?? "");
};
player.addEventListener("fastpixsubtitlecue", onCue);
return () => player.removeEventListener("fastpixsubtitlecue", onCue);
}, [playbackId, isActive]);Full project reference: AUDIO_SUBTITLE_TRACKS_API.md at the web-player repo root.
To avoid mounting every short’s <fastpix-player> at once:
App.tsxrenders only a window of items aroundactiveIndex(e.g. current ±2), each wrapped in an absolutely positioned slot attop: i * 100vhso scroll position still matches indexi.preloadstays strict:"auto"for the active short,"metadata"for neighbors,"none"for others in the window.playAtskips re-running play logic for the same index repeatedly (dedupe withlastPlayIndexRef) so you do not get play/pause flicker when scroll handlers fire multiple times.
Optional: App.tsx logs [perf] ShortsApp first render and [perf] First short started playing (ms since module load) for quick checks in dev vs production (npm run build && npm run preview).
At the application level we treat each FastPix instance as a small API surface:
// In ShortsApp (App.tsx)
const playerRefsByIndex = useRef<Record<number, FastPixPlayerElement | null>>({});
const registerPlayer = useCallback((index: number, player: FastPixPlayerElement | null) => {
if (player) playerRefsByIndex.current[index] = player;
else delete playerRefsByIndex.current[index];
}, []);ShortItem calls registerPlayer(itemIndex, el) when it mounts / unmounts.
const playAt = useCallback(
(index: number, options: { resetTime?: boolean } = {}) => {
const { resetTime = true } = options;
// Pause + mute all other players
Object.entries(playerRefsByIndex.current).forEach(([i, p]) => {
const playerEl = p;
if (!playerEl) return;
if (Number(i) !== index) {
playerEl.mute?.();
playerEl.pause?.();
}
});
const player = playerRefsByIndex.current[index];
if (!player) return;
// Optionally reset time when snapping to a new short
if (resetTime && player.video) player.video.currentTime = 0;
// Autoplay policy: before any gesture, always muted; after interaction, honor app mute state
if (!hasUserInteractedRef.current) {
player.mute?.();
} else {
isMutedRef.current ? player.mute?.() : player.unmute?.();
}
const playResult = player.play?.();
if ((playResult as Promise<void> | undefined)?.catch) {
(playResult as Promise<void>).catch(() => {
requestAnimationFrame(() => {
if (activeIndexRef.current === index) {
player.play?.()?.catch?.(() => {});
}
});
});
}
},
[],
);What FastPix handles:
- HLS/DASH streaming, manifests, network logic.
- Thumbnails, captions, audio tracks, quality selection.
- Keyboard shortcuts (when not disabled), volume persistence, ads/shoppable integrations, etc.
What the app handles:
- Which short is active (
activeIndexRef+ scroll listener / wheel / keyboard). - Global mute state and user interaction tracking.
- When to call
play(),pause(),mute(),unmute()across the playlist. - UI layer: creator chip, subscribe, like/share/follow, HUD counters, scroll arrows.
This separation keeps streaming and player internals inside FastPix while React orchestrates feed behavior and UX.
The demo hides most of the built‑in FastPix UI and renders its own Shorts-style overlays and seekbar, while still letting the FastPix logic handle hover thumbnails and seek behavior.
In src/index.css:
fastpix-player {
--middle-controls-mobile: none;
--mobile-play-button-initialized: none;
--bottom-right-controls-mobile: none;
--bottom-right-controls: none;
--left-controls-bottom: none;
--left-controls-bottom-mobile: none;
--seekbar-bottom: 0px;
/* Hide progress bar visually but keep it for hover/seek so timestamp preview works */
--progress-bar-invisible: 1;
--play-button-initialized: none;
}- Shorts-style UX – We want the player frame to look like a shorts reel:
- Custom top bar (play/pause, mute/unmute, fullscreen).
- Custom right rail (scroll arrows, like/share/follow).
- Custom bottom progress stripe that matches the card’s rounded corners.
- Branding & layout control – App code controls spacing, border radius, and overlays consistently across the feed.
By setting the FastPix CSS variables to none, we hide:
- Middle play/pause controls.
- Mobile overlay play buttons.
- Bottom-right control clusters (volume, settings, etc.).
- Left-bottom overlays.
But we do not disable the underlying logic – only the visuals.
In ShortItem.tsx, the progress bar you see at the bottom of each card is fully app‑driven, using the FastPix video element:
// Progress state
const [progress, setProgress] = useState(0);
useEffect(() => {
const player = playerRef.current;
const vid = player?.video;
if (!vid) return;
const paint = () => {
const duration = vid.duration;
if (duration > 0 && isFinite(duration)) {
setProgress((vid.currentTime / duration) * 100);
} else {
setProgress(0);
}
};
let rafId: number | null = null;
const loop = () => {
paint();
rafId = requestAnimationFrame(loop);
};
const startRAF = () => {
if (rafId == null) rafId = requestAnimationFrame(loop);
};
const stopRAF = () => {
if (rafId != null) {
cancelAnimationFrame(rafId);
rafId = null;
}
paint();
};
paint();
vid.addEventListener("play", startRAF);
vid.addEventListener("playing", startRAF);
vid.addEventListener("pause", stopRAF);
vid.addEventListener("ended", stopRAF);
vid.addEventListener("seeking", paint);
vid.addEventListener("seeked", paint);
return () => {
if (rafId != null) cancelAnimationFrame(rafId);
vid.removeEventListener("play", startRAF);
vid.removeEventListener("playing", startRAF);
vid.removeEventListener("pause", stopRAF);
vid.removeEventListener("ended", stopRAF);
vid.removeEventListener("seeking", paint);
vid.removeEventListener("seeked", paint);
};
}, []);
// Render
<div
style={{
position: "absolute",
bottom: 0,
left: 20,
right: 20,
height: 3,
zIndex: 3,
background: "rgba(255,255,255,0.2)",
pointerEvents: "none",
borderRadius: "0 0 16px 16px",
}}
>
<div
style={{
height: "100%",
width: `${progress}%`,
background: accentColor,
borderRadius: "inherit",
transition: "none",
}}
/>
</div>This bar is read‑only – it reflects FastPix’s playback state, but all click‑to‑seek and thumbnail preview responsibility stays inside the FastPix player.
Because we use:
--progress-bar-invisible: 1– the built‑in progress bar is visually hidden, but:- Hover thumbnails, timestamp preview, and click‑to‑seek continue to work on the underlying FastPix bar.
- Our own progress stripe is layered on top, giving full visual control without re‑implementing seeking logic.
In other words:
- FastPix still handles:
- Computing seek positions, updating internal range, showing thumbnails.
- Your React app:
- Renders a custom progress bar synced to
video.currentTime. - Owns the visual design of the seekbar, while leveraging FastPix’s tried-and-tested seeking behavior underneath.
- Renders a custom progress bar synced to
Below is a minimal pattern (derived from the real App.tsx) that shows how to wire UI buttons to FastPix methods.
const [activeIndex, setActiveIndex] = useState(0);
const playerRefsByIndex = useRef<Record<number, FastPixPlayerElement | null>>({});
const activeIndexRef = useRef(0);
const isMutedRef = useRef(true);
const hasUserInteractedRef = useRef(false);
activeIndexRef.current = activeIndex;
isMutedRef.current = isMuted;
const registerPlayer = useCallback(
(index: number, player: FastPixPlayerElement | null) => {
if (player) playerRefsByIndex.current[index] = player;
else delete playerRefsByIndex.current[index];
},
[],
);ShortItem calls registerPlayer(itemIndex, el) when its fastpix-player mounts.
const playAt = useCallback(
(index: number, options: { resetTime?: boolean } = {}) => {
const { resetTime = true } = options;
// Pause + mute everyone else
Object.entries(playerRefsByIndex.current).forEach(([i, p]) => {
const playerEl = p;
if (!playerEl) return;
if (Number(i) !== index) {
playerEl.mute?.();
playerEl.pause?.();
}
});
const player = playerRefsByIndex.current[index];
if (!player) return;
if (resetTime && player.video) {
player.video.currentTime = 0;
}
// Autoplay policy vs user mute preference
if (!hasUserInteractedRef.current) {
player.mute?.();
} else {
isMutedRef.current ? player.mute?.() : player.unmute?.();
}
const playResult = player.play?.();
if ((playResult as Promise<void> | undefined)?.catch) {
(playResult as Promise<void>).catch(() => {
requestAnimationFrame(() => {
if (activeIndexRef.current === index) {
player.play?.()?.catch?.(() => {});
}
});
});
}
},
[],
);
const handleMuteToggle = useCallback(() => {
const player = playerRefsByIndex.current[activeIndexRef.current];
if (!player) return;
if (isMuted) {
hasUserInteractedRef.current = true;
player.unmute?.();
setIsMuted(false);
} else {
player.mute?.();
setIsMuted(true);
}
}, [isMuted]);In the ShortItem overlay we use the underlying video element for play/pause:
// Inside ShortItem.tsx
<button
type="button"
onClick={(e) => {
e.stopPropagation();
const vid = playerRef.current?.video;
if (vid) vid.paused ? vid.play() : vid.pause();
}}
aria-label={isPlaying ? "Pause" : "Play"}
>
{isPlaying ? "Pause" : "Play"}
</button>In the ShortsApp container we wire a mute/unmute button to handleMuteToggle:
// In the top HUD (App.tsx)
<button
type="button"
onClick={handleMuteToggle}
>
{isMuted ? "Tap to unmute" : "Mute"}
</button>You can also expose explicit Play / Pause buttons for the current short by using playAt and the active player:
const handlePlayClick = () => {
playAt(activeIndexRef.current, { resetTime: false });
};
const handlePauseClick = () => {
const player = playerRefsByIndex.current[activeIndexRef.current];
player?.pause?.();
};
// Example buttons:
<button type="button" onClick={handlePlayClick}>Play current</button>
<button type="button" onClick={handlePauseClick}>Pause current</button>This is the exact pattern the demo uses: central refs + helpers in App.tsx, and small buttons in the UI that call play(), pause(), mute(), and unmute() on the right FastPix instance.
In ShortItem we mount the player like this:
useEffect(() => {
const container = containerRef.current;
if (!container || playerRef.current) return;
const el = document.createElement("fastpix-player") as FastPixPlayerElement;
el.setAttribute("playback-id", playbackId);
el.setAttribute("autoplay-shorts", "");
el.setAttribute("muted", "");
el.setAttribute("loop", "");
el.setAttribute("disable-keyboard-controls", "");
el.setAttribute("preload", preload);
el.setAttribute("hide-native-subtitles", "");
// Optional: el.setAttribute("disable-hidden-captions", "");
// Optional: el.setAttribute("default-audio-track", "French");
// Optional: el.setAttribute("default-subtitle-track", "English");
el.style.width = "100%";
el.style.height = "100%";
el.style.objectFit = "cover";
container.appendChild(el);
playerRef.current = el;
registerPlayer(itemIndex, el);
return () => {
registerPlayer(itemIndex, null);
try { if (typeof el.destroy === "function") el.destroy(); } catch {}
if (container.contains(el)) container.removeChild(el);
playerRef.current = null;
};
}, [playbackId, itemIndex, registerPlayer]);Instead of:
// We intentionally DO NOT do this today:
// <fastpix-player playback-id={...} autoplay-shorts muted loop />-
Lifecycle control for a shorts feed
With programmaticdocument.createElement, we have full control over when a player is created and destroyed. In a shorts UX this is important to:- Avoid keeping too many
<fastpix-player>instances alive at once. - Explicitly clean up (calling
destroy()if available) when a card unmounts.
- Avoid keeping too many
-
Attribute-only API today
The current web player is designed as a framework-agnostic custom element:- All configuration is via attributes (
playback-id,autoplay-shorts,muted,loop, etc.). - React 19 can render it as JSX, but:
- Attribute updates and ref timing can be trickier in complex virtualized lists.
- We want deterministic mounting/teardown tied to our
useEffect, especially for autoplay + shorts snapping behavior.
- All configuration is via attributes (
-
Interop safety while React-based version is in progress
We’re actively working on a React-based version of the FastPix player, which will expose a first-class<FastPixPlayer />component with idiomatic props, hooks, and React 19 semantics.Until that is released, using
document.createElement("fastpix-player"):- Keeps the integration explicit and predictable.
- Avoids subtle issues around how React reconciles custom elements across rapid feed changes.
- Matches what you would do in any non-React environment (plain JS, Next.js with custom layout effects, etc.).
When the React-native wrapper is available, this demo can be simplified to:
<FastPixPlayer
playbackId={short.id}
autoplayShorts
muted
loop
disableKeyboardControls
onReady={(player) => registerPlayer(itemIndex, player)}
/>;For now, this example is a reference implementation for using the web component version of FastPix in a React 19 shorts experience. It shows:
- How to control the player via refs and methods.
- How to read the underlying
videoelement when needed. - How to switch audio and subtitle tracks (
getAudioTracks,setSubtitleTrack,fastpixsubtitlecue, etc.) — see Audio & subtitle tracks above. - How to keep feed-level logic inside React while delegating playback to FastPix.
This demo deliberately replaces most of the built‑in player chrome with React components so you can see how to wire your own UI on top of FastPix.
At a high level:
-
Play / Pause (per short)
-
Implemented in
ShortItem.tsxby reading the underlyingvideoelement:const vid = playerRef.current?.video; if (vid) vid.paused ? vid.play() : vid.pause();
-
We also mirror that state into
isPlaying(viaplay/pauselisteners) to render the right icon.
-
-
Mute / Unmute (feed‑level)
-
Implemented in
ShortsApp(App.tsx) viahandleMuteToggle:const player = playerRefsByIndex.current[activeIndexRef.current]; if (!player) return; if (isMuted) { hasUserInteractedRef.current = true; player.unmute?.(); setIsMuted(false); } else { player.mute?.(); setIsMuted(true); }
-
We keep a single mute flag for the whole feed and let
playAtapply that flag whenever the active short changes.
-
-
Fullscreen
-
We fullscreen the scroll container, not the individual player:
const el = scrollRef.current; // ... (el.requestFullscreen || (el as any).webkitRequestFullscreen)?.call(el);
-
This keeps scroll snapping and the custom overlays working the same way in fullscreen and windowed modes.
-
-
Like / Follow / Share
- All handled in React state only (no special FastPix APIs required):
liked/followedbooleans for eachShortItem.incrementLabel()helper for compact like counts ("12.4K"→"12.5K").handleShare()builds a URL for the currentplaybackIdand:- Uses
navigator.share(...)when available. - Falls back to
navigator.clipboard.writeText(...).
- Uses
- All handled in React state only (no special FastPix APIs required):
The important idea: FastPix provides playback and metadata; the app owns all UI state and interactions. You are free to rearrange or re‑style the controls without touching any streaming logic.
Feel free to copy/paste or adapt any of the source under my-react-app/src into your own projects. This demo is meant to be a working cookbook, not just a black‑box example.
By default, FastPix captures many keyboard keys (space, arrows, etc.) to control the player. That is great for standalone players, but in a shorts feed it can interfere with app‑level navigation.
In ShortItem.tsx we set:
el.setAttribute("disable-keyboard-controls", "");This tells the player to ignore keyboard input, so the app can own it instead. We then implement our own handlers in ShortsApp:
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
e.preventDefault();
wheelSnapTo(activeIndexRef.current + (e.key === "ArrowDown" ? 1 : -1));
}, [wheelSnapTo]);
useEffect(() => {
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [handleKeyDown]);With this setup:
- Arrow keys move to the next/previous short (just like many social apps).
- FastPix does not attempt to change volume/seek in response to those keys.
- You can extend the same pattern for other keys (e.g.
Lfor like,Mfor mute, etc.) entirely at the app level.
This makes keyboard behavior predictable and app‑specific, while still letting FastPix handle all media playback under the hood.