Skip to content

Commit

Permalink
feat: Sound metadata is stored centrally now and will automatically u…
Browse files Browse the repository at this point in the history
…pdate navigator.mediasession when changed, providing accurate now-playing info to iOS lock screens, etc
  • Loading branch information
jkeen committed Nov 20, 2021
1 parent 5093542 commit 848588b
Show file tree
Hide file tree
Showing 17 changed files with 188 additions and 48 deletions.
15 changes: 7 additions & 8 deletions addon/-private/utils/error-cache.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
import { A as emberArray, makeArray } from '@ember/array';
import debug from 'debug';
import { tracked } from '@glimmer/tracking';
import StereoUrl from 'ember-stereo/-private/utils/stereo-url';
import hasEqualUrls from 'ember-stereo/-private/utils/has-equal-urls';
import normalizeIdentifier from './normalize-identifier';
import { inject as service } from '@ember/service';

/**
* This class caches errors based on urls.
* @private
*/

export default class ErrorCache {
@service stereo;

@tracked cachedCount = 0;
@tracked cachedErrors = [];
@tracked cachedList = []
@tracked _cache = {};
name = 'ember-stereo:error-cache'

constructor(stereo) {
this.stereo = stereo;
}

reset() {
this._cache = {};
this.cachedCount = 0;
Expand All @@ -34,7 +33,7 @@ export default class ErrorCache {
* @return {Sound}
*/
find(urls) {
let identifiers = makeArray(urls).map(i => new StereoUrl(i));
let identifiers = makeArray(urls).map(i => normalizeIdentifier(i));
let errors = emberArray(identifiers).map(identity => this.cachedErrors.find(err => hasEqualUrls(err.url, identity)));
let foundErrors = emberArray(errors).compact();

Expand All @@ -48,7 +47,7 @@ export default class ErrorCache {
}

remove(urls) {
let identifiers = makeArray(urls).map(i => new StereoUrl(i));
let identifiers = makeArray(urls).map(i => normalizeIdentifier(i));
this.cachedErrors = this.cachedErrors.filter(err => !hasEqualUrls(err.url, identifiers));

identifiers.forEach(identity => {
Expand All @@ -57,7 +56,7 @@ export default class ErrorCache {
}

cache({ url, error, connectionKey, debugInfo }) {
let identifier = new StereoUrl(url).key
let identifier = normalizeIdentifier(url);

if (!this._cache[identifier]) {
this._cache[identifier] = {}
Expand Down
14 changes: 14 additions & 0 deletions addon/-private/utils/metadata-cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import ObjectCache from 'ember-stereo/-private/utils/object-cache';

/**
* This class caches metadata.
@private
*/


export default class MetadataCache extends ObjectCache {
name = 'ember-stereo:metadata-cache'



}
14 changes: 14 additions & 0 deletions addon/-private/utils/normalize-identifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import StereoUrl from './stereo-url';
import BaseSound from 'ember-stereo/stereo-connections/base';

export default function normalizeIdentifier(identifier) {
if (typeof identifier === 'string') {
return new StereoUrl(identifier).key;
} else if (identifier instanceof StereoUrl) {
return identifier.key;
} else if (identifier instanceof BaseSound) {
return new StereoUrl(identifier.url).key
} else {
return identifier;
}
}
26 changes: 17 additions & 9 deletions addon/-private/utils/object-cache.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
import { tracked } from '@glimmer/tracking';
import normalizeIdentifier from './normalize-identifier';
import { inject as service } from '@ember/service';
import EmberObject from '@ember/object';

/**
* This class caches things based on a strings or objects. You shouldn't have to interact with this class.
*/

export default class ObjectCache {
export default class ObjectCache extends EmberObject {
@service stereo;

@tracked objectCache = new WeakMap();
@tracked keyCache = {}
name = 'ember-stereo:object-cache'

constructor(stereo) {
this.stereo = stereo;
}

has(identifier) {
has(_identifier) {
let identifier = normalizeIdentifier(_identifier);
return this.objectCache.has(identifier) || (identifier in this.keyCache)
}

find(identifier) {
find(_identifier) {
let identifier = normalizeIdentifier(_identifier);

if (this.objectCache.has(identifier)) {
return this.objectCache.get(identifier)
} else if (this.keyCache[identifier]) {
return this.keyCache[identifier]
}
}

remove(identifier) {
remove(_identifier) {
let identifier = normalizeIdentifier(_identifier);

if (this.objectCache.has(identifier)) {
this.objectCache.remove(identifier)
}
Expand All @@ -34,7 +40,9 @@ export default class ObjectCache {
}
}

store(identifier, value) {
store(_identifier, value) {
let identifier = normalizeIdentifier(_identifier);

if (identifier) {
if (identifier.then || (typeof identifier === 'object')) {
this.objectCache.set(identifier, value)
Expand Down
6 changes: 4 additions & 2 deletions addon/-private/utils/one-at-a-time.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { A as emberArray } from '@ember/array';
import debug from 'debug';
import hasEqualUrls from './has-equal-urls';
import { inject as service } from '@ember/service';
export default class OneAtATime {
constructor(service) {
this.stereo = service;
@service stereo;

constructor() {
this.sounds = emberArray();
}

Expand Down
35 changes: 18 additions & 17 deletions addon/-private/utils/sound-cache.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import { A as emberArray, makeArray } from '@ember/array';
import debug from 'debug';
import { tracked } from '@glimmer/tracking';
import StereoUrl from 'ember-stereo/-private/utils/stereo-url';
import BaseSound from 'ember-stereo/stereo-connections/base';
import hasEqualUrls from './has-equal-urls';

import normalizeIdentifier from './normalize-identifier';
import { inject as service } from '@ember/service';
/**
* This class caches sound objects based on urls.
* @private
*/

export default class SoundCache {
@service stereo;

@tracked cachedCount = 0;
@tracked cachedList = [];
@tracked cachedSounds = [];
@tracked _cache = {};
name = 'ember-stereo:sound-cache'

constructor(stereo) {
this.stereo = stereo;
}

reset() {
this._cache = {};
this.cachedCount = 0;
Expand All @@ -34,19 +32,18 @@ export default class SoundCache {
* @param {String} identifiers
* @return {Sound}
*/
find(identifiers) {
find(_identifiers) {
let cache = this._cache;

let stereoUrls = makeArray(identifiers).map(identity => new StereoUrl(identity))

let sounds = emberArray(stereoUrls).map(url => cache[url.key]);
_identifiers = makeArray(_identifiers);
let identifiers = _identifiers.map(identity => normalizeIdentifier(identity))
let sounds = emberArray(identifiers).map(url => cache[url]);
let foundSounds = emberArray(sounds).compact();

if (foundSounds.length > 0) {
debug(this.name)(`cache hit for ${foundSounds[0].url}`);
debug(this.name)(`cache hit for `, foundSounds[0].url);
}
else {
debug(this.name)(`cache miss for ${stereoUrls.map(u => u.url).join(',')}`);
debug(this.name)(`cache miss for`, identifiers);
}

return foundSounds[0];
Expand All @@ -58,10 +55,14 @@ export default class SoundCache {
* @method remove
* @param {Sound} sound
*/
remove(identifier) {
remove(_identifier) {
let identifier;

if (this.isDestroyed) return;
if (identifier instanceof BaseSound) {
identifier = identifier.url
if (_identifier instanceof BaseSound) {
identifier = _identifier.url
} else {
identifier = normalizeIdentifier(_identifier);
}

let url = Object.keys(this._cache).find(key => hasEqualUrls(key, identifier))
Expand All @@ -82,7 +83,7 @@ export default class SoundCache {
*/
cache(sound) {
if (this.isDestroyed) return;
let identifier = new StereoUrl(sound.url).key
let identifier = normalizeIdentifier(sound.url);

debug(this.name)(`caching sound with url: ${identifier}`);

Expand Down
7 changes: 5 additions & 2 deletions addon/-private/utils/strategizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { makeArray, A as emberArray } from '@ember/array';
import { isEmpty } from '@ember/utils';
import { cached } from 'tracked-toolbox';
import { assert } from '@ember/debug';

import { getOwner, setOwner } from '@ember/application';
export default class Strategizer {
@tracked urls
@tracked options
Expand All @@ -25,7 +25,10 @@ export default class Strategizer {
sharedAudioAccess: this.useSharedAudioAccess ? this.sharedAudioAccess : undefined,
}

return new Strategy(connection, new StereoUrl(url), strategyOptions)
let strategy = new Strategy(connection, new StereoUrl(url), strategyOptions)
setOwner(strategy, getOwner(this));
return strategy;

}

get sharedAudioAccess() {
Expand Down
5 changes: 3 additions & 2 deletions addon/-private/utils/strategy.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { tracked } from '@glimmer/tracking';
import { cached } from 'tracked-toolbox';
import { setOwner } from '@ember/application';
import { setOwner, getOwner } from '@ember/application';

export default class Strategy {
@tracked stereoUrl;
@tracked config
Expand Down Expand Up @@ -80,7 +81,7 @@ export default class Strategy {
options: this.options
})

setOwner(sound, this.connection)
setOwner(sound, getOwner(this));
return sound;
}
}
47 changes: 41 additions & 6 deletions addon/services/stereo.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import EmberEvented from '@ember/object/evented';
import ErrorCache from 'ember-stereo/-private/utils/error-cache';
import OneAtATime from 'ember-stereo/-private/utils/one-at-a-time';
import UrlCache from 'ember-stereo/-private/utils/url-cache';
import MetadataCache from 'ember-stereo/-private/utils/metadata-cache';
import SharedAudioAccess from 'ember-stereo/-private/utils/shared-audio-access';
import SoundCache from 'ember-stereo/-private/utils/sound-cache';
import ObjectCache from 'ember-stereo/-private/utils/object-cache';
Expand Down Expand Up @@ -85,10 +86,19 @@ export default class Stereo extends Service.extend(EmberEvented) {

this.sharedAudioAccess = new SharedAudioAccess();
this.oneAtATime = new OneAtATime();
this.soundCache = new SoundCache(this);
this.errorCache = new ErrorCache(this);
this.urlCache = new UrlCache(this);
this.proxyCache = new ObjectCache(this);

this.soundCache = new SoundCache();
this.errorCache = new ErrorCache();
this.metadataCache = new MetadataCache();
this.urlCache = new UrlCache();
this.proxyCache = new ObjectCache();

setOwner(this.oneAtATime, owner);
setOwner(this.soundCache, owner);
setOwner(this.errorCache, owner);
setOwner(this.metadataCache, owner);
setOwner(this.urlCache, owner);
setOwner(this.proxyCache, owner);

this.poll = setInterval(
this._setCurrentPosition.bind(this),
Expand Down Expand Up @@ -163,7 +173,7 @@ export default class Stereo extends Service.extend(EmberEvented) {
* @public
*/
get currentMetadata() {
return this.currentSound?.metadata;
return this.metadataCache.find(this.currentSound?.url);
}

/**
Expand Down Expand Up @@ -668,7 +678,9 @@ export default class Stereo extends Service.extend(EmberEvented) {
}

_buildStrategies(urlsToTry, options) {
return new Strategizer(urlsToTry, options).strategies;
let strategizer = new Strategizer(urlsToTry, options)
setOwner(strategizer, getOwner(this));
return strategizer.strategies;
}

_handlePlaybackError({ sound, options }) {
Expand Down Expand Up @@ -783,6 +795,7 @@ export default class Stereo extends Service.extend(EmberEvented) {
}
this._unregisterEvents(this._currentSound);
this._registerEvents(sound);
this._updateNowPlaying(sound);
sound._setVolume(this.volume);
this._currentSound = sound;
debug('ember-stereo')(`setting current sound -> ${sound.url}`);
Expand Down Expand Up @@ -892,6 +905,7 @@ export default class Stereo extends Service.extend(EmberEvented) {
this.soundCache.remove(url);
this.errorCache.remove(url);
this.proxyCache.remove(url);
this.metadataCache.remove(url);

if (this.currentSound?.url === url) {
this._unregisterEvents(this.currentSound);
Expand Down Expand Up @@ -1050,6 +1064,7 @@ export default class Stereo extends Service.extend(EmberEvented) {
// Named functions so Ember Evented can successfully register/unregister them

_relayPlayedEvent(info) {
this._updateNowPlaying(this.currentSound);
this._relayEvent('audio-played', info);
}
_relayPausedEvent(info) {
Expand Down Expand Up @@ -1083,9 +1098,29 @@ export default class Stereo extends Service.extend(EmberEvented) {
this._relayEvent('audio-will-fast-forward', info);
}
_relayMetadataChangedEvent(info) {
this._updateNowPlaying(this.currentSound);
this._relayEvent('audio-metadata-changed', info);
}

/**
* Updates now playing info from metadata if appropriate keys exist
*/

_updateNowPlaying(sound) {
if (!sound) return;

let { title, artist, album, artwork } = sound.metadata;

if ('mediaSession' in navigator && 'MediaMetadata' in window) {
navigator.mediaSession.metadata = new window.MediaMetadata({
title,
artist,
album,
artwork
});
}
}

/**
* Creates an empty audio element and plays it to unlock audio on a mobile (iOS)
* device at the beggining of a play event.
Expand Down
Loading

0 comments on commit 848588b

Please sign in to comment.