Skip to content

engine-v0.4.0

Choose a tag to compare

@github-actions github-actions released this 30 Aug 15:43
· 12 commits to main since this release

Added

  • Stylesheets can read how wide an element renders: --segment-width-em, --line-width-em, --word-width-em. The engine mounts the subtree under the consumer's own stylesheet and reads the boxes back, so the widths account for the classes the element actually carries. They are multiples of the font size, which is what lets a rule derive the size from the width. Measured once per caption per style, and skipped for a stylesheet that reads none of them.
  • --segment-anchor-y and --segment-anchor-origin-y report where placement put the caption and which part of the box landed there. A rule can position a caption absolutely instead of assuming an anchor the consumer may have moved.
  • --line-char-count, beside the segment and word counts already published.
  • Audio decoding without WebCodecs. WebAudioAudioDecoder decodes through AudioContext.decodeAudioData, FallbackAudioDecoder retries a failed primary decoder through a second one, and MediaBunnyAudioOnlyRemuxer copies a container's audio packets out so neither has to load the video bytes. Safari before 26 ships WebCodecs with no AudioDecoder.
  • WebAudioAudioDecoder decodes in bounded segments rather than in one call, so peak memory is one segment plus the output whatever the source's length. decodeAudioData holds its input and its output at once, and a 38-minute recording died 5.3 s in on iOS 18.7. Measured on a 2290 s source in Chromium: 760 MB peak against 2884 MB, at the same wall clock. AudioSegmentLayout places the segments and gives each a margin of context at both ends, which the codec needs to reconstruct the frames at a seam.
  • Document.language carries the ISO 639-1 two-letter code the document was transcribed in, null when unknown.
  • WebCodecsOnlyVideoFrameDecoderFactory returns the WebCodecs decoder or throws VideoFrameDecoderSelectionFailedError. The default factory falls back to a hidden <video> element, which runs at playback speed. A host that would rather rewrite the source needs to see that refusal instead of a slow path taken in silence.
  • RenderJob reports the decoder the run picked, through VideoFrameDecoderKind and VideoFrameDecoderSelection. Both are null when the run ended before a decoder was chosen.
  • MediaBunnyTranscodeRequest.signal aborts a run. Checked between frames, so it stops cleanly, and rejects with an AbortError.
  • SpriteSheetProbeObserver reports a host that can hold only one tile per sprite sheet. One tile per sheet means one rasterization per frame, and the capacity alone never said whether that was the host refusing or a legitimate answer. SpriteSheetRasterProbe backs the size walk and returns why a raster was refused rather than a boolean.
  • DocumentEditor.insertFirstSegment(doc, time, sectionKind), for a Document with no segments. kind is opaque to the engine, so the consumer names the Section.
  • Word.getTagCssClasses() returns the classes a word's tags contribute, without asking for a timestamp.
  • BalancedLineSplitter takes a minCharsPerLine floor. It uses one line fewer instead of leaving a short one.

