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:
@@ -699,7 +699,11 @@ export async function issuePublicHlsCookie(assetId: string) {
|
||||
|
||||
// --- HLS upload lifecycle ---
|
||||
|
||||
async function deleteHlsAssetObjects(bucket: string, prefix: string): Promise<void> {
|
||||
function sleepMs(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function deleteHlsAssetObjectsOnce(bucket: string, prefix: string): Promise<void> {
|
||||
const { DeleteObjectsCommand, ListObjectsV2Command } = await import('@aws-sdk/client-s3');
|
||||
const client = getS3Client();
|
||||
|
||||
@@ -721,6 +725,66 @@ async function deleteHlsAssetObjects(bucket: string, prefix: string): Promise<vo
|
||||
} while (continuationToken);
|
||||
}
|
||||
|
||||
async function deleteHlsAssetObjects(bucket: string, prefix: string, maxAttempts = 3): Promise<void> {
|
||||
let lastError: unknown;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
try {
|
||||
await deleteHlsAssetObjectsOnce(bucket, prefix);
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
console.error(`deleteHlsAssetObjects attempt ${attempt}/${maxAttempts} failed for prefix ${prefix}`, error);
|
||||
if (attempt < maxAttempts) {
|
||||
await sleepMs(Math.min(500 * 2 ** (attempt - 1), 4000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError instanceof Error ? lastError : new Error(`Failed to delete HLS objects under ${prefix}`);
|
||||
}
|
||||
|
||||
async function writeHlsManifestBody(bucket: string, key: string, body: string): Promise<void> {
|
||||
const { PutObjectCommand } = await import('@aws-sdk/client-s3');
|
||||
const client = getS3Client();
|
||||
await client.send(
|
||||
new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
Body: body,
|
||||
ContentType: 'application/vnd.apple.mpegurl'
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function stripP1080FromMasterManifest(manifest: string): string {
|
||||
const lines = manifest.split('\n');
|
||||
const result: string[] = [];
|
||||
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const line = lines[index]?.trim() ?? '';
|
||||
if (line === 'p1080/playlist.m3u8') {
|
||||
const previous = result[result.length - 1]?.trim() ?? '';
|
||||
if (previous.startsWith('#EXT-X-STREAM-INF')) {
|
||||
result.pop();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('#EXT-X-STREAM-INF')) {
|
||||
const nextLine = lines[index + 1]?.trim() ?? '';
|
||||
if (nextLine === 'p1080/playlist.m3u8') {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result.push(lines[index]!);
|
||||
}
|
||||
|
||||
return `${result.join('\n').trimEnd()}\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort a `processing` HLS asset: clean up any partial R2 objects under
|
||||
* `videos/{assetId}/` and delete the asset row. Idempotent — caller can
|
||||
@@ -729,12 +793,23 @@ async function deleteHlsAssetObjects(bucket: string, prefix: string): Promise<vo
|
||||
*/
|
||||
export async function abortHlsAssetService(orgId: string, assetId: string) {
|
||||
try {
|
||||
const asset = assertAssetExists(await getAssetById(assetId, orgId));
|
||||
const config = getStorageConfig();
|
||||
const asset = await getAssetById(assetId, orgId);
|
||||
|
||||
if (!asset) {
|
||||
try {
|
||||
await deleteHlsAssetObjects(config.bucketVideos, `${assetId}/`);
|
||||
} catch (error) {
|
||||
console.error('abortHlsAssetService: orphan sweep failed for missing asset row', error);
|
||||
}
|
||||
|
||||
throw new AppError('Asset not found', ErrorCodes.ASSET_NOT_FOUND, 404);
|
||||
}
|
||||
|
||||
if (asset.status !== 'processing') {
|
||||
throw new AppError('Asset is not in processing state', ErrorCodes.VALIDATION_ERROR, 409);
|
||||
}
|
||||
|
||||
const config = getStorageConfig();
|
||||
try {
|
||||
await deleteHlsAssetObjects(config.bucketVideos, `${assetId}/`);
|
||||
} catch (error) {
|
||||
@@ -755,6 +830,53 @@ export async function abortHlsAssetService(orgId: string, assetId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop a failed manual p1080 rendition: delete partial `p1080/` objects,
|
||||
* restore the master manifest if it was patched, and reset rendition metadata.
|
||||
*/
|
||||
export async function abortHls1080RenditionService(orgId: string, assetId: string) {
|
||||
try {
|
||||
const asset = assertAssetExists(await getAssetById(assetId, orgId));
|
||||
if (asset.status !== 'active' || !asset.hlsManifestKey) {
|
||||
throw new AppError('Asset has no active HLS manifest', ErrorCodes.VALIDATION_ERROR, 409);
|
||||
}
|
||||
|
||||
const config = getStorageConfig();
|
||||
try {
|
||||
await deleteHlsAssetObjects(config.bucketVideos, `${assetId}/p1080/`);
|
||||
} catch (error) {
|
||||
console.error('abortHls1080RenditionService: failed to delete p1080 objects', error);
|
||||
}
|
||||
|
||||
const manifestKey = asset.hlsManifestKey;
|
||||
const manifestBody = await readHlsManifestBody(config.bucketVideos, manifestKey);
|
||||
const strippedManifest = stripP1080FromMasterManifest(manifestBody);
|
||||
if (strippedManifest !== manifestBody) {
|
||||
await writeHlsManifestBody(config.bucketVideos, manifestKey, strippedManifest);
|
||||
}
|
||||
|
||||
const metadata = readHlsAssetMetadata(asset);
|
||||
const renditions = (metadata.hlsRenditions ?? []).filter((rendition) => rendition !== 'p1080');
|
||||
const mergedMetadata = {
|
||||
...(typeof asset.metadata === 'object' && asset.metadata !== null && !Array.isArray(asset.metadata)
|
||||
? asset.metadata
|
||||
: {}),
|
||||
hlsRenditions: renditions,
|
||||
hls1080Status: 'none' as THls1080Status
|
||||
};
|
||||
|
||||
const updated = await updateAsset(assetId, orgId, { metadata: mergedMetadata });
|
||||
return assertAssetExists(updated);
|
||||
} catch (error) {
|
||||
if (error instanceof AppError) throw error;
|
||||
throw new AppError(
|
||||
error instanceof Error ? error.message : 'Failed to abort 1080p HLS rendition',
|
||||
ErrorCodes.ASSET_UPDATE_FAILED,
|
||||
500
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const HLS_CONTENT_TYPES: Record<string, string> = {
|
||||
'.m3u8': 'application/vnd.apple.mpegurl',
|
||||
'.ts': 'video/mp2t',
|
||||
|
||||
Reference in New Issue
Block a user