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:
+17
-13
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user