Skip to content

Commit

Permalink
fix: enable/disable not being respected after the content script has …
Browse files Browse the repository at this point in the history
…been loaded
  • Loading branch information
WofWca committed Jul 19, 2020
1 parent e95486b commit fa678bf
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 15 deletions.
45 changes: 43 additions & 2 deletions src/content/Controller.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';
import { audioContext, mediaElementSourcesMap } from './audioContext';
import PitchPreservingStretcherNode from './PitchPreservingStretcherNode';
import {
getRealtimeMargin,
Expand Down Expand Up @@ -35,9 +36,15 @@ export default class Controller {
}

async init() {
let resolveInitPromise;
// TODO how about also rejecting it when `init()` throws? Would need to put the whole initialization in the promise
// executor?
this._initPromise = new Promise(resolve => resolveInitPromise = resolve);

this.element.playbackRate = this.settings.soundedSpeed;

const ctx = new AudioContext();
const ctx = audioContext;
this.audioContext = ctx;
await ctx.audioWorklet.addModule(chrome.runtime.getURL('SilenceDetectorProcessor.js'));
await ctx.audioWorklet.addModule(chrome.runtime.getURL('VolumeFilter.js'));

Expand Down Expand Up @@ -73,7 +80,15 @@ export default class Controller {
this._lookahead = lookahead;
const stretcher = new PitchPreservingStretcherNode(ctx, maxMaginStretcherDelay);
this._stretcher = stretcher;
const src = ctx.createMediaElementSource(this.element);
let src;
const srcFromMap = mediaElementSourcesMap.get(this.element);
if (srcFromMap) {
src = srcFromMap;
src.disconnect();
} else {
src = ctx.createMediaElementSource(this.element);
mediaElementSourcesMap.set(this.element, src)
}
src.connect(lookahead);
src.connect(volumeFilter);
volumeFilter.connect(silenceDetectorNode);
Expand Down Expand Up @@ -248,9 +263,35 @@ export default class Controller {
}, 1);
}

resolveInitPromise(this);
return this;
}

/**
* Assumes `init()` has been called (but not necessarily that its return promise has been resolved).
* TODO make it work when it's false?
*/
async destroy() {
await this._initPromise; // TODO would actually be better to interrupt it if it's still going.

const src = mediaElementSourcesMap.get(this.element);
src.disconnect();
src.connect(audioContext.destination);


this._silenceDetectorNode.port.close(); // So the message handler can no longer be triggered.

this._stretcher.destroy();
// TODO make `AudioWorkletProcessor`'s get collected.
// https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor/process#Return_value
// Currently they always return `true`.

// TODO close `AudioWorkletProcessor`'s message ports?

// TODO make sure built-in nodes (like gain) are also garbage-collected (I think they should be).
this.element.playbackRate = 1; // TODO how about store the initial speed
}

/**
* Can be called either when initializing or when updating settings.
* TODO It's more performant to only update the things that rely on settings that changed, in a reactive way, but for
Expand Down
17 changes: 16 additions & 1 deletion src/content/PitchPreservingStretcherNode.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PitchShift, connect as ToneConnect, setContext as toneSetContext } from 'tone';
import { PitchShift, connect as ToneConnect, setContext as toneSetContext, ToneAudioNode } from 'tone';
import { getStretchSpeedChangeMultiplier } from './helpers';

export default class PitchPreservingStretcherNode {
Expand Down Expand Up @@ -131,4 +131,19 @@ export default class PitchPreservingStretcherNode {
setDelay(value) {
this.delayNode.value = value;
}

destroy() {
const toneAudioNodes = [this.speedUpPitchShift, this.slowDownPitchShift];
for (const node of toneAudioNodes) {
node.dispose();
}

if (process.env.NODE_ENV !== 'production') {
Object.values(this).forEach(propertyVal => {
if (propertyVal instanceof ToneAudioNode && !toneAudioNodes.includes(propertyVal)) {
console.warn('Undisposed ToneAudioNode found. Expected all to be disposed upon `destroy()` call');
}
})
}
}
}
7 changes: 7 additions & 0 deletions src/content/audioContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const audioContext = new AudioContext();

// Doing it the way it's suggested in https://stackoverflow.com/a/39725071/10406353
/**
* @type {WeakMap<HTMLVideoElement, MediaElementAudioSourceNode>}
*/
export const mediaElementSourcesMap = new WeakMap();
51 changes: 39 additions & 12 deletions src/content/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,51 @@ import defaultSettings from '../defaultSettings';
chrome.storage.sync.get(
defaultSettings,
function (settings) {
if (!settings.enabled) {
return;
}

const video = document.querySelector('video');
if (video === null) {
// TODO search again when document updates? Or just after some time?
console.log('Jump cutter: no video found. Exiting');
return;
}
const controller = new Controller(video, settings);
/**
* @type {null | Controller}
*/
let controller = null;

chrome.storage.onChanged.addListener(function (changes) {
function watchInstanceSettings(changes) {
const newValues = {};
for (const [settingName, change] of Object.entries(changes)) {
newValues[settingName] = change.newValue;
}
controller.updateSettings(newValues);
}
function initIfVideoPresent() {
const v = document.querySelector('video');
if (!v) {
// TODO search again when document updates? Or just after some time?
console.log('Jump cutter: no video found. Exiting');
return;
}
chrome.storage.sync.get(
defaultSettings,
function (settings) {
controller = new Controller(v, settings);
chrome.storage.onChanged.addListener(watchInstanceSettings);
controller.init();
}
);
}

if (settings.enabled) {
initIfVideoPresent();
}

chrome.storage.onChanged.addListener(function (changes) {
// Don't need to check if it's already initialized/deinitialized because it's a setting CHANGE, and it's already
// initialized/deinitialized in accordance to the setting a few lines above.
if (changes.enabled != undefined) {
if (changes.enabled.newValue === false) {
controller.destroy();
controller = null;
chrome.storage.onChanged.removeListener(watchInstanceSettings);
} else {
initIfVideoPresent();
}
}
});

controller.init();
Expand Down

0 comments on commit fa678bf

Please sign in to comment.