Liking cljdoc? Tell your friends :D

dk.simongray.play-nice.controls

Keyboard shortcuts and a seek bar for a player of your own, which press the same handlers as the lock screen.

A shortcut of a single key that types a character must work only while the player has the focus, unless the user can turn it off or remap it (WCAG 2.2, §2.1.4). So listen! follows the keys pressed in an element of your choice, e.g. the one that holds the player, and calls the handlers of the Media Session with the details of each action:

(def handlers
  (session/element-actions audio {:seekbackward 10 :seekforward 30}))

(session/actions! handlers)

(def stop!
  (listen! player audio handlers))

Everything but listen! runs on the JVM too, e.g. to write a seek bar's ARIA attributes into a page on the server.

Keyboard shortcuts and a seek bar for a player of your own, which press
the same handlers as the lock screen.

A shortcut of a single key that types a character must work only while
the player has the focus, unless the user can turn it off or remap it
(WCAG 2.2, §2.1.4). So listen! follows the keys pressed in an element of
your choice, e.g. the one that holds the player, and calls the handlers
of the Media Session with the details of each action:

    (def handlers
      (session/element-actions audio {:seekbackward 10 :seekforward 30}))

    (session/actions! handlers)

    (def stop!
      (listen! player audio handlers))

Everything but listen! runs on the JVM too, e.g. to write a seek bar's
ARIA attributes into a page on the server.
raw docstring

dk.simongray.play-nice.display.fullscreen

Fullscreen of a player, with the fallback that an iPhone needs, where only a video element itself can go fullscreen, in the system's own player.

