/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::rc::Rc; use base::id::PipelineId; use dom_struct::dom_struct; use js::rust::HandleObject; use servo_media::audio::context::{LatencyCategory, ProcessingState, RealTimeAudioContextOptions}; use crate::conversions::Convert; use crate::dom::baseaudiocontext::{BaseAudioContext, BaseAudioContextOptions}; use crate::dom::bindings::codegen::Bindings::AudioContextBinding::{ AudioContextLatencyCategory, AudioContextMethods, AudioContextOptions, AudioTimestamp, }; use crate::dom::bindings::codegen::Bindings::AudioNodeBinding::AudioNodeOptions; use crate::dom::bindings::codegen::Bindings::BaseAudioContextBinding::AudioContextState; use crate::dom::bindings::codegen::Bindings::BaseAudioContextBinding::BaseAudioContext_Binding::BaseAudioContextMethods; use crate::dom::bindings::codegen::UnionTypes::AudioContextLatencyCategoryOrDouble; use crate::dom::bindings::error::{Error, Fallible}; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::num::Finite; use crate::dom::bindings::refcounted::{Trusted, TrustedPromise}; use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object_with_proto}; use crate::dom::bindings::root::DomRoot; use crate::dom::htmlmediaelement::HTMLMediaElement; use crate::dom::mediaelementaudiosourcenode::MediaElementAudioSourceNode; use crate::dom::mediastream::MediaStream; use crate::dom::mediastreamaudiodestinationnode::MediaStreamAudioDestinationNode; use crate::dom::mediastreamaudiosourcenode::MediaStreamAudioSourceNode; use crate::dom::mediastreamtrack::MediaStreamTrack; use crate::dom::mediastreamtrackaudiosourcenode::MediaStreamTrackAudioSourceNode; use crate::dom::promise::Promise; use crate::dom::window::Window; use crate::realms::InRealm; use crate::script_runtime::CanGc; #[dom_struct] pub(crate) struct AudioContext { context: BaseAudioContext, latency_hint: AudioContextLatencyCategory, /// base_latency: f64, /// output_latency: f64, } impl AudioContext { #[cfg_attr(crown, allow(crown::unrooted_must_root))] // https://webaudio.github.io/web-audio-api/#AudioContext-constructors fn new_inherited( options: &AudioContextOptions, pipeline_id: PipelineId, ) -> Fallible { // Steps 1-3. let context = BaseAudioContext::new_inherited( BaseAudioContextOptions::AudioContext(options.convert()), pipeline_id, )?; // Step 4.1. let latency_hint = match options.latencyHint { AudioContextLatencyCategoryOrDouble::AudioContextLatencyCategory(category) => category, AudioContextLatencyCategoryOrDouble::Double(_) => { AudioContextLatencyCategory::Interactive }, // TODO }; // Step 4.2. The sample rate is set during the creation of the BaseAudioContext. // servo-media takes care of setting the default sample rate of the output device // and of resampling the audio output if needed. // Steps 5 and 6 of the construction algorithm will happen in `resume`, // after reflecting dom object. Ok(AudioContext { context, latency_hint, base_latency: 0., // TODO output_latency: 0., // TODO }) } #[cfg_attr(crown, allow(crown::unrooted_must_root))] fn new( window: &Window, proto: Option, options: &AudioContextOptions, can_gc: CanGc, ) -> Fallible> { let pipeline_id = window.pipeline_id(); let context = AudioContext::new_inherited(options, pipeline_id)?; let context = reflect_dom_object_with_proto(Box::new(context), window, proto, can_gc); context.resume(); Ok(context) } fn resume(&self) { // Step 5. if self.context.is_allowed_to_start() { // Step 6. self.context.resume(); } } pub(crate) fn base(&self) -> DomRoot { DomRoot::from_ref(&self.context) } } impl AudioContextMethods for AudioContext { // https://webaudio.github.io/web-audio-api/#AudioContext-constructors fn Constructor( window: &Window, proto: Option, can_gc: CanGc, options: &AudioContextOptions, ) -> Fallible> { AudioContext::new(window, proto, options, can_gc) } // https://webaudio.github.io/web-audio-api/#dom-audiocontext-baselatency fn BaseLatency(&self) -> Finite { Finite::wrap(self.base_latency) } // https://webaudio.github.io/web-audio-api/#dom-audiocontext-outputlatency fn OutputLatency(&self) -> Finite { Finite::wrap(self.output_latency) } // https://webaudio.github.io/web-audio-api/#dom-audiocontext-outputlatency fn GetOutputTimestamp(&self) -> AudioTimestamp { // TODO AudioTimestamp { contextTime: Some(Finite::wrap(0.)), performanceTime: Some(Finite::wrap(0.)), } } // https://webaudio.github.io/web-audio-api/#dom-audiocontext-suspend fn Suspend(&self, comp: InRealm, can_gc: CanGc) -> Rc { // Step 1. let promise = Promise::new_in_current_realm(comp, can_gc); // Step 2. if self.context.control_thread_state() == ProcessingState::Closed { promise.reject_error(Error::InvalidState, can_gc); return promise; } // Step 3. if self.context.State() == AudioContextState::Suspended { promise.resolve_native(&(), can_gc); return promise; } // Steps 4 and 5. let trusted_promise = TrustedPromise::new(promise.clone()); match self.context.audio_context_impl().lock().unwrap().suspend() { Ok(_) => { let base_context = Trusted::new(&self.context); let context = Trusted::new(self); self.global().task_manager().dom_manipulation_task_source().queue( task!(suspend_ok: move || { let base_context = base_context.root(); let context = context.root(); let promise = trusted_promise.root(); promise.resolve_native(&(), CanGc::note()); if base_context.State() != AudioContextState::Suspended { base_context.set_state_attribute(AudioContextState::Suspended); context.global().task_manager().dom_manipulation_task_source().queue_simple_event( context.upcast(), atom!("statechange"), ); } }) ); }, Err(_) => { // The spec does not define the error case and `suspend` should // never fail, but we handle the case here for completion. self.global() .task_manager() .dom_manipulation_task_source() .queue(task!(suspend_error: move || { let promise = trusted_promise.root(); promise.reject_error(Error::Type("Something went wrong".to_owned()), CanGc::note()); })); }, }; // Step 6. promise } // https://webaudio.github.io/web-audio-api/#dom-audiocontext-close fn Close(&self, comp: InRealm, can_gc: CanGc) -> Rc { // Step 1. let promise = Promise::new_in_current_realm(comp, can_gc); // Step 2. if self.context.control_thread_state() == ProcessingState::Closed { promise.reject_error(Error::InvalidState, can_gc); return promise; } // Step 3. if self.context.State() == AudioContextState::Closed { promise.resolve_native(&(), can_gc); return promise; } // Steps 4 and 5. let trusted_promise = TrustedPromise::new(promise.clone()); match self.context.audio_context_impl().lock().unwrap().close() { Ok(_) => { let base_context = Trusted::new(&self.context); let context = Trusted::new(self); self.global().task_manager().dom_manipulation_task_source().queue( task!(suspend_ok: move || { let base_context = base_context.root(); let context = context.root(); let promise = trusted_promise.root(); promise.resolve_native(&(), CanGc::note()); if base_context.State() != AudioContextState::Closed { base_context.set_state_attribute(AudioContextState::Closed); context.global().task_manager().dom_manipulation_task_source().queue_simple_event( context.upcast(), atom!("statechange"), ); } }) ); }, Err(_) => { // The spec does not define the error case and `suspend` should // never fail, but we handle the case here for completion. self.global() .task_manager() .dom_manipulation_task_source() .queue(task!(suspend_error: move || { let promise = trusted_promise.root(); promise.reject_error(Error::Type("Something went wrong".to_owned()), CanGc::note()); })); }, }; // Step 6. promise } /// fn CreateMediaElementSource( &self, media_element: &HTMLMediaElement, can_gc: CanGc, ) -> Fallible> { let global = self.global(); let window = global.as_window(); MediaElementAudioSourceNode::new(window, self, media_element, can_gc) } /// fn CreateMediaStreamSource( &self, stream: &MediaStream, can_gc: CanGc, ) -> Fallible> { let global = self.global(); let window = global.as_window(); MediaStreamAudioSourceNode::new(window, self, stream, can_gc) } /// fn CreateMediaStreamTrackSource( &self, track: &MediaStreamTrack, can_gc: CanGc, ) -> Fallible> { let global = self.global(); let window = global.as_window(); MediaStreamTrackAudioSourceNode::new(window, self, track, can_gc) } /// fn CreateMediaStreamDestination( &self, can_gc: CanGc, ) -> Fallible> { let global = self.global(); let window = global.as_window(); MediaStreamAudioDestinationNode::new(window, self, &AudioNodeOptions::empty(), can_gc) } } impl Convert for AudioContextLatencyCategory { fn convert(self) -> LatencyCategory { match self { AudioContextLatencyCategory::Balanced => LatencyCategory::Balanced, AudioContextLatencyCategory::Interactive => LatencyCategory::Interactive, AudioContextLatencyCategory::Playback => LatencyCategory::Playback, } } } impl Convert for &AudioContextOptions { fn convert(self) -> RealTimeAudioContextOptions { RealTimeAudioContextOptions { sample_rate: *self.sampleRate.unwrap_or(Finite::wrap(44100.)), latency_hint: match self.latencyHint { AudioContextLatencyCategoryOrDouble::AudioContextLatencyCategory(category) => { category.convert() }, AudioContextLatencyCategoryOrDouble::Double(_) => LatencyCategory::Interactive, // TODO }, } } }