# Current Limitations

[Return to UniversalMediaController Home](../index.html)

This document tracks what the HTML5 proof-of-concept milestone does *not*
do, either because it was explicitly postponed (see Docs/Version 1
Requirements.md, "Explicitly postponed") or because it was discovered
during implementation and needs a follow-up pass.

## Explicitly postponed (per the approved requirements)

- External player adapters (YouTube, Vimeo, Brightcove, Kaltura, Panopto,
  Wistia). Only the HTML5 adapter exists in this milestone.
- React, Vue, and Web Component packages (`Packages/React`, `Packages/Vue`,
  `Packages/WebComponent` remain empty placeholders).
- Global keyboard shortcuts and single-letter shortcuts.
- Transcript search/editing, bookmarks and notes, playback history/completed
  status, audio description authoring.
- Automated image recognition for title discovery (OCR / AI title
  generation) -- not used anywhere in this milestone.
- Provider metadata and native media metadata as title sources (items 6-7 in
  the full Docs/Architecture.md priority list) -- out of scope for the
  HTML5-only milestone, since HTML5 media elements do not expose a
  standardized metadata API comparable to a provider's.

## Discovered during implementation

- **Buffering announcements are not implemented.** The adapter emits
  `bufferingstart`/`bufferingend` events, but the controller does not
  currently turn them into announcements. Docs/Screen Reader Behavior.md
  only requires announcing buffering "when it materially interrupts
  playback" -- distinguishing a materially-interrupting stall from routine,
  brief buffering (including the brief buffering a seek itself can cause)
  needs real-world tuning that this milestone did not attempt. Treat this as
  a known gap, not a silent omission.
- **Page Up / Page Down on the volume slider are not implemented.** Docs/
  Keyboard Interaction Model.md allows this "if testing confirms consistent
  behavior"; native `<input type="range">` does not provide this by default
  in any tested browser, and adding it would require custom keydown
  handling that risks conflicting with browser/OS expectations. Left out
  rather than added inconsistently.
- **"Restart or Stop" was implemented as a single Restart control** that
  pauses playback and resets position to zero (it does not resume playing
  automatically). The source requirement listed "Restart or Stop" as one
  line without fully specifying which behavior was intended; pausing-and-
  resetting was chosen as the safer default so a restart never surprises a
  screen reader user with unexpected audio. If continue-playing-after-
  restart is the intended behavior instead, that is a one-line change in
  `universal-media-controller.js`'s restart handler.
- **Caption track selection (multiple tracks) is not implemented.** The
  adapter contract includes `listCaptionTracks()` and the HTML5 adapter
  implements it, but the UI only exposes a single on/off toggle, matching
  the "Captions toggle" requirement. Selecting among multiple tracks, if a
  media item ever has more than one, is not yet exposed in the controls.
- **No automated test runner is included.** Tests/Keyboard,
  Tests/ScreenReaders, and Tests/Browser contain test plans and the results
  of a scripted headless-browser verification pass performed during this
  milestone, but there is no `npm test` or equivalent -- see those files for
  what was and was not verified, including what still needs a human with
  real assistive technology.

## Milestone 2: accessibility-first control order

