# NEXT Athlete OS RC113 · Pass video controls QA

## Scope

RC113 adds small film controls directly under the active film in the pass player:

- 10 sek rewind
- Play
- Pause

The controls are styled in the same family as the compact calendar phase/week buttons: blue outline, dark blue inner surface, cyan/blue icon chip.

## Guardrails read first

- `docs/ARCHITECTURE_DECISIONS.md`
- `docs/DO_NOT_TOUCH_WITHOUT_DESIGN.md`
- `docs/RC111_PASS_PLAYER_STANDARDIZATION_QA.md`
- `docs/RC112_PASS_CHROME_REPOSITION_QA.md`

## What changed

### `preview.html`

Added one static control rail directly after `#videoWrap` inside the existing `.next-pass-media-card`:

- `#passVideoControls`
- `#passVideoRewindBtn`
- `#passVideoPlayBtn`
- `#passVideoPauseBtn`

The rail is outside `#videoWrap`, so `renderVideo()` can continue replacing the video/fallback content without destroying the controls.

### `assets/js/app.js`

Added small control functions that operate on the existing active player element:

- `passVideoPlay()`
- `passVideoPause()`
- `passVideoRewind(seconds = 10)`
- `syncPassVideoControls()`
- `schedulePassVideoControlsSync()`
- `handleYouTubePassMessage()`
- `bindPassNativeVideoControls(video)`

The control path reuses existing player pieces:

- existing `#videoFrame`
- existing `lastVideoIsYouTube`
- existing `ytCommand()` / `ytCommandTo()` iframe API path
- existing `state.soundOn` / `state.soundUnlocked`
- existing `renderControls()`
- existing `setPassPlayerState()` state hooks from RC111

### YouTube control behaviour

YouTube embeds already use `enablejsapi=1` in the existing `withVideoParams()` path. RC113 uses that existing path:

- Play: `playVideo`, `setVolume`, `unMute`
- Pause: `pauseVideo`
- Rewind: `getCurrentTime` + `seekTo(currentTime - 10)`

A lightweight `message` listener stores the latest `infoDelivery.currentTime` and `playerState` from YouTube. This is only for the control buttons; it does not replace the player lifecycle.

### Native/server/local video behaviour

For native `<video id="videoFrame">` elements, RC113 uses the DOM video API:

- Play: `video.play()`
- Pause: `video.pause()`
- Rewind: `video.currentTime = max(0, currentTime - 10)`

Existing native `controls` remain untouched for safety; the new buttons sit below the film as the consistent app control surface.

### `assets/css/next-system.css`

Added an RC113 CSS block:

- `.next-pass-video-controls`
- `.next-pass-video-control`
- `.pass-video-control-icon`
- compact mobile sizing rules

The rail is hidden for start/rest/fallback/non-video states and shown only when a controllable iframe/native video exists.

## What did NOT change

- No new player lifecycle.
- No replacement of `renderVideo()`.
- No changed video source priority.
- No changed YouTube/OTA/server/local backup selection model.
- No changed timer/rest/wake-lock/preload flow.
- No new renderer.
- No image generation.
- No foreground/background cutout image model.
- No server deploy test from this sandbox.

## Real-DOM QA method

The QA uses the RC108+ real-DOM method:

- Loads actual `preview.html`.
- Inlines actual CSS and JS.
- Runs the real app DOM with Playwright/Chromium using `page.set_content()`.
- Uses the existing player lifecycle functions:
  - `startWorkout()`
  - `primeWorkoutStart()`
  - `renderRest()`
- Mocks only local fetch endpoints:
  - `ota_youtube_proxy.php`
  - `video_backup_api.php`
- Aborts external iframe/network loading in the sandbox.

External YouTube iframe rendering remains blocked in the sandbox, so screenshots show a grey iframe area. The DOM/API path is still verified: `data-video-source="youtube"`, iframe present, controls visible, and `playVideo`, `pauseVideo`, `seekTo` commands issued through the real command function.

The child-mode QA runner was used per viewport/state to avoid Chromium iframe/timer bleed in the sandbox:

