基于H5的音乐播放器开发(1)(前端篇)

预览地址:http://doc.djtao.net/cms/media/audio

这是我个人练习的小项目。基于koa2-iview+less定制。用于个人对播放器的复习。现已集成于个人网站上了。

前端部分要改成更通用性的界面也是没什么问题的。对于主要用了icon而已。

需求分析

其实要求比较简单:就是仿豆瓣fm。'

https://fm.douban.com

基本功能可拆解为:

  • css原生动画
  • 播放控制:音量,播放器开关。几个原生api
  • 歌词显示
  • 播放模式·
  • 顶/踩
  • 播放列表
  • 异常处理
  • 因为页面太空,下方增加歌词播放界面。解析lrc歌词。

播放器前端部分其实就围绕一个

布局与样式

写出来的样式如下:

相信不是太难。但是我其实最烦的就是样式了,调来调去很花时间。以下记录几个开发小难点。

音量

音量需要在鼠标悬停的时候。以动画划出。

同时,类似豆瓣这些小清新系的播放器有个特点,就是显示出来的进度槽特别细。在此处给的值是2px高度。

相比之下,爱奇艺的进度条简直是播放器设计界的看泥石流,

怎样让小清新系的音量控制条也好点击呢?我采用以下方案:

  <div id="volume" style="display:flex;margin-top:6px;">
    <!-- 音量小喇叭图标 -->
    <Icon style="margin-top:-4px;cursor:pointer;" size="14" type="md-volume-up" />&nbsp;
    <!-- 音量槽 -->
    <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:

  • duration:总时长(秒)
  • currentTime:已播放时长(秒)
  • play/pause

播放进度条

先在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 属性返回一个数字值,它表示音频的错误状态:

  • 1 = MEDIA_ERR_ABORTED - 取回过程被用户中止
  • 2 = MEDIA_ERR_NETWORK - 当下载时发生错误
  • 3 = MEDIA_ERR_DECODE - 当解码时发生错误
  • 4 = MEDIA_ERR_SRC_NOT_SUPPORTED - 不支持音频/视频

出错了怎么办?让播放停止。同时报错。

需要在两个地方监听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 + "%";
    },

原文发布于微信公众号 - 一Li小麦(gh_c88159ec1309)

原文发表时间:2019-08-11

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券