仿微信语音聊天webrtc
主要技术
MediaRecorder 录音
webrtc 获取麦克风
URL.createObjectURL 转换为url(实际生产中,通过后端转换blob为mp3网址)
实现elementui+vue
1.html
<div class="chat-record">
<audio ref="chataudio"></audio>
<transition-group tag="ul" class="msg-list" name="fade">
<li v-for="(item, index) in chunList" :key="index" class="msg" @click="onPlay(index)" @touchend.prevent="onPlay(index)" :style="`flex-direction:${item.sendUserName===userName?'row-reverse':'row'}`">
<div class="avatar">
<dt>{{item.sendUserName}}</dt>
<dd>{{item.sendTime}}</dd>
</div>
<div v-cloak class="audio" :style="{width: 20 * item.audioStream + 'px'}" :class="{wink: item.wink,rotate:item.sendUserName!==userName}">
<span>(</span>
<span>(</span>
<span>(</span>
</div>
<div class="duration">{{item.audioStream}}"</div>
</li>
</transition-group>
</div>
<span slot="footer" class="dialog-footer">
<el-button type="primary" class="submit-btn" @mousedown.native="onMousedown" @touchstart.native="onMousedown" @mouseup.native="onMouseup" @touchend.native="onMouseup">{{btnText}}</el-button>
</span>
touchstart.native为了移动端触感灵敏
2.判断浏览器是否有相关api
function getUserMedia(constrains, success, error) {
let promise
if (navigator.mediaDevices.getUserMedia) {
//最新标准API
promise = navigator.mediaDevices
.getUserMedia(constrains)
.then(success)
.catch(error)
} else if (navigator.webkitGetUserMedia) {
//webkit内核浏览器
promise = navigator
.webkitGetUserMedia(constrains)
.then(success)
.catch(error)
} else if (navigator.mozGetUserMedia) {
//Firefox浏览器
promise = navigator.mozGetUserMedia(constrains).then(success).catch(error)
} else if (navigator.getUserMedia) {
//旧版API
promise = navigator.getUserMedia(constrains).then(success).catch(error)
}
return promise
}
function canGetUserMediaUse() {
return !!(
navigator.mediaDevices.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia
)
}
3. new window.MediaRecorder(stream)
if (canGetUserMediaUse()) {
getUserMedia(
{
video: false,
audio: true,
},
(stream) => {
this.recorder = new window.MediaRecorder(stream)
this.bindEvents()
},
(error) => {
console.log(error)
alert('出错,请确保已允许浏览器获取录音权限' + error)
}
)
} else {
alert('您的浏览器不兼容')
}
},
4.录音开始,暂停,保存,转换
onMousedown() {
this.btnText = '松开结束'
this.onStart()
},
onMouseup() {
this.btnText = '按住说话'
this.onStop()
},
onStart() {
this.recorder.start()
},
onStop() {
this.recorder.stop()
},
//重点
bindEvents() {
this.recorder.ondataavailable = this.getRecordingData
this.recorder.onstop = this.saveRecordingData
},
getRecordingData(e) {
this.chunks.push(e.data)
},
saveRecordingData() {
let blob = new Blob(this.chunks, { type: 'audio/ogg; codecs=opus' })
let audioStream = URL.createObjectURL(blob)
//估算时长
let duration = parseInt(blob.size / 6600)
if (duration <= 0) {
alert('说话时间太短')
return
}
if (duration > 60) {
duration = 60
}
this.chunkList.push({audioStream: audioStream, duration: duration})
this.chunks = []
},
5.播放
onPlay(index) {
let item = this.chunList[index]
this.audio = this.$refs.chataudio
this.audio.src = item.audioStream
this.audio.play()
this.bindAudioEvent(index)
},
完整代码
这是props获取websoket数据,自己使用请把wsdata换成chunkList
点击查看代码
<template>
<el-dialog append-to-body class="box-dialog" custom-class="dark-dialog" :visible.sync="dialogVisible" width="500px" center>
<div class="chat-record">
<audio ref="chataudio"></audio>
<transition-group tag="ul" class="msg-list" name="fade">
<li v-for="(item, index) in wsdata" :key="index" class="msg" @click="onPlay(index)" @touchend.prevent="onPlay(index)" :style="`flex-direction:${item.sendUserName===userName?'row-reverse':'row'}`">
<div class="avatar">
<dt>{{item.sendUserName}}</dt>
<dd>{{item.sendTime}}</dd>
</div>
<div v-cloak class="audio" :style="{width: 20 * item.sendContentLength + 'px'}" :class="{wink: item.wink,rotate:item.sendUserName!==userName}">
<span>(</span>
<span>(</span>
<span>(</span>
</div>
<div class="duration">{{item.sendContentLength}}"</div>
</li>
</transition-group>
</div>
<span slot="footer" class="dialog-footer">
<el-button type="primary" class="submit-btn" @mousedown.native="onMousedown" @touchstart.native="onMousedown" @mouseup.native="onMouseup" @touchend.native="onMouseup">{{btnText}}</el-button>
</span>
</el-dialog>
</template>
<script>
function getUserMedia(constrains, success, error) {
let promise
if (navigator.mediaDevices.getUserMedia) {
//最新标准API
promise = navigator.mediaDevices
.getUserMedia(constrains)
.then(success)
.catch(error)
} else if (navigator.webkitGetUserMedia) {
//webkit内核浏览器
promise = navigator
.webkitGetUserMedia(constrains)
.then(success)
.catch(error)
} else if (navigator.mozGetUserMedia) {
//Firefox浏览器
promise = navigator.mozGetUserMedia(constrains).then(success).catch(error)
} else if (navigator.getUserMedia) {
//旧版API
promise = navigator.getUserMedia(constrains).then(success).catch(error)
}
return promise
}
function canGetUserMediaUse() {
return !!(
navigator.mediaDevices.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia
)
}
export default {
data() {
return {
dialogVisible: false,
chunks: [],
audio: '',
chunkList: [],
btnText: '按住说话',
}
},
props: {
wsdata: {
type: Array,
default: () => {},
},
userName: {
type: String,
default: '',
},
},
mounted() {
this.requestAudioAccess()
},
methods: {
open(data) {
this.dialogVisible = true
},
close() {
this.dialogVisible = false
this.resetForm()
},
requestAudioAccess() {
if (canGetUserMediaUse()) {
getUserMedia(
{
video: false,
audio: true,
},
(stream) => {
this.recorder = new window.MediaRecorder(stream)
this.bindEvents()
},
(error) => {
console.log(error)
alert('出错,请确保已允许浏览器获取录音权限' + error)
}
)
} else {
alert('您的浏览器不兼容')
}
},
onMousedown() {
this.btnText = '松开结束'
this.onStart()
},
onMouseup() {
this.btnText = '按住说话'
this.onStop()
},
onStart() {
this.recorder.start()
},
onStop() {
this.recorder.stop()
},
onPlay(index) {
this.wsdata.forEach((item) => {
this.$set(item, 'wink', false)
})
let item = this.wsdata[index]
this.audio = this.$refs.chataudio
this.audio.src = item.sendContent
this.audio.play()
this.bindAudioEvent(index)
},
bindAudioEvent(index) {
let item = this.wsdata[index]
this.audio.onplaying = () => {
this.$set(item, 'wink', true)
}
this.audio.onended = () => {
this.$set(item, 'wink', false)
}
},
bindEvents() {
this.recorder.ondataavailable = this.getRecordingData
this.recorder.onstop = this.saveRecordingData
},
getRecordingData(e) {
this.chunks.push(e.data)
},
saveRecordingData() {
let blob = new Blob(this.chunks, { type: 'audio/ogg; codecs=opus' })
let audioStream = URL.createObjectURL(blob)
//估算时长
let duration = parseInt(blob.size / 6600)
if (duration <= 0) {
alert('说话时间太短')
return
}
if (duration > 60) {
duration = 60
}
this.$emit('getvoice', {blob: blob, duration: duration})
this.chunks = []
},
},
}
</script>
<style lang="scss" scoped>
.box-dialog::v-deep .el-dialog__footer {
padding:0 0;
padding-bottom: 10px;
}
.box-dialog::v-deep .el-dialog__body {
padding-bottom:0;
//background-color: rgba(#181b40, 0.9);
}
.box-dialog::v-deep .el-dialog__header {
display: none;
}
.submit-btn {
width: 96%;
height: 45px;
position: relative;
background-color: #181b40;
&:active {
background-color: #03225c;
}
&:active:before {
position: absolute;
left: 50%;
transform: translate(-50%, 0);
top: -2px;
content: '';
width: 0%;
height: 2px;
background-color: #7bed9f;
animation: loading 1s ease-in-out infinite backwards;
}
}
.chat-record {
width: 100%;
height: 300px;
overflow-y: scroll;
color: #fff;
}
.msg-list {
margin: 0;
padding: 0;
height: 100%;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.msg-list::-webkit-scrollbar {
display: none;
}
.msg-list .msg {
list-style: none;
padding: 0 8px;
margin: 10px 0;
overflow: hidden;
cursor: pointer;
}
.msg-list .msg {
display: flex;
flex-direction: row-reverse;
}
.rotate{
transform: rotate(180deg);
}
.msg-list .msg .avatar {
height: 34px;
line-height: 14px;
font-size: 12px;
background-size: 100%;
dt {
font-size: 14px;
}
}
.msg-list .msg .audio {
position: relative;
margin-right: 6px;
max-width: 116px;
min-width: 30px;
height: 24px;
line-height: 24px;
padding: 0 4px 0 10px;
border-radius: 2px;
color: #000;
text-align: right;
background-color: rgba(107, 197, 107, 0.85);
}
.msg-list .msg.eg {
cursor: default;
}
.msg-list .msg.eg .audio {
text-align: left;
}
.msg-list .msg .audio:before {
position: absolute;
right: -8px;
top: 8px;
content: '';
display: inline-block;
width: 0;
height: 0;
border-style: solid;
border-width: 4px;
border-color: transparent transparent transparent rgba(107, 197, 107, 0.85);
}
.msg-list .msg .audio span {
color: rgba(255, 255, 255, 0.8);
display: inline-block;
transform-origin: center;
}
.msg-list .msg .audio span:nth-child(1) {
font-weight: 400;
}
.msg-list .msg .audio span:nth-child(2) {
transform: scale(0.8);
font-weight: 500;
}
.msg-list .msg .audio span:nth-child(3) {
transform: scale(0.5);
font-weight: 700;
}
.msg-list .msg .audio.wink span {
animation: wink 1s ease infinite;
}
.msg-list .msg .duration {
margin: 3px 2px;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
@keyframes wink {
from {
color: rgba(255, 255, 255, 0.8);
}
to {
color: rgba(255, 255, 255, 0.1);
}
}
@keyframes loading {
from {
width: 0%;
}
to {
width: 100%;
}
}
</style>
本文来自博客园,作者:流云君,转载请注明原文链接:https://www.cnblogs.com/yun10011/p/16520306.html