feat: make video uploads faster

- Implemented a new endpoint to delete failed manual p1080 renditions, restoring the master manifest and updating metadata.
- Enhanced the HLS upload process with better error handling and cleanup for aborted uploads.
- Introduced tests for HLS encoder functionality to ensure correct selection of rungs based on video source properties.
- Updated video upload component to manage reserved asset IDs and perform cleanup on abo
This commit is contained in:
Rotimi Best
2026-06-07 23:20:42 +01:00
parent 27a612e92f
commit 4eee866e85
6 changed files with 357 additions and 52 deletions
@@ -1,4 +1,4 @@
import { mkdir, stat } from 'node:fs/promises';
import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { tmpdir } from 'node:os';
@@ -45,6 +45,57 @@ function transcodeToMp3(inputPath: string, outputPath: string): Promise<void> {
]);
}
/**
* Download an HLS media playlist and its segment objects from the videos
* bucket into a temp directory so ffmpeg can demux the audio track.
*/
async function downloadHlsPlaylistToTempFile(
bucket: string,
playlistKey: string
): Promise<{ inputPath: string; tempDir: string }> {
const downloadedPlaylist = await downloadObjectToTempFile(bucket, playlistKey, 'playlist.m3u8');
const rawPlaylist = await readFile(downloadedPlaylist, 'utf-8');
await safeUnlink(downloadedPlaylist);
const tempDir = path.join(tmpdir(), 'cio-jobs', 'hls-audio', `${Date.now()}_${process.pid}`);
await mkdir(tempDir, { recursive: true });
const playlistPrefix = path.posix.dirname(playlistKey);
const localLines: string[] = [];
for (const line of rawPlaylist.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
localLines.push(line);
continue;
}
const segmentKey = trimmed.includes('/') ? trimmed : `${playlistPrefix}/${trimmed}`;
const localSegmentPath = path.join(tempDir, path.basename(trimmed));
const downloadedSegment = await downloadObjectToTempFile(bucket, segmentKey, path.basename(trimmed));
await rename(downloadedSegment, localSegmentPath);
localLines.push(path.basename(trimmed));
}
const inputPath = path.join(tempDir, 'playlist.m3u8');
await writeFile(inputPath, localLines.join('\n'));
return { inputPath, tempDir };
}
async function resolveAudioSourcePath(
bucket: string,
storageKey: string
): Promise<{ inputPath: string; tempDir?: string }> {
if (storageKey.endsWith('.m3u8')) {
return downloadHlsPlaylistToTempFile(bucket, storageKey);
}
return {
inputPath: await downloadObjectToTempFile(bucket, storageKey, 'audio-source.bin')
};
}
/**
* Extract a 16 kHz mono MP3 (32 kbps) from the source video for Whisper (25 MB
* API limit). The transcribe step reads the object from the media bucket.
@@ -71,9 +122,12 @@ export async function processExtractAudio(rawData: unknown): Promise<ExtractAudi
});
let inputPath: string | undefined;
let inputTempDir: string | undefined;
let outputPath: string | undefined;
try {
inputPath = await downloadObjectToTempFile(videosBucket(), storageKey, 'audio-source.bin');
const source = await resolveAudioSourcePath(videosBucket(), storageKey);
inputPath = source.inputPath;
inputTempDir = source.tempDir;
const dir = path.join(tmpdir(), 'cio-jobs', 'audio');
await mkdir(dir, { recursive: true });
@@ -112,7 +166,11 @@ export async function processExtractAudio(rawData: unknown): Promise<ExtractAudi
});
throw error;
} finally {
await safeUnlink(inputPath);
if (inputTempDir) {
await rm(inputTempDir, { recursive: true, force: true }).catch(() => undefined);
} else {
await safeUnlink(inputPath);
}
await safeUnlink(outputPath);
}
}