Skip to content

Commit

Permalink
feat: improve firing resolution of audio-position-changed event. refs #…
Browse files Browse the repository at this point in the history
…24

chore: refactor where position is set on a playing sound
  • Loading branch information
jkeen committed Nov 12, 2023
1 parent 4c26633 commit 0777253
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 131 deletions.
43 changes: 14 additions & 29 deletions addon-test-support/utils/fake-media-element.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Evented from 'ember-stereo/-private/utils/evented';
import { tracked } from '@glimmer/tracking';
import TestAudioUrl from './test-audio-url';
import { task, rawTimeout, animationFrame } from 'ember-concurrency';
import debug from 'debug';
// Ready state values
// const HAVE_NOTHING = 0;
Expand Down Expand Up @@ -45,6 +46,7 @@ export default class FakeMediaElement extends Evented {
this._currentTime = 0;
this.loaded = false;
this.crossorigin = 'anonymous';
this.startTickingTask.cancelAll();
}

async load() {
Expand Down Expand Up @@ -125,21 +127,19 @@ export default class FakeMediaElement extends Evented {

debug('ember-stereo:fake-element')(`${this.src} play`);
this.paused = false;
this.startTimer();

this.trigger('playing', { target: this });
return Promise.resolve(this);
return this.startTickingTask.perform();
}

pause() {
debug('ember-stereo:fake-element')(`${this.src} pausing`);
this.trigger('pause', { target: this });
this.stopTimer();
this.paused = true;
}

stop() {
this.pause();
this.stopTimer();
}

get seekable() {
Expand All @@ -166,7 +166,6 @@ export default class FakeMediaElement extends Evented {
}

remove() {
this.stopTimer();
this.paused = true;
}

Expand Down Expand Up @@ -208,35 +207,21 @@ export default class FakeMediaElement extends Evented {
return this[name];
}

startTimer() {
this._stereoFakeMediaElementPoller = setInterval(
this.advance.bind(this),
100
);
}

stopTimer() {
clearInterval(this.poller);
}
@task
*startTickingTask() {
let cutoffTime = new Date().getTime() + 2 * 1000;

resetTimer() {
this.counter = 0;
}

advance() {
if (!this.paused && this.src) {
var diff = this._previous ? Date.now() - this._previous : 0;
this._previous = Date.now();
while (!this.paused && this.src && new Date() < cutoffTime) {
// don't let a sound live longer than 2 seconds when testing
yield animationFrame();
var diff = this._previous ? new Date().getTime() - this._previous : 0;
this._previous = new Date().getTime();
this.currentTime = this.currentTime + diff / 1000;
this.trigger('timeupdate', { target: this });

debug('ember-stereo:fake-element')(`${this.src} ${this.currentTime}`);
} else {
// this._previous = false;
}
}

willDestroy() {
clearInterval(window._stereoFakeMediaElementPoller);
yield rawTimeout(50);
}
}
}
30 changes: 0 additions & 30 deletions addon/services/stereo.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,6 @@ export default class Stereo extends Service.extend(EmberEvented) {
@tracked urlCache = new UrlCache();
proxyCache = new UntrackedObjectCache();

pollInterval = 500;

constructor() {
super(...arguments);
const owner = getOwner(this);
Expand All @@ -101,11 +99,6 @@ export default class Stereo extends Service.extend(EmberEvented) {
setOwner(this.urlCache, owner);
setOwner(this.proxyCache, owner);

this.poll = setInterval(
this._setCurrentPosition.bind(this),
macroCondition(isTesting()) ? 20 : this.pollInterval
);

if (macroCondition(isTesting())) {
this._determineAutoplayPermissions();
}
Expand Down Expand Up @@ -1001,28 +994,6 @@ export default class Stereo extends Service.extend(EmberEvented) {
]);
}

/**
* Sets the current sound with its current position, so the sound doesn't have
* to deal with timers. The service runs the show.
*
* @method _setCurrentPosition
* @private
*/

_setCurrentPosition() {
let sound = this._currentSound;
if (sound) {
try {
let newPosition = sound._currentPosition();
if (sound._position != newPosition) {
sound._position = newPosition;
}
} catch (e) {
console.error(e);
// continue regardless of error
}
}
}
/**
* Register events on a current sound. Audio events triggered on that sound
* will be relayed and triggered on this service
Expand Down Expand Up @@ -1203,7 +1174,6 @@ export default class Stereo extends Service.extend(EmberEvented) {
}

willDestroy() {
clearInterval(this.poll);
this.loadTask.cancelAll();
this.playTask.cancelAll();
}
Expand Down
39 changes: 33 additions & 6 deletions addon/stereo-connections/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Evented from 'ember-stereo/-private/utils/evented';
import hasEqualUrls from 'ember-stereo/-private/utils/has-equal-urls';
import { getOwner } from '@ember/application';
import { registerDestructor } from '@ember/destroyable';
import { task, animationFrame, timeout, didCancel } from 'ember-concurrency';

/**
* This is the base sound object from which other sound objects are derived.
Expand Down Expand Up @@ -263,7 +264,7 @@ export default class Sound extends Evented {
* @public
*/
get position() {
// _position is updated by the service on the currently playing sound
// _position is updated on a loop on the currently playing sound
return this._position;
}
set position(v) {
Expand All @@ -276,6 +277,28 @@ export default class Sound extends Evented {
this._position = this._setPosition(v);
}

/* we both want to query for the playing sounds position, and fire change events
more often than an audio element would, as documented in this issue: https://github.com/jkeen/ember-stereo/issues/24 */

updatePositionTask = task({ drop: true, maxConcurrency: 1 }, async () => {
while (this.isPlaying) {
await animationFrame();
await timeout(50);

let previousPosition = this._position;
let currentPosition = this._currentPosition();

if (previousPosition != currentPosition) {
this._position = currentPosition;

this.trigger('audio-position-changed', {
sound: this,
position: this._position,
});
}
}
});

/**
* get the sound's current real time position (probably only available on certain HLS sounds)
* @property currentTime
Expand Down Expand Up @@ -329,6 +352,12 @@ export default class Sound extends Evented {
this.isBlocked = false;
this.error = null;

this.updatePositionTask.perform().catch((e) => {
if (!didCancel(e)) {
throw e;
}
});

if (audioPlayed) {
audioLoading(this);
}
Expand All @@ -346,7 +375,7 @@ export default class Sound extends Evented {

this.on('audio-ended', () => {
this.isPlaying = false;
this._position = this._setPosition(0);
this.position = 0;
if (audioEnded) {
audioEnded(this);
}
Expand Down Expand Up @@ -455,8 +484,7 @@ export default class Sound extends Evented {
currentPosition,
newPosition,
});
this._setPosition(newPosition);
this._position = this._currentPosition();
this.position = newPosition;
}

/**
Expand All @@ -473,8 +501,7 @@ export default class Sound extends Evented {
currentPosition,
newPosition,
});
this._setPosition(newPosition);
this._position = this._currentPosition();
this.position = newPosition;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion addon/stereo-connections/howler.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export default class Howler extends BaseSound {
this.isLoading = true;
this.debug('#play');
if (typeof position !== 'undefined') {
this._setPosition(position);
this.position = position;
}
this.howl.play();
}
Expand Down
51 changes: 3 additions & 48 deletions tests/unit/services/stereo-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,51 +302,6 @@ module('Unit | Service | stereo', function (hooks) {
assert.equal(findSpy.callCount, 3, 'cache should have been checked');
});

test('position gets polled regularly on the currentSound but not on the others', function (assert) {
this.clock = sandbox.useFakeTimers();

const service = this.owner
.lookup('service:stereo')
.loadConnections(['NativeAudio']);

const INTERVAL = 20;

let sound1 = new (service.connectionLoader.get('NativeAudio'))({
url: '/good/1000/silence.mp3',
});
let sound2 = new (service.connectionLoader.get('NativeAudio'))({
url: '/good/1000/silence2.mp3',
});

// setOwner(sound1, getOwner(service));
// setOwner(sound2, getOwner(service));

let spy1 = sandbox.spy(sound1, '_currentPosition');
let spy2 = sandbox.spy(sound2, '_currentPosition');

assert.equal(spy1.callCount, 0, 'sound 1 should not have been polled yet');
assert.equal(spy2.callCount, 0, 'sound 1 should not have been polled yet');
service.set('pollInterval', INTERVAL);
service.currentSound = sound1;

this.clock.tick(INTERVAL * 4);

assert.equal(spy1.callCount, 4, 'sound 1 should have been polled 4 times');
assert.equal(spy2.callCount, 0, 'sound 2 should not have been polled yet');
service.currentSound = sound2;

this.clock.tick(INTERVAL * 2);

assert.equal(
spy1.callCount,
4,
'sound 1 should not have been polled again'
);
assert.equal(spy2.callCount, 2, 'sound 2 should have been polled twice');

this.clock.restore();
});

test('volume changes are set on the current sound', function (assert) {
assert.expect(7);
const service = this.owner
Expand Down Expand Up @@ -610,7 +565,7 @@ module('Unit | Service | stereo', function (hooks) {
'second sound should have its own position'
);

await sound2.play();
sound2.play();
sound2.position = 125;

assert.equal(
Expand All @@ -624,14 +579,14 @@ module('Unit | Service | stereo', function (hooks) {
'second sound should still have its own position'
);

await sound1.play();
sound1.play();
assert.equal(
Math.floor(sound1._currentPosition()),
100,
'first sound should still have its own position'
);
sound2.position = 300;
await sound2.play();
sound2.play();
assert.equal(
Math.floor(sound2._currentPosition()),
300,
Expand Down
Loading

0 comments on commit 0777253

Please sign in to comment.