Repository navigation
engine-v0.4.0
·
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-yand--segment-anchor-origin-yreport 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.
WebAudioAudioDecoderdecodes throughAudioContext.decodeAudioData,FallbackAudioDecoderretries a failed primary decoder through a second one, andMediaBunnyAudioOnlyRemuxercopies a container's audio packets out so neither has to load the video bytes. Safari before 26 ships WebCodecs with noAudioDecoder. WebAudioAudioDecoderdecodes in bounded segments rather than in one call, so peak memory is one segment plus the output whatever the source's length.decodeAudioDataholds 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.AudioSegmentLayoutplaces the segments and gives each a margin of context at both ends, which the codec needs to reconstruct the frames at a seam.Document.languagecarries the ISO 639-1 two-letter code the document was transcribed in,nullwhen unknown.WebCodecsOnlyVideoFrameDecoderFactoryreturns the WebCodecs decoder or throwsVideoFrameDecoderSelectionFailedError. 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.RenderJobreports the decoder the run picked, throughVideoFrameDecoderKindandVideoFrameDecoderSelection. Both are null when the run ended before a decoder was chosen.MediaBunnyTranscodeRequest.signalaborts a run. Checked between frames, so it stops cleanly, and rejects with anAbortError.SpriteSheetProbeObserverreports 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.SpriteSheetRasterProbebacks 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.kindis 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.BalancedLineSplittertakes aminCharsPerLinefloor. 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.
getMaxBatchSizeis nowgetMaxTilesPerBatch: how many distinct pictures one sprite sheet can hold.getFramesreturns 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 mostgetMaxTilesPerBatch()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 wasframes / capacityhowever 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, andFramePainterandSubtitleLayerSourcenow take runs of frames rather than single ones. AtlookAhead() === 1the 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
feTurbulenceseed 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 sameSvgFilterDefsRendererthe tile uses. Avar()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. SvgFilterreports the custom property names its body reads (variableNames), andSvgFilterDefinitionsthe 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 untilclose. Frames from different batches can be held and painted together, which a caller gathering a run across a batch boundary needs. TextMeasurer.measuretakes the CSS classes the run will carry. It used to read one typography from a probe wearing no tags, so a rule sizing.emphasisdifferently was invisible and every word was measured as a plain one.- The default
maxWidthRatiois 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. insertSegmentAthandles only the anchored case and refuses a Document with no segments. UseinsertFirstSegmentfor that one.SpriteSheetSizeProberis nowSpriteSheetSizeProbe, andBrowserSubtitleFrameRenderer.createaccepts one throughoptions.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.getFrameswith 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
getComputedStyleoff 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 throughAnimationStateFingerprint, picked throughBrowserSubtitleFrameRenderer.create'sanimationStateFingerprintoption.keyframe-scan, the default, reads the style's CSS once, including the inline styles per-element overrides set.subtree-mountmounts the subtree and asksgetAnimations({ subtree: true }), which reaches pseudo-elements at any depth and resolvescalc()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.withSegmentsandwithMetadatano longer dropnarrationPace. 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.FallbackAudioDecoderreports 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 theerrorsof anAggregateError, since neither caused the other.RenderProgress.totalFramescarries a number. It was hard-coded to0at 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
WordlistTaggerandRegexTaggeralso handed back a fresh id, which is what per-line state is keyed by.