录音功能

先上组件代码,仅供参考,自行根据需求修改

<template>
	<view class="b_f p_20 mb_20">
		<view class="title_3 fw_700">现场录音</view>
		<view class="f_aic_fdc">
			<image v-if="isRecording" id="blinking-image" src="/static/images/record.png"></image>
			<view v-if="isRecording" class="mb_5">正在录音中... {{ duration }}秒</view>
			<audio v-if="this.audioData.filePath && !isRecording" :src="this.audioData.filePath" controls class="mb_5"></audio>
		</view>
		<view class="f_aic_jcsb">
			<view class="btn mr_10" @click="chooseRecord">上传录音</view>
			<view class="btn" @click="recordImmediately">{{ isRecording ? '停止录音' : '立即录音'}}</view>
		</view>
	</view>
</template>

<script>
	import storage from '@/utils/storage.js';
	import {
		getAudio
	} from '@/api/image.js';
	import {
		environment
	} from '@/config/environment.js'

	export default {
		props: {
			rhrId: {
				type: [String, Number],
				default: ''
			},
			dataId: {
				type: [String, Number],
				default: ''
			},
		},
		data() {
			return {
				BASEAPI: getApp().globalData.BASEAPI,
				isRecording: false,
				// H5 录音对象
				mediaRecorder: null,
				audioChunks: [],
				recordFilePath: '',
				audioData: {},
				recordFileList: [],
				duration: 0,
				timer: null,
				// 小程序和APP录音对象
				recorderManager: null
			};
		},
		onLoad() {
		  // #ifdef MP-WEIXIN || APP-PLUS
		  this.init();
		  // #endif
		},
		created() {
			this.getAudioFile();
		},
		methods: {
			async getAudioFile() {
				try {
					let res = await getAudio(this.dataId);
					this.audioData = res.data;	
				} catch {
					uni.hideLoading();
				}
			},
			init() {
				// 获取全局的录音管理器
				this.recorderManager = uni.getRecorderManager();
				
				this.recorderManager.onStart(() => {
				  console.log('录音开始');
				});
				
				this.recorderManager.onStop(res => {
				  this.recordFilePath = res.tempFilePath;
				  let formData = {
					  rhrId: this.rhrId,
					  dataId: this.dataId
				  }
				  // this.isRecording = false;
				  clearInterval(this.timer);
				  this.uploadAudio(this.recordFilePath, 2);
				  console.log('录音停止', res.tempFilePath);
				});
				
				this.recorderManager.onError(res => {
				  console.error('录音错误:', res);
				  // this.isRecording = false;
				  clearInterval(this.timer);
				  uni.showToast({
				    title: '录音失败',
				    icon: 'none'
				  });
				});
			},
			// 立即录音
			recordImmediately() {
				this.isRecording = !this.isRecording;
				if (this.isRecording) {
					this.startRecording();
				} else {
					this.stopRecording();
				}
			},
			async startRecording() {
			  // #ifdef H5
			  this.startH5Record();
			  // #endif
				  
			  // #ifdef MP-WEIXIN
			  this.startMiniProgramRecord('mp3');
			  // #endif
			  
			  // #ifdef APP-PLUS
			  this.startMiniProgramRecord('wav');
			  // #endif
			},
			stopRecording() {
				// #ifdef H5
				this.stopH5Record();
				// #endif
				
				// #ifdef MP-WEIXIN || APP-PLUS
				this.stopMiniProgramRecord();
				// #endif
			},
			async startH5Record() {
				// H5端实现
				  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
					  try {
						const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
						this.mediaRecorder = new MediaRecorder(stream);
						this.mediaRecorder.ondataavailable = (event) => {
						  this.audioChunks.push(event.data);
						};
						this.mediaRecorder.onstop = async () => {
						  const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
						  // this.recordFilePath = URL.createObjectURL(audioBlob);
                          //this.uploadAudio(this.recordFilePath, 2);
						  // 这里需要把blob文件获取后缀名,重新创建文件,否则传入后端没有后缀名
						  const extension = this.getFileExtension(audioBlob.type);
						  const fileName = `recording_${Date.now()}.${extension}`;
						  const newFile = new File([audioBlob], fileName, { type: 'audio/wav' });
						  
						  this.uploadAudio(newFile, 1);
						};
						this.mediaRecorder.start();
						// this.isRecording = true;
						this.duration = 0;
						this.timer = setInterval(() => {
						  this.duration++;
						}, 1000);
					  } catch (error) {
						console.error('获取麦克风权限失败:', error);
					  }
				  } else {
					uni.showToast({
					  title: '浏览器不支持录音',
					  icon: 'none'
					});
				}
			},
			stopH5Record() {
				if (this.mediaRecorder) {
				  this.mediaRecorder.stop();
				  // this.isRecording = false;
				  clearInterval(this.timer);
				}
			},
			startMiniProgramRecord(type) {
				this.recorderManager.start({
				  format: type, // 录音格式,可选 aac/mp3/wav
				});
				
				// this.isRecording = true;
				this.duration = 0;
				this.timer = setInterval(() => {
				  this.duration++;
				}, 1000);
			},
			stopMiniProgramRecord() {
				console.log('录音结束', this.recorderManager);
				this.recorderManager.stop();
				// this.isRecording = false;
				clearInterval(this.timer);
			},
			// 选择文件上传
			chooseRecord(audioBlob) {
				uni.chooseFile({
				  count: 1, //默认100
				  extension:['.wav', '.mp3', '.aac'],
					success: res => {
						console.log(JSON.stringify(res.tempFilePaths));
						this.uploadAudio(res.tempFilePaths[0], 2);
					},
					fail: err => {
						if (err.errMsg == "chooseFile:fail cancel") {
							uni.$u.toast('取消上传');
						} else {
						    uni.$u.toast('上传失败');
						}
					}
				});
			},
			// 上传录音
			uploadAudio(url, type) {
				const formData = {
					rhrId: this.rhrId,
				    dataId: this.dataId
				}
				let filePath = url;
				uni.showLoading({
					title: '上传中'
				});
				// 判断是传的1 文件还是2 临时文件路径
				if (type == 1) {
					uni.uploadFile({
					  url: this.BASEAPI + '/phip-phr/file/uploadAudio',
					  file: filePath,
					  name: 'audio',
					  header: {
					  	Authorization: storage.get('AccessToken')
					  },
					  formData: formData,
					  success: (res) => {
						uni.hideLoading()
						uni.$u.toast('上传成功');
						this.getAudioFile();
					    console.log('上传成功:', res);
					  },
					  fail: (err) => {
						uni.hideLoading()
						uni.$u.toast('上传失败');
					    console.error('上传失败:', err);
					  }
					});
				} else {
					uni.uploadFile({
					  url: this.BASEAPI + '/phip-phr/file/uploadAudio',
					  filePath: filePath,
					  name: 'audio',
					  header: {
					  	Authorization: storage.get('AccessToken')
					  },
					  formData: formData,
					  success: (res) => {
						uni.hideLoading()
						uni.$u.toast('上传成功');
						this.getAudioFile();
					    console.log('上传成功:', res);
					  },
					  fail: (err) => {
						uni.hideLoading()
						uni.$u.toast('上传失败');
					    console.error('上传失败:', err);
					  }
					});
				}
			},
			// 获取文件扩展名
			getFileExtension(mimeType) {
			  const extensions = {
			    "audio/webm": "webm",
			    "audio/ogg": "ogg",
			    "audio/mp4": "mp4",
			    "audio/mpeg": "mp3",
			    "audio/wav": "wav",
			  };
			  return extensions[mimeType] || "webm";
			}
		},
		beforeDestroy() {
		  if (this.recorderManager && this.isRecording) {
		    this.recorderManager.stop();
		  }
		  clearInterval(this.timer);
		  this.stopH5Record();
		}
	};
