jkeen / ember-stereo

The best way to reactively handle audio in your modern ember app
https://ember-stereo.com
MIT License
19 stars 3 forks source link
audio ember ember-addon ember-hifi emberjs

ember-stereo

The best way to reactively handle audio in your modern ember app

CI Download count all time npm version Ember Observer Score semantic-release

Compatibility

Installation

ember install ember-stereo

Interactive docs at ember-stereo.com!

Upgrading from ember-hifi? Read the upgrade guide

API

ember-stereo operates on sounds by providing its helpers an identifier. Usually this is just a URL string, but an identifier could also be an object with a url property (and maybe a mimeType property), an already loaded stereo Sound object, an array of any of the previous items, or even a promise that resolves to any of the previous. Whatever the case, you're covered.

Template Helpers

Actions
<button
  type='button'
  class='button is-link'
  {{on 'click' (toggle-play-sound @identifier)}}
>Play/Pause</button>
<button
  type='button'
  class='button is-link'
  {{on 'click' (play-sound @identifier)}}
>Play</button>
<button
  type='button'
  class='button is-link'
  {{on 'click' (load-sound @identifier)}}
>Load</button>
<button
  type='button'
  class='button is-link'
  {{on 'click' (pause-sound @identifier)}}
>Pause</button>
<button
  type='button'
  class='button is-link'
  {{on 'click' (stop-sound @identifier)}}
>Stop</button>
<button
  type='button'
  class='button is-link'
  {{on 'click' (fastforward-sound @identifier increment=5000)}}
>Fast Forward</button>
<button
  type='button'
  class='button is-link'
  {{on 'click' (rewind-sound @identifier increment=5000)}}
>Rewind</button>
<button
  type='button'
  class='button is-link'
  {{on 'click' (seek-sound @identifier position=5000)}}
>Seek</button>
Conditionals
{{#if (sound-is-loaded @identifier)}}
  sound is loaded and ready to play
{{/if}}
{{#if (sound-is-loading @identifier)}}
  [show loading spinner]
{{/if}}
{{#if (sound-is-playing @identifier)}}
  <button
    type='button'
    class='button is-link'
    {{on 'click' (pause-sound @identifier)}}
  >Pause</button>
{{/if}}
{{#if (sound-is-errored @identifier)}}
  {{sound-error-details @identifier}}
{{/if}}
{{#if (sound-is-fastforwardable @identifier)}}
  <button
    type='button'
    class='button is-link'
    {{on 'click' (fastforward-sound @identifier increment=5000)}}
  >Fast Forward</button>
{{/if}}
{{#if (sound-is-rewindable @identifier)}}
  <button
    type='button'
    class='button is-link'
    {{on 'click' (rewind-sound @identifier increment=5000)}}
  >Fast Forward</button>
{{/if}}
{{#if (sound-is-rewindable @identifier)}}
  Sound is fastforwardable
{{/if}}
{{#if (sound-is-blocked @identifier)}}
  Browser has blocked auto play, needs user input
{{/if}}
{{#if (autoplay-allowed @identifier)}}
  Browser allows autoplaying of sounds
{{/if}}

Getters

{{sound-metadata @identifier}}
{{sound-duration @identifier load=true format=time}}
{{sound-position @identifier format=percentage}}
#=> 12
{{sound-position @identifier format=time}}
#=> 00:20
{{current-sound}} #=> currently playing/paused sound
{{find-sound @identifier}} #=> currently playing/paused sound

Service API

stereo plays one sound at a time. Multiple sounds can be loaded and ready to go, but only one sound plays at a time. The currently playing sound is set to currentSound on the service, and most methods and properties on the service simply proxy to that sound.

Methods

play calls load with the same arguments, and then on success plays the sound, returning it to you.

play can take one or more URLs, or a promise returning one or more URLs.

If the audio URLs are not known at the time of a play event, give play the promise to resolve, otherwise your mobile users might have to click the play button twice (due to some restrictions on autoplaying audio).

export default class StereoComponent extends Component {
  @service stereo
  ...
  @action
  play(id) {
    let urlPromise = this.store.findRecord('story', id).then(story => story.getProperties('aacUrl', 'hlsUrl'))

    this.stereo.play(urlPromise).then(({sound}) => {
      // sound object

    }).catch(error => {

    })
  }
}

If you already know the URLs, just pass them in.

export default class StereoComponent extends Component {
  @service stereo
  ...
  @action
  play(urls) {
    this.stereo.play(urls).then(({sound}) => {
      // sound object

    }).catch(error => {

    })
  }
}
export default class StereoComponent extends Component {
  @service stereo
  ...

  get sound() {
    return this.stereo.findSound(this.args.identifier)
  }
}
Gettable/Settable Properties

System volume. Bind a range element to this property for a simple volume control


//component.js
import { inject as service } from "@ember/service";
export default Component.extend({
  stereo: service(),
})

//template.hbs
{{input type="range" value=stereo.volume}}

Here's a silly way to make a position control, too.

//component.js
export default Component.extend({
  stereo: service(),
})

//template.hbs
{{input type="range" value=stereo.position min=0 max=stereo.duration step=1000}}
Read Only Properties

Sound API

Methods
Gettable/Settable Properties
Read Only Properties

Events

The stereo service and the sound objects are extended with Ember.Evented. You can subscribe to the following events in your application.

Triggered on both the sound and relayed through the stereo service
Stereo service events

Details

Included audio connections

  1. NativeAudio - Uses the native <audio> element for playing and streaming audio
  2. HLS - Uses HLS.js for playing HLS streams on the desktop.
  3. Howler - Uses howler to play audio

stereo will take a list of urls and find the first connection/url combo that works. For desktop browsers, we'll try each url on each connection in the order the urls were specified.

For mobile browsers, we'll first try all the URLs on the NativeAudio using a technique to (hopefully) get around any autoplaying restrictions that sometimes require mobile users to click a play button twice.

Testing

If you need to test audio handling that involves ember-stereo in your app, you're gonna need this helper. It sets up and cleans up a few stereo-related items, but most importantly it stubs out the native browser audio and video elements replacing it with a FakeMediaElement that behaves sanely in the test environment.

You can control how the sound behaves by providing a url in one of these formats:

URL Formats

URLs that will successfully load:

URLs that will fail

Here's an example test, testing an example player, making sure that fast forward and rewind buttons are disabled.

import { setupStereoTest } from 'ember-stereo/test-support/stereo-setup';

module('Integration | Component | player', function (hooks) {
  setupStereoTest(hooks);

  test('it does not display rewind and ff buttons when stream', async function (assert) {
    let stereo = this.owner.lookup('service:stereo');
    await stereo.play('/good/stream/test.mp3', {
      metadata: {
        show,
        track,
      },
    });
    await render(hbs`<Player/>`);

    assert.dom('[data-test-element="fastforward-button"]').isDisabled();
    assert.dom('[data-test-element="rewind-button"]').isDisabled();
    assert.dom('[data-test-element="play-pause-button"]').exists();
  });
});

Writing Your Own Stereo Connection

Do you need to support a funky audio format that stereo's built-in connections can't handle? Read more about how to write your own custom connection here.