fix: stop playback stutter and save failures via async yt-dlp, per-download OPFS workers, lazy playback blobs, GPU composite trims, and landscape pane scrolling
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
import { Hono } from 'hono';
|
||||
import { serveStatic } from 'hono/bun';
|
||||
import { logger } from 'hono/logger';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { createServer } from 'node:http';
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
@@ -82,19 +82,27 @@ const CHANNEL_LIMIT = 60;
|
||||
// yt-dlp helpers
|
||||
// ============================================================================
|
||||
|
||||
// Run yt-dlp synchronously and return stdout as a string.
|
||||
// Throws on non-zero exit.
|
||||
// Run yt-dlp asynchronously and resolve stdout as a string.
|
||||
// MUST stay async (spawn, not spawnSync): a sync child process blocks Bun's
|
||||
// event loop for the full yt-dlp runtime (~2-3s per call), which stalls every
|
||||
// concurrent request — including in-flight /api/download proxy streams, which
|
||||
// Bun then kills at its idle timeout ("fetch failed" mid-download on clients).
|
||||
// Rejects on non-zero exit.
|
||||
function runYtdlp(args) {
|
||||
const result = spawnSync(YTDLP, args, {
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 32 * 1024 * 1024, // 32 MB — large channel dumps can be big
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(YTDLP, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let out = '';
|
||||
let err = '';
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stderr.setEncoding('utf8');
|
||||
child.stdout.on('data', (d) => { out += d; });
|
||||
child.stderr.on('data', (d) => { err += d; });
|
||||
child.on('error', (e) => reject(new Error('yt-dlp not found: ' + e.message)));
|
||||
child.on('close', (code) => {
|
||||
if (code !== 0) reject(new Error(err.trim() || 'yt-dlp exited with code ' + code));
|
||||
else resolve(out);
|
||||
});
|
||||
});
|
||||
if (result.error) throw new Error('yt-dlp not found: ' + result.error.message);
|
||||
if (result.status !== 0) {
|
||||
const err = (result.stderr || '').trim();
|
||||
throw new Error(err || 'yt-dlp exited with code ' + result.status);
|
||||
}
|
||||
return result.stdout || '';
|
||||
}
|
||||
|
||||
// Helpers to pick the right field from a yt-dlp JSON record
|
||||
@@ -180,7 +188,7 @@ app.get('/api/search', async (c) => {
|
||||
if (!q) return c.json({ ok: false, error: 'empty query' }, 400);
|
||||
|
||||
try {
|
||||
const out = runYtdlp([
|
||||
const out = await runYtdlp([
|
||||
`ytsearch${SEARCH_LIMIT}:${q}`,
|
||||
'--dump-json', '--flat-playlist',
|
||||
'--no-warnings', '--ignore-errors',
|
||||
@@ -198,7 +206,7 @@ app.get('/api/channel', async (c) => {
|
||||
|
||||
try {
|
||||
const url = channelToUrl(chan);
|
||||
const out = runYtdlp([
|
||||
const out = await runYtdlp([
|
||||
url,
|
||||
'--dump-json', '--flat-playlist',
|
||||
'--no-warnings', '--ignore-errors',
|
||||
@@ -231,7 +239,7 @@ app.get('/api/streams', async (c) => {
|
||||
|
||||
try {
|
||||
const url = `https://www.youtube.com/watch?v=${videoId}`;
|
||||
const out = runYtdlp(['-J', '--no-warnings', url]);
|
||||
const out = await runYtdlp(['-J', '--no-warnings', url]);
|
||||
const info = JSON.parse(out);
|
||||
const formats = Array.isArray(info.formats) ? info.formats : [];
|
||||
|
||||
@@ -300,7 +308,7 @@ app.get('/api/download/:videoId', async (c) => {
|
||||
// --get-url with bestvideo+bestaudio/best format is not what we want
|
||||
// here — we need a SINGLE FILE so no ffmpeg muxing is required in the
|
||||
// browser. Use -f "bestvideo[ext=mp4][acodec!=none]/best[ext=mp4]/best"
|
||||
const out = runYtdlp([
|
||||
const out = await runYtdlp([
|
||||
url,
|
||||
'--no-warnings',
|
||||
'-f', 'bestvideo[ext=mp4][acodec!=none]/bestvideo[acodec!=none]/best[ext=mp4]/best',
|
||||
@@ -430,6 +438,10 @@ async function main() {
|
||||
Bun.serve({
|
||||
port: PORT,
|
||||
fetch: app.fetch,
|
||||
// Default is 10s, which killed /api/download proxy streams whenever the
|
||||
// connection went idle mid-transfer. 240s covers slow saves; Bun caps
|
||||
// this field at 255.
|
||||
idleTimeout: 240,
|
||||
});
|
||||
|
||||
console.log(`[ytplayer] Listening → http://localhost:${PORT}`);
|
||||
|
||||
Reference in New Issue
Block a user