• Stars
    star
    199
  • Rank 195,177 (Top 4 %)
  • Language
    TypeScript
  • Created about 7 years ago
  • Updated about 3 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

๐Ÿ”Š Stream large audio files without the dreaded 'DOMException: play() can only be initiated by a user gesture' error.

phonograph

๐Ÿ”Š Stream large audio files without the dreaded 'DOMException: play() can only be initiated by a user gesture' error.

Read Phonograph.js: Tolerable mobile web audio for more background.

The problem

You want to play some audio in your web app, but you don't want to use an <audio> element because mobile browser makers โ€“ in their infinite wisdom โ€“ have decided that playback must be initiated by a 'user gesture'.

You've read about the Web Audio API, in particular the AudioBuffer,ย which seems like it might be useful except for the bit that says it's 'designed to hold small audio snippets, typically less than 45s'. And they're not kidding about that โ€“ not only do you have to fetch the entire file before you can play it, but you have to have enough spare RAM to store the uncompressed PCM data (aka .wav โ€“ typically ten times the size of the source .mp3) otherwise the browser will crash instantly.

The solution

By breaking up the data into small chunks, we can use decodeAudioData to create a few seconds of PCM data at a time, making it very unlikely that we'll crash the browser. We can then play a short chunk, swapping it out for the next chunk (with a bit of overlap to avoid audible glitches) when ready.

By using the fetch() API, we can stream the data rather than waiting for the whole file to load. That's so fetch!

(Note: in Safari and Edge, it falls back to regular old XHR โ€“ no streaming, but we still get chunking. Similarly with Firefox, which implements fetch() but not the streaming part. Hopefully those browsers will catch up soon.)

Installation

npm i phonograph

...or download from npmcdn.com/phonograph.

Usage

import { Clip } from 'phonograph';

const clip = new Clip({ url: 'some-file.mp3' });

clip.buffer().then(() => {
  clip.play();
});

API

import { Clip, getContext } from 'phonograph';

context = getContext();
// returns the AudioContext shared by all clips. Saves you having
// to create your own.


/* ------------------------ */
/*       INSTANTIATION      */
/* ------------------------ */

clip = new Clip({
  url: 'some-file.mp3', // Required
  volume: 0.5           // Optional (default 1)
});


/* ------------------------ */
/*          METHODS         */
/* ------------------------ */

promise = clip.buffer(complete).then(...);
// Returns a Promise that resolves on 'canplaythrough' event (see
// below) or (if `complete === true`) on 'load' event. The `complete`
// parameter is optional and defaults to `false`

clone = clip.clone();
// Returns a lightweight clone of the original clip, which can
// be played independently but shares audio data with the original.

clip.connect(destination, output, input);
// Connects to a specific AudioNode. All clips are initially
// connected to the default AudioContext's `destination` โ€“
// if you connect to another node then it will disconnect
// from the default. `output` and `input` are optional. See
// https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/connect(AudioNode)

clip.disconnect(destination, output, input);
// Disconnects from the `destination` (if specified). All
// parameters are optional โ€“ see
// https://developer.mozilla.org/en-US/docs/Web/API/AudioNode/disconnect

listener = clip.on(eventName, callback);
// Listen for an event (see below)

listener.cancel();
// Equivalent to `clip.off( eventName, callback )`

clip.off(eventName, callback);
// Stop listening for the specified event

listener = clip.once(eventName, callback);
// Listen for an event, but stop listening once it's happened

clip.play();
// Starts playing the clip. Returns a promise that resolves
// once the clip has finished playing (for a looping clip,
// this is never!) or rejects on clip.dispose() or if
// there's a load/playback error

clip.pause();
// Stops playing the clip

clip.dispose();
// Unloads the clip, freeing up memory


/* ------------------------ */
/*        PROPERTIES        */
/* ------------------------ */

clip.buffered;
// How many bytes have been buffered

clip.canplaythrough;
// Whether or not Phonograph estimates that the clip can be played
// all the way through (i.e. all the data will download before the
// end is reached)

clip.currentTime;
// The position of the 'playhead', in seconds

clip.duration;
// Duration of the audio, in seconds. Returns `null` if the
// clip has not yet loaded. Read-only

clip.ended;
// Whether or not the clip has ended following the most recent play()

clip.length;
// The size of the clip in bytes

clip.loaded;
// Whether the clip has finished fetching data

clip.loop;
// If `true`, the clip will restart once it finishes

clip.paused;
// self-explanatory

clip.playing
// the inverse of clip.paused

clip.volume;
// Volume between 0 (silent) and 1 (max)


/* ------------------------ */
/*          EVENTS          */
/* ------------------------ */

