小程序中实现音频播放(原生 + uniapp)

原生:

miniprogram/components/mp-audio/index.wxml

html 复制代码
<view class="imt-audio">
  <view class="top">
    <view class="audio-control-wrapper">
      <image src="{{poster}}" mode="aspectFill" class="cover"></image>
      <image src="/static/components/loading.png" wx:if="{{playState ==='loading'}}" class="play loading"></image>
      <block wx:else>
        <image src="/static/components/playbtn.png" alt="play" bind:tap="play" class="play" wx:if="{{playState === 'pause'}}"></image>
        <image src="/static/components/pausebtn.png" alt="pause" bind:tap="pause" class="play" wx:else></image>
      </block>
    </view>
  </view>
  <view class="audio-wrapper">
    <view class="titlebox">
      <view class="title text-max1">{{name}}</view>
    </view>
    <slider class="audio-slider" block-size="12" max="{{duration}}" value="{{currentTime}}" bindchange="sliderChange" block-color="#999999" bindchanging="sliderChanging"></slider>
    <view class="slidebox">
      <view>{{showCurrentTime}} / {{showDuration}}</view>
    </view>
  </view>
</view>

miniprogram/components/mp-audio/index.ts

TypeScript 复制代码
Component({
  observers: {
    currentTime(value) {
      this.setData({
        showCurrentTime: this.formatSeconds(value)
      })
    },
    duration(value) {
      this.setData({
        showDuration: this.formatSeconds(value)
      })
    }
  },
  detached(){
    this.data.audioCtx.destroy()
    this.setData({
      audioCtx: null as unknown as WechatMiniprogram.InnerAudioContext
    })
  },
  lifetimes: {
    created() {
      this.setData({
        audioCtx: wx.createInnerAudioContext({
          useWebAudioImplement: false
        })
      })
      this.data.audioCtx.onTimeUpdate((e) => {
        const currentTime = this.data.audioCtx.currentTime
        this.setData({
          currentTime,
        })
      })
      this.data.audioCtx.onPlay(() => console.log("播放"))
      this.data.audioCtx.onPause(() => console.log("暂停"))
      this.data.audioCtx.onStop(() => console.log("停止"))
      this.data.audioCtx.onEnded(() => {
        console.log("结束")
        this.setData({
          playState: 'pause',
          currentTime: 0
        })
        this.data.audioCtx.seek(0)
      })
    }
  },
  /**
   * 组件的属性列表
   */
  properties: {
    /**
     * 音频源地址
     */
    src: {
      type: String,
      value: ''
    },
    /**
     * 封面图片地址
     */
    poster: {
      type: String,
      value: ''
    },
    /**
     * 音频标题
     */
    name: {
      type: String,
      value: '标题'
    },
    /**
     * 歌手名称
     */
    singer: {
      type: String,
      value: ''
    },

  },

  /**
   * 组件的初始数据
   */
  data: {
    src: "",
    poster: "",
    name: "标题",
    singer: "",
    duration: 10,
    currentTime: 0,
    showCurrentTime: '00:00',
    showDuration: '00:00',
    playState: "pause", //"loading"/"playing"/"pause"
    isSliderChanging: false,
    audioCtx: null as unknown as WechatMiniprogram.InnerAudioContext
  },
  /**
   * 组件的方法列表
   */
  methods: {
    setSrc(value: string) {
      this.data.audioCtx.src = value
      let v1 = setInterval(() => {
        if (this.data.audioCtx.duration > 0) {
          this.setData({
            duration: this.data.audioCtx.duration
          })
          clearInterval(v1)
        }
      },500)
    },
    setPoster(value: string) {
      this.setData({ poster: value });
    },
    setName(value: string) {
      this.setData({ name: value });
    },
    setSinger(value: string) {
      this.setData({ singer: value });
    },
    playerOnPlay() {
      this.setData({
        playState: 'playing'
      })
      this.triggerEvent('play');
    },
    playerOnPause() {
      this.setData({
        playState: 'pause'
      })
      this.triggerEvent('pause');
    },
    playerOnEnded() {
      this.setData({
        playState: 'pause'
      })
      this.triggerEvent('ended');
    },
    playerOnWaiting() {
      this.setData({
        playState: 'loading'
      })
    },
    playerOnError(e: WechatMiniprogram.VideoError) {
      wx.$log.error("playerOnError音频播放失败:" + JSON.stringify(e))
      this.setData({
        playState: 'pause'
      })
      this.triggerEvent('error', e);
    },
    formatSeconds(seconds: number | string): string {
      var result = typeof seconds === "string" ? parseFloat(seconds) : seconds;
      if (isNaN(result))
        return "";
      let h = Math.floor(result / 3600) < 10 ?
        "0" + Math.floor(result / 3600) :
        Math.floor(result / 3600);
      let m = Math.floor((result / 60) % 60) < 10 ?
        "0" + Math.floor((result / 60) % 60) :
        Math.floor((result / 60) % 60) + (Number(h)) * 60;
      let s = Math.floor(result % 60) < 10 ?
        "0" + Math.floor(result % 60) :
        Math.floor(result % 60);
      return `${m}:${s}`;
    },
    stop() {
      try {
        if (this.data.audioCtx) {
          this.data.audioCtx.stop();
        }
        this.setData({ currentTime: 0 });
      } catch (error: any) {
        console.error('停止失败:', error);
        this.triggerEvent('error', error);
      }
    },
    seek(t: number) {
      if (this.data.audioCtx) {
        this.data.audioCtx.seek(t);
      }
    },
    play() {
      try {
        if (this.data.audioCtx) {
          this.data.audioCtx.play();
          this.setData({
            playState: "playing"
          })
        }
      } catch (error: any) {
        console.error('播放失败:', error);
        this.triggerEvent('error', error);
      }
    },
    pause() {
      try {
        if (this.data.audioCtx) {
          this.data.audioCtx.pause();
          this.setData({
            playState: "pause"
          })
        }
      } catch (error: any) {
        console.error('暂停失败:', error);
        this.triggerEvent('error', error);
      }
    },
    sliderChange(e: WechatMiniprogram.SliderChanging) {
      console.log("sliderChange", e)
      this.setData({
        isSliderChanging: false
      })
      //就会存在滚动条拖不动的情况
      if (this.data.audioCtx) {
        this.data.audioCtx.seek(e.detail.value);
        this.setData({
          currentTime: e.detail.value
        })
      }
    },
    sliderChanging() {
      this.setData({
        isSliderChanging: true
      })
    },
  }
})

