Support X and Twitter video URLs with reliable resolved playback

Share yt-dlp resolution across YouTube and X/Twitter, preserve separate audio/video inputs and headers, and use finite duration metadata directly. Prefer direct HTTPS formats for X, support multi-video posts and explicit selections, and enforce single-connection hosts before resolution and on resolved sources.

Include the related recorded-media probing, seeking recovery, and metadata polling fixes, with regression coverage for playback and protected sources.
This commit is contained in:
2026-10-23 00:26:57 -07:00
parent 91e96894b7
commit 87286c748b
8 changed files with 722 additions and 64 deletions
+17 -13
View File
@@ -68,7 +68,6 @@ const FRAME_UNPAINTED_PACKET_RESET_MS = 20000;
const PLAYBACK_RESTART_COOLDOWN_MS = 15000;
const PLAYBACK_RESTART_DELAY_MS = 750;
const MIN_RECOVERY_RESUME_SECONDS = 2;
const METADATA_REFRESH_ATTEMPTS = 20;
const METADATA_REFRESH_INTERVAL_MS = 650;
const state = {
@@ -487,7 +486,6 @@ function startSession(session) {
connectPlaybackStreams();
startFrameWatchdog();
void refreshSessionMetadata(session.id);
}
function connectPlaybackStreams() {
@@ -546,6 +544,7 @@ function connectPlaybackStreams() {
elements.audio.load();
void playAudio();
startRenderLoop();
void refreshSessionMetadata(session.id);
}
async function playAudio() {
@@ -1356,23 +1355,25 @@ async function seekTo(value) {
}
async function refreshSessionMetadata(sessionId) {
const generation = state.generation;
for (let attempt = 0; attempt < METADATA_REFRESH_ATTEMPTS; attempt += 1) {
if (generation !== state.generation || state.session?.id !== sessionId) {
return;
}
const streamGeneration = state.streamGeneration;
// The server bounds probe retries. Keep polling for their full lifetime,
// including after reconnects, instead of giving up before a slow probe ends.
while (streamGeneration === state.streamGeneration && state.session?.id === sessionId && state.session.metadataStatus === 'pending') {
try {
const response = await fetch(`/api/session/${encodeURIComponent(sessionId)}`, { cache: 'no-store' });
if (!response.ok) {
return;
if (response.status === 404) {
return;
}
throw new Error('Metadata temporarily unavailable.');
}
const payload = await response.json();
if (generation !== state.generation || state.session?.id !== sessionId) {
if (streamGeneration !== state.streamGeneration || state.session?.id !== sessionId) {
return;
}
@@ -1383,7 +1384,7 @@ async function refreshSessionMetadata(sessionId) {
return;
}
} catch {
return;
// A transient network failure should not leave seeking disabled either.
}
await delay(METADATA_REFRESH_INTERVAL_MS);
@@ -1391,11 +1392,14 @@ async function refreshSessionMetadata(sessionId) {
}
function updateSessionMetadata(payload) {
if ('duration' in payload) {
// A delayed WebSocket ready message can still contain the initial null
// duration after polling has already received the completed probe result.
const hasDuration = normalizeDuration(payload.duration) !== null;
if ('duration' in payload && (hasDuration || state.duration === null)) {
state.duration = normalizeDuration(payload.duration);
}
if ('seekable' in payload) {
if ('seekable' in payload && (hasDuration || state.duration === null)) {
state.seekable = Boolean(payload.seekable);
}