动手写个音乐播放器

云开发—音乐播放器

随着版权意识的增长,越来越多的歌曲需要vip才可以听。就拿QQ音乐来说,以前pc端下载下来还是MP3格式,现在好像下载下来是qmc3格式,vip过期了也是不可以听vip歌曲。

看到网上有大佬提供了qmc3转格式的方法 自己在pc端下载了一些vip歌曲,再上传小程序的云存储。利用小程序的api就可以写出自己的播放器。

更新(老年版本2020):
视频播放地址

程序员给奶奶的礼物 嘿嘿!

先看成果图:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

准备工作:

  1. 下载歌曲,尽量mp3格式,QQ音乐的vip歌曲下载下来是qmc3格式,需要先转换为mp3,转换工具下载
  2. 新建小程序 选择云开发 在云开发中选择存储 点击上传文件
    在这里插入图片描述
  3. 新建集合 music 详细见下图
    注意:播放地址 music_path为云存储的下载地址 (这里我选择的背景音频的api 支持云地址)
    在这里插入图片描述
    在这里插入图片描述
    JS部分:
    1、获取所有歌曲的记录
    云开发提供的数据获取api,和大多数数据库一样,一次只可以获取最多20条记录,但是歌曲的数量肯定不止20条,那么怎么获取完呢?
    这里我想了两种方法:
    一种是先获取20条,像QQ动态一样,上滑到屏幕底部,利用onreachdown,再次获取下20条记录,依次类推。后来仔细一想,QQ音乐一下就可以把列表加载完全,没有多次加载。
    第二种是在小程序onload的时候,获取完所有的记录。
    这里我用的利用for循环,先获取到所有记录的总数,再除以20,就可以计算出需要循环几次。比如数据库有70条记录,一次我们获取20条,那么我们就需要获取70/20=4次。
    这里还有一个坑,小程序js中的for循环,和c++的for有点区别,有很多次我都栽在这里。
    举个例子:
    for(i=0;i<5;i++)
    在c++中
    i的值是固定的:0 1 2 3 4
    但是在小程序js中
    i的值确是随机的 比如:1 2 4 0 3
    这样在有些特定场景判断循环终止就会出现一些小错误。
    代码:
 var k = this
    var x = 0
    this.setData({
      loadModal: true,
      x:app.flag
    })
   
    var total = app.music_num
    const batchTimes = Math.ceil(total / 20)
    console.log(batchTimes)
    var arraypro = []
    //初次循环获取云端数据库的分次数的promise数组
    for (let i = 0; i < batchTimes; i++) {

      console.log(i)
      db.collection('music').skip(i * 20).get({
        success: function (res) {
          x++
          console.log(res.data)
          for (let j = 0; j < res.data.length; j++) {
            arraypro.push(res.data[j])
          }
          console.log(arraypro)
          console.log(x)
          if (x == batchTimes) {     //利用x == batchTimes作为循环完成判定
            app.music_flag=1
            k.setData({
              songs: arraypro,
              song: arraypro[x],
              currentIndex: x,
              song_index: x,
              loadModal: false

            })
          }
        }
      })

    }

2、上一曲、下一曲、暂停
在这个程序中,当小程序加载完成的时候,就将所有歌曲的信息存在了songs数组中了。
所以实现上面三个功能,只需要知道现在播放歌曲的index值,在加减就行了。这里就不贴代码了,最后有完整代码。
3.自动切换歌曲
比如一首歌播放完成后,肯定是需要继续播放的啊。
这里我把监听事件放在onshow中,一旦播放事件停止,index一次顺延即可。
代码:

    var k = this

    wx.onBackgroundAudioStop((res) => {
      ++k.data.song_index
      k.setData({
        song: k.data.songs[k.data.song_index],
        song_index: k.data.song_index,
        currentIndex: k.data.song_index,
      })
      wx.playBackgroundAudio({
        dataUrl: k.data.songs[k.data.song_index].music_path,
        title: k.data.songs[k.data.song_index].music_name,
        coverImgUrl: k.data.songs[k.data.song_index].singer_img
      })
    })

已知BUG:
1、现在就是在播放的时候,切出界面,再切换回去,因为会重新调用onload,所以不会回到播放当前歌曲的状态,歌还是有的,就是界面反应不过来。
举个例子:比如现在播放稻香 放着的时候,我切出去了 再回来,播放的仍然是稻香,但是界面显示的歌曲名字 信息确是重新加载后的
2.现在还只实现的顺序播放,还不支持随机播放、单曲循环。以后慢慢写
哈哈

