delete comparison code, react-native-compressor
This commit is contained in:
@@ -287,7 +287,6 @@ module.exports = function (_config) {
|
||||
sounds: PLATFORM === 'ios' ? ['assets/dm.aiff'] : ['assets/dm.mp3'],
|
||||
},
|
||||
],
|
||||
'react-native-compressor',
|
||||
[
|
||||
'@bitdrift/react-native',
|
||||
{
|
||||
|
||||
+3
@@ -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
|
||||
|
||||
@@ -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.
|
||||
Generated
-16
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
-35
@@ -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
|
||||
Reference in New Issue
Block a user