clip.on('loadprogress', (progress, length, total) => {
  // Fires when data is fetched. `progress` is a value
  // between 0 and 1, equal to `length / total` (both
  // measured in bytes)
  progressBar.value = value;
  const percent = progress * 100;
  status.textContent = `${percent.toFixed(1)}% loaded`;
});

clip.on('canplaythrough', () => {
  // Phonograph estimates (based on clip size and bandwidth)
  // that it will be able to play the clip through without
  // stopping. YMMV!
  clip.play();
});

clip.on('load', () => {
  // All the audio data has been loaded
  clip.play();
});

clip.on('play', () => {
  button.textContent = 'pause';
});

clip.on('progress', () => {
  playhead.style.transform = `translate(${clip.currentTime/clip.duration}%,0)`;
});

clip.on('pause', () => {
  button.textContent = 'play';
});

clip.on('ended', () => {
  alert( 'that\'s all, folks!' );
});

clip.on('loaderror', err => {
  alert( 'Clip failed to load' );
});

clip.on('playbackerror', err => {
  alert( 'Something went wrong during playback' );
});

Caveats and limitations

  • No automated tests. I have no idea how you would test something like this.
  • Firefox doesn't want to decode mp3 files. May have to fall back to <audio> and MediaElementSourceNode in FF.

License

MIT

More Repositories

1

degit

Straightforward project scaffolding
JavaScript
6,905
star
2

ramjet

Morph DOM elements from one state to another with smooth animations and transitions
JavaScript
5,454
star
3

magic-string

Manipulate strings like a wizard
JavaScript
2,304
star
4

devalue

Gets the job done when JSON.stringify can't
JavaScript
2,085
star
5

pancake

Experimental charting library for Svelte
JavaScript
1,285
star
6

shimport

Use JavaScript modules in all browsers, including dynamic imports
JavaScript
1,238
star
7

svelte-cubed

Svelte โค๏ธ Three
Svelte
1,191
star
8

butternut

The fast, future-friendly minifier
JavaScript
1,176
star
9

agadoo

Check whether a package is tree-shakeable
JavaScript
535
star
10

headless-qr

A simple, modern QR code library
JavaScript
411
star
11

estree-walker

Traverse an ESTree-compliant AST
JavaScript
385
star
12

code-red

Experimental toolkit for writing x-to-JavaScript compilers
JavaScript
332
star
13

dts-buddy

WORK IN PROGRESS DO NOT USE
JavaScript
313
star
14

react-svelte

Use Svelte components inside a React app
JavaScript
302
star
15

sorcery

Resolve a chain of sourcemaps back to the original source, like magic
JavaScript
289
star
16

packd

Rollup as a service (with a little help from Browserify)
JavaScript
255
star
17

sveltekit-on-the-edge

SvelteKit, running on the edge
Svelte
217
star
18

simulant

Simulated DOM events for automated testing
JavaScript
202
star
19

svelte-knobby

Svelte
201
star
20

svg-parser

JavaScript
196
star
21

svelte-undo

A small utility for managing an undo stack
JavaScript
193
star
22

svelte-gl

Just an idea. For now.
191
star
23

vlq

Generate, and decode, base64 VLQ mappings for sourcemaps and other uses
JavaScript
188
star
24

roadtrip

Client-side routing library. It's about the journey, not just the destination
JavaScript
155
star
25

object-cull

Create a copy of an object with just the bits you actually need
TypeScript
144
star
26

sveltesnaps

Svelte
143
star
27

sander

Promise-based power tool for common filesystem tasks
JavaScript
117
star
28

snowpack-svelte-ssr

JavaScript
116
star
29

svelte-workshop

The website for the Svelte workshop
JavaScript
115
star
30

sveltekit-movies-demo

Movies demo
Svelte
114
star
31

svelte-split-pane

A <SplitPane> component
Svelte
107
star
32

ematchi

Svelte
103
star
33

yootils

Stuff I often need. WIP
JavaScript
103
star
34

Points

Another Pointer Events polyfill
JavaScript
96
star
35

svelte-template-electron

A template for building Electron apps with Svelte
TypeScript
91
star
36

boxxy

Layout manager for web apps.
JavaScript
87
star
37

periscopic

Utility for analyzing scopes belonging to an ESTree-compliant AST
JavaScript
85
star
38

svelte-gl-boxes

a quick Svelte GL demo
HTML
76
star
39

lit-node

Self-documenting Node scripts through literate programming
JavaScript
76
star
40

svelteflix

Svelte
75
star
41

stacking-order

Determine which of two elements is in front of the other
JavaScript
71
star
42

sourcemap-codec

Encode/decode sourcemap mappings
JavaScript
69
star
43

