mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-30 12:01:20 -08:00
refactor(mask): [PKG-MASK-04] migrate strict types and preserve public consumers
This commit is contained in:
1 parent
aba125262a
commit
aca4bc1cd2
30 files changed
+1848
-72
No files matched your search
+3
-1
@@ -1,4 +1,6 @@
|
||||
export default function maskConfig(option) {
|
||||
import type { MaskConfig, MaskOptions } from './types'
|
||||
|
||||
export default function maskConfig(option: MaskOptions): MaskConfig {
|
||||
return {
|
||||
solutionPath: option.solutionPath || 'https://cdn.jsdelivr.net/npm/@mediapipe/selfie_segmentation',
|
||||
modelSelection: option.modelSelection || 1,
|
||||
+27
-14
@@ -1,8 +1,18 @@
|
||||
import type { MaskConfig, MaskHost, MaskNodes, MaskRun } from './types'
|
||||
import { createOutput, releaseOutput, renderMask } from './output'
|
||||
import { loadSegmenter, releaseSegmenter } from './sdk'
|
||||
|
||||
export default class MaskController {
|
||||
constructor(art, config, { $video, $danmuku }) {
|
||||
declare art: MaskHost
|
||||
declare config: MaskConfig
|
||||
declare video: HTMLVideoElement
|
||||
declare layer: HTMLElement
|
||||
declare closed: boolean
|
||||
declare run: MaskRun | null
|
||||
declare tail: Promise<void>
|
||||
declare ready: () => void
|
||||
|
||||
constructor(art: MaskHost, config: MaskConfig, { $video, $danmuku }: MaskNodes) {
|
||||
this.art = art
|
||||
this.config = config
|
||||
this.video = $video
|
||||
@@ -38,11 +48,11 @@ export default class MaskController {
|
||||
}
|
||||
}
|
||||
|
||||
active(run) {
|
||||
active(run: MaskRun): boolean {
|
||||
return run === this.run && run.running && !this.closed && !this.art.isDestroy
|
||||
}
|
||||
|
||||
release(run) {
|
||||
release(run: MaskRun): Promise<void> {
|
||||
if (run.releasing)
|
||||
return run.releasing
|
||||
const segmenter = run.segmenter
|
||||
@@ -63,7 +73,7 @@ export default class MaskController {
|
||||
return run.releasing
|
||||
}
|
||||
|
||||
schedule(run) {
|
||||
schedule(run: MaskRun): void {
|
||||
if (!this.active(run) || run.frame !== null || run.busy)
|
||||
return
|
||||
run.frame = requestAnimationFrame(() => {
|
||||
@@ -72,7 +82,7 @@ export default class MaskController {
|
||||
})
|
||||
}
|
||||
|
||||
tick(run) {
|
||||
tick(run: MaskRun): void {
|
||||
if (!this.active(run) || run.busy)
|
||||
return
|
||||
const video = this.video
|
||||
@@ -86,7 +96,8 @@ export default class MaskController {
|
||||
// Reserve the work slot before SDK code can synchronously reenter start/stop.
|
||||
await Promise.resolve()
|
||||
if (this.active(run))
|
||||
await renderMask(run.output, video, this.layer, run.segmenter, this.config, () => this.active(run))
|
||||
// Only initialized runs reach tick; release waits for this busy work to settle.
|
||||
await renderMask(run.output!, video, this.layer, run.segmenter!, this.config, () => this.active(run))
|
||||
}
|
||||
catch (error) {
|
||||
if (this.active(run))
|
||||
@@ -103,14 +114,16 @@ export default class MaskController {
|
||||
this.tail = work.catch(error => console.warn('Failed to release danmuku mask resources:', error))
|
||||
}
|
||||
|
||||
async start() {
|
||||
async start(): Promise<void> {
|
||||
if (this.closed || this.art.isDestroy)
|
||||
return
|
||||
// The start promise is assigned before initialization resumes after its first await.
|
||||
if (this.run?.running)
|
||||
return this.run.started
|
||||
let cancel
|
||||
const cancelled = new Promise(resolve => cancel = resolve)
|
||||
const run = { running: true, initializing: true, busy: false, frame: null, segmenter: null, output: null, cancel, started: null }
|
||||
return this.run.started!
|
||||
// Promise executors install their resolver synchronously.
|
||||
let cancel!: () => void
|
||||
const cancelled = new Promise<void>(resolve => cancel = resolve)
|
||||
const run: MaskRun = { running: true, initializing: true, busy: false, frame: null, segmenter: null, output: null, cancel, started: null }
|
||||
const previous = this.tail
|
||||
this.run = run
|
||||
const initialize = (async () => {
|
||||
@@ -149,7 +162,7 @@ export default class MaskController {
|
||||
return run.started
|
||||
}
|
||||
|
||||
halt() {
|
||||
halt(): void {
|
||||
const run = this.run
|
||||
if (run) {
|
||||
run.running = false
|
||||
@@ -163,13 +176,13 @@ export default class MaskController {
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
stop(): void {
|
||||
this.halt()
|
||||
if (this.layer?.style)
|
||||
this.layer.style.maskImage = 'none'
|
||||
}
|
||||
|
||||
destroy() {
|
||||
destroy(): void {
|
||||
if (this.closed)
|
||||
return
|
||||
this.closed = true
|
||||
+3
-2
@@ -1,8 +1,9 @@
|
||||
import type { MaskHost, MaskOptions } from './types'
|
||||
import maskConfig from './config'
|
||||
import MaskController from './controller'
|
||||
|
||||
export default function artplayerPluginDanmukuMask(option = {}) {
|
||||
return (art) => {
|
||||
export default function artplayerPluginDanmukuMask(option: MaskOptions = {}) {
|
||||
return (art: MaskHost) => {
|
||||
const { template: { $video, $danmuku } } = art
|
||||
const controller = new MaskController(art, maskConfig(option), { $video, $danmuku })
|
||||
async function startSegmentation() {
|
||||
+8
-5
@@ -1,15 +1,18 @@
|
||||
import type { BodySegmenter } from '@tensorflow-models/body-segmentation'
|
||||
import type { Active, MaskConfig, MaskOutput } from './types'
|
||||
import { drawMask, toBinaryMask } from './sdk'
|
||||
|
||||
export function makeWhiteTransparent(imageData) {
|
||||
export function makeWhiteTransparent(imageData: ImageData): ImageData {
|
||||
const data = imageData.data
|
||||
// Native ImageData stores complete four-byte RGBA pixels.
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
if (data[i] > 250 && data[i + 1] > 250 && data[i + 2] > 250)
|
||||
if (data[i]! > 250 && data[i + 1]! > 250 && data[i + 2]! > 250)
|
||||
data[i + 3] = 0
|
||||
}
|
||||
return imageData
|
||||
}
|
||||
|
||||
export function createOutput(layer) {
|
||||
export function createOutput(layer: HTMLElement): MaskOutput {
|
||||
const canvas = document.createElement('canvas')
|
||||
try {
|
||||
const ctx = canvas.getContext('2d')
|
||||
@@ -31,7 +34,7 @@ export function createOutput(layer) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function renderMask(output, video, layer, segmenter, config, active) {
|
||||
export async function renderMask(output: MaskOutput, video: HTMLVideoElement, layer: HTMLElement, segmenter: BodySegmenter, config: MaskConfig, active: Active): Promise<void> {
|
||||
const { canvas, ctx } = output
|
||||
canvas.width = video.videoWidth
|
||||
canvas.height = video.videoHeight
|
||||
@@ -57,7 +60,7 @@ export async function renderMask(output, video, layer, segmenter, config, active
|
||||
layer.style.maskImage = `url(${url})`
|
||||
}
|
||||
|
||||
export function releaseOutput(output) {
|
||||
export function releaseOutput(output: MaskOutput | null): void {
|
||||
if (output) {
|
||||
output.canvas.width = 0
|
||||
output.canvas.height = 0
|
||||
@@ -0,0 +1,3 @@
|
||||
// TF 4.22.0 hash_util.d.ts uses global Long without importing its own dependency.
|
||||
// Load the official UMD namespace for this internal check; emit no runtime import.
|
||||
import 'long'
|
||||
+10
-6
@@ -1,21 +1,25 @@
|
||||
import type { BodySegmenter } from '@tensorflow-models/body-segmentation'
|
||||
import type { Active, MaskConfig, SegmenterConfig } from './types'
|
||||
import * as bodySegmentation from '@tensorflow-models/body-segmentation'
|
||||
import * as tf from '@tensorflow/tfjs-core'
|
||||
import '@tensorflow/tfjs-backend-webgl'
|
||||
import '@tensorflow/tfjs-backend-cpu'
|
||||
|
||||
export async function loadSegmenter(config, active) {
|
||||
export async function loadSegmenter(config: MaskConfig, active: Active): Promise<BodySegmenter | null> {
|
||||
try {
|
||||
await tf.setBackend('webgl')
|
||||
}
|
||||
catch (error) {
|
||||
if (!active())
|
||||
return null
|
||||
console.warn('WebGL backend not available, falling back to CPU', error.message)
|
||||
// Keep the historical message property read, including non-Error rejection behavior.
|
||||
console.warn('WebGL backend not available, falling back to CPU', (error as { message: unknown }).message)
|
||||
await tf.setBackend('cpu')
|
||||
}
|
||||
if (!active())
|
||||
return null
|
||||
try {
|
||||
// Validate the full legacy object without dropping keys absent from the SDK config union.
|
||||
return await bodySegmentation.createSegmenter(bodySegmentation.SupportedModels.MediaPipeSelfieSegmentation, {
|
||||
runtime: 'mediapipe',
|
||||
modelType: 'general',
|
||||
@@ -25,7 +29,7 @@ export async function loadSegmenter(config, active) {
|
||||
minDetectionConfidence: config.minDetectionConfidence,
|
||||
minTrackingConfidence: config.minTrackingConfidence,
|
||||
selfieMode: config.selfieMode,
|
||||
})
|
||||
} satisfies SegmenterConfig as SegmenterConfig)
|
||||
}
|
||||
catch (error) {
|
||||
if (active())
|
||||
@@ -34,7 +38,7 @@ export async function loadSegmenter(config, active) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function releaseSegmenter(segmenter) {
|
||||
export async function releaseSegmenter(segmenter: BodySegmenter): Promise<void> {
|
||||
try {
|
||||
await segmenter.dispose()
|
||||
}
|
||||
@@ -43,5 +47,5 @@ export async function releaseSegmenter(segmenter) {
|
||||
}
|
||||
}
|
||||
|
||||
export const toBinaryMask = (...args) => bodySegmentation.toBinaryMask(...args)
|
||||
export const drawMask = (...args) => bodySegmentation.drawMask(...args)
|
||||
export const toBinaryMask = (...args: Parameters<typeof bodySegmentation.toBinaryMask>) => bodySegmentation.toBinaryMask(...args)
|
||||
export const drawMask = (...args: Parameters<typeof bodySegmentation.drawMask>) => bodySegmentation.drawMask(...args)
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { BodySegmenter, MediaPipeSelfieSegmentationMediaPipeModelConfig } from '@tensorflow-models/body-segmentation'
|
||||
|
||||
// Owned implementation contracts; the historical public entrypoint is maintained separately.
|
||||
export interface MaskOptions {
|
||||
solutionPath?: string
|
||||
modelSelection?: number
|
||||
smoothSegmentation?: boolean
|
||||
minDetectionConfidence?: number
|
||||
minTrackingConfidence?: number
|
||||
selfieMode?: boolean
|
||||
drawContour?: boolean
|
||||
foregroundThreshold?: number
|
||||
opacity?: number
|
||||
maskBlurAmount?: number
|
||||
}
|
||||
|
||||
export type MaskConfig = Required<MaskOptions>
|
||||
export type Active = () => boolean
|
||||
|
||||
// These legacy extra keys are passed through even where the current adapter ignores them.
|
||||
export type SegmenterConfig = MediaPipeSelfieSegmentationMediaPipeModelConfig & Pick<MaskConfig, 'modelSelection' | 'smoothSegmentation' | 'minDetectionConfidence' | 'minTrackingConfidence' | 'selfieMode'>
|
||||
|
||||
export interface MaskNodes {
|
||||
$video: HTMLVideoElement
|
||||
$danmuku: HTMLElement
|
||||
}
|
||||
|
||||
export interface MaskHost {
|
||||
isDestroy: boolean
|
||||
template: MaskNodes
|
||||
on: (event: 'ready' | 'destroy', callback: () => void) => unknown
|
||||
off: (event: 'ready' | 'destroy', callback: () => void) => unknown
|
||||
}
|
||||
|
||||
export interface MaskOutput {
|
||||
canvas: HTMLCanvasElement
|
||||
ctx: CanvasRenderingContext2D
|
||||
}
|
||||
|
||||
export interface MaskRun {
|
||||
running: boolean
|
||||
initializing: boolean
|
||||
busy: boolean
|
||||
frame: number | null
|
||||
segmenter: BodySegmenter | null
|
||||
output: MaskOutput | null
|
||||
cancel: () => void
|
||||
started: Promise<void> | null
|
||||
releasing?: Promise<void>
|
||||
}
|
||||
Reference in new issue
Block a user