import Artplayer from "artplayer"; import type { ComponentOption, Selector } from "artplayer/types/component.js"; const TAG = "[artplayer-plugin-quality]:"; export const ArtPlayer_PLUGIN_QUALITY_KEY = "artplayer-plugin-quality"; export type ArtPlayerPluginQualityOption = { /** 播放器来自 */ from: "video" | "bangumi"; qualityList: { /** 画质显示的文字 */ html: string; /** 画质对应的链接 */ url: string; /** 画质代码,会根据画质代码进行排序 */ quality: number; /** 帧率信息 */ frameRate: string; /** 类型,一般是video/mp4 */ mimeType: string; /** 编码类型 */ codecid: number; /** 编码信息 */ codecs: string; /** 码率 */ bandwidth: number; }[]; }; export type ArtPlayerPluginQualityResult = { name: string; update(option: ArtPlayerPluginQualityOption): void; getCurrentQualityOption(): ArtPlayerPluginQualityOption["qualityList"]["0"] | null; }; export type ArtPlayerPluginQualityStorageOption = { /** 画质代码 */ quality: number; }; /** * 视频编码类型 */ const VideoCodingCodeMap = { AVC: 7, HEVC: 12, AV1: 13, }; /** * 视频编码 * * 播放策略 */ class VideoEncoding { art; from; $key = { SETTING_KEY: "video-playback-codeid", }; constructor(art: Artplayer, from: string) { this.art = art; this.from = from; this.updateSetting(); } /** * 更新设置菜单 * @param codeIdConfig 配置 */ updateSetting(codeIdConfig?: { /** 允许的画质编码列表 */ acceptCodeIdList?: (number | string)[]; /** 默认的画质编码(当不存在选择的画质编码时,将使用该值) */ defaultCodeId?: number; }) { // 先判断setting中是否存在该配置 let setting = this.getSetting(); if (Array.isArray(codeIdConfig?.acceptCodeIdList)) { for (let index = 0; index < setting.selector.length; index++) { const selectorItem = setting.selector[index]; let findIndex = codeIdConfig.acceptCodeIdList.findIndex( (item) => item.toString() === selectorItem.value.toString() ); if (findIndex === -1) { // 不存在该编码,移除 setting.selector.splice(index, 1); index--; } } // 重新处理defalut的值 let hasDefault = setting.selector.find((it) => it.default); if (!hasDefault && setting.selector.length) { // 由于默认画质编码id不存在 if (typeof codeIdConfig?.defaultCodeId === "number") { let findDefaultIndex = setting.selector.findIndex((it) => it.value === codeIdConfig.defaultCodeId); if (findDefaultIndex !== -1) { // 存在该编码,设置为默认 setting.selector[findDefaultIndex].default = true; setting.tooltip = setting.selector[findDefaultIndex].html; } else { // 不存在该编码,默认第一个 setting.selector[0].default = true; setting.tooltip = setting.selector[0].html; } } else { setting.selector[0].default = true; setting.tooltip = setting.selector[0].html; } } } if (this.art.setting.find(this.$key.SETTING_KEY)) { // 已有该菜单,更新 this.art.setting.update(setting); } else { // 还没有该菜单,添加 this.art.setting.add(setting); } } /** * 获取设置界面的配置 */ getSetting() { const that = this; let userChooseVideoCodingCode = this.getUserChooseVideoCodingCode(); let selectorList = [ { html: "AV1", value: VideoCodingCodeMap["AV1"], }, { html: "HEVC", value: VideoCodingCodeMap["HEVC"], }, { html: "AVC", value: VideoCodingCodeMap["AVC"], }, ].map((it) => Object.assign(it, { default: it.value === userChooseVideoCodingCode, }) ); let findValue = selectorList.find((it) => it.default); if (!findValue) { // 都没有,那么默认第一个 selectorList = selectorList.map((it, index) => { it.default = index === 0; return it; }); console.warn(TAG + "没有找到用户选择对应的画质编码,将使用排序第一个的画质:" + selectorList[0].html); } let tooltip = selectorList.find((it) => it.default)!; return { name: this.$key.SETTING_KEY, html: "播放策略", tooltip: tooltip.html, icon: ``, selector: selectorList, onSelect: function (item: any) { let videoCodingCode: number = item.value; // 切换编码 that.setCurrentVideoCodingCode(videoCodingCode); // 更新菜单 that.onSettingSelect(videoCodingCode); return item.html; }, }; } /** * 菜单选项选中后的回调 */ onSettingSelect(selectValue: number) { // TODO } get storageVideoCodingKey() { return `bili-${this.from}-artplayer-videoCodingCode`; } /** * 设置当前视频编码 */ setCurrentVideoCodingCode(videoCodingCode: number) { this.art.storage.set(this.storageVideoCodingKey, videoCodingCode); } /** * 获取用户选择的视频编码 */ getUserChooseVideoCodingCode() { // 遍历视频 let codingCode = (this.art.storage.get(this.storageVideoCodingKey) as number) || VideoCodingCodeMap.AV1; if (!Object.values(VideoCodingCodeMap).includes(codingCode)) { console.error( TAG + "意外情况,选择的编码格式不是允许的编码,将强制使用默认(av1),防止过滤掉的视频链接为空:" + codingCode ); codingCode = VideoCodingCodeMap.AV1; } return codingCode; } } class VideoQuality extends VideoEncoding { $data = { /** 请求到的视频画质信息数据 */ qualityOption: null as any as ArtPlayerPluginQualityOption, /** 处理后的画质列表 */ qualityOptionList: [], /** 请求到的视频的画质编码列表 */ qualityCodeIdList: [], /** 当前的画质编码id */ currentQualityCodecId: VideoCodingCodeMap["AV1"] as number | undefined, /** 当前选中的画质信息 */ currentSelectQualityInfo: null as { index: number; html: string; url: string; } | null, /** 当前选中的画质配置 */ currentQualityOption: null as any as ArtPlayerPluginQualityOption["qualityList"]["0"] | null, }; constructor(art: Artplayer, from: string) { super(art, from); } /** * 设置当前画质配置数据 */ setCurrentQualityOption(qualityOption: ArtPlayerPluginQualityOption["qualityList"]["0"] | null = null) { this.$data.currentQualityOption = null; this.$data.currentQualityOption = qualityOption; } /** * 获取存储键 */ getStorageKey(from: string) { return `artplayer-quality-${from}`; } /** * 更新画质信息 */ update(option: ArtPlayerPluginQualityOption) { // @ts-ignore this.$data.qualityOption = null; this.$data.qualityOption = option; this.$data.qualityOptionList.length = 0; this.$data.qualityCodeIdList.length = 0; this.$data.currentSelectQualityInfo = null; this.$data.currentQualityCodecId = void 0; if (import.meta.hot) { console.log(TAG + "更新画质信息:", option); } this.setCurrentQualityOption(); if (option.qualityList.length) { // 如果存在画质选项 // 更新画质信息 let currentSelectQualityInfo = this.getQualityInfo(); this.addControls(); super.updateSetting({ acceptCodeIdList: this.$data.qualityCodeIdList, defaultCodeId: this.$data.currentQualityCodecId, }); // 设置播放地址 this.art.url = currentSelectQualityInfo.url; } else { this.removeControls(); } } /** * 获取面板配置 */ getControlsOption(): ComponentOption { const that = this; // 调整一下select的default的值 let selectorList: (Selector & ArtPlayerPluginQualityOption["qualityList"]["0"])[] = this.$data.qualityOptionList.map((itemInfo, index) => { return { default: index === this.$data.currentSelectQualityInfo?.index, html: itemInfo.html, url: itemInfo.url, quality: itemInfo.quality, frameRate: itemInfo.frameRate, mimeType: itemInfo.mimeType, codecid: itemInfo.codecid, codecs: itemInfo.codecs, bandwidth: itemInfo.bandwidth, }; }); const controlsOption: ComponentOption = { name: ArtPlayer_PLUGIN_QUALITY_KEY, index: 10, position: "right", html: this.$data.currentSelectQualityInfo!.html, selector: selectorList, onSelect: function (selector) { let itemInfo = selector as Selector & ArtPlayerPluginQualityOption["qualityList"]["0"]; // 切换画质 console.log(TAG + "切换画质", itemInfo); that.art.switchQuality(itemInfo.url); // 保存切换的画质 that.art.storage.set(that.getStorageKey(that.$data.qualityOption.from), { quality: itemInfo.quality, } as ArtPlayerPluginQualityStorageOption); that.setCurrentQualityOption({ html: itemInfo.html, url: itemInfo.url, quality: itemInfo.quality, frameRate: itemInfo.frameRate, mimeType: itemInfo.mimeType, codecid: itemInfo.codecid, codecs: itemInfo.codecs, bandwidth: itemInfo.bandwidth, }); return selector.html; }, }; return controlsOption; } /** * 添加面板 */ addControls() { if (this.isAddControls()) { this.updateQualityControls(); } else { let controlOption = this.getControlsOption(); this.art.controls.add(controlOption); } } /** * 更新画质信息 */ getQualityInfo() { // 获取选择的编码 let userChooseVideoCodingCode = this.getUserChooseVideoCodingCode(); // 按编码筛选画质 let qualityList = this.$data.qualityOption.qualityList.filter((item) => item.codecid === userChooseVideoCodingCode); // 按画质排序(降序) qualityList.sort((leftItem, rightItem) => { return rightItem.quality - leftItem.quality; }); // 根据画质编码分类映射字典 const qualityListMap: { [key: number | string]: ArtPlayerPluginQualityOption["qualityList"]; } = {}; for (let index = 0; index < this.$data.qualityOption.qualityList.length; index++) { const qualityItem = this.$data.qualityOption.qualityList[index]; const qualityMapValue = qualityListMap[qualityItem.codecid] || []; qualityMapValue.push(qualityItem); qualityListMap[qualityItem.codecid] = qualityMapValue; } if (qualityList.length === 0) { // 未登录的情况 // mp4的情况 // 又或者是不存在该编码的画质 qualityList = Object.values(qualityListMap)[0]; this.$data.currentQualityCodecId = qualityList[0].codecid; console.warn(TAG + "该画质:" + userChooseVideoCodingCode + "不存在,将使用第一个画质", qualityList); } this.$data.qualityOptionList = []; this.$data.qualityOptionList = qualityList; this.$data.qualityCodeIdList = Object.keys(qualityListMap); let firstQualityInfo = qualityList[0]; // 存储键 const storageKey = this.getStorageKey(this.$data.qualityOption.from); // 获取本地存储的上次选的画质,默认最高画质 const storageQualityInfo = this.art.storage.get(storageKey) as ArtPlayerPluginQualityStorageOption; // 根据本地保存的记录和当前提供的画质列表筛选出当前的画质 let currentSelectQualityInfo = { index: 0, html: firstQualityInfo?.html, /** 播放的地址 */ url: firstQualityInfo?.url, }; this.setCurrentQualityOption(qualityList[0]); if (storageQualityInfo) { // 判断当前的画质中是否命中上次选择的画质,若未命中,默认使用第一个画质(最高画质) const findQualityIndex = qualityList.findIndex((item) => item.quality === storageQualityInfo.quality); if (findQualityIndex !== -1) { const findQuality = qualityList[findQualityIndex]; currentSelectQualityInfo.index = findQualityIndex; currentSelectQualityInfo.url = findQuality.url; currentSelectQualityInfo.html = findQuality.html; this.setCurrentQualityOption(findQuality); } else { console.warn(TAG + "没有找到上次选的画质,使用当前默认第一个画质"); } } this.$data.currentSelectQualityInfo = null; this.$data.currentSelectQualityInfo = currentSelectQualityInfo; return currentSelectQualityInfo; } /** * 更新画质切换面板 */ updateQualityControls() { let controlOption = this.getControlsOption(); console.log(TAG + "更新画质切换面板信息", this.$data.qualityOptionList, this.$data.currentQualityOption); this.art.controls.update(controlOption); } /** * 移除画质切换面板 */ removeControls() { if (this.isAddControls()) { this.art.controls.remove(ArtPlayer_PLUGIN_QUALITY_KEY); } } /** * 是否已经添加了面板 */ isAddControls() { return Reflect.has(this.art.controls, ArtPlayer_PLUGIN_QUALITY_KEY); } onSettingSelect(selectValue: number): void { this.getQualityInfo(); this.updateQualityControls(); this.updateSetting({ acceptCodeIdList: this.$data.qualityCodeIdList, defaultCodeId: this.$data.currentQualityCodecId, }); if (this.$data.currentSelectQualityInfo) { // 更新播放地址 this.art.url = this.$data.currentSelectQualityInfo.url; } } } /** * 画质切换工具 * @param option */ export const artplayPluginQuality = (option: ArtPlayerPluginQualityOption) => { return (art: Artplayer) => { let videoQuality = new VideoQuality(art, option.from); videoQuality.update(option); return { name: ArtPlayer_PLUGIN_QUALITY_KEY, update(option: ArtPlayerPluginQualityOption) { videoQuality.update(option); }, getCurrentQualityOption() { return videoQuality.$data.currentQualityOption; }, } as ArtPlayerPluginQualityResult; }; };