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> {
|
): Promise<CompressedVideo> {
|
||||||
const {onProgress, signal, onProbe} = opts || {}
|
const {onProgress, signal, onProbe} = opts || {}
|
||||||
|
|
||||||
// Probe data is purely informational - fired into telemetry to validate
|
const isAcceptableFormat = SUPPORTED_MIME_TYPES.includes(
|
||||||
// future smart-skip thresholds. Failures must not block the upload.
|
file.mimeType as SupportedMimeTypes,
|
||||||
if (onProbe && file.mimeType !== 'image/gif') {
|
)
|
||||||
|
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 {
|
try {
|
||||||
onProbe(toProbedMetadata(await probe(file.uri)))
|
metadata = toProbedMetadata(await probe(file.uri))
|
||||||
|
onProbe?.(metadata)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('video probe failed', {safeMessage: 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.
|
// Pre-check the threshold ourselves so we can label the skip in telemetry.
|
||||||
// rnc would do the same skip internally via minimumFileSizeForCompress, but
|
// rnc would do the same skip internally via minimumFileSizeForCompress, but
|
||||||
// that path is invisible to us.
|
// that path is invisible to us.
|
||||||
const isAcceptableFormat = SUPPORTED_MIME_TYPES.includes(
|
if (isBelowByteThreshold && !metadata?.isHDR) {
|
||||||
file.mimeType as SupportedMimeTypes,
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
isAcceptableFormat &&
|
|
||||||
file.fileSize != null &&
|
|
||||||
file.fileSize < MIN_SIZE_FOR_COMPRESSION_BYTES
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
uri: file.uri,
|
uri: file.uri,
|
||||||
size: file.fileSize,
|
size: file.fileSize!,
|
||||||
mimeType: file.mimeType ?? 'video/mp4',
|
mimeType: file.mimeType ?? 'video/mp4',
|
||||||
passthroughReason: 'below-byte-threshold',
|
passthroughReason: 'below-byte-threshold',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,9 +50,15 @@ export async function compressVideo(
|
|||||||
// decision. Fires before doCompression so the probed event lands ahead of
|
// decision. Fires before doCompression so the probed event lands ahead of
|
||||||
// compressCompleted/compressSkipped in the funnel. GIFs and missing
|
// compressCompleted/compressSkipped in the funnel. GIFs and missing
|
||||||
// WebCodecs both skip - mediabunny needs a parseable container + decoder.
|
// 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 {
|
try {
|
||||||
onProbe(await probeWithMediaBunny(blob))
|
metadata = await probeWithMediaBunny(blob)
|
||||||
|
onProbe?.(metadata)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('video probe failed', {safeMessage: e})
|
logger.debug('video probe failed', {safeMessage: e})
|
||||||
}
|
}
|
||||||
@@ -73,7 +79,7 @@ export async function compressVideo(
|
|||||||
fallbackReason = 'gif'
|
fallbackReason = 'gif'
|
||||||
} else if (!hasCodecs) {
|
} else if (!hasCodecs) {
|
||||||
fallbackReason = 'no-webcodecs'
|
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'
|
fallbackReason = 'below-byte-threshold'
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user