Bilibili-Evolved/registry/lib/components/video/player/common/speed.ts
JLoeve 8c8bc9f45a feat: 从「倍速记忆」/「倍速增强」中分离出「扩展倍速」组件和「视频倍速 - 快捷键支持」插件
* 【新增】支持固定全局倍速值和设置倍速菜单最大高度(后者不显示选项)
* 【新增】支持隐藏倍速菜单滚动条
* 【优化】改变选项实时更新
* 【优化】使用了更稳妥的方式获取菜单项元素
* 【优化】使用自实现的 mini-rxjs 处理事件流,避免重复操作,实现更改选项值实时响应
* 【优化】为切换倍速等操作加上了防抖处理
* 【修复】添加空倍速会报倍速值太小的错误信息
* 【迁移】添加了迁移提示和自动迁移配置的功能
* 【更名】「倍速记忆」/「倍速增强」->「记忆倍速」
* 【更名】「扩展倍速菜单」->「扩展倍速」
2022-03-16 23:23:37 +08:00

217 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
ComponentEntry,
ComponentMetadata,
ComponentOption,
} from '@/components/types'
import { CoreApis } from '@/core/core-apis'
import { addComponentListener, ComponentSettings } from '@/core/settings'
import { logError } from '@/core/utils/log'
import { getHook } from '@/plugins/hook'
import {
bindCallback,
Subject,
subject,
TeardownLogic,
} from './mini-rxjs'
import { distinctUntilChanged } from './mini-rxjs/operators/distinctUntilChanged'
import {
getSpeedContext, SpeedContext, SpeedSeekPosition, useShareBuildArgument$,
} from './speed/context'
export type VideoIdObject = { aid: string; cid: string }
/** 原生支持的倍速值 */
export const NATIVE_SUPPORTED_VALUES = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0]
/** 浏览器支持设定的最小倍速值 */
export const MIN_BROWSER_SPEED_VALUE = 0.0625
/** 浏览器支持设定的最大倍速值 */
export const MAX_BROWSER_SPEED_VALUE = 16
export type EntryContext = Parameters<ComponentEntry>[0]
export type OptionSubjects<O> = O & { [K in keyof O as `${Exclude<K, symbol>}$`]: Subject<O[K]> }
export class EntrySpeedComponent<O = Record<string, unknown>>
implements EntryContext {
static create: <
OO extends Record<string, any> = unknown
>(metadata: Omit<
ComponentMetadata,
'entry' | 'reload' | 'unload' | 'options'
> & {
options?: { [K in keyof OO]: ComponentOption }
}) => ComponentMetadata;
static contextMap: Partial<Record<keyof EntrySpeedComponent, keyof SpeedContext | string>> = {
getVideoIdObject: 'videoIdObject',
getAvailableSpeedValues: 'getAvailableSpeedValues',
getOldActiveVideoSpeed: 'getOldActiveVideoSpeed',
getVideoSpeed: 'videoElement.playbackRate',
setVideoSpeed: 'set',
forceVideoSpeed: 'force',
resetVideoSpeed: 'reset',
toggleVideoSpeed: 'toggle',
increaseVideoSpeed: 'increase',
decreaseVideoSpeed: 'decrease',
}
constructor(protected readonly entryContext: EntryContext) {
lodash.assign(
this,
entryContext,
{
options: entryContext.settings.options,
},
)
// 执行迁移操作
this.migrate?.()
lodash.assign(
this,
lodash.mapValues(
EntrySpeedComponent.contextMap,
path => async (...args) => {
// 这里调用 getSpeedContext 不是第一次调用,因此拿到的一定是缓存
// 无需传递 build 参数
const context = await getSpeedContext()
const value = lodash.get(context, path) as any
const result = lodash.isFunction(value) ? await value(...args) : value
return result
},
),
)
}
speedContext?: SpeedContext
settings: ComponentSettings<O>
coreApis: CoreApis
metadata: ComponentMetadata
options: OptionSubjects<O>
readonly getVideoIdObject: () => Promise<VideoIdObject>
readonly getAvailableSpeedValues: () => Promise<number[]>
readonly getOldActiveVideoSpeed: () => Promise<number | undefined>
readonly forceVideoSpeed: (value: number) => Promise<void>
readonly getVideoSpeed: () => Promise<number>
readonly setVideoSpeed: (value: number, timeout?: number) => Promise<void>
readonly resetVideoSpeed: () => Promise<void>
readonly toggleVideoSpeed:
(legacy?: boolean) => Promise<void>
| ((offset: number, whence?: SpeedSeekPosition) => Promise<void>)
readonly increaseVideoSpeed: () => Promise<void>
readonly decreaseVideoSpeed: () => Promise<void>
getSpeedContextMixin?(context: SpeedContext): Partial<SpeedContext>
onSpeedContext?(context: SpeedContext): TeardownLogic | any
protected migrate?(): void
}
getSpeedContext((components: EntrySpeedComponent[]) => disposableSpeedContext => {
const safeContext = lodash.omit(disposableSpeedContext, 'dispose')
const mixins = components.map(component => component.getSpeedContextMixin(safeContext))
if (mixins.length > 1) {
// 检查是否有重复覆盖的字段
const repeatedKeys = lodash.intersection(...mixins.map(Object.keys))
if (repeatedKeys.length) {
throw new Error(
'In the registered speed component, there is an implementation of getSpeedContextMixin that causes the speed context to be mixed in ambiguous.\n'
+ `The repeated key names are ${repeatedKeys.join(', ')}`,
)
}
}
// 混入
lodash.assign(safeContext, ...mixins)
const allOptions$: Subject<unknown>[] = []
components.forEach(component => {
const options$ = lodash(component.settings.options)
.mapValues(
(_, optionName) => bindCallback(
addComponentListener,
`${component.metadata.name}.${optionName}`,
).pipe(distinctUntilChanged()),
)
.mapKeys((_, optionName) => `${optionName}$`)
.value()
allOptions$.push(...lodash.values(options$))
// 重新生成 options 代理对象
component.options = new Proxy<OptionSubjects<unknown>>(
component.settings.options, {
get: (target, p, receiver) => {
if (lodash.isSymbol(p)) {
return Reflect.get(target, p, receiver)
}
if (!Reflect.has(target, p) && p.endsWith('$')) {
return options$[p]
}
return Reflect.get(target, p, receiver)
},
},
)
component.speedContext = safeContext
component.onSpeedContext(safeContext)
if (!component.settings.enabled) {
return
}
// 通知一下组件的选项值
lodash(options$).entries().forEach(([optionName, optionValue$]: [string, Subject<unknown>]) => {
optionValue$.next(
component.settings.options[optionName.slice(0, -1)],
)
})
})
return {
...safeContext,
dispose: () => {
allOptions$.forEach(option$ => option$.complete())
disposableSpeedContext.dispose()
},
}
})
// 不知道为啥Webpack 会用 EntrySpeedComponent 直接替换类内定义的静态函数中的 this
// 这将导致 create 函数无法正常工作,因此只能在类外定义
// 这样做也不是没有缺点typescript 会认为此函数不是 EntrySpeedComponent 的一部分,对于 EntrySpeedComponent 静态成员的访问受限
EntrySpeedComponent.create = function create(metadata) {
const enabled$ = subject<boolean>().pipe(distinctUntilChanged())
return {
...metadata,
entry: (entryContext: EntryContext) => {
const component: EntrySpeedComponent | Error = lodash.attempt(() => new this(
entryContext,
))
if (component instanceof Error) {
logError(component)
return null
}
const [shareBuildArgument$] = useShareBuildArgument$()
enabled$.subscribe(() => {
shareBuildArgument$.next(component)
})
shareBuildArgument$.next(component)
getHook(`speed.component.${metadata.name}`).after(component)
return component
},
reload: () => enabled$.next(true),
unload: () => enabled$.next(false),
}
}