Compact HE-AAC audio saves: server lane with fdkaac, shrink saved songs from Saved videos

This commit is contained in:
Jonathan Sykes
2026-10-18 17:28:47 +08:00
parent eef9fbf05f
commit b48d36b3dd
12 changed files with 342 additions and 22 deletions

View File

@@ -412,6 +412,19 @@ const SaveQueue = (() => {
// single-request save (HEVC copy this device can't play, too long for the
// server cache, no worker/OPFS sync access), otherwise the save result —
// { ok:false, paused:true } means the partial is kept and will resume.
// Saves that hold the server's compact HE-AAC audio (this device only).
const CompactSaves = (() => {
const KEY = 'ytpCompactSaves';
const read = () => { try { return new Set(JSON.parse(localStorage.getItem(KEY) || '[]')); } catch { return new Set(); } };
const write = (set) => { try { localStorage.setItem(KEY, JSON.stringify([...set])); } catch { /* storage blocked */ } };
return {
has: (id) => read().has(id),
mark(id) { const s = read(); if (!s.has(id)) { s.add(id); write(s); } },
drop(id) { const s = read(); if (s.delete(id)) write(s); },
clear() { write(new Set()); },
};
})();
async function resumableOpfsSave(videoId, qs, prepQs, onProgress, signal) {
if (typeof window.OPFS.downloadVideo !== 'function' || typeof Worker === 'undefined') return null;
const enc = encodeURIComponent(videoId);
@@ -442,7 +455,8 @@ async function resumableOpfsSave(videoId, qs, prepQs, onProgress, signal) {
resumable: { etag: prep.etag, size: prep.size, sha256: prep.sha256 || null, ext: prep.ext || 'mp4' },
onProgress: (received, total, m) => report({ phase: m && m.verifying ? 'verifying' : 'downloading', received, total }),
});
if (w.ok) return { ok: true, cached: true, sha256: w.sha256 || null, expectedSha: w.expectedSha || null, size: w.size || 0 };
if (w.ok) return { ok: true, cached: true, sha256: w.sha256 || null, expectedSha: w.expectedSha || null, size: w.size || 0,
ext: prep.ext || 'mp4', compact: !!prep.compact };
if (w.changed) continue; // the server copy was replaced — start over on the new one
if (w.paused) return { ok: false, paused: true, error: w.error || 'connection lost', received: w.received || 0 };
if (w.fallback) return null;
@@ -451,7 +465,7 @@ async function resumableOpfsSave(videoId, qs, prepQs, onProgress, signal) {
return { ok: false, error: 'the server copy kept changing — try again' };
}
async function opfsDownload(videoId, { mux = false, retry = false, onProgress = null, signal } = {}) {
async function opfsDownload(videoId, { mux = false, retry = false, onProgress = null, signal, compact = false } = {}) {
if (!window.OPFS || !window.OPFS.isSupported()) {
return { ok: false, error: 'OPFS not supported in this browser' };
}
@@ -465,6 +479,9 @@ async function opfsDownload(videoId, { mux = false, retry = false, onProgress =
if (hevcCapable()) params.set('hevc', '1');
// Album-art songs come back as audio + picture instead of a video of one frame.
params.set('still', '1');
// Compact audio (setting, or Shrink in Saved videos): the server's HE-AAC copy.
const he = (compact || data.settings.compactAudio) && !isUploadId(videoId);
if (he) params.set('audio', 'he');
const qs = params.toString();
const url = `/api/download/${encodeURIComponent(videoId)}${qs ? '?' + qs : ''}`;
@@ -477,7 +494,11 @@ async function opfsDownload(videoId, { mux = false, retry = false, onProgress =
return { ok: false, paused: true, error: error.name === 'AbortError' ? 'Save paused' : 'Connection lost' };
}
}
const resumed = await resumableOpfsSave(videoId, qs, params.get('hevc') === '1' ? 'hevc=1&still=1' : 'still=1', onProgress, signal);
const prepParams = new URLSearchParams();
if (params.get('hevc') === '1') prepParams.set('hevc', '1');
prepParams.set('still', '1');
if (he) prepParams.set('audio', 'he');
const resumed = await resumableOpfsSave(videoId, qs, prepParams.toString(), onProgress, signal);
if (resumed) return resumed;
// Preferred path: a dedicated Web Worker does the fetch AND the OPFS writes,
@@ -488,7 +509,7 @@ async function opfsDownload(videoId, { mux = false, retry = false, onProgress =
let workerError = null;
if (typeof window.OPFS.downloadVideo === 'function' && typeof Worker !== 'undefined') {
const w = await window.OPFS.downloadVideo(videoId, url, { signal });
if (w.ok) return { ok: true, cached: true, sha256: w.sha256 || null, expectedSha: w.expectedSha || null, size: w.size || 0 };
if (w.ok) return { ok: true, cached: true, sha256: w.sha256 || null, expectedSha: w.expectedSha || null, size: w.size || 0, ext: w.ext || null };
if (w.paused) return w;
workerError = w.error || null;
}
@@ -570,6 +591,7 @@ async function opfsList() {
async function opfsDelete(videoId) {
if (!window.OPFS || !window.OPFS.isSupported()) return { ok: true };
try { await window.OPFS.deleteVideo(videoId); } catch { /* ignore */ }
CompactSaves.drop(videoId);
if (window.DeviceDB) { await window.DeviceDB.deleteFile(videoId); if (window.P2PClient) window.P2PClient.changed(); }
return { ok: true };
}
@@ -577,6 +599,7 @@ async function opfsDelete(videoId) {
async function opfsClear() {
if (!window.OPFS || !window.OPFS.isSupported()) return { ok: true };
try { await window.OPFS.clearAll(); } catch { /* ignore */ }
CompactSaves.clear();
if (window.DeviceDB) { await window.DeviceDB.clear(); if (window.P2PClient) window.P2PClient.changed(); }
return { ok: true };
}
@@ -674,6 +697,7 @@ const DEFAULT_SETTINGS = {
p2pReceive: true, // fetch from other devices when YouTube and the server can't — ON by default
autoSavePlayed: true, // keep every video you play for offline (background save once playback starts)
saveBeforePlay: false, // download (server-muxed single file) before playing instead of streaming
compactAudio: false, // new saves are compact HE-AAC audio only (~⅓ of an audio save, no video)
repeatMode: 'off', // 'off' | 'all' — repeat the playing list when it ends
loopOne: false, // repeat the single current video
shuffle: false, // shuffle upcoming tracks when playing a list
@@ -1969,6 +1993,58 @@ async function saveStillArt(id) {
return true;
}
// Saved videos: what format a saved file is in.
function savedFormatLabel(it) {
if (isUploadId(it.id)) return 'Library file';
if (!/\.m4a$/i.test(it.name || '')) return 'Video';
return CompactSaves.has(it.id) ? 'Compact audio' : 'Audio';
}
function canShrinkSaved(it) {
const id = it && it.id;
return WEB && !!id && cachedIds.has(id) && !isUploadId(id) && !String(id).startsWith('edit_') && !CompactSaves.has(id);
}
// Swap a saved copy for the server's compact HE-AAC audio. The old file stays
// until the new one is complete, so a failed shrink loses nothing.
async function shrinkSaved(video) {
const id = video && video.id;
if (!canShrinkSaved({ id })) return { ok: false, error: 'nothing to shrink' };
if (downloading.has(id)) return { ok: false, error: 'it is still saving' };
if (current?.meta?.id === id && current.localUrl) return { ok: false, error: 'it is playing from that file — play something else first' };
const before = (await window.OPFS.getFileObject(id).catch(() => null))?.size || 0;
downloading.add(id);
let res;
try { res = await opfsDownload(id, { compact: true }); }
finally { downloading.delete(id); }
if (!res || !res.ok) return { ok: false, error: (res && res.error) || 'save failed' };
if (res.ext) await window.OPFS.keepOnly(id, `${id}.${res.ext}`);
if (res.compact) CompactSaves.mark(id);
await saveStillArt(id);
// An audio save is not the server's video file: stop offering it to other devices.
if (window.DeviceDB && /^m4a$/i.test(res.ext || '')) { await window.DeviceDB.deleteFile(id).catch(() => {}); window.P2PClient?.changed(); }
const after = (await window.OPFS.getFileObject(id).catch(() => null))?.size || 0;
return { ok: true, compact: !!res.compact, before, after };
}
// Shrink several saves one after another (the server converts each once).
let shrinkRunning = false;
async function shrinkSavedAll(ids) {
if (shrinkRunning) { toast('Already shrinking saved songs'); return; }
shrinkRunning = true;
let done = 0, saved = 0, failed = 0;
try {
for (const id of ids) {
if (current?.meta?.id === id) continue; // playing from that file right now
toast(`Shrinking saved songs… ${done + failed + 1}/${ids.length}`, { duration: 2500 });
const r = await shrinkSaved(videoById(id) || { id });
if (r.ok) { done++; saved += Math.max(0, r.before - r.after); } else failed++;
}
} finally { shrinkRunning = false; }
toast(`Shrunk ${done} song${done === 1 ? '' : 's'} · freed ${fmtBytes(saved)}${failed ? ` · ${failed} could not be shrunk` : ''}`, { duration: 6000 });
if (view.type === 'saved') renderSaved();
}
// Playback fields for a saved still-picture song: play it as audio and show
// its saved picture. {} for anything else.
async function localStill(id) {
@@ -2027,6 +2103,7 @@ async function preload(video, { quiet = false, mux = false, retry = false, direc
cacheMutations++;
// A still-picture song saved as audio isn't the server's video file, so
// it is never offered to other devices as one.
if (res.compact) CompactSaves.mark(id); else CompactSaves.drop(id);
saveStillArt(id).then((still) => { if (!still) recordDeviceFile(id, res); });
warmThumb(thumbUrlFor(id, video));
if (!quiet) toast(`Saved “${video.title}” ✓`);