2025-07-17 15:44:34 +08:00

261 lines
9.6 KiB
TypeScript

import pLimit from 'p-limit';
import { Alert } from 'react-native';
import { transformData } from '@/components/utils/objectFlat';
import { ExtendedAsset } from './types';
import { getMediaByDateRange } from './media';
import { checkMediaLibraryPermission, getFileExtension, getMimeType } from './utils';
import { convertHeicToJpeg, uploadVideoThumbnail } from './fileProcessor';
import { compressImage } from '../image-process/imageCompress';
import { getUploadUrl, confirmUpload, addMaterial } from './api';
import { uploadFile } from './uploader';
import * as MediaLibrary from 'expo-media-library';
// 设置最大并发数
const CONCURRENCY_LIMIT = 10; // 同时最多上传10个文件
const limit = pLimit(CONCURRENCY_LIMIT);
// 处理单个媒体文件上传
export const processMediaUpload = async (asset: ExtendedAsset) => {
try {
// 检查权限
const { hasPermission } = await checkMediaLibraryPermission();
if (!hasPermission) {
throw new Error('No media library permission');
}
const isVideo = asset.mediaType === 'video';
// 上传原始文件
const uploadOriginalFile = async () => {
try {
let fileToUpload: File;
const isVideo = asset.mediaType === 'video';
const mimeType = getMimeType(asset.filename || '', isVideo);
// 生成文件名,保留原始扩展名
let filename = asset.filename ||
`${isVideo ? 'video' : 'image'}_${Date.now()}_original.${isVideo ? (getFileExtension(asset.filename || 'mp4') || 'mp4') : 'jpg'}`;
// 处理 HEIC 格式
if (filename.toLowerCase().endsWith('.heic') || filename.toLowerCase().endsWith('.heif')) {
fileToUpload = await convertHeicToJpeg(asset.uri);
filename = filename.replace(/\.(heic|heif)$/i, '.jpg');
} else {
// 获取资源信息
const assetInfo = await MediaLibrary.getAssetInfoAsync(asset.id, {
shouldDownloadFromNetwork: true
});
if (!assetInfo.localUri) {
throw new Error('无法获取资源的本地路径');
}
// 获取文件扩展名
const fileExtension = getFileExtension(assetInfo.filename || '') ||
(isVideo ? 'mp4' : 'jpg');
// 确保文件名有正确的扩展名
if (!filename.toLowerCase().endsWith(`.${fileExtension}`)) {
const baseName = filename.split('.')[0];
filename = `${baseName}.${fileExtension}`;
}
// 获取文件内容
const response = await fetch(assetInfo.localUri);
const blob = await response.blob();
// 创建文件对象
fileToUpload = new File([blob], filename, { type: mimeType });
console.log('文件准备上传:', {
name: fileToUpload.name,
type: fileToUpload.type,
size: fileToUpload.size
});
}
// 准备元数据
let exifData = {};
if (asset.exif) {
try {
exifData = transformData({
...asset,
exif: {
...asset.exif,
'{MakerApple}': undefined
}
});
} catch (exifError) {
console.warn('处理 EXIF 数据时出错:', exifError);
}
}
// 获取上传 URL
const { upload_url, file_id } = await getUploadUrl(fileToUpload, {
originalUri: asset.uri,
creationTime: asset.creationTime,
mediaType: isVideo ? 'video' : 'image',
isCompressed: false,
...exifData,
GPSVersionID: undefined
});
// 上传文件
await uploadFile(fileToUpload, upload_url);
await confirmUpload(file_id);
console.log('文件上传成功:', {
fileId: file_id,
filename: fileToUpload.name,
type: fileToUpload.type
});
return {
success: true,
file_id,
filename: fileToUpload.name
};
} catch (error: any) {
const errorMessage = error instanceof Error ? error.message : '未知错误';
console.error('上传原始文件时出错:', {
error: errorMessage,
assetId: asset.id,
filename: asset.filename,
uri: asset.uri
});
throw new Error(`上传失败: ${errorMessage}`);
}
};
// 上传压缩文件(仅图片)
const uploadCompressedFile = async () => {
if (isVideo) return { success: true, file_id: null }; // 视频不压缩
try {
const manipResult = await compressImage(asset.uri);
const response = await fetch(manipResult.uri);
const blob = await response.blob();
const filename = asset.filename ?
`compressed_${asset.filename}` :
`image_${Date.now()}_compressed.jpg`;
const compressedFile = new File([blob], filename, { type: 'image/jpeg' });
const { upload_url, file_id } = await getUploadUrl(compressedFile, {
originalUri: asset.uri,
creationTime: asset.creationTime,
mediaType: 'image',
isCompressed: true
});
await uploadFile(compressedFile, upload_url);
await confirmUpload(file_id);
return { success: true, file_id };
} catch (error) {
return { success: false, error };
}
};
// 先上传原始文件
const originalResult = await uploadOriginalFile();
// 如果是图片,再上传压缩文件
let compressedResult = { success: true, file_id: null };
if (!isVideo) {
compressedResult = await uploadCompressedFile();
// 添加素材
addMaterial(originalResult.file_id, compressedResult?.file_id || '');
} else {
// 上传压缩首帧
const thumbnailResult = await uploadVideoThumbnail(asset);
if (thumbnailResult.success) {
addMaterial(originalResult.file_id, thumbnailResult.file_id || '');
}
}
return {
originalSuccess: originalResult.success,
compressedSuccess: compressedResult.success,
fileIds: {
original: originalResult.file_id,
compressed: compressedResult.file_id
}
};
} catch (error: any) {
if (error.message === 'No media library permission') {
throw error;
}
return {
originalSuccess: false,
compressedSuccess: false,
error: error.message
};
}
};
// 手动触发上传
export const triggerManualUpload = async (startDate: Date, endDate: Date) => {
try {
const media = await getMediaByDateRange(startDate, endDate);
if (media.length === 0) {
Alert.alert('提示', '在指定时间范围内未找到媒体文件');
return [];
}
// 分离图片和视频
const photos = media.filter(item => item.mediaType === 'photo');
const videos = media.filter(item => item.mediaType === 'video');
console.log('videos11111111', videos);
const results: any[] = [];
// 处理所有图片(带并发控制)
const processPhoto = async (item: any) => {
try {
const result = await processMediaUpload(item);
results.push({
id: item.id,
...result
});
} catch (error: any) {
results.push({
id: item.id,
originalSuccess: false,
compressedSuccess: false,
error: error.message
});
}
};
// 处理所有视频(带并发控制)
const processVideo = async (item: any) => {
try {
const result = await processMediaUpload(item);
results.push({
id: item.id,
...result
});
} catch (error: any) {
results.push({
id: item.id,
originalSuccess: false,
compressedSuccess: false,
error: error.message
});
}
};
// 并发处理图片和视频
await Promise.all([
...photos.map(photo => limit(() => processPhoto(photo))),
...videos.map(video => limit(() => processVideo(video)))
]);
return results;
} catch (error) {
Alert.alert('错误', '上传过程中出现错误');
throw error;
}
};
export { registerBackgroundUploadTask } from './task';