为保证功能的正常,基本的浏览器检测是必要的。javascript
另外兼容性这块推荐 Modernizrhtml
使用数组来存储 DOM 结构,经过配置项选择性地往数组里 push
相应的 DOM,最终使用 join
导出 DOM 的字符串格式。经过 insertAdjacentHTML把 DOM 插入文档树中。为方便之后 DOM 的操做和总体状态样式的控制,能够将控制器 DOM 结构跟 Audio 标签包裹在同一 DIV 中。前端
html.push('<div class="myaudio__controls">');
if (_.inArray(controls, 'play')) {
html.push(
` <button type="button" data-myaudio="play"> <svg><use xlink:href="#${icon.play}" /></svg> </button> <button type="button" data-myaudio="pause"> <svg><use xlink:href="#${icon.pause}" /></svg> </button> `
);
}
html.push('</div>');
html.join('');
复制代码
为方便 DOM 操做,可基于原生querySelectorAll查询播放器相关 DOM 并暂存起来,(如播放按钮,进度条,声音控制器等)。vue
控制器相关的事件有java
input
,故监听 Change
来更新进度条 )播放和音量控制都涉及两种状态,可使用 toggle 的方式来控制。webpack
togglePlay(toggle) {
if (!_.is.boolean(toggle)) {
toggle = this.media.paused;
}
if (toggle) {
this.play();
} else {
this.pause();
}
return toggle;
}
复制代码
进度条控制中 input change 触发的 seek 回调git
//
seek(input) {
// 跳转时间
let targetTime = 0;
let paused = this.media.paused;
// 获取总时长
let duration = this.getDuration();
if (_.is.number(input)) {
// 若是传入的参数为数值则直接设置为目标时间
targetTime = input;
} else if (
// 通常状况下,传入的参数为 input 元素,
// 因为 targetTime / duration = input.target.value / input.target.max
// 因此跳转时间为 targetTime = input.target.value / input.target.max * duration;
_.is.object(input) &&
_.inArray(['input', 'change'], input.type)
) {
targetTime = input.target.value / input.target.max * duration;
}
// 特殊状况处理
// 让跳转时间的区间在 0 至 总时长
if (targetTime < 0) {
targetTime = 0;
} else if (targetTime > duration) {
targetTime = duration;
}
// 更新进度条
this.updateSeekDisplay(targetTime);
// 让Audio元素的当前时间等于跳转时间完成跳转
// TODO:若是需求是拖动进度条过程不改变音频当前时间则须要作其余处理
try {
this.media.currentTime = targetTime.toFixed(4);
} catch (e) {}
// 确保音频暂停状态一致
if (paused) {
this.pause();
}
// 触发 audio 原生 timeupdate 和 seeking,目的是状态保持一致
// 这里涉及 自定义事件 想深刻的同窗可自行了解
this.triggerEvent(this.media, 'timeupdate');
this.triggerEvent(this.media, 'seeking');
}
复制代码
同播放控制github
setVolume(volume) {
let max = this.config.volumeMax;
let min = this.config.volumeMin;
// 取storage的值
if (_.is.undefined(volume)) {
volume = this.storage.volume;
}
// 取默认值
if (volume === null || isNaN(volume)) {
volume = this.config.volume;
}
// 控制音量区间在 min 至 max
if (volume > max) {
volume = max;
}
if (volume < min) {
volume = min;
}
this.media.volume = parseFloat(volume / max);
// 同步 音量的进度条
if (this.volume.display) {
this.volume.display.value = volume;
}
// 音量肯定是否静音
if (volume === 0) {
this.media.muted = true;
} else if (this.media.muted && volume > 0) {
this.toggleMute();
}
}
复制代码
durationchange
loadedmetadata
触发时,获取时长 或者 手动设置时长。web
displayDuration() {
// 支持ie9 以上
if (!this.supported.full) {
return;
}
// Audio 不少事件都基于 duration 正常获取,可是duration在每一个设备中值可能不一样
// TODO:当须要懒加载时,音频不加载则没法获取时长,此时需手动设置
let duration = this.getDuration() || 0;
// 只在开始的时候显示时长,设置的条件是没有时长的DOM,displayDuration 为true,视频暂停时。
if (!this.duration && this.config.displayDuration && this.media.paused) {
this.updateTimeDisplay(duration, this.currentTime);
}
if (this.duration) {
// 转换时间格式后,经过 innerHTML 直接设置
this.updateTimeDisplay(duration, this.duration);
}
}
复制代码
timeupdate
seeking
触发时,更新时间数组
timeUpdate(event) {
// 更新音频当前时间
this.updateTimeDisplay(this.media.currentTime, this.currentTime);
if (event && event.type === 'timeupdate' && this.media.seeking) {
return;
}
// 更新进度条
this.updateProgress(event);
}
复制代码
progress
playing
触发时,更新缓存时长
updateProgress(event) {
if (!this.supported.full) {
return;
}
let progress = this.progress.played;
let value = 0;
let duration = this.getDuration();
if (event) {
switch (event.type) {
// 已播放时长设置
case 'timeupdate':
case 'seeking':
value = this.getPercentage(this.media.currentTime, duration);
if (event.type === 'timeupdate' && this.buttons.seek) {
this.buttons.seek.value = value;
}
break;
// 缓存时长设置
case 'playing':
case 'progress':、
progress = this.progress.buffer;
value = (() => {
let buffered = this.media.buffered;
if (buffered && buffered.length) {
return this.getPercentage(buffered.end(0), duration);
} else if (_.is.number(buffered)) {
return buffered * 100;
}
return 0;
})();
break;
}
}
// Set values
this.setProgress(progress, value);
}
复制代码
// 进度条有两种,一、已播放的 二、缓存
setProgress(progress, value) {
if (!this.supported.full) {
return;
}
if (_.is.undefined(value)) {
value = 0;
}
if (_.is.undefined(progress)) {
if (this.progress && this.progress.buffer) {
progress = this.progress.buffer;
} else {
return;
}
}
if (_.is.htmlElement(progress)) {
progress.value = value;
} else if (progress) {
if (progress.bar) {
progress.bar.value = value;
}
if (progress.text) {
progress.text.innerHTML = value;
}
}
}
复制代码
volumechange
触发时,更新音量
updateVolume() {
// 静音时,音量为0
let volume = this.media.muted
? 0
: this.media.volume * this.config.volumeMax;
if (this.supported.full) {
if (this.volume.input) {
// 音频控制圆点位置更新
this.volume.input.value = volume;
}
if (this.volume.display) {
// 音量位置更新
this.volume.display.value = volume;
}
}
this.updateStorage({ volume: volume });
// 添加静音全局样式控制类
_.toggleClass(this.container, this.config.classes.muted, volume === 0);
}
复制代码
play
pause
ended
触发时,更新播放状态
checkPlaying() {
// 暂停和播放样式切换
_.toggleClass(
this.container,
this.config.classes.playing,
!this.media.paused
);
_.toggleClass(
this.container,
this.config.classes.stopped,
this.media.paused
);
}
复制代码
waiting
canplay
seeked
触发时,更新loading状态
checkLoading(event) {
let loading = event.type === 'waiting';
let _this = this;
clearTimeout(this.timers.loading);
// 当不是 waiting 事件时,把事件在当前调用栈最后执行
this.timers.loading = setTimeout(function() {
_.toggleClass(_this.container, _this.config.classes.loading, loading);
}, loading ? 250 : 0);
}
复制代码
audio 的事件并很少,DOM结构也并不复杂。只要你们按上面的思路理一遍,基本也能本身写一个原生的audio。
实现方式大同小异,只是需求不一样。但愿这个教程对你们有所帮助。接下来的教程系列将会从audio 中的遇到的坑来说解。
谢谢阅读!
如下推荐阅读,读者可选读: