Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement EventTarget interface #61

Merged
merged 15 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ version = "0.14.0"
crate-type = ["cdylib"]

[dependencies]
napi = {version="2.13", features=["napi6", "tokio_rt"]}
napi = {version="2.13", features=["napi9", "tokio_rt"]}
napi-derive = "2.13"
web-audio-api = "0.39"
# web-audio-api = { path = "../web-audio-api-rs" }
# web-audio-api = "0.39"
web-audio-api = { path = "../web-audio-api-rs" }

[target.'cfg(all(any(windows, unix), target_arch = "x86_64", not(target_env = "musl")))'.dependencies]
mimalloc = {version = "0.1"}
Expand Down
24 changes: 17 additions & 7 deletions examples/change-state.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ const audioContext = new AudioContext({ latencyHint });

console.log('Context state - %s', audioContext.state);

audioContext.addEventListener('statechange', event => {
// should be called second
console.log('addEventListener', event);
});

audioContext.onstatechange = event => {
// should be called first
console.log('onstatechange', event);
};

const sine = audioContext.createOscillator();
sine.connect(audioContext.destination);
sine.frequency.value = 200;
Expand All @@ -27,21 +37,21 @@ sine.start();
// drop(mic);

console.log('> Playback for 2 seconds');
await new Promise(resolve => setTimeout(resolve, 2 * 1000));
await new Promise(resolve => setTimeout(resolve, 1 * 1000));

console.log('> Pause audioContext for 2 seconds');
console.log('Context state before suspend - %s', audioContext.state);
await audioContext.suspend();
console.log('Context state after suspend - %s', audioContext.state);

await new Promise(resolve => setTimeout(resolve, 2 * 1000));
await new Promise(resolve => setTimeout(resolve, 1 * 1000));

console.log('> Resume audioContext for 2 seconds');
console.log('Context state before resume - %s', audioContext.state);
await audioContext.resume();
console.log('Context state after resume - %s', audioContext.state);
// console.log('> Resume audioContext for 2 seconds');
// console.log('Context state before resume - %s', audioContext.state);
// await audioContext.resume();
// console.log('Context state after resume - %s', audioContext.state);

await new Promise(resolve => setTimeout(resolve, 2 * 1000));
// await new Promise(resolve => setTimeout(resolve, 2 * 1000));

// Closing the audioContext should halt the media stream source
console.log('> Close audioContext');
Expand Down
50 changes: 49 additions & 1 deletion generator/templates/audio_context.tmpl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ use std::io::Cursor;
use std::sync::Arc;

use napi::*;
use napi::threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunctionCallMode};
use napi_derive::js_function;
use web_audio_api::context::*;
use web_audio_api::Event;

use crate::*;

Expand Down Expand Up @@ -59,6 +61,8 @@ impl ${d.napiName(d.node)} {
Property::new("resume")?.with_method(resume_offline),
`
}
// private
Property::new("__initEventTarget__")?.with_method(init_event_target),
],
)
}
Expand Down Expand Up @@ -142,10 +146,13 @@ fn constructor(ctx: CallContext) -> Result<JsUndefined> {
let audio_context = ${d.name(d.node)}::new(number_of_channels, length, sample_rate);
`}

// wrap audio context in Arc
let audio_context = Arc::new(audio_context);

// -------------------------------------------------
// Wrap context
// -------------------------------------------------
let napi_audio_context = ${d.napiName(d.node)}(Arc::new(audio_context));
let napi_audio_context = ${d.napiName(d.node)}(audio_context);
ctx.env.wrap(&mut js_this, napi_audio_context)?;

