一个简单的HTML音视频播放器
适用场景
本播放器适用场景: 播客类型的文章,需要一个播放器,但是不需要预加载资源文件的。可以尽可能的减少不必要的加载及访客流量浪费。
版本
本播放器有两个版本:
- 基于 Jquery 的版本。本文的源码属于这个版本。
- 基于Angular 11 的版本。源码见【Github】需要的自取。这个版本音频视频播放器是分开的,而且视频内置了自动区分 iframe 使用。
注意:播放器中的图标都是字体图标,所以只能参考修改。
代码
interface IPlayerOption {
[key: string]: any;
src: string;
type?: 'audio' | 'video' | 'iframe'
}
;(function($: any) {
const EVENT_TIME_UPDATE = 'timeupdate';
const EVENT_PLAY = 'play';
const EVENT_PAUSE = 'pause';
const EVENT_ENDED = 'ended';
const EVENT_VOLUME_UPDATE = 'volumeupdate';
const EVENT_TAP_PLAY = 'tap_play';
const EVENT_TAP_PAUSE = 'tap_pause';
const EVENT_BOOT = 'boot';
const EVENT_TAP_VOLUME = 'tap_volume';
const EVENT_TAP_TIME = 'tap_time';
const EVENT_ENTER_FULL_SCREEN = 'full_screen';
const EVENT_EXIT_FULL_SCREEN = 'exit_full_screen';
const screenFull = function() {
const fnMap = [
[
'requestFullscreen',
'exitFullscreen',
'fullscreenElement',
'fullscreenEnabled',
'fullscreenchange',
'fullscreenerror'
],
// New WebKit
[
'webkitRequestFullscreen',
'webkitExitFullscreen',
'webkitFullscreenElement',
'webkitFullscreenEnabled',
'webkitfullscreenchange',
'webkitfullscreenerror'
],
// Old WebKit
[
'webkitRequestFullScreen',
'webkitCancelFullScreen',
'webkitCurrentFullScreenElement',
'webkitCancelFullScreen',
'webkitfullscreenchange',
'webkitfullscreenerror'
],
[
'mozRequestFullScreen',
'mozCancelFullScreen',
'mozFullScreenElement',
'mozFullScreenEnabled',
'mozfullscreenchange',
'mozfullscreenerror'
],
[
'msRequestFullscreen',
'msExitFullscreen',
'msFullscreenElement',
'msFullscreenEnabled',
'MSFullscreenChange',
'MSFullscreenError'
]
];
for (const item of fnMap) {
if (item && item[1] in document) {
return item;
}
}
return false;
}();
class MediaPlayer {
constructor(
public element: JQuery,
public options: IPlayerOption
) {
if (!this.options.src) {
return;
}
this.init();
this.bindCustomEvent();
}
private playerElement: HTMLVideoElement|HTMLAudioElement;
private playerBar: JQuery;
private booted = false;
private volumeLast = 100;
private duration = 0;
public on(event: string, callback: Function): this {
this.options['on' + event] = callback;
return this;
}
public hasEvent(event: string): boolean {
return this.options.hasOwnProperty('on' + event);
}
public trigger(event: string, ... args: any[]) {
let realEvent = 'on' + event;
if (!this.hasEvent(event)) {
return;
}
return this.options[realEvent].call(this, ...args);
}
private bindCustomEvent() {
this.on(EVENT_BOOT, () => {
if (this.booted) {
return;
}
if (this.options.type === 'audio') {
this.bindAudioEvent();
return;
}
if (this.options.type === 'iframe') {
this.videoFrame();
this.booted = true;
return;
}
this.videoPlayer();
this.initBar(this.element.find('.player-bar'));
this.bindVideoEvent();
}).on(EVENT_TAP_PLAY, () => {
this.playerElement.play();
}).on(EVENT_TAP_PAUSE, () => {
this.playerElement.pause();
}).on(EVENT_TIME_UPDATE, (p: number, t: number) => {
this.duration = t;
this.playerBar.find('.time').text(this.formatMinute(p) + '/' + this.formatMinute(t));
const progess = this.playerBar.find('.slider .progress');
progess.attr('title', parseInt(p.toString()));
progess.find('.progress-bar').css('width', p * 100 / t + '%');
}).on(EVENT_PLAY, () => {
this.playerBar.find('.icon .fa').addClass('fa-pause').removeClass('fa-play');
}).on(EVENT_PAUSE, () => {
this.playerBar.find('.icon .fa').removeClass('fa-pause').addClass('fa-play');
}).on(EVENT_ENDED, () => {
this.trigger(EVENT_PAUSE);
}).on(EVENT_TAP_VOLUME, (v: number) => {
if (!this.playerElement) {
return;
}
this.playerElement.volume = v / 100;
this.trigger(EVENT_VOLUME_UPDATE, v);
}).on(EVENT_VOLUME_UPDATE, (v: number) => {
const progess = this.playerBar.find('.volume-slider .progress');
progess.attr('title', parseInt(v.toString()));
progess.find('.progress-bar').css('width', v + '%');
let volumeCls = 'fa-volume-up';
if (v <= 0) {
volumeCls = 'fa-volume-off';
} else if (v < 60) {
volumeCls = 'fa-volume-down';
}
this.playerBar.find('.volume-icon .fa').attr('class', 'fa ' + volumeCls);
}).on(EVENT_TAP_TIME, (p: number) => {
if (!this.playerElement) {
return;
}
this.playerElement.currentTime = p;
}).on(EVENT_EXIT_FULL_SCREEN, () => {
this.playerBar.find('.full-icon .fa').attr('class', 'fa fa-expand');
this.element.removeClass('player-full');
}).on(EVENT_ENTER_FULL_SCREEN, () => {
this.element.addClass('player-full');
this.playerBar.find('.full-icon .fa').attr('class', 'fa fa-compress');
});
}
private init() {
if (this.options.type === 'audio') {
this.initAudio();
return;
}
this.initVideo();
}
private initAudio() {
this.audioPlayer();
this.initBar(this.element);
}
private initBar(bar: JQuery) {
this.playerBar = bar;
const that = this;
bar.on('click', '.icon .fa', function() {
if (!that.booted) {
that.trigger(EVENT_BOOT);
}
that.trigger($(this).hasClass('fa-play') ? EVENT_TAP_PLAY : EVENT_TAP_PAUSE);
}).on('click', '.volume-icon .fa', function() {
const $this = $(this);
if ($this.hasClass('fa-volume-mute') || $this.hasClass('fa-volume-off')) {
that.trigger(EVENT_TAP_VOLUME, that.volumeLast);
return;
}
if (that.playerElement) {
that.volumeLast = that.playerElement.volume * 100;
}
that.trigger(EVENT_TAP_VOLUME, 0);
}).on('click', '.slider .progress', function(event) {
const $this = $(this);
that.trigger(EVENT_TAP_TIME, (event.clientX - $this.offset().left) * that.duration / $this.width());
}).on('click', '.volume-slider .progress', function(event) {
const $this = $(this);
that.trigger(EVENT_TAP_VOLUME, (event.clientX - $this.offset().left) * 100 / $this.width());
}).on('click', '.full-icon .fa', function() {
if (that.element.hasClass('player-full')) {
that.exitFullscreen();
return;
}
that.fullScreen();
});
}
private initVideo() {
this.videoMask();
this.element.on('click', '.player-mask', () => {
this.trigger(EVENT_BOOT);
if (this.playerElement) {
this.trigger(EVENT_TAP_PLAY);
}
});
}
private bindAudioEvent() {
if (this.booted) {
return;
}
this.booted = true;
this.playerElement = document.createElement('audio');
this.playerElement.src = this.options.src;
this.playerElement.addEventListener('timeupdate', () => {
if (isNaN(this.playerElement.duration) || !isFinite(this.playerElement.duration) || this.playerElement.duration <= 0) {
this.trigger(EVENT_TIME_UPDATE, 0, 0);
return;
}
this.trigger(EVENT_TIME_UPDATE, this.playerElement.currentTime, this.playerElement.duration);
});
this.playerElement.addEventListener('ended', () => {
this.trigger(EVENT_ENDED);
});
this.playerElement.addEventListener('pause', () => {
this.trigger(EVENT_PAUSE);
});
this.playerElement.addEventListener('play', () => {
this.trigger(EVENT_PLAY);
});
this.trigger(EVENT_VOLUME_UPDATE, this.playerElement.volume * 100);
}
private bindVideoEvent() {
if (this.booted) {
return;
}
this.booted = true;
this.playerElement = this.element.find('.player-video')[0] as HTMLVideoElement;
this.playerElement.addEventListener('timeupdate', () => {
if (isNaN(this.playerElement.duration) || !isFinite(this.playerElement.duration) || this.playerElement.duration <= 0) {
this.trigger(EVENT_TIME_UPDATE, 0, 0);
return;
}
this.trigger(EVENT_TIME_UPDATE, this.playerElement.currentTime, this.playerElement.duration);
});
this.playerElement.addEventListener('ended', () => {
this.trigger(EVENT_ENDED);
});
this.playerElement.addEventListener('pause', () => {
this.trigger(EVENT_PAUSE);
});
this.playerElement.addEventListener('play', () => {
this.trigger(EVENT_PLAY);
});
this.trigger(EVENT_VOLUME_UPDATE, this.playerElement.volume * 100);
if (screenFull) {
document.addEventListener(screenFull[4], () => {
if (this.checkFull()) {
this.trigger(EVENT_ENTER_FULL_SCREEN);
return;
}
this.trigger(EVENT_EXIT_FULL_SCREEN);
});
}
}
private audioPlayer() {
this.element.addClass('audio-player');
this.element.html(`<div class="icon" title="播放">
<i class="fa fa-play"></i>
</div>
<div class="slider">
<div class="progress" title="0">
<div class="progress-bar"></div>
</div>
</div>
<div class="time">
00:00/00:00
</div>
<div class="volume-icon">
<i class="fa fa-volume-up"></i>
</div>
<div class="volume-slider">
<div class="progress" title="100">
<div class="progress-bar" style="width: 100%;"></div>
</div>
</div>`);
}
private videoMask() {
this.element.addClass('video-player');
this.element.html(`<div class="player-mask" title="此处有视频,点击即可播放">
<i class="fa fa-play"></i>
</div>`);
}
private videoFrame() {
this.element.html(`
<iframe class="player-frame" src="${this.options.src}" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>`);
}
private videoPlayer() {
this.element.html(`<video class="player-video" src="${this.options.src}"></video>
<div class="player-bar">
<div class="icon" title="播放">
<i class="fa fa-play"></i>
</div>
<div class="slider">
<div class="progress" title="0">
<div class="progress-bar"></div>
</div>
</div>
<div class="time">
00:00/00:00
</div>
<div class="volume-icon">
<i class="fa fa-volume-up"></i>
</div>
<div class="volume-slider">
<div class="progress" title="100">
<div class="progress-bar" style="width: 100%;"></div>
</div>
</div>
<div class="full-icon">
<i class="fa fa-expand"></i>
</div>
</div>`);
}
private formatMinute(time: number): string {
return this.twoPad(Math.floor(time / 60)) + ':' + this.twoPad(Math.floor(time % 60));
}
private twoPad(n: number) {
const str = n.toString();
return str[1] ? str : '0' + str;
}
private fullScreen(element: any = document.documentElement) {
if (!screenFull) {
return;
}
element[screenFull[0]]();
}
private exitFullscreen(element: any = document) {
if (!screenFull) {
return;
}
element[screenFull[1]]();
}
private checkFull(): boolean {
return screenFull && Boolean(document[screenFull[2]]);
}
}
$.fn.player = function(option?: IPlayerOption) {
return new MediaPlayer(this, option);
};
})(jQuery);
样式
.audio-player,
.video-player {
.progress {
height: .4em;
border-radius: 0;
margin-top: .2em;
display: flex;
overflow: hidden;
line-height: 0;
font-size: .75rem;
background-color: #e9ecef;
.progress-bar {
cursor: default;
display: flex;
flex-direction: column;
justify-content: center;
overflow: hidden;
color: #fff;
text-align: center;
white-space: nowrap;
background-color: #007bff;
transition: width .6s ease;
}
&:hover {
height: .8em;
margin-top: 0;
}
}
}
.audio-player {
box-shadow: 0 2px 2px 0 rgb(0 0 0 / 7%), 0 1px 5px 0 rgb(0 0 0 / 10%);
display: flex;
box-sizing: border-box;
line-height: 2.5em;
i {
font-style: normal;
}
.icon {
width: 2.5em;
text-align: center;
}
.slider {
flex: 1;
padding-top: 1em;
}
.time {
line-height: 80rpx;
font-size: .8em;
}
.volume-icon {
padding-left: .5em;
}
.volume-slider {
width: 3em;
padding-top: 1em;
padding-right: .5em;
}
&.player-mini {
.volume-icon,
.volume-slider {
display: none;
}
}
}
.video-player {
position: relative;
box-shadow: 0 2px 2px 0 rgb(0 0 0 / 7%), 0 1px 5px 0 rgb(0 0 0 / 10%);
i {
font-style: normal;
}
.player-mask {
background-color: #000;
text-align: center;
padding: 1em;
.fa {
color: #fff;
font-size: 4em;
}
&:hover {
.fa {
color: #a10000;
}
}
}
.player-frame {
border: 0;
width: 100%;
height: 100vw;
max-height: 400px;
}
.player-video {
border: 0;
width: 100%;
height: 100vw;
max-height: 400px;
background-color: #000;
margin: 0;
}
.player-bar {
display: flex;
box-sizing: border-box;
line-height: 2.5em;
.icon {
width: 2.5em;
text-align: center;
}
.slider {
flex: 1;
padding-top: 1em;
}
.time {
line-height: 80rpx;
font-size: .8em;
}
.volume-icon {
padding-left: .5em;
}
.volume-slider {
width: 3em;
padding-top: 1em;
padding-right: .5em;
}
.full-icon {
width: 2em;
text-align: center;
}
}
&.player-full {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: 0;
z-index: 9999;
background-color: #000;
box-shadow: none;
.player-video {
width: 100%;
height: 100%;
max-height: 100%;
}
.player-bar {
background-color: rgba(43,51,63,.7);
color: #fff;
position: absolute;
bottom: 0;
left: 0;
right: 0;
opacity: 0;
transition: 1s opacity;
&:hover {
opacity: 1;
}
}
}
}
使用
<link type="text/css" href="player.css" rel="stylesheet" media="all">
<div id="player"></div>
<script src="jquery.player.min.js"></script>
<script type="text/javascript">
jQuery(document).ready(function () {
$('#player').player({
src: "1616073275141535.mp3",
type: "audio"
});
});
</script>
请先引入jquery.js
参数说明
src 为资源文件的网络路径
type 可选值 video|audio|iframe ;默认为 video ; audio 即音频;iframe 为其他网址的视频,例如 bilibili 分享的链接
参考资料
转载请保留原文链接: https://zodream.cn/blog/id/198.html