```bash
python3 tests/rc113_pass_video_controls_real_dom_qa.py --child start mobile 390 844
python3 tests/rc113_pass_video_controls_real_dom_qa.py --child youtube mobile 390 844
python3 tests/rc113_pass_video_controls_real_dom_qa.py --child rest mobile 390 844
python3 tests/rc113_pass_video_controls_real_dom_qa.py --child start ipad 820 1180
python3 tests/rc113_pass_video_controls_real_dom_qa.py --child youtube ipad 820 1180
python3 tests/rc113_pass_video_controls_real_dom_qa.py --child rest ipad 820 1180
python3 tests/rc113_pass_video_controls_real_dom_qa.py --child start desktop 1440 1024
python3 tests/rc113_pass_video_controls_real_dom_qa.py --child youtube desktop 1440 1024
python3 tests/rc113_pass_video_controls_real_dom_qa.py --child rest desktop 1440 1024
```

Metrics were combined into:

- `qa_screenshots/rc113_pass_player_dom_metrics.json`

## Screenshots produced

Start state:

- `qa_screenshots/rc113_pass_player_start_mobile_390x844.png`
- `qa_screenshots/rc113_pass_player_start_ipad_820x1180.png`
- `qa_screenshots/rc113_pass_player_start_desktop_1440x1024.png`

YouTube state with controls:

- `qa_screenshots/rc113_pass_player_youtube_mobile_390x844.png`
- `qa_screenshots/rc113_pass_player_youtube_ipad_820x1180.png`
- `qa_screenshots/rc113_pass_player_youtube_desktop_1440x1024.png`

Rest state:

- `qa_screenshots/rc113_pass_player_rest_mobile_390x844.png`
- `qa_screenshots/rc113_pass_player_rest_ipad_820x1180.png`
- `qa_screenshots/rc113_pass_player_rest_desktop_1440x1024.png`

## Metrics verified

For mobile, iPad and desktop:

- `#passVideoControls` is directly after `#videoWrap`.
- YouTube exercise state shows the video controls.
- Start/rest states keep the video controls hidden and disabled.
- YouTube exercise state has enabled Rewind/Play/Pause buttons.
- YouTube command path issued `playVideo`, `pauseVideo`, and `seekTo`.
- Existing RC111/RC112 pass state hooks remain active.
- No broken local images.

## Commands run and passed

```bash
node tests/rc113_pass_video_controls_smoke.js
node tests/rc112_pass_chrome_reposition_smoke.js
node tests/rc111_pass_player_standardization_smoke.js
node tests/rc110_pass_page_standardization_smoke.js
node tests/rc109_asset_path_cleanup_smoke.js
node tests/rc107_asset_and_library_smoke.js
node tests/rc109_library_compactness_static_smoke.js
node tests/rc104_direct_composite_smoke.js
node tests/rc106_smoke.js assets/js/core/exercise-illustrations.js "$PWD"
find assets/js -name '*.js' -print0 | xargs -0 -n1 node --check
php -l ota_youtube_proxy.php
php -l video_backup_api.php
python3 tests/rc113_pass_video_controls_real_dom_qa.py --child ...
python3 - <<'PY'
import json
j=json.load(open('qa_screenshots/rc113_pass_player_dom_metrics.json'))
for state in ['start','youtube','rest']:
  for m in j[state]:
    assert m['helperLoaded']
    assert m['videoControlsAfterWrap']
    assert not m['brokenImages']
    if state=='youtube':
      assert m['videoControlsVisible']
      assert m['videoControlsAriaHidden']=='false'
      assert not m['rewindDisabled'] and not m['playDisabled'] and not m['pauseDisabled']
      funcs=[c['func'] for c in m['passVideoCommands']]
      assert 'playVideo' in funcs and 'pauseVideo' in funcs and 'seekTo' in funcs
      assert m['hasIframe'] and m['videoSource']=='youtube'
    else:
      assert not m['videoControlsVisible']
      assert m['videoControlsAriaHidden']=='true'
      assert m['rewindDisabled'] and m['playDisabled'] and m['pauseDisabled']
print('RC113 metrics OK')
PY
unzip -t next_v96_rc113_pass_video_controls.zip
```

## Deploy caveat

The real deployed host was not tested from this sandbox. RC113 is verified with real preview DOM/CSS/JS using local inlined Playwright. Final validation should still be done on the deployed `preview.html` host, especially for real YouTube iframe loading and iPad casting behaviour.