miniprogram/components/mp-audio/index.scss

css 复制代码
@mixin textoverflow() {
	display: -webkit-box;
	overflow: hidden;
	text-overflow: ellipsis;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 1;
}
@keyframes rowup {
	0% {
		-webkit-transform: translate(-50%, -50%) rotate(0deg);
		transform-origin: center center;
	}

	100% {
		-webkit-transform: translate(-50%, -50%) rotate(360deg);
		transform-origin: center center;
	}
}
.imt-audio{
	background: #ffffff;
  box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
	width: 100%;
	overflow: hidden;
	display: flex;
	box-sizing: border-box;
	position:relative;
  padding: 12rpx;
  border-radius: 12rpx;
	.top {
		width: 140rpx;
		position: relative;
	}

	.audio-wrapper {
		display: flex;
		flex-direction: column;
    justify-content: space-between;
		flex: 1;
		color: #fff;
		margin-left: 20rpx;
    width: 100%;
    height: 100%;

		.titlebox {
			display: flex;
      color: #000000;
			.title {
				font-size: 30rpx;
				@include textoverflow;
			}

			.singer {
				margin-left: 20rpx;
				font-size: 28rpx;
				max-width: 50%;
				@include textoverflow;
			}
		}
	}
	.slidebox {
		display: flex;
		justify-content: space-between;
		width: 96%;
    color: #333;
	}
	.uni-slider-tap-area {
		padding: 0;
	}
	.uni-slider-wrapper {
		min-height: 0;
	}
	.uni-slider-handle-wrapper {
		height: 6px;
	}
	.audio-slider {
    margin: 0 !important;
	}


	.cover {
		width: 120rpx;
		height: 120rpx;
		position: absolute;
		top: 50%;
		left: 50%;
		transform: translate(-50%, -50%);
		animation-fill-mode: forwards;
		-webkit-animation-fill-mode: forwards;
	}

	.play {
		width: 80rpx;
		height: 80rpx;
		z-index: 99;
		background: rgba(0, 0, 0, 0.4);
		border-radius: 50%;
		position: absolute;
		top: 50%;
		left: 50%;
		transform: translate(-50%, -50%);
		&.loading{
			width: 60rpx;
			height: 60rpx;
			animation: rotating_theme3 2s linear infinite;
		}
	}
}

