上传音频文件
思路
1、自定义Upload
重点:<input ref={inputRef} type="file" accept={accept} onClick={e => e.stopPropagation()} onChange={uploadFile} multiple={multiple}/>
使用input标签设置type是file,将input元素通过forwardRef暴露给父组件,使父组件可以通过useImperativeHandle透传的resetValue方法在外部控制input的value值,
2、自定义AudioUpload
通过区分iOS和Android设置不同的accept来解决格式的兼容性问题
上传流程:文件格式校验——音频时长校验——获取upload token——上传文件——获取到ossUrl
3、使用 AudioUpload组件
.mp3, .wav, .m4a 和 audio/*
.mp3, .wav, 和 .m4a 是具体的音频文件格式
audio/* 是一个 MIME 类型,它表示所有音频文件类型
- MP3 比较流行,有损压缩的音频格式
- wav 无损未压缩的,文件较大
- m4a 通常用于Apple设备
- audio/*支持大多数音频
总结:需要处理特定格式的音频文件用前者;希望支持多种音频格式用后者;
1、自定义Upload
Upload/index.module.css
.tongyi-upload {outline: 0;
}
Upload/index.tsx
import React, { ReactNode, forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import classnames from 'classnames';
import styles from './index.module.css';interface UploadChangeParam {file: File;fileList: File[];event?: { percent: number };
}interface UploadProps {className?: string;style?: React.CSSProperties;accept?: string;multiple?: boolean;withCredentials?: boolean;children: ReactNode;// 限制大小maxSize?: number;// onChange?: (info: UploadChangeParam) => void;customRequest: (info: { file: File }) => Promise<void>;onError?: (e: any) => void;
}export default forwardRef((props: UploadProps, ref: any) => {const { className, accept, maxSize, multiple = false, customRequest, onError, children } = props;const inputRef = useRef<HTMLInputElement>(null);const uploadFile = (e: React.ChangeEvent<HTMLInputElement>) => {const { files } = e.target;if (!files) {return;}const originFiles = [...files] as File[];originFiles.forEach((file: File) => {if (maxSize && file.size > maxSize) {onError && onError({code: 'FILE_EXCEEDS_SIZE'});return;}return customRequest && customRequest({file});});};useImperativeHandle(ref, () => {return {resetValue: () => {if (inputRef.current) {inputRef.current.value = '';}}}});return (<divclassName={classnames(styles['tongyi-upload'], className)}onClick={() => inputRef.current?.click()}style={props.style}><inputtype="file"ref={inputRef}style={{ display: 'none' }}accept={accept}onClick={e => e.stopPropagation()}onChange={uploadFile}multiple={multiple}/>{children}</div>)
})
2、自定义AudioUpload
AudioUpload.tsx
import { getUploadToken, uploadFile } from '@/services/file';
import Upload from './Upload';
import { getDeviceType } from '@/utils';
import { getAudioDuration } from '@/utils/audioFile';
import React, {forwardRef,ReactNode,useImperativeHandle,useRef,
} from 'react';interface UploadTokenData {accessId: string;policy: string;signature: string;dir: string;host: string;expire: number;bucketName: string;key: string;
}interface UploadProps {className?: string;accept?: string;multiple?: boolean;withCredentials?: boolean;style?: React.CSSProperties;children: ReactNode;beforeUpload?: () => void;onChange?: (ossUrl: string) => void;onError?: (err: any) => void;
}// 文件大小限制
const maxSize = 10 * 1024 * 1024;
const audioFileType = ['mp3', 'aac', 'wav', 'flac', 'ogg', 'm4a'];export default forwardRef((props: UploadProps, ref: any) => {const uploadStatusRef = useRef<any>({});const uploadRef = useRef<any>({});const proxyFn = (fn: () => Promise<any>) => {if (uploadStatusRef.current.status !== 'cancel') {return fn();}return Promise.reject('cancel');};const customRequest = async (info: { file: File }) => {uploadStatusRef.current.status = 'ready';if (!info.file?.type.startsWith('audio/')) {props.onError &&props.onError({code: 'FILE_TYPE_ERROR',message: '',});return Promise.reject('FILE_TYPE_ERROR');}const duration: number = await getAudioDuration(info.file);console.log('获取到的时长', duration);if (duration < 10 || duration > 30) {props.onError &&props.onError({code: 'FILE_DURATION_ERROR',message: '',});return Promise.reject('FILE_DURATION_ERROR');}props.beforeUpload && props.beforeUpload();console.log('文件信息=========', info.file);return (// proxyFn(() => getAudioUploadToken(info.file.name))proxyFn(() => getUploadToken()).then((data: UploadTokenData) => {console.log('getAudioUploadToken的结果', data);return proxyFn(() => uploadFile(data, info.file)).then((uploadRes) => {console.log('uploadFile成功了', uploadRes);props.onChange && props.onChange(uploadRes?.ossUrl);}).catch((e) => {console.log('uploadFile报错了===========', e);props.onError &&props.onError({code: 'UNKNOW',message: e.errorMsg,});});}).catch((e) => {console.log('e=========', e);const { errorMsg } = e;if (e !== 'cancel') {props.onError &&props.onError({code: 'UPLOAD_ERROR',});}}));};useImperativeHandle(ref,() => ({cancel: () => {uploadStatusRef.current.status = 'cancel';uploadRef.current.resetValue();clearTimeout(uploadStatusRef.current.clock);},}),[],);const accept = getDeviceType() ? '.mp3, .wav, .m4a' : 'audio/*';return (<UploadmaxSize={maxSize}accept={accept}{...props}ref={uploadRef}customRequest={customRequest}>{props.children}</Upload>);
});
3、使用 AudioUpload组件
// 上传组件
const uploadRef = useRef<any>();
// 上传状态
const[uploadStatus, setUploadStatus] = useState<string>('default');
// 上传定时器
const uploadTimer = useRef<any>();
// 合成进度
const [percent, setPercent] = useState<number>(0);/*** 开始上传*/const startUpload = () => {console.log('上传中');setUploadStatus('processing');setPercent(0);const fn = () => {const i = Math.ceil(Math.random() * 3);uploadTimer.current = setTimeout(() => {fn();}, 1 * 1000);setPercent((pre) => {let current = pre + i;if (current >= 100) {current = 100;clearTimeout(uploadTimer.current);}return current;});};clearTimeout(uploadTimer.current);uploadTimer.current = setTimeout(() => {fn();}, 1 * 1000);};
/*** 上传失败* @param e*/const onError = (e: any) => {console.log('上传失败了');onChange('');setUploadStatus('error');clearTimeout(uploadTimer.current);uploadRef.current.cancel();setPercent(0);let msg = '';switch (e.code) {case 'FILE_DURATION_ERROR':msg = '请上传10-30秒音频文件';break;case 'FILE_EXCEEDS_SIZE':msg = '请上传10M以下的文件';break;case 'FILE_TYPE_ERROR':msg = '抱歉,请上传音频类型的文件';break;case 'SEC_RESULT':msg = e.message || `${e.code}抱歉,出错了,请换一个文件试试!`;break;case 'UPLOAD_ERROR':msg = '抱歉,上传失败,请重新上传';break;default:msg = `${e.code}抱歉,出错了,请换一个文件试试!`;break;}Toast.show({type: 'error',content: msg,});};/*** 上传成功*/const onUploaded = (ossUrl: string) => {console.log('上传成功获取到ossUrl', ossUrl);clearTimeout(uploadTimer.current);setUploadStatus('success');// 上传成功之后调用接口合成数字声音。。。onMergeSound(ossUrl);};
return (<AudioUploadonError={onError}onChange={onUploaded}beforeUpload={startUpload}ref={uploadRef}><div>上传</div></AudioUpload>
)
4、音频时长校验
/*** 异步获取音频文件的时长* @param file 音频文件* @returns 返回音频的时长(秒)*/
export const getAudioDuration = async (file) => {try {const audio = new Audio(URL.createObjectURL(file));await new Promise((resolve) => (audio.onloadedmetadata = resolve));const { duration } = audio;return duration;} catch (error) {console.error('获取音频时长时发生错误:', error);return 0;}
};
5、上传文件
export const uploadFile = (data: UploadTokenData, file: File) => {console.log('uploadFile开始了', data, '====', file);const bodyFormData = new FormData();const url = `${data.host}/${data.dir}${file.name}`;bodyFormData.append('OSSAccessKeyId', data.accessId);bodyFormData.append('policy', data.policy);bodyFormData.append('signature', data.signature);bodyFormData.append('key', `${data.dir}${file.name}`);bodyFormData.append('dir', data.dir);bodyFormData.append('success_action_status', '200');bodyFormData.append('file', file);console.log('uploadFile上传的url: ', url);return new Promise((resolve, reject) => {const xhr = new XMLHttpRequest();xhr.onerror = function error(e) {console.log('upload error', e);reject(e);};xhr.onload = async () => {// allow success when 2xx status see https://github.com/react-component/upload/issues/34if (xhr.status < 200 || xhr.status >= 300) {reject('上传异常');}console.log('upload success');resolve({...data,ossUrl: url,});};xhr.open('post', data.host, true);xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');xhr.send(bodyFormData);});
};