完整代码:
1.前端wxml 这里我用的大佬的UI(自己写不出)

  <view class="player" v-show="playlist.length>0">
    <view class="normal-player" wx:if="fullScreen">
      <view class="background">
      </view>
      <view class="top">
        <view class="title">{{song.music_name || '暂无正在播放歌曲'}}</view>
        <view class="subtitle">{{song.singer_name}}</view>
      </view>
      <swiper class="middle" style="height: 700rpx" bindchange="changeDot">
        <swiper-item class="middle-l" style="overflow: visible">
          <view class="cd-wrapper" ref="cdWrapper">
            <view class="cd {{cdCls}}">
              <image src="{{song.singer_img}}" alt="" class="image"/>
            </view>
          </view>
          <view class="currentLyricWrapper">{{currentText}}</view>
        </swiper-item>
        <swiper-item class="middle-r">
          <scroll-view class="lyric-wrapper" scroll-y scroll-into-view="line{{toLineNum}}" scroll-with-animation>
            <view v-if="currentLyric">
              <view ref="lyricLine"
                    id="line{{index}}"
                    class="text {{currentLineNum == index ? 'current': '' }}"
                    wx:for="{{currentLyric.lines}}">{{item.txt}}
              </view>
            </view>
            <view wx:if="{{!currentLyric}}">
              <view class="text current">暂无歌词</view>
            </view>
          </scroll-view>
        </swiper-item>
      </swiper>
      <view class="dots-wrapper">
        <view class="dots {{currentDot==index?'current':''}}" wx:for="{{dotsArray}}"></view>
      </view>
      <view class="bottom">
        <view class="progress-wrapper">
          <text class="time time-l">{{currentTime}}</text>
          <view class="progress-bar-wrapper">
            <progress-bar percent="{{percent}}"></progress-bar>
          </view>
          <text class="time time-r">{{duration}}</text>
        </view>
        <view class="operators">
          <view class="icon i-left">
            <i bindtap="changeMod"
               class="{{playMod==1? 'icon-sequence':''}}{{playMod==2? ' icon-random':''}}{{playMod==3?' icon-loop':''}}"></i>
          </view>
          <view class="icon i-left">
            <i class="icon-prev" bindtap="prev"></i>
          </view>
          <view class="icon i-center" bindtap="togglePlayings">
            <i class="{{playIcon}}" ></i>
          </view>
          <view class="icon i-right">
            <i class="icon-next" bindtap="next"></i>
          </view>
          <view class="icon i-right" bindtap="openList">
            <i class="icon-playlist"></i>
          </view>
        </view>
      </view>
    </view>
    <view class="content-wrapper {{translateCls}}">
      <view class="close-list"  bindtap="close"></view>
      <view class="play-content">
        <view class="plyer-list-title">播放队列({{songs.length}}首)</view>
        <scroll-view class="playlist-wrapper" scroll-y scroll-into-view="list{{currentIndex}}">
          <view class="item {{index==currentIndex ? 'playing':''}}" wx:for="{{songs}}" id="{{index}}"
                data-index="{{songs.music_path}}" bindtap="playthis" wx:key="{{index}}">
            <view class="name" style='text-white'>{{item.music_name}}</view>
            <view class="play_list__line">-</view>
            <view class="singer">{{item.singer_name}}</view>
            
          </view>
        </scroll-view>
        <view class="close-playlist" bindtap="close">关闭</view>
      </view>
    </view>
  </view>


<view class='cu-load load-modal' wx:if="{{loadModal}}">
  <!-- <view class='cuIcon-emojifill text-orange'></view> -->
  <image src='/images/T1.jpg' class='png' mode='aspectFit'></image>
  <view class='text-red'>加载中...</view>
</view>
  1. js
const app = getApp().globalData
const song = require('../../../utils/song.js')
const Lyric = require('../../../utils/lyric.js')
const util = require('../../../utils/util.js')
const db = wx.cloud.database()

const innerAudioContext = wx.createInnerAudioContext()

const SEQUENCE_MODE = 1
const RANDOM_MOD = 2
const SINGLE_CYCLE_MOD = 3

