只是一个常规的播放组件,需要考虑微信,微博这类环境的播放
微信和微博,若没有用其官方的js-sdk
初始化,没法播放。
我的文章从来都不推崇copy
,仅供参考学习..具体业务具体分析定制才是最合理的
vue && vuex
ES5+
Nuxt
的基本用法这篇文章的内容需基于上篇内容的,要用到一些设备信息
这是当前服务端版本的效果,因为还没上线,LOGO
已经马赛克
之前老的客户端实现思路
vue.prototype
上beforeMount
创建script
标签,引入对应js
,然后用promise
拿到成功加入head
的状态vuex
来维护播放状态服务端的思路也差不多
考虑的东西多些,在之前客户端实现的基础上加以完善
用中间件这些来动态注入js-sdk
全部耦合到组件内,虽然可以正常播放(包括微信和微博) 且不是单例模式,对于多音频页面,有毒
<template>
<div class="play-voice-area">
<div class="cover-player">
<div
:class="playState?'active':''"
class="cover-pic">
<img :src="coverUrl ? coverUrl:defaultAvatar">
<i
:class="playState? 'sx-mobile-icon-':'sx-mobile-bofang'"
class="sx-mobile cover-icon"
@click="playAudio" />
</div>
</div>
<div class="sound-desrc">
<p class="username">{{ userName }}的声兮</p>
<p class="timeline">{{ currentPlayTime }}/{{ voiceTime }}</p>
</div>
</div>
</template>
<script>
export default {
props: {
userName: {
type: String,
default: 'Super Hero'
},
duration: {
type: [String, Number],
default: ''
},
autoplay: {
type: [Boolean, String],
default: false
},
sourceUrl: {
type: String,
default: ''
},
coverpic: {
type: String,
default: ''
}
},
data() {
return {
defaultAvatar: require('@/assets/share/yourAppIcon@2x.png'), // 默认头像
audioElm: '', // 音频播放器 DOM
soundCurrentStopTime: '', // 当前声音暂停的时间戳
playState: false, // 播放状态的图标控制
timeStepState: '', // 时间迭代
voicePlayMessage: '', // 音频资源的状况
currentPlayTime: '00:00', // 当前播放的时间,默认为0
cacheCurrentTime: 0 // 缓存播放时间
};
},
computed: {
coverUrl() {
if (!this.coverpic) {
return this.defaultAvatar;
}
return this.coverpic;
},
voiceTime() {
if (this.duration) {
return this.second2time(Number(this.duration));
}
}
},
watch: {
sourceUrl(newVal, oldVal) {
if (newVal) {
this.playAudio();
}
}
},
created() {
this.$store.commit('OPEN_LOADING');
},
beforeMount() {
// 初始化音频播放器
this.initAudioElm();
},
mounted() {
// 检测微博微信平台
this.checkWeiBo_WeiChat();
this.audioElm.addEventListener('stalled', this.stalled);
this.audioElm.addEventListener('loadstart', this.loadstart);
this.audioElm.addEventListener('loadeddata', this.loadeddata);
this.audioElm.addEventListener('canplay', this.canplay);
this.audioElm.addEventListener('ended', this.ended);
this.audioElm.addEventListener('pause', this.pause);
this.audioElm.addEventListener('timeupdate', this.timeupdate);
this.audioElm.addEventListener('error', this.error);
this.audioElm.addEventListener('abort', this.abort);
},
beforeDestroy() {
this.audioElm.removeEventListener('loadstart', this.loadstart);
this.audioElm.removeEventListener('stalled', this.stalled);
this.audioElm.removeEventListener('canplay', this.canplay);
this.audioElm.removeEventListener('timeupdate', this.timeupdate);
this.audioElm.removeEventListener('pause', this.pause);
this.audioElm.removeEventListener('error', this.error);
this.audioElm.removeEventListener('ended', this.ended);
},
methods: {
initAudioElm() {
let audio = new Audio();
audio.autobuffer = true; // 自动缓存
audio.preload = 'metadata';
audio.src = this.sourceUrl;
audio.load();
this.audioElm = audio;
},
checkWeiBo_WeiChat() {
let ua = navigator.userAgent.toLowerCase(); // 获取判断用的对象
const script = document.createElement('script');
if (/micromessenger/.test(ua)) {
// 返回一个独立的promise
script.src = 'https://res.wx.qq.com/open/js/jweixin-1.2.0.js';
new Promise((resolve, reject) => {
let done = false;
script.onload = script.onreadystatechange = () => {
if (
!done &&
(!script.readyState ||
script.readyState === 'loaded' ||
script.readyState === 'complete')
) {
done = true;
// 避免内存泄漏
script.onload = script.onreadystatechange = null;
resolve(script);
}
};
script.onerror = reject;
document
.getElementsByTagName('head')[0]
.appendChild(script);
}).then(res => {
this.initWeixinSource();
});
}
if (/WeiBo|weibo/i.test(ua)) {
script.src =
'https://tjs.sjs.sinajs.cn/open/thirdpart/js/jsapi/mobile.js';
new Promise((resolve, reject) => {
let done = false;
script.onload = script.onreadystatechange = () => {
if (
!done &&
(!script.readyState ||
script.readyState === 'loaded' ||
script.readyState === 'complete')
) {
done = true;
// 避免内存泄漏
script.onload = script.onreadystatechange = null;
resolve(script);
}
};
script.onerror = reject;
document
.getElementsByTagName('head')[0]
.appendChild(script);
}).then(res => {
this.initWeiboSource();
});
}
},
canplay() {
this.$store.commit('CLOSE_LOADING');
},
initWeixinSource() {
wx.config({
// 配置信息, 即使不正确也能使用 wx.ready
debug: false,
appId: '',
timestamp: 1,
nonceStr: '',
signature: '',
jsApiList: []
});
wx.ready(() => {
let st = setTimeout(() => {
clearTimeout(st);
this.audioElm.load();
}, 50);
});
},
initWeiboSource() {
window.WeiboJS.init(
{
appkey: '3779229073',
debug: false,
timestamp: 1429258653,
noncestr: '8505b6ef40',
scope: [
'getNetworkType',
'networkTypeChanged',
'getBrowserInfo',
'checkAvailability',
'setBrowserTitle',
'openMenu',
'setMenuItems',
'menuItemSelected',
'setSharingContent',
'openImage',
'scanQRCode',
'pickImage',
'getLocation',
'pickContact',
'apiFromTheFuture'
]
},
ret => {
this.audioElm.load();
}
);
},
playAudio() {
// 播放暂停音频
if (this.audioElm.readyState > 2) {
// 当资源可以播放的时候
if (this.audioElm.paused) {
this.cacheCurrentTime === 0
? (this.audioElm.currentTime = 0)
: (this.audioElm.currentTime = this.cacheCurrentTime);
this.playState = true;
this.audioElm.play();
} else {
this.audioElm.pause();
}
}
},
second2time(currentTime) {
// 秒数化为分钟
let min = Math.floor(currentTime / 60); // 向下取整分钟
let second = Math.floor(currentTime % 60); // 取模得到剩余秒数
if (min < 10) {
min = '0' + min;
}
if (second < 10) {
second = '0' + second;
}
return `${min}:${second}`;
},
stalled() {
// 资源需要缓存的时候暂停
this.audioElm.pause();
// 缓存加载待播的时候,若是当前播放时间已经走动则触发播放
if (this.audioElm.currentTime !== 0) {
// 判断当前播放的时间是否到达结束,否则则继续播放
if (this.audioElm.currentTime !== this.audioElm.duration) {
this.playAudio();
} else {
this.ended();
}
}
},
timeupdate() {
if (
this.audioElm.readyState > 2 &&
this.audioElm.currentTime > 0.2
) {
this.cacheCurrentTime = this.audioElm.currentTime;
this.currentPlayTime = this.second2time(
Number(this.audioElm.currentTime)
);
if (
this.audioElm.ended ||
this.audioElm.currentTime === this.audioElm.duration
) {
this.ended();
}
}
},
ended() {
this.audioElm.pause();
// 清除缓存的时间
this.cacheCurrentTime = 0;
this.voicePlayMessage = '';
},
pause() {
// 当音频/视频已暂停时
this.playState = false;
},
error(err) {
// 当在音频/视频加载期间发生错误时
this.audioElm.pause();
this.voicePlayMessage = '音频加载资源错误!';
console.log('我报错了:' + err);
},
abort() {
this.audioElm.pause();
}
}
};
</script>
<style lang="scss" scoped>
.play-voice-area {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
}
.cover-player {
position: relative;
display: flex;
align-items: center;
flex-direction: column;
flex-shrink: 0;
justify-content: center;
.cover-pic {
display: block;
overflow: hidden;
width: 446px;
height: 446px;
transition: animation 0.28s;
border: 15px solid hsla(0, 0%, 100%, 0.1);
border-radius: 223px;
img {
display: inline-block;
width: 446px;
height: 446px;
}
&.active {
animation: rotation 8s 0.1s linear infinite;
}
}
.cover-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-size: 100px;
}
a,
button,
input,
textarea {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
}
.sound-desrc {
display: flex;
overflow: hidden;
align-items: center;
flex-direction: column;
justify-content: center;
padding: 40px 0 0 0;
.username {
min-width: 243px;
height: 38px;
margin: 22px 0;
text-align: center;
letter-spacing: 0px;
text-overflow: ellipsis;
color: #c4c9e2;
font-size: 36px;
font-weight: normal;
font-weight: 700;
font-stretch: normal;
line-height: 38px;
}
.timeline {
width: 243px;
height: 38px;
text-align: center;
color: #c4c9e2;
font-size: 36px;
font-weight: normal;
font-stretch: normal;
line-height: 38px;
line-height: 38px;
}
}
@keyframes rotation {
from {
-webkit-transform: rotate(0deg);
}
to {
-webkit-transform: rotate(360deg);
}
}
</style>
这个版本考虑了多音频播放,所以在主入口直接单例挂载了一个播放器
其次考虑音频的切换播放,所以必须依赖Vuex
来共享状态
main.js
-主入口// 创建全局播放器
const music = new Audio();
Vue.prototype.player = music;
状态很简单,就一些基础信息,module
的方式,state
通过getters
暴露
export default {
state: {
index: '',
playState: false,
curTime: '00:00'
},
mutations: {
CURRENT_PLAY: (state, index) => {
state.index = index;
},
CURRENT_TIME: (state, time) => {
state.curTime = time;
},
SetPlayState(state, status) {
state.playState = status;
}
}
};
<template>
<div
@click="playstop"
class="icon-wrap"
:class="iconSize"
:style="{color:iconColor}">
<i
class="sx-mobile"
:class="playState ? iconShow.stop : iconShow.play" />
</div>
</template>
<script>
export default {
props: {
iconShow: {
type: Object,
default: function() {
return {
play: 'sx-mobile-bofang',
stop: 'sx-mobile-icon-'
};
}
},
iconSize: {
type: String,
default: 'normal'
},
iconColor: {
type: String,
default: '#FFF'
},
playState: {
type: Boolean,
default: false
},
sourceUrl: {
type: String,
default: ''
},
mode: {
type: String,
default: 'self'
}
},
created() {
// 检测微博微信平台
this.checkWeiBo_WeiChat();
console.log(this.sourceUrl);
},
mounted() {
this.player.addEventListener('end', this.voiceEnd);
},
methods: {
checkWeiBo_WeiChat() {
let ua = navigator.userAgent.toLowerCase(); // 获取判断用的对象
const script = document.createElement('script');
if (/micromessenger/.test(ua)) {
// 返回一个独立的promise
script.src = 'https://res.wx.qq.com/open/js/jweixin-1.2.0.js';
new Promise((resolve, reject) => {
let done = false;
script.onload = script.onreadystatechange = () => {
if (
!done &&
(!script.readyState ||
script.readyState === 'loaded' ||
script.readyState === 'complete')
) {
done = true;
// 避免内存泄漏
script.onload = script.onreadystatechange = null;
resolve(script);
}
};
script.onerror = reject;
document
.getElementsByTagName('head')[0]
.appendChild(script);
}).then(res => {
this.initWeixinSource();
});
}
if (/WeiBo|weibo/i.test(ua)) {
script.src =
'https://tjs.sjs.sinajs.cn/open/thirdpart/js/jsapi/mobile.js';
new Promise((resolve, reject) => {
let done = false;
script.onload = script.onreadystatechange = () => {
if (
!done &&
(!script.readyState ||
script.readyState === 'loaded' ||
script.readyState === 'complete')
) {
done = true;
// 避免内存泄漏
script.onload = script.onreadystatechange = null;
resolve(script);
}
};
script.onerror = reject;
document
.getElementsByTagName('head')[0]
.appendChild(script);
}).then(res => {
this.initWeiboSource();
});
}
},
initWeixinSource() {
wx.config({
// 配置信息, 即使不正确也能使用 wx.ready
debug: false,
appId: '',
timestamp: 1,
nonceStr: '',
signature: '',
jsApiList: []
});
wx.ready(() => {
let st = setTimeout(() => {
clearTimeout(st);
this.player.load();
}, 50);
});
},
initWeiboSource() {
window.WeiboJS.init(
{
appkey: '3779229073',
debug: false,
timestamp: 1429258653,
noncestr: '8505b6ef40',
scope: [
'getNetworkType',
'networkTypeChanged',
'getBrowserInfo',
'checkAvailability',
'setBrowserTitle',
'openMenu',
'setMenuItems',
'menuItemSelected',
'setSharingContent',
'openImage',
'scanQRCode',
'pickImage',
'getLocation',
'pickContact',
'apiFromTheFuture'
]
},
ret => {
this.player.load();
}
);
},
second2time(currentTime) {
/* 秒数化为分钟 */
let min = parseInt(currentTime / 60, 10); // 向下取整分钟
let second = parseInt(currentTime % 60, 10); // 取模得到剩余秒数
if (min < 10) {
min = '0' + min;
}
if (second < 10) {
second = '0' + second;
}
return `${min}:${second}`;
},
playstop() {
if (this.mode === 'self') {
this.player.paused ? this.playVoice() : this.pauseVoice();
} else {
if (this.$store.getters.vindex === this.index) {
this.player.paused ? this.playVoice() : this.pauseVoice();
} else {
this.player.src = this.sourceUrl;
this.player.play();
if (!this.player.paused) {
this.$store.commit('SetPlayState', true);
this.$store.commit('CURRENT_PLAY', this.index);
}
}
}
},
playVoice() {
if (this.player.src !== '') {
this.player.play();
if (!this.player.paused) {
this.$store.commit('SetPlayState', true);
this.$store.commit('CURRENT_PLAY', this.index);
if (this.mode === 'self') {
this.playState = true;
}
}
} else {
this.player.src = this.sourceUrl;
this.playVoice();
}
},
pauseVoice() {
this.player.pause();
this.$store.commit('SetPlayState', false);
if (this.mode === 'self') {
this.playState = false;
}
},
voiceEnd() {
if (this.mode === 'self') {
this.$emit('update:playState', false);
}
}
},
};
</script>
<style lang="scss" scoped>
.icon-wrap {
&.small {
font-size: 16px;
}
&.normal {
font-size: 32px;
}
&.large {
font-size: 64px;
}
&.huge {
font-size: 96px;
}
&.big {
font-size: 128px;
}
i {
font-size: inherit;
}
}
</style>
// 这里给标签加了spec标记,是为了防止多次访问同一个页面的时候,
// 无限的插入新增的js
// 这次就不再nuxt.config.js引入中间件了.因为不是面向全局,直接在对应的页面引入即可
export default context => {
const { env } = context.deviceType;
const HeadScript = context.app.head.script;
if (env === "wechat") {
if (!HeadScript[HeadScript.length - 1].spec) {
HeadScript.push({
src: "https://res.wx.qq.com/open/js/jweixin-1.3.2.js",
type: "text/javascript",
charset: "utf-8",
spec: true,
});
}
}
if (env === "weibo") {
if (!HeadScript[HeadScript.length - 1].spec) {
HeadScript.push({
src: "http://tjs.sjs.sinajs.cn/open/thirdpart/js/jsapi/mobile.js",
type: "text/javascript",
charset: "utf-8",
spec: true,
});
}
}
};
import Vue from "vue";
export default ({ app, store }) => {
let player = new Audio();
player.preload = "auto";
// 把单例的播放器提交到vuex去管控
store.commit("voice/SetPlayer", player);
};
因为audio
对象只有客户端才有,所以不能服务端初始化 设置ssr:false
就代表在客户端的时候才注入,默认不写ssr
是true
module.exports = {
plugins: [ { src: "~plugins/player.js", ssr: false }]
};
index.js
是根状态,其他再改目录下的js
文件均默认当做vuex
的module
// index.js
import Vuex from "vuex";
export const state = () => ({
deviceType: {},
});
export const mutations = {
SetDeviceType(state, payload) {
state.deviceType = payload;
},
};
export const getters = {
deviceType(state) {
return state.deviceType;
},
player(state) {
return state.voice.player;
},
playState(state) {
return state.voice.playState;
},
playUrl(state) {
return state.voice.playUrl;
},
playIndex(state) {
return state.voice.playIndex;
},
playTime(state) {
return state.voice.playTime;
},
voiceTotalTime(state) {
return state.voice.voiceTotalTime;
},
};
// voice.js
import Vuex from "Vuex";
export const state = () => ({
player: "", // 播放器
playState: false, // 当前播放的状态
playUrl: "", // 播放的链接
playIndex: 0, // 当前播放的索引
playTime: "00:00", // 当前的播放时间
voiceTotalTime: "00:00", // 曲目总时长
});
export const mutations = {
SetPlayer(state, payload) {
state.player = payload;
},
SetPlayState(state, payload) {
state.playState = payload;
},
SetPlayUrl(state, payload) {
state.playUrl = payload;
state.player.src = payload;
},
SetPlayIndex(state, payload) {
state.playIndex = payload;
},
SetPlayTime(state, payload) {
state.playTime = payload;
},
SetVoiceTotalTime(state, payload) {
state.voiceTotalTime = payload;
},
ResetVoice(state) {
state.playState = false;
state.playUrl = "";
state.playTime = "00:00";
state.voiceTotalTime = "00:00";
},
};
播放状态均由vuex
来管理,这样对于多音频或者跨组件控制播放非常有帮助
<template>
<div class="player"
:class="$store.getters.playState ? 'animation-roate':''"
:style="{background:`url(${CoverImg}) center center no-repeat`,backgroundSize: 'cover'}">
<div class="icon-wrap">
<img :src="playstate? StopIcon:PlayIcon"
alt="播放器操作按钮"
@click="changePlayState(playstate)">
</div>
</div>
</template>
<script>
const CoverImg = require('./images/cover@2x.png');
const PlayIcon = require('./images/play@2x.png');
const StopIcon = require('./images/stop@2x.png');
export default {
data() {
return {
CoverImg,
PlayIcon,
StopIcon,
}
},
props: {
playstate: {
type: Boolean,
default: false
},
playurl: {
type: String,
default: 'http://www.ytmp3.cn/down/51013.mp3'
}
},
mounted() {
this.$store.getters.player.addEventListener('loadedmetadata', () => {
// 缓存播放总时长
this.$store.commit('voice/SetVoiceTotalTime', this.second2time(this.$store.getters.player.duration));
})
this.$store.getters.player.addEventListener('stalled', () => {
// 重置播放状态
this.$store.commit('voice/ResetVoice');
})
this.$store.getters.player.addEventListener('abort', () => {
// 重置播放状态
this.$store.commit('voice/ResetVoice');
})
this.$store.getters.player.addEventListener('play', () => {
this.$store.commit('voice/SetPlayState', true);
})
this.$store.getters.player.addEventListener('pause', () => {
this.$store.commit('voice/SetPlayState', false);
})
this.$store.getters.player.addEventListener('timeupdate', () => {
this.$store.commit('voice/SetPlayTime', this.second2time(this.$store.getters.player.currentTime));
})
this.$store.getters.player.addEventListener('ended', () => {
this.$store.commit('voice/ResetVoice');
})
},
beforeDestroy() {
this.$store.getters.player.removeEventListener('loadedmetadata', () => {
this.$store.commit('voice/SetVoiceTotalTime', this.second2time(this.$store.getters.player.duration));
})
this.$store.getters.player.removeEventListener('stalled', () => {
this.$store.commit('voice/ResetVoice');
})
this.$store.getters.player.removeEventListener('abort', () => {
this.$store.commit('voice/ResetVoice');
})
this.$store.getters.player.removeEventListener('play', () => {
this.$store.commit('voice/SetPlayState', true);
})
this.$store.getters.player.removeEventListener('pause', () => {
this.$store.commit('voice/SetPlayState', false);
})
this.$store.getters.player.removeEventListener('timeupdate', () => {
this.$store.commit('voice/SetPlayTime', this.second2time(this.$store.getters.player.currentTime));
console.log(this.$store.getters.player.currentTime)
})
this.$store.getters.player.removeEventListener('ended', () => {
this.$store.commit('voice/ResetVoice');
})
},
methods: {
changePlayState(playstate) {
// 设置播放源
if (!this.$store.getters.playUrl) {
this.$store.commit('voice/SetPlayUrl', this.playurl)
}
// 设置播放状态
if (playstate) {
this.$store.getters.player.pause();
} else {
this.$store.getters.player.play();
}
playstate = !playstate;
},
initWeixinSource() {
wx.config({
// 配置信息, 即使不正确也能使用 wx.ready
debug: false,
appId: '',
timestamp: 1,
nonceStr: '',
signature: '',
jsApiList: []
});
wx.ready(() => {
let st = setTimeout(() => {
clearTimeout(st);
this.player.load();
}, 50);
});
},
initWeiboSource() {
window.WeiboJS.init(
{
appkey: '3779229073',
debug: false,
timestamp: 1429258653,
noncestr: '8505b6ef40',
scope: [
'getNetworkType',
'networkTypeChanged',
'getBrowserInfo',
'checkAvailability',
'setBrowserTitle',
'openMenu',
'setMenuItems',
'menuItemSelected',
'setSharingContent',
'openImage',
'scanQRCode',
'pickImage',
'getLocation',
'pickContact',
'apiFromTheFuture'
]
},
ret => {
this.audioElm.load();
}
);
},
second2time(currentTime) {
// 秒数化为分钟
let min = Math.floor(currentTime / 60); // 向下取整分钟
let second = Math.floor(currentTime % 60); // 取模得到剩余秒数
if (min < 10) {
min = '0' + min;
}
if (second < 10) {
second = '0' + second;
}
return `${min}:${second}`;
},
}
}
</script>
<style lang="scss" scoped>
.player {
height: 100%;
width: 100%;
border-radius: 100%;
position: relative;
.icon-wrap {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
img {
display: block;
height: 94px;
width: 94px;
}
}
}
@keyframes fade-rotate {
from {
opacity: 0.8;
transform: rotate(0) scale(1);
}
to {
opacity: 1;
transform: rotate(360deg) scale(1.1);
}
}
.animation-roate {
transform: translate3d(0, 0, 0);
animation: fade-rotate 18s ease-in-out infinite alternate;
}
</style>
有不对之处请留言,会及时修正,谢谢阅读。