预览地址:http://doc.djtao.net/cms/media/audio
这是我个人练习的小项目。基于koa2-iview+less定制。用于个人对播放器的复习。现已集成于个人网站上了。
前端部分要改成更通用性的界面也是没什么问题的。对于主要用了icon而已。
其实要求比较简单:就是仿豆瓣fm。'
https://fm.douban.com
基本功能可拆解为:
播放器前端部分其实就围绕一个
写出来的样式如下:
相信不是太难。但是我其实最烦的就是样式了,调来调去很花时间。以下记录几个开发小难点。
音量需要在鼠标悬停的时候。以动画划出。
同时,类似豆瓣这些小清新系的播放器有个特点,就是显示出来的进度槽特别细。在此处给的值是2px高度。
相比之下,爱奇艺的进度条简直是播放器设计界的看泥石流,
怎样让小清新系的音量控制条也好点击呢?我采用以下方案:
<div id="volume" style="display:flex;margin-top:6px;">
<!-- 音量小喇叭图标 -->
<Icon style="margin-top:-4px;cursor:pointer;" size="14" type="md-volume-up" />
<!-- 音量槽 -->
<div @click="setvolume" id="volumeControlArea">
<div id="volumeControl" class="volume">
<div class="valunm-range" :style="{width:volumeWidth}"></div>
</div>
</div>
</div>
div#volumeControlArea
包着音量槽(div.#olumeControl
),而音量槽正常时是隐藏的。悬停在小喇叭上才显示(把宽度加到100px):
// 音量
.volume {
width: 0px;
height: 2px;
background: #ddd;
margin-top: 2px;
transition: width 0.2s;
transition-timing-function: ease-out;
}
#volumeControlArea {
padding-top: 10px;
height: 20px;
margin-top: -10px;
cursor: pointer;
}
.valunm-range {
height: 2px;
background: #888;
}
.show {
width: 100px;
/* 属性过渡(动画) */
transition: width 0.2s; /* 2s表示动画持续时间,多个属性之间用","号隔开 */
transition-timing-function: ease-out;
}
在js这么写。当悬停/移出div.volume
时,触发动画。如果你鼠标继续移到弹出来的音量槽,事件依然被div.colume
捕获。因此不会出现抖动。
// mounted
const volumeControl = document.querySelector("#volumeControl");
volume.addEventListener("mouseenter", e => {
let _this = e.target;
volumeControl.classList.add("show");
});
volume.addEventListener("mouseleave", e => {
let _this = e.target;
volumeControl.classList.remove("show");
});
而从样式得知:div#volumeControl
本身就带transition属性。因此也能做到平滑滚动。
而核心在于div#volumeControlArea
,它是有意做高了区域。
给了20像素的高度。那就不至于点不准了。
封面图是一个正圆,宽度是百分比,你如果用img来做是比较掉价的,所以用背景图。
<Col span="8">
<div class="cover" id="cover"></div>
</Col>
.cover {
width: 80%;
margin: auto;
height: 0;
padding-top: 80%;
border-radius: 50%;
/* ... */
background-size: 110% 110%;
overflow: hidden;
animation: play 8s linear infinite;
animation-play-state:paused;
}
.cover-play{
animation-play-state:running;
}
设置高度为0,给一个和宽度相等的百分比,便是正放形。
动画是8秒匀速转一圈,当播放开始时,给它加上.cover-play
的类就可以了。反之去掉。
相比之下,播放进度其实并没有那么难。
涉及关键api:
先在mounted初始化:
const audio = document.querySelector("#audio");
this.audio = audio;
this.currentTime = audio.currentTime;
// 获取播放时长
this.audio.addEventListener("canplay", e => {
this.duration = e.target.duration;
});
// 监听播放进度变化
audio.addEventListener("timeupdate", e => {
this.currentTime = e.target.currentTime;
});
以isPlay为状态,暂时先不考虑播放结束或报错的问题。只是以点击播放按钮为目标:
<!-- 进度显示 -->
<div class="audio-progress">
<div class="played" :style="{right:played}"></div>
</div>
<!-- 播放/暂停 -->
<Icon @click="setplay" class="play-btn" :type="isPlay?'md-pause':'md-play'" />
<script>
// method...
// 播放暂停设置
setplay() {
this.isPlay = !this.isPlay;
const cover=document.querySelector('#cover');
if(this.isPlay){
this.audio.play();
cover.classList.add('cover-play');
}else{
this.audio.pause();
cover.classList.remove('cover-play');
}
},
</sript>
div.play是绝对定位,通过监听进度百分比来变动数值。
把duration-currentTime的值转化为播放器的时间格式(HH-MM-SS)
// 渲染秒为分钟
formatSeconds(time) {
var minute = time / 60;
var minutes = parseInt(minute);
if (minutes < 10) {
minutes = "0" + minutes;
}
//秒
var second = time % 60;
var seconds = Math.round(second);
if (seconds < 10) {
seconds = "0" + seconds;
}
return `${minutes}:${seconds}`;
}
直接渲染就可以了。
音量控制在样式那里已经做的足够好了。接下来就处理设置音量的问题。首先明确一下原生事件的属性:
clientX、clientY:点击位置距离当前body可视区域的x,y坐标
pageX、pageY:对于整个页面来说,包括了被卷去的body部分的长度
screenX、screenY:点击位置距离当前电脑屏幕的x,y坐标
offsetX、offsetY:相对于带有定位的父盒子的x,y坐标
用offset就可以了。
// 设置音量
setvolume(e) {
// console.log(e.offsetX);
let volumeSet = document.querySelector(".show");
let width = parseInt(getComputedStyle(volumeSet).width);
let volume = (e.offsetX / width) * 100;
// 设置播放器音量
this.audio.volume = volume / 100;
// 设置音量槽宽度
this.volumeWidth = volume + "%";
},
是的,核心功能差不多就结束了。还留下2个坑。
最常见的异常就是网络链接挂了。还有一个是播完了怎么办。
ended-----判断是否已经播放完毕,返回true/false
因为目前只是前端在搞,所以,播完就让他结束吧。我们把它放到返回进度条百分比计算属性里判断。
played() {
let percents = Math.round(100 * (1 - this.currentTime / this.duration));
if (this.audio && this.audio.ended) {
const cover = document.querySelector("#cover");
this.isPlay = false;
cover.classList.remove("cover-play");
this.audio.pause();
}
return percents + "%";
},
可以提炼出两个方法以优化代码:
// 播放
play() {
const cover = document.querySelector("#cover");
this.isPlay = false;
cover.classList.remove("cover-play");
this.audio.play()
},
// 暂停
pause() {
const cover = document.querySelector("#cover");
this.isPlay = false;
cover.classList.remove("cover-play");
this.audio.pause()
},
error----在发生了错误后,返回错误代码
MediaError 对象由一个code和message组成,其中 code 属性返回一个数字值,它表示音频的错误状态:
出错了怎么办?让播放停止。同时报错。
需要在两个地方监听error事件:播放过程中,设置播放时。
// 播放暂停设置
setplay() {
if (!this.audio.error) {
this.isPlay = !this.isPlay;
if (this.isPlay) {
this.play();
} else {
this.pause();
cover.classList.remove("cover-play");
}
}else{
this.pause();
this.$Message.error(this.audio.error.message);
}
},
// 监听
played() {
let percents = Math.round(100 * (1 - this.currentTime / this.duration));
if ((!!this.audio) && this.audio.ended) {
this.pause();
}
if((!!this.audio) && this.audio.error){
this.pause();
this.$Message.error(this.audio.error.message)
}
return percents + "%";
},