Changed

  • A batch covers as much video as the captions hold still for, not a fixed number of frames. getMaxBatchSize is now getMaxTilesPerBatch: how many distinct pictures one sprite sheet can hold. getFrames returns a prefix of the timestamps it was given, ending where those pictures run out, so a caller offers a whole look-ahead and advances by the length of the answer. Passing at most getMaxTilesPerBatch() timestamps still comes back whole, so a caller on the old contract sees only the rename. The old shape assumed every timestamp was a distinct picture, so the sheet count was frames / capacity however well the frames deduplicated, and every sheet costs the same whatever it holds. Simulated over ten seconds of captions at 30 fps: 15 sheets become 1 at 21 tiles per sheet, and 300 become 20 at one.
  • A caption bound to the video's own pixels batches too. Its picture only exists once the frame beneath it does, so it used to rasterize one sheet per output frame. FramePainter.lookAhead() tells the coordinator how many frames to gather, and FramePainter and SubtitleLayerSource now take runs of frames rather than single ones. At lookAhead() === 1 the loop is the old one, with no copy and no retention. On a phone, a style doing this took 160 s and now takes 58.6 s.
  • A style with SVG filters no longer rasterizes a tile per frame just for having them. Only a filter reading a value from the render context varies with time, typically a feTurbulence seed driven by a tick, and the cache treated every filter set that way. The planner now compares the filter markup each timestamp would paint, rendered through the same SvgFilterDefsRenderer the tile uses. A var() the scope leaves unresolved still forces separate frames: the document's CSS decides what it becomes, and the planner cannot see that. Measured on an export of 1702 frames at 720×1280 with an outline filter: 1636 tiles became 471.
  • SvgFilter reports the custom property names its body reads (variableNames), and SvgFilterDefinitions the union across its filters. A caller can describe the scope a filter set is sensitive to without materializing it.
  • A SubtitleFrame's raster belongs to the batch that produced it, not to the renderer, and stays valid until close. Frames from different batches can be held and painted together, which a caller gathering a run across a batch boundary needs.
  • TextMeasurer.measure takes the CSS classes the run will carry. It used to read one typography from a probe wearing no tags, so a rule sizing .emphasis differently was invisible and every word was measured as a plain one.
  • The default maxWidthRatio is 0.72 rather than 0.8. The short-form players draw an action rail down the right edge, and the widest of them takes about a ninth of the width. 0.72 is also what 0.8 produced before the measurement fix below, so a template on the default looks as it did.
  • insertSegmentAt handles only the anchored case and refuses a Document with no segments. Use insertFirstSegment for that one.
  • SpriteSheetSizeProber is now SpriteSheetSizeProbe, and BrowserSubtitleFrameRenderer.create accepts one through options.sizeProbe. The size walk decodes rasters up to the whole pixel budget for an answer that depends only on the output size and the host, so renderers working at the same size should share it. Both were paying for it separately.

Removed

  • SubtitleFrameRenderer.getFrame. getFrames with one timestamp is the same call, and keeping both meant two raster lifetimes to document.
  • TextMeasurer.spaceWidth. The renderer emits words as adjacent elements with nothing between them, so the gap is each word's margin and a space character was never part of a line.

Fixed

  • An animation on a pseudo-element no longer exports as a still image. The planner decided whether a caption was moving by mounting a chain of divs and reading getComputedStyle off the leaf, which never asks about pseudo-elements, so the tile was shared across the run and burned in while the preview played it. The question now goes through AnimationStateFingerprint, picked through BrowserSubtitleFrameRenderer.create's animationStateFingerprint option. keyframe-scan, the default, reads the style's CSS once, including the inline styles per-element overrides set. subtree-mount mounts the subtree and asks getAnimations({ subtree: true }), which reaches pseudo-elements at any depth and resolves calc() delays.
  • A line is measured at the width it renders at. The arithmetic was wrong three ways and came out 8% to 11.5% over the same words laid out by a browser: it counted a space between every pair of words, it spent letter spacing between a word's letters but not after the last, and it read one typography for every word. The corrections pull in opposite directions and the result now agrees with a real layout to within half a percent. Lines hold about a tenth more, so a template's width wants revisiting against what it was tuned to.
  • Document.withSegments and withMetadata no longer drop narrationPace. Neither has a caller in this repo, so the loss was silent for anyone using the published package. Both clone paths now carry every field.
  • FallbackAudioDecoder reports both failures rather than the primary's alone. Blaming the WebCodecs path on a runtime where it can never work is what told a reader to convert a video that was already MP4/H.264/AAC. The two travel as the errors of an AggregateError, since neither caused the other.
  • RenderProgress.totalFrames carries a number. It was hard-coded to 0 at the only place the progress event is built, so a consumer showing "frame N of M" had nothing to put in M. It now comes from the output duration and the source's frame rate, both of which the coordinator already resolves, and the field documents that it is an estimate: a variable-frame-rate source has no single rate to read.
  • A tagger keeps what the line it touches was already carrying. All five rebuilt their lines field by field, dropping anything tagging has no opinion about, and WordlistTagger and RegexTagger also handed back a fresh id, which is what per-line state is keyed by.