diff --git a/src/components/video/player-agent/bangumi.ts b/src/components/video/player-agent/bangumi.ts index fd43c9e080..9759562cbd 100644 --- a/src/components/video/player-agent/bangumi.ts +++ b/src/components/video/player-agent/bangumi.ts @@ -59,46 +59,18 @@ export class BangumiPlayerAgent extends PlayerAgent { super() bpxPlayerPolyfill() } - isMute() { - const icon = this.query.control.buttons.volume.sync() as HTMLElement - return icon?.classList.contains('squirtle-volume-mute-state') ?? false - } - changeVolume(change: number) { - const video = this.query.video.element.sync() as HTMLVideoElement - if (!video) { - return null - } - video.volume = lodash.clamp(video.volume + change / 100, 0, 1) - return Math.round(video.volume * 100) - } + seek(time: number) { - const video = this.query.video.element.sync() as HTMLVideoElement - if (!video) { - return null + const seekResult = super.seek(time) + if (seekResult === null) { + return seekResult } - video.play() setTimeout(() => { - video.currentTime = lodash.clamp(time, 0, video.duration) const toastText = dq('.bpx-player-toast-row .bpx-player-toast-item .bpx-player-toast-text') if (toastText?.textContent?.startsWith('已为您定位至')) { toastText.textContent = '已为您定位至00:00' } }) - return video.currentTime - } - changeTime(change: number) { - const video = this.query.video.element.sync() as HTMLVideoElement - if (!video) { - return null - } - video.currentTime = lodash.clamp(video.currentTime + change, 0, video.duration) - return video.currentTime - } - async toggleLight(on: boolean) { - const checkbox = this.query.control.settings.lightOff.sync() - // 开灯状态 && 关灯 -> 关灯 - !checkbox.classList.contains('active') && !on && checkbox.click() - // 关灯状态 && 开灯 -> 开灯 - checkbox.classList.contains('active') && on && checkbox.click() + return seekResult } } diff --git a/src/components/video/player-agent/base.ts b/src/components/video/player-agent/base.ts index e24264fb43..b482e6d3dc 100644 --- a/src/components/video/player-agent/base.ts +++ b/src/components/video/player-agent/base.ts @@ -1,3 +1,4 @@ +/* eslint-disable class-methods-use-this */ import { select } from '@/core/spin-query' import { raiseEvent } from '@/core/utils' import { @@ -29,9 +30,21 @@ export const click = (target: ElementQuery) => { button?.click() return button } -export abstract class PlayerAgent { + +export enum PlayerAgentEventTypes { + Play = 'play', +} +export abstract class PlayerAgent + extends EventTarget + implements EnumEventTarget<`${PlayerAgentEventTypes}`> +{ abstract type: AgentType abstract query: PlayerQuery + + constructor() { + super() + } + provideCustomQuery>( config: CustomQueryProvider, ) { @@ -71,19 +84,27 @@ export abstract class PlayerAgent { /** true 开灯,false 关灯 */ async toggleLight(on?: boolean) { - const checkbox = (await this.query.control.settings.lightOff()) as HTMLInputElement + if (!this.nativeApi) { + return null + } + const isCurrentLightOff = this.nativeApi.getLightOff() // 无指定参数, 直接 toggle if (on === undefined) { - checkbox.click() - return + this.nativeApi.setLightOff(!isCurrentLightOff) + return !isCurrentLightOff } // 关灯状态 && 要开灯 -> 开灯 - checkbox.checked && on && checkbox.click() - // 开灯状态 && 要关灯 -> 关灯 - !checkbox.checked && !on && checkbox.click() + if (on && isCurrentLightOff) { + this.nativeApi.setLightOff(true) + return true + } + if (!on && !isCurrentLightOff) { + this.nativeApi.setLightOff(false) + return false + } + return null } - // eslint-disable-next-line class-methods-use-this getPlayerConfig(target: string) { return lodash.get(JSON.parse(localStorage.getItem('bilibili_player_settings')), target, false) } @@ -92,11 +113,112 @@ export abstract class PlayerAgent { return this.getPlayerConfig('video_status.autoplay') } - abstract isMute(): boolean + // https://github.com/the1812/Bilibili-Evolved/discussions/4341 + get nativeApi() { + return unsafeWindow.player || window.playerRaw + } + + get nanoApi() { + return unsafeWindow.nano + } + + private eventHandlerMap = new Map< + EventListenerOrEventListenerObject, + { + handler: () => void + options: boolean | AddEventListenerOptions + } + >() + + addEventListener( + type: `${PlayerAgentEventTypes}`, + callback: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ): void { + super.addEventListener(type, callback, options) + switch (type) { + default: { + break + } + case PlayerAgentEventTypes.Play: { + const handler = () => { + this.dispatchEvent(new Event(PlayerAgentEventTypes.Play)) + } + if (typeof options === 'object' && options.once) { + this.nativeApi.once(this.nanoApi.EventType.Player_Initialized, handler) + } else { + this.nativeApi.on(this.nanoApi.EventType.Player_Initialized, handler) + } + this.eventHandlerMap.set(callback, { + handler, + options, + }) + } + } + } + + removeEventListener( + type: `${PlayerAgentEventTypes}`, + callback: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void { + super.removeEventListener(type, callback, options) + switch (type) { + default: { + break + } + case PlayerAgentEventTypes.Play: { + const handlerData = this.eventHandlerMap.get(callback) + if (!handlerData || lodash.isEqual(options, handlerData.options)) { + break + } + this.nativeApi.off(this.nanoApi.EventType.Player_Initialized, handlerData.handler) + } + } + } + + /** 获取是否静音 */ + isMute() { + if (!this.nativeApi) { + return null + } + if (this.nativeApi.isMuted) { + return this.nativeApi.isMuted() + } + return this.nativeApi.isMute() + } /** 更改音量 (%) */ - abstract changeVolume(change: number): number + changeVolume(change: number) { + if (!this.nativeApi) { + return null + } + if (this.nativeApi.getVolume) { + const current = this.nativeApi.getVolume() + this.nativeApi.setVolume(current + change / 100) + return Math.round(this.nativeApi.getVolume() * 100) + } + const current = this.nativeApi.volume() + this.nativeApi.volume(current + change / 100) + return Math.round(this.nativeApi.volume() * 100) + } /** 跳转到指定时间 */ - abstract seek(time: number): number + seek(time: number) { + if (!this.nativeApi) { + return null + } + this.nativeApi.seek(time) + return this.nativeApi.getCurrentTime() as number + } /** 更改时间 */ - abstract changeTime(change: number): number + changeTime(change: number) { + if (!this.nativeApi) { + return null + } + const video = this.query.video.element.sync() as HTMLVideoElement + if (!video) { + return null + } + this.nativeApi.seek(video.currentTime + change, video.paused) + return this.nativeApi.getCurrentTime() + } } diff --git a/src/components/video/player-agent/video-player-mixed.ts b/src/components/video/player-agent/video-player-mixed.ts index 79460d2502..d7001af87c 100644 --- a/src/components/video/player-agent/video-player-mixed.ts +++ b/src/components/video/player-agent/video-player-mixed.ts @@ -3,6 +3,7 @@ import { selectorWrap } from './base' import { PlayerQuery, ElementQuery } from './types' import { VideoPlayerV2Agent } from './video-player-v2' +/** 兼容旧版播放器和 BPX 播放器的 VideoPlayerAgent */ export class VideoPlayerMixedAgent extends VideoPlayerV2Agent { query = selectorWrap({ playerWrap: '.player-wrap', @@ -63,21 +64,4 @@ export class VideoPlayerMixedAgent extends VideoPlayerV2Agent { this.checkBwpVideo() v3PlayerPolyfill() } - - seek(time: number) { - if (!this.nativeApi) { - return null - } - this.nativeApi.play() - setTimeout(() => { - this.nativeApi.seek(time) - const toastText = dq( - '.bilibili-player-video-toast-bottom .bilibili-player-video-toast-item:first-child .bilibili-player-video-toast-item-text span:nth-child(2)', - ) - if (toastText) { - toastText.textContent = ' 00:00' - } - }) - return this.nativeApi.getCurrentTime() - } } diff --git a/src/components/video/player-agent/video-player-v2.ts b/src/components/video/player-agent/video-player-v2.ts index e32889683c..b44cf08d36 100644 --- a/src/components/video/player-agent/video-player-v2.ts +++ b/src/components/video/player-agent/video-player-v2.ts @@ -4,10 +4,6 @@ import { PlayerAgent, selectorWrap } from './base' import { AgentType, PlayerQuery, ElementQuery } from './types' export class VideoPlayerV2Agent extends PlayerAgent { - // eslint-disable-next-line class-methods-use-this - get nativeApi() { - return unsafeWindow.player - } type: AgentType = 'video' query = selectorWrap({ playerWrap: '.player-wrap', @@ -89,35 +85,12 @@ export class VideoPlayerV2Agent extends PlayerAgent { })() } - isMute() { - if (!this.nativeApi) { - return null - } - if (this.nativeApi.isMuted) { - return this.nativeApi.isMuted() - } - return this.nativeApi.isMute() - } - changeVolume(change: number) { - if (!this.nativeApi) { - return null - } - if (this.nativeApi.getVolume) { - const current = this.nativeApi.getVolume() - this.nativeApi.setVolume(current + change / 100) - return Math.round(this.nativeApi.getVolume() * 100) - } - const current = this.nativeApi.volume() - this.nativeApi.volume(current + change / 100) - return Math.round(this.nativeApi.volume() * 100) - } seek(time: number) { - if (!this.nativeApi) { - return null + const seekResult = super.seek(time) + if (seekResult === null) { + return seekResult } - this.nativeApi.play() setTimeout(() => { - this.nativeApi.seek(time) const toastText = dq( '.bilibili-player-video-toast-bottom .bilibili-player-video-toast-item:first-child .bilibili-player-video-toast-item-text span:nth-child(2)', ) @@ -125,17 +98,6 @@ export class VideoPlayerV2Agent extends PlayerAgent { toastText.textContent = ' 00:00' } }) - return this.nativeApi.getCurrentTime() - } - changeTime(change: number) { - if (!this.nativeApi) { - return null - } - const video = this.query.video.element.sync() as HTMLVideoElement - if (!video) { - return null - } - this.nativeApi.seek(video.currentTime + change, video.paused) - return this.nativeApi.getCurrentTime() + return seekResult } } diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 7c292f2296..90fad90312 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -205,8 +205,7 @@ export const isNotHtml = () => document.contentType !== 'text/html' * @param eventName 事件名称 */ export const raiseEvent = (element: HTMLElement, eventName: string) => { - const event = document.createEvent('HTMLEvents') - event.initEvent(eventName, true, true) + const event = new Event(eventName) element.dispatchEvent(event) } /** 根据图片URL生成 `srcset`, 范围从 `@1x` 至 `@4x`, 每 `0.25x` 产生一个 `src`