</script>

<style lang="scss" scoped>
	.opinion-image {
		display: flex;
		flex-wrap: wrap;
		width: 100%;
	}

	.image {
		width: 30vw;
		height: 29.5vw;
		margin: 5rpx;
		border-radius: 5rpx;
		overflow: hidden;
		position: relative;
	}

	.image-content {
		width: 100%;
		height: 100%;
	}

	.image-del {
		position: absolute;
		top: 10rpx;
		right: 10rpx;
		width: 40rpx;
		height: 40rpx;
		color: #fff;
		background-color: rgba(0, 0, 0, 0.3);
		border-radius: 20rpx;
		text-align: center;
		line-height: 40rpx;
	}

	.image-select {
		display: flex;
		justify-content: center;
		align-items: center;
		border: 1rpx dashed #888;
		border-radius: 5rpx;
	}

	// .image-select i {
	// 	width: 4vw;
	// 	height: 4vw;
	// 	line-height: 4vw;
	// 	text-align: center;
	// 	font-size: 35rpx;
	// 	font-weight: 300;
	// }
	.cross {
		position: relative;
		width: 200px;
		height: 200px;
	}

	.cross::before,
	.cross::after {
		content: '';
		position: absolute;
	}

	.cross::before {
		top: 50%;
		left: 20%;
		height: 0px;
		width: 60%;
		border-top: 1rpx dashed #888;
	}

	.cross::after {
		top: 35%;
		left: 50%;
		height: 30%;
		width: 0px;
		border-right: 1rpx dashed #888;
	}
	
	.btn {
		flex: 1;
		height: 80rpx;
		line-height: 80rpx;
		background: #27d8b3;
		border-radius: 5px;
		color: #fff;
		font-size: 16px;
		text-align: center;
	}
	
	/* 定义一个名为 'blink' 的关键帧 */
	@keyframes blink {
	  0%, 100% {
	    opacity: 1; /* 完全不透明 */
	  }
	  50% {
	    opacity: 0.2; /* 半透明 */
	  }
	}
	 
	
	#blinking-image {
	  animation: blink 2s infinite; 
	  width: 130rpx;
	  height: 100rpx;
	}
