uni-app中实现音乐播放器

uni-app中实现音乐播放器

1、主要利用的是uni-app中提供的uni.createInnerAudioContext()来进行实现;

2、代码示例

(1)主页面代码展示

<template>
  <view class="music-layout">
    <view class="tn-flex">
      <view class="left-content">
        <view class="left-pic-layout">
          <img :src="bgUrl" :class="isPlay ? 'img-rotate' : ''">
          <view class="small-circle"></view>
        </view>
        <view class="like-layout">
          <text class="tn-icon-like-fill" v-if="musicDetail.liked" @click="onLikeMusic(false, musicDetail)"></text>
          <text class="tn-icon-like" v-else @click="onLikeMusic(true, musicDetail)"></text>
        </view>
      </view>
      <view class="right-content">
        <view class="song-name-layout">
          <view>{{ musicDetail.bsmb002 }}</view>
          <view v-if="isPlay"><PlayerAnimation></PlayerAnimation></view>
        </view>
        <view class="progress-layout">
          <text>{{ formatTime(musicCurrentTime) }}</text>
          <tn-slider
              :min="0"
              :max="musicTotalTime"
              class="progress"
              v-model="musicCurrentTime"
              inactiveColor="#EAEAEA"
              activeColor="#FF3370"
              :blockWidth="1"
              :lineHeight="4">
          </tn-slider>
          <text>{{formatTime(musicTotalTime)}}</text>
        </view>
        <view class="actions-layout">
          <view class="toggle-type-layout" @click="handleToggleType">
            <img src="http://192.168.101.152:8848/commonStatic/ckzs/fetusMovement/loop-black-icon.png" v-if="toggleType == 1" >
            <img src="http://192.168.101.152:8848/commonStatic/ckzs/fetusMovement/single-black-icon.png" v-else-if="toggleType == 2">
            <img src="http://192.168.101.152:8848/commonStatic/ckzs/fetusMovement/random-black-icon.png" v-else>
          </view>
          <view class="action-center">
            <img src="http://192.168.101.152:8848/commonStatic/ckzs/fetusMovement/previous-icon.png" class="previous-icon" @click="handlePreviousPlay">
            <img src="http://192.168.101.152:8848/commonStatic/ckzs/fetusMovement/play-icon.png" v-if="isPlay" class="player-icon" @click="handlePlay(false)">
            <img src="http://192.168.101.152:8848/commonStatic/ckzs/fetusMovement/stop-icon.png" v-else class="player-icon" @click="handlePlay(true)">
            <img src="http://192.168.101.152:8848/commonStatic/ckzs/fetusMovement/next-icon.png" class="next-icon" @click="handleNextPlay">
          </view>
          <img src="http://192.168.101.152:8848/commonStatic/ckzs/fetusMovement/music-list.png" class="music-list-icon" @click="handleMusicListShow">
        </view>
      </view>
    </view>
    <view class="add-music" @click="handleAddWisk">
      点这里添加您的音乐心愿单~
    </view>
  </view>
</template>

<script>

import { formatTime } from '@/utils/util'; // 代码在下方对应文件中
import PlayerAnimation from "../../../components/player-animation/player-animation.vue"; // 播放效果动画(代码在下方对应文件中)
import { musicList, saveCollect, cancleCollect } from "@/api/fetusMovement"; // 接口
import { mediaUrl } from '@/utils/env'; // 静态资源地址前缀