the-answer

The answer to the question of life, the universe and everything
JavaScript
67
star
44

bundler-comparison

A quick test to see how various bundlers compare when bundling the Lodash source code.
JavaScript
66
star
45

svelte-gl-demo

A very basic Svelte GL scene
JavaScript
62
star
46

tape-modern

Minimum viable testing framework
TypeScript
62
star
47

sevenup

Tool for making and loading sprites
JavaScript
61
star
48

zimmerframe

A tool for walking
JavaScript
60
star
49

golden-fleece

Parse and manipulate JSON5 strings
TypeScript
59
star
50

port-authority

Utilities for dealing with ports in Node apps
JavaScript
55
star
51

rollup-svelte-code-splitting

demo repo
JavaScript
50
star
52

svelte-d3-arc-demo

Using Svelte and D3 together to create lightweight interactive visualisations with SSR
JavaScript
49
star
53

begin-svelte-app

Begin app
JavaScript
45
star
54

Statesman

The JavaScript state management library
JavaScript
44
star
55

esrap

Parse in reverse
JavaScript
44
star
56

node-console-group

console.group() for node.js
JavaScript
41
star
57

sveltekit-pdf-demo

Using SvelteKit to generate PDFs dynamically
TypeScript
40
star
58

svelte-three-demo

bare bones demo of svelte-three
JavaScript
38
star
59

locate-character

JavaScript
37
star
60

birdland

The weather report
HTML
35
star
61

svelte-ssr-bundle

Demo of using Svelte and Rollup to create a SSR bundle
JavaScript
35
star
62

generated-types

An example of generated types
TypeScript
32
star
63

svelte-preprocessor-demo

JavaScript
31
star
64

spelunk

Traverse a folder in node, turning its contents into an object for easy consumption
JavaScript
30
star
65

cameoparison-starter

Workshop repo for https://cameoparison.netlify.app
JavaScript
25
star
66

typescript-lib

Project template for a TypeScript library
JavaScript
24
star
67

is-reference

Determine whether an AST node is a reference
JavaScript
22
star
68

ractive-dbmonster

An adaptation of Ember's 'dbmonster' demo of Glimmer
JavaScript
22
star
69

deepClone

Utility for cloning objects and arrays so you can manipulate them without borking your original data
JavaScript
18
star
70

magic-viewbox

Library for making draggable, zoomable, interactive SVG viewboxes
TypeScript
17
star
71

sapper-ws-chat

playing around with websockets
JavaScript
17
star
72

Neo

A matrix manipulation library that tries to explain what the hell's going on
JavaScript
16
star
73

svelte-google-maps

demo of using Svelte with Google Maps
HTML
15
star
74

terser-playground

A playground for seeing how Terser minifies JavaScript
HTML
15
star
75

oscars-data

Svelte
15
star
76

smooth-keyframes

Smoothly interpolate keyframes
TypeScript
14
star
77

transition-this

Svelte
14
star
78

sql

Opinionated wrapper around mysql2
JavaScript
14
star
79

tippex

Find and erase strings and comments in JavaScript code
JavaScript
13
star
80

namey-mcnameface

Generate short but memorable random names for stuff
JavaScript
13
star
81

viewbox

Pan and zoom the contents of SVG elements, and translate between coordinate systems
JavaScript
11
star
82

geotile

Split GeoJSON data up into vector tiles
JavaScript
10
star
83

Ractive-TodoMVC

Example TodoMVC implementation using Ractive.js
CSS
10
star
84

ffmpeg-wasm-demo

JavaScript
10
star
85

pathologist

JavaScript
9
star
86

modulepreload-demo

JavaScript
9
star
87

gurgle

A stream library
JavaScript
9
star
88

birdland-starter

Starter repo for birdland
JavaScript
9
star
89

fowl-play

A demo of SvelteKit's `read` function
JavaScript
9
star
90

svelte-gl-stress-test

JavaScript
8
star
91

superjson-and-devalue

quick script to compare superjson and devalue
JavaScript
7
star
92

sensor

sensor.js - DOM events that don't suck
JavaScript
7
star
93

auto-import-repro

JavaScript
7
star
94

vite-env-reload

HTML
6
star
95

vite-safe-modules-repro

JavaScript
6
star
96

d3-modules

JavaScript
6
star
97

vite-dynamic-import-repro

Repro for https://github.com/sveltejs/kit/issues/8516
JavaScript
6
star
98

svelte-accessors-demo

Using accessors to get and set Svelte component data
JavaScript
6
star
99

simple-css-parser

Parse CSS into JSON
CSS
6
star
100

stackblur

Fork of StackBlur (http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html), used in canvg
JavaScript
6
star