The functions take these opts:

  • :video, the video element of the player, for that fallback

    (-> (enter! player {:video video :orientation :landscape}) (.then #(swap! page assoc :fullscreen %))) ;; {:fullscreen :element :orientation {:refused :not-supported}} ;; on a desktop, or {:fullscreen :video …} on an iPhone

Fullscreen of a player, with the fallback that an iPhone needs, where
only a video element itself can go fullscreen, in the system's own
player.

The functions take these opts:

- :video, the video element of the player, for that fallback

    (-> (enter! player {:video video :orientation :landscape})
        (.then #(swap! page assoc :fullscreen %)))
    ;; {:fullscreen :element :orientation {:refused :not-supported}}
    ;; on a desktop, or {:fullscreen :video …} on an iPhone
raw docstring

dk.simongray.play-nice.display.picture-in-picture

Picture-in-Picture of a video: whether it's available, entering and leaving it, and the size of its window, as data.

Each engine refuses a video in its own words, e.g. a video with disablePictureInPicture throws an InvalidStateError in Chrome and Firefox, but a NotSupportedError in WebKit. So enter! checks the video first, and answers with one keyword for each cause:

(-> (enter! video)
    (.then prn))
;; prints {:picture-in-picture true :width 320 :height 180}, or e.g.
;; {:refused :not-allowed}

To keep track of whether the video is in Picture-in-Picture, use watch!, which also sees the user close the window.

Picture-in-Picture of a video: whether it's available, entering and
leaving it, and the size of its window, as data.

Each engine refuses a video in its own words, e.g. a video with
disablePictureInPicture throws an InvalidStateError in Chrome and
Firefox, but a NotSupportedError in WebKit. So enter! checks the video
first, and answers with one keyword for each cause:

    (-> (enter! video)
        (.then prn))
    ;; prints {:picture-in-picture true :width 320 :height 180}, or e.g.
    ;; {:refused :not-allowed}

To keep track of whether the video is in Picture-in-Picture, use
watch!, which also sees the user close the window.
raw docstring

dk.simongray.play-nice.display.remote-playback

Casting and AirPlay of a media element, from Remote Playback: whether a device is available, the browser's picker of devices, and the connection to one, as data.

Firefox has no Remote Playback, so there no device is available and prompt! answers {:refused :not-supported}. WebKit also fires its older AirPlay events, which watch! follows as well, since Video.js finds the standard events unreliable in Safari.

Casting and AirPlay of a media element, from Remote Playback: whether a
device is available, the browser's picker of devices, and the connection
to one, as data.

Firefox has no Remote Playback, so there no device is available and
prompt! answers {:refused :not-supported}. WebKit also fires its older
AirPlay events, which watch! follows as well, since Video.js finds the
standard events unreliable in Safari.
raw docstring

dk.simongray.play-nice.display.wake-lock

The screen kept on while a media element plays, through Screen Wake Lock.

A playing video keeps the screen on by itself, but no engine does that for audio, so a page that shows a transcript or lyrics while an episode plays goes dark:

(def stop!
  (hold-while-playing! audio))

The functions take these opts:

  • :window, the window whose navigator holds the lock, instead of the page's, e.g. a stand-in in a test
The screen kept on while a media element plays, through Screen Wake
Lock.

A playing video keeps the screen on by itself, but no engine does that
for audio, so a page that shows a transcript or lyrics while an episode
plays goes dark:

    (def stop!
      (hold-while-playing! audio))

The functions take these opts:

- :window, the window whose navigator holds the lock, instead of the
  page's, e.g. a stand-in in a test
raw docstring

dk.simongray.play-nice.media

The state of an audio or video element as data, and the calls that play it, seek, load a source and set its rate and volume, which apply! also takes as data.

The functions take the element itself, so in a test any object with the element's attributes and methods stands in for it, e.g. an EventTarget in Node. The keys of the maps are the element's attributes in kebab-case, e.g. :current-time, and its numbered states are keywords, e.g. :have-metadata for HAVE_METADATA. The promises never reject, and each resolves to a map:

(-> (play! audio)
    (.then prn))
;; prints {:playing true}, or e.g. {:refused :not-allowed}
The state of an audio or video element as data, and the calls that play
it, seek, load a source and set its rate and volume, which apply! also
takes as data.

The functions take the element itself, so in a test any object with the
element's attributes and methods stands in for it, e.g. an EventTarget in
Node. The keys of the maps are the element's attributes in kebab-case,
e.g. :current-time, and its numbered states are keywords, e.g.
:have-metadata for HAVE_METADATA. The promises never reject, and each
resolves to a map:

    (-> (play! audio)
        (.then prn))
    ;; prints {:playing true}, or e.g. {:refused :not-allowed}
raw docstring

dk.simongray.play-nice.media.capabilities

What the browser can play, from Media Capabilities and canPlayType.

A configuration is a map of the keys of MediaDecodingConfiguration, e.g.

{:type :file :audio {:content-type "audio/mpeg" :bitrate 128000}}

On its own, Media Capabilities refuses some of the commonest files:

  • WebKit says it can't play MP3, and Firefox rejects the configuration.
  • Every engine refuses audio/mp4 without codecs, which is how most podcast feeds give AAC.
  • Every engine rejects a video without its width, height, bitrate and frame rate.

So where it says no or rejects a configuration, the answer also has canPlayType's, and playable? accepts either.

Servers and feeds also name some formats in ways that no engine knows, e.g. audio/opus for an Ogg Opus file, so decoding-info! asks about the name that the engines know of those in type-aliases.

What the browser can play, from Media Capabilities and canPlayType.

A configuration is a map of the keys of MediaDecodingConfiguration, e.g.

    {:type :file :audio {:content-type "audio/mpeg" :bitrate 128000}}

On its own, Media Capabilities refuses some of the commonest files:

- WebKit says it can't play MP3, and Firefox rejects the configuration.
- Every engine refuses audio/mp4 without codecs, which is how most
  podcast feeds give AAC.
- Every engine rejects a video without its width, height, bitrate and
  frame rate.

So where it says no or rejects a configuration, the answer also has
canPlayType's, and playable? accepts either.

Servers and feeds also name some formats in ways that no engine knows,
e.g. audio/opus for an Ogg Opus file, so decoding-info! asks about the
name that the engines know of those in type-aliases.
raw docstring

dk.simongray.play-nice.media.captions

Captions in the style that the user prefers, as the CSS of ::cue, from a map of the settings that the FCC's rule for caption displays names.

A page can't read the system's own caption settings, so you keep the user's style and the functions turn it into CSS:

(css {:font-scale 1.5 :color :yellow :edge :drop-shadow})
;; => "video::cue{color:rgba(255,255,0,1);font-size:150%;
;;     text-shadow:2px 2px 3px rgba(0,0,0,1)}"

The style is a map of these keys, each optional, which leaves the browser's own setting:

  • :font-scale, the size as a share of the browser's own, from 0.5 to 2 as the rule asks, e.g. 1.5
  • :font, one of fonts
  • :color and :background-color, one of colors, or a hex code, e.g. "#ffcc00"
  • :opacity and :background-opacity, from 0 to 1, e.g. 0.5 for semi-transparent
  • :edge, one of edges, and :edge-color, black by default

Chrome 153 on macOS kept its own background behind the text, and its own letters for :small-capitals, whatever the page asked for, but applied the rest. Everything but style! runs on the JVM too, e.g. to write the CSS into a page on the server.

Captions in the style that the user prefers, as the CSS of ::cue, from a
map of the settings that the FCC's rule for caption displays names.

A page can't read the system's own caption settings, so you keep the
user's style and the functions turn it into CSS:

    (css {:font-scale 1.5 :color :yellow :edge :drop-shadow})
    ;; => "video::cue{color:rgba(255,255,0,1);font-size:150%;
    ;;     text-shadow:2px 2px 3px rgba(0,0,0,1)}"

The style is a map of these keys, each optional, which leaves the
browser's own setting:

- :font-scale, the size as a share of the browser's own, from 0.5 to 2
  as the rule asks, e.g. 1.5
- :font, one of fonts
- :color and :background-color, one of colors, or a hex code, e.g.
  "#ffcc00"
- :opacity and :background-opacity, from 0 to 1, e.g. 0.5 for
  semi-transparent
- :edge, one of edges, and :edge-color, black by default

Chrome 153 on macOS kept its own background behind the text, and its own
letters for :small-capitals, whatever the page asked for, but applied the
rest. Everything but style! runs on the JVM too, e.g. to write the CSS
into a page on the server.
raw docstring

dk.simongray.play-nice.media.fragment

The times that a link points at: the temporal media fragment of a media file, e.g. #t=73,133, and the t of a page, e.g. ?t=1m13s.

A media fragment addresses the file, which every engine starts at the fragment's start. A page's t is a convention of its own, e.g. for the key moments of a video in Google's search, and must stay off the URL of the file itself, since podcast hosts count it as another file.

The times are numbers of seconds, which are doubles on the JVM.

The times that a link points at: the temporal media fragment of a media
file, e.g. #t=73,133, and the t of a page, e.g. ?t=1m13s.

A media fragment addresses the file, which every engine starts at the
fragment's start. A page's t is a convention of its own, e.g. for the
key moments of a video in Google's search, and must stay off the URL of
the file itself, since podcast hosts count it as another file.

The times are numbers of seconds, which are doubles on the JVM.
raw docstring

dk.simongray.play-nice.media.tracks

The text tracks of a media element as data: captions and subtitles to choose and show, and chapters to follow in every browser.

A track is a map of the members of TextTrack in kebab-case, with the values of its enums as keywords, e.g.

{:kind        :chapters
 :label       "Chapters"
 :language    "en"
 :mode        :hidden
 :active-cues [{:start-time 95 :end-time 1830 :text "News"}]}

Only Chromium accepts chapters in the Media Session's metadata, but a hidden chapters track fires cuechange in every engine. So a chapters track from add! and chapter-cues, followed with watch!, says which chapter plays.

The text tracks of a media element as data: captions and subtitles to
choose and show, and chapters to follow in every browser.

A track is a map of the members of TextTrack in kebab-case, with the
values of its enums as keywords, e.g.

    {:kind        :chapters
     :label       "Chapters"
     :language    "en"
     :mode        :hidden
     :active-cues [{:start-time 95 :end-time 1830 :text "News"}]}

Only Chromium accepts chapters in the Media Session's metadata, but a
hidden chapters track fires cuechange in every engine. So a chapters
track from add! and chapter-cues, followed with watch!, says which
chapter plays.
raw docstring

dk.simongray.play-nice.page.autoplay

The browser's autoplay policy, the wait for the click, tap or key press after which the page may play, and autoplay of a video while it's in view.

The policy and the wait take these opts:

  • :window, the window to ask and listen at instead of the page's, e.g. a stand-in in a test

Only Firefox has Autoplay Policy Detection, so elsewhere you find out by calling media/play!, which answers {:refused :not-allowed} where the browser won't play. play-when-allowed! plays then at the user's next click, tap or key press.

Chrome and WebKit on iOS pause a muted video with the autoplay attribute when it scrolls out of view, and play it again when it returns, but leave alone a video that the page plays itself. play-in-view! does the same for any video, in every engine, through media/play!:

(def stop!
  (play-in-view! hero #(swap! page assoc :hero %) {:threshold 0.5}))
The browser's autoplay policy, the wait for the click, tap or key press
after which the page may play, and autoplay of a video while it's in
view.

The policy and the wait take these opts:

- :window, the window to ask and listen at instead of the page's, e.g.
  a stand-in in a test

Only Firefox has Autoplay Policy Detection, so elsewhere you find out by
calling media/play!, which answers {:refused :not-allowed} where the
browser won't play. play-when-allowed! plays then at the user's next
click, tap or key press.

Chrome and WebKit on iOS pause a muted video with the autoplay attribute
when it scrolls out of view, and play it again when it returns, but
leave alone a video that the page plays itself. play-in-view! does the
same for any video, in every engine, through media/play!:

    (def stop!
      (play-in-view! hero #(swap! page assoc :hero %) {:threshold 0.5}))
raw docstring

dk.simongray.play-nice.page.embed

The allow attribute of an iframe that embeds a player from another site, built from a map of the Permissions Policy features that the player may use.

Without the attribute, a player from another site can't go fullscreen, and its document reports document.fullscreenEnabled as false.

The allow attribute of an iframe that embeds a player from another site,
built from a map of the Permissions Policy features that the player may
use.

Without the attribute, a player from another site can't go fullscreen,
and its document reports document.fullscreenEnabled as false.
raw docstring

dk.simongray.play-nice.page.tabs

One player at a time across the tabs of an app, through Web Locks.

On a desktop, no engine stops the audio of one tab when another tab starts, so two tabs of a podcast app play at once and race to save the position. attach! holds a lock named for the app while the element plays, and takes it from any other tab that holds it, which then pauses its own element:

(def detach!
  (attach! audio "my-podcast-app"))

The functions take these opts:

  • :window, the window whose navigator holds the locks, instead of the page's, e.g. a stand-in in a test
One player at a time across the tabs of an app, through Web Locks.

On a desktop, no engine stops the audio of one tab when another tab
starts, so two tabs of a podcast app play at once and race to save the
position. attach! holds a lock named for the app while the element
plays, and takes it from any other tab that holds it, which then pauses
its own element:

    (def detach!
      (attach! audio "my-podcast-app"))

The functions take these opts:

- :window, the window whose navigator holds the locks, instead of the
  page's, e.g. a stand-in in a test
raw docstring

dk.simongray.play-nice.schema

The shapes of the library's maps as Malli schemas, written as plain data so that the library doesn't depend on Malli. With Malli in your app:

(require '[malli.core :as m])

(m/validate schema/media-state (media/state audio))
;; => true

The maps are open, as Malli's are by default, so a key that a browser adds passes.

The shapes of the library's maps as Malli schemas, written as plain data
so that the library doesn't depend on Malli. With Malli in your app:

    (require '[malli.core :as m])

    (m/validate schema/media-state (media/state audio))
    ;; => true

The maps are open, as Malli's are by default, so a key that a browser
adds passes.
raw docstring

dk.simongray.play-nice.session

The page's Media Session, which puts what plays on the lock screen and in the notifications, and lets the headphone buttons control it.

The functions that touch the browser do nothing where it has no Media Session, e.g. in Node or the Android WebView. Each takes these opts:

  • :session, the MediaSession to set instead of the page's, e.g. a stand-in in a test, or nil for none

The keys of the maps are the members of the spec's dictionaries in kebab-case, and the values of its enums are keywords, e.g. {:action :seekto :seek-time 754.2}. A player whose buttons just play, pause and seek its media element needs three calls:

(def detach!
  (attach! audio))

(metadata! {:title "Blue in Green" :artist "Miles Davis"})

(actions! (element-actions audio {:seekbackward 10
                                  :seekforward  10}))
The page's Media Session, which puts what plays on the lock screen and
in the notifications, and lets the headphone buttons control it.

The functions that touch the browser do nothing where it has no Media
Session, e.g. in Node or the Android WebView. Each takes these opts:

- :session, the MediaSession to set instead of the page's, e.g. a
  stand-in in a test, or nil for none

The keys of the maps are the members of the spec's dictionaries in
kebab-case, and the values of its enums are keywords, e.g. {:action
:seekto :seek-time 754.2}. A player whose buttons just play, pause and
seek its media element needs three calls:

    (def detach!
      (attach! audio))

    (metadata! {:title "Blue in Green" :artist "Miles Davis"})

    (actions! (element-actions audio {:seekbackward 10
                                      :seekforward  10}))
raw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close