Page({
  data: {
    song_index: 0,//当前播放音乐的index 0 1 2
    songs_length: '',// 播放曲目的总数
    songs: [],//曲目总数
    song: [],//第一曲
    flag: 1,//判断暂停还是继续
    currentIndex: 0,
    num: 0,
    all_nums: "",
    nums: app.music_num,
    x:"",
    playurl: '',
    playIcon: 'icon-play',
    cdCls: 'true',
    currentLyric: null,
    currentLineNum: 0,
    toLineNum: -1,
    currentSong: null,
    dotsArray: new Array(2),
    currentDot: 0,
    playMod: SEQUENCE_MODE
  },
  onShow: function () {
    var k = this

    wx.onBackgroundAudioStop((res) => {
      ++k.data.song_index
      k.setData({
        song: k.data.songs[k.data.song_index],
        song_index: k.data.song_index,
        currentIndex: k.data.song_index,
      })
      wx.playBackgroundAudio({
        dataUrl: k.data.songs[k.data.song_index].music_path,
        title: k.data.songs[k.data.song_index].music_name,
        coverImgUrl: k.data.songs[k.data.song_index].singer_img
      })
    })
  },
  onLoad: function () {
   
    var k = this
    //this._init()
    var x = 0
    this.setData({
      loadModal: true,
      x:app.flag
    })
   
    var total = app.music_num
    const batchTimes = Math.ceil(total / 20)
    console.log(batchTimes)
    var arraypro = []
    //初次循环获取云端数据库的分次数的promise数组
    for (let i = 0; i < batchTimes; i++) {

      console.log(i)
      db.collection('music').skip(i * 20).get({
        success: function (res) {
          x++
          console.log(res.data)
          for (let j = 0; j < res.data.length; j++) {
            arraypro.push(res.data[j])
          }
          console.log(arraypro)
          console.log(x)
          if (x == batchTimes) {
            app.music_flag=1
            k.setData({
              songs: arraypro,
              song: arraypro[x],
              currentIndex: x,
              song_index: x,
              loadModal: false

            })
          }
        }
      })

    }


   
   
  },


  prev: function () {

    var k = this
    if (k.data.song_index == 0) {
      wx.showToast({
        title: '已经是第一首了哦!',
        icon: 'none'
      })
    }
    else {
      k.data.song_index--
      k.setData({
        song: k.data.songs[k.data.song_index],
        playIcon: 'icon-pause',
        currentIndex: k.data.song_index,
        flag: 2
      })
      console.log(k.data.song_index)
      wx.playBackgroundAudio({
        dataUrl: k.data.songs[k.data.song_index].music_path,
        title: k.data.songs[k.data.song_index].music_name,
        coverImgUrl: k.data.songs[k.data.song_index].singer_img
      })
    }
  },
  next: function () {
    var k = this
    if (k.data.song_index == app.music_num - 1) {
      wx.showToast({
        title: '已经是最后一首了哦!',
        icon: "none"
      })
    }
    else {
      k.data.song_index++
      console.log(k.data.song_index)
      wx.playBackgroundAudio({
        dataUrl: k.data.songs[k.data.song_index].music_path,
        title: k.data.songs[k.data.song_index].music_name,
        coverImgUrl: k.data.songs[k.data.song_index].singer_img
      })
      k.setData({
        song: k.data.songs[k.data.song_index],
        playIcon: 'icon-pause',
        currentIndex: k.data.song_index,
        flag: 2

      })
    }


  },
  /**
   * 获取不同播放模式下的下一曲索引
   * @param nextFlag: next or prev
   * @returns currentIndex
   */
  togglePlayings: function (e) {
    var k = this

    if (k.data.flag == 1) {
      wx.playBackgroundAudio({
        dataUrl: k.data.songs[k.data.song_index].music_path,
        title: k.data.songs[k.data.song_index].music_name,
        coverImgUrl: k.data.songs[k.data.song_index].singer_img
      })
      //innerAudioContext.autoplay = true
      //innerAudioContext.src = this.data.song.music_path
      //innerAudioContext.src ="cloud://bwtx-5ea6eb.6277-bwtx-5ea6eb/zjl-tuihou.mp3"
      k.setData({
        playIcon: 'icon-pause',
        flag: 2
      })
    }
    else if (k.data.flag == 2) {
      //innerAudioContext.pause()
      wx.pauseBackgroundAudio({
        title: k.data.songs[k.data.song_index].music_name,
        coverImgUrl: k.data.songs[k.data.song_index].singer_img

      })
      k.setData({
        flag: 3,
        playIcon: "icon-play"
      })

    }
    else {
      wx.playBackgroundAudio({
        title: k.data.songs[k.data.song_index].music_name,
        coverImgUrl: k.data.songs[k.data.song_index].singer_img

      })
      k.setData({
        flag: 2,
        playIcon: "icon-pause"
      })
    }
  },
  openList: function () {

    this.setData({
      translateCls: 'uptranslate'
    })
  },
  close: function () {
    this.setData({
      translateCls: 'downtranslate'
    })
  },
  playthis: function (e) {
    console.log(e.currentTarget.id)
    var k = this
    k.setData({
      playIcon: 'icon-pause',

      song_index: e.currentTarget.id,
      song: k.data.songs[e.currentTarget.id],
      currentIndex: e.currentTarget.id
    })
    wx.playBackgroundAudio({
      dataUrl: k.data.songs[k.data.song_index].music_path,
      title: k.data.songs[k.data.song_index].music_name,
      coverImgUrl: k.data.songs[k.data.song_index].singer_img
    })
    this.setData({
      translateCls: 'downtranslate'
    })
  },
  changeDot: function (e) {
    this.setData({
      currentDot: e.detail.current
    })
  }
})

源码获取

需要源码的小伙伴
可以在海轰的微信公众号:海轰Pro
回复:海轰
自提源码

发布了155 篇原创文章 · 获赞 110 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/weixin_44225182/article/details/104219782