Vue音频播放组件(毫秒级的精度控制)

经过测试,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后就需要重新加载文件再解码。

还可以优化,但这里已经满足了我的业务需求

效果

相关推荐
四喜花露水29 分钟前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
程序员爱技术5 小时前
Vue 2 + JavaScript + vue-count-to 集成案例
前端·javascript·vue.js
cs_dn_Jie10 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic10 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿10 小时前
webWorker基本用法
前端·javascript·vue.js
customer0811 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
getaxiosluo12 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v12 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
栈老师不回家13 小时前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙13 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js