Compact HE-AAC audio saves: server lane with fdkaac, shrink saved songs from Saved videos
This commit is contained in:
@@ -1348,6 +1348,9 @@ const media = createMediaCache({
|
||||
},
|
||||
ffmpeg: FFMPEG,
|
||||
ffprobe: process.env.FFPROBE_PATH || 'ffprobe',
|
||||
// Compact HE-AAC audio for phone saves (?audio=he). MEDIA_COMPACT_AUDIO=0 turns it off.
|
||||
fdkaac: process.env.FDKAAC_PATH || 'fdkaac',
|
||||
compact: { enabled: process.env.MEDIA_COMPACT_AUDIO !== '0', bitrate: envNum('MEDIA_COMPACT_BITRATE', 48000) },
|
||||
maxBytes: envNum('MEDIA_CACHE_MAX_BYTES', 10 * 1024 ** 3),
|
||||
minFreeBytes: envNum('MEDIA_MIN_FREE_BYTES', 5 * 1024 ** 3),
|
||||
autoMaxSeconds: envNum('MEDIA_AUTO_MAX_SECONDS', 3 * 3600),
|
||||
@@ -1738,6 +1741,20 @@ registerDownloadRetryRoute(app, {
|
||||
media, priority: HIGH, clearHints: id => { prepareSkips.delete(id); streamCache.delete(id); },
|
||||
});
|
||||
const DOWNLOAD_EXPOSE = 'X-Content-SHA256, Content-Range, Content-Length, ETag, Accept-Ranges';
|
||||
function compactDownloadResponse(c, videoId, fp, he) {
|
||||
if (fp && !c.req.header('range')) recordVideoAccess(fp, { id: videoId }).catch(() => {});
|
||||
media.touch(videoId);
|
||||
return rangeFileResponse(c, he.path, 'audio/mp4', 'no-store', {
|
||||
etag: `"${videoId}.${he.gen}.he"`,
|
||||
headers: {
|
||||
'Content-Disposition': `attachment; filename="${videoId}.m4a"`,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Expose-Headers': DOWNLOAD_EXPOSE,
|
||||
...(he.sha256 ? { 'X-Content-SHA256': he.sha256 } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function cachedDownloadResponse(c, videoId, fp, row) {
|
||||
if (fp && !c.req.header('range')) recordVideoAccess(fp, { id: videoId }).catch(() => {});
|
||||
media.touch(videoId);
|
||||
@@ -1797,6 +1814,20 @@ app.get('/api/download/:videoId/prepare', async (c) => {
|
||||
if (ready) {
|
||||
// ?still=1: the device would rather keep the audio + picture of an
|
||||
// album-art song than a video of one unchanging frame.
|
||||
const stillArt = stillAudioPath(videoId, ready) ? { still: true, art: `/api/media/${videoId}/art?g=${ready.gen}` } : {};
|
||||
// ?audio=he: the phone saves the compact HE-AAC audio (~48 kbps) instead.
|
||||
// It is made once on the server and kept with the copy; until it exists
|
||||
// the device polls like for any other preparing save.
|
||||
if (c.req.query('audio') === 'he') {
|
||||
const he = await media.compactAudio(videoId);
|
||||
if (he.status === 'ready') {
|
||||
media.touch(videoId);
|
||||
return c.json({ ok: true, state: 'ready', gen: he.gen, size: he.size, sha256: he.sha256 || null,
|
||||
etag: `"${videoId}.${he.gen}.he"`, ext: 'm4a', compact: true, ...stillArt }, 200, nocache);
|
||||
}
|
||||
if (he.status === 'working') return c.json({ ok: true, state: 'working', status: 'compressing' }, 200, nocache);
|
||||
// 'off' (no encoder, or this song didn't shrink): the normal answer below.
|
||||
}
|
||||
const stillAudio = c.req.query('still') === '1' ? stillAudioPath(videoId, ready) : null;
|
||||
if (stillAudio) {
|
||||
media.touch(videoId);
|
||||
@@ -1879,6 +1910,10 @@ app.get('/api/download/:videoId', async (c) => {
|
||||
let cacheErr = null;
|
||||
try {
|
||||
const row = await media.ensureCached(videoId, { priority: HIGH });
|
||||
if (c.req.query('audio') === 'he') {
|
||||
const he = await media.compactAudio(videoId);
|
||||
if (he.status === 'ready') return compactDownloadResponse(c, videoId, fp, he);
|
||||
}
|
||||
// A device that can't decode HEVC must not save the HEVC copy — give it
|
||||
// the legacy H.264 save instead (the ?mux=1 path included).
|
||||
if (servableTo(row, c.req.query('hevc') === '1') || stillAudioPath(videoId, row) && c.req.query('still') === '1') {
|
||||
|
||||
Reference in New Issue
Block a user