APP-2866 Compress HDR videos before upload
This commit is contained in:
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -21,11 +21,22 @@ export async function compressVideo(
|
||||
): Promise<CompressedVideo> {
|
||||
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',
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user