export default {
  components: {
    PlayerAnimation
  },
  data() {
    return {
      isPlay: false, // 是否播放
      toggleType: 1, // 播放顺序 (1:循环;2:单曲循环;3:随机)
      musicDetail : { }, // 音乐详情
      current: null, // 当前播放的是哪首
      currentSrc: '', // 当前音乐路径
      musicPlayCtx: null, // 音频上下文
      musicCurrentTime: 0, // 音乐当前播放进度
      musicTotalTime: 100, // 音乐总的时间
      musicList: [], // 音乐列表
      bgUrl: 'http://192.168.101.152:8848/commonStatic/ckzs/fetusMovement/tjyy_bg.png',
      timers: null,
      openId: ''
    }
  },
  watch: {
    currentSrc: {
      handler() {
        if(this.musicPlayCtx) {
          this.musicPlayCtx.destroy();
        }
        this.musicPlayCtx = uni.createInnerAudioContext();
        this.musicPlayCtx.obeyMuteSwitch = false;
        this.musicCurrentTime = 0;
        this.musicPlayCtx.src = encodeURI(mediaUrl+ this.musicDetail.bsmb007);
        this.musicTotalTime = this.musicDetail.bsmb009;
        if(this.isPlay){
          this.handleAudioPlay();
        }
      },
      immediate: true,
      deep: true,
    }
  },
  mounted() {
    this.openId = getApp().globalData.openId;
    this.getMusicList();
  },
  methods: {
    // 打开音乐列表
    getMusicList() {
      musicList(this.openId).then(res => {
        if (res.success) {
          this.musicList = res.result;
          this.current = 0;
          this.musicDetail = this.musicList[this.current];
          this.currentSrc = encodeURI(mediaUrl+ this.musicDetail.bsmb007);
        }
      })
    },
    // 播放列表弹窗显示
    handleMusicListShow() {
      const { isPlay, toggleType, musicList, current} = this;
      this.$emit('handleMusicListShow', {
        bool: true,
        isPlay,
        toggleType,
        musicList,
        current
      });
    },
    formatTime,
    // 处理播放
    handlePlay(bool) {
      this.isPlay = bool;
      if(bool) {
        this.musicPlayCtx.seek(this.musicCurrentTime + 1);
        this.handleAudioPlay();
      } else {
        this.musicPlayCtx.stop();
      }
    },
    // 播放
    handleAudioPlay() {
      this.musicPlayCtx.play();
      if(this.timers) {
        clearTimeout(this.timers);
      }
      this.timers = setTimeout(() => {
        console.log(this.musicPlayCtx.paused);
      }, 200);
      // 播放完成
      this.musicPlayCtx.onEnded(() => {
        if(this.toggleType == 1) {
          this.handleNextPlay(); // 列表循环
        } else if(this.toggleType == 2) {
          this.musicPlayCtx.play(); // 单曲循环
        } else {
          this.current = Math.floor(Math.random() * this.musicList.length);
          this.musicDetail = this.musicList[this.current];
          this.currentSrc = encodeURI(mediaUrl+ this.musicDetail.bsmb007);
        }
      })
      // 更新播放时间
      this.musicPlayCtx.onTimeUpdate(() => {
        this.onTimeUpdate();
      })
      // 播放出现错误
      this.musicPlayCtx.onError(() => {
        this.onError();
      })
    },
    // 下一首
    handleNextPlay() {
      this.isPlay = true;
      if(this.current < this.musicList.length - 1) {
        uni.showToast({
          title: '即将播放下一首',
          icon: 'none'
        })
        this.current ++;
        this.musicDetail = this.musicList[this.current];
        this.currentSrc = encodeURI(mediaUrl+ this.musicDetail.bsmb007);
        return
      }
      uni.showToast({
        title: '已经为最后一首了',
        icon: 'none'
      })
    },
    // 上一首
    handlePreviousPlay() {
      this.isPlay = true;
      if(this.current) {
        this.current --;
        uni.showToast({
          title: '即将播放上一首',
          icon: 'none'
        })
        this.musicDetail = this.musicList[this.current];
        this.currentSrc = encodeURI(mediaUrl+ this.musicDetail.bsmb007);
        return;
      }
      uni.showToast({
        title: '已经为第一首了',
        icon: 'none'
      })
    },
    // 播放类型
    handleToggleType(parmas) {
      if(parmas) {
        this.toggleType = parmas;
        const { isPlay, toggleType, musicList, current} = this;
        this.$emit('handleMusicListShow', {
          bool: true,
          isPlay,
          toggleType,
          musicList,
          current
        });
        return;
      }
      this.toggleType = this.toggleType == 1 ? 2 : this.toggleType == 2 ? 3 : 1;
    },
    // 切换音乐
    handleMusicChange(index) {
      this.isPlay = true;
      this.musicDetail = this.musicList[index];
      this.current = index;
      this.currentSrc = encodeURI(mediaUrl+ this.musicDetail.bsmb007);
      this.handleMusicListShow();
    },
    // 加载失败
    onError() {
      uni.showToast({
        title: '音频加载失败',
        icon: 'none'
      })
    },
    // 时间更新
    onTimeUpdate() {
      if (this.musicPlayCtx.currentTime > 0 && this.musicPlayCtx.currentTime <= 1) {
        this.musicCurrentTime = 1;
      } else if (this.musicCurrentTime !== Math.floor(this.musicPlayCtx.currentTime)) {
        this.musicCurrentTime = Math.floor(this.musicPlayCtx.currentTime);
      }
    },
    // 喜欢
    onLikeMusic(bool, item) {
      this.musicDetail.liked = bool;
      if (bool) {
        //收藏
        saveCollect({
          bsmId: item.id,
          openId: this.openId
        }).then(res => {
          if (res.success) {
            uni.showToast({
              title: '收藏成功',
              icon: 'none'
            })
            this.getMusicList()
          }
        })
      } else {
        //取消收藏
        cancleCollect({
          bsmId: item.id,
          openId: this.openId,
          id: item.collectId
        }).then(res => {
          if (res.success) {
            uni.showToast({
              title: '已取消收藏',
              icon: 'none'
            })
            this.getMusicList()
          }
        })
      }
    },
    // 心愿歌单
    handleAddWisk() {
      uni.navigateTo({
        url: '/toolsPages/fetusMovement/addWish'
      })
    }
  },
  // 销毁
  destroyed() {
    if(this.musicPlayCtx) this.musicPlayCtx.destroy();
    if(this.timers) clearTimeout(this.timers);
  }
}
</script>

