Skip to content

Commit 86bacc2

Browse files
committed
Route embeds through API fast player
1 parent cee4823 commit 86bacc2

4 files changed

Lines changed: 70 additions & 3 deletions

File tree

apps/site/next-config.test.mts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ import test from "node:test";
33
import nextConfig from "./next.config.ts";
44

55
test("normal embed route is served by the fast embed shell", async () => {
6+
const redirectsFn = nextConfig.redirects;
7+
if (typeof redirectsFn !== "function") {
8+
throw new Error("nextConfig.redirects must be a function");
9+
}
10+
const redirects = await redirectsFn();
11+
assert.deepEqual(redirects, []);
12+
613
const rewritesFn = nextConfig.rewrites;
714
if (typeof rewritesFn !== "function") {
815
throw new Error("nextConfig.rewrites must be a function");
@@ -17,3 +24,32 @@ test("normal embed route is served by the fast embed shell", async () => {
1724
],
1825
});
1926
});
27+
28+
test("production embed route redirects to the API fast embed shell", async () => {
29+
const original = process.env.REND_SITE_FAST_EMBED_REDIRECT_ORIGIN;
30+
process.env.REND_SITE_FAST_EMBED_REDIRECT_ORIGIN = "https://api.rend.so/";
31+
const { default: productionConfig } = await import(
32+
`./next.config.ts?redirect=${Date.now()}`
33+
);
34+
35+
try {
36+
const redirectsFn = productionConfig.redirects;
37+
if (typeof redirectsFn !== "function") {
38+
throw new Error("nextConfig.redirects must be a function");
39+
}
40+
const redirects = await redirectsFn();
41+
assert.deepEqual(redirects, [
42+
{
43+
source: "/embed/:assetId",
44+
destination: "https://api.rend.so/embed-fast/:assetId",
45+
permanent: false,
46+
},
47+
]);
48+
} finally {
49+
if (original === undefined) {
50+
delete process.env.REND_SITE_FAST_EMBED_REDIRECT_ORIGIN;
51+
} else {
52+
process.env.REND_SITE_FAST_EMBED_REDIRECT_ORIGIN = original;
53+
}
54+
}
55+
});

apps/site/next.config.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
11
import type { NextConfig } from "next";
22

