Skip to content

fix: SCORM playback on mobile browsers (range requests, viewport, rotation) - #2480

Closed
ashudev77 wants to merge 2 commits into
frappe:developfrom
ashudev77:fix/scorm-mobile-playback
Closed

ashudev77 wants to merge 2 commits into
frappe:developfrom
ashudev77:fix/scorm-mobile-playback

Conversation

@ashudev77

Copy link
Copy Markdown

Summary

Three fixes for SCORM chapter playback on mobile browsers, found while testing an Articulate Storyline (SCORM 1.2) package on iOS Safari and Android Chrome. On desktop everything worked; the issues were mobile-specific.

1. SCORM media doesn't play on iOS Safari — HTTP Range support

SCORMRenderer._serve_file streamed the whole file with 200 and no Accept-Ranges/206 handling. iOS Safari requires HTTP range requests to play <audio>/<video>, so all mp3/mp4 in a package were silent/blank on iPhone (desktop Chrome tolerates a 200, which is why it only showed on mobile).

Fix: make the response range-aware via Response.make_conditional(...). Range requests now return 206 + Content-Range; non-range requests still return 200 and advertise Accept-Ranges. Also enables media seeking.

2. Player mis-sized on mobile — 100vh → 100dvh

100vh is unreliable on mobile across toolbar/orientation changes. Switched the SCORM iframe to 100dvh.

3. SCORM iframe blanks on rotation

On orientation change, mobile browsers blank the SCORM iframe and (on iOS) do not repaint newly-added DOM until a full reload — so in-page recovery UI (overlay/button) never paints. Recover by reloading on orientationchange; existing resume support restores the learner's position. A short delay lets the debounced suspend_data save flush first.

Note: #3 is a pragmatic workaround for an iOS WebKit repaint limitation (a full reload was the only reliable recovery in testing). Happy to iterate toward a lighter approach if maintainers prefer.

Testing

Verified on a self-hosted v15 bench with a SCORM 1.2 package containing mp3/mp4:

  • iOS Safari + Android Chrome: media now plays; portrait completion + resume work.
  • Range requests return 206 with correct Content-Range; non-range 200 with Accept-Ranges.
  • Rotation now auto-recovers and resumes instead of showing a blank screen needing a manual refresh.

🤖 Generated with Claude Code

ashudev77 and others added 2 commits June 17, 2026 11:30
SCORM packages containing audio/video (mp3/mp4) fail to play in mobile
Safari. SCORMRenderer._serve_file streamed the whole file with a 200 and
no Accept-Ranges / 206 handling, and iOS Safari requires HTTP range
requests to play <audio>/<video>. Desktop Chrome tolerates a 200, which
is why this only surfaced on iPhone.

Make the response range-aware via Response.make_conditional so range
requests get 206 + Content-Range; non-range requests still get 200 and
now advertise Accept-Ranges. This also enables media seeking everywhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Use 100dvh instead of 100vh for the SCORM iframe. 100vh is unreliable
  on mobile browsers across toolbar/orientation changes and mis-sizes the
  player.
- Mobile browsers (notably iOS Safari) blank the SCORM iframe on
  orientation change and do not repaint added DOM until a full reload, so
  in-page recovery UI cannot help. Reload the page on orientationchange;
  existing resume support restores the learner's position. A short delay
  lets the debounced suspend_data save flush first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@raizasafeel

Copy link
Copy Markdown
Contributor

I don't think the frontend part (viewport class + rotation handler) is ready. Mind splitting this into two PRs so the range fix doesn't have to wait on that discussion?

A few things to sort out first:

  1. Would need a rebase/resolve conflicts

  2. Please run the linters as per frappes contribution guidelines otherwise prettier is going to fail, also please read through the guidelines

  3. The window.location.reload() in handleScormRotate — this is the one I really want to talk through. Reloading on rotate blows away the whole SCORM runtime, and the learner just gets dropped wherever the package's own resume logic happens to put them. That feels like a lot of collateral damage for what's really a layout problem.

    Also, the guard meant to stop this from firing twice doesn't really work — scormRotateHandled is just a module-level variable, and the reload it triggers resets it anyway. So in practice it reloads on every single rotation. It's also attached to window with no check for device type, so tablets and even desktop monitors that pivot will trigger it too.

    If 100dvh is actually what fixes the mobile sizing issue — and it looks like it is — I'd just drop this handler entirely. If there's something else it's fixing that a re-layout wouldn't, let me know what that is.

    (Small aside: orientationchange itself is deprecated now in favor of screen.orientation's change event — but honestly I'd rather just remove this than update it.)

One more small thing on the backend side: while you're in _serve_file, Content-Length never gets set on the plain 200 response, because direct_passthrough makes calculate_content_length() return None. You've already got the file size on hand, so might as well set it — helps players that send a HEAD request before ranging:

response.headers["Accept-Ranges"] = "bytes"
response.content_length = os.path.getsize(path)
response.make_conditional(frappe.local.request, accept_ranges=True, complete_length=os.path.getsize(path))

And last thing — any chance of adding a test for the 206 and 416 cases? The renderer is reachable from the test suite, so it should just be a matter of encoding the two scenarios from the table above. Not blocking, just would be nice to have.

@raizasafeel

Copy link
Copy Markdown
Contributor

Closing due to inactivity, please reopen with requested changes

@raizasafeel raizasafeel closed this Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants