开发一个web音乐播放器到底有多难?

1269 words, 6 minutes to read / August 22, 2019

前段时间比较闲,顺便就开发了自己的音乐主页👉~~https://music.jw1.dev~~ (已下线),开发过程中遇到不少问题,一度以为解决不了,但是最后看来,其实实现都非常简单。

首先看看 html 吧

<audio src="path/to/your/audio/file"></audio>

<!-- 或者 -->
<audio>
    <source src="path/to/your/audio/file" type="audio/mpeg">
</audio>

这时候打开页面你会发现… 什么都没有。

<audio src="path/to/your/audio/file" controls></audio>

加上controls这个属性,浏览器才会显示原生的音频组件。至于浏览器兼容性我就不讲了,还是老样子扔个 MDN 链接在这里😁:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio#Browser_compatibility

所以,搞定了?

不!像我这种有强迫症的人,怎么可能让每个浏览器的显示都不一致!

要做的第一件事情就是,隐藏audio的原生控制组件:

<audio src="path/to/your/audio/file"></audio>
<!-- 去掉 controls -->

(👆三遍了🤣

然后用 CSS 将audio 移出文档流:

audio{position:fixed;top:0;left:-12138px;}

为什么要这么做?

—— 为了避免一些非主流的浏览器会让audio在没有controls属性的情况下出现在不应该出现的地方。(好累

接下来看 JS 的部分

let audio = document.querySelector('audio')

// 或者用jQuery
let audio = $('audio')

console.log(audio)

原生 JavaScript 在 log 下只能看到标签,而 jQuery 则会把所有和这个元素有关的东西都列出来,不论是有用的还是没有用的。但是知道有那么多属性 / 事件可以用是件好事,在这里说几样比较重要的。

接下来 js 区域中的audio都是以原生 js 获取的对象,如果你使用 jQuery,audio 赋值应该为

let audio = $($('audio')[0])

audio.play()

可以调用此方法来播放音频。因为市面上大多数浏览器不允许直接用 js 触发音乐播放,必须要由用户来触发该事件 (‘click’, ‘scroll’, ‘focus’, etc.),所以我们可以加一个播放按钮来触发事件:

<audio src="path/to/your/audio/file"></audio>

<button class="play">play</button>
// 获取play button
let playBtn = document.querySelector('.play');

// 给play button添加点击事件
playBtn.addEventListener('click',function(){
    
    // play button 点击后播放音频
    audio.play()
})

audio.pause() & audio.paused

调用audio.pause()来暂停音频。

调用audio.paused来获取音频暂停状态,true为暂停状态,false则为播放状态。

比如我现在需要再点击一次 play button 来暂停音频,我们可以这样做:

playBtn.addEventListener('click',function(){
    
    // 点击后判断音频是否为暂停状态
    if(audio.paused){
        
        // 如果audio.paused为true
        // 则音频为暂停状态
        // 这时候我们就要播放音频
        // 同时修改button的文字为pause
        audio.play()
        playBtn.innerHTML = 'pause'
    }else{
        
        // 如果audio.paused为false
        // 则音频为播放状态
        // 这时候我们就要暂停音频
        // 同时修改button的文字为play
        audio.pause()
        playBtn.innerHTML = 'play'
    }
})

audio.currentTime

调用此属性获取音频当前已播放时间。

修改此属性可以达到切换音频当前播放位置的功能:

// 快进十秒
fastForward = function(){
    audio.currentTime = audio.currentTime + 10
}
fastForward()

audio.duration

调用此属性获取音频的长度,单位为秒。

已知问题

  1. Firefox上会出现读不到 duration 的情况,可能和修改audio.src有关,我在自己项目中的做法是使用了ffmpeg读取出 duration,直接当作变量使用,没有使用浏览器给的这个属性。

audio.ontimeupdate

此方法在音频播放期间会循环调用,经测试,调用间隔大约为 200ms 一次。

此方法可以用在更新音频进度条上。有了上面的audio.currentTimeaudio.duration,我们可以算出当前音频播放进度的百分比:

// 将百分比定为全局变量
let audioProgressPercent = 0;
audio.ontimeupdate = function(){
    
    // 在audio时间更新的时候计算当前进度百分比
    audioProgressPercent = audio.currentTime / audio.duration
    console.log(audioProgressPercent)
}

这个时候如果你打开页面开始播放音频,就会看到控制台一直在更新进度百分比了,至于这个数值怎么用,不用我教大家了吧。😁

audio.buffered

调用此属性可以获取当前音频的缓冲进度和区间:

// 每200ms获取一次缓冲区间
let bufferedTimeInterval = setInterval(function(){

    // 缓冲可以有多个区间
    // 所以我们尽量都获取到
    let bufferedLength = audio.buffered.length

    // 以bufferedLength做循环
    for (let i = 0; i < bufferedLength; i++) {
        let bufferedStart = audio.buffered.start(i)
        let bufferedEnd = audio.buffered.end(i)
        console.log(bufferedStart, bufferedEnd)
    }
},200)

控制台输出:

> 0 17.64
  56.45 78.90

输出的含义为:

这一次读取audio.buffered得到了两个区间,所以循环了两次。

第一次获得的区间是[0,17.64],代表第 0 秒到第 17.64 秒之间的音频已经缓冲好了,可以直接播放。

第二次获得的区间是[56.45,78.90],代表这两个数字之间的音频已经缓冲好了,可以直接播放。

audio.onwaiting & audio.onplay

audio.onwaiting会在音频开始缓冲的时候被激活;

audio.onplay会在音频开始播放的时候被激活;

这时候我们可以做一个 loading 的动画,告诉用户音频正在加载了:

audio.onwaiting = function () {
    console.log('audio is loading')
    // 在这里激活loading动画
}
audio.onplay = function () {
    console.log('audio is playing')
    // 在这里关闭loading动画
}

audio.ended & audio.src

audio.ended顾名思义,会在音频结束后被激活;

audio.src是音频文件的位置,可修改;

当你有一个播放列表,且想在音频结束后播放下一首歌,你就可以:

// 定义播放列表
let playList = [
    'path/to/song1',
    'path/to/song2',
    'path/to/song3'
]

// 定义当前播放歌曲的ID
let currentSongID = 0;

// 音频结束后自动播放下一首
audio.onended = function(){
    
    // 更新当前播放歌曲的ID
    currentSongID = currentSongID + 1
    
    // 如果当前播放歌曲为播放列表中最后一首
    // 就从第一首开始放
    if(currentSongID >= playList.length){
        currentSongID = 0
    }
    
    // 改变audio的src属性
    audio.src = playList[currentSongID]
    audio.play()
}

哦还有!

audio.volume

调用获取音频的音量,可修改;

可以做点 fade in /fade out 的效果,使用 jQuery 可以很简单的做出来,原文中也有原生 js 的实现方法 (太长没看

$audio.animate({volume: newVolume}, 1000)

原文链接:https://stackoverflow.com/questions/7451508/html5-audio-playback-with-fade-in-and-fade-out

好了,以上就是我开发音乐主页之后想和大家分享的。

至于题中的问题,我的答案是:真的很简单。

只是 js 有一些性能上的限制,不能什么功能都往网页上搬…

在我的音乐主页中有一个砍掉的功能

—— 节拍器

为啥砍掉了呢…

因为 js 的处理效率真的不高 (或者只是我太辣鸡),中低端手机做出来的节拍器根本不准,至于实现原理,很简单:

歌曲的 bpm (beat per minute) 是已知的:

// 假设现在我有一首歌bpm为120
let bpm = 120

// 每40毫秒更新一次,目的是让bpm更新不低于24fps
// 然而中低端手机的效果却不尽人意
// pc端倒是没啥问题
let bpmTimeInterval = setInterval(function(){
    // 计算每秒有多少beats
    let interval = bpm / 60
    
    // _b会在时间线呈现出一个锯齿状的图形
    // 最高那个点,也就是齿尖,就是一个beat
    let _b = audio.currentTime % interval
    // ...
},40)

希望有一天 js 能有开发 Digital Audio Workstation 的能力!