Skip to content

Commit 77bf2cc

Browse files
committed
perf(media): publish HLS aliases concurrently
1 parent 04679e9 commit 77bf2cc

2 files changed

Lines changed: 49 additions & 25 deletions

File tree

apps/site/components/AssetDetailClient.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,14 @@ type AssetTab = "overview" | "artifacts" | "analytics" | "embed";
3535
type DetailResponse = { status: "ok"; asset: AssetDetail };
3636
type AnalyticsResponse = { status: "ok"; analytics: AssetPlaybackAnalytics };
3737

38-
const MAX_PROCESSING_POLLS = 30;
38+
const MAX_PROCESSING_POLLS = 120;
3939

40-
function pollDelay(attempt: number) {
41-
return Math.min(2_000 * Math.pow(1.35, attempt), 10_000);
40+
function pollDelay(attempt: number, playableState: string) {
41+
const openerReady = playableState === "opener_ready";
42+
return Math.min(
43+
(openerReady ? 1_000 : 1_500) * Math.pow(1.25, attempt),
44+
openerReady ? 5_000 : 3_000
45+
);
4246
}
4347

4448
function formatBytes(value: number | undefined) {
@@ -100,6 +104,7 @@ export default function AssetDetailClient({
100104
const [deleteMessage, setDeleteMessage] = useState("");
101105
const [tab, setTab] = useState<AssetTab>(initialTab);
102106
const pollAttempt = useRef(0);
107+
const lastPlayableState = useRef(initialAsset.playable_state);
103108
const router = useRouter();
104109
const pathname = usePathname();
105110

@@ -127,6 +132,10 @@ export default function AssetDetailClient({
127132
"message" in body && typeof body.message === "string" ? body.message : "Asset refresh failed";
128133
throw new Error(message);
129134
}
135+
if (lastPlayableState.current !== body.asset.playable_state) {
136+
lastPlayableState.current = body.asset.playable_state;
137+
pollAttempt.current = 0;
138+
}
130139
setAsset(body.asset);
131140
return body.asset;
132141
}, [assetId]);
@@ -192,7 +201,7 @@ export default function AssetDetailClient({
192201
setPollError(error instanceof Error ? error.message : "Asset refresh failed");
193202
})
194203
.finally(() => setPollVersion((version) => version + 1));
195-
}, pollDelay(attempt));
204+
}, pollDelay(attempt, asset.playable_state));
196205

197206
return () => window.clearTimeout(timer);
198207
}, [asset.playable_state, deleteState, pollExhausted, pollVersion, refreshAsset]);

services/rend-api/src/media.rs

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use axum::{
2121
routing::get,
2222
};
2323
use bytes::Bytes;
24-
use futures_util::stream;
24+
use futures_util::{StreamExt, TryStreamExt, stream};
2525
use serde::Deserialize;
2626
use sqlx::{PgPool, Postgres, Transaction};
2727
use tokio::{fs, io::AsyncReadExt, net::TcpListener, process::Command, task::JoinHandle, time};
@@ -36,6 +36,7 @@ const HLS_X264_PRESET: &str = "superfast";
3636
const HLS_AUDIO_BITRATE: &str = "96k";
3737
const OPENER_MAX_DIMENSION: i32 = 640;
3838
const OPENER_VIDEO_CRF: &str = "27";
39+
const PRIVATE_ALIAS_RENAME_CONCURRENCY: usize = 16;
3940
const HLS_TARGET_SEGMENT_SECONDS: u32 = 1;
4041
const HLS_FFMPEG_INIT_FILENAME: &str = "init.mp4";
4142
const HLS_DEFAULT_KEYFRAME_INTERVAL_FRAMES: u32 = 30;
@@ -1893,26 +1894,40 @@ async fn promote_private_playback_aliases(
18931894
return Ok(());
18941895
}
18951896

1896-
let mut ordered = artifacts.to_vec();
1897-
ordered.sort_by_key(|artifact| publication_order(&artifact.object_key));
1898-
for artifact in ordered {
1899-
let source_key = uploaded_artifact_object_key(request, &artifact.object_key);
1900-
let destination_key = durable_artifact_storage_key(request, &artifact.object_key);
1901-
if source_key == destination_key {
1902-
continue;
1903-
}
1904-
tigris_metadata_rename(
1905-
&request.s3,
1906-
&request.s3_bucket,
1907-
&source_key,
1908-
&destination_key,
1909-
)
1910-
.await
1911-
.with_context(|| {
1912-
format!(
1913-
"failed to metadata-rename private playback artifact {source_key} to {destination_key}"
1914-
)
1915-
})?;
1897+
// A typical 1080p asset has hundreds of segments. Renaming those aliases
1898+
// serially kept hls_ready hidden for tens of seconds after FFmpeg had
1899+
// finished. Preserve the publication fence and manifest-last ordering,
1900+
// while allowing independent objects in each tier to move concurrently.
1901+
for order in 0..=2 {
1902+
let renames = artifacts
1903+
.iter()
1904+
.copied()
1905+
.filter(|artifact| publication_order(&artifact.object_key) == order)
1906+
.filter_map(|artifact| {
1907+
let source_key = uploaded_artifact_object_key(request, &artifact.object_key);
1908+
let destination_key = durable_artifact_storage_key(request, &artifact.object_key);
1909+
(source_key != destination_key).then_some((source_key, destination_key))
1910+
})
1911+
.collect::<Vec<_>>();
1912+
let s3 = request.s3.clone();
1913+
let bucket = request.s3_bucket.clone();
1914+
stream::iter(renames)
1915+
.map(|(source_key, destination_key)| {
1916+
let s3 = s3.clone();
1917+
let bucket = bucket.clone();
1918+
async move {
1919+
tigris_metadata_rename(&s3, &bucket, &source_key, &destination_key)
1920+
.await
1921+
.with_context(|| {
1922+
format!(
1923+
"failed to metadata-rename private playback artifact {source_key} to {destination_key}"
1924+
)
1925+
})
1926+
}
1927+
})
1928+
.buffer_unordered(PRIVATE_ALIAS_RENAME_CONCURRENCY)
1929+
.try_collect::<Vec<_>>()
1930+
.await?;
19161931
}
19171932
Ok(())
19181933
}

0 commit comments

Comments
 (0)