云开发—音乐播放器
随着版权意识的增长,越来越多的歌曲需要vip才可以听。就拿QQ音乐来说,以前pc端下载下来还是MP3格式,现在好像下载下来是qmc3格式,vip过期了也是不可以听vip歌曲。
看到网上有大佬提供了qmc3转格式的方法 自己在pc端下载了一些vip歌曲,再上传小程序的云存储。利用小程序的api就可以写出自己的播放器。
更新(老年版本2020):
视频播放地址
程序员给奶奶的礼物 嘿嘿!
先看成果图:
准备工作:
- 下载歌曲,尽量mp3格式,QQ音乐的vip歌曲下载下来是qmc3格式,需要先转换为mp3,转换工具下载
- 新建小程序 选择云开发 在云开发中选择存储 点击上传文件
- 新建集合 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>
- 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
回复:海轰
自提源码
更多推荐
动手写个音乐播放器
发布评论