@keyframes rotating {
	0% {
		  transform: rotateZ(0deg)
	}
	100% {
		  transform: rotateZ(360deg)
	}
}
@keyframes rotating_theme3 {
	0% {
		  transform: translate(-50%, -50%) rotateZ(0deg)
	}
	100% {
		  transform: translate(-50%, -50%) rotateZ(360deg)
	}
}

.hItem
{
	margin-left: 16rpx;
}

.extrButton
{
	font-size: 36rpx;
}

使用:

css 复制代码
<mp-audio id="mpAudio" />


handleSetSrcInfo() {
    nextTick(() => {
      const child = this.selectComponent('#mpAudio');
      if (child) {
        child.setSrc(this.data.mission!.Media![0]);
        child.setName(this.data.mission.Title);
      }
    })
  },

uniapp:

components/yz-audio/yz-audio.vue

html 复制代码
<template>
  <view class="imt-audio">
    <view class="top">
      <view class="audio-control-wrapper">
        <image :src="poster" mode="aspectFill" class="cover"></image>
        <image :src="require('./static/loading.png')" v-if="playState ==='loading'" class="play loading"></image>
        <block v-else>
          <image :src="require('./static/playbtn.png')" alt="play" @click="play" class="play"
            v-if="playState === 'pause'"></image>
          <image :src="require('./static/pausebtn.png')" alt="pause" @click="pause" class="play" v-else></image>
        </block>
      </view>
    </view>
    <view class="audio-wrapper">
      <view class="titlebox">
        <view class="title text-max1">{{name}}</view>
      </view>
      <slider class="audio-slider" block-size="12" :max="duration" :value="currentTime" @change="sliderChange"
        block-color="#999999" @changing="sliderChanging"></slider>
      <view class="slidebox">
        <view>{{formatSeconds(currentTime)}} / {{formatSeconds(duration)}}</view>
      </view>
    </view>
  </view>
</template>

<script>
  export default {
    data() {
      return {
        src: "",
        poster: "",
        name: "标题",
        singer: "",
        duration: 10,
        currentTime: 0,
        playState: "pause", //"loading"/"playing"/"pause"
        isSliderChanging: false,
        audioCtx: null
      };
    },
    created() {
      this.audioCtx = uni.createInnerAudioContext()
      this.audioCtx.onTimeUpdate((e) => {
        const currentTime = this.audioCtx.currentTime
        this.currentTime = currentTime
      })
      this.audioCtx.onPlay(() => console.log("播放"))
      this.audioCtx.onPause(() => console.log("暂停"))
      this.audioCtx.onStop(() => console.log("停止"))
      this.audioCtx.onEnded(() => {
        console.log("结束")
        this.playState = 'pause',
          this.currentTime = 0
        this.audioCtx.seek(0)
      })
    },
    destroyed() {
      this.audioCtx.destroy()
      this.audioCtx = null
    },
    methods: {
      setSrc(value) {
        this.audioCtx.src = value
        let v1 = setInterval(() => {
          console.log("this.audioCtx.duration", this.audioCtx.duration)
          if (this.audioCtx.duration > 0) {
            this.duration = this.audioCtx.duration
            clearInterval(v1)
          }
        }, 500)
      },
      setPoster(value) {
        this.poster = value;
      },
      setName(value) {
        this.name = value;
      },
      play() {
        if (this.audioCtx) {
          this.audioCtx.play();
          this.playState = "playing"
        }
      },
      pause() {
        this.audioCtx.pause();
        this.playState = "pause"
      },
      sliderChange(e) {
        this.isSliderChanging = false;
        if (this.audioCtx) {
          this.audioCtx.seek(e.detail.value);
          this.currentTime = e.detail.value
        }
      },
      formatSeconds(seconds) {
        var result = typeof seconds === "string" ? parseFloat(seconds) : seconds;
        if (isNaN(result))
          return "";
        let h = Math.floor(result / 3600) < 10 ?
          "0" + Math.floor(result / 3600) :
          Math.floor(result / 3600);
        let m = Math.floor((result / 60) % 60) < 10 ?
          "0" + Math.floor((result / 60) % 60) :
          Math.floor((result / 60) % 60) + (Number(h)) * 60;
        let s = Math.floor(result % 60) < 10 ?
          "0" + Math.floor(result % 60) :
          Math.floor(result % 60);
        return `${m}:${s}`;
      },
      sliderChanging() {
        this.isSliderChanging = true;
      },
    },
  }
