尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

前端音频播放器封装

前端音频播放器封装 /*** component 音频播放器* desc* 基于原生 Audio 的全局单例音频播放器。** 支持的音频源** 1. 普通 URL* https://xxx.com/audio.mp3** 2. Data URL* data:audio/mpeg;base64,xxxx** 3. Blob URL* blob:https://xxx/xxxx** 4. Blob* new Blob([...], { type: audio/mpeg })** 5. ArrayBuffer* ArrayBuffer / TypedArray** 6. 纯 Base64* 例如* AAAAGGZ0eXBtcDQy...** 注意* 纯 Base64 无法从内容本身可靠判断 MIME 类型* 因此需要通过 mimeType 参数告诉播放器音频格式。** 示例** AudioPlayer.play(https://xxx.com/test.mp3);** AudioPlayer.play(data:audio/mpeg;base64,xxxx);** AudioPlayer.play(blob);** AudioPlayer.play(arrayBuffer, audio/mpeg);** AudioPlayer.play(base64, audio/mpeg);*/export default class AudioPlayer {/*** 当前唯一的 Audio 实例** type {HTMLAudioElement|null}* private*/static _audioInstance null;/*** 当前 Audio 使用的临时 Blob URL。** 只有当播放器内部通过 Blob / ArrayBuffer 创建 Blob URL 时* 这里才会保存对应的 URL。** 普通 http/https/blob/data URL 不由播放器负责释放。** type {string|null}* private*/static _objectUrl null;/*** 播放音频** 如果传入新的 source* 1. 停止并清理当前音频* 2. 标准化音频源* 3. 创建新的 Audio* 4. 开始播放** 如果不传 source* 继续播放当前暂停中的音频。** param {string|Blob|ArrayBuffer|ArrayBufferView} [source]* 音频源** param {string} [mimeType]* 音频 MIME 类型。** 对普通 URL / Data URL / Blob URL 不需要传。** 对 Blob 通常也不需要传因为 Blob 自己带 type。** 对纯 Base64 / ArrayBuffer 必须传* 例如* audio/mpeg* audio/wav* audio/mp4** returns {Promisevoid}* 播放成功 resolve* 播放失败 reject*/static play(source, mimeType) {// // 没有传 source// 认为调用方想继续播放当前暂停的音频// if (source undefined) {const audio AudioPlayer._audioInstance;if (!audio) {return Promise.reject(new Error([AudioPlayer] 当前没有可继续播放的音频));}// 已经在播放不重复调用 play()if (!audio.paused !audio.ended) {return Promise.resolve();}return AudioPlayer._play(audio);}// // 有新的 source// 创建新的音频// let audioSource;try {// 播放新音频之前先停止当前音频AudioPlayer.stop();// 参数校验 音频源标准化audioSource AudioPlayer._normalizeSource(source,mimeType);const audio new Audio(audioSource);// 提前加载音频audio.preload auto;// 保存当前实例AudioPlayer._audioInstance audio;// 音频播放完成audio.onended () {console.info([AudioPlayer] 音频播放完毕);// 防止旧 Audio 的异步事件影响新 Audioif (AudioPlayer._audioInstance audio) {AudioPlayer._clear();}};// 音频加载 / 解码失败audio.onerror event {const error AudioPlayer._createMediaError(音频加载或解码失败,audio,event);console.error([AudioPlayer], error);// 只清理当前 Audioif (AudioPlayer._audioInstance audio) {AudioPlayer._clear();}};// 开始播放return AudioPlayer._play(audio);} catch (error) {console.error([AudioPlayer] 音频初始化失败:,error);AudioPlayer._clear();return Promise.reject(error);}}/*** 暂停当前音频** pause* - 不清理 Audio* - 不重置播放进度* - 后续可以通过 play() 继续播放*/static pause() {const audio AudioPlayer._audioInstance;if (!audio) {return;}if (!audio.paused) {audio.pause();}}/*** 停止当前音频** stop* - 停止播放* - 重置播放进度* - 移除事件监听* - 清理播放器引用* - 释放播放器自己创建的 Blob URL*/static stop() {AudioPlayer._clear();}/*** 判断当前是否正在播放** returns {boolean}*/static isPlaying() {const audio AudioPlayer._audioInstance;return !!audio !audio.paused !audio.ended;}/*** 判断当前是否暂停** returns {boolean}*/static isPaused() {const audio AudioPlayer._audioInstance;return !!audio audio.paused audio.currentTime 0;}/*** 判断当前是否存在音频** returns {boolean}*/static hasAudio() {return !!AudioPlayer._audioInstance;}/*** 获取当前播放时间** returns {number}*/static getCurrentTime() {const audio AudioPlayer._audioInstance;return audio ? audio.currentTime : 0;}/*** 获取当前音频总时长** returns {number}*/static getDuration() {const audio AudioPlayer._audioInstance;if (!audio || !Number.isFinite(audio.duration)) {return 0;}return audio.duration;}/*** 标准化音频源** 支持** string* Blob* ArrayBuffer* ArrayBufferView** param {*} source* param {string} [mimeType]* returns {string}* private*/static _normalizeSource(source, mimeType) {// // 1. 字符串// if (typeof source string) {const value source.trim();if (!value) {throw new TypeError([AudioPlayer] 音频源不能为空字符串);}/** 字符串可能是** https://...* http://...* blob:...* data:audio/...** 这些都是 Audio 可以直接使用的 source。*/if (/^https?:\/\//i.test(value) ||/^blob:/i.test(value) ||/^data:audio\//i.test(value)) {return value;}/** 其他字符串** 这里认为调用方传的是纯 Base64。** 由于纯 Base64 本身无法可靠判断 MIME* 必须显式传入 mimeType。*/if (!mimeType) {throw new TypeError([AudioPlayer] 纯 Base64 音频必须提供 mimeType);}AudioPlayer._validateMimeType(mimeType);return data:${mimeType};base64,${value};}// // 2. Blob// if (source instanceof Blob) {/** Blob 自己通常已经包含 MIME 类型** blob.type audio/mpeg** 如果 Blob 没有 type则要求调用方传 mimeType。*/const type source.type || mimeType;if (!type) {throw new TypeError([AudioPlayer] Blob 未提供 MIME 类型);}AudioPlayer._validateAudioMimeType(type);/** Blob URL 是播放器自己创建的* 因此后续必须由播放器负责 revokeObjectURL。*/return AudioPlayer._createObjectUrl(source);}// // 3. ArrayBuffer// if (source instanceof ArrayBuffer) {if (!mimeType) {throw new TypeError([AudioPlayer] ArrayBuffer 音频必须提供 mimeType);}AudioPlayer._validateAudioMimeType(mimeType);const blob new Blob([source],{type: mimeType});return AudioPlayer._createObjectUrl(blob);}// // 4. TypedArray / DataView// if (ArrayBuffer.isView(source)) {if (!mimeType) {throw new TypeError([AudioPlayer] TypedArray 音频必须提供 mimeType);}AudioPlayer._validateAudioMimeType(mimeType);/** 不能直接把 TypedArray 当作 Blob 数据* 这里取出它实际对应的 ArrayBuffer 区间。*/const buffer source.buffer.slice(source.byteOffset,source.byteOffset source.byteLength);const blob new Blob([buffer],{type: mimeType});return AudioPlayer._createObjectUrl(blob);}// // 5. 不支持的类型// throw new TypeError([AudioPlayer] 不支持的音频源类型: ${Object.prototype.toString.call(source)});}/*** 创建 Blob URL** param {Blob} blob* returns {string}* private*/static _createObjectUrl(blob) {if (!(blob instanceof Blob)) {throw new TypeError([AudioPlayer] 创建 Blob URL 失败参数不是 Blob);}/** 理论上 play() 前已经 stop()* 这里仍然再释放一次旧 Object URL* 作为额外兜底。*/AudioPlayer._revokeObjectUrl();const objectUrl URL.createObjectURL(blob);AudioPlayer._objectUrl objectUrl;return objectUrl;}/*** 校验 MIME 类型** 这里校验的是语法而不是判断浏览器是否真的支持该格式。** param {string} mimeType* private*/static _validateMimeType(mimeType) {if (typeof mimeType ! string ||!mimeType.trim()) {throw new TypeError([AudioPlayer] mimeType 必须是非空字符串);}if (!/^[\w.-]\/[\w.-](?:\s*;.*)?$/i.test(mimeType)) {throw new TypeError([AudioPlayer] 无效的 MIME 类型: ${mimeType});}}/*** 校验 Audio MIME 类型** param {string} mimeType* private*/static _validateAudioMimeType(mimeType) {AudioPlayer._validateMimeType(mimeType);if (!/^audio\//i.test(mimeType)) {throw new TypeError([AudioPlayer] MIME 类型必须是 audio/*: ${mimeType});}}/*** 执行 Audio.play()** param {HTMLAudioElement} audio* returns {Promisevoid}* private*/static _play(audio) {if (!(audio instanceof HTMLAudioElement)) {return Promise.reject(new TypeError([AudioPlayer] 无效的 Audio 实例));}let playPromise;try {playPromise audio.play();} catch (error) {if (AudioPlayer._audioInstance audio) {AudioPlayer._clear();}return Promise.reject(error);}/** 现代浏览器的 HTMLMediaElement.play()* 应该返回 Promise。** 这里仍然做兼容兜底。*/if (!playPromise ||typeof playPromise.then ! function) {return Promise.resolve();}return playPromise.catch(error {console.error([AudioPlayer] 音频播放失败:,error);if (AudioPlayer._audioInstance audio) {AudioPlayer._clear();}/** 不吞掉错误。** 让调用方可以** try {* await AudioPlayer.play(source);* } catch (error) {* ...* }*/throw error;});}/*** 创建媒体错误对象** param {string} message* param {HTMLAudioElement} audio* param {Event} event* returns {Error}* private*/static _createMediaError(message, audio, event) {const mediaError audio?.error;const error new Error(message);/** MediaError.code** 1 MEDIA_ERR_ABORTED* 2 MEDIA_ERR_NETWORK* 3 MEDIA_ERR_DECODE* 4 MEDIA_ERR_SRC_NOT_SUPPORTED*/error.code mediaError?.code ?? null;// 保存原始 MediaError方便业务排查error.mediaError mediaError ?? null;// 保存原始 Eventerror.event event ?? null;return error;}/*** 清理当前 Audio 实例** private*/static _clear() {const audio AudioPlayer._audioInstance;/** 先把全局引用清掉。** 这是为了防止清理过程中产生异步事件* 继续把这个 Audio 当成当前实例。*/AudioPlayer._audioInstance null;if (audio) {/** 先移除事件监听。** 避免清理过程中触发 onended / onerror。*/audio.onended null;audio.onerror null;try {audio.pause();} catch (error) {console.warn([AudioPlayer] 停止音频失败:,error);}try {audio.currentTime 0;} catch (error) {/** 音频还没有完成加载时* 设置 currentTime 在部分浏览器可能失败。** 不影响后续资源释放。*/console.warn([AudioPlayer] 重置播放进度失败:,error);}try {/** 解除音频资源。*/audio.removeAttribute(src);/** 让浏览器重新进入无媒体资源状态。*/audio.load();} catch (error) {console.warn([AudioPlayer] 释放 Audio 资源失败:,error);}}/** 如果当前音频是播放器自己创建的 Blob URL* 这里必须释放。*/AudioPlayer._revokeObjectUrl();}/*** 释放播放器自己创建的 Blob URL** private*/static _revokeObjectUrl() {if (!AudioPlayer._objectUrl) {return;}try {URL.revokeObjectURL(AudioPlayer._objectUrl);} catch (error) {console.warn([AudioPlayer] 释放 Blob URL 失败:,error);} finally {AudioPlayer._objectUrl null;}}}
返回列表