h5实现录音功能(navigator.mediaDevices)
封装组件(soundRecording.vue):
<template> <view class="recorder"> </view> </template> <script> export default { data() { return { isUserMedia: false, stream: null, audio: null, recorder: null, chunks: [] }; }, mounted() { /** * error 事件的返回状态 * 100: 请在HTTPS环境中使用 * 101: 浏览器不支持 * 201: 用户拒绝授权 * 500: 未知错误 * */ if (origin.indexOf('https') === -1) { this.$emit('error', '100'); uni.showModal({ title: '提示', content: '请在https环境中使用录音功能,确认返回上一个页面', showCancel: false, success: (res) => { if (res.confirm) { uni.navigateBack(); } } }); } if (!navigator.mediaDevices || !window.MediaRecorder) { this.$emit('error', '101'); uni.showModal({ title: '提示', content: '当前浏览器不支持录音功能,确认返回上一个页面', showCancel: false, success: (res) => { if (res.confirm) { uni.navigateBack(); } } }); } this.getRecorderManager(); }, methods: { getRecorderManager() { this.audio = document.createElement('audio'); navigator.mediaDevices .getUserMedia({ audio: true }) .then((stream) => { this.isUserMedia = true; stream.getTracks().forEach((track) => { track.stop(); }); }) .catch((err) => { this.onErrorHandler(err); }); }, start() { if (!this.isUserMedia) { uni.showModal({ title: '提示', content: '当前设备不支持,确认返回上一个页面', showCancel: false, success: (res) => { if (res.confirm) { uni.navigateBack(); } } }); } navigator.mediaDevices .getUserMedia({ audio: true }) .then((stream) => { this.stream = stream; this.recorder = new MediaRecorder(stream); this.recorder.ondataavailable = this.getRecordingData; this.recorder.onstop = this.saveRecordingData; this.recorder.start(); }) .catch((err) => { this.onErrorHandler(err); }); }, stop() { if (this.recorder) { this.recorder.stop(); this.stream.getTracks().forEach((track) => { track.stop(); }); } }, getRecordingData(e) { this.chunks.push(e.data); }, saveRecordingData() { const blob = new Blob(this.chunks, { type: 'audio/mpeg' }), localUrl = URL.createObjectURL(blob); this.chunks = []; let lock = true; const temporaryAudio = document.createElement('audio'); temporaryAudio.src = localUrl; temporaryAudio.muted = true; temporaryAudio.load(); temporaryAudio.play(); temporaryAudio.addEventListener('timeupdate', (e) => { if (!Number.isFinite(temporaryAudio.duration)) { temporaryAudio.currentTime = Number.MAX_SAFE_INTEGER; temporaryAudio.currentTime = 0; } else { document.body.append(temporaryAudio); document.body.removeChild(temporaryAudio); if (lock) { lock = false; const recorder = { data: blob, duration: temporaryAudio.duration, localUrl: localUrl }; this.$emit('success', recorder); } } }); }, onErrorHandler(err) { console.log(err); if (err.name === 'NotAllowedError') { this.$emit('error', '201'); uni.showModal({ title: '提示', content: '用户拒绝了当前浏览器的访问请求,确认返回上一个页面', showCancel: false, success: (res) => { if (res.confirm) { uni.navigateBack(); } } }); } if (err.name === 'NotReadableError') { this.$emit('error', '101'); uni.showModal({ title: '提示', content: '当前浏览器不支持,确认返回上一个页面', showCancel: false, success: (res) => { if (res.confirm) { uni.navigateBack(); } } }); } this.$emit('error', '500'); uni.showModal({ title: '提示', content: '调用失败,确认返回上一个页面', showCancel: false, success: (res) => { if (res.confirm) { uni.navigateBack(); } } }); } }, destroyed() { this.stop(); } }; </script>
基础使用组件:
<template> <view> <view class="audio" v-if="recorder"> <audio :src="recorder.localUrl" name="本地录音" controls="true"></audio> <br /> <button type="primary" @click="handlerSave">保存录音</button> </view> <h3 v-else>点击下方按钮录音</h3> <div class="container" v-if="status"> <div class="wave0"></div> <div class="wave1"></div> </div> <view @click="handlerOnCahnger" class="statusBox" :class="{ active: status }"> <view class="status"></view> </view> <sound-recording ref="recorder" @success="handlerSuccess" @error="handlerError"></sound-recording> </view> </template> <script> import SoundRecording from '@/components/soundRecording/soundRecording.vue'; export default { components: { SoundRecording }, data() { return { status: false, recorder: null }; }, methods: { handlerSave() { uni.downloadFile({ url: this.recorder.localUrl, //仅为示例,并非真实的资源 success: (res) => { if (res.statusCode === 200) { var oA = document.createElement('a'); oA.download = ''; // 设置下载的文件名,默认是'下载' oA.href = res.tempFilePath; //临时路径再保存到本地 document.body.appendChild(oA); oA.click(); oA.remove(); // 下载之后把创建的元素删除 } }, fail: (err) => { uni.showToast({ title: `下载失败${err}` }); } }); }, handlerOnCahnger() { if (this.status) { this.$refs.recorder.stop(); } else { this.$refs.recorder.start(); } this.status = !this.status; }, handlerSuccess(res) { console.log(res); this.recorder = res; }, handlerError(code) { switch (code) { case '101': uni.showModal({ content: '当前浏览器版本较低,请更换浏览器使用,推荐在微信中打开。' }); break; case '201': uni.showModal({ content: '麦克风权限被拒绝,请刷新页面后授权麦克风权限。' }); break; default: uni.showModal({ content: '未知错误,请刷新页面重试' }); break; } } } }; </script> <style lang="scss" scoped> .audio { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20rpx; } h3 { text-align: center; padding: 20rpx; } .statusBox { z-index: 11; width: 120rpx; height: 120rpx; background-color: aliceblue; border-radius: 50%; box-shadow: 5rpx 5rpx 10rpx rgba(000, 000, 000, 0.35); display: flex; align-items: center; justify-content: center; position: fixed; left: 50%; bottom: 120rpx; transform: translateX(-50%); .status { width: 60rpx; height: 60rpx; background-color: aliceblue; border-radius: 50%; border: 10rpx solid #5c5c66; } &.active { background-color: rgba(118, 218, 255, 0.45); .status { background-color: rgba(118, 218, 255, 0.45); border: 10rpx solid rgba(255, 255, 255, 0.75); } } } .container { z-index: 10; position: fixed; bottom: -200rpx; padding: 0; border: 0; width: 750rpx; height: 750rpx; background-color: rgb(118, 218, 255); } .wave0, .wave1, .wave2 { position: absolute; width: 750rpx * 2; height: 750rpx * 2; margin-top: -150%; margin-left: -50%; background-color: rgba(255, 255, 255, 0.4); border-radius: 45%; animation: spin 15s linear -0s infinite; z-index: 1; /* border: 1px solid;*/ } .wave1 { margin-top: -152%; background-color: rgba(255, 255, 255, 0.9); border-radius: 47%; animation: spin 30s linear -15s infinite; z-index: 2; } @keyframes spin { 0% { transform: translate(-0%, -0%) rotate(0deg) scale(1); } 25% { transform: translate(-1%, -1%) rotate(90deg) scale(1); } 50% { transform: translate(-0%, -2%) rotate(180deg) scale(1); } 75% { transform: translate(1%, -1%) rotate(270deg) scale(1); } 100% { transform: translate(-0%, -0%) rotate(360deg) scale(1); } } </style>
效果:
按照这个功能写了一个类似微信语音聊天功能:
<template> <view> <view class="record"> <block v-for="(item, index) in records" :key="index"> <view class="item"> <view class="content" v-if="item.type === 'text'">{{ item.content }}</view> <view class="content" v-else-if="item.type === 'img'"> <image :src="item.content" mode="widthFix" @click="openImg(item.content)"></image> </view> <view class="content" :class="{ active: playItemIndex === index }" v-else-if="item.type === 'recorder'" @click="playRecorder(index)"> <view class="recorder"> <view class="time" :style="{ marginRight: changerWidth(item.content.duration) }"> {{ item.content.duration | integer }}" </view> <view class="icon"> <text class="on1">(</text> <text class="on2">(</text> <text class="on3">(</text> </view> </view> </view> <!-- <image class="hander" src="/static/imitate_weChat/logo.jpg" /> --> </view> </block> </view> <view class="toolBox"> <view class="recorder" :class="{ active: isUseRecorder }" @touchstart.prevent="startRecorder" @touchend.prevent="endRecorder"> {{ isUseRecorder ? '松开 结束' : '按住 说话' }} </view> <!-- <view class="camrea" @click="camera">55</view> --> </view> <view class="toolBg"></view> <sound-recording ref="recorderRef" @success="handlerSuccess" @error="handlerError"></sound-recording> </view> </template> <script> import SoundRecording from '@/components/soundRecording/soundRecording.vue'; export default { components: { SoundRecording }, data() { return { audio: null, records: [ { type: 'text', content: 'Hello World!' }, { type: 'text', content: 'Hello!' } ], isUseRecorder: false, playItemIndex: -1, currentAudio: '' }; }, mounted() { this.audio = document.createElement('audio'); this.audio.addEventListener('ended', () => { this.playItemIndex = -1; this.currentAudio = ''; }); }, methods: { openImg(img) { uni.previewImage({ urls: [img] }); }, startRecorder() { this.$refs.recorderRef.start(); this.isUseRecorder = true; }, endRecorder() { this.$refs.recorderRef.stop(); this.isUseRecorder = false; }, playRecorder(index) { this.playItemIndex = index; this.currentAudio = this.records[index].content.localUrl; this.audio.src = this.currentAudio; this.audio.play(); }, handlerSuccess(res) { if (res.duration < 1) return uni.showToast({ title: '语言时间小于1秒', icon: 'error' }); this.records.push({ type: 'recorder', content: res }); }, changerWidth(v) { let a = 40; let b = v * 20; if (b > 450) b = 450; return a + b + 'rpx'; }, handlerError(code) { switch (code) { case '101': uni.showModal({ content: '当前浏览器版本较低,请更换浏览器使用,推荐在微信中打开。' }); break; case '201': uni.showModal({ content: '麦克风权限被拒绝,请刷新页面后授权麦克风权限。' }); break; default: uni.showModal({ content: '未知错误,请刷新页面重试' }); break; } }, camera() { uni.showToast({ title: '录像模块开发中', icon: 'none' }); } }, filters: { integer(v) { return Math.ceil(v); } } }; </script> <style lang="scss" scoped> page { background-color: #f1eded; } .record { padding: 20rpx; font-size: 28rpx; .item { display: flex; justify-content: flex-end; padding: 10rpx; margin-bottom: 15rpx; .content { margin-right: 20rpx; position: relative; background-color: rgba(107, 197, 107, 0.85); padding: 20rpx 30rpx; border-radius: 10rpx; &::before { position: absolute; right: -8px; top: 8px; content: ''; display: inline-block; width: 0; height: 0; border: 4px solid transparent; border-left-color: rgba(107, 197, 107, 0.85); } image { max-width: 400rpx; } .recorder { display: flex; align-items: center; .time { margin-right: 40rpx; } .icon { font-weight: bold; color: #fff; font-size: 32rpx; display: flex; align-items: center; text { display: block; } & text:nth-of-type(1) { transform: scale(1.2); } & text:nth-of-type(2) { transform: scale(0.8); } & text:nth-of-type(3) { transform: scale(0.6); } } } &.active { .recorder { .icon { animation: play 1.5s ease-in-out infinite backwards; } } } } .hander { width: 80rpx; height: 80rpx; border-radius: 50%; } } } .toolBg { height: 140rpx; } .toolBox { border-top: 1px solid #ccc; position: fixed; bottom: 0; left: 0; right: 0; display: flex; align-items: center; padding: 0rpx 30rpx; height: 140rpx; background-color: rgba(255, 255, 255, 0.45); .recorder { width: 550rpx; display: flex; align-items: center; justify-content: center; background-color: #fff; padding: 17rpx; border-radius: 10rpx; font-size: 28rpx; box-shadow: 2rpx 3rpx 10rpx rgba(0, 0, 0, 0.2); position: relative; &.active { background-color: #95a5a6; &::before { position: absolute; left: 50%; transform: translate(-50%); top: -3px; content: ''; width: 0; height: 3px; background-color: #7bed9f; animation: loading-data 1.25s ease-in-out infinite backwards; } } } @keyframes loading-data { 0% { width: 0; } 100% { width: 100%; } } @keyframes play { 0% { color: #fff; } 50% { color: #c3c3c3; } 100% { columns: #fff; } } .camrea { flex: 1; display: flex; align-items: center; justify-content: center; } } </style>
效果:
注意:需要调用录音功能的域名必须是https,否则调用失败!
希望大佬看到有不对的地方,提出博主予以改正!