Add expo-bluesky-video-compress native module
Replaces react-native-compressor's video path with a local Expo module. Native pipeline: - iOS: AVAssetReader + AVMutableVideoComposition + AVAssetWriter with VideoToolbox encode, BGRA reader for HDR -> SDR conversion, BT.709 color tagging, DataRateLimits hard cap, AAC re-encode. - Android: MediaExtractor -> MediaCodec(decoder) -> GL pipeline -> MediaCodec(encoder) -> MediaMuxer. BITRATE_MODE_CBR for tight target enforcement, GL transform-matrix rotation (no double-rotation), raw AAC passthrough, hardware encoder selection with software fallback, QTI AVC denylist. Codec selection: 'auto' resolves to h264 (HLS pipeline + licensing). HEVC remains opt-in via codec: 'hevc' for future feature-flagged use. compress.ts adds probe-based smart-skip: clips that are already at or below 5 Mbps / 1920px / 100MB bypass re-encoding entirely. react-native-compressor stays as a dep for now since pickVideo and VideoTranscodeBackdrop still use its non-compression helpers; full removal is a follow-up.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import {type EventSubscription} from 'expo-modules-core'
|
||||
|
||||
import NativeModule from './src/ExpoBlueskyVideoCompressModule'
|
||||
import {
|
||||
type CodecPreference,
|
||||
type CompressCallbacks,
|
||||
type CompressOptions,
|
||||
type CompressResult,
|
||||
type VideoMetadata,
|
||||
} from './src/types'
|
||||
|
||||
export type {
|
||||
CodecPreference,
|
||||
CompressCallbacks,
|
||||
CompressOptions,
|
||||
CompressResult,
|
||||
VideoMetadata,
|
||||
}
|
||||
|
||||
class AbortError extends Error {
|
||||
name = 'AbortError'
|
||||
constructor() {
|
||||
super('Aborted')
|
||||
}
|
||||
}
|
||||
|
||||
let jobIdCounter = 0
|
||||
|
||||
export function probe(uri: string): Promise<VideoMetadata> {
|
||||
return NativeModule.probe(uri)
|
||||
}
|
||||
|
||||
export function compress(
|
||||
uri: string,
|
||||
options: CompressOptions = {},
|
||||
callbacks?: CompressCallbacks,
|
||||
): Promise<CompressResult> {
|
||||
const jobId = ++jobIdCounter
|
||||
let subscription: EventSubscription | undefined
|
||||
|
||||
if (callbacks?.signal?.aborted) {
|
||||
return Promise.reject(new AbortError())
|
||||
}
|
||||
|
||||
const nativeOptions = {
|
||||
targetBitrate: options.targetBitrate ?? 0,
|
||||
maxSize: options.maxSize ?? 1920,
|
||||
codec: options.codec ?? 'auto',
|
||||
frameRateCap: options.frameRateCap ?? 30,
|
||||
jobId,
|
||||
}
|
||||
|
||||
return new Promise<CompressResult>((resolve, reject) => {
|
||||
if (callbacks?.onProgress) {
|
||||
subscription = NativeModule.addListener(
|
||||
'onProgress',
|
||||
(event: {id: number; progress: number}) => {
|
||||
if (event.id === jobId) {
|
||||
callbacks.onProgress!(event.progress)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const abortHandler = () => {
|
||||
NativeModule.cancel()
|
||||
subscription?.remove()
|
||||
reject(new AbortError())
|
||||
}
|
||||
|
||||
if (callbacks?.signal) {
|
||||
callbacks.signal.addEventListener('abort', abortHandler, {once: true})
|
||||
}
|
||||
|
||||
NativeModule.compress(uri, nativeOptions)
|
||||
.then(result => {
|
||||
callbacks?.signal?.removeEventListener('abort', abortHandler)
|
||||
subscription?.remove()
|
||||
resolve(result)
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
callbacks?.signal?.removeEventListener('abort', abortHandler)
|
||||
subscription?.remove()
|
||||
reject(error instanceof Error ? error : new Error(String(error)))
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user