{"id":15015985,"url":"https://github.com/jkeen/ember-stereo","last_synced_at":"2025-10-08T19:11:13.579Z","repository":{"id":43360494,"uuid":"332126486","full_name":"jkeen/ember-stereo","owner":"jkeen","description":"The best way to reactively handle audio in your modern ember app","archived":false,"fork":false,"pushed_at":"2024-03-23T16:40:31.000Z","size":65365,"stargazers_count":18,"open_issues_count":1,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-04-24T19:49:27.757Z","etag":null,"topics":["audio","ember","ember-addon","ember-hifi","emberjs"],"latest_commit_sha":null,"homepage":"https://ember-stereo.com","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jkeen.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"jkeen"}},"created_at":"2021-01-23T04:24:46.000Z","updated_at":"2023-08-28T14:18:43.000Z","dependencies_parsed_at":"2024-01-12T02:33:15.436Z","dependency_job_id":"a669ee24-2fb1-4bfa-85d3-67fcb38eee86","html_url":"https://github.com/jkeen/ember-stereo","commit_stats":{"total_commits":684,"total_committers":14,"mean_commits":"48.857142857142854","dds":"0.45906432748538006","last_synced_commit":"1d71a0454dd2807fa2e9f2d288bb4a8b505f7888"},"previous_names":[],"tags_count":119,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jkeen%2Fember-stereo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jkeen%2Fember-stereo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jkeen%2Fember-stereo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jkeen%2Fember-stereo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jkeen","download_url":"https://codeload.github.com/jkeen/ember-stereo/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248547592,"owners_count":21122538,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audio","ember","ember-addon","ember-hifi","emberjs"],"created_at":"2024-09-24T19:48:14.756Z","updated_at":"2025-10-08T19:11:08.545Z","avatar_url":"https://github.com/jkeen.png","language":"JavaScript","funding_links":["https://github.com/sponsors/jkeen"],"categories":[],"sub_categories":[],"readme":"# ember-stereo\n\n## The best way to reactively handle audio in your modern ember app\n\n[![CI](https://github.com/jkeen/ember-stereo/actions/workflows/ci.yml/badge.svg)](https://github.com/jkeen/ember-stereo/actions/workflows/ci.yml)\n![Download count all time](https://img.shields.io/npm/dt/ember-stereo.svg) [![npm version](https://img.shields.io/npm/v/ember-stereo.svg?style=flat-square)](https://www.npmjs.com/package/ember-stereo) [![Ember Observer Score](http://emberobserver.com/badges/ember-stereo.svg)](http://emberobserver.com/addons/ember-stereo)\n[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)\n\n## Compatibility\n\n- Ember.js v3.28 or above\n- Ember CLI v3.28 or above\n- Node.js v14 or above\n\n## Installation\n\n```\nember install ember-stereo\n```\n\n### Interactive docs at [ember-stereo.com](https://ember-stereo.com/docs)!\n\n##### Upgrading from `ember-hifi`? Read the [upgrade guide](https://jkeen.github.com/ember-stereo)\n\n### API\n\n`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.\n\n#### Template Helpers\n\n##### Actions\n\n- `toggle-play-sound`\n\n```hbs\n\u003cbutton\n  type='button'\n  class='button is-link'\n  {{on 'click' (toggle-play-sound @identifier)}}\n\u003ePlay/Pause\u003c/button\u003e\n```\n\n- `play-sound`\n\n```hbs\n\u003cbutton\n  type='button'\n  class='button is-link'\n  {{on 'click' (play-sound @identifier)}}\n\u003ePlay\u003c/button\u003e\n```\n\n- `load-sound`\n\n```hbs\n\u003cbutton\n  type='button'\n  class='button is-link'\n  {{on 'click' (load-sound @identifier)}}\n\u003eLoad\u003c/button\u003e\n```\n\n- `pause-sound`\n\n```hbs\n\u003cbutton\n  type='button'\n  class='button is-link'\n  {{on 'click' (pause-sound @identifier)}}\n\u003ePause\u003c/button\u003e\n```\n\n- `stop-sound`\n\n```hbs\n\u003cbutton\n  type='button'\n  class='button is-link'\n  {{on 'click' (stop-sound @identifier)}}\n\u003eStop\u003c/button\u003e\n```\n\n- `fastforward-sound`\n\n```hbs\n\u003cbutton\n  type='button'\n  class='button is-link'\n  {{on 'click' (fastforward-sound @identifier increment=5000)}}\n\u003eFast Forward\u003c/button\u003e\n```\n\n- `rewind-sound`\n\n```hbs\n\u003cbutton\n  type='button'\n  class='button is-link'\n  {{on 'click' (rewind-sound @identifier increment=5000)}}\n\u003eRewind\u003c/button\u003e\n```\n\n- `seek-sound`\n\n```hbs\n\u003cbutton\n  type='button'\n  class='button is-link'\n  {{on 'click' (seek-sound @identifier position=5000)}}\n\u003eSeek\u003c/button\u003e\n```\n\n##### Conditionals\n\n- `sound-is-loaded`\n\n```hbs\n{{#if (sound-is-loaded @identifier)}}\n  sound is loaded and ready to play\n{{/if}}\n```\n\n- `sound-is-loading`\n\n```hbs\n{{#if (sound-is-loading @identifier)}}\n  [show loading spinner]\n{{/if}}\n```\n\n- `sound-is-playing`\n\n```hbs\n{{#if (sound-is-playing @identifier)}}\n  \u003cbutton\n    type='button'\n    class='button is-link'\n    {{on 'click' (pause-sound @identifier)}}\n  \u003ePause\u003c/button\u003e\n{{/if}}\n```\n\n- `sound-is-errored`\n\n```hbs\n{{#if (sound-is-errored @identifier)}}\n  {{sound-error-details @identifier}}\n{{/if}}\n```\n\n- `sound-is-fastforwardable`\n\n```hbs\n{{#if (sound-is-fastforwardable @identifier)}}\n  \u003cbutton\n    type='button'\n    class='button is-link'\n    {{on 'click' (fastforward-sound @identifier increment=5000)}}\n  \u003eFast Forward\u003c/button\u003e\n{{/if}}\n```\n\n- `sound-is-rewindable`\n\n```hbs\n{{#if (sound-is-rewindable @identifier)}}\n  \u003cbutton\n    type='button'\n    class='button is-link'\n    {{on 'click' (rewind-sound @identifier increment=5000)}}\n  \u003eFast Forward\u003c/button\u003e\n{{/if}}\n```\n\n- `sound-is-seekable`\n\n```hbs\n{{#if (sound-is-rewindable @identifier)}}\n  Sound is fastforwardable\n{{/if}}\n```\n\n- `sound-is-blocked`\n\n```hbs\n{{#if (sound-is-blocked @identifier)}}\n  Browser has blocked auto play, needs user input\n{{/if}}\n```\n\n- `autoplay-allowed`\n\n```hbs\n{{#if (autoplay-allowed @identifier)}}\n  Browser allows autoplaying of sounds\n{{/if}}\n```\n\n### Getters\n\n- `sound-metadata`\n\n```hbs\n{{sound-metadata @identifier}}\n```\n\n- `sound-duration(@identifier, load=false, format=false)`\n\n```hbs\n{{sound-duration @identifier load=true format=time}}\n```\n\n- `sound-position(@identifier, format=false defaultValue=0)`\n\n```hbs\n{{sound-position @identifier format=percentage}}\n#=\u003e 12\n{{sound-position @identifier format=time}}\n#=\u003e 00:20\n```\n\n- `current-sound`\n\n```hbs\n{{current-sound}} #=\u003e currently playing/paused sound\n```\n\n- `find-sound`\n\n```hbs\n{{find-sound @identifier}} #=\u003e currently playing/paused sound\n```\n\n#### Service API\n\n`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.\n\n###### Methods\n\n- `play(urlsOrPromise, options)`\n\n`play` calls `load` with the same arguments, and then on success plays the sound, returning it to you.\n\n`play` can take one or more URLs, or a promise returning one or more URLs.\n\nIf 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).\n\n```javascript\nexport default class StereoComponent extends Component {\n  @service stereo\n  ...\n  @action\n  play(id) {\n    let urlPromise = this.store.findRecord('story', id).then(story =\u003e story.getProperties('aacUrl', 'hlsUrl'))\n\n    this.stereo.play(urlPromise).then(({sound}) =\u003e {\n      // sound object\n\n    }).catch(error =\u003e {\n\n    })\n  }\n}\n```\n\nIf you already know the URLs, just pass them in.\n\n```javascript\nexport default class StereoComponent extends Component {\n  @service stereo\n  ...\n  @action\n  play(urls) {\n    this.stereo.play(urls).then(({sound}) =\u003e {\n      // sound object\n\n    }).catch(error =\u003e {\n\n    })\n  }\n}\n```\n\n- `playTask(urlsOrPromise, options)` the ember concurrency task that `play` calls.\n\n- `pause()`\n  Pauses the current sound\n\n- `togglePause()`\n  Toggles the play state of the current sound\n\n- `fastForward(duration)`\n  Moves the playhead of the current sound forwards by duration (in ms)\n\n- `rewind(duration)`\n  Moves the playhead of the current sound backwards by duration (in ms)\n\n- `load(urlsOrPromise, options)`\n  Tries each stereo connection with each url and returns the ready `sound` from the first combination that works. The sound is cached internally so on subsequent load requests with the same url the already prepared sound will be returned. Calling `play` on the returned sound will start playback immediately.\n\n- `loadTask(urlsOrPromise, options)` the ember concurrency task that `load` calls.\n\n- `findSound(identifier)`\n  Returns a sound once it loads. This works reactively, so you can do something like:\n\n```javascript\nexport default class StereoComponent extends Component {\n  @service stereo\n  ...\n\n  get sound() {\n    return this.stereo.findSound(this.args.identifier)\n  }\n}\n```\n\n###### Gettable/Settable Properties\n\n- `volume` (integer, 0-100)\n\nSystem volume. Bind a range element to this property for a simple volume control\n\n```javascript\n\n//component.js\nimport { inject as service } from \"@ember/service\";\nexport default Component.extend({\n  stereo: service(),\n})\n\n//template.hbs\n{{input type=\"range\" value=stereo.volume}}\n```\n\n- `position` (integer, in ms)\n\nHere's a silly way to make a position control, too.\n\n```javascript\n//component.js\nexport default Component.extend({\n  stereo: service(),\n})\n\n//template.hbs\n{{input type=\"range\" value=stereo.position min=0 max=stereo.duration step=1000}}\n```\n\n###### Read Only Properties\n\n- `isLoading` (boolean)\n- `isPlaying` (boolean)\n- `isStream` (boolean)\n- `isFastForwardable` (boolean)\n- `isRewindable` (boolean)\n\n- `duration` (integer, in ms)\n- `percentLoaded` (integer 0-100, when available)\n\n- `currentSound` the currently loaded sound\n\n### Sound API\n\n###### Methods\n\n- `play()`\n  Plays the sound\n- `pause()`\n  Pauses the sound\n- `togglePause()`\n  Toggles the play state of the sound\n- `fastForward(duration)`\n  Moves the playhead of the sound forwards by duration (in ms)\n- `rewind(duration)`\n  Moves the playhead of the sound backwards by duration (in ms)\n\n###### Gettable/Settable Properties\n\n- `position` (integer, in ms)\n\n###### Read Only Properties\n\n- `isLoading` (boolean)\n- `isPlaying` (boolean)\n- `isStream` (boolean)\n- `isSeekable` (boolean)\n- `isFastForwardable` (boolean)\n- `isRewindable` (boolean)\n\n- `duration` (integer, in ms)\n- `percentLoaded` (integer, not always available)\n- `url` the url of the sound\n\n### Events\n\nThe `stereo` service and the `sound` objects are extended with [Ember.Evented](https://www.emberjs.com/api/classes/Ember.Evented.html). You can subscribe to the following events in your application.\n\n###### Triggered on both the sound and relayed through the stereo service\n\n- `audio-played` ({ sound }) - the sound started playing\n- `audio-paused` ({ sound }) - the sound was paused\n- `audio-ended` ({ sound }) - the sound finished playing\n- `audio-load-error` ({ sound }) - loading sound failed\n- `audio-ready` ({ sound }) - the sound is ready to play\n- `audio-will-rewind` ({sound, currentPosition, newPosition}) - fired before rewinding a sound\n- `audio-will-fast-forward` ({sound, currentPosition, newPosition}) - fired before fast-forwarding a sound\n- `audio-position-will-change` ({sound, currentPosition, newPosition}) - fired before audio position change\n- `audio-position-changed` ({sound})\n- `audio-blocked` ({ sound }) - the sound was prevented from being played by the browser due to auto play restrictions\n\n###### Stereo service events\n\n- `current-sound-changed` ({sound, previousSound}) - triggered when the current sound changes. On initial play, previousSound will be undefined.\n- `current-sound-interrupted` ({sound, previousSound}) - triggered when a sound has been playing and a new one takes its place by being played, pausing the first one\n- `new-load-request` ({loadPromise, urlsOrPromise, options}) - triggered whenever `.load` or `.play` is called.\n- `pre-load` ({loadPromise, urlsOrPromise, options}) - triggered whenever `.load` or `.play` is called.\n\n## Details\n\n### Included audio connections\n\n1. `NativeAudio` - Uses the native `\u003caudio\u003e` element for playing and streaming audio\n1. `HLS` - Uses HLS.js for playing HLS streams on the desktop.\n1. `Howler` - Uses [howler](http://howlerjs.com) to play audio\n\n`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.\n\nFor 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.\n\n# Testing\n\nIf 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.\n\nYou can control how the sound behaves by providing a url in one of these formats:\n\n## URL Formats\n\n#### URLs that will successfully load:\n\n- `good/10000/test-url.mp3`: an mp3 that is 10 seconds long\n- `good/stream/the-current.aac`: an aac audio stream, duration = Infinity, will behave like a stream does\n\n#### URLs that will fail\n\n- `bad/codec-error/the-current.aac`: an aac sound that will fail with 'codec-error'\n- `bad/some%20custom%20string/the-current.aac`: an aac sound that will fail with error message 'some custom string'\n\nHere's an example test, testing an example player, making sure that fast forward and rewind buttons are disabled.\n\n```javascript\nimport { setupStereoTest } from 'ember-stereo/test-support/stereo-setup';\n\nmodule('Integration | Component | player', function (hooks) {\n  setupStereoTest(hooks);\n\n  test('it does not display rewind and ff buttons when stream', async function (assert) {\n    let stereo = this.owner.lookup('service:stereo');\n    await stereo.play('/good/stream/test.mp3', {\n      metadata: {\n        show,\n        track,\n      },\n    });\n    await render(hbs`\u003cPlayer/\u003e`);\n\n    assert.dom('[data-test-element=\"fastforward-button\"]').isDisabled();\n    assert.dom('[data-test-element=\"rewind-button\"]').isDisabled();\n    assert.dom('[data-test-element=\"play-pause-button\"]').exists();\n  });\n});\n```\n\n## [Writing Your Own Stereo Connection](CUSTOM_CONNECTIONS.md)\n\nDo 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](CUSTOM_CONNECTIONS.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjkeen%2Fember-stereo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjkeen%2Fember-stereo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjkeen%2Fember-stereo/lists"}