Skip to content

Fix unreachable YouTube player controls on the activity plan hero (web) #8013

Closed

Description

@wcjord

Problem

On web, a YouTube hero played inline on the activity plan page renders and plays, but the embed's own controls are unreachable. The control bar never appears on hover, so there is no pause, scrub, volume, captions (CC), or quality/speed settings. Only our close (X) strip responds to the mouse.

Captions burned in by YouTube do render, which confirms the iframe is alive and playing — it simply receives no mouse events.

Impact: for a listening activity the learner cannot pause, replay a passage, or turn captions on/off — the controls that matter most for comprehensible input.

Scope: ActivityStartHero's inline player path (web/desktop). Native mobile plays on its own screen (ActivityVideoScreen) and is not affected by the same wrapper.

TO TEST

  • On web, open an activity whose lead media is a YouTube video — verify the poster shows with a play badge
  • Tap the play badge — verify the video starts playing inline
  • Move the mouse over the video — verify YouTube's control bar appears
  • Click pause on that bar — verify the video actually pauses
  • Drag the scrubber to a later point — verify playback jumps there
  • Click the CC button — verify captions toggle off and on
  • Open the settings (gear) menu — verify playback speed and, when the video offers more than one caption track, the subtitles language list are reachable
  • Verify the X still closes the video and the role cards come back
  • On iOS and Android, open the same activity and tap the video — verify it opens on its own full-screen player, plays, and the close button returns to the plan

Activity

  1. wcjord commented on Jul 29, 2026

    @wcjord
    Author

    Root cause (reproduced and confirmed on a local web build against staging)

    Not the close (X) strip, and nothing in ActivityStartHero. The embed is smothered by a Flutter accessibility semantics node.

    With the semantics tree on, document.elementFromPoint over every part of the playing embed — centre, control-bar strip, corners — returns:

    flt-semantics#flt-semantic-node-N → iframe#youtube-0 → flt-glass-pane

    That node is viewport-sized (0,0,1095,992), carries flt-tappable, and therefore gets pointer-events: all from Flutter web. Every ancestor is correctly pointer-events: none; this single node is the blanket. Its only semantic child is the 40×40 ⓘ map-attribution button in the bottom-left corner, so it originates in the world map's subtree (RichAttributionWidget is SafeArea > Align > Stack, and flutter_map's map gesture surface is itself viewport-sized and tappable).

    Proof: setting pointer-events: none on that one node — changing nothing else — immediately made the iframe the top element at the control-bar point, and hovering the video brought up YouTube's full chrome (title, volume, CC, settings gear, scrubber, time). Those are exactly the controls reported dead.

    Because the iframe is a DOM platform view and is not part of the semantics tree, any tappable semantics node overlapping it wins native hit-testing regardless of Flutter paint order. So this hits every DOM embed layered over the map — the YouTube <iframe> and an uploaded <video> alike.

    Severity is gated on the semantics tree

    • Staging: main_deploy.yaml writes ENABLE_SEMANTICS=true, so staging always reproduces it.
    • Production: no ENABLE_SEMANTICS key, so the tree is off and the controls work — except that Flutter web can enable semantics at runtime when it detects assistive technology, which would put AT users in the broken state. (Worth confirming for our Flutter version before deciding priority.)

    Fixes tried that do NOT clear it

    1. explicitChildNodes: true on the map's Semantics(label: activities, container: true) wrapper — fixed that container (it stops being tappable) but exposed the next viewport-sized tappable node below it.
    2. showFlutterMapAttribution: false — removed only the node's label, not the blanket.
    3. Semantics(explicitChildNodes: true) wrapped directly around RichAttributionWidget — no change.

    The remaining node appears to be flutter_map's own viewport-sized tappable gesture/semantics surface, which can't be neutralised from our side without an ExcludeSemantics over the map (which would also remove the pins' own Semantics(button) a11y), a layout change so the map's node no longer extends under the panels, or an upstream flutter_map fix. That is a design call, not a mechanical one.

    Branch reverted to main; no partial fix committed.

  2. wcjord commented on Jul 30, 2026

    @wcjord
    Author

    Correction to my earlier comment: staging has plenty of YouTube activities. My "0 of 2962" was a measurement bug, not a fact about the data.

    I scanned activities-v2 reading doc['media']. The plan's media actually lives at doc['res']['plan']['media'], so every document looked media-less. The tell was in my own output — every row also came back with no title — and I missed it.

    Corrected numbers on staging:

    • 342 activities carry a youtube media block.
    • All 342 lead with it, so they all take the hero inline-player path this issue is about.

    Two consequences:

    1. QA is unblocked. No test content needs authoring. English-titled examples: "Km 0 in Madrid" (b62fbccb-f594-43b4-9363-3a4010a32fb7), "Jacket return at Selfridges" (4292b83d-d751-4380-9c60-27c1aab96e8e), "Find the train station" (ddc50b88-2fa9-4f3e-9c24-70600a27482b).
    2. The local repro harness was unnecessary. A real staging activity would have exercised the hero path directly. It does not change the diagnosis or the fix, both of which were verified by measuring the semantics tree, but it does mean the end-to-end click-through can now be done on staging against real content.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions