Changelog โ
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.4.0] - 2026-08-15 โ
Added โ
- Custom controls API:
addControl()lets a host add its own buttons and context-menu rows โ described once (icon, label, toggle/button, which side/anchor it sits on) and the element builds both faces and keeps them in sync. Controls can open nested submenus (any depth), take a hotkey that shows up in the Keyboard Shortcuts panel and flashes the OSD, group into a bar capsule of their own, and declaremedia: "video" | "audio" | "both"so they hide themselves for content they don't apply to.anchors: { bar, menu }places a control differently per surface, and an anchor can be a list for a neighbour that only sometimes exists (HDR, captions, audio track).showOverlay()/updateOverlay()/hideOverlay()put a host's own panel (end screen, up-next) over the picture, including in fullscreen. - Host-supplied error screen:
part=on every piece of the built-in error overlay for restyling, aslot="error"for replacing it outright, and anerrordisplayevent (pluserrorTitle/errorMessageproperties) carrying the exact wording on screen โ separate from the rawerrorevent โ including format/codec failures that never raise a runtime error. Bridged through the React/Vue/Svelte wrappers. - Settings-change events:
aspectchange,loopchange,stablevolumechange,hdrchange,ambientchange,rotatechange, andaudioonlychangenow fire so a host can persist a viewer's choice the way it already could for quality and volume. - External chapters: a
chaptersattribute/property accepts a chapter list from outside the media file ([{title, start, end?}]) for sources โ YouTube-style watch pages, a CMS โ that never carry chapter atoms in the container itself. Supplied chapters win over the container's and survive a quality-switch/error player rebuild. - Granular
fastseek: the attribute now takes a value naming which skip affordances to turn on โbuttons,keys,gestures(plus aliases liketouch/keyonly) โ instead of switching on all three at once, so a page can offer just the double-tap gesture, or just the arrow keys. titlemode: controls where the title bar shows (fullscreen,windowed,both,back,back-mobile-fullscreen) and whether it carries a back arrow, which fires a cancelablebackevent for the host to handle. Comes with a newexitFullscreen()method covering all three fullscreen routes.themecolortakes a secondary colour (themecolor="#8B5CF6 #22D3EE"), used for the centre play/pause flash; a single colour still works exactly as before.cropbars: strips real letterbox/pillarbox padding baked into the source pixels (a 2.39:1 film in a 16:9 frame, a phone clip in a 4:3 frame) before applyingcover/fill/zoom, instead of scaling the bars along with the picture. Detected automatically per source with a conservative brightness/duration check so a genuine dark scene or fade-to-black is never mistaken for a bar.- Karaoke subtitles get a per-line backdrop and scroll on wrap: each line of a rolling WebVTT cue now has its own sized pill instead of one box squared to the widest line, and a line pushed up by a wrap slides into place the way YouTube's rolling captions do, instead of jumping.
- Full native media API surface: all 78
HTMLMediaElement/HTMLVideoElementmembers now answer, includingbuffered/seekable/played,textTracks/audioTracks/videoTracks,srcObject,canPlayType,captureStream,getVideoPlaybackQuality,requestVideoFrameCallback,setSinkId,fastSeek, and theabort/suspend/encrypted/waitingforkey/cuechangeevents โ so code written against a native<video>has nothing left to reach for that isn't there. - Firefox extension: the same player-in-a-tab experience as the Chrome extension, sharing its UI (popup, content script, player page) via a build step so there is one codebase to maintain, not two that drift.
backgroundplaynow actually keeps a phone playing when the tab is hidden (previously only had an effect on desktop browsers).- Visitor feedback wall on the landing page's FAQ โ a note plus an optional 1-5 star rating, with server-side abuse filtering and rate limiting.
- HLS/DASH streams the browser can't decode now play anyway: when Shaka/hls.js/dash.js can't decode a stream's codec (e.g. Safari rejecting HE-AAC, or an unsupported video codec), playback now escalates through the other MSE engine and, as a last resort, falls back to the player's own FFmpeg-WASM software demuxer โ for both DASH and HLS. Quality switching, alternate audio-language tracks, and subtitle/caption tracks (WebVTT and TTML) all carry over into that fallback mode.
- Seamless, in-place quality switching everywhere: manual and automatic quality changes โ across the WASM-demuxer fallback, multi-file quality sources (one file per resolution), and now HLS/DASH โ swap the active rendition in place instead of tearing the player down and reloading, so there's no loading gap, no dropped playhead, and no dead silence. The incoming rendition's decoder also runs ahead of the swap point, so a quarter-second of decoded frames is already ready the instant the old rendition's queue is retired โ no frozen frame sitting under the spinner for as long as a fresh keyframe takes to arrive.
- Auto (adaptive) quality: an "Auto" option that measures your link's throughput and picks the best rendition it can sustain, now available on the demuxer-fallback and multi-file quality-source paths too, not just HLS/DASH. It runs a short pre-play speed test past a CDN's caching/pacing burst and opens directly on the rung the link can sustain, rather than climbing one rung at a time or trusting a stale cross-video estimate โ so no opening 1080p on a 0.7 Mbps link, and no visible ramp up from 144p on a fast connection. Upshifts are confirmed by probing the target rung, so a burst can only make the decision more conservative; rungs are barred by codec and height together (a failed 4K AV1 rung no longer poisons 4K H.264 too); decode cost is priced by width ร height ร fps ร codec factor instead of by height alone; the software-decode ceiling is sized separately per codec and no longer caps a healthy hardware 1080p session at 480p; and a codec that can't be hardware-decoded at all switches to software after one failed rung instead of walking the whole ladder down first. It persists your Auto preference across videos and shows the active rung like "Auto (1080p)". Also fixed along the way: thrashing between rungs, downshifting on estimate noise instead of an actually-draining buffer, losing a voluntary upshift while the tab is hidden, and switching quality mid-seek โ which could corrupt the demuxer and desync audio/video by tens of seconds.
- Pluggable
SubtitleRendererfor ASS/SSA (issue #17): a host hook to plug in a custom subtitle renderer (e.g. jassub/libass-wasm) for full ASS/SSA styling โ positioning, karaoke, embedded fonts โ since the player's canvas-only pipeline can't use a native<track>overlay for these. fallback="native": when the WASM/WebCodecs pipeline can't read a source (a no-CORS cross-origin file, a transient network failure), playback now hands off to the browser's own<video>element wrapped in the player's own controls, instead of a dead end. The degraded surface keeps more than the transport: a<source>quality ladder still switches in place โ with an Auto mode measured by stall rate rather than buffer depth, since a native element decodes opaquely and its buffer reads as permanently starved by the usual rule โ<track>subtitles are parsed and painted into Movi's own overlay so the subtitle styling and delay controls keep working, and split video+audio sources, which a lone<video>can't play at all, get a synced companion<audio>: mirrored transport, drift correction, and a hard resync when the two disagree, including multi-language audio switching.engineattribute: choose which playback engine leads and what follows it โwasm(Movi's demuxer + WebCodecs, and for a manifest its own DASH/HLS handling),shaka,dashjs,hlsjs,native. A space-separated list, first name first:engine="native wasm"plays through the browser and only falls back to Movi's pipeline,engine="dashjs shaka"prefers dash.js for manifests. Unset keeps the built-in order.- Pluggable URL-scheme adapter registry:
registerSourceAdapter()teaches the player about customsrcschemes โs3://,ipfs://,ws://, etc. โ with no per-element wiring. - Adaptive load shedding for underpowered devices: a detector measures the achieved frame-present rate against the source's rate and sheds load on a sustained deficit, so a device that can't keep up (low-end mobile on 4K, software-decoded AV1) degrades gracefully instead of stuttering indefinitely.
movi-player/element/slim: a second, smaller build (4.2MB of JS vs. 11.4MB) that streams the FFmpeg WASM from a separatemovi.wasmfile instead of embedding it, with automatic fallback to native playback if the WASM can't be fetched. The framework wrappers have slim twins too โmovi-player/react/slim,movi-player/vue/slim,movi-player/svelte/slimโ same components, props, and events, andwasmurlpoints the loader at wherever you host the.wasm.- Standard
HTMLMediaElementevents: ten previously-missing standard events โ includingseeking/seekedanddurationchangeโ now fire, so code ported from a native<video>works without changes. - Full IDL property reflection: every documented attribute (all 57) can now be read and set as a JS property (
el.rotate = 90), not just viasetAttribute/getAttribute. - Declarative
rotateattribute (0/90/180/270): set rotation from markup instead of only through the hotkey/menu. versionexposed at runtime:MoviElement.version(static) andel.version(instance), jQuery-style. Alongside it,MoviElement.build/el.build(and theBUILDexport) report which bundle is running โ"slim"or"full"โ since the two are otherwise indistinguishable at runtime despite loading the engine differently. The stats panel leads with both:Player: 0.4.0 (slim).- Framework wrappers type all 57 attributes: React/Vue/Svelte wrappers now type the complete attribute set and accept typed
<source>/<track>children (height,srcLang,kind="audio", etc.) instead of needing string/attribute-spread workarounds. - VS Code IntelliSense for
<movi-player>: attribute and value completion with hover docs, plus CSS completion for the 38--movi-*theme variables. - Examples gallery (
/examples): a page of live, copy-paste<movi-player>recipes with modal demos; existing recipes expanded and reorganized into themed sections, including a new ASS/SSA subtitle recipe. - Embed-code dialog: a toggle-driven dialog (autoplay, muted, loop, thumbnails, resume, ambient glow) generates a live
/embedsnippet, replacing the old one-shot copy button;/embeditself is now restricted to framed contexts, with a branded block page when opened directly. - Google Drive video player (
/drive): sign in, pick a video from your Drive, and stream it straight intomovi-player.
Changed โ
- Every setting moved behind one gear: quality, speed, stable volume, aspect, loop, audio track and HDR used to each have their own button on the bar; now there's a single gear panel the way most other players do it, and the bar keeps only what you touch while watching (play, volume, time, captions, PiP, fullscreen). Rows appear only when they lead somewhere, track rows now name the language ("HIN ยท Surround") instead of just the channel layout, and picking a value closes the panel while toggles don't.
- Control bar redesign: buttons group into capsules, chapter breaks are real gaps cut through the whole progress bar (groove, buffer and fill all break together) rather than ticks painted over it, the bar sits closer to the frame edge with a thicker/hoverable progress track, the quality badge reads height and frame rate ("720p@60"), an HDR badge joins it, and aspect ratio moved beside the gear. The keyboard shortcuts panel now fits inside a phone-sized player instead of running off the bottom.
bindav(bind audio+video on a stall) is on by default now, opt out withbindav="false". Whichever of audio or video runs dry now waits for the other before either resumes, instead of the picture racing seconds ahead of frozen audio (or vice versa) on a slow link.- HTTP range-request sizing tuned against a real paced CDN: opens with a 4MB chunk for a fast first byte, then fills with 8MB chunks (previously always 4MB, which spent most of a high-bitrate stream's time on per-request overhead instead of filling the buffer).
- The settings gear now hides rows that would silently do nothing: stable volume and aspect ratio disappear from the panel on a source playing through the native fallback or an adaptive-stream engine, where neither setting has anywhere to act.
- Compare page (
/compare) now served from the slim build, and the player bundle preloads during<head>so its download starts earlier on the main app. - README and docs discoverability: an honest comparison table (added dash.js and Shaka Player columns) and a new "Alternatives" section naming peer libraries; docs are now served under
/docs, and a Terms of Service page was added. - Third-party license attribution completed for every bundled component, including the FFmpeg LGPL notice.
Fixed โ
- Picture-in-Picture: the floating window now draws its own buffered range and loading spinner (previously both were painted on the page nobody was looking at) and renders subtitles inside the Document Picture-in-Picture window; its clock survives a change of video instead of freezing on the old one; the page's own clock, progress and controls keep running while PiP is open instead of freezing when the bar auto-hides or the tab backgrounds; the next auto-advanced video keeps painting into an already-open PiP window instead of going black, and starts directly in that window instead of waiting for the tab to be visible; the window picks up
themecolorinstead of always rendering violet; it closes automatically when the element is torn down, so no orphaned black window survives a video-to-video rebuild; and the PiP control is hidden inside any iframe, where the browser forbids opening a PiP window anyway. - Firefox autoplay false-mute: Firefox flips an
AudioContextfrom "suspended" to "running" about 100ms afterresume(), after the promise has already resolved โ the autoplay check read the state once, right afterplay(), and muted audio Firefox had already allowed, leaving "Tap to unmute" on screen for the whole video. - Muted-audio semantics: pausing then pressing mute no longer shows an unmute pill as though the browser had blocked audio; unmuting a paused player no longer starts it playing over a still picture; and a deliberate mute is no longer clawed back by the same recovery path that restores audio the browser itself had blocked.
- Split audio+video robustness: the split-audio demuxer no longer starves during the opening speed-test burst (the opening rung is sized to leave it headroom, and a stalled open retries with backoff); a software audio-decode circuit-breaker trip now reports the failure and restores the hardware decoder that was working instead of leaving audio silent for the rest of the video; and the audio clock is rebuilt correctly after audio was dropped while suspended.
- Rounded corners stop eroding the outermost pixel row, and now survive fullscreen entry/exit, a quality switch, and losing/regaining the GPU context โ previously several of these dropped the clip entirely or cut into the picture itself.
- Seek bar falsely reading "fully buffered": a large MKV (cues at the tail) or an MP4 with a trailing
moovmade an opening read of the file's last few bytes look like the whole file had downloaded; the buffered bar now checks that the read window is actually near the playhead, not just that it happens to reach the end of the file. - A refused byte range is now reported instead of read as end-of-file: an expired signed URL (403) used to look like the video had simply finished โ clock jumped to the duration, "ended" fired โ instead of surfacing an error; a URL that keeps refusing now backs off and stops retrying instead of firing over a thousand requests in a few seconds, and reports a message that matches the failure (expired link vs. not found vs. server error).
- Image subtitles: stopped re-encoding the same PGS/VobSub bitmap to a data URL on every render tick (was happening 60x/sec even when nothing had changed), and a cue that lands after a gap no longer visibly glides ~50px into position over a third of a second.
- Centre play/pause icon: no longer pops in-then-out when the button was already on screen (e.g. the poster); an in-flight fade is now cancelled the moment the button becomes a persistent control, fixing an occasional flicker back to full opacity; and a media-session play/pause (lock screen, media keys) now flashes the same receipt a click does.
- Timeline thumbnail strip: a touch device gets paging arrows again (sized for a fingertip) instead of relying on scroll alone with no sense of how much strip is left; and the scroll indicator now only draws while something is actually scrolling instead of a static grey line at all times.
- Context menu / settings panel agreement: both now agree on what's actually available on the current source; a tall context menu rests on the player's bottom edge instead of running past it; the aspect-ratio icon and open submenu update on every change; and the speed submenu ticks the right row regardless of what triggered the rate change.
- Audio-only โ video transitions: the picture now rejoins before the audio-only path stops the sound for it (previously could stop cold), coming back from audio-only restores the picture instead of staying audio-only, the timeline holds correctly while the picture is gone, and a player that's been torn down stops downloading video no one will see.
- Legacy/unmapped codecs now fall back to software decoding instead of failing outright โ WebCodecs has no registered codec string for several FFmpeg-supported legacy codecs (Motion JPEG in AVI among them), which used to leave the renderer black (#19, thanks @Wgmlgz).
persistnow works when a framework mounts the element: React (and similar) set attributes after appending the element, so the persisted-settings restore ran beforepersistexisted and silently restored nothing; it now re-runs when the attribute actually arrives.- Audio-track reporting: the audio-track list is rebuilt on a source change instead of carrying over the previous file's tracks, and the reported "current" track now matches what's actually playing instead of what was last selected.
- A removed player could keep playing: a source whose load failed after the element was taken out of the document (a host swapping videos on navigation) built its fallback player on the detached element โ the teardown had already run, so nothing could ever stop it and the old audio played on underneath the next video. Player creation now bails once the element is gone, and the audio elements a quality switch hands between players are released instead of being left running.
- Spurious "1x" / "100%" OSD on every playback start: restoring persisted settings re-entered the volume/speed updaters (and reflected the values back onto the attributes, re-entering again), each time flashing the OSD as if the user had just changed something. The OSD now fires only on an actual change.
- Karaoke subtitles printed their internal delimiter: cue text carries the full upcoming sentence after a ghost marker so the caption box can be sized to the finished line; renderers that didn't strip it (the native surface, hls.js) put the marker and the whole sentence on screen.
- Playback recovery instead of dead ends: the player now recovers from a corrupt/short demuxer read, a frozen frame with audio still advancing after a long backgrounded spell, a stream that's exhausted its recovery budget (now a self-healing "Reconnectingโฆ" instead of a permanent error), a stuck cached WASM module after an abort, and a hardware decode error on too heavy a rendition (drops to a lower rung under Auto instead of a dead-end error overlay).
- Alternate audio-language and subtitle switching in HLS/DASH: audio-language pickers that didn't work at all in some paths now do; subtitle-language switches no longer leave the previous language's caption stuck on screen; duplicate same-language audio renditions no longer clutter the menu with entries like "English ยท 48kbps / 64kbps / 32kbps".
- Rotation, timeline, and subtitles across stream playback: rotating a video (or resizing while rotated) is now respected during HLS/DASH/Shaka playback โ it previously touched the wrong renderer and could be a silent no-op, or revert on resize.
- Context menu in audio-only mode: menu item queries are portal-aware everywhere now, the menu live-updates the moment you switch to audio-only, and video-only items (Aspect Ratio, Rotate, etc.) are hidden while in audio mode.
- HDR re-detected on quality change: a rendition ladder can mix HDR and SDR rungs (e.g. an HDR 4K rung under an SDR 1080p one) โ the HDR indicator now re-evaluates on every quality change instead of sticking with whatever the first-loaded rung reported.
- Album art no longer flashes over a playing video during a quality switch.
- Seek bar and progress UI: clicking the seek bar no longer double-seeks and snaps back to the old position; ABR and the recovery watchdogs no longer fight an in-flight seek; the played/buffered bar draws seamlessly and resets to zero on a fresh load.
- Audio robustness: split (separate-URL) audio no longer stalls when the tab is backgrounded; audio-only mode stops downloading the unwatched video stream and correctly suppresses the "Play at 1x" stutter hint; the screen wake lock is now acquired on tap-to-unmute, ordered so Safari doesn't consume the gesture on the wrong request; the pitch stretcher warms up at the correct rate; stale pre-seek audio is dropped on seek instead of playing briefly.
- Source resiliency: a streamed source retries open-ended instead of failing when a bounded byte-range request gets a 403; file size is recovered via a ranged GET when a
HEADrequest throws (issue #14). - Playback rate changes stay seek-free when a healthy audio clock can anchor them, instead of always reseeking.
- Google Drive sign-in no longer pops up unbidden on page load โ it now only triggers on an explicit click.
- Chrome extension no longer tags the page DOM for presence detection, which was showing up as a hydration mismatch on React/Next.js and similar pages; it now uses an in-memory window flag instead.
[0.3.5] - 2026-07-11 โ
Added โ
- Captions rotate with the video: rotating the video (
R/ menu) turns the subtitles with it and keeps them on the rotated bottom edge โ text and image (PGS/VobSub) subtitles both. - Blurred backdrop for embedded album art: audio with embedded cover art now gets the same blurred-artwork backdrop as poster-based audio, instead of a plain dark background.
Fixed โ
- Fullscreen: the right-click / gear menu wouldn't open: the menu portals to a body-level layer to escape page clipping, but in fullscreen the player is the fullscreen element so that layer rendered outside it โ the menu now stays within the player in fullscreen.
- Context-menu toggles didn't reflect their new state: Ambient Mode / Stable Volume / Loop / Rotate / HDR toggled from the menu now update their On/Off label and highlight (the state was written to the wrong place while the menu was portaled).
- Volume slider offered a 200% boost on native audio: a single external audio source plays through a native
<audio>element that can't boost โ the slider now caps at 100% there.
Changed โ
- Wider context-menu submenus: audio-track and audio-output submenus are wider so long track / device names don't wrap.
[0.3.4] - 2026-07-11 โ
Added โ
- OS Media Session โ lock-screen & hardware-key controls: title metadata, artwork, and play/pause/stop/seek controls on the OS lock screen and notification shade via
navigator.mediaSession, with a synced position scrubber. - Press-and-hold to 2x (touch): YouTube-style long-press speeds playback to 2x while held, reverts on release.
- Settings (gear) button: touch-friendly way to open the context menu now that long-press drives hold-to-2x.
- "Play at 1x" stutter hint: OSD nudge when a heavy source can't sustain above 1x (dropped video frames, smooth audio), with a cooldown.
- 360ยฐ/VR seek-bar preview reprojection: seek-bar hover preview now matches your current viewing angle instead of the raw flat frame.
- Themeable control-bar colors: more chrome colors driven by overridable
--movi-*CSS custom properties for embedder theming. - Volume boost to 200%: the volume slider now reaches 200% (VLC-style) with a tinted boost zone above the unity mark, for quiet sources.
- Screen-reader accessibility: an off-screen
aria-liveregion announces captions, and the seek/volume controls are now realrole="slider"widgets with spoken position text. - QoE analytics (
movi-qoe): a versioned QoE event stream (startup, rebuffering, bitrate switches, decode-fallback, errors, heartbeats) via a DOM event,addQoeSink()/getQoeSession(), and a built-inbeaconSink(url). - Framework wrappers + typed element: official typed
movi-player/react/vue/sveltewrappers (subpaths of the one package โ no extra install), plusHTMLElementTagNameMaptyping for<movi-player>. - Embed / headless bare player +
noerrorscreen: nocontrolsattribute makes<movi-player>a pure display surface (no resume dialog, empty state, spinner, or mouse interaction);noerrorscreenalso suppresses the built-in error overlays. - Number-key seeking (
1โ9):1โ9jump to 10%โ90% of the timeline (YouTube-style), alongside0/Home. - Swipe-to-dismiss touch menu: the gear-opened context menu is a right-side drawer on touch โ drag it toward the right edge to close it. Vertical scrolling and taps inside the menu are unaffected.
- Pinch to change aspect fit in fullscreen (touch): a two-finger pinch switches the fit YouTube-style โ spread to zoom-to-fill (crop), pinch in to fit โ with the same Fit/Fill OSD. Fullscreen-only (won't fight inline page pinch-zoom); excluded from 360ยฐ mode.
- iOS Safari pseudo-fullscreen fallback: iOS Safari only allows element-fullscreen on
<video>and the player renders to canvas โ fullscreen falls back to a CSS viewport-fill mode (behind Safari's toolbars) with forced-landscape rotation for landscape video. iPad / real element-fullscreen browsers unaffected. - Small-player control bar & menu overhaul: usable down to ~100px wide โ tighter padding/buttons below 400px, PiP folds behind the "more" toggle, the expanded cluster scrolls horizontally, fullscreen folds in below 290px, the resume dialog stacks instead of clipping, dropdown menus cap to the room inside the player, and the gear hides while a bottom dropdown or the timeline is open.
Fixed โ
- HTTP streaming without cross-origin isolation: HTTP(S) sources no longer need COOP/COEP (
SharedArrayBuffer) โ single-threaded Asyncify WASM falls back to a plain-buffer path; fixes aTimeout at 0on pages without the headers. The headers now only enable an optional zero-copy fast-path. - Fullscreen forced landscape for portrait video (Android): the orientation lock now reads the effective display rotation, so a portrait clip stored as landscape-frames-plus-rotation stays portrait in fullscreen.
- Audio-strip (collapsed) mode: shortcuts work while collapsed; the gear aligns with the title, stays visible and appears on load; the touch menu no longer overflows; the strip reflows on late cover art; a strip with no
controlshides itself. - Picture-in-Picture cursor: the cursor stays visible above the controls in document PiP.
- Touch hold-to-2x too eager: threshold raised to 600ms and the gesture cancels when it becomes a scroll.
- Resume-dialog selection ring: visible on pointer devices (a global
outlinereset was hiding it), hidden on touch. - Blank snapshot on hardware AV1: falls back to the decoded
VideoFramewhen WebGL read-back is blank. - Settings gear on non-touch: hidden where right-click already opens the menu.
- Safari first-load flash: no flash of unstyled overlays on first load.
- OS Media Session focus: the silent audio anchor is now โฅ5s so the OS grants full media-key focus.
- Volume clamping: native-element volume clamped to
[0,1]; the slider caps at 100% unless boost is on. - Context menu clamped on-screen: the menu repositions after opening so it never spills off-viewport.
- Lite / proxy browsers without cross-origin isolation: the player runs on reduced browsers with no
SharedArrayBuffer. - Open-GOP CRA-opening HEVC stuck buffering: a seek now accepts a CRA at or before the target instead of waiting indefinitely for an IDR.
- TrueHD/DTS buzzy/jittery audio around seeks and replays: software decoder now flushes on seek, a cold-start cushion applies on every seek/replay (not just first play), and replay no longer trips a spurious desync resync.
- Multi-audio-track files stalling repeatedly: unused audio streams are now discarded at the demuxer level so the active track isn't starved by interleaved packets from a track nobody's listening to.
- Crash on load with newer WASM builds: heap bytes are now copied out before
TextDecoder.decode()(resizableArrayBufferheaps threw and blocked every file from loading). - Playback stalling after resuming from the background: the un-throttled background decode timer now restarts on resume (including via Media Session), so audio no longer starves and video no longer jumps ahead of audio.
- SigV4 presigned URLs (S3 / R2 / GCS) failing to load: falls back to ranged/plain
GETwhen aHEADprobe returns 403/401 instead of treating it as access-denied; extension link probe also recognizes presigned links without a file extension. - Context-menu submenu options all showing "active" (touch): Fit and Speed submenus no longer accumulate a stale highlight.
- Selected-track text unreadable in light theme + auto-hide stuck open: readable codec/language text in light theme; switching audio tracks restarts the auto-hide timer.
- VS Code extension bundle out of date: republished with the current player build (was missing the 0.3.3 audio-output/VR/FLAC fixes).
- VS Code: fullscreen button hidden despite fullscreen working: the webview iframe reports
fullscreenEnabled=false(so the button auto-hid), but fullscreen works via the extension host โ the webview now grants the capability so the button stays. - Embed: dead fullscreen/PiP/audio-output controls no longer shown: hidden (instead of silently failing) when the embedding iframe's Permissions Policy disallows them; PiP also retires itself if opening its window throws.
playsinlinenow also gates touch gestures: an inline touch player suppresses swipe-seek/volume gestures so they don't fight page scroll (resumes in fullscreen). The separategesturefsattribute is now deprecated (still honored).- Right-click context menu clipped by page layout: the menu portals to a body-level layer so it escapes clipping ancestors (a wrapper's
overflow:hidden) instead of being chopped at the player edge; submenus are viewport-aware in this mode. - Resume-dialog selection ring didn't follow the pointer: the ring now moves to whichever button the pointer is over, matching arrow-key behavior.
- Scrub thumbnail previews depended on attribute order:
<movi-player src=โฆ thumb>no longer silently builds with previews disabled; order doesn't matter, and the timeline key (T) generates thumbnails regardless of thethumbattribute. - Timeline panel: thumbnails touching + controls-bar flash on close: bigger thumbnail-strip gap and a smaller hover/active pop so thumbnails don't overlap on small players; closing no longer flashes the auto-hidden controls bar.
- 360ยฐ mode: scroll-wheel accidentally zoomed the view: removed the wheel-zoom listener so page scrolling behaves normally; drag-to-look and pinch-to-zoom remain.
- Aspect-fit menu item didn't show the Fit/Fill OSD: changing aspect from the context menu now flashes the same OSD as the button and keyboard shortcut.
- Pinch-to-fit and press-and-hold-to-2x could fire together: a second finger landing cancels hold-to-2x so a pinch can't trigger 2x mid-gesture.
- HTTP streaming: large remote files restarted mid-playback on small out-of-window reads: served with a one-off range fetch while the main stream keeps running (fixes the request "cancelling" on large torbox-style files), with a cap on consecutive one-off fetches so a real seek still restarts the stream. (Thanks @anilabhadatta.)
[0.3.3] - 2026-06-29 โ
Added โ
- Immersive / VR video (
vrattribute): 360ยฐ equirectangular, 180ยฐ (VR180), fisheye, side-by-side stereo (3D), and stereographic "little planet" projections, rendered with a WebGL2 fullscreen-quad raycast and a spring-animated look-around camera (drag / arrow keys / pinch-zoom). Auto-enters the right projection from the source's spherical metadata โ no toggle UI โ or force it with tokens (vr="180",vr="fisheye sbs",vr="littleplanet"). Opt-in on-screen joystick viavrpad. - Audio output device selection (
audiooutputattribute /setAudioOutput()API): route playback to any system output device (speakers, Bluetooth, virtual).getAudioOutputs()lists devices,setAudioOutput(deviceId)switches โ it also accepts a label substring (e.g."Headphones") since device ids are session-salted โgetAudioOutput()reads the current sink, and anaudiooutputchangeevent fires on change. Surfaced as an "Audio Output" submenu in the right-click menu (the browser asks for device permission on first use; granted hosts list devices directly). Routed throughAudioContext.setSinkId.
Fixed โ
- FLAC audio playback: FLAC now always uses the software (FFmpeg-WASM) decoder โ WebCodecs' FLAC decoder throws
EncodingErroron these streams and the errorโsoftware fallback didn't recover in every browser, leaving FLAC silent. - Title HTML-entity decoding: titles from scraped / download-site sources that carry entities like
"(and the non-standard&Quot;),&,'now render as the real characters (",&,'). - Audio-strip title placement: when an audio file with a title collapses to the thin control strip, the title now sits in its own row above the controls instead of overlapping the control row.
- Controls auto-hide vs. the timeline & menus: the control bar no longer auto-hides while the storyboard timeline (
T) is open or when clicking a thumbnail in it, and it correctly re-arms its inactivity auto-hide after the right-click menu closes.
[0.3.2] - 2026-06-17 โ
Added โ
- Desktop app โ Windows / macOS / Linux (
desktop/): a new Electron app wrapping the player engine. Plays MKV, HEVC, AV1 and 4K HDR locally through the same WebCodecs + FFmpeg-WASM pipeline, served from a cross-origin-isolated localhost server so the WASM demuxer keepsSharedArrayBuffer. Includes drag-and-drop, native Open With / file associations for every supported format, URL playback through a built-in proxy (no CORS limits), a multi-file playlist with auto-advance, recent files, an Open-URL dialog with clipboard paste, full-window keyboard shortcuts, and a native always-on-top Picture-in-Picture window (Electron doesn't render Document PiP, so PiP is a real OS window that hands the source off and resumes on return). Cross-platform installers (dmg/nsis/AppImage+deb) and document icons via electron-builder.
Fixed โ
- Software-decoder fallback without WebCodecs: when the browser has no WebCodecs
VideoDecoder(e.g. Firefox, especially on mobile) the player now falls back to the WASM software decoder instead of failing โ video was left stuck buffering while audio fell back on its own. - Seek before ready is queued: the
currentTimesetter now holds a seek requested before the player is ready and applies it on the next seekable state, so a hand-off / early seek no longer stalls on a still-loading source. <movi-player hidden>now hides: the component's:host { display: block }was overriding the UA[hidden]rule, so the standardhiddenattribute did nothing.- Centre play/pause + loading spinner positioning: sit at the true centre on the initial / autoplay-off screen and lift slightly to balance the controls bar only once playback has started; they also animate in compact / PiP layouts. (Previously keyed off a
:host:has()rule that some engines, e.g. Electron's Chromium, don't apply to shadow descendants.)
[0.3.1] - 2026-06-10 โ
Added โ
- MPEG-DASH playback (
.mpd) (closes #9): DASH manifests now play alongside HLS through the adaptive pipeline. - Unified adaptive streaming via Shaka (HLS / DASH / Smooth): Shaka is the primary engine for
.m3u8/.mpd/.ism; hls.js and dash.js remain as automatic fallbacks.DashFallbackplays bare-BaseURLmanifests Shaka rejects via the demuxer. - Live-stream UI:
LIVEbadge that jumps to the live edge, DVR-window seeking, Auto-mode quality badge. - Custom request headers (
headersattribute / property): Auth tokens / signed headers across manifest + segments, progressive HTTP, thumbnails, and the encrypted source. JSON-string attribute or object property; alsoPlayerConfig.headers. - Audio-only data-saver mode (
audioonlyattribute /audioOnlyproperty): Play just the audio to save CPU/bandwidth โ muxed files skip the video decode, streams switch to an audio-only rendition, split sources stop downloading the video body. Live-toggleable, forces the album-art strip UI. - Non-range (no-Range) server playback: Servers that ignore
Range(200not206) now play via a forward-only sliding window (linear mode); a newlinearmodeevent lets the UI adapt. - MPEG-5 LCEVC decoding (
lcevc/lcevcurlattributes): Opt-in LCEVC enhancement-layer decoding for adaptive streams. - Muted-autoplay fallback for split native-audio tracks: rolls video muted + shows the tap-to-unmute pill instead of freezing.
- Extension: detect
.mpd(DASH) URLs in page scan. - VS Code extension: adaptive streaming via URL โ
Movi: Open Video from URLplays.m3u8/.mpd/.ismby loading them directly in the player engine (no host byte-range proxy); progressive files still use the proxy. - VS Code extension:
.ts(MPEG-TS) added to the open-file dialog (not the single-click association).
Changed โ
- DRM key-system order: Widevine โ PlayReady โ FairPlay.
- Manifests load directly, never via
/proxy: fixes relative segment resolution and the proxy content-type allowlist for.m3u8/.mpd/.ism. - Size resolution hardened: HEAD โ ranged GET โ plain GET retry chain for CDNs that strip
Content-Length. - deps: add
shaka-player ^4.11.2,dashjs ^5.2.0; bumphls.jsto^1.6.16.
Fixed โ
- Robust startup: visible play affordance on blocked autoplay, guarded first-play seek, background-tab autoplay deferred until visible.
- Don't flash wrapper errors mid-fallback: no spurious "Try Software Decoding" while falling back behind Shaka.
- HTTP errors surface real messages:
403/404/5xxmap to access-denied / not-found / server-error. - Wake lock: skip while hidden, retry transient failure, re-acquire on visible/resize.
- Don't collapse a loading/errored video into the 56px audio strip on resize.
- App proxy: don't proxy same-origin URLs (522 loop); don't magic-sniff tiny range probes.
- Volume slider opens on first touch; ambient re-applies after
srcchange. - Cover-art backdrop blur via CSS
filter:blur()(works in Safari < 17); audio-onlyposterrenders as album art. - Audio-only replay/loop restarts the native
<audio>; no context menu withoutcontrols.
[0.3.0] - 2026-06-02 โ
Added โ
- Signalsmith Stretch audio rate-change pipeline: Replaced SoundTouch with Signalsmith Stretch as the sole pitch-preserving time-stretcher.
- First-class audio-only support with strip UI: Audio-only files play through the canvas pipeline with a dedicated audio strip UI (cover art, title, progress bar, controls).
- Muted-autoplay fallback with tap-to-unmute: When autoplay is blocked, the player starts muted and shows an "unmute" pill overlay.
- Cover art display for audio: JS-only album art extraction via an isolated demuxer context.
- Custom
SourceAdapterfor<movi-player>andMoviPlayer(closes #7): Plug any custom byte protocol directly into the element or player. - File-source preload settling gate:
play()and resume gated until initial preload fills. - YouTube-style centre play button: Always-visible large play/pause icon.
- Unified controls chrome โ dark gradient bar + redesigned OSD: Opaque backgrounds replace backdrop-filter blurs.
- Extension: playlist shuffle, autoplay toggle, next button
- Extension: hover-probe links + opt-in toggle + flag detection
- Compare page (
/compare): Side-by-side native vs movi-player. - VS Code extension โ URL streaming, multi-window commands, Activity Bar entry
- Homepage redesign
Changed โ
- 4K playback rate cap raised to 2x (only 8K+ capped at 1.5x)
- Renderer queue split for 4K vs 8K
- UI update loop throttled to 4Hz
- Volume slider uses perceptual (log) gain curve
- Ambient mode FBO mirror (no more full canvas readback)
- Thumbnail hover latency cut
- Dropped backdrop-filter blur from all UI surfaces
- README redesigned, browser support updated (Firefox 130+)
- AGENTS.md shipped in package
Fixed โ
- Decode: RASL leading pictures after CRA/BLA seek, DoVi/HDR HEVC on hardware, open-GOP HEVC fallback, tiny packet drop, AV1 Temporal Delimiter OBU
- Seek: prefer IDR fall back to CRA, resume into buffering on timeout
- Playback: mid-playback decode-error recovery, auto-start on rate restore, rapid seeks stuck, stall detection during recovery, audio tail on rate change, EOF detection
- Audio: log gain curve, pitch-shift at startup, audio-only shortcuts, near-end seek clipping
- UI: controls flash, play/pause icon state, centre play flickering, cursor hiding, loop toggle replay
- Canvas: display-p3 fallback, rec2100-pq preservation
- Thumbnail: recreate dead WebCodecs decoder
- Extension: video link detection, loop replays
- HTTP: surface server errors
[0.2.3] - 2026-05-07 โ
Added โ
- Subtitle Delay / Offset (closes #4): Shift subtitle timing relative to video โ
subtitledelayattribute,subtitleDelayproperty,setSubtitleDelay()/getSubtitleDelay()API methods, and a newsubtitledelaychangeCustomEvent. Sign convention matches VLC/mpv (positive = subtitles later). UI cap ยฑ300s with widened input. Z/X hotkeys nudge by 100ms per press; OSD shows the current offset. Auto-prefetch when delay becomes non-zero so negative offsets work (cues from stream positions ahead of the demuxer cursor). Applied at the renderer's active-cue check so a single offset works for text and image (PGS/DVB) cues without re-decoding. - Subtitle Customization Panel:
subtitlesize,subtitlecolor,subtitlebg,subtitleedgeattributes, plus an in-player customize panel persisted to localStorage. Size multiplier drives both bitmap (PGS/VOBSUB) and text (SRT/ASS/VTT) cues; edge style applies to text subs. - Subtitle Transcript Browser: Full-cover panel with search, click-to-seek, active-cue highlight, italic/bold/entity rendering, and delay-aware timestamps. Click on a live caption opens the transcript at the current cue. Backed by a native
movi_prefetch_subtitle_cuesthat usesAVDISCARD_ALLso a 700 MB scan touches only subtitle packets, not every audio/video body. - Karaoke Captions for VTT: Tag-only-token folding, min-width anchor measured offscreen, render-key cache to stop the 60fps
innerHTMLrewrite that prevented fade-in during playback. Format-aware backdrop (VTT-only). - Premuxed Quality Menu: Multiple
<source data-height="...">children give a YouTube-style quality picker for plain MP4/MKV files โ no HLS manifest needed. Adopt/release native<audio>across switches preserves the user-activation token so the next switch isn't blocked by autoplay policy. - Multi-Language Audio via
<source kind="audio">: Two or more audio<source>tags withsrclang(orlabel) become parallel language tracks; the player surfaces the audio-language menu andgetAudioLangs()/selectAudioLang()work exactly as for muxed tracks. Default pick: explicitdefault/data-defaultโ first locale match (navigator.languageprefix) โ first track. Single<source kind="audio">continues to use the legacy split-audio path. - External Subtitles via
<track>: Standard<video>-style declarative markup โ<track kind="subtitles">,kind="captions", or nokindare recognized. Readssrclang,label, anddata-format(defaults to VTT, setsrtfor SRT sidecars). Lets integrators ship full caption configurations as plain HTML without wiring upsource({ subtitles: [...] })from JS. - Host Fullscreen Handoff: New cancelable
movi-fullscreen-requestCustomEvent +setHostFullscreen(active)method. Lets embedders (VS Code webviews, custom app shells) take over fullscreen with their own chrome while keeping the player's toolbar icon, OSD, and context-menu label in sync. Fullscreen state is now reflected in the context-menu label. - File Revoked Event:
filerevokedCustomEvent fires when the browser silently revokes aFilehandle (mobile background / memory pressure).FileSourceraces each chunk read against an 8s timeout โ no more demuxer hanging forever โ and surfaces the failure via a one-shotonRevokedcallback onMoviPlayer. MoviPlayer.hasAudibleSource(): Unified gate covering muxed audio, split native<audio>, and HLS audio (which lives inside the hidden native<video>). Used internally to decide whether to show volume controls / accept volume hotkeys.- VS Code Extension: New
vscode-extension/package (Marketplace 0.2.5). Webview-hosted player registered as a CustomEditor โ single-click opens any MP4/MKV/HEVC/AV1/WebM/MOV/TS file VS Code can't natively play. True streaming via a customDataSource(webview'sFileproxy delegatesslice().arrayBuffer()to extension-hostfs.createReadStreamchunks); memory cost drops from O(filesize) to ~chunk size, so multi-GB and 8K HDR files no longer hit the 4 GB Blob limit. Movi fullscreen toggle hides workbench chrome with auto-cleanup on crash. OS wake lock (caffeinate -i/systemd-inhibit/SetThreadExecutionState) held during fullscreen. Multi-window playback viamovi.openInNewWindow. Output channel surfaces bundled-player logs. - Web App Explorer-Style Playlist: Folder hierarchy tree with collapsible groups + guide rail, multi-select, live search (folders auto-expand on match), keyboard navigation (Tab toggle, Up/Down/Enter, Esc). Thumbnails + metadata cached in IndexedDB so reopening the same files skips every WASM call. SEO overhaul, landing animations, gradient circle brand mark.
- Chrome Extension Explorer Playlist: Folder tree, breadcrumb, badges, progress, drag/drop, multi-file + folder picking. Shared isolated WASM instance (2-instance budget) for thumbnail generation, cached in IndexedDB across sessions. Install detection on moviplayer.com hides the "Add to Chrome" prompt when the extension is already present. Gradient circle play-button branding to match the main app.
- Stats 8K / 16K Tiers:
4320p(8K) and8640p(16K) labels in both native and HLS stats paths โ previously bucketed as 4K.
Changed โ
- HLS Volume Controls Now Visible: Volume button,
ArrowUp/ArrowDownhotkeys, and volume OSD were gated only on muxed/split audio, so HLS streams (audio inside the native<video>) had no mute control. Consolidated behindhasAudibleSource()so the HLS path is covered too. - Audio Decode Stays Running While Muted: The demux loop no longer drops audio packets when muted โ
AudioRendererkeeps gain at 0 instead. Fixes the "atak atak" judder on unmute, where the audio clock pivoted forward to the demuxer's lookahead (~1โ3s ahead of presentation) andCanvasRendererchased it 25%/frame. - Bluetooth A2DP Keepalive: Pause path now suspends the AudioContext but starts a near-silent looping
<audio>element so the OS audio session stays claimed. BT devices stop dropping/re-pairing on every pause without re-introducing the "2โ3s jump-ahead on resume" regression. - DPR-Scaled Canvas Backbuffer: Canvas backbuffer scales with
devicePixelRatio(capped at 2ร) so downsampling 4K/8K sources stays sharp. CSS dimensions remain in logical pixels. - Encrypted Source Static Import:
EncryptedHttpSourcehoisted to a top-level import โ no more async boundary on every encrypted load. Matches the other source adapters. - FFmpeg Bumped to n8.1.1: Picks up upstream point-release fixes on the n8.1 branch.
dvbsubtitle/dvdsubtitledecoders renamed todvbsub/dvdsubto match. - Subtitle Default Sizing: Bumped the text-subtitle base size and replaced the desktop-era 60px floor on bottom padding with a height-proportional 8% (24px floor) so subtitles don't crowd into the middle of small embeds.
- Menu Animations: Pop-in / pop-out on the audio, subtitle, quality, and speed dropdowns plus a fade between the customize panel and track list. Bottom-controls dropdowns enforce one-at-a-time. Click on the player area closes any open menu instead of toggling play/pause.
- Keyboard Shortcuts Ignored While Typing in Inputs: Hotkeys no longer fire when an input/textarea inside the shadow DOM is focused.
- Audio Menu Always Shows Language Code:
formatAudioBadgepreviously dropped the language code when channel info was available, so muxed tracks from MKV/MP4 displayed only "AAC Stereo" with no way to tell languages apart. - COOP/COEP Hard-Required: README/docs corrected โ the player hard-blocks without
Cross-Origin-Opener-Policy: same-origin+Cross-Origin-Embedder-Policy: require-corp. Surfaces a "Security Headers Missing" diagnostic instead of a cryptic timeout. Mentioncoi-serviceworkeras a static-host workaround.
Fixed โ
- ~1s Fullscreen Freeze: ResizeObserver fires repeatedly during the fullscreen animation. Each call set
canvas.widthtwice (inupdateCanvasSizeandCanvasRenderer.resize), clearing the WebGL framebuffer on every burst. Coalesced same-size resizes, dropped the duplicate width/height assignment, dropped the<video>.width/heightno-ops. - WebGL Context Loss Recovery (Mobile): Capture canvas to JPEG on
visibilitychangeโhiddenwhile the GL context is alive; on return, hide instantly ifgl.isContextLost()is false, otherwise leave it up sohandleContextLost/handleContextRestoredcan run recovery without a corrupt framebuffer flashing through. RestoredisLoadingclear soinitializePlayerdoesn't early-return after long minimize. 5s cooldown between audio-resync seeks prevents stutter loops on slow software audio decoders. - HLS First-Frame Black: Manifests without
RESOLUTIONcausedconfigure(0,0)on the canvas renderer, producing a black frame that only cleared on the next ResizeObserver tick. Defer configure to<video> loadedmetadatawhen manifest dims are missing. - HLS Quality Badge / Fit Animation: Re-emit
tracksChangeonLEVEL_SWITCHEDso the gear badge reflects the active rendition in Auto mode. Skip smoothing-state reset on same-size canvas resizes so fit-mode toggles can lerp instead of snapping; clone last frame in the direct-render path so HLS paused redraws have a source to animate against. - PauseโResume 2โ3s Jump-Ahead: Pause now preserves scheduled buffers and sync anchors exactly, so the first packet drained from
pendingPrebufferPackets(whose timestamp is the demuxer's lookahead) doesn't become the newfirstBufferMediaTimeanchor on resume. - Aspect-Ratio Change While Paused: Fit-mode change now repaints the canvas after a seek-to-paused and animates via a dedicated RAF loop instead of snapping. Poster overlay tracks the active fit mode, lets pointer events pass through so dblclick / gesture handlers still fire, and stays hidden until a source is set.
- Mobile Speaker Tap: Switched from
PointerEvent.pointerType(Android Chrome synthesizes click withpointerType="mouse"from a touch tap) tomatchMedia("(hover: none)")as the primary touch signal.composedPath()to cross shadow boundary in the close-volume listener. Mobile media query no longer hides the slider on.active. - Embed Security Headers Diagnostic: Every
src=change now re-runscheckSecurityHeaders(), so reused players surface "Security Headers Missing" instead of a cryptic "Failed to open media: Timeout at 0". - Pause Buffering Loop / Single-Track Streams: Pause buffer loop required both audio and video targets via AND, so audio-only / video-only streams never satisfied it and ran to the 3000-packet safety cap (~30s of demux), surfacing as a burst of cache-read spam after pause. Now only checks targets for tracks that exist.
- A/V Drift Loop on Hardware Burst Decoders: Hardware decoders that emit 8K frames in bursts queued many future-PTS frames; on >60Hz displays the fallback drained them faster than wall-clock and tripped the audio-desync resync seek loop. Reject frames more than one frame interval ahead of playback time.
- Audio-less Video in Background Tab: Without an audio track, the background
processLoophad no backpressure (video decode skipped, no audio buffer to fill), so the demuxer raced to EOF in seconds. Pause on hide and auto-resume on visible. - Volume Keys for Native Audio Sources: Hotkeys + OSD gated on
getAudioTracks().length(empty for split video/audio sources). Now also accepthasNativeAudio(). - Buffered Bar Stability: Buffered bar is now monotonic between seeks; pause-time buffering no longer pushes past
HttpSource's buffered end and trigger a window-resetting refetch. - PGS Subtitles On-Canvas: Image-subtitle overlay was sized in DPR-scaled buffer pixels, pushing the flex-anchored bitmap off-screen on retina. Switched to the canvas's CSS rect (matching the text-subtitle path).
- Skip A/V Desync Check When Muted: Demux loop drops audio decode while muted, so
maxScheduledMediaTimefreezes andgetAudioClock()clamps to a stale value โ disabled the 500ms desync detector while muted. - No Corrective Seek After Unmute: Reset the desync cooldown on unmute so the audio clock can catch up first instead of forcing a resync seek (visible as a loading shutter).
- Progress Handle at 0%: Dropped the
Math.max(1, โฆ)floor so the handle sits at 0% at the start instead of jumping in from 1%. - Pre-Play Seek: Re-arm the
seekTargetTimefilter on first-play re-seek so Open-GOP recovery frames (1โ2s behind the seek target) get dropped instead of presented; matching drop on resume from pause. getCurrentPlaybackTimeFrozen When Paused:updateActiveSubtitlecalled viasetSubtitleCuesduring pause can no longer jump to a wall-clock-driven time.- PiP Exit Buffer Resize: Invalidate
_lastCanvasW/Hon PiP exit so the buffer resizes back to host dimensions instead of staying pinned at PiP resolution. - Seek OSD Accuracy: Track the actual delta between the pre-seek time and the clamped target instead of a fixed 10s step. Anchor chained presses on the previous target. Dismiss the OSD on a boundary hit / sub-second / NaN delta.
- Coalesce Rapid
currentTimeSets: Overlapping seeks now collapse into a single tail seek instead of queueing them all. preventScrollon Hover Focus:focus()on mouseenter no longer yanks the page when the player is partly off-screen.- Subtitle Re-render on Resize via rAF: Previously a burst per ResizeObserver tick stalled the presentation loop on window drags.
- Centre Non-VTT Subtitle Lines: Multi-line SRT cues (e.g.
"- A long line\n- short") now sit at the player's centre instead of drifting left. - Worker /proxy Probe Failures: Transient probe errors no longer get misreported as
415 Unsupported Media Type.
Documentation โ
- WebCodecs team outreach playbook (
docs/webcodecs-outreach.md).
[0.2.2] - 2026-04-26 โ
Added โ
postertimeAttribute: Generate a native-resolution poster from any timestamp without an explicitposterURL. Accepts"10%","5","1:30", or"0:01:30". Uses an isolated thumbnail pipeline (WASM +ThumbnailBindings), respects rotation metadata, and is race-guarded so in-flight generators can't paint stale frames after asrcchange.dispose()Method: Tears down the internal player and resets transient UI (subtitles, timeline, time, title, generated poster) back to the no-source state. Called automatically on everysrcchange so playlist-style flows never leak state between sources. Safe to call when nothing is loaded.playingGetter: Read-only boolean that'strueonly while the player is actively playing โ distinguishes it fromready,loading,seeking, andbufferingstates (precise inverse ofpaused).MoviElement.cleanVideoTitle(filename)Static: Utility exposed for playlist UIs to derive the same cleaned title the player uses internally โ useful for computing the resume localStorage key (movi-resume:<cleanVideoTitle(name)>).- Folder Playlist (web demo app): Sidebar/below-player playlist via File System Access API (with
webkitdirectoryfallback). YouTube-style items with thumbnail, duration, HDR chip, codec/quality/size meta, and watched-progress bar. Lazy thumbnail generation, natural-sort, autoplay-next toggle, drag-and-drop multi-file support.
Changed โ
play()Semantics: Now queues a play intent duringisLoadingand flushes it frominitializePlayer()'s finally block โ matchesHTMLMediaElementbehavior. Previously bailed silently when called during load.- Software Decoder Fallback Per-Source: Choosing "Try software" no longer sticks across
srcchanges. The next video gets a fresh hardware-decode attempt; theswattribute is cleared on dispose. - Encrypted Playback Protocol:
EncryptedHttpSourcerewritten โ block prefetch high-water/low-water tuning, concurrent-stream cap,getPosition()reports the real read cursor, and parent position field is kept in sync so buffer math stays honest. Encrypted-server ported to match the new protocol. - Buffer Tuning: Runtime tuning of prefetch high-water, refill threshold, and block cache cap via the existing
buffersizeattribute. README/docs corrected to clarify the value is in megabytes (not seconds) and applies to both HTTP and encrypted sources. - Production Bundles: Re-enabled terser
drop_consoleanddrop_debuggerso release builds ship without dev-only logging. - Build Stability:
app:releasescript ties build + R2 upload + worker deploy into a single command. Build version cache-bust scoped to the quoted__BUILD_VERSION__literal so unrelated lines aren't rewritten.
Fixed โ
- Post-Seek A/V Sync: Cap the post-seek audio gap at 200ms โ when the first video frame after a seek arrives late (sparse keyframes / slow HEVC+HDR decoders), sync the clock to video time and drop stale audio instead of syncing to the earliest audio packet. Small gaps still prefer audio for continuity.
- Pre-Play Seek Position: Scrubbing the timeline before pressing play no longer resets to 0 โ the first-play poster-seek now reads
clock.getTime()instead of a hardcoded start time. Pipeline is flushed on user seek so prebuffered start audio doesn't briefly play before jumping to the target. - Fully-Cached Buffered Duration: Buffered range now reports the full media duration when the file is fully cached, instead of stopping at the last network read.
- Buffer Indicator Race: Collapsed the seek-race scan sweep that could draw a phantom buffered range mid-seek.
- Encrypted Thumbnails: Share the main source for thumbnail reads instead of opening a parallel session โ cuts redundant token churn. Concurrent stream cap prevents seek-storm thrash. Hardened thumbnail read failures (no more fragile retry/cooldown loop).
- Worker
/proxyEmpty 206: Retry empty 206 responses from upstream before streaming back, so transient origin hiccups don't surface as broken playback. - Worker Probe Failures: Transient probe errors no longer get misreported as
415 Unsupported Media Type. - TMDb Title Parser: Detect TV shows when the episode title trails the
SxxExxcode (e.g.,Show.S05E01.Title).
Security โ
- Worker Referer Allowlist:
/proxyand/eproxyendpoints now gate requests by Referer to block hotlinking from unauthorized origins. - Worker Magic-Byte Validation:
/proxyresponses are validated against expected media magic bytes before being streamed back, mitigating MIME confusion attacks.
[0.2.1] - 2026-04-16 โ
Added โ
- Persistent Preferences:
stableVolume,ambientMode, andhdrtoggles now persist via OPFS alongsidevolume,muted, andplaybackRate. User toggles win over HTML attribute defaults on subsequent loads. - Split-Source Volume Control: Volume button now visible when a separate native audio element is loaded, even if the video file has no muxed audio track.
- Smart Title Extraction: VLC-style filename cleaning strips release tags, codecs, and quality markers from tab titles.
Content-Dispositionfilename used when the server provides one. - Chrome Extension: Local file playback via popup file picker, drag-and-drop onto the player page, and a redesigned popup layout.
Changed โ
- Context Menu: Slide-panel variant now only used on touch devices (
pointer: coarse); narrow desktop windows get the regular hover-driven menu. - Context Menu Scrolling: Max-height clamped to player height with subtle scrollbar styling so tall menus stay accessible on short players.
- Theme Color Cascade:
themecolorattribute now flows to--movi-primary-lightand--movi-primary-darkviacolor-mix, so active menu items and highlights follow the custom theme. titleAttribute: No longer triggers the browser's native tooltip on hover โ title is rendered only by the in-player overlay.- Subtitle/Audio Track Menus: Show language codes alongside track labels for clarity.
Fixed โ
- Short Video Stutter: Prebuffer media before
readysoplay()doesn't immediately stall on short clips. - Background Audio at 50/60 fps: Skip video decode while hidden so audio keeps flowing on high-fps content.
- Narrow Viewport Controls: Buttons, gaps, and center play button tightened on viewports โค 480px to prevent the controls bar from overflowing the player box on iPhone 12 Pro-class widths.
- Empty State Placement: "No Video" placeholder no longer clips into the controls bar on short/narrow players.
- OSD Speed Icon: Correct speed icon shown when playback rate changes via hotkeys/context menu.
[0.2.0] - 2026-04-09 โ
Added โ
- Ambient Mode: Dynamic letterbox glow that samples video colors in real-time. Smooth 60fps color transitions via WebGL clearColor. Toggle with
Gkey or context menu. Works in fullscreen (letterbox) and normal mode (external wrapper).ambientmodeattribute. - Split Source Support: Separate video, audio, and subtitle file URLs via
videosrc,audiosrc,subtitlesrcattributes. - PGS Image Subtitles: Bitmap subtitle decoding with zlib decompression support.
- Network Disconnect Recovery: Intelligent CORS vs transient network failure detection (3-strike threshold). Online-event-aware backoff for instant retry on reconnection. Auto re-seek on recovery. 30s timeout on offline wait.
- Document Picture-in-Picture: Floating video window with play/pause, seek, mute, progress bar, time display, keyboard shortcuts, and back-to-tab button. Portrait video sizing. Rotation save/restore on PiP enter/exit.
- DRM Support:
drmandlicenseurlattributes for HLS streams with Widevine/FairPlay via EME API. - HLS Quality Menu: Duplicate resolutions show bitrate (e.g., "1080p ยท 5000 kbps").
- HLS Nerd Stats: Video codec, resolution, quality, frame rate, bitrate, buffer, HLS level, bandwidth, live latency, frames decoded/dropped.
- VLC-style Shortcuts:
Vsubtitles,Baudio,+/-speed,Lloop,Ustable volume,HHDR,PPiP,Gambient,Aaspect ratio. - Aspect Ratio Controls:
Akey cycles contain/cover/fill/zoom. Sub-menu with icons in context menu and bottom controls. - Stable Volume: DynamicsCompressorNode for loudness normalization. Opt-in via
stablevolumeattribute. - Nerd Stats: Press
Ifor codec, resolution, FPS, decoder type, buffer health, color info, and live network/disk activity graph. - Timeline: Press
Tfor auto-generated thumbnail strip with chapter support. Arrow key navigation, click-to-seek. - Chapter Support: Extract chapters from video metadata. Chapter markers on progress bar, chapter titles in seek tooltip.
- Video Rotation: Press
Rto rotate 90ยฐ. Metadata rotation auto-applied. Disabled during PiP. - Keyboard Shortcuts Panel: Press
?to view all shortcuts. - Resume Playback:
resumeattribute saves position to localStorage with resume/start-over dialog. - Encrypted Playback: AES-256-GCM chunked encryption with HMAC-SHA256 signed requests.
- Background Audio: Video keeps playing audio when tab is in background via Web Worker timer fallback.
- Chrome Extension: Popup with "Paste & Play" and "Play from Computer", context menu on video links, play button overlay on detected URLs.
- Privacy Policy: Published at docs site for Chrome Web Store compliance.
Changed โ
- Buffering state now stops presentation loop so frames accumulate for reliable recovery.
- Buffering exit requires video frames ready (with 3s fallback for async decoder delays).
- Pause during buffering allowed from all UI controls (click, keyboard, buttons, PiP, context menu).
buffering โ endedstate transition allowed for EOF during rebuffer.- Invalid packet size at EOF treated as EOF (not fatal error) for FFmpeg stale buffer data.
- HDR icon changed to text badge style in OSD and context menu (matches bottom controls).
- Extension description rewritten to remove excessive format keywords (Chrome Web Store compliance).
- Console logs dropped in production build.
Fixed โ
- Hardware decoder error recovery with keyframe cache and software fallback.
- Seek and play/pause during buffering state.
- Network disconnect causing permanent CORS misclassification when
navigator.onLinelags. - Stale stream loops from leaked online event listeners during backoff.
- Multiple concurrent fetch loops after network recovery.
- Clock advancing during buffering (presentation loop consuming frames).
- PiP rotation clipping โ rotation reset on PiP enter, restored on close.
- PiP portrait video oversized โ height-limited sizing for portrait aspect ratios.
- Pause-seek loading stuck โ
VideoDecoder.flush()1s timeout with reset+reconfigure fallback. - EOF not triggering โ relaxed condition with 0.5s tolerance.
- PiP canvas restore using
shadowRootdirectly. - PiP frame freeze on tab switch with
isPiPActiveguard. - EncryptedHttpSource network resilience matching HttpSource.
- Nerd stats graph fullscreen positioning and CSS specificity.
[0.1.5] - 2026-02-15 โ
Added โ
- Pitch preservation for playback rate changes
- Pitch preservation support for HLS playback
- MediaSession API integration for background playback and media controls
- HTTPS support for local development environment
Changed โ
- Simplified error messages to be more concise and consistent
- Replaced all hardcoded purple colors with CSS variables (--movi-primary) for full theme customization
- Enhanced center play button with theme color by default
- Updated loading spinner with responsive sizing and theme-aware colors
Fixed โ
- Improved playback stability with enhanced error handling and timeout management
- Resolved audio-video sync issues with hardware decoding
- Distinguished 403/401/404 errors from CORS errors for better error reporting
- CORS errors now propagate immediately instead of waiting for timeout
- Title bar z-index now properly positioned below control menus in mobile view
- Center play button backdrop blur now enabled on mobile/touch devices
- Controls no longer auto-hide when menus are open on mobile
[0.1.4] - 2026-02-11 โ
Fixed โ
- Resolved video stalling during playback and improved A/V sync
- Playback speed changes now take immediate effect on audio
- Auto-unmute when volume slider is moved while muted
- Mute button now correctly toggles audio muting
Previous Versions โ
See git commit history for changes in versions prior to 0.1.4.