delete comparison code, react-native-compressor

This commit is contained in:
Samuel Newman
2026-03-11 14:37:11 +02:00
parent 7dee4ac1e1
commit 14b74a2986
17 changed files with 130 additions and 375 deletions
-1
View File
@@ -287,7 +287,6 @@ module.exports = function (_config) {
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
},
],
'react-native-compressor',
[
'@bitdrift/react-native',
{
@@ -39,6 +39,7 @@ object VideoProber {
// Get codec and frame rate from MediaExtractor for more accuracy
val extractor = MediaExtractor()
var codec = "unknown"
var mimeType = "video/mp4"
var extractedFrameRate = frameRate
try {
@@ -52,6 +53,7 @@ object VideoProber {
val format = extractor.getTrackFormat(i)
val mime = format.getString(MediaFormat.KEY_MIME)
if (mime?.startsWith("video/") == true) {
mimeType = mime
codec = mime.removePrefix("video/")
if (format.containsKey(MediaFormat.KEY_FRAME_RATE)) {
extractedFrameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE).toFloat()
@@ -79,6 +81,7 @@ object VideoProber {
"duration" to durationSeconds,
"bitrate" to effectiveBitrate,
"fileSize" to fileSize,
"mimeType" to mimeType,
"codec" to codec,
"hasAudio" to hasAudio,
"frameRate" to extractedFrameRate.toDouble(),
@@ -1,4 +1,5 @@
import AVFoundation
import UniformTypeIdentifiers
struct VideoProber {
static func probe(url: URL) async throws -> [String: Any] {
@@ -49,6 +50,14 @@ struct VideoProber {
fileSize = 0
}
// MIME type from file extension
let mimeType: String
if let utType = UTType(filenameExtension: url.pathExtension) {
mimeType = utType.preferredMIMEType ?? "video/mp4"
} else {
mimeType = "video/mp4"
}
// Bitrate: use estimated data rate, or calculate from file size
let durationSeconds = CMTimeGetSeconds(duration)
var bitrate = Int(estimatedDataRate)
@@ -62,6 +71,7 @@ struct VideoProber {
"duration": durationSeconds,
"bitrate": bitrate,
"fileSize": fileSize,
"mimeType": mimeType,
"codec": codec,
"hasAudio": hasAudio,
"frameRate": nominalFrameRate,
@@ -4,6 +4,7 @@ export type VideoMetadata = {
duration: number // seconds
bitrate: number // bps
fileSize: number // bytes
mimeType: string
codec: string
hasAudio: boolean
frameRate: number
-1
View File
@@ -217,7 +217,6 @@
"react-is": "19",
"react-keyed-flatten-children": "^5.0.0",
"react-native": "0.81.5",
"react-native-compressor": "1.13.0",
"react-native-date-picker": "^5.0.13",
"react-native-device-attest": "^0.1.6",
"react-native-drawer-layout": "^4.2.3",
@@ -1,59 +0,0 @@
diff --git a/android/build.gradle b/android/build.gradle
index 5071139f8ee5fbba085d2afe3b2093de8eda915c..84bee34a238c6510169f6b6bdb0fda0594c77136 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -115,7 +115,6 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"
implementation 'org.mp4parser:isoparser:1.9.56'
- implementation 'com.github.banketree:AndroidLame-kotlin:v0.0.1'
implementation 'javazoom:jlayer:1.0.1'
}
diff --git a/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt b/android/src/main/java/com/reactnativecompressor/Audio/AudioCompressor.kt
deleted file mode 100644
index 9292d3ee50776bd9d7760b8dcf6d123d44b4e31b..0000000000000000000000000000000000000000
diff --git a/android/src/main/java/com/reactnativecompressor/Audio/AudioExtractor.kt b/android/src/main/java/com/reactnativecompressor/Audio/AudioExtractor.kt
deleted file mode 100644
index c6551828014437a14dc8f2f19488b647dba1bbe1..0000000000000000000000000000000000000000
diff --git a/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt b/android/src/main/java/com/reactnativecompressor/Audio/AudioHelper.kt
deleted file mode 100644
index 42040b4916573463415ef2f57789b3c4fa25d135..0000000000000000000000000000000000000000
diff --git a/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt b/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt
index 446d4fb8b69e7cfdb51b29603aa2d52aac1ab8c8..f02190992dac823b6bbf2d77880a25adc48f16c7 100644
--- a/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt
+++ b/android/src/main/java/com/reactnativecompressor/Audio/AudioMain.kt
@@ -11,7 +11,9 @@ class AudioMain(private val reactContext: ReactApplicationContext) {
promise: Promise) {
try {
- AudioCompressor.CompressAudio(fileUrl,optionMap,reactContext,promise)
+ // Skip compression on Android to avoid libandroidlame dependency
+ // Return the original file URL without compression
+ promise.resolve(fileUrl)
} catch (ex: Exception) {
promise.reject(ex)
}
diff --git a/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt b/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt
index c14b727e930f4114765bfbe15b742ddcdeaa392f..1198908fcc66eeeea5e537085d7632a0d4b04545 100644
--- a/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt
+++ b/android/src/main/java/com/reactnativecompressor/Utils/Utils.kt
@@ -7,7 +7,6 @@ import android.provider.OpenableColumns
import android.util.Log
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
-import com.reactnativecompressor.Audio.AudioCompressor
import com.reactnativecompressor.Video.VideoCompressor.CompressionListener
import com.reactnativecompressor.Video.VideoCompressor.VideoCompressorClass
import java.io.FileNotFoundException
@@ -152,10 +151,6 @@ object Utils {
}
}
- fun addLog(log: String) {
- Log.d(AudioCompressor.TAG, log)
- }
-
val exifAttributes = arrayOf(
"FNumber",
"ApertureValue",
@@ -1,5 +0,0 @@
# react-native-compressor
Patch file taken from https://github.com/numandev1/react-native-compressor/pull/355#issuecomment-3180870738
This patch removes the audio compression feature on Android from the library. This is because `libandroidlame.so`, the native dependency, does not support 16kb page sizes, and the Play Store has made this mandatory as of 1st Nov 2025.
-16
View File
@@ -225,7 +225,6 @@ patchedDependencies:
expo-modules-core@3.0.30: 952fb1c8cb6dc8a0d8ef4e1114942a341d89b025e011f1acfff788929997346e
expo-notifications@0.32.17: a45a8dcf3d8c4b5df4ee0e62bc79b755fcf9d28ca51a6f685b85830bf4afc2e5
expo-updates@29.0.17: 04f28cb005b770e9ae8f0065eab96e43cbb1e58107f5f6ad1bdd18f6deb66487
react-native-compressor@1.13.0: 58379dfaace6ced8590cb341c77f2ca8099dfa8f7df6297032ec51de767a9925
react-native-date-picker@5.0.13: f2a6697da7a7ca79b4f39efda142eee1b82db6de3aa5010ad4bd59df41fe2ce1
react-native-drawer-layout@4.2.3: 74f2c043cc22ab87054f219e7c7373a509b779b18bfa79d27e7d051d73355130
react-native-keyboard-controller@1.21.8: 2f6791837622fb391c3e41fc044b91ddedbe503da14da86165ee0a73924b05f1
@@ -613,9 +612,6 @@ importers:
react-native:
specifier: 0.81.5
version: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
react-native-compressor:
specifier: 1.13.0
version: 1.13.0(patch_hash=58379dfaace6ced8590cb341c77f2ca8099dfa8f7df6297032ec51de767a9925)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-date-picker:
specifier: ^5.0.13
version: 5.0.13(patch_hash=f2a6697da7a7ca79b4f39efda142eee1b82db6de3aa5010ad4bd59df41fe2ce1)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@@ -8000,13 +7996,6 @@ packages:
react: '>=18.0.0'
react-is: '>=18.0.0'
react-native-compressor@1.13.0:
resolution: {integrity: sha512-vJYbrcjz2S7kgE3Q7444F71CjEDq5Qg6sGn67N9fJ0I1K6EhB/ZxpULdDe3FxJ4h/ncJc0oxOzNWZhxtGTqcQw==}
engines: {node: '>= 16.0.0'}
peerDependencies:
react: '*'
react-native: '*'
react-native-date-picker@5.0.13:
resolution: {integrity: sha512-qCLUODZVsJetO5zuoXjw1D39K527XWqBG8sOfhWdHyPzf13h8RXR1/RSKd1N0fdRDi5GdyizYmB0lPAK12/hbw==}
peerDependencies:
@@ -17855,11 +17844,6 @@ snapshots:
react: 19.1.0
react-is: 19.2.6
react-native-compressor@1.13.0(patch_hash=58379dfaace6ced8590cb341c77f2ca8099dfa8f7df6297032ec51de767a9925)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-native: 0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)
react-native-date-picker@5.0.13(patch_hash=f2a6697da7a7ca79b4f39efda142eee1b82db6de3aa5010ad4bd59df41fe2ce1)(react-native@0.81.5(patch_hash=2656ac6deb71b92a4df4af4593d13ace6a5740936432e5e7e8ef32bc3cd05194)(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
-1
View File
@@ -29,7 +29,6 @@ patchedDependencies:
'expo-modules-core@3.0.30': patches/expo-modules-core@3.0.30.patch
'expo-notifications@0.32.17': patches/expo-notifications@0.32.17.patch
'expo-updates@29.0.17': patches/expo-updates@29.0.17.patch
'react-native-compressor@1.13.0': patches/react-native-compressor@1.13.0.patch
'react-native-date-picker@5.0.13': patches/react-native-date-picker@5.0.13.patch
'react-native-drawer-layout@4.2.3': patches/react-native-drawer-layout@4.2.3.patch
'react-native-keyboard-controller@1.21.8': patches/react-native-keyboard-controller@1.21.8.patch
-56
View File
@@ -1,56 +0,0 @@
import {getVideoMetaData, Video} from 'react-native-compressor'
import {type ImagePickerAsset} from 'expo-image-picker'
import {SUPPORTED_MIME_TYPES, type SupportedMimeTypes} from '#/lib/constants'
import {type CompressedVideo} from './types'
import {extToMime} from './util'
const MIN_SIZE_FOR_COMPRESSION = 25 // 25mb
export async function compressVideo(
file: ImagePickerAsset,
opts?: {
signal?: AbortSignal
onProgress?: (progress: number) => void
},
): Promise<CompressedVideo> {
const {onProgress, signal} = opts || {}
const isAcceptableFormat = SUPPORTED_MIME_TYPES.includes(
file.mimeType as SupportedMimeTypes,
)
if (file.mimeType === 'image/gif') {
// let's hope they're small enough that they don't need compression!
// this compression library doesn't support gifs
// worst case - server rejects them. I think that's fine -sfn
return {uri: file.uri, size: file.fileSize ?? -1, mimeType: 'image/gif'}
}
const minimumFileSizeForCompress = isAcceptableFormat
? MIN_SIZE_FOR_COMPRESSION
: 0
const compressed = await Video.compress(
file.uri,
{
compressionMethod: 'manual',
bitrate: 3_000_000, // 3mbps
maxSize: 1920,
// WARNING: this ONE SPECIFIC ARG is in MB -sfn
minimumFileSizeForCompress,
getCancellationId: id => {
if (signal) {
signal.addEventListener('abort', () => {
Video.cancelCompression(id)
})
}
},
},
onProgress,
)
const info = await getVideoMetaData(compressed)
return {uri: compressed, size: info.size, mimeType: extToMime(info.extension)}
}
-103
View File
@@ -1,103 +0,0 @@
import {type ImagePickerAsset} from 'expo-image-picker'
import {SUPPORTED_MIME_TYPES, type SupportedMimeTypes} from '#/lib/constants'
import {logger} from '#/logger'
import {compress, probe} from '../../../../modules/expo-bluesky-video-compress'
import {type CompressedVideo} from './types'
// Skip compression if bitrate is at/below this threshold (bps)
const PASSTHROUGH_BITRATE = 5_000_000
// Max dimension that doesn't need downscaling
const PASSTHROUGH_MAX_DIMENSION = 1920
// Max file size the server accepts (bytes)
const MAX_UPLOAD_SIZE = 100 * 1000 * 1000 // 100MB
export async function compressVideo(
file: ImagePickerAsset,
opts?: {
signal?: AbortSignal
onProgress?: (progress: number) => void
},
): Promise<CompressedVideo> {
if (file.mimeType === 'image/gif') {
return {uri: file.uri, size: file.fileSize ?? -1, mimeType: 'image/gif'}
}
const isAcceptableFormat = SUPPORTED_MIME_TYPES.includes(
file.mimeType as SupportedMimeTypes,
)
// Probe the video to make a smart compression decision
const metadata = await probe(file.uri)
const needsCompression = shouldCompress(metadata, isAcceptableFormat)
if (!needsCompression) {
return {
uri: file.uri,
size: metadata.fileSize,
mimeType: file.mimeType ?? 'video/mp4',
}
}
const result = await compress(
file.uri,
{
targetBitrate: 3_000_000,
maxSize: 1920,
},
{
onProgress: opts?.onProgress,
signal: opts?.signal,
},
)
return {
uri: result.uri,
size: result.size,
mimeType: result.mimeType,
}
}
function shouldCompress(
metadata: {bitrate: number; width: number; height: number; fileSize: number},
isAcceptableFormat: boolean,
): boolean {
const maxDimension = Math.max(metadata.width, metadata.height)
const bitrateKbps = Math.round(metadata.bitrate / 1000)
const sizeMB = (metadata.fileSize / 1_000_000).toFixed(1)
// Always compress unacceptable formats (e.g. MOV → MP4)
if (!isAcceptableFormat) {
logger.debug('shouldCompress: yes (unsupported format)')
return true
}
// Must compress if over upload limit
if (metadata.fileSize > MAX_UPLOAD_SIZE) {
logger.debug(`shouldCompress: yes (file too large: ${sizeMB}MB)`)
return true
}
// Skip if already low bitrate, small resolution, and under upload limit
if (
metadata.bitrate <= PASSTHROUGH_BITRATE &&
maxDimension <= PASSTHROUGH_MAX_DIMENSION
) {
logger.debug(
`shouldCompress: no (${bitrateKbps}kbps, ${maxDimension}px, ${sizeMB}MB)`,
)
return false
}
if (metadata.bitrate > PASSTHROUGH_BITRATE) {
logger.debug(
`shouldCompress: yes (bitrate ${bitrateKbps}kbps > ${PASSTHROUGH_BITRATE / 1000}kbps)`,
)
} else {
logger.debug(
`shouldCompress: yes (dimension ${maxDimension}px > ${PASSTHROUGH_MAX_DIMENSION}px)`,
)
}
return true
}
+86 -48
View File
@@ -1,14 +1,16 @@
import {type ImagePickerAsset} from 'expo-image-picker'
import * as Toast from '#/components/Toast'
import {IS_DEV} from '#/env'
import {SUPPORTED_MIME_TYPES, type SupportedMimeTypes} from '#/lib/constants'
import {logger} from '#/logger'
import {compress, probe} from '../../../../modules/expo-bluesky-video-compress'
import {type CompressedVideo} from './types'
// Toggle for A/B comparison. Set to true to use the new Expo module.
const USE_NEW_COMPRESSOR = false
// In dev, run both compressors and show a comparison toast.
const DEV_COMPARE_BOTH = true
// Skip compression if bitrate is at/below this threshold (bps)
const PASSTHROUGH_BITRATE = 5_000_000
// Max dimension that doesn't need downscaling
const PASSTHROUGH_MAX_DIMENSION = 1920
// Max file size the server accepts (bytes)
const MAX_UPLOAD_SIZE = 100 * 1000 * 1000 // 100MB
export async function compressVideo(
file: ImagePickerAsset,
@@ -17,49 +19,85 @@ export async function compressVideo(
onProgress?: (progress: number) => void
},
): Promise<CompressedVideo> {
if (IS_DEV && DEV_COMPARE_BOTH) {
return compressAndCompare(file, opts)
if (file.mimeType === 'image/gif') {
return {uri: file.uri, size: file.fileSize ?? -1, mimeType: 'image/gif'}
}
if (USE_NEW_COMPRESSOR) {
const {compressVideo: compressVideoNew} = await import('./compress.new')
return compressVideoNew(file, opts)
} else {
const {compressVideo: compressVideoLegacy} = await import(
'./compress.legacy'
)
return compressVideoLegacy(file, opts)
}
}
async function compressAndCompare(
file: ImagePickerAsset,
opts?: {
signal?: AbortSignal
onProgress?: (progress: number) => void
},
): Promise<CompressedVideo> {
const {compressVideo: compressVideoLegacy} = await import('./compress.legacy')
const {compressVideo: compressVideoNew} = await import('./compress.new')
// Run legacy first (progress goes to UI)
const legacyStart = performance.now()
const legacyResult = await compressVideoLegacy(file, opts)
const legacyMs = performance.now() - legacyStart
// Run new second
const newStart = performance.now()
const newResult = await compressVideoNew(file, opts)
const newMs = performance.now() - newStart
const fmt = (ms: number, size: number) =>
`${(ms / 1000).toFixed(1)}s → ${(size / 1_000_000).toFixed(1)}MB`
Toast.show(
`legacy: ${fmt(legacyMs, legacyResult.size)}\nnew: ${fmt(newMs, newResult.size)}`,
{duration: 8000},
const isAcceptableFormat = SUPPORTED_MIME_TYPES.includes(
file.mimeType as SupportedMimeTypes,
)
// Return whichever is selected
return USE_NEW_COMPRESSOR ? newResult : legacyResult
// Probe the video to make a smart compression decision
const metadata = await probe(file.uri)
const needsCompression = shouldCompress(metadata, isAcceptableFormat)
if (!needsCompression) {
return {
uri: file.uri,
size: metadata.fileSize,
mimeType: file.mimeType ?? 'video/mp4',
}
}
const result = await compress(
file.uri,
{
targetBitrate: 3_000_000,
maxSize: 1920,
},
{
onProgress: opts?.onProgress,
signal: opts?.signal,
},
)
return {
uri: result.uri,
size: result.size,
mimeType: result.mimeType,
}
}
function shouldCompress(
metadata: {bitrate: number; width: number; height: number; fileSize: number},
isAcceptableFormat: boolean,
): boolean {
const maxDimension = Math.max(metadata.width, metadata.height)
const bitrateKbps = Math.round(metadata.bitrate / 1000)
const sizeMB = (metadata.fileSize / 1_000_000).toFixed(1)
// Always compress unacceptable formats (e.g. MOV → MP4)
if (!isAcceptableFormat) {
logger.debug('shouldCompress: yes (unsupported format)')
return true
}
// Must compress if over upload limit
if (metadata.fileSize > MAX_UPLOAD_SIZE) {
logger.debug(`shouldCompress: yes (file too large: ${sizeMB}MB)`)
return true
}
// Skip if already low bitrate, small resolution, and under upload limit
if (
metadata.bitrate <= PASSTHROUGH_BITRATE &&
maxDimension <= PASSTHROUGH_MAX_DIMENSION
) {
logger.debug(
`shouldCompress: no (${bitrateKbps}kbps, ${maxDimension}px, ${sizeMB}MB)`,
)
return false
}
if (metadata.bitrate > PASSTHROUGH_BITRATE) {
logger.debug(
`shouldCompress: yes (bitrate ${bitrateKbps}kbps > ${PASSTHROUGH_BITRATE / 1000}kbps)`,
)
} else {
logger.debug(
`shouldCompress: yes (dimension ${maxDimension}px > ${PASSTHROUGH_MAX_DIMENSION}px)`,
)
}
return true
}
+2 -3
View File
@@ -180,7 +180,7 @@ import {
type VideoState,
} from './state/video'
import {type TextInputRef} from './text-input/TextInput.types'
import {getVideoMetadata} from './videos/pickVideo'
import {getVideoMetadata} from './videos/videoMetadata'
import {clearThumbnailCache} from './videos/VideoTranscodeBackdrop'
type CancelRef = {
@@ -476,8 +476,7 @@ export const ComposePost = ({
let uri = videoInfo.uri
if (IS_ANDROID) {
// Android: expo-file-system double-encodes filenames with special chars.
// The file exists, but react-native-compressor's MediaMetadataRetriever
// can't handle the double-encoded URI. Copy to a temp file with a simple name.
// Copy to a temp file with a simple name to avoid URI parsing issues.
const sourceFile = new FileSystem.File(videoInfo.uri)
const tempFileName = `draft-video-${Date.now()}.${mimeToExt(videoInfo.mimeType)}`
const tempFile = new FileSystem.File(
@@ -1,6 +1,6 @@
import {clearCache, createVideoThumbnail} from 'react-native-compressor'
import Animated, {FadeIn} from 'react-native-reanimated'
import {Image} from 'expo-image'
import * as VideoThumbnails from 'expo-video-thumbnails'
import {type QueryClient, useQuery} from '@tanstack/react-query'
import {atoms as a} from '#/alf'
@@ -8,7 +8,6 @@ import {atoms as a} from '#/alf'
export const RQKEY = 'video-thumbnail'
export function clearThumbnailCache(queryClient: QueryClient) {
clearCache().catch(() => {})
void queryClient.resetQueries({queryKey: [RQKEY]})
}
@@ -16,7 +15,8 @@ export function VideoTranscodeBackdrop({uri}: {uri: string}) {
const {data: thumbnail} = useQuery({
queryKey: [RQKEY, uri],
queryFn: async () => {
return await createVideoThumbnail(uri)
const result = await VideoThumbnails.getThumbnailAsync(uri)
return result.uri
},
})
@@ -25,7 +25,7 @@ export function VideoTranscodeBackdrop({uri}: {uri: string}) {
<Animated.View style={a.flex_1} entering={FadeIn}>
<Image
style={a.flex_1}
source={thumbnail.path}
source={thumbnail}
cachePolicy="none"
accessibilityIgnoresInvertColors
blurRadius={15}
-43
View File
@@ -1,43 +0,0 @@
import {getVideoMetaData} from 'react-native-compressor'
import {
type ImagePickerAsset,
launchImageLibraryAsync,
UIImagePickerPreferredAssetRepresentationMode,
} from 'expo-image-picker'
import {VIDEO_MAX_DURATION_MS} from '#/lib/constants'
import {extToMime} from '#/lib/media/video/util'
export async function pickVideo() {
return await launchImageLibraryAsync({
exif: false,
mediaTypes: ['videos'],
quality: 1,
legacy: true,
preferredAssetRepresentationMode:
UIImagePickerPreferredAssetRepresentationMode.Current,
videoMaxDuration: VIDEO_MAX_DURATION_MS / 1000,
})
}
/**
* Gets video metadata from a file or uri, depending on the platform
*
* @param file File on web, uri on native
*/
export async function getVideoMetadata(
file: File | string,
): Promise<ImagePickerAsset> {
if (typeof file !== 'string')
throw new Error(
'getVideoMetadata was passed a File, when on native it should be a uri',
)
const metadata = await getVideoMetaData(file)
return {
uri: file,
mimeType: extToMime(metadata.extension),
width: metadata.width,
height: metadata.height,
duration: metadata.duration,
}
}
@@ -0,0 +1,23 @@
import {type ImagePickerAsset} from 'expo-image-picker'
import {probe} from '../../../../../modules/expo-bluesky-video-compress'
/**
* Gets video metadata from a uri on native.
*/
export async function getVideoMetadata(
file: File | string,
): Promise<ImagePickerAsset> {
if (typeof file !== 'string')
throw new Error(
'getVideoMetadata was passed a File, when on native it should be a uri',
)
const metadata = await probe(file)
return {
uri: file,
mimeType: metadata.mimeType,
width: metadata.width,
height: metadata.height,
duration: metadata.duration * 1000, // seconds -> ms
}
}
@@ -1,38 +1,4 @@
import {type ImagePickerAsset, type ImagePickerResult} from 'expo-image-picker'
import {SUPPORTED_MIME_TYPES} from '#/lib/constants'
// mostly copied from expo-image-picker and adapted to support gifs
// also adds support for reading video metadata
export async function pickVideo(): Promise<ImagePickerResult> {
const input = document.createElement('input')
input.style.display = 'none'
input.setAttribute('type', 'file')
// TODO: do we need video/* here? -sfn
input.setAttribute('accept', SUPPORTED_MIME_TYPES.join(','))
input.setAttribute('id', String(Math.random()))
document.body.appendChild(input)
return new Promise(resolve => {
input.addEventListener('change', async () => {
if (input.files) {
const file = input.files[0]
resolve({
canceled: false,
assets: [await getVideoMetadata(file)],
})
} else {
resolve({canceled: true, assets: null})
}
document.body.removeChild(input)
})
const event = new MouseEvent('click')
input.dispatchEvent(event)
})
}
import {type ImagePickerAsset} from 'expo-image-picker'
// TODO: we're converting to a dataUrl here, and then converting back to an
// ArrayBuffer in the compressVideo function. This is a bit wasteful, but it