WEB音频连播

72 阅读1分钟

方案

多个 url 连播

代码

eventBus

export class EventBus {
    events: any;

    constructor() {
        this.events = Object.create(null);
    }

    on(eventName: string, handler: (args?: any) => void) {
        if (!this.events[eventName]) {
            this.events[eventName] = [];
        }
        this.events[eventName].push(handler);
    }

    once(eventName: string | string[], handler: (args?: any) => void) {
        // eslint-disable-next-line no-param-reassign
        handler.prototype.once = true;
        if (Array.isArray(eventName)) {
            eventName.forEach((event: string) => {
                this.on(event, handler);
            });
        } else {
            this.on(eventName, handler);
        }
    }

    off(eventName: string, handler: (args?: any) => void) {
        if (!this.events[eventName]) {
            return;
        }
        const handlerIndex = this.events[eventName].findIndex((fn: () => void) => fn === handler);
        this.events[eventName].splice(handlerIndex, 1);
        if (this.events[eventName].length === 0) {
            delete this.events[eventName];
        }
    }

    emit(eventName: string, ...args: any[]) {
        if (!this.events[eventName]) {
            return;
        }
        this.events[eventName].forEach((handler: (args?: any) => void) => {
            handler(...args);
            if (handler.prototype?.once) {
                this.off(eventName, handler);
            }
        });
    }
}

audioPlayService

import { EventBus } from './EventBus';

export interface AutoPlayAudioServiceOptions {
    onIndexChange?: (index: number) => void;
    onPlayFinish?: () => void;
    delay?: number;
    onPlayError?: (error: ErrorEvent) => void;
    onPlayingChange?: (isPlaying: boolean) => void;
}

const initState: AutoPlayAudioServiceOptions = {
    onIndexChange: undefined,
    onPlayFinish: undefined,
    delay: 0,
    onPlayError: undefined,
    onPlayingChange: undefined,
};

export class AutoPlayAudioService implements AutoPlayAudioServiceOptions {
    onIndexChange?: (index: number) => void;

    onPlayFinish?: () => void;

    onPlayError?: (error: ErrorEvent) => void;

    onPlayingChange?: (isPlaying: boolean) => void;

    constructor(options?: AutoPlayAudioServiceOptions) {
        this.init(options);
    }

    public delay = 0;

    public audioList: (HTMLAudioElement | AutoPlayAudioService)[] = [];

    private canAddAudio = true;

    private delayTimer: NodeJS.Timeout | null = null;

    private eventBus = new EventBus();

    private _curPlaryingIndex = -1;

    get curPlaryingIndex() {
        return this._curPlaryingIndex;
    }

    set curPlaryingIndex(index: number) {
        this._curPlaryingIndex = index;
        this.onIndexChange?.(index);
    }

    private _isPlaying = false;

    set isPlaying(value) {
        this._isPlaying = value;
        this.onPlayingChange?.(value);
    }

    get isPlaying() {
        return this._isPlaying;
    }

    private _loading = false;

    set loading(val: boolean) {
        this._loading = val;
    }

    get loading() {
        return this._loading;
    }

    get curAudio() {
        return this.audioList[this.curPlaryingIndex];
    }

    init(options?: AutoPlayAudioServiceOptions) {
        Object.assign(this, initState, options);
    }

    onEnded(ev: Event) {
        const audio = ev.target as HTMLAudioElement;
        audio.removeEventListener('ended', this.bindEndListener);
        if (this.curPlaryingIndex + 1 === this.audioList.length) {
            this.isPlaying = false;
            this.eventBus.emit('ended', ev);
            this.onPlayFinish?.();
            return;
        }
        if (this.delay) {
            this.delayTimer = setTimeout(() => {
                this.curPlaryingIndex += 1;
                this.audioList[this.curPlaryingIndex].play();
            }, this.delay);
        } else {
            this.curPlaryingIndex += 1;
            this.audioList[this.curPlaryingIndex].play();
        }
    }

    bindEndListener = this.onEnded.bind(this);

    addAudio(audio: HTMLAudioElement | AutoPlayAudioService, autoPlay = true) {
        if (!audio) {
            return;
        }
        if (!this.canAddAudio) {
            return;
        }
        audio.addEventListener('error', (e) => {
            Logger.aliyunLog('[autoPlayAudio] error', e);
            this.eventBus.emit('error', e);
            this.isPlaying = false;
            audio.removeEventListener('ended', this.bindEndListener);
            if (this.curPlaryingIndex + 1 === this.audioList.length) {
                this.onPlayError?.(e);
                return;
            }
            this.curPlaryingIndex += 1;
            this.audioList[this.curPlaryingIndex].play();
        });
        audio.addEventListener('ended', this.bindEndListener);
        this.audioList.push(audio);
        const canFirstPlay = this.curPlaryingIndex === -1 && !this.loading;
        const canNextPlay = !this.isPlaying && !this.loading;
        const canPlay = autoPlay && (canFirstPlay || canNextPlay);
        if (!canPlay) {
            audio.load();
            return;
        }
        this.isPlaying = true;
        this.loading = true;
        this.curPlaryingIndex = this.curPlaryingIndex += 1;
        return audio
            .play()
            .catch((error) => {
                Logger.aliyunLog('[autoPlayAudio] playError', { error });
            })
            .finally(() => {
                this.loading = false;
            });
    }

    initAudioList(options?: AutoPlayAudioServiceOptions) {
        this.init(options);
        this.stopAudio();
        this.audioList = [];
        this.curPlaryingIndex = -1;
        this.canAddAudio = true;
    }

    stopAudio() {
        this.isPlaying = false;
        if (this.delayTimer) {
            clearTimeout(this.delayTimer);
            this.delayTimer = null;
        }
        this.audioList.forEach((audio) => {
            audio.pause();
            audio.removeEventListener('ended', this.bindEndListener);
        });
    }
    pause() {
        if (!this.isPlaying) {
            return;
        }
        this.isPlaying = false;
        this.audioList[this.curPlaryingIndex]?.pause();
    }
    play() {
        if (this.isPlaying) {
            return;
        }
        if (this.curPlaryingIndex === -1) {
            this.curPlaryingIndex = 0;
        }
        const audio = this.audioList[this.curPlaryingIndex];

        if (!audio) {
            return;
        }
        this.isPlaying = true;
        return audio.play();
    }

    load() {
        this.audioList.forEach((audio) => {
            audio.load();
        });
    }

    addEventListener(eventName: string, handler: (args?: any) => void) {
        this.eventBus.on(eventName, handler);
    }

    removeEventListener(eventName: string, handler: (args?: any) => void) {
        this.eventBus.off(eventName, handler);
    }
}