Skip to content

A visit that starts with swipes stays silent on iOS until a tap happens to carry a cue #17

Description

@mykolasolodukha

Summary

On iOS Safari, a touch that ends as a swipe grants the page no user activation; only a tap does. play() checks navigator.userActivation.hasBeenActive and returns, so a first cue fired from a swipe never creates the AudioContext. A later tap on empty space, or on a control with no cue of its own, still leaves the next swipe creating the context off the gesture's call stack, suspended, so it takes a second tap that carries a cue before anything is heard.

The pending-resume half of this also hits the README's own async example, await navigator.clipboard.writeText(); play("success"), when it is the first cue of a visit: the continuation after await is off the click's call stack, WebKit leaves resume() pending, and every later play() inherits the silence.

Trace from an iPhone

A dev spy wrapped window.AudioContext and logged the window events. The second column is navigator.userActivation.hasBeenActive (- false, A true). A swipe deck built on a pointer-event drag library; the deck element has touch-action: none, so swipes end in pointerup, never pointercancel.

  2419 - ev pointerdown
  2420 - ev touchstart
  2499 - ev pointerup
  2499 - ev touchend
  ... six more swipes, all "-", no context created ...
  7263 - ev pointerdown
  7264 - ev touchstart
  7319 A ev pointerup          <- the first tap
  7320 A ev touchend
  7325 A ev click
  7326 A ctx created state=suspended on=click
  7327 A resume#1 state=suspended on=click
  7343 A resume#1 settled state=running
  7343 A render osc state=running t=0.000
  8333 A ev pointerup          <- swipes now render directly
  8335 A render noise state=running t=0.955

Eight swipes, all without activation and without a context. The first tap flips the flag, creates the context inside the click, and resume() settles 16 ms later. Desktop Chrome is unaffected: it honours sticky activation.

Proposed fix

  • A play() the browser blocks, before activation or off a gesture's stack, arms one set of passive capture listeners on window for touchend, click, keydown and mousedown. The first gesture that counts creates the shared context if needed and calls resume() on that stack. The listeners leave once the context runs.
  • A cue waiting on a pending resume() renders if it was asked for within the last second, otherwise it is dropped, so a backlog cannot burst out on a later tap.
  • An explicit prime() for apps that want to start the context from a gesture of their own.

Independent of #14 / #15, which address the cold Chrome renderer.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions