Skip to content

Commit 596e5f0

Browse files
committed
Preload hosted player startup media
1 parent 4b62cf5 commit 596e5f0

4 files changed

Lines changed: 158 additions & 2 deletions

File tree

apps/site/app/embed/[assetId]/page.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
instantPlaybackScript,
1515
playbackStateMessage,
1616
readyBootstrap,
17+
startupPreloadHints,
1718
type PlaybackEngine,
1819
type StartupMode,
1920
} from "../../../lib/player-engine.ts";
@@ -136,6 +137,7 @@ export default async function EmbedPage({ params, searchParams }: EmbedPageProps
136137
const poster = ready?.poster_url;
137138
const message = playbackStateMessage(initialBootstrap, state);
138139
const edgeHint = playbackEdgeHint(initialBootstrap);
140+
const preloadHints = startupPreloadHints(initialBootstrap, startupMode);
139141

140142
const playerId = `rend-embed-${assetId}`;
141143
const sectionStyle = accent ? ({ "--rend-accent": accent } as CSSProperties) : undefined;
@@ -153,6 +155,17 @@ export default async function EmbedPage({ params, searchParams }: EmbedPageProps
153155
<link rel="preconnect" href={edgeHint.origin} crossOrigin="" />
154156
</>
155157
)}
158+
{preloadHints.map((hint) => (
159+
<link
160+
key={`${hint.artifactPath}:${hint.url}`}
161+
rel="preload"
162+
as={hint.as}
163+
href={hint.url}
164+
type={hint.contentType}
165+
crossOrigin="use-credentials"
166+
data-rend-startup-preload={hint.artifactPath}
167+
/>
168+
))}
156169
<section
157170
id={playerId}
158171
className={sectionClassName}

apps/site/app/watch/[assetId]/page.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
safeWatchBootstrapMs,
88
type WatchPlaybackBootstrapResponse,
99
} from "../../../lib/watch-bootstrap.ts";
10+
import { startupPreloadHints } from "../../../lib/player-engine.ts";
1011
import { WatchInstantPlayer } from "./WatchInstantPlayer";
1112

