From 36e142c6bec73dc2ca76e71720b3c208121c6322 Mon Sep 17 00:00:00 2001 From: vineyardbovines Date: Thu, 13 Aug 2026 11:14:37 -0400 Subject: [PATCH] APP-2866 Compress HDR videos before upload --- .../media/video/__tests__/compress.test.ts | 67 +++++++++++++++++++ src/lib/media/video/compress.ts | 30 +++++---- src/lib/media/video/compress.web.ts | 12 +++- 3 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 src/lib/media/video/__tests__/compress.test.ts diff --git a/src/lib/media/video/__tests__/compress.test.ts b/src/lib/media/video/__tests__/compress.test.ts new file mode 100644 index 0000000000..c410182002 --- /dev/null +++ b/src/lib/media/video/__tests__/compress.test.ts @@ -0,0 +1,67 @@ +import {compress, probe} from '@bsky.app/video-compressor' + +import {compressVideo} from '../compress' + +jest.mock('@bsky.app/video-compressor', () => ({ + compress: jest.fn(), + probe: jest.fn(), +})) + +const mockCompress = jest.mocked(compress) +const mockProbe = jest.mocked(probe) + +const asset = { + uri: 'file:///video.mov', + width: 1920, + height: 1080, + type: 'video' as const, + mimeType: 'video/quicktime', + fileSize: 10 * 1024 * 1024, +} + +const metadata = { + width: 1920, + height: 1080, + duration: 10, + bitrate: 8_000_000, + fileSize: asset.fileSize, + mimeType: asset.mimeType, + codec: 'hevc', + hasAudio: true, + frameRate: 30, + rotation: 0, + isHDR: false, +} + +describe('compressVideo', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('compresses an HDR video below the normal byte threshold', async () => { + mockProbe.mockResolvedValue({...metadata, isHDR: true}) + mockCompress.mockResolvedValue({ + uri: 'file:///compressed.mp4', + size: 5_000_000, + mimeType: 'video/mp4', + }) + + await compressVideo(asset) + + expect(mockCompress).toHaveBeenCalledWith( + asset.uri, + expect.objectContaining({passthroughBelowBytes: 0}), + expect.any(Object), + ) + }) + + it('still skips a non-HDR video below the byte threshold', async () => { + mockProbe.mockResolvedValue(metadata) + + await expect(compressVideo(asset)).resolves.toMatchObject({ + uri: asset.uri, + passthroughReason: 'below-byte-threshold', + }) + expect(mockCompress).not.toHaveBeenCalled() + }) +}) diff --git a/src/lib/media/video/compress.ts b/src/lib/media/video/compress.ts index d38c1cdecf..537724680e 100644 --- a/src/lib/media/video/compress.ts +++ b/src/lib/media/video/compress.ts @@ -21,11 +21,22 @@ export async function compressVideo( ): Promise { const {onProgress, signal, onProbe} = opts || {} - // Probe data is purely informational - fired into telemetry to validate - // future smart-skip thresholds. Failures must not block the upload. - if (onProbe && file.mimeType !== 'image/gif') { + const isAcceptableFormat = SUPPORTED_MIME_TYPES.includes( + file.mimeType as SupportedMimeTypes, + ) + const isBelowByteThreshold = + isAcceptableFormat && + file.fileSize != null && + file.fileSize < MIN_SIZE_FOR_COMPRESSION_BYTES + let metadata: ProbedMetadata | undefined + + // Probe data feeds telemetry and lets small HDR inputs bypass the normal + // size-based skip so they are transcoded instead of looking washed out. + // Failures must not block the upload. + if ((onProbe || isBelowByteThreshold) && file.mimeType !== 'image/gif') { try { - onProbe(toProbedMetadata(await probe(file.uri))) + metadata = toProbedMetadata(await probe(file.uri)) + onProbe?.(metadata) } catch (e) { logger.debug('video probe failed', {safeMessage: e}) } @@ -46,17 +57,10 @@ export async function compressVideo( // Pre-check the threshold ourselves so we can label the skip in telemetry. // rnc would do the same skip internally via minimumFileSizeForCompress, but // that path is invisible to us. - const isAcceptableFormat = SUPPORTED_MIME_TYPES.includes( - file.mimeType as SupportedMimeTypes, - ) - if ( - isAcceptableFormat && - file.fileSize != null && - file.fileSize < MIN_SIZE_FOR_COMPRESSION_BYTES - ) { + if (isBelowByteThreshold && !metadata?.isHDR) { return { uri: file.uri, - size: file.fileSize, + size: file.fileSize!, mimeType: file.mimeType ?? 'video/mp4', passthroughReason: 'below-byte-threshold', } diff --git a/src/lib/media/video/compress.web.ts b/src/lib/media/video/compress.web.ts index aba5f1ec0a..b91d1c241c 100644 --- a/src/lib/media/video/compress.web.ts +++ b/src/lib/media/video/compress.web.ts @@ -50,9 +50,15 @@ export async function compressVideo( // decision. Fires before doCompression so the probed event lands ahead of // compressCompleted/compressSkipped in the funnel. GIFs and missing // WebCodecs both skip - mediabunny needs a parseable container + decoder. - if (onProbe && !isGif && hasCodecs) { + let metadata: ProbedMetadata | undefined + if ( + (onProbe || blob.size < COMPRESSION_MIN_SIZE_BYTES) && + !isGif && + hasCodecs + ) { try { - onProbe(await probeWithMediaBunny(blob)) + metadata = await probeWithMediaBunny(blob) + onProbe?.(metadata) } catch (e) { logger.debug('video probe failed', {safeMessage: e}) } @@ -73,7 +79,7 @@ export async function compressVideo( fallbackReason = 'gif' } else if (!hasCodecs) { fallbackReason = 'no-webcodecs' - } else if (blob.size < COMPRESSION_MIN_SIZE_BYTES) { + } else if (blob.size < COMPRESSION_MIN_SIZE_BYTES && !metadata?.isHDR) { fallbackReason = 'below-byte-threshold' } else { try {