<style scoped lang="scss">

.music-layout {
  position: relative;
  width: 690rpx;
  padding: 20rpx 30rpx;
  background: #FFFFFF;
  border-radius: 30rpx;
  backdrop-filter: blur(16px);
}

.left-content {
  width: 130rpx;
  .like-layout {
    margin-top: 20rpx;
    > text {
      color: #FF3370;
      font-size: 40rpx;
    }
  }
}

.left-pic-layout {
  position: relative;
  width: 100rpx;
  height: 100rpx;
  border-radius: 50%;
  overflow: hidden;
  img {
    width: 100%;
    height: 100%;
    border-radius: 50%;
  }
  .small-circle{
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 20rpx;
    height: 20rpx;
    background-color: white;
    border-radius: 50%;
  }
}

.right-content {
  flex: 1;
  margin-left: 30rpx;
}

.song-name-layout {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 32rpx;
  color: #323A59;
  line-height: 40rpx;
  height: 40rpx;
  text {
    color: rgba(115, 121, 141, 1);
  }
}

.progress-layout {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 25rpx 0 0 2rpx;
  font-size: 26rpx;
  color: #73798D;
  line-height: 36rpx;
  .progress {
    display: flex;
    align-items: center;
    flex: 1;
    margin: 0 20rpx;
  }
}

.actions-layout {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 25rpx 0 0 4rpx;
  .toggle-type-layout {
    > img {
      width: 30rpx;
      height: 26rpx;
    }
  }

  .action-center {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex: 1;
    margin: 0 86rpx;
  }

  .previous-icon, .next-icon {
    width: 30rpx;
    height: 32rpx;
  }

  .player-icon {
    width: 42rpx;
    height: 42rpx;
  }

  .music-list-icon {
    width: 30rpx;
    height: 30rpx;
  }
}

.img-rotate {
  transform-origin: center center;
  animation: rotate 5s infinite linear; /* 实现旋转动画效果 */
}

