Giter VIP home page Giter VIP logo

phonograph's Introduction

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

phonograph's People

Contributors

rich-harris avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

phonograph's Issues

Missed index.d.ts file in npm package

There is no src/index.d.ts file in npm package after installation but it is exists in the repo. Could you publish a new package with src/index.d.ts?

Incorrect parameters passed to loadprogress event

I've been trying to use the loadprogress event but it seems there is an issues with the parameters passed.

In the example it shows three parameters; progress, length, total.

clip.on('loadprogress', (progress, length, total) => { ... });

However when logging these, length and total return undefined and progress returns an object containing the expected three parameters but progress is logging as NaN.

Example: console.log( progress, length, total )
Result:
screenshot 2019-02-02 at 10 57 10

Gapless playback between consecutive audios

Hi,
first of all thank you for the amazing work. So far unfortunately I haven't been able (probably because of my inexperience) to play two songs consecutively without hearing a gap between them, which I would like to avoid, for example when listening to live albums.
Here's my simple attempt:

const clip = new Clip({ url: 'https://myserver.com/audio1.mp3' });
var clip2;

clip.buffer().then(() => {
    clip.play();
    clip2 = new Clip({ url: 'https://myserver.com/audio2.mp3' });
        clip2.buffer();
});

clip.on('ended', () => {
    clip2.play();
});

Do you have any idea on how this can be done?
Thanks :)

install error

hiya when I download this and run npm i, i get the following error

src/index.ts β†’ dist/phonograph.es.js, dist/phonograph.umd.js...
(!) Circular dependency: src/Clone.ts -> src/Clip.ts -> src/Clone.ts
created dist/phonograph.es.js, dist/phonograph.umd.js in 655ms

> [email protected] build-declarations /Users/jonahfox/Downloads/phonograph-master
> tsc -d && node scripts/move-type-declarations.js

Files:           28
Lines:        37652
Nodes:       171585
Identifiers:  57902
Symbols:      50673
Types:        14499
Memory used: 95094K
I/O read:     0.02s
I/O write:    0.01s
Parse time:   0.49s
Bind time:    0.25s
Check time:   1.08s
Emit time:    0.12s
Total time:   1.94s
internal/modules/cjs/loader.js:638
    throw err;
    ^

Error: Cannot find module '/Users/jonahfox/Downloads/phonograph-master/scripts/move-type-declarations.js'

Am I doing something wrong ?

Play live stream

Is possible to play a live stream like an Icecast mp3 stream ?

I made a test, but the canplaythrough event is never fired.

Play after pause fails in Chrome

In Chrome, if a clip is paused, calling play() again does not play the clip, and throws the following error:

DOMException: Cannot decode detached ArrayBuffer

I tested this with Phonograph v1.5.0 on the following platforms:

  • Chrome 62 on macOS 10.13 (πŸ‘Ž)
  • Chrome 62 on Android 8.1 (πŸ‘Ž)
  • Safari on macOS 11.2 (πŸ‘)
  • Safari on iOS 11.2 (πŸ‘)

Here's a Pen to demonstrate.

doesn't always work in safari

Hi I'm running the demo on a recent mac. The demo.html seems to work fine in chrome, but only works intermittently in safari ... clicking play causes no sound to play, although it does seem to think it's playing.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.