Files
dvcp_v2_wxcp_app/library/apps/AppBroadcast/LiveBroadcast.vue
2024-10-31 14:34:57 +08:00

686 lines
15 KiB
Vue

<template>
<div class="LiveBroadcast" @touchend="onTouchend" @touchmove.stop="onTouchmove">
<div class="top" @click="toChoose" hover-class="bg-hover">
<div class="left">
<h2 v-if="!equipmentList.length">请选择设备</h2>
<h2>{{ equipmentStr }}</h2>
<h2 v-if="equipmentList.length > 2">...等</h2>
<span v-if="equipmentList.length > 2">{{ equipmentList.length }}</span>
<h2 v-if="equipmentList.length > 2">个设备</h2>
</div>
<image src="./img/right.png"/>
</div>
<div class="middle">
<div class="record" v-if="isShowRecord">
<h2>喊话记录</h2>
<scroll-view scroll-y class="record-wrapper">
<div class="record-item" v-for="(item, index) in recordList" :key="index">
<image :src="user.avatar"/>
<div class="right-wrapper">
<div class="right" :style="{width: 'calc(83px + ' + (item.duration / 2) + '%)'}" @click="play(item.src, index)">
<image mode="aspectFit" v-if="!item.isPlay" src="./img/voice-icon.png"/>
<image v-else src="./img/voice.gif"/>
<span>{{ item.duration }}"</span>
</div>
</div>
</div>
</scroll-view>
</div>
<div class="tips" v-if="!isShowRecord">
<image src="./img/body.png"/>
<p>请先选择设备再按住下方按钮开始喊话~</p>
</div>
</div>
<div class="bottom" @touchstart="onLongtap">
<image src="./img/microphone.png"/>
<p>按住说话</p>
</div>
<div class="voice" :class="[isShow ? 'active' : '']">
<div class="voice-bottom">
<image src="./img/voice-w.png"/>
</div>
<image class="close" :class="[isImpact ? 'close-active' : '']" :src="isImpact ? closeW : close"/>
<p>松开发送</p>
<div class="header-line">
<span class="line1 animation"></span>
<span class="line2 animation"></span>
<span class="line3 animation"></span>
<span class="line4 animation"></span>
<span class="line5 animation"></span>
<span class="line6 animation"></span>
<span class="line7 animation"></span>
<span class="line8 animation"></span>
<span class="line9 animation"></span>
<span class="line10 animation"></span>
<span class="line11 animation"></span>
<span class="line12 animation"></span>
<span class="line13 animation"></span>
<span class="line14 animation"></span>
<span class="line15 animation"></span>
<span class="line16 animation"></span>
<span class="line17 animation"></span>
<span class="line18 animation"></span>
<span class="line19 animation"></span>
<span class="line20 animation"></span>
</div>
<h2>{{ time }}S</h2>
</div>
</div>
</template>
<script>
import {mapState} from 'vuex'
import Recorder from 'recorder-core'
import 'recorder-core/src/engine/mp3'
import 'recorder-core/src/engine/mp3-engine'
export default {
name: 'LiveBroadcast',
appName: '实时喊话',
data() {
return {
x: 0,
y: 0,
w: 0,
h: 0,
isShowRecord: false,
close: require('./img/close.png'),
closeW: require('./img/close-w.png'),
isShow: false,
isImpact: false,
startTime: 0,
isRecording: false,
recorder: null,
blobFile: null,
time: 60,
timingTimeout: null,
equipmentList: [],
recordList: [],
innerAudioContext: null,
currIndex: -1,
broadcastId: ''
}
},
computed: {
...mapState(['user']),
equipmentStr() {
if (!this.equipmentList.length) {
return ''
}
return this.equipmentList.filter((item, index) => index < 2).map(v => v.serialName).join('、')
}
},
mounted() {
this.$nextTick(() => {
const close = document.querySelector('.close')
this.x = close.offsetLeft
this.y = close.offsetTop
this.w = close.clientWidth
this.h = close.clientHeight
})
},
destroyed() {
this.recorder.close()
},
onLoad() {
this.innerAudioContext = uni.createInnerAudioContext()
this.innerAudioContext.autoplay = true
this.innerAudioContext.onEnded(() => {
if (this.currIndex > -1) {
this.recordList.forEach((v, index) => {
this.$set(this.recordList[index], 'isPlay', false)
})
this.currIndex = -1
}
})
this.innerAudioContext.onPlay(() => {
this.$set(this.recordList[this.currIndex], 'isPlay', true)
})
uni.$on('chooseEquipment', e => {
this.equipmentList = e.equipmentList
})
this.recorder = Recorder({
type: 'mp3',
sampleRate: 16000,
bitRate: 16,
onProcess(buffers, powerLevel, bufferDuration, bufferSampleRate, newBufferIdx, asyncEnd) {
//可利用extensions/waveview.js扩展实时绘制波形
}
})
this.recorder.open((e) => {
console.log(e)
}, e => {
console.log(e)
})
},
methods: {
bindEvent(e) {
e.preventDefault()
},
onLongtap() {
this.time = 60
if (!this.equipmentList.length) {
return this.$u.toast('请选择播发设备')
}
this.isImpact = false
this.startTime = new Date().getTime()
this.isShow = true
this.record()
},
toChoose() {
uni.navigateTo({
url: './selectEquipment'
})
},
record() {
this.recorder.start()
this.timing()
},
timing() {
if (this.time === 0) {
this.stop()
return false
}
this.time = this.time - 1
this.timingTimeout = setTimeout(() => {
this.timing()
}, 1000)
},
play(url, index) {
if (this.currIndex === index) {
this.innerAudioContext.destroy()
this.recordList.forEach((v, index) => {
this.$set(this.recordList[index], 'isPlay', false)
})
this.currIndex = -1
return false
}
this.innerAudioContext.destroy()
this.recordList.forEach((v, index) => {
this.$set(this.recordList[index], 'isPlay', false)
})
this.currIndex = index
this.$nextTick(() => {
this.innerAudioContext.src = url
})
},
stop(isCancel) {
clearTimeout(this.timingTimeout)
this.recorder.stop((blob, duration) => {
if (isCancel) {
this.time = 60
return false
}
let formData = {}
formData = new FormData()
formData.append('file', new window.File([blob], `${(new Date).getTime()}`))
this.$loading()
this.$http.post(`/app/appdlbresource/uploadDlbFile`, formData).then(res => {
if (res.code === 0) {
this.$loading()
this.$http.post(`/app/appzyvideobroadcast/playrealtime`, {
mediaId: res.data.mediaId,
accessUrl: res.data.accessUrl,
messageLevel: 1,
taskType: 0,
coverageType: 4,
broadcastId: this.broadcastId || '',
serialNo: this.equipmentList.map(v => v.serialNo).join(',')
}).then(res => {
if (res.code === 0) {
!this.broadcastId && (this.broadcastId = res.data.broadcastId)
this.$u.toast('播发成功')
this.recordList.push({
src: (window.URL || webkitURL).createObjectURL(blob),
isPlay: false,
duration: (duration / 1000).toFixed(0)
})
this.isShowRecord = true
}
this.isShow = false
}).catch(() => {
this.isShow = false
uni.hideLoading()
})
} else {
this.isShow = false
}
}).catch(() => {
uni.hideLoading()
this.isShow = false
})
console.log(blob, (window.URL || webkitURL).createObjectURL(blob), '时长:' + duration + 'ms')
}, msg => {
console.log('录音失败:' + msg)
this.isShow = false
})
},
onTouchend() {
if (!this.isShow) return
if (this.isShow && new Date().getTime() - this.startTime < 1500) {
this.isImpact = false
this.isShow = false
this.stop(true)
return this.$u.toast('说话时间太短')
}
if (this.isImpact) {
this.isShow = false
this.isImpact = false
this.stop(true)
return false
}
this.isImpact = false
this.stop()
},
onTouchmove(e) {
if (this.isShow) {
const x = e.touches[0].clientX
const y = e.touches[0].clientY
if (x >= this.x && x <= this.x + this.w && y >= this.y && y <= this.y + this.h) {
this.isImpact = true
} else {
this.isImpact = false
}
}
},
submit() {
}
}
}
</script>
<style lang="scss" scoped>
.LiveBroadcast {
height: 100vh;
overflow: hidden;
user-select: none;
background: #F6F8FC;
.voice {
display: flex;
position: fixed;
flex-direction: column-reverse;
align-items: center;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
text-align: center;
opacity: 0;
background: rgba(0, 0, 0, 0.67);
transition: all ease 0.5s;
&.active {
z-index: 111;
opacity: 1;
}
.voice-bottom {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 234px;
margin-top: 48px;
background: url(./img/voice-bg.png);
background-size: 100% 100%;
image {
width: 96px;
height: 96px;
pointer-events: none;
user-select: none;
}
}
.header-line {
display: flex;
position: relative;
align-items: center;
justify-content: center;
width: 322px;
height: 164px;
padding: 0 20px;
border-radius: 20px;
background: #86B7FF;
&::after {
position: absolute;
bottom: 0;
left: 50%;
z-index: 1;
width: 0;
height: 0;
border-top: 18px solid #86B7FF;
border-right: 18px solid transparent;
border-left: 18px solid transparent;
content: ' ';
transform: translate(-50%, 100%);
}
span {
display: inline-block;
width: 6px;
height: 10px;
margin: 0 6px;
border: none;
border-radius: 4px;
background-color: #4B7CC3;
}
}
h2 {
margin-bottom: 48px;
font-weight: 400;
font-size: 96px;
color: #A2A3A4;
line-height: 134px;
}
& > p {
margin: 48px;
color: #A2A3A4;
font-size: 28px;
}
& > image {
width: 132px;
height: 132px;
transition: all ease 0.3s;
}
.close-active {
transform: scale(1.1);
}
}
.top {
display: flex;
align-items: center;
justify-content: space-between;
height: 128px;
padding: 0 32px;
.left {
display: flex;
align-items: center;
}
span {
color: #1174FE;
}
h2 {
color: #333333;
font-size: 32px;
font-weight: 600;
}
image {
width: 32px;
height: 32px;
}
}
.middle {
height: calc(100% - 378px);
overflow: hidden;
background: #fff;
.tips {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 100%;
image {
width: 406px;
height: 306px;
margin-bottom: 40px;
}
p {
width: 346px;
font-size: 30px;
font-weight: 400;
color: #999999;
line-height: 42px;
text-align: center;
}
}
.record {
height: 100%;
& > h2 {
height: 116px;
line-height: 116px;
padding: 0 32px;
font-size: 38px;
font-weight: 600;
color: #333333;
background: #FFFFFF;
}
.record-wrapper {
width: 100%;
height: calc(100% - 116px);
padding: 0 32px;
box-sizing: border-box;
.record-item {
display: flex;
position: relative;
align-items: center;
margin-bottom: 32px;
&:last-child {
margin-bottom: 0;
padding-bottom: 32px;
}
& > image {
width: 76px;
height: 76px;
margin-right: 26px;
border-radius: 8px;
}
.right-wrapper {
// flex: 1;
width: calc(100% - 102px);
}
.right {
display: flex;
align-items: center;
position: relative;
height: 74px;
padding: 0 32px;
border-radius: 12px;
min-width: 160px;
max-width: 100%;
background: #C0DAFF;
box-sizing: border-box;
&::after {
position: absolute;
top: 50%;
left: 0;
z-index: 1;
width: 0;
height: 0;
border-right: 12px solid #C0DAFF;
border-left: 12px solid transparent;
border-bottom: 12px solid transparent;
border-top: 12px solid transparent;
content: " ";
transform: translate(-100%, -50%);
}
image {
width: 28px;
height: 28px;
margin-right: 14px;
}
}
}
}
}
}
.bottom {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 250px;
width: min-content;
margin: 0 auto;
image {
width: 128px;
height: 128px;
margin-bottom: 16px;
pointer-events: none;
user-select: none;
}
p {
color: #333333;
font-size: 30px;
}
}
.animation {
animation: note 0.24s ease-in-out;
animation-iteration-count: infinite;
animation-direction: alternate;
}
@keyframes note {
from {
transform: scaleY(1);
}
to {
transform: scaleY(4);
}
}
.header-line span.line1 {
animation-delay: -1s;
}
.header-line span.line2 {
animation-delay: -0.9s;
}
.header-line span.line3 {
animation-delay: -0.8s;
}
.header-line span.line4 {
animation-delay: -0.7s;
}
.header-line span.line5 {
animation-delay: -0.6s;
}
.header-line span.line6 {
animation-delay: -0.5s;
}
.header-line span.line7 {
animation-delay: -0.4s;
}
.header-line span.line8 {
animation-delay: -0.4s;
}
.header-line span.line9 {
animation-delay: -0.2s;
}
.header-line span.line10 {
animation-delay: -0.1s;
}
.header-line span.line11 {
animation-delay: -1s;
}
.header-line span.line12 {
animation-delay: -0.9s;
}
.header-line span.line13 {
animation-delay: -0.8s;
}
.header-line span.line14 {
animation-delay: -0.7s;
}
.header-line span.line15 {
animation-delay: -0.6s;
}
.header-line span.line16 {
animation-delay: -0.5s;
}
.header-line span.line17 {
animation-delay: -0.4s;
}
.header-line span.line18 {
animation-delay: -0.3s;
}
.header-line span.line19 {
animation-delay: -0.2s;
}
.header-line span.line20 {
animation-delay: -0.1s;
}
}
</style>