js_this.define_properties(&[
Expand All @@ -156,6 +163,10 @@ fn constructor(ctx: CallContext) -> Result<JsUndefined> {
])?;


// test symbol as property name
// let test_symbol = ctx.env.symbol_for("test").unwrap();
// js_this.set_property(test_symbol, &ctx.env.create_string("test").unwrap())?;

// -------------------------------------------------
// Bind AudioDestination
// -------------------------------------------------
Expand All @@ -168,6 +179,7 @@ fn constructor(ctx: CallContext) -> Result<JsUndefined> {
ctx.env.get_undefined()
}


#[js_function]
fn get_current_time(ctx: CallContext) -> Result<JsNumber> {
let js_this = ctx.this_unchecked::<JsObject>();
Expand Down Expand Up @@ -512,3 +524,39 @@ fn resume_offline(ctx: CallContext) -> Result<JsObject> {
}
`
}


// ----------------------------------------------------
// Private Event Target initialization
// ----------------------------------------------------
#[js_function]
fn init_event_target(ctx: CallContext) -> Result<JsUndefined> {
let js_this = ctx.this_unchecked::<JsObject>();
let napi_obj = ctx.env.unwrap::<${d.napiName(d.node)}>(&js_this)?;
let context = napi_obj.0.clone();

let dispatch_event_symbol = ctx.env.symbol_for("napiDispatchEvent").unwrap();
let js_func = js_this.get_property(dispatch_event_symbol).unwrap();

let tsfn = ctx.env.create_threadsafe_function(&js_func, 0, |ctx: ThreadSafeCallContext<Event>| {
let event_type = ctx.env.create_string(ctx.value.type_)?;
Ok(vec![event_type])
})?;

let context_clone = context.clone();
context.set_onstatechange(move |e| {
tsfn.call(Ok(e), ThreadsafeFunctionCallMode::Blocking);

if context_clone.state() == AudioContextState::Closed {
// We need to clean things around so that the js object can be garbage collected.
// But we also need to wait so that the previous tsfn.call is executed,
// this is not clean, but don't see how to implement that properly right now.
std::thread::sleep(std::time::Duration::from_millis(100));
context_clone.clear_onstatechange();
let _ = tsfn.clone().abort();
}
});

ctx.env.get_undefined()
}

10 changes: 9 additions & 1 deletion js/AudioContext.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
const EventTargetMixin = require('./lib/EventTarget.mixin.js');

let contextId = 0;

const kProcessId = Symbol('processId');
const kKeepAwakeId = Symbol('keepAwakeId');

const kDispatchEvent = Symbol.for('napiDispatchEvent');

module.exports = function(NativeAudioContext) {
class AudioContext extends NativeAudioContext {
class AudioContext extends EventTargetMixin(NativeAudioContext, ['statechange']) {
// class AudioContext extends NativeAudioContext {
constructor(options = {}) {
super(options);
// EventTargetMixin[kDispatchEvent] is bound to this, this is safe to
// finalize event target initialization
super.__initEventTarget__();

const id = contextId++;
// store in process to prevent garbage collection
Expand Down
59 changes: 59 additions & 0 deletions js/lib/EventTarget.mixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const { isFunction } = require('./utils.js');

const kEventListeners = Symbol('eventListeners');
const kDispatchEvent = Symbol.for('napiDispatchEvent');

module.exports = (superclass, eventTypes) => class extends superclass {
[kEventListeners] = new Map();

constructor(...args) {
super(...args);

eventTypes.forEach((eventType) => {
this[`on${eventType}`] = null;
});

// we need to bind because calling [kDispatchEvent] loose `this`
this[kDispatchEvent] = this[kDispatchEvent].bind(this);
}

// instance might
addEventListener(eventType, callback) {
if (!this[kEventListeners].has(eventType)) {
this[kEventListeners].set(eventType, new Set());
}

const callbacks = this[kEventListeners].get(eventType);
callbacks.add(callback);
}

removeEventListener(eventType, callback) {
// this is valid event eventType, otherwaise just ignore
if (this[kEventListeners].has(eventType)) {
const callbacks = this[kEventListeners].get(eventType);
callbacks.delete(callback);
}
}

dispatchEvent(event) {
if (isFunction(this[`on${event.type}`])) {
this[`on${event.type}`](event);
}

if (this[kEventListeners].has(event.type)) {
const callbacks = this[kEventListeners].get(event.type);
callbacks.forEach(callback => callback(event));
}
}

// called from rust
[kDispatchEvent](err, eventType) {
const event = new Event(eventType);
// cannot override, this would need to derive EventTarget
// cf. https://www.nearform.com/blog/node-js-and-the-struggles-of-being-an-eventtarget/
// event.target = this;
// event.currentTarget = this;
// event.srcElement = this;
this.dispatchEvent(event);
}
}
5 changes: 5 additions & 0 deletions js/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ exports.isPositiveInt = function isPositiveInt(n) {
exports.isPositiveNumber = function isPositiveNumber(n) {
return Number(n) === n && 0 < n;
};

exports.isFunction = function isFunction(val) {
return Object.prototype.toString.call(val) == '[object Function]' ||
Object.prototype.toString.call(val) == '[object AsyncFunction]';
};
49 changes: 48 additions & 1 deletion src/audio_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
use std::io::Cursor;
use std::sync::Arc;

use napi::threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunctionCallMode};
use napi::*;
use napi_derive::js_function;
use web_audio_api::context::*;
use web_audio_api::Event;

use crate::*;

Expand Down Expand Up @@ -70,6 +72,8 @@ impl NapiAudioContext {
Property::new("resume")?.with_method(resume),
Property::new("suspend")?.with_method(suspend),
Property::new("close")?.with_method(close),
// private
Property::new("__initEventTarget__")?.with_method(init_event_target),
],
)
}
Expand Down Expand Up @@ -141,10 +145,13 @@ fn constructor(ctx: CallContext) -> Result<JsUndefined> {

let audio_context = AudioContext::new(audio_context_options);

// wrap audio context in Arc
let audio_context = Arc::new(audio_context);

// -------------------------------------------------
// Wrap context
// -------------------------------------------------
let napi_audio_context = NapiAudioContext(Arc::new(audio_context));
let napi_audio_context = NapiAudioContext(audio_context);
ctx.env.wrap(&mut js_this, napi_audio_context)?;

js_this.define_properties(&[
Expand All @@ -154,6 +161,10 @@ fn constructor(ctx: CallContext) -> Result<JsUndefined> {
.with_property_attributes(PropertyAttributes::Static),
])?;

// test symbol as property name
// let test_symbol = ctx.env.symbol_for("test").unwrap();
// js_this.set_property(test_symbol, &ctx.env.create_string("test").unwrap())?;

// -------------------------------------------------
// Bind AudioDestination
// -------------------------------------------------
Expand Down Expand Up @@ -602,3 +613,39 @@ fn create_media_stream_source(ctx: CallContext) -> Result<JsObject> {

ctor.new_instance(&[js_this, options])
}

// ----------------------------------------------------
// Private Event Target initialization
// ----------------------------------------------------
#[js_function]
fn init_event_target(ctx: CallContext) -> Result<JsUndefined> {
let js_this = ctx.this_unchecked::<JsObject>();
let napi_obj = ctx.env.unwrap::<NapiAudioContext>(&js_this)?;
let context = napi_obj.0.clone();

let dispatch_event_symbol = ctx.env.symbol_for("napiDispatchEvent").unwrap();
let js_func = js_this.get_property(dispatch_event_symbol).unwrap();

let tsfn =
ctx.env
.create_threadsafe_function(&js_func, 0, |ctx: ThreadSafeCallContext<Event>| {
let event_type = ctx.env.create_string(ctx.value.type_)?;
Ok(vec![event_type])
})?;

let context_clone = context.clone();
context.set_onstatechange(move |e| {
tsfn.call(Ok(e), ThreadsafeFunctionCallMode::Blocking);

if context_clone.state() == AudioContextState::Closed {
// We need to clean things around so that the js object can be garbage collected.
// But we also need to wait so that the previous tsfn.call is executed,
// this is not clean, but don't see how to implement that properly right now.
std::thread::sleep(std::time::Duration::from_millis(100));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope this hack is only needed for the onStateChange event? Or also for other types?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum no it seems to be required for other types as well, need to investigate further

context_clone.clear_onstatechange();
let _ = tsfn.clone().abort();
}
});

ctx.env.get_undefined()
}
Loading
Loading