Files
ArtPlayer/packages/artplayer-tool-thumbnail/ARCHITECTURE.md

206 lines
13 KiB
Markdown

# Thumbnail tool maintenance map
The runtime responsibilities were separated under PKG-TOOL-THUMB-03 and are now
strict TypeScript under PKG-TOOL-THUMB-04. Public declarations and isolated installed
entrypoints now have checks; final integration/release gates remain open.
The user approved the
[compatibility decision](../../refactor/thumbnail-compatibility-decision.md) on 2026-09-15:
recovered 3.5.31 defaults versus opt-in workspace 4.4.0 behavior, including the
class-level DEFAULTS boundary. The root constructor now defaults to the published
policy; `compatibility: 'workspace-4.4'` selects the previous workspace behavior.
| File | Responsibility |
| --- | --- |
| src/index.ts | Public class, historical method names, construction and destruction entry |
| src/lifecycle.ts | Private state, source/input generations, cancellation and resource release |
| src/source.ts | File loading, native metadata/error listeners and source/thumbnail Blob URLs |
| src/extraction.ts | Owned metadata wait and serial frame job, callback/error completion and cancellation |
| src/input.ts | Option validation/clamps, file-input wrapper creation, listener registration/replacement and release |
| src/policy.ts | Approved mode selection and numeric normalization; no DOM or timer ownership |
| src/sheet.ts | Midpoint grid, canvas/footer geometry, temporary download anchor |
| src/emitter.ts | Typed local adaptation of tiny-emitter; preserves on/once/emit/off behavior |
| src/utils.ts | Pure clamp and filename helpers; unused sleep/serial helpers removed |
| src/types.ts | Internal option, frame, event tuple, job and lifecycle contracts; no runtime output |
| types/artplayer-tool-thumbnail.d.ts | Public class/namespace; d.cts/d.mts wrappers share its identity |
The entry delegates to input/source/extraction/sheet. Helpers import its type only;
the imports are erased and do not create a runtime dependency on the entry.
Input/source use lifecycle state, extraction uses lifecycle/source, and sheet uses
only filename calculation. types.ts contains no imports or executable state.
Input records live in a private WeakMap, so callers replacing `option` cannot
lose ownership of generated inputs/listeners. Normal construction preserves the
existing instance-field order and bound inputChange/ondrop methods. Public method
spelling and return values stay unchanged. DEFAULTS includes published delay: 300.
Input setup validates options before changing DOM or committed options. A wrapper
receives one owned input; repeated setup with that wrapper reuses it. Replacement
installs the new listeners before releasing the old input. A failed new listener
installation rolls back its partial listeners/DOM. Explicit caller-provided file
inputs are never removed. Generated inputs are removed and the wrapper's previous
position is restored only if its current position still matches the tool's write.
The existing bound ondrop method is now registered correctly.
Destroy is idempotent and attempts input, video, current URL and destroy-event
cleanup even when one step throws; the first cleanup error escapes afterward.
Setup/file-input/drop/load calls cannot recreate input/source resources after
destruction. The emitter is not globally cleared. Destruction closes the instance
before cleanup, cancels its job and releases private URLs even if public URL fields
were overwritten. Current public URLs are also revoked, preserving historical
destroy behavior. The video is paused, its src removed, load resets its decoder
state, and its node is removed. Registration checks for reentrant closure/replacement.
Cleanup records whether an exception occurred separately from its value. It attempts
all remaining cleanup callbacks and then rethrows the first value unchanged, including
undefined, null, false, zero, an empty string or NaN. In particular, a destroy listener
must not have its exception swallowed just because it is falsy. The instance remains
closed after a failed destroy; a repeated destroy is a no-op. This is distinct from
emitter dispatch: an exception still stops the remaining listeners in that dispatch.
`test/thumbnail-cleanup-errors.test.js` compares recovered 3.5.31 and frozen workspace
behavior, checks first-error identity across nested cleanup and verifies resource release.
`test/browser/thumbnail-emitter.spec.js` repeats the listener contract with real DOM.
## Extraction and event ordering
Loading emits file before assigning video.src, then video after the configured
delay in the default published policy, or synchronously in workspace mode.
A source generation prevents an older file callback or URL creation
hook from overwriting a nested newer load. Successful replacement revokes old
source URLs. Native errors report once for their source; stale listeners cannot
fail the latest job. Existing sheet URLs stay available until the next sheet frame
or destruction, matching the previous public behavior.
start creates one owned job. Metadata waits use native events plus the existing
one-second polling fallback. Waiting before the first file adopts that selection;
replacing an existing source cancels its old job. Duplicate starts are rejected.
Preflight stays synchronous when metadata is ready. Canvas precedes processing=true;
updates observe true, done observes false and may start another job without old
completion clearing the new state.
Published mode keeps configured height (clamped to 10-1000), waits delay after each
seek and delay * 2 after the last update before done. Delay defaults to 300 and is
clamped to 10-1000. Workspace mode derives height from video aspect ratio, ignores
delay and resets the selected input value; published mode retains that value.
Source notifications and extraction waits own their timers separately, so replacement,
error and destroy cancel both without late events. A null policy delay means no
timer; NaN retains the historical asynchronous browser timer behavior.
Frames wait for readiness/seek as well as any policy delay, accept readiness/Blob callbacks once and restore
oncanplay only while still owning it. Seek/draw/encoding failures, null Blob and
throwing callbacks settle the promise and detach job resources. Source replacement
or destruction rejects with AbortError without an error event. The public promise
still rejects for awaiting consumers; an internal handler owns ignored cancellation.
Late callbacks cannot create URLs, emit updates or schedule frames. There is no
arbitrary new metadata deadline; waiting can be cancelled by destroy/replacement.
Sheet extraction preserves fractional coordinates, historical `creat*` names,
the 30-pixel footer and the filename algorithm. The temporary download anchor is
removed even if click throws. Aspect-derived height and synchronous video events
apply only to explicit workspace-4.4 mode; the default preserves recovered
3.5.31 fixed height and delays. See the frozen
[contract](../../refactor/baselines/thumbnail-contract.md) before changing defaults.
## Validation and continuation
Use Yarn and the repository scripts:
```sh
yarn test:thumbnail
yarn typecheck
yarn build artplayer-tool-thumbnail
yarn test:browser test/browser/thumbnail-tool.spec.js test/browser/thumbnail-native.spec.js test/browser/thumbnail-input.spec.js
```
`test/thumbnail-input.test.js` contains candidate input/export regressions; set
ARTPLAYER_THUMBNAIL_BASELINE=1 to reproduce failures against frozen workspace
main. ARTPLAYER_THUMBNAIL_ARTIFACT selects an actual built artifact for Node and
browser candidate checks. `test/thumbnail.test.js` remains immutable-behavior
evidence against old implementations, including intentionally reproduced bugs.
Browser records distinguish actual extraction from native Blob-unavailable
controls. Windows WebKit can load tested MP4 files over HTTP but returns error 4
for native Blob URLs; this is not successful file extraction. Actual input/DOM
cleanup runs on all three engines, while extraction acceptance needs supported
Safari/WebKit evidence in 05. The independent tool example is
`docs/assets/example/tool.thumbnail.js`, not the external thumbnail plugin example.
`test/thumbnail-lifecycle.test.js` covers jobs, source cancellation, native errors,
callback failures, reentrancy and private resources. Browser tests hold real PNG
callbacks and replace a selected file during encoding; only the latest job may
complete. Preserve old tests and the approved delay/height boundary. The new
`test/thumbnail-policy.test.js` covers both modes, owned delays and cancellation;
browser extraction checks both policies against the frozen historical bundles.
The [public type guide](types/README.md)
documents declaration ownership, module entries, events and installed consumers.
See [task plan](../../refactor/plan.md) and [risk ledger](../../refactor/risks.json).
## Generated sheets consumed by ArtPlayer
The tool owns both `videoUrl` and `thumbnailUrl`. Destroy the player borrowing
those URLs before generating a replacement sheet or destroying the tool. Pass
the same `number`, `column`, `width` and `height` to the player's thumbnails
option; the PNG includes a 30px attribution footer, so do not infer cell height
from the full image height. The integration suite uses `scale: 1`.
Run `yarn test:browser:source thumbnail-core.spec.js --workers=1` for native file
extraction, two generated sheets, real mouse hover, screenshot pixels and URL
ownership against associated core 3.5.31, published 5.4.0 and the candidate core.
`ARTPLAYER_THUMBNAIL_ARTIFACT` selects the installed tool main. Historical cores'
first-cell/row-boundary defects have explicit expectations; the candidate must
use the correct cell. The old tool uses its original 300ms delay, while the
candidate still exercises 20ms. Recovered 3.5.31 at 20ms produced black cells in
Firefox because it draws after a timer without awaiting seek completion.
Windows WebKit Blob controls do not validate extraction. See the
[combination checkpoint](../../refactor/changes/2026-09-16-PKG-TOOL-THUMB-05-core-combinations.md)
for exact passing scopes and remaining Safari/device/archive evidence.
## Type and provenance boundaries
All nine executable source modules and the shared type module are checked with
strict, noUncheckedIndexedAccess, noImplicitOverride, skipLibCheck=false and no
ambient Node/test globals. Public fields use declare so TypeScript does not add
early undefined properties or change constructor property order. File/URL/density
fields retain their absence until the corresponding operation publishes them.
Unknown option fields remain unknown. Custom string/number/symbol events infer
caller-defined callback tuples; built-in events have precise tuples. Their
heterogeneous registry erases tuple types at storage and applies a local assertion
when dispatching. Public and source constructors are checked in both directions.
Error payloads remain unknown because callbacks can throw arbitrary message
values. This does not change runtime error delivery.
Assertions are limited to existing runtime boundaries: a temporary empty option
object before setup validation; the input/wrapper conversion; event target and
dataTransfer/files supplied by native input/drop events; private records inserted
before lookup; nonempty/dense frame arrays; a successful 2D canvas context; and
the old property read of message on arbitrary thrown values. These assertions
preserve existing runtime validation/errors rather than adding new coercions.
The constructor and destroy rollback paths also intentionally tolerate fields
that have not yet been assigned. DOM listener casts connect known event names to
the matching bound callbacks. Keep new assertions equally local and documented.
The emitter structurally corresponds to tiny-emitter 2.1.0, which the recovered
3.5.31 manifest declared as ^2.1.0. The exact original copied revision is unknown.
The pinned [reference files](../../refactor/baselines/thumbnail-vendor/sources.json)
and [MIT notice](THIRD_PARTY_NOTICES) retain upstream attribution. The repository
build includes the complete notice in main, legacy and ESM bundle headers.
Current packed contents include the complete notice and no implementation source;
new release candidates must repeat the installed check. Do not replace this local
emitter with the core emitter as part of a type-only change: dispatch semantics
need their own behavior review. The subsequent emitter fix checks only own
registry keys, defines new slots as ordinary enumerable/writable/configurable
data properties and leaves the registry prototype unchanged. Inherited getters
and setters cannot intercept event registration or dispatch. Each once wrapper
is consumed before removal/invocation so nested snapshots cannot invoke it twice,
including after a callback throws. New registrations of the same callback remain
independent. Normal snapshot order and off(originalCallback) are preserved.
`test/thumbnail-runtime.test.js` compares upstream, recovered/historical bundles
and the selected candidate for dispatch behavior, and checks candidate public
descriptors and validation. `test/thumbnail-vendor.test.js` verifies fixed source
bytes, complete notices and dist/docs equality. `test/types/thumbnail-runtime.ts`
checks source consumers with positive cases and six rejected invalid uses.
`test/thumbnail-emitter.test.js` reproduces the old key-collision/nested-once
failures and checks the candidate; `test/browser/thumbnail-emitter.spec.js` repeats
the behavior with the real browser class and native DOM cleanup.