Address review: extract shared compression constants, log empty output
- Move TARGET_BITRATE, MAX_DIMENSION, and the 25 MB skip threshold to a new src/lib/media/video/constants.ts so both compress.web.ts and the upcoming native engine use the same values. Codec lists stay local to compress.web.ts since they are mediabunny-specific. - When mediabunny's BufferTarget returns a null buffer after a clean execute(), log via logger.error so Sentry captures the unexpected state. The outer catch still demotes the throw to a warn and falls back to the original blob, so user-visible behavior is unchanged.
This commit is contained in:
@@ -18,12 +18,13 @@ import {VIDEO_MAX_SIZE} from '#/lib/constants'
|
|||||||
import {VideoTooLargeError} from '#/lib/media/video/errors'
|
import {VideoTooLargeError} from '#/lib/media/video/errors'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {hasWebCodecs} from '#/view/com/composer/videos/metadata'
|
import {hasWebCodecs} from '#/view/com/composer/videos/metadata'
|
||||||
|
import {
|
||||||
|
COMPRESSION_MAX_DIMENSION,
|
||||||
|
COMPRESSION_MIN_SIZE_BYTES,
|
||||||
|
COMPRESSION_TARGET_BITRATE,
|
||||||
|
} from './constants'
|
||||||
import {type CompressedVideo} from './types'
|
import {type CompressedVideo} from './types'
|
||||||
|
|
||||||
const TARGET_BITRATE = 3_000_000 // 3mbps, matches native
|
|
||||||
const MAX_DIMENSION = 1920 // matches native
|
|
||||||
const MIN_SIZE_FOR_COMPRESSION = 25 * 1000 * 1000 // 25mb, matches native
|
|
||||||
|
|
||||||
// Codecs to try in order of preference
|
// Codecs to try in order of preference
|
||||||
// avc (H.264) is most compatible, vp9/vp8 are fallbacks for WebM
|
// avc (H.264) is most compatible, vp9/vp8 are fallbacks for WebM
|
||||||
const VIDEO_CODECS: VideoCodec[] = ['avc', 'hevc', 'vp9', 'vp8']
|
const VIDEO_CODECS: VideoCodec[] = ['avc', 'hevc', 'vp9', 'vp8']
|
||||||
@@ -51,12 +52,12 @@ export async function compressVideo(
|
|||||||
size: blob.size,
|
size: blob.size,
|
||||||
mimeType: blob.type,
|
mimeType: blob.type,
|
||||||
isGif,
|
isGif,
|
||||||
minSizeForCompression: MIN_SIZE_FOR_COMPRESSION,
|
minSizeForCompression: COMPRESSION_MIN_SIZE_BYTES,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Try MediaBunny compression if WebCodecs is available and file is large enough
|
// Try MediaBunny compression if WebCodecs is available and file is large enough
|
||||||
// Skip GIFs - MediaBunny doesn't support them
|
// Skip GIFs - MediaBunny doesn't support them
|
||||||
if (hasWebCodecs() && blob.size >= MIN_SIZE_FOR_COMPRESSION && !isGif) {
|
if (hasWebCodecs() && blob.size >= COMPRESSION_MIN_SIZE_BYTES && !isGif) {
|
||||||
try {
|
try {
|
||||||
return await doCompression(blob, asset.uri, {onProgress, signal})
|
return await doCompression(blob, asset.uri, {onProgress, signal})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -68,7 +69,7 @@ export async function compressVideo(
|
|||||||
logger.debug('compress: skipping compression', {
|
logger.debug('compress: skipping compression', {
|
||||||
hasWebCodecs: hasWebCodecs(),
|
hasWebCodecs: hasWebCodecs(),
|
||||||
blobSize: blob.size,
|
blobSize: blob.size,
|
||||||
minSize: MIN_SIZE_FOR_COMPRESSION,
|
minSize: COMPRESSION_MIN_SIZE_BYTES,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +94,7 @@ async function findEncodableVideoCodec(
|
|||||||
const canEncode = await canEncodeVideo(codec, {
|
const canEncode = await canEncodeVideo(codec, {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
bitrate: TARGET_BITRATE,
|
bitrate: COMPRESSION_TARGET_BITRATE,
|
||||||
})
|
})
|
||||||
logger.debug('compress: checking video codec', {
|
logger.debug('compress: checking video codec', {
|
||||||
codec,
|
codec,
|
||||||
@@ -182,7 +183,7 @@ async function doCompression(
|
|||||||
const {width, height} = calculateDimensions(
|
const {width, height} = calculateDimensions(
|
||||||
videoTrack.displayWidth,
|
videoTrack.displayWidth,
|
||||||
videoTrack.displayHeight,
|
videoTrack.displayHeight,
|
||||||
MAX_DIMENSION,
|
COMPRESSION_MAX_DIMENSION,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug('compress: video dimensions', {
|
logger.debug('compress: video dimensions', {
|
||||||
@@ -232,7 +233,7 @@ async function doCompression(
|
|||||||
output,
|
output,
|
||||||
video: {
|
video: {
|
||||||
codec: codecInfo.codec,
|
codec: codecInfo.codec,
|
||||||
bitrate: TARGET_BITRATE,
|
bitrate: COMPRESSION_TARGET_BITRATE,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
fit: 'contain',
|
fit: 'contain',
|
||||||
@@ -268,7 +269,13 @@ async function doCompression(
|
|||||||
const bytes = target.buffer
|
const bytes = target.buffer
|
||||||
|
|
||||||
if (!bytes) {
|
if (!bytes) {
|
||||||
throw new Error('MediaBunny compression produced no output')
|
// mediabunny's BufferTarget reports a null buffer after a successful
|
||||||
|
// execute(). Should not happen in normal use; recoverable here because
|
||||||
|
// the outer compressVideo() catches and falls back to the original blob,
|
||||||
|
// but worth flagging in Sentry so we can chase the root cause.
|
||||||
|
const err = new Error('Compression produced empty output')
|
||||||
|
logger.error(err, {})
|
||||||
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
const mimeType = codecInfo.useWebM ? 'video/webm' : 'video/mp4'
|
const mimeType = codecInfo.useWebM ? 'video/webm' : 'video/mp4'
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Shared compression knobs. Mirrored between native (compress.ts) and web
|
||||||
|
// (compress.web.ts) so both platforms produce videos with the same target.
|
||||||
|
|
||||||
|
export const COMPRESSION_TARGET_BITRATE = 3_000_000 // 3 Mbps
|
||||||
|
export const COMPRESSION_MAX_DIMENSION = 1920
|
||||||
|
// Web skips compression entirely for files under this size; native applies its
|
||||||
|
// own threshold logic inside expo-bluesky-video-compress's probe step.
|
||||||
|
export const COMPRESSION_MIN_SIZE_BYTES = 25_000_000
|
||||||
Reference in New Issue
Block a user