</style>

问题

在做一个uniapp 移动 h5 项目时,遇到一个需要上传录音的需求,一是可以点击上传录音,二是需要立即录音。上传录音直接通过 uni.chooseFile 选择文件上传没有问题(文件是直接有后缀名的),但是点击立即录音,直接生成的 blob 文件传给后端则是无后缀名的。类似 file-1735458785

这样的格式

解决办法

uni-app 的 H5 环境中,通过 uni.uploadFile() 上传 Blob 类型的文件时,确实可能会遇到文件没有后缀扩展名的问题。这个问题会导致服务端无法正确判断文件类型和进行有效处理。为了完美解决这个问题,可以使用以下方法确保文件上传时带上正确的扩展名

  1. 使用 Blob 的 MIME 类型判断扩展名:通过 Blob 对象的 type 属性来获取文件的 MIME 类型。根据 MIME 类型推测文件的扩展名(例如:image/png 对应 .pngimage/jpeg 对应 .jpg)。
  2. 通过 File 对象上传文件:将 Blob 转换为 File 对象,给文件指定正确的文件名和扩展名,然后再进行上传。
1. 获取 Blob 的 MIME 类型

首先,获取 Blob 的 MIME 类型,并根据 MIME 类型为文件添加合适的扩展名。

function getFileExtensionFromMimeType(mimeType) {
  const mimeTypes = {
    'image/png': 'png',
    'image/jpeg': 'jpg',
    'image/gif': 'gif',
    'image/webp': 'webp',
    'image/bmp': 'bmp',
    'image/tiff': 'tiff',
  };

  return mimeTypes[mimeType] || 'unknown';
}
2. 创建带扩展名的 File 对象

通过 Blobtype 属性获取 MIME 类型,并根据 MIME 类型生成文件的扩展名,然后创建一个新的 File 对象。

function createFileWithExtension(blob) {
  const mimeType = blob.type;
  const extension = getFileExtensionFromMimeType(mimeType);
  
  // 为文件生成一个随机文件名或者基于当前时间戳的文件名
  const fileName = `image_${new Date().getTime()}.${extension}`;

  // 创建一个新的 File 对象
  return new File([blob], fileName, { type: mimeType });
}
3. 使用 uni.uploadFile() 上传文件

通过将 Blob 转换为 File 对象后,使用 uni.uploadFile() 方法上传文件。

function uploadBlobImage(blob) {
  // 创建带有扩展名的文件对象
  const file = createFileWithExtension(blob);

  // 上传文件
  uni.uploadFile({
    url: 'https://your-upload-api-url.com',  // 替换为你的上传接口
    file: file,  // 直接传入 File 对象
    name: 'file',  // 后端接收文件的字段名
    formData: {
      rhrId: '12345',  // 你可以添加其他表单数据
    },
    success: (res) => {
      console.log('上传成功', res);
    },
    fail: (err) => {
      console.error('上传失败', err);
    }
  });
}

这样录音完成后,上传的就是自定义设置的文件名字啦

录音功能参考博文:uniapp实现H5页面麦克风权限获取与录音功能_uniapp h5 录音-CSDN博客

                                 web网页上实现录音功能(vue3)_vue 录音-CSDN博客

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