1213
type WatchPageProps = {
@@ -106,7 +107,9 @@ export default async function WatchPage({ params, searchParams }: WatchPageProps
106107
const [{ assetId }, query, headerStore] = await Promise.all([params, searchParams, headers()]);
107108
const initialBootstrap = decodeWatchBootstrapHeader(headerStore.get(WATCH_BOOTSTRAP_HEADER));
108109
const initialBootstrapMs = safeWatchBootstrapMs(headerStore.get(WATCH_BOOTSTRAP_MS_HEADER));
110+
const startupMode = playerStartupMode(query.startupMode, query.startup);
109111
const edgeHint = playbackEdgeHint(initialBootstrap);
112+
const preloadHints = startupPreloadHints(initialBootstrap, startupMode);
110113

111114
return (
112115
<main className="rend-embed-page">
@@ -116,6 +119,17 @@ export default async function WatchPage({ params, searchParams }: WatchPageProps
116119
<link rel="preconnect" href={edgeHint.origin} crossOrigin="" />
117120
</>
118121
)}
122+
{preloadHints.map((hint) => (
123+
<link
124+
key={`${hint.artifactPath}:${hint.url}`}
125+
rel="preload"
126+
as={hint.as}
127+
href={hint.url}
128+
type={hint.contentType}
129+
crossOrigin="use-credentials"
130+
data-rend-startup-preload={hint.artifactPath}
131+
/>
132+
))}
119133
<section className="rend-embed-shell" aria-label="Rend video player">
120134
<WatchInstantPlayer
121135
assetId={assetId}
@@ -124,7 +138,7 @@ export default async function WatchPage({ params, searchParams }: WatchPageProps
124138
initialBootstrap={initialBootstrap}
125139
initialBootstrapMs={initialBootstrapMs}
126140
playbackEngine={playerPlaybackEngine(query.playbackEngine, query.engine)}
127-
startupMode={playerStartupMode(query.startupMode, query.startup)}
141+
startupMode={startupMode}
128142
telemetryAppVersion={telemetryAppVersion()}
129143
telemetryEnabled={telemetryEnabled(query.telemetry)}
130144
telemetryOrganizationId={

apps/site/lib/player-engine.test.mts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,40 @@
11
import assert from "node:assert/strict";
22
import test from "node:test";
3-
import { watchHeartbeatDelta } from "./player-engine.ts";
3+
import { startupPreloadHints, watchHeartbeatDelta } from "./player-engine.ts";
4+
5+
const READY_BOOTSTRAP = {
6+
status: "ready" as const,
7+
asset_id: "00000000-0000-0000-0000-000000000001",
8+
source_state: "uploaded",
9+
playable_state: "hls_ready",
10+
playback_url: "https://ash-1.play.rend.so/v/asset/hls/master.m3u8",
11+
playback_content_type: "application/vnd.apple.mpegurl",
12+
playback_token_expires_at: 1_800_000_000,
13+
ttl_seconds: 900,
14+
opener_url: "https://ash-1.play.rend.so/v/asset/opener.mp4",
15+
opener_content_type: "video/mp4",
16+
manifest_url: "https://ash-1.play.rend.so/v/asset/hls/master.m3u8",
17+
manifest_content_type: "application/vnd.apple.mpegurl",
18+
poster_url: "https://ash-1.play.rend.so/v/asset/thumbnail.jpg",
19+
poster_content_type: "image/jpeg",
20+
prefetch_hints: [
21+
{
22+
artifact_path: "hls/360p/index.m3u8",
23+
url: "https://ash-1.play.rend.so/v/asset/hls/360p/index.m3u8",
24+
content_type: "application/vnd.apple.mpegurl",
25+
},
26+
{
27+
artifact_path: "hls/360p/init_360p.mp4",
28+
url: "https://ash-1.play.rend.so/v/asset/hls/360p/init_360p.mp4",
29+
content_type: "video/mp4",
30+
},
31+
{
32+
artifact_path: "hls/master.m3u8",
33+
url: "https://ash-1.play.rend.so/v/asset/hls/master.m3u8",
34+
content_type: "application/vnd.apple.mpegurl",
35+
},
36+
],
37+
};
438

539
test("watch heartbeat waits for a real interval before emitting", () => {
640
assert.deepEqual(watchHeartbeatDelta(null, 0), {
@@ -38,3 +72,36 @@ test("watch heartbeat clamps large deltas and reseeds after seeks", () => {
3872
deltaMs: null,
3973
});
4074
});
75+
76+
test("startup preload hints prioritize manifest and startup media", () => {
77+
assert.deepEqual(
78+
startupPreloadHints(READY_BOOTSTRAP, "hls").map((hint) => [
79+
hint.artifactPath,
80+
hint.as,
81+
hint.url,
82+
]),
83+
[
84+
[
85+
"hls/master.m3u8",
86+
"fetch",
87+
"https://ash-1.play.rend.so/v/asset/hls/master.m3u8",
88+
],
89+
[
90+
"hls/360p/index.m3u8",
91+
"fetch",
92+
"https://ash-1.play.rend.so/v/asset/hls/360p/index.m3u8",
93+
],
94+
[
95+
"hls/360p/init_360p.mp4",
96+
"fetch",
97+
"https://ash-1.play.rend.so/v/asset/hls/360p/init_360p.mp4",
98+
],
99+
["thumbnail.jpg", "image", "https://ash-1.play.rend.so/v/asset/thumbnail.jpg"],
100+
]
101+
);
102+
});
103+
104+
test("startup preload hints include opener first only when requested", () => {
105+
assert.equal(startupPreloadHints(READY_BOOTSTRAP, "hls")[0]?.artifactPath, "hls/master.m3u8");
106+
assert.equal(startupPreloadHints(READY_BOOTSTRAP, "opener")[0]?.artifactPath, "opener.mp4");
107+
});

apps/site/lib/player-engine.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ export type SourceSelection = {
1313
url: string;
1414
};
1515

16+
export type StartupPreloadHint = {
17+
artifactPath: string;
18+
as: "fetch" | "image" | "video";
19+
contentType?: string;
20+
url: string;
21+
};
22+
1623
export type AttachPlaybackOptions = {
1724
assetId: string;
1825
autoPlay: boolean;
@@ -78,6 +85,7 @@ const HLS_STARTUP_CONFIG = {
7885
const WATCH_HEARTBEAT_INTERVAL_MS = 10_000;
7986
const WATCH_HEARTBEAT_MAX_DELTA_MS = 30_000;
8087
const WATCH_HEARTBEAT_MIN_FORCED_DELTA_MS = 1_000;
88+
const MAX_STARTUP_PRELOAD_HINTS = 10;
8189

8290
export function watchHeartbeatDelta(
8391
previousPositionMs: number | null,
@@ -170,6 +178,60 @@ export function playbackStateMessage(data: WatchPlaybackBootstrapResponse | null
170178
}
171179
}
172180

181+
export function startupPreloadHints(
182+
data: WatchPlaybackBootstrapResponse | null,
183+
startupMode: StartupMode
184+
): StartupPreloadHint[] {
185+
const ready = readyBootstrap(data);
186+
if (!ready) return [];
187+
188+
const seen = new Set<string>();
189+
const hints: StartupPreloadHint[] = [];
190+
const push = (hint: StartupPreloadHint) => {
191+
if (!hint.url || seen.has(hint.url) || hints.length >= MAX_STARTUP_PRELOAD_HINTS) return;
192+
seen.add(hint.url);
193+
hints.push(hint);
194+
};
195+
196+
if (startupMode === "opener" && ready.opener_url) {
197+
push({
198+
artifactPath: "opener.mp4",
199+
as: "video",
200+
contentType: ready.opener_content_type ?? "video/mp4",
201+
url: ready.opener_url,
202+
});
203+
}
204+
205+
if (ready.manifest_url) {
206+
push({
207+
artifactPath: "hls/master.m3u8",
208+
as: "fetch",
209+
contentType: ready.manifest_content_type ?? "application/vnd.apple.mpegurl",
210+
url: ready.manifest_url,
211+
});
212+
}
213+
214+
for (const hint of ready.prefetch_hints) {
215+
push({
216+
artifactPath: hint.artifact_path,
217+
as: "fetch",
218+
contentType: hint.content_type,
219+
url: hint.url,
220+
});
221+
}
222+
223+
if (ready.poster_url) {
224+
push({
225+
artifactPath: "thumbnail.jpg",
226+
as: "image",
227+
contentType: ready.poster_content_type,
228+
url: ready.poster_url,
229+
});
230+
}
231+
232+
return hints;
233+
}
234+
173235
function hlsSource(
174236
data: WatchPlaybackBootstrapReady,
175237
support: { nativeHls: boolean; hlsJs: boolean },

0 commit comments

Comments
 (0)