Fork Me On Github

一个简单的HTML音视频播放器

适用场景

本播放器适用场景: 播客类型的文章,需要一个播放器,但是不需要预加载资源文件的。可以尽可能的减少不必要的加载及访客流量浪费。

版本

本播放器有两个版本:

  1. 基于 Jquery 的版本。本文的源码属于这个版本。
  2. 基于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 分享的链接

参考资料

  1. screenfull.js
点击查看全文
0 19 0