</script>

<style lang="scss">
  @import './index.scss';
</style>

components/yz-audio/index.scss

css 复制代码
@mixin textoverflow() {
	display: -webkit-box;
	overflow: hidden;
	text-overflow: ellipsis;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 1;
}
@keyframes rowup {
	0% {
		-webkit-transform: translate(-50%, -50%) rotate(0deg);
		transform-origin: center center;
	}

	100% {
		-webkit-transform: translate(-50%, -50%) rotate(360deg);
		transform-origin: center center;
	}
}
.imt-audio{
	background: #ffffff;
  box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
	width: 100%;
	overflow: hidden;
	display: flex;
	box-sizing: border-box;
	position:relative;
  padding: 12rpx;
  border-radius: 12rpx;
	.top {
		width: 140rpx;
		position: relative;
	}

	.audio-wrapper {
		display: flex;
		flex-direction: column;
    justify-content: space-between;
		flex: 1;
		color: #fff;
		margin-left: 20rpx;
    width: 100%;
    height: 100%;

		.titlebox {
			display: flex;
      color: #000000;
			.title {
				font-size: 30rpx;
				@include textoverflow;
			}

			.singer {
				margin-left: 20rpx;
				font-size: 28rpx;
				max-width: 50%;
				@include textoverflow;
			}
		}
	}
	.slidebox {
		display: flex;
		justify-content: space-between;
		width: 96%;
    color: #333;
	}
	.uni-slider-tap-area {
		padding: 0;
	}
	.uni-slider-wrapper {
		min-height: 0;
	}
	.uni-slider-handle-wrapper {
		height: 6px;
	}
	.audio-slider {
    margin: 0 !important;
	}


	.cover {
		width: 120rpx;
		height: 120rpx;
		position: absolute;
		top: 50%;
		left: 50%;
		transform: translate(-50%, -50%);
		animation-fill-mode: forwards;
		-webkit-animation-fill-mode: forwards;
	}

	.play {
		width: 80rpx;
		height: 80rpx;
		z-index: 99;
		background: rgba(0, 0, 0, 0.4);
		border-radius: 50%;
		position: absolute;
		top: 50%;
		left: 50%;
		transform: translate(-50%, -50%);
		&.loading{
			width: 60rpx;
			height: 60rpx;
			animation: rotating_theme3 2s linear infinite;
		}
	}
}

@keyframes rotating {
	0% {
		  transform: rotateZ(0deg)
	}
	100% {
		  transform: rotateZ(360deg)
	}
}
@keyframes rotating_theme3 {
	0% {
		  transform: translate(-50%, -50%) rotateZ(0deg)
	}
	100% {
		  transform: translate(-50%, -50%) rotateZ(360deg)
	}
}

.hItem
{
	margin-left: 16rpx;
}

.extrButton
{
	font-size: 36rpx;
}

使用:

css 复制代码
<YzAudio v-if="obj.Media && obj.Media.length" ref="player1" />


handleSetAudioSrcInfo() {
        if (!this.obj.Media) return
        this.$nextTick(() => {
          const player1 = this.$refs.player1;
          player1.setSrc(this.obj.Media[0]);
          player1.setName(this.obj.Title);
        })
      }
相关推荐
MurphyChen2 分钟前
前端请求进化史 :从 Form 到 Server Actions 🚀
前端·javascript·面试
兰德里的折磨55016 分钟前
基于若依和elementui实现文件上传(导入Excel表)
前端·elementui·excel
喝拿铁写前端19 分钟前
一个列表页面,初级中级高级前端之间的鸿沟就显出来了
前端·架构·代码规范
编程毕设1 小时前
【开题报告+论文+源码】基于SpringBoot+Vue的招聘管理系统的设计与实现
vue.js·spring boot·后端
magic 2451 小时前
ES6变量声明:let、var、const全面解析
前端·javascript·ecmascript·es6
M_chen_M2 小时前
es6学习02-let命令和const命令
前端·学习·es6
好_快2 小时前
Lodash源码阅读-dropWhile
前端·javascript·源码阅读
M_chen_M2 小时前
JS6(ES6)学习01-babel转码器
前端·学习·es6
好_快2 小时前
Lodash源码阅读-dropRightWhile
前端·javascript·源码阅读
二川bro2 小时前
Vue 项目中 package.json 文件的深度解析
前端·vue.js·json