经过测试,html自带的<audio>标签无法对音频精度到毫秒级别的播放控制 为了实现毫秒级精度控制,使用vue对WebAudioApi进行了封装
完整代码
js
<template>
<el-popover placement="left" trigger="manual" content="这是一段内容,这是一段内容,这是一段内容,这是一段内容。" @show="handleShow"
@hide="handleHide" v-model="isShow">
<div class="audio">
<template v-if="isShow">
<img v-if="isClick" src="./stop.svg" alt="" @click="pause">
<img v-else src="./play.svg" alt="" @click="playAudio">
<span>{{ formatCurrentTime }} / {{ formatEndTime }}</span>
</template>
</div>
<a slot="reference">
<i class="el-icon-video-play" style="font-size: 24px; cursor: pointer" @click="changeShow"></i>
</a>
</el-popover>
</template>
<script>
export default {
name: "Audio",
props: {
id: Number,
begin: Number,
end: Number,
current: Number,
url: {
type: String,
required: true,
default: ""
},
},
data() {
return {
currentTime: 0,
audioContext: null,
source: null,
isClick: false,
audioArrayBuffer: null,
timer: null,
isShow: false,
isFirstPlay: true,//是否第一次播放,(播放完毕后状态也要重置为第一次播放)
};
},
computed: {
formatCurrentTime() {
let minutes = Math.floor(this.currentTime / 60);
let seconds = Math.floor(this.currentTime % 60);
let milliseconds = Math.floor((this.currentTime % 1) * 100);
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}:${milliseconds.toString().padStart(2, '0')}`;
},
formatEndTime() {
let minutes = Math.floor(this.end / 60);
let seconds = Math.floor(this.end % 60);
let milliseconds = Math.floor((this.end % 1) * 100);
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}:${milliseconds.toString().padStart(2, '0')}`;
}
},
created() {
this.decodeAudioData()
this.currentTime = this.begin
},
watch: {
current: {
handler(val) {
console.log(val);
if (val != this.id) {
this.isShow = false;
}
},
},
},
beforeDestroy() {
this.close()
},
methods: {
async handleShow() {
if (this.audioContext.state === "closed") {
await this.decodeAudioData()
this.playAudio()
} else {
setTimeout(() => {
this.playAudio()
}, 500);
}
},
handleHide() {
this.close()
},
changeShow() {
this.isShow = !this.isShow;
this.$emit("callback", this.id);
},
//解码音频文件
decodeAudioData() {
console.log(`-------------${this.id}解码音频文件------------------`);
return new Promise((resolve, reject) => {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
fetch("test.mp3")
.then(response => response.blob())
.then(blob => {
return blob.arrayBuffer();
})
.then(arrayBuffer => {
this.audioContext.decodeAudioData(arrayBuffer, (buffer) => {
this.audioArrayBuffer = buffer
this.source = this.audioContext.createBufferSource();
this.source.buffer = buffer;
resolve()
}).catch((error) => {
console.error('Error decoding audio data:', error);
reject(error)
});
})
.catch((err) => {
console.error(err);
reject(err)
});
})
},
playAudio() {
if (this.isClick) {
return
}
this.isClick = !this.isClick
// 开始播放
if (this.isFirstPlay) {
// 创建新的源并开始播放
this.isFirstPlay = false
this.currentTime = this.begin
this.source = this.audioContext.createBufferSource();
this.source.buffer = this.audioArrayBuffer;
this.source.connect(this.audioContext.destination);
this.source.start(0, this.begin, this.end - this.begin);
this.source.onended = () => {
console.log('音频回放结束');
//重置状态为第一次播放
this.isFirstPlay = true
this.isClick = false; // 重置点击状态
};
} else if (this.audioContext.state === "suspended") {
//恢复之前暂停播放的音频
this.audioContext.resume();
}
this.syncTime()
},
//暂停播放
pause() {
if (this.source) {
// this.source.stop(); // 停止播放
if (this.audioContext.state === 'running') {
this.audioContext.suspend().then(() => {
clearInterval(this.timer)
this.isClick = false; // 重置点击状态
console.log("暂停:", this.audioContext.state);
});
}
}
},
//同步时间
syncTime() {
clearInterval(this.timer)
let times = this.currentTime - this.begin
this.timer = setInterval(() => {
times += 0.1
this.currentTime = times + this.begin
if (this.currentTime >= this.end) {
this.currentTime = this.end
clearInterval(this.timer)
}
}, 100);
},
close() {
if (this.audioContext) {
this.audioContext.close();
}
clearInterval(this.timer)
this.isClick = false;
this.isFirstPlay = true
}
},
};
</script>
<style scoped >
.audio {
width: 160px;
height: 40px;
background-color: #F1F3F4;
padding: 0 20px;
border-radius: 15px;
display: flex;
justify-content: center;
align-items: center;
}
.audio img {
width: 20px;
height: 20px;
margin-right: 10px;
}
</style>
使用组件
js
<template>
<div id="app">
<el-table :data="data" style="width: 100%">
<el-table-column prop="id" label="id" width="100" align="center">
</el-table-column>
<el-table-column label="时间" width="180" align="center">
<template slot-scope="scope">
<span>{{ scope.row.begin / 1000 }}~{{ scope.row.end / 1000 }}秒</span>
</template>
</el-table-column>
<el-table-column label="播放" min-width="180" align="right">
<template slot-scope="scope">
<Audio :url="'test.mp3'" :id="scope.row.id" :current="current" :begin="scope.row.begin / 1000"
:end="scope.row.end / 1000" @callback="changeCurrent" />
</template>
</el-table-column>
<el-table-column prop="content" label="内容" min-width="190"></el-table-column>
</el-table>
</div>
</template>
<script>
import Audio from './components/audio/index.vue'
export default {
name: 'App',
components: {
Audio
},
data() {
return {
current: 1,
data: [
{
begin: 1190,
end: 5540,
id: 1,
content: "政府对外援助项目,海岸带综合管理研修课程,"
},
{
begin: 5750,
end: 10800,
id: 2,
content: "面向全球100多个发展中国家开展培训和经验推广。"
},
{
begin: 10970,
end: 22180,
id: 3,
content: "2021年云塘湖生态修复还作为中国生态修复典型案例之一,在联合国生物多样性公约缔约方会第十五次会议相关论,"
},
{
begin: 22240,
end: 23030,
id: 4,
content: "你想发布"
},
{
begin: 23690,
end: 32600,
id: 5,
content: "如今生态环境优美的引导组,已经成为厦门高颜值生态花园城市人与自然和谐共生的典范。"
},
{
begin: 33010,
end: 38260,
id: 6,
content: "我国海洋生态系统性综合性治理是从云端口治理起步,"
},
{
begin: 38590,
end: 46130,
id: 7,
content: "经过36年历史积累和检验的厦门实践,是习近平生态文明思想孕育丰富"
}
],
}
},
methods: {
changeCurrent(id) {
this.current = id
}
}
}
</script>
解释
核心就是start方法AudioBufferSourceNode.start([when],[offset],[duration]);,接收三个参数
sql
1、when:倒计时,when后开始播放,为0则立即播放,单位毫秒
2、offset:开始播放的时间,从offset开始播放,单位毫秒
3、duration:播放持续时间,播放duration秒结束,单位毫秒
缺点是:需要频繁加载文件源,关闭上下文后需要重新解码,重新解码需要重新请求源文件,
尝试在父组件获取文件流,所有子组件用同一个文件流,发现用同一个文件流会报错。所以在触发了close后就需要重新加载文件再解码。
还可以优化,但这里已经满足了我的业务需求