上传音频文件

思路

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 (
    <div
      className={classnames(styles['tongyi-upload'], className)}
      onClick={() => inputRef.current?.click()}
      style={props.style}
    >
      <input
        type="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 (
    <Upload
      maxSize={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 (
	<AudioUpload
	  onError={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/34
      if (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);
  });
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值