fix dataurl roundtripping
This commit is contained in:
@@ -4,7 +4,7 @@ import {VIDEO_MAX_SIZE} from '#/lib/constants'
|
||||
import {VideoTooLargeError} from '#/lib/media/video/errors'
|
||||
import {type CompressedVideo} from './types'
|
||||
|
||||
// doesn't actually compress, converts to ArrayBuffer
|
||||
// doesn't actually compress, just reads the File bytes
|
||||
export async function compressVideo(
|
||||
asset: ImagePickerAsset,
|
||||
_opts?: {
|
||||
@@ -12,45 +12,24 @@ export async function compressVideo(
|
||||
onProgress?: (progress: number) => void
|
||||
},
|
||||
): Promise<CompressedVideo> {
|
||||
const {mimeType, base64} = parseDataUrl(asset.uri)
|
||||
const blob = base64ToBlob(base64, mimeType)
|
||||
const uri = URL.createObjectURL(blob)
|
||||
let bytes: ArrayBuffer
|
||||
if (asset.file) {
|
||||
bytes = await asset.file.arrayBuffer()
|
||||
} else {
|
||||
// fallback: fetch from blob/object URL
|
||||
bytes = await fetch(asset.uri).then(res => res.arrayBuffer())
|
||||
}
|
||||
|
||||
if (blob.size > VIDEO_MAX_SIZE) {
|
||||
if (bytes.byteLength > VIDEO_MAX_SIZE) {
|
||||
throw new VideoTooLargeError()
|
||||
}
|
||||
|
||||
const mimeType = asset.mimeType ?? asset.file?.type ?? 'video/mp4'
|
||||
|
||||
return {
|
||||
size: blob.size,
|
||||
uri,
|
||||
bytes: await blob.arrayBuffer(),
|
||||
size: bytes.byteLength,
|
||||
uri: asset.uri,
|
||||
bytes,
|
||||
mimeType,
|
||||
}
|
||||
}
|
||||
|
||||
function parseDataUrl(dataUrl: string) {
|
||||
const [mimeType, base64] = dataUrl.slice('data:'.length).split(';base64,')
|
||||
if (!mimeType || !base64) {
|
||||
throw new Error('Invalid data URL')
|
||||
}
|
||||
return {mimeType, base64}
|
||||
}
|
||||
|
||||
function base64ToBlob(base64: string, mimeType: string) {
|
||||
const byteCharacters = atob(base64)
|
||||
const byteArrays = []
|
||||
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
|
||||
const slice = byteCharacters.slice(offset, offset + 512)
|
||||
const byteNumbers = new Array(slice.length)
|
||||
|
||||
for (let i = 0; i < slice.length; i++) {
|
||||
byteNumbers[i] = slice.charCodeAt(i)
|
||||
}
|
||||
|
||||
const byteArray = new Uint8Array(byteNumbers)
|
||||
byteArrays.push(byteArray)
|
||||
}
|
||||
|
||||
return new Blob(byteArrays, {type: mimeType})
|
||||
}
|
||||
|
||||
@@ -465,7 +465,7 @@ export const ComposePost = ({
|
||||
let asset: ImagePickerAsset
|
||||
|
||||
if (IS_WEB) {
|
||||
// Web: Convert blob URL to a File, then get video metadata (returns data URL)
|
||||
// Web: Convert blob URL to a File, then get video metadata
|
||||
const response = await fetch(videoInfo.uri)
|
||||
const blob = await response.blob()
|
||||
const file = new File([blob], 'restored-video', {
|
||||
@@ -1545,26 +1545,18 @@ let ComposerPost = memo(function ComposerPost({
|
||||
)
|
||||
|
||||
const onPhotoPasted = useCallback(
|
||||
async (uri: string) => {
|
||||
if (
|
||||
uri.startsWith('data:video/') ||
|
||||
(IS_WEB && uri.startsWith('data:image/gif'))
|
||||
) {
|
||||
if (IS_NATIVE) return // web only
|
||||
const [mimeType] = uri.slice('data:'.length).split(';')
|
||||
async (uriOrFile: string | File) => {
|
||||
if (uriOrFile instanceof File) {
|
||||
const mimeType = uriOrFile.type
|
||||
if (!SUPPORTED_MIME_TYPES.includes(mimeType as SupportedMimeTypes)) {
|
||||
Toast.show(l`Unsupported video type: ${mimeType}`, {
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
const name = `pasted.${mimeToExt(mimeType)}`
|
||||
const file = await fetch(uri)
|
||||
.then(res => res.blob())
|
||||
.then(blob => new File([blob], name, {type: mimeType}))
|
||||
onSelectVideo(post.id, await getVideoMetadata(file))
|
||||
onSelectVideo(post.id, await getVideoMetadata(uriOrFile))
|
||||
} else {
|
||||
const res = await pasteImage(uri)
|
||||
const res = await pasteImage(uriOrFile)
|
||||
onImageAdd([res])
|
||||
}
|
||||
},
|
||||
|
||||
@@ -113,8 +113,8 @@ export function TextInput({
|
||||
if (transfer) {
|
||||
const items = transfer.items
|
||||
|
||||
getImageOrVideoFromUri(items, (uri: string) => {
|
||||
textInputWebEmitter.emit('media-pasted', uri)
|
||||
getImageOrVideoFromUri(items, (uriOrFile: string | File) => {
|
||||
textInputWebEmitter.emit('media-pasted', uriOrFile)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -190,9 +190,12 @@ export function TextInput({
|
||||
view.pasteText(text)
|
||||
preventDefault = true
|
||||
}
|
||||
getImageOrVideoFromUri(clipboardData.items, (uri: string) => {
|
||||
textInputWebEmitter.emit('media-pasted', uri)
|
||||
})
|
||||
getImageOrVideoFromUri(
|
||||
clipboardData.items,
|
||||
(uriOrFile: string | File) => {
|
||||
textInputWebEmitter.emit('media-pasted', uriOrFile)
|
||||
},
|
||||
)
|
||||
if (preventDefault) {
|
||||
// Return `true` to prevent ProseMirror's default paste behavior.
|
||||
return true
|
||||
@@ -478,7 +481,7 @@ const styles = StyleSheet.create({
|
||||
|
||||
function getImageOrVideoFromUri(
|
||||
items: DataTransferItemList,
|
||||
callback: (uri: string) => void,
|
||||
callback: (uri: string | File) => void,
|
||||
) {
|
||||
for (let index = 0; index < items.length; index++) {
|
||||
const item = items[index]
|
||||
@@ -495,21 +498,21 @@ function getImageOrVideoFromUri(
|
||||
}
|
||||
|
||||
if (blob.type.startsWith('video/')) {
|
||||
blobToDataUri(blob).then(callback, err => console.error(err))
|
||||
callback(new File([blob], 'dropped', {type: blob.type}))
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (type.startsWith('image/')) {
|
||||
} else if (type.startsWith('image/') && !type.startsWith('image/gif')) {
|
||||
const file = item.getAsFile()
|
||||
|
||||
if (file) {
|
||||
blobToDataUri(file).then(callback, err => console.error(err))
|
||||
}
|
||||
} else if (type.startsWith('video/')) {
|
||||
} else if (type.startsWith('video/') || type.startsWith('image/gif')) {
|
||||
const file = item.getAsFile()
|
||||
|
||||
if (file) {
|
||||
blobToDataUri(file).then(callback, err => console.error(err))
|
||||
callback(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
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
|
||||
// lets us use the ImagePickerAsset type, which the rest of the code expects.
|
||||
// We should unwind this and just pass the ArrayBuffer/objectUrl through the system
|
||||
// instead of a string -sfn
|
||||
export function getVideoMetadata(
|
||||
file: File | string,
|
||||
): Promise<ImagePickerAsset> {
|
||||
@@ -12,55 +7,53 @@ export function getVideoMetadata(
|
||||
throw new Error(
|
||||
'getVideoMetadata was passed a uri, when on web it should be a File',
|
||||
)
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const uri = reader.result as string
|
||||
const blobUrl = URL.createObjectURL(file)
|
||||
|
||||
if (file.type === 'image/gif') {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
resolve({
|
||||
uri,
|
||||
mimeType: 'image/gif',
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
// todo: calculate gif duration. seems possible if you read the bytes
|
||||
// https://codepen.io/Ryman/pen/nZpYwY
|
||||
// for now let's just let the server reject it, since that seems uncommon -sfn
|
||||
duration: null,
|
||||
})
|
||||
}
|
||||
img.onerror = (_ev, _source, _lineno, _colno, error) => {
|
||||
console.log('Failed to grab GIF metadata', error)
|
||||
reject(new Error('Failed to grab GIF metadata'))
|
||||
}
|
||||
img.src = uri
|
||||
} else {
|
||||
const video = document.createElement('video')
|
||||
const blobUrl = URL.createObjectURL(file)
|
||||
|
||||
video.preload = 'metadata'
|
||||
video.src = blobUrl
|
||||
|
||||
video.onloadedmetadata = () => {
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
resolve({
|
||||
uri,
|
||||
mimeType: file.type,
|
||||
width: video.videoWidth,
|
||||
height: video.videoHeight,
|
||||
// convert seconds to ms
|
||||
duration: video.duration * 1000,
|
||||
})
|
||||
}
|
||||
video.onerror = (_ev, _source, _lineno, _colno, error) => {
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
console.log('Failed to grab video metadata', error)
|
||||
reject(new Error('Failed to grab video metadata'))
|
||||
}
|
||||
if (file.type === 'image/gif') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
resolve({
|
||||
uri: blobUrl,
|
||||
file,
|
||||
mimeType: 'image/gif',
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
// todo: calculate gif duration. seems possible if you read the bytes
|
||||
// https://codepen.io/Ryman/pen/nZpYwY
|
||||
// for now let's just let the server reject it, since that seems uncommon -sfn
|
||||
duration: null,
|
||||
})
|
||||
}
|
||||
img.onerror = (_ev, _source, _lineno, _colno, error) => {
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
console.log('Failed to grab GIF metadata', error)
|
||||
reject(new Error('Failed to grab GIF metadata'))
|
||||
}
|
||||
img.src = blobUrl
|
||||
})
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video')
|
||||
video.preload = 'metadata'
|
||||
video.src = blobUrl
|
||||
|
||||
video.onloadedmetadata = () => {
|
||||
resolve({
|
||||
uri: blobUrl,
|
||||
file,
|
||||
mimeType: file.type,
|
||||
width: video.videoWidth,
|
||||
height: video.videoHeight,
|
||||
// convert seconds to ms
|
||||
duration: video.duration * 1000,
|
||||
})
|
||||
}
|
||||
video.onerror = (_ev, _source, _lineno, _colno, error) => {
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
console.log('Failed to grab video metadata', error)
|
||||
reject(new Error('Failed to grab video metadata'))
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user