import { log, utils } from "@/env"; import type Artplayer from "artplayer"; import type { Selector } from "artplayer/types/component.js"; import type { Events, Setting } from "artplayer"; import { unsafeWindow } from "ViteGM"; const TAG = "[artplayer-plugin-m4sAudioSupport]:"; /** 控制面板key */ const ArtPlayer_PLUGIN_M4S_SUPPORT_SETTING_KEY = "setting-bilibili-m4sAudio"; export type ArtPlayerPluginM4SAudioSupportEvent = | "m4sAudio:loadedmetadata" | "m4sAudio:canplaythrough" | "m4sAudio:error" | "m4sAudio:restart" | "m4sAudio:pause" | "m4sAudio:syncPlayBackRate" | "m4sAudio:syncMuted" | "m4sAudio:syncVolume" | "m4sAudio:syncTime" | "m4sAudio:syncPlayState" | "m4sAudio:play"; export type ArtPlayerPluginM4SAudioSupportOption = { /** 来源 */ from: "video" | "bangumi"; /** 音频列表信息,为空就不播放 */ audioList: { /** 音频链接 */ url: string; /** 音质代码数字 */ soundQualityCode: number; /** 音质代码数字对应的文字 */ soundQualityCodeText: string; /** 类型,一般是video/mp4 */ mimeType: string; /** 编码信息 */ codecs: string; /** 带宽 */ bandwidth: number; /** 文件大小 */ size: number; }[]; /** 是否显示选择音频的菜单 @default true */ showSetting: boolean; /** 视频切换时触发该回调,可以更新audio信息 */ onRestart?: (url: string) => string; }; type ArtPlayerPluginM4SAudioSupportUpdateOption = { from: ArtPlayerPluginM4SAudioSupportOption["from"]; audioList: ArtPlayerPluginM4SAudioSupportOption["audioList"]; }; type ArtPlayerPluginM4SAudioSupportStorageOption = { /** 音频质量代码 */ soundQualityCode: number; }; export type ArtPlayerPluginM4SAudioSupportResult = { name: string; /** 主动更新音频 */ update(option: ArtPlayerPluginM4SAudioSupportUpdateOption): void; /** 获取Audio元素 */ getAudio(): HTMLAudioElement; /** 获取当前播放的音频信息 */ getCurrentPlayConfig(): ArtPlayerPluginM4SAudioSupportOption["audioList"]["0"] | undefined; }; const M4SAudioUtils = { $flag: { /** * 是否正在循环中 */ isIntervaling: false, }, /** * 自定义某个函数执行N次和间隔时间 * @param fn 需要执行的函数 * @param count 重复执行的次数 * @param delayTime 重复执行的间隔时间 */ intervalHandler(fn: Function, count: number = 2, delayTime: number = 900) { if (M4SAudioUtils.$flag.isIntervaling) { return; } M4SAudioUtils.$flag.isIntervaling = true; /** 已经循环的次数 */ let intervalCount = 0; let intervalId: undefined | number = void 0; let callback = () => { if (intervalCount > count) { M4SAudioUtils.$flag.isIntervaling = false; clearInterval(intervalId); return; } if (typeof fn === "function") { try { fn(); } catch (error) { console.error(TAG, error); } } intervalCount++; }; callback(); if (count > 1) { // 2次以上(包括2次)就循环 intervalId = setInterval(callback, delayTime); } else { M4SAudioUtils.$flag.isIntervaling = false; } }, }; const M4SAudio = { $key: { plugin_KEY: "plugin-bilibili-m4sAudio", }, $data: { /** artplayer实例 */ art: null as any as Artplayer, /** 播放的音频 */ audio: new Audio(), /** 上次同步的所在的进度 */ latestSyncTime: 0, /** 音频的重新连接的配置 */ reconnectConfig: { /** 最大连接的次数 */ maxCount: 5, /** 尝试重新连接的间隔时间 */ delayTime: 1000, }, /** 音频的重新连接的信息 */ reconnectInfo: { /** 重新连接的链接url */ url: "", /** 已失败连接的次数 */ count: 0, }, option: null as any as ArtPlayerPluginM4SAudioSupportOption["audioList"], }, userEvent: { onRestart: void 0 as ((url: string) => string) | void, }, events: { /** * artplayer 播放 * * 同步进度 - 同步音量 - 播放音频 */ play: () => { if (import.meta.hot) { console.log(TAG + "play"); } M4SAudio.handler.play(); M4SAudio.handler.syncVolume(); M4SAudio.handler.syncMuted(); M4SAudioUtils.intervalHandler(() => { M4SAudio.handler.syncTime(); }, 1); }, /** * 视频进度更新(主动改变的,而不是播放的改变) * * 音频同步进度 * @param currentTime 当前的进度 */ seek: (currentTime) => { if (import.meta.hot) { console.log(TAG + "seek:" + currentTime); } M4SAudioUtils.intervalHandler(() => { M4SAudio.handler.syncTime(); M4SAudio.handler.syncPlayState(); }); }, /** * 视频暂停 * * 音频暂停 */ pause: () => { if (import.meta.hot) { console.log(TAG + "pause"); } M4SAudioUtils.intervalHandler(() => { M4SAudio.handler.syncTime(); }, 1); M4SAudio.handler.pause(); }, /** * 视频重载,这里的音频也重载 * * 触发回调 - 获取新的音频 - 同步进度 * @param url */ restart: (url) => { if (import.meta.hot) { console.log(TAG + "restart", url); } if (typeof M4SAudio.userEvent.onRestart === "function") { let newAudioUrl = M4SAudio.userEvent.onRestart(url); // 更新audio的url M4SAudio.handler.playUrl(newAudioUrl); } M4SAudioUtils.intervalHandler(() => { M4SAudio.handler.syncTime(); }, 1); M4SAudio.handler.syncPlayState(); }, /** * 静音状态改变 * @param state */ muted: (state) => { if (import.meta.hot) { console.log(TAG + "muted", state); } M4SAudio.handler.syncVolume(); M4SAudio.handler.syncMuted(); }, /** * artplayer 销毁 * * 音频暂停 */ destroy: () => { if (import.meta.hot) { console.log(TAG + "destory"); } M4SAudio.handler.pause(); }, /** * 视频出岔子了无法播放 * * 音频暂停 - 同步进度 * @param error * @param reconnectTime */ error: (error, reconnectTime) => { if (import.meta.hot) { console.log(TAG + "error", error, reconnectTime); } M4SAudio.handler.pause(); }, /** * 当播放器尺寸变化时触发 * * 可能会音视频不停步 */ resize: () => { if (import.meta.hot) { console.log(TAG + "resize"); } M4SAudioUtils.intervalHandler(() => { M4SAudio.handler.syncTime(); }); }, /** * 当播放器发生窗口全屏时触发 * * 可能会音视频不停步 */ fullscreen: () => { if (import.meta.hot) { console.log("fullscreen"); } M4SAudioUtils.intervalHandler(() => { M4SAudio.handler.syncTime(); }); }, /** * 视频播放完毕 * * 音频暂停 */ "video:ended": () => { if (import.meta.hot) { console.log(TAG + "video:ended"); } M4SAudio.handler.pause(); }, /** * 视频倍速改变 * * 同步视频的倍速 */ "video:ratechange": () => { if (import.meta.hot) { console.log(TAG + "video:ratechange"); } M4SAudio.handler.syncPlayBackRate(); }, /** * 视频缓冲暂停 * * 音频暂停 然后同步进度 */ "video:waiting": () => { if (import.meta.hot) { console.log(TAG + "video:waiting"); } M4SAudio.handler.pause(); }, /** * 视频缓冲恢复,音频也恢复 */ "video:playing": () => { if (import.meta.hot) { console.log(TAG + "video:playing"); } M4SAudioUtils.intervalHandler(() => { M4SAudio.handler.syncTime(); }, 1); M4SAudio.handler.play(); }, /** * 切换页面视频会被暂停 */ "video:pause": () => { if (import.meta.hot) { console.log(TAG + "video:pause"); } M4SAudio.handler.pause(); M4SAudioUtils.intervalHandler(() => { M4SAudio.handler.syncTime(); }, 1); }, /** * 同步音量 */ "video:volumechange": () => { if (import.meta.hot) { console.log(TAG + "video:volumechange"); } M4SAudio.handler.syncVolume(); M4SAudio.handler.syncMuted(); // 如果video设置了autoplay,那么在play函数中可能无法让Audio播放,尤其是在移动端webview上 // 这时候手动取消静音成功,但因为audio未播放,所以需要同步播放状态 // 这里需要同步播放状态 M4SAudio.handler.syncPlayState(); }, /** * 视频更新进度 */ "video:timeupdate": () => { let videoTime = M4SAudio.$data.art.currentTime; if (Math.abs(videoTime - M4SAudio.$data.latestSyncTime) >= 3) { // 每3秒同步一次进度 M4SAudio.$data.latestSyncTime = videoTime; M4SAudioUtils.intervalHandler(() => { M4SAudio.handler.syncTime(0.777); }, 1); } }, } as { [key in keyof Events]?: (...args: Events[key]) => unknown; }, audioEvents: { loadedmetadata: (event) => { M4SAudio.$data.art.emit( // @ts-ignore "m4sAudio:loadedmetadata" as ArtPlayerPluginM4SAudioSupportEvent, event ); console.log(TAG + "Audio预加载完成"); // 重置重连信息 M4SAudio.$data.reconnectInfo.count = 0; M4SAudio.$data.reconnectInfo.url = ""; M4SAudio.$data.latestSyncTime = 0; // 同步音量、进度 M4SAudio.handler.syncPlayState(); M4SAudio.handler.syncPlayBackRate(); M4SAudio.handler.syncVolume(); M4SAudio.handler.syncMuted(); M4SAudioUtils.intervalHandler(() => { M4SAudio.handler.syncTime(); }); }, canplaythrough: (event) => { M4SAudio.$data.art.emit( // @ts-ignore "m4sAudio:canplaythrough" as ArtPlayerPluginM4SAudioSupportEvent, event ); console.log(TAG + "浏览器估计该音频可以在不停止内容缓冲的情况下播放媒体直到结束"); // 同步进度 M4SAudioUtils.intervalHandler(() => { M4SAudio.handler.syncTime(); }); }, error: (event) => { M4SAudio.$data.art.emit( // @ts-ignore "m4sAudio:error" as ArtPlayerPluginM4SAudioSupportEvent, event ); console.error(TAG + `Audio加载失败`, event); if (utils.isNull(M4SAudio.$data.reconnectInfo.url)) { M4SAudio.$data.reconnectInfo.url = M4SAudio.$data.audio.src; } if (M4SAudio.$data.reconnectInfo.count < M4SAudio.$data.reconnectConfig.maxCount) { // 还在允许重新连接次数的范围之内 console.log(TAG + `Audio第${M4SAudio.$data.reconnectInfo.count + 1}次尝试重新连接`); // 左上角notice提示 M4SAudio.$data.art.notice.show = `Audio第${M4SAudio.$data.reconnectInfo.count + 1}次尝试重新连接`; M4SAudio.$data.reconnectInfo.count++; setTimeout(() => { M4SAudio.handler.playUrl(""); M4SAudio.handler.playUrl(M4SAudio.$data.reconnectInfo.url); M4SAudio.$data.audio.load(); }, M4SAudio.$data.reconnectConfig.delayTime); } else { // 已超出重连次数 console.error(TAG + `Audio已超出重连次数`); M4SAudio.$data.art.notice.show = `Audio已超出重连次数,请尝试切换源`; } }, } as { [key in keyof HTMLMediaElementEventMap]?: (ev: HTMLMediaElementEventMap[key]) => any; }, /** * 音频工具处理 */ handler: { /** * 播放音频链接,会自行处理判断是否是字符串链接 */ playUrl(url: any) { if (typeof url !== "string") { return; } M4SAudio.$data.audio.src = url; M4SAudio.unbindAudio(); if (utils.isNotNull(url)) { M4SAudio.bindAudio(); } M4SAudio.$data.art.emit( // @ts-ignore "m4sAudio:restart" as ArtPlayerPluginM4SAudioSupportEvent, url ); M4SAudio.handler.syncTime(); M4SAudio.handler.syncPlayState(); }, /** 播放音频 */ play() { if (M4SAudio.$data.audio.paused) { M4SAudio.$data.audio.play(); M4SAudio.$data.art.emit( // @ts-ignore "m4sAudio:play" as ArtPlayerPluginM4SAudioSupportEvent ); } }, /** 暂停音频 */ pause() { if (!M4SAudio.$data.audio.paused) { M4SAudio.$data.audio.pause(); M4SAudio.$data.art.emit( // @ts-ignore "m4sAudio:pause" as ArtPlayerPluginM4SAudioSupportEvent ); } }, /** 同步播放状态 */ syncPlayState() { if (M4SAudio.$data.art.playing) { // 视频播放中 this.play(); } else { // 视频暂停 this.pause(); } M4SAudio.$data.art.emit( // @ts-ignore "m4sAudio:syncPlayState" as ArtPlayerPluginM4SAudioSupportEvent ); }, /** * 音频同步视频进度 * @param offset 差距大小 */ syncTime(offset: number = 0.1) { let videoTime = M4SAudio.$data.art.currentTime; let audioTime = M4SAudio.$data.audio.currentTime; if (Math.abs(videoTime - audioTime) >= Math.abs(offset)) { // 视频进度和音频进度的差距超过设定的偏移 M4SAudio.$data.audio.currentTime = videoTime; // 同步音量 this.syncVolume(); // 同步静音状态 this.syncMuted(); if (import.meta.hot) { console.log(TAG + `同步进度,视频:${videoTime} 音频:${audioTime} 差距:${videoTime - audioTime}`); } M4SAudio.$data.art.emit( // @ts-ignore "m4sAudio:syncTime" as ArtPlayerPluginM4SAudioSupportEvent ); } else { // console.warn( // TAG + // `不同步进度,视频:${videoTime} 音频:${audioTime} 差距:${ // videoTime - audioTime // }` // ); } }, /** 同步音量 */ syncVolume() { M4SAudio.$data.audio.volume = M4SAudio.$data.art.volume; M4SAudio.$data.art.emit( // @ts-ignore "m4sAudio:syncVolume" as ArtPlayerPluginM4SAudioSupportEvent ); }, /** 同步静音状态 */ syncMuted() { let artMuted = M4SAudio.$data.art.muted; // 同步 M4SAudio.$data.audio.muted = artMuted; M4SAudio.$data.art.emit( // @ts-ignore "m4sAudio:syncMuted" as ArtPlayerPluginM4SAudioSupportEvent ); }, /** 同步播放倍速 */ syncPlayBackRate() { let artPlayBackRate = M4SAudio.$data.art.playbackRate; let audioPlayBackRate = M4SAudio.$data.audio.playbackRate; if (artPlayBackRate !== audioPlayBackRate) { // 同步 M4SAudio.$data.audio.playbackRate = artPlayBackRate; M4SAudio.$data.art.emit( // @ts-ignore "m4sAudio:syncPlayBackRate" as ArtPlayerPluginM4SAudioSupportEvent ); } }, }, /** * 更新 * @param audioList */ update(option: ArtPlayerPluginM4SAudioSupportUpdateOption) { this.unbind(); this.unbindAudio(); // @ts-ignore this.$data.option = null; this.$data.option = option.audioList; this.$data.latestSyncTime = 0; const that = this; if (option.audioList?.length) { // 按音频质量排序(降序) option.audioList.sort((leftItem, rightItem) => { return rightItem.bandwidth - leftItem.bandwidth; }); // 如果存在audio选项 let firstAudioInfo = option.audioList[0]; // 存储键 const storageKey = `artplayer-m4s-audio-${option.from}`; // 获取本地存储的上次选的画质,默认最高画质 const storageAudioInfo = this.$data.art.storage.get(storageKey) as ArtPlayerPluginM4SAudioSupportStorageOption; // 根据本地保存的记录和当前提供的音频列表筛选出当前的音频 let currentSelectAudioInfo = { index: 0, html: firstAudioInfo.soundQualityCodeText, /** 播放的地址 */ url: firstAudioInfo.url, }; if (storageAudioInfo) { // 判断当前的音频中是否同样的音频,存在的话就使用这个音频,默认第一个音频的 const findAudioIndex = option.audioList.findIndex( (item) => item.soundQualityCode === storageAudioInfo.soundQualityCode ); if (findAudioIndex !== -1) { const findAudio = option.audioList[findAudioIndex]; currentSelectAudioInfo.index = findAudioIndex; currentSelectAudioInfo.url = findAudio.url; currentSelectAudioInfo.html = findAudio.soundQualityCodeText; } else { console.warn(TAG + "没有找到上次选的音频代码,使用当前默认第一个音频"); } } // 调整一下select的default的值 let selectorList: (Selector & ArtPlayerPluginM4SAudioSupportOption["audioList"]["0"])[] = option.audioList.map( (item, index) => { return { default: index === currentSelectAudioInfo.index, html: item.soundQualityCodeText, url: item.url, soundQualityCode: item.soundQualityCode, soundQualityCodeText: item.soundQualityCodeText, codecs: item.codecs, mimeType: item.mimeType, bandwidth: item.bandwidth, size: item.size, }; } ); // 判断面板是否存在 // 存在就更新 // 不存在就添加 const settingOption = { name: ArtPlayer_PLUGIN_M4S_SUPPORT_SETTING_KEY, width: 200, html: "音频", tooltip: currentSelectAudioInfo.html, icon: /*html*/ ` `, selector: selectorList, onSelect: function (selector) { let itemInfo = selector as any as Selector & ArtPlayerPluginM4SAudioSupportOption["audioList"]["0"]; // 切换音频 console.log(TAG + "切换音频", itemInfo); that.handler.playUrl(itemInfo.url); // 保存切换的音频 that.$data.art.storage.set(storageKey, { soundQualityCode: itemInfo.soundQualityCode, } as ArtPlayerPluginM4SAudioSupportStorageOption); return selector.html; }, } as Setting; let findSettingValue = M4SAudio.$data.art.setting.find(ArtPlayer_PLUGIN_M4S_SUPPORT_SETTING_KEY); if (findSettingValue) { // 已存在面板,更新 M4SAudio.$data.art.setting.update(settingOption); } else { // 不存在面板,添加 M4SAudio.$data.art.setting.add(settingOption); } // 设置播放地址 log.info("加载m4s的音频:", currentSelectAudioInfo); M4SAudio.handler.playUrl(currentSelectAudioInfo.url); this.bind(); this.bindAudio(); } else { // 没有audio M4SAudio.handler.playUrl(""); // 移除旧的菜单 let oldSetting = M4SAudio.$data.art.setting.option.find( (item) => item.name === ArtPlayer_PLUGIN_M4S_SUPPORT_SETTING_KEY ); if (oldSetting) { M4SAudio.$data.art.setting.remove(ArtPlayer_PLUGIN_M4S_SUPPORT_SETTING_KEY); } } }, /** * 绑定事件 */ bind() { Object.keys(this.events).forEach((eventName) => { this.$data.art.on(eventName as keyof Events, (this.events as any)[eventName as keyof Events]); }); }, /** * 绑定音频事件 */ bindAudio() { Object.keys(this.audioEvents).forEach((eventName) => { this.$data.audio.addEventListener(eventName, (this.audioEvents as any)[eventName], { once: true, }); }); }, /** * 取消绑定事件 */ unbind() { Object.keys(this.events).forEach((eventName) => { this.$data.art.off(eventName as keyof Events, (this.events as any)[eventName as keyof Events]); }); }, /** * 取消绑定音频事件 */ unbindAudio() { Object.keys(this.audioEvents).forEach((eventName) => { this.$data.audio.removeEventListener(eventName, (this.audioEvents as any)[eventName]); }); }, }; if (import.meta.hot) { Reflect.set(unsafeWindow, "M4SAudio", M4SAudio); } /** * m4s视频的音频支持 */ export const artplayerPluginM4SAudioSupport = (option: ArtPlayerPluginM4SAudioSupportOption) => { return (art: Artplayer): ArtPlayerPluginM4SAudioSupportResult => { M4SAudio.$data.art = art; if (typeof option.onRestart === "function") { M4SAudio.userEvent.onRestart = option.onRestart; } M4SAudio.update({ from: option.from, audioList: option.audioList, }); return { name: M4SAudio.$key.plugin_KEY, update(...args) { M4SAudio.update(...args); M4SAudio.handler.syncVolume(); M4SAudio.handler.syncMuted(); M4SAudio.handler.syncTime(); }, getAudio() { return M4SAudio.$data.audio; }, getCurrentPlayConfig() { return M4SAudio.$data.option.find((it) => it.url === M4SAudio.$data.audio.src); }, }; }; }; /** * 插件id */ export const ArtPlayer_PLUGIN_M4S_AUDIO_SUPPORT_KEY = M4SAudio.$key.plugin_KEY;