- **Audio Description and Transcript are not implemented in the HTML5
  adapter.** `supportsAudioDescription()` and `supportsTranscript()` both
  return `false`. Both controls exist, are first/third in tab order as
  required, carry the correct accessible name (Audio Description carries
  the media title as the first focusable control; Transcript stays
  concise), and remain `aria-disabled="true"` and keyboard-reachable rather
  than being removed. Activating either announces that the feature is not
  available for the media rather than doing nothing. Actually authoring
  audio-described tracks or a transcript engine remains future work (see
  Docs/Architecture.md, "Audio Description Engine" and "Transcript
  Engine").
- **Bookmarks and chapters capability methods exist but have no UI.**
  `supportsBookmarks()` and `supportsChapters()` are defined on the adapter
  contract (both return `false` for the HTML5 adapter) so a future adapter
  can report them, but neither capability corresponds to a visible control
  in this milestone's control order -- the requirement list did not include
  them.
- **The state machine does not yet model buffering as its own visible
  state.** `bufferingstart`/`bufferingend` adapter events still exist but
  are not wired into `ControllerStateMachine`; see the buffering item above,
  which still applies.

## Milestone 3: manual testing and external-player readiness

- **No external-player adapter is implemented.** This milestone deliberately
  only prepared the architecture (see Docs/Adapter Contract.md, "External
  player readiness") -- the constructor now accepts an explicit `adapter`
  instance and no longer requires its mediaItem to be an HTMLMediaElement
  when one is supplied. No Vimeo, YouTube, Brightcove, Panopto, or Wistia
  code was written.
- **The readiness change is unverified against a real provider.** The
  constructor's `adapter` injection point was verified by construction
  (reading the code path, and confirming the default HTML5 path is
  unaffected) but there is no second adapter implementation yet to prove it
  against. Treat the injection point as architecturally sound, not
  battle-tested.
- **`Tools/serve-examples.py` is a testing convenience, not a production
  server.** It exists solely to make local manual testing reliable (Range
  request support). It has not been reviewed for production use -- do not
  deploy it as-is.

## Milestone 4: real JAWS testing findings

- **Issue 1 (Audio Description, Captions, and Transcript not consistently
  reachable via Tab/quick-nav): the first attempted fix (below) did not
  resolve it in real JAWS testing, and a different, more fundamental fix
  has now been made.** The first attempt (previous milestone) set every
  control's final aria-disabled/aria-label/aria-pressed state before DOM
  insertion and added explicit `tabindex="0"`. A second round of real JAWS
  testing found the problem persisted. The most likely explanation:
  `aria-disabled="true"` itself is very plausibly the cause. JAWS's own
  Tab-in-Forms-Mode and quick-navigation features (F for form fields, B
  for buttons) are widely reported to filter out elements whose
  accessibility-tree state reports as disabled/restricted -- a JAWS-side
  navigation heuristic, unrelated to the browser's native DOM tab order
  (which `aria-disabled` never affects) and not something a `tabindex` can
  override, since it never depended on raw keyboard focus order in the
  first place. Unavailable controls (Audio Description, Captions,
  Transcript, and the same pattern used by Restart/Skip/Mute/Volume/Speed
  for a hypothetical future adapter that doesn't support them) **no longer
  set `aria-disabled` at all.** The control remains a fully ordinary,
  enabled interactive element from every assistive technology's
  perspective; "unavailable" is communicated entirely through the
  accessible name (e.g. "Captions, unavailable"), which every screen
  reader speaks as plain text and which no known quick-navigation
  heuristic filters by. A `data-umc-unavailable="true"` attribute (plain
  HTML, never exposed to the accessibility tree) replaces `aria-disabled`
  purely for the CSS visual-dimming rule sighted users see. This is **not
  yet re-verified with real JAWS** -- it is our best-reasoned response to a
  fix that visibly did not work the first time, not a confirmed
  resolution. See `Docs/Manual Screen Reader Test Plan.md` for what to
  specifically re-check next.
- **The demonstration media's loudness (Issue 3) was addressed by
  regenerating the demo audio/video at a much lower amplitude with fades,
  and by starting every demonstration page at 50 percent volume instead of
  full volume.** This is a testing-experience fix, not a framework
  default change -- `UniversalMediaController` itself still has no opinion
  about starting volume; each example's own JavaScript sets
  `element.volume = 0.5` before constructing the controller.

## Milestone 5: not-available buttons, not disabled buttons

- **The "no aria-disabled" fix from Milestone 4 was itself still
  incomplete.** Real testing this round covered both JAWS and NVDA, not
  JAWS alone, and found: JAWS may skip a control marked unavailable on
  forward Tab even without `aria-disabled` present (Shift+Tab could still
  reach it), and NVDA's behavior, while more consistent, still wasn't
  ideal. This pointed to something more fundamental than any specific
  ARIA attribute: describing a fully functional button as "unavailable" at
  all -- in its state, even without `aria-disabled` -- may itself be part
  of what different assistive technologies inconsistently handle for
  forward-only navigation.
- **The semantic model changed, not just the markup.** A control whose
  feature isn't available for the current media is no longer modeled as
  "a control in an unavailable state" at all -- it is modeled as an
  ordinary, fully enabled button whose job is to tell the user the feature
  isn't available when activated. This is a genuine behavioral fact, not a
  wording trick: the button always does something useful (explains itself)
  regardless of whether the underlying feature exists. The accessible name
  says so directly ("Captions, not available"), with no separate ARIA
  state layered on top. See `Packages/Core/ui/build-controls.js`,
  "Semantic model", for the full reasoning.
- **A standalone diagnostic page (`Tests/Diagnostic/focus-order-diagnostic.html`)
  now exists specifically to test this exact pattern in isolation**, with
  no framework code and no real media, so a real JAWS/NVDA pass can
  confirm or rule out the pattern itself before trusting it in the full
  controller. Do not remove this page until that verification has
  happened.
- **This is not yet verified with real JAWS or NVDA.** Two previous
  attempts at Issue 1 (aria-disabled + tabindex, then aria-disabled
  removed) both turned out to be insufficient on their own. This is a
  third, more fundamental attempt, and per the explicit instruction for
  this milestone, it should not be described as resolved until it has
  actually been tested.

### Media identification and repeated speech

Real NVDA testing found that entering the controller's group and landing
on its first control in one keystroke produces two announcements back to
back that both name the media -- the group's own name, then the first
control's name -- and that this reads as unnecessarily repetitive.

Both of these are independently required and cannot be removed without
weakening a different, already-documented requirement:

- The first control (Audio Description) must carry the title, because a
  user who navigates directly by form controls or buttons and never
  encounters the group boundary must still learn which media they are
  controlling from that control alone (Docs/Version 1 Requirements.md,
  "First focusable control").
- The group's own accessible name must remain meaningful and unique per
  controller, because a page with multiple controllers needs each one to
  be distinguishable by region/group browsing alone, independent of
  whether the user goes on to enter it (an existing multiple-controllers
  requirement).

Given both are genuinely necessary and serve different navigation
pathways that occasionally chain together in a single keystroke, full
elimination of the repetition is not possible without weakening one of
the two. The mitigation made this milestone: the container's accessible
name changed from `Media controls for "Title"` to the shorter `Controls
for "Title"`, so it no longer reads as a near-identical template to the
first control's own name ("Audio Description for 'Title', not
available"). The title itself is still spoken in both places -- that
part is inherent to satisfying both pathways -- but the surrounding
phrasing is shorter and less templated. This is a modest, partial
improvement, not a full resolution, and is worth re-evaluating after
real NVDA testing confirms whether the shorter phrasing meaningfully
helped.

## Vimeo Adapter Milestone 1

**This entire section describes an experimental, not-yet-field-tested
integration.** The Vimeo adapter was built entirely from Vimeo's
published Player SDK documentation and could not be executed against a
live Vimeo embed in this development environment: `player.vimeo.com` is
not reachable here (confirmed with a direct request, which returned HTTP
403), so no live browser testing, and no real JAWS/NVDA testing, has
happened against an actual Vimeo video. Everything below is either (a)
verified against a mock Player object built from the documented SDK
surface (`Tests/Vimeo/run-mock-tests.js`, 14 passing assertions), or (b)
a direct transcription of what Vimeo's documentation states, not
independently confirmed. Do not treat this adapter as production-ready.

### What is implemented

- Play, pause, restart (pause + seek to 0), skip backward/forward, current
  time, duration, volume, mute (via `setMuted`/`getMuted` where the SDK
  exposes them, falling back to volume-zero emulation for older SDK
  versions that don't), and playback speed (via `setPlaybackRate`) --
  all through Vimeo's official `@vimeo/player` SDK
  (`https://player.vimeo.com/api/player.js`), never by reaching into the
  Vimeo iframe's internal DOM.
- Capability detection: `supportsCaptions()` is a static platform
  capability (`true`, since the SDK has a text-track API); per-item
  caption availability (`hasCaptions()`) and playback-speed permission
  (`supportsPlaybackSpeed()`) are only knowable once the adapter's async
  initial load resolves, so both start conservatively (not available) and
  are re-checked once the adapter reports readiness -- see
  `Packages/Core/universal-media-controller.js`,
  `_refreshPlaybackSpeedAvailability()`.
- Playback-speed rejection handling: Vimeo only permits rate changes when
  the video owner's plan and per-video settings allow it, and
  `setPlaybackRate()`/`getPlaybackRate()` are documented to reject
  otherwise. The adapter probes this with `getPlaybackRate()` during
  initial load rather than assuming support, and the controller reverts
  the control and announces a graceful message rather than a raw error if
  a change is ever rejected mid-session.
- Media identity: `player.getVideoTitle()` is used as a genuine
  provider-derived title candidate, one tier below anything the site
  owner configures explicitly or that exists in the surrounding page
  (aria-label, heading, etc.), matching the documented priority order.
  Because this is only known asynchronously, a small, deliberately scoped
  extension was added to the core controller
  (`_refreshTitleFromAdapter()`) and to `build-controls.js`
  (`updateTitle()`) to update the title -- and everywhere it's displayed
  (the container's name, Audio Description's name) -- once Vimeo's title
  resolves, but only when no explicit or DOM-based title already won.
  This is the only core-controller change this milestone required beyond
  making a handful of action handlers tolerate a Promise-returning
  adapter method (see "Async-aware core changes" below); no Vimeo-specific
  conditionals were added to the core.
- Error handling: every adapter method that touches the SDK is
  Promise-based; a rejection is caught and translated into a graceful
  status message or a `console.warn` + `"error"` event, never an
  unhandled rejection or a crash. Confirmed directly (not just in theory):
  loading the demo page in this network-blocked sandbox caused the real
  SDK script tag to fail to load, and the adapter degraded exactly as
  designed -- the controller still built and rendered correctly, with the
  title falling back to its low-confidence default, and no page crash or
  unhandled exception.
- Multiple Vimeo instances: no module-level or otherwise shared state
  exists in `VimeoMediaAdapter` -- every instance holds its own player
  reference, cached state, and ready-promise. The Vimeo Player SDK script
  itself is loaded at most once per page (a second `<script>` tag would be
  wasteful) via a shared loader promise, but this is a loading
  optimization, not shared player state, and does not prevent multiple
  independent `Vimeo.Player` instances from coexisting. Not demonstrated
  with an actual multi-Vimeo page this milestone (not requested unless
  trivial, and a second live embed couldn't be tested here regardless).

### What is not implemented

- **Audio Description:** the Vimeo Player SDK does not expose an
  alternate-audio-track or audio-description API as of this milestone's
  research. `supportsAudioDescription()` returns `false` unconditionally,
  the same discoverable-but-not-available treatment as the HTML5 adapter
  gives it. This was not a data limitation found through testing -- it's
  the documented absence of a capability in the SDK itself.
- **Transcript:** no transcript API exists in the Vimeo Player SDK either.
  `supportsTranscript()` returns `false` unconditionally, for the same
  reason. A future milestone could let a site owner supply a transcript
  independently of any provider, but building that provider-independent
  mechanism was out of scope for this milestone (it does not yet exist
  for HTML5 either).
- **Multiple caption languages:** `listCaptionTracks()` reports every
  track Vimeo's `getTextTracks()` returns, but `setCaptionsEnabled()`
  always acts on the first track in that list -- there is no
  language-selection UI. Flagged for a later milestone rather than built
  now, per the explicit scope instruction for this milestone.
- **Hiding Vimeo's native player controls:** deliberately not attempted.
  The demonstration leaves Vimeo's own control bar exactly as Vimeo serves
  it by default. This does mean the demo page currently has two
  overlapping ways to control playback (Vimeo's own bar, inside the
  iframe, and UniversalMediaController's bar, outside it) -- an
  intentional, documented tradeoff for this milestone rather than an
  oversight. Investigating current Vimeo requirements/restrictions around
  hiding native controls, and whether doing so creates account-level or
  accessibility problems, is explicitly deferred to a later milestone.

### What was learned from Able Player (engineering reference only)

Able Player's Vimeo integration was reviewed as research, not copied.
Relevant, genuinely Vimeo-specific lessons (not simply "how Able Player
built its UI," which was deliberately not followed):

- Able Player also communicates with Vimeo exclusively through the
  official Player SDK rather than the iframe's internal DOM, confirming
  that is the only realistic integration path for any accessible wrapper
  around Vimeo -- not just a preference of this project's.
- Able Player treats a rejected `setPlaybackRate()` as an expected,
  routine outcome (not an error condition to surface loudly to the user)
  rather than assuming every video supports rate changes -- this shaped
  this adapter's decision to probe support via `getPlaybackRate()` during
  initial load rather than optimistically assuming `true`.
- Able Player's control order, its practice of omitting controls for
  unavailable accessibility features, its Faster/Slower speed-adjustment
  model, and its Preferences-menu architecture were all reviewed and
  deliberately not carried over -- UniversalMediaController's
  accessibility-first control order and not-available-button semantic
  model are unchanged and take precedence, per this milestone's explicit
  instruction.

### Which behaviors remain dependent on Vimeo configuration or account capabilities

- Playback-speed control depends on the video owner's Vimeo plan and
  per-video settings; it may be unavailable for some videos and available
  for others, and this adapter is designed to reflect that per-video
  reality rather than assume a fixed answer.
- Whether `setMuted`/`getMuted` exist depends on which version of the
  Player SDK a given embed loads; the adapter feature-detects this at
  runtime rather than assuming a version.
- Caption availability and language depend entirely on what the video
  owner has uploaded to Vimeo for that specific video.

### Demonstration video

`Examples/Vimeo/vimeo-example.html` embeds Vimeo video ID `76979871`, a
video ID commonly referenced in Vimeo's own official Player SDK
documentation examples. Its current public availability and embeddability
could not be confirmed in this environment (no network access to
vimeo.com) -- verify it loads before relying on this demonstration, and
substitute a different public, unauthenticated, embeddable video ID if it
does not.

The demonstration deliberately supplies no explicit title, `aria-label`,
`aria-labelledby`, or `title` attribute on the iframe, so it exercises the
Vimeo-provided-title fallback path described above rather than the
higher-priority sources already demonstrated by the HTML5 examples. Its
current behavior in this sandbox (SDK blocked): the controller falls back
to its lowest-confidence filename-style default, deriving `"76979871"`
from the iframe's `src` URL -- the same fallback path the HTML5 audio
untitled-fallback demo already exercises, just via a different route. In
an environment where the Vimeo SDK actually loads, this should be
replaced by the real video title once `_refreshTitleFromAdapter()` runs.