@keyframes rotate {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

.add-music {
  margin-top: 20rpx;
  text-align: center;
  font-size: 20rpx;
  color: #999999;
  text-decoration: underline #999999;
}
</style>

(2)utils中util.js文件代码

function fixedZero(val) {
    return val * 1 < 10 ? `0${val}` : val;
}

const formatTime = (time) =>  {
    const minutes = 60;
    const m = Math.floor(time / minutes);
    const s = Math.floor(time % 60);
    return `${fixedZero(m)}:${fixedZero(s)}`;
}

(3)components中player-animation文件夹player-animation.vue文件代码

<template>
    <view class="loading">
      <view class="item"></view>
      <view class="item"></view>
      <view class="item"></view>
      <view class="item"></view>
      <view class="item"></view>
    </view>
</template>

<script>
export default {
  name: "player-animation"
}
</script>

<style scoped>
/* 设置位置 */
.loading {
  height: 24rpx;
  display: flex;
  align-items: center;
}

.item {
  height: 24rpx;
  width: 2rpx;
  background: #FF3370;
  margin: 0 3rpx;
  border-radius: 10rpx;
  animation: loading 2s infinite;
}

/* 设置动画 */
@keyframes loading {
  0% {
    height: 0;
  }

  50% {
    height: 24rpx;
  }

  100% {
    height: 0;
  }
}

/* 为每一个竖条设置延时 */
.item:nth-child(2) {
  animation-delay: 0.2s;
}

.item:nth-child(3) {
  animation-delay: 0.4s;
}

.item:nth-child(4) {
  animation-delay: 0.6s;
}

.item:nth-child(5) {
  animation-delay: 0.8s;
}

</style>

(4)播放器列表弹窗代码

<template>
  <tn-popup v-model="show"
            safeAreaInsetBottom
            mode="bottom"
            height="1200rpx"
            @close="handleClose">
    <view class="music-container">
      <view class="header-layout">
        <view class="header-left">
          胎教音乐列表<text>({{ musicLst.length }}首)</text>
        </view>
        <view class="header-right" @click="() => handleToggle()">
          <img src="http://192.168.101.152:8848/commonStatic/ckzs/fetusMovement/loop-icon.png" v-if="toggleType == 1" >
          <img src="http://192.168.101.152:8848/commonStatic/ckzs/fetusMovement/single-icon.png" v-else-if="toggleType == 2">
          <img src="http://192.168.101.152:8848/commonStatic/ckzs/fetusMovement/random-icon.png" v-else>
          顺序播放
        </view>
      </view>
      <scroll-view scroll-y style="height: 1050rpx;">
        <view class="music-list">
          <view :class="['music-item', current  == index && isPlay ? 'active' : '']"
                v-for="(item, index) in musicLst"
                :key="index"
                @click="handleItemClick(index)">
            <view class="name">{{item.bsmb002}}</view>
            <PlayerAnimation v-if="current == index && isPlay"></PlayerAnimation>
          </view>
        </view>
      </scroll-view>
    </view>
  </tn-popup>

</template>

<script>
import PlayerAnimation from "../../../components/player-animation/player-animation.vue";
export default {
  props: {
    musicLst : {
      type: Array,
      default: []
    },
    current: {
      type: Number,
      default: 0
    },
    toggleType:{
      type:Number,
      default: 1
    },
    isPlay:{
      type:Boolean,
      default: false
    }
  },
  components: {
    PlayerAnimation
  },
  data() {
    return {
      show: false
    }
  },
  methods: {
    // 切换歌曲
    handleItemClick(index) {
      this.$emit('handleMusicChange', index);
    },
    // 关闭
    handleClose() {
      this.show = false;
    },
    // 播放类型切换
    handleToggle() {
      const playType = this.toggleType == 1 ? 2 : this.toggleType == 2 ? 3 : 1;
      this.$emit('handleToggleType', playType);
    },
  }
}
</script>

<style scoped lang="scss">
.music-container {
  padding: 0 30rpx 0;
  .header-layout {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 10rpx;
    height: 90rpx;
  }
  .header-left {
    display: flex;
    align-items: center;
    font-size: 32rpx;
    font-weight: 500;
    color: #333333;
    line-height: 44rpx;
    text {
      font-size: 24rpx;
      font-weight: 400;
      color: #666666;
      line-height: 34rpx;
    }
  }
  .header-right {
    display: flex;
    align-items: center;
    height: 56rpx;
    background: linear-gradient(135deg, #FF74A2 0%, #FF3370 100%);
    border-radius: 28rpx;
    padding: 0 12rpx;
    font-size: 26rpx;
    color: white;
    img {
      margin-right: 10rpx;
      width: 26rpx;
      height: 26rpx;
    }
  }
}

.music-list {
  font-size: 28rpx;
  color: #333333;
  line-height: 40rpx;

  .music-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 20rpx 0;
    font-size: 28rpx;
    color: #333333;
    line-height: 40rpx;
  }
}

.active {
  color: #FF3370;
}

</style>

3、实现也面向效果展示

(1)播放器
在这里插入图片描述
(2)播放器弹窗
在这里插入图片描述
4、实现该功能过程所遇到的问题总结

(1)当一首歌曲播完之后,进度条不更新问题,在网上查看到的方法都是说:该问题是存在的一个bug,解决的方案是我们再代码中要主动的调用paused()方法

this.timers = setTimeout(() => {
    console.log(this.musicPlayCtx.paused);
}, 200);

(2)当点击暂停播放后,音乐从头播放的问题,解决方案:利用seek()方法使其跳转到指定位置

 this.musicPlayCtx.seek(this.musicCurrentTime + 1);

(3)音乐播放器的地址,含有中文名称,在模拟器上面可以正常播放,手机上面不可以,解决方案:需要将该地址进行编码,编译成编译器可以识别地址

this.currentSrc = encodeURI(mediaUrl+ this.musicDetail.bsmb007);

5、以上代码可以直接粘贴复制到项目即可使用

  • 12
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值