3+
const fastEmbedRedirectOrigin =
4+
process.env.REND_SITE_FAST_EMBED_REDIRECT_ORIGIN?.trim().replace(/\/+$/, "") ||
5+
(process.env.VERCEL_ENV === "production" ? "https://api.rend.so" : "");
6+
37
const nextConfig: NextConfig = {
48
transpilePackages: ["@rend/player"],
9+
async redirects() {
10+
return fastEmbedRedirectOrigin
11+
? [
12+
{
13+
source: "/embed/:assetId",
14+
destination: `${fastEmbedRedirectOrigin}/embed-fast/:assetId`,
15+
permanent: false,
16+
},
17+
]
18+
: [];
19+
},
520
async rewrites() {
621
return {
722
beforeFiles: [

services/rend-api/src/main.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4159,7 +4159,7 @@ body{{overflow:hidden}}
41594159
<div class="rend-fast__message" role="status" aria-live="polite">Ready</div>
41604160
</main>
41614161
<script>
4162-
(()=>{{const root=document.querySelector("[data-rend-player-state]");const video=document.querySelector("video");if(!root||!video)return;const inlineStartup={inline_startup_json};const fallback={fallback_json};const autoPlay={auto_play_js};const fetchCredentials="{fetch_credentials}";const mark=(name)=>{{if(!root.getAttribute(name))root.setAttribute(name,String(Math.round(performance.now())))}};const dims=()=>{{if(video.videoWidth)root.setAttribute("data-rend-selected-width",String(video.videoWidth));if(video.videoHeight)root.setAttribute("data-rend-selected-height",String(video.videoHeight))}};const play=()=>{{if(autoPlay)video.play().catch(()=>{{}})}};const selection=(label,artifactPath,engine)=>{{root.setAttribute("data-rend-player-selected",label);root.setAttribute("data-rend-player-artifact",artifactPath);root.setAttribute("data-rend-playback-engine",engine);root.setAttribute("data-rend-player-state","ready")}};const bytes=(value)=>{{const binary=atob(value);const output=new Uint8Array(binary.length);for(let i=0;i<binary.length;i++)output[i]=binary.charCodeAt(i);return output}};const append=(buffer,data)=>new Promise((resolve,reject)=>{{const done=()=>{{cleanup();resolve()}};const fail=()=>{{cleanup();reject(new Error("append failed"))}};const cleanup=()=>{{buffer.removeEventListener("updateend",done);buffer.removeEventListener("error",fail)}};buffer.addEventListener("updateend",done);buffer.addEventListener("error",fail);buffer.appendBuffer(data)}});const sourceOpen=(mediaSource)=>new Promise((resolve,reject)=>{{if(mediaSource.readyState==="open"){{resolve();return}}const done=()=>{{cleanup();resolve()}};const fail=()=>{{cleanup();reject(new Error("source open failed"))}};const cleanup=()=>{{mediaSource.removeEventListener("sourceopen",done);mediaSource.removeEventListener("sourceclose",fail);mediaSource.removeEventListener("sourceended",fail)}};mediaSource.addEventListener("sourceopen",done,{{once:true}});mediaSource.addEventListener("sourceclose",fail,{{once:true}});mediaSource.addEventListener("sourceended",fail,{{once:true}})}});const fetchSegment=(url)=>fetch(url,{{credentials:fetchCredentials}}).then(response=>{{if(!response.ok)throw new Error("segment fetch failed");return response.arrayBuffer()}}).then(buffer=>new Uint8Array(buffer));const startNative=()=>{{selection(fallback.label,fallback.artifactPath,"native");if(fallback.url&&video.getAttribute("src")!==fallback.url){{video.src=fallback.url;video.load()}}play()}};const startInline=async()=>{{if(!inlineStartup||!("MediaSource"in window)||!MediaSource.isTypeSupported(inlineStartup.mimeType))return false;const pending=[];let nextSegment=0;const pump=()=>{{while(pending.length<4&&nextSegment<inlineStartup.segmentUrls.length)pending.push(fetchSegment(inlineStartup.segmentUrls[nextSegment++]))}};pump();selection("mse_inline",inlineStartup.artifactPath,"mse-inline");const mediaSource=new MediaSource();const objectUrl=URL.createObjectURL(mediaSource);video.removeAttribute("src");video.src=objectUrl;video.load();await sourceOpen(mediaSource);const sourceBuffer=mediaSource.addSourceBuffer(inlineStartup.mimeType);await append(sourceBuffer,bytes(inlineStartup.startup));play();(async()=>{{while(pending.length){{const data=await pending.shift();pump();await append(sourceBuffer,data)}}if(mediaSource.readyState==="open")mediaSource.endOfStream()}})().catch(()=>{{}});return true}};video.addEventListener("loadedmetadata",()=>{{dims();mark("data-rend-metadata-ms")}},{{once:true}});video.addEventListener("canplay",()=>{{dims();mark("data-rend-canplay-ms")}},{{once:true}});video.addEventListener("playing",()=>{{root.setAttribute("data-rend-player-state","playing");dims()}});if("requestVideoFrameCallback"in video){{video.requestVideoFrameCallback(()=>{{dims();mark("data-rend-first-frame-ms")}})}}else{{video.addEventListener("playing",()=>mark("data-rend-first-frame-ms"),{{once:true}})}}startInline().then((started)=>{{if(!started)startNative()}}).catch(()=>startNative())}})();
4162+
(()=>{{const root=document.querySelector("[data-rend-player-state]");const video=document.querySelector("video");if(!root||!video)return;const inlineStartup={inline_startup_json};const fallback={fallback_json};const autoPlay={auto_play_js};const fetchCredentials="{fetch_credentials}";const mark=(name)=>{{if(!root.getAttribute(name))root.setAttribute(name,String(Math.round(performance.now())))}};const dims=()=>{{if(video.videoWidth)root.setAttribute("data-rend-selected-width",String(video.videoWidth));if(video.videoHeight)root.setAttribute("data-rend-selected-height",String(video.videoHeight))}};const play=()=>{{if(autoPlay)video.play().catch(()=>{{}})}};const selection=(label,artifactPath,engine)=>{{root.setAttribute("data-rend-player-selected",label);root.setAttribute("data-rend-player-artifact",artifactPath);root.setAttribute("data-rend-playback-engine",engine);root.setAttribute("data-rend-player-state","ready")}};const bytes=(value)=>{{const binary=atob(value);const output=new Uint8Array(binary.length);for(let i=0;i<binary.length;i++)output[i]=binary.charCodeAt(i);return output}};const append=(buffer,data)=>new Promise((resolve,reject)=>{{const done=()=>{{cleanup();resolve()}};const fail=()=>{{cleanup();reject(new Error("append failed"))}};const cleanup=()=>{{buffer.removeEventListener("updateend",done);buffer.removeEventListener("error",fail)}};buffer.addEventListener("updateend",done);buffer.addEventListener("error",fail);buffer.appendBuffer(data)}});const sourceOpen=(mediaSource)=>new Promise((resolve,reject)=>{{if(mediaSource.readyState==="open"){{resolve();return}}const done=()=>{{cleanup();resolve()}};const fail=()=>{{cleanup();reject(new Error("source open failed"))}};const cleanup=()=>{{mediaSource.removeEventListener("sourceopen",done);mediaSource.removeEventListener("sourceclose",fail);mediaSource.removeEventListener("sourceended",fail)}};mediaSource.addEventListener("sourceopen",done,{{once:true}});mediaSource.addEventListener("sourceclose",fail,{{once:true}});mediaSource.addEventListener("sourceended",fail,{{once:true}})}});const fetchSegment=(url)=>fetch(url,{{credentials:fetchCredentials}}).then(response=>{{if(!response.ok)throw new Error("segment fetch failed");return response.arrayBuffer()}}).then(buffer=>new Uint8Array(buffer));const bufferedAhead=()=>{{try{{for(let index=0;index<video.buffered.length;index++){{const start=video.buffered.start(index);const end=video.buffered.end(index);if(start<=video.currentTime&&end>video.currentTime)return end-video.currentTime}}}}catch{{}}return 0}};const waitForBufferRoom=()=>new Promise(resolve=>{{if(bufferedAhead()<8){{resolve();return}}const cleanup=()=>{{video.removeEventListener("timeupdate",tick);video.removeEventListener("playing",tick);clearTimeout(timeout)}};const tick=()=>{{if(bufferedAhead()<5){{cleanup();resolve()}}}};const timeout=setTimeout(()=>{{cleanup();resolve()}},1000);video.addEventListener("timeupdate",tick);video.addEventListener("playing",tick)}});const startNative=()=>{{selection(fallback.label,fallback.artifactPath,"native");if(fallback.url&&video.getAttribute("src")!==fallback.url){{video.src=fallback.url;video.load()}}play()}};const startInline=async()=>{{if(!inlineStartup||!("MediaSource"in window)||!MediaSource.isTypeSupported(inlineStartup.mimeType))return false;selection("mse_inline",inlineStartup.artifactPath,"mse-inline");const mediaSource=new MediaSource();const objectUrl=URL.createObjectURL(mediaSource);video.removeAttribute("src");video.src=objectUrl;video.load();await sourceOpen(mediaSource);const sourceBuffer=mediaSource.addSourceBuffer(inlineStartup.mimeType);await append(sourceBuffer,bytes(inlineStartup.startup));play();(async()=>{{for(const url of inlineStartup.segmentUrls){{await waitForBufferRoom();await append(sourceBuffer,await fetchSegment(url))}}if(mediaSource.readyState==="open")mediaSource.endOfStream()}})().catch(()=>{{}});return true}};video.addEventListener("loadedmetadata",()=>{{dims();mark("data-rend-metadata-ms")}},{{once:true}});video.addEventListener("canplay",()=>{{dims();mark("data-rend-canplay-ms")}},{{once:true}});video.addEventListener("playing",()=>{{root.setAttribute("data-rend-player-state","playing");dims()}});if("requestVideoFrameCallback"in video){{video.requestVideoFrameCallback(()=>{{dims();mark("data-rend-first-frame-ms")}})}}else{{video.addEventListener("playing",()=>mark("data-rend-first-frame-ms"),{{once:true}})}}startInline().then((started)=>{{if(!started)startNative()}}).catch(()=>startNative())}})();
41634163
</script>
41644164
</body>
41654165
</html>"#,
@@ -4502,17 +4502,22 @@ fn playback_cookie_header(
45024502
playback_base_url: &str,
45034503
cookie_domain: Option<&str>,
45044504
) -> String {
4505+
let is_secure = playback_base_url.starts_with("https://");
45054506
let mut parts = vec![
45064507
format!("{PLAYBACK_COOKIE_NAME}={token}"),
45074508
"Path=/v/".to_owned(),
45084509
format!("Max-Age={ttl_seconds}"),
45094510
"HttpOnly".to_owned(),
4510-
"SameSite=Lax".to_owned(),
4511+
if is_secure {
4512+
"SameSite=None".to_owned()
4513+
} else {
4514+
"SameSite=Lax".to_owned()
4515+
},
45114516
];
45124517
if let Some(domain) = cookie_domain {
45134518
parts.push(format!("Domain={domain}"));
45144519
}
4515-
if playback_base_url.starts_with("https://") {
4520+
if is_secure {
45164521
parts.push("Secure".to_owned());
45174522
}
45184523
parts.join("; ")

services/rend-api/src/tests.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,6 +1537,12 @@ fn playback_bootstrap_urls_are_tokenless_and_cookie_carries_playback_token() {
15371537
"https://edge.local",
15381538
Some("rend.so"),
15391539
);
1540+
let local_cookie = playback_cookie_header(
1541+
&response.playback_token,
1542+
response.ttl_seconds,
1543+
"http://edge.local",
1544+
None,
1545+
);
15401546

15411547
assert_eq!(
15421548
playback_url,
@@ -1550,7 +1556,10 @@ fn playback_bootstrap_urls_are_tokenless_and_cookie_carries_playback_token() {
15501556
assert!(cookie.starts_with("__rend_playback="));
15511557
assert!(cookie.contains("; Domain=rend.so"));
15521558
assert!(cookie.contains("; HttpOnly"));
1559+
assert!(cookie.contains("; SameSite=None"));
15531560
assert!(cookie.contains("; Secure"));
1561+
assert!(local_cookie.contains("; SameSite=Lax"));
1562+
assert!(!local_cookie.contains("; Secure"));
15541563
assert_eq!(claims.asset_id, "asset-123");
15551564
assert_eq!(claims.organization_id.as_deref(), Some(LOCAL_ORG_ID));
15561565
assert_eq!(claims.exp, NOW + 600);
@@ -1624,6 +1633,8 @@ fn api_fast_embed_can_render_inline_mse_startup_without_token_material() {
16241633
assert!(html.contains("data-rend-playback-engine=\"mse-inline\""));
16251634
assert!(html.contains("\"mimeType\":\"video/mp4; codecs=\\\"avc1.64001f,mp4a.40.2\\\"\""));
16261635
assert!(html.contains("segment_00001.m4s"));
1636+
assert!(html.contains("waitForBufferRoom"));
1637+
assert!(!html.contains("pending.length<4"));
16271638
assert!(html.contains("rel=\"preload\" as=\"fetch\""));
16281639
assert!(!html.contains("src=\"https://api.rend.so/v/asset-123/hls/360p/progressive.mp4\""));
16291640
assert!(!html.contains(&response.playback_token), "{html}");

0 commit comments

Comments
 (0)