/
githubmirror
/
servo
Обзор
Документация
Войти
/
githubmirror
/
servo
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
components/script/dom/audio/baseaudiocontext.rs
619 строк
25 KB
Josh Matthews
script: Do not panic when getting contents of detached array buffers. (#46504)
20 июл 2026, 14:29
Не верифицирован
20 июл 2026, 14:29
05acee3
Код
Авторство
О чём код?
/* 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::cell::Cell; use std::collections::hash_map::Entry; use std::collections::{HashMap, VecDeque}; use std::rc::Rc; use std::sync::{Arc, Mutex}; use dom_struct::dom_struct; use js::context::JSContext; use js::realm::CurrentRealm; use js::rust::CustomAutoRooterGuard; use js::typedarray::ArrayBuffer; use script_bindings::cell::DomRefCell; use script_bindings::cformat; use servo_base::id::PipelineId; use servo_media::audio::context::{ AudioContext, AudioContextOptions, OfflineAudioContextOptions, ProcessingState, RealTimeAudioContextOptions, }; use servo_media::audio::decoder::AudioDecoderCallbacksBuilder; use servo_media::audio::graph::NodeId; use servo_media::{ClientContextId, ServoMedia}; use uuid::Uuid; use crate::conversions::Convert; use crate::dom::audio::analysernode::AnalyserNode; use crate::dom::audio::audiobuffer::AudioBuffer; use crate::dom::audio::audiobuffersourcenode::AudioBufferSourceNode; use crate::dom::audio::audiodestinationnode::AudioDestinationNode; use crate::dom::audio::audiolistener::AudioListener; use crate::dom::audio::audionode::MAX_CHANNEL_COUNT; use crate::dom::audio::biquadfilternode::BiquadFilterNode; use crate::dom::audio::channelmergernode::ChannelMergerNode; use crate::dom::audio::channelsplitternode::ChannelSplitterNode; use crate::dom::audio::constantsourcenode::ConstantSourceNode; use crate::dom::audio::gainnode::GainNode; use crate::dom::audio::iirfilternode::IIRFilterNode; use crate::dom::audio::oscillatornode::OscillatorNode; use crate::dom::audio::pannernode::PannerNode; use crate::dom::audio::stereopannernode::StereoPannerNode; use crate::dom::bindings::callback::ExceptionHandling; use crate::dom::bindings::codegen::Bindings::AnalyserNodeBinding::AnalyserOptions; use crate::dom::bindings::codegen::Bindings::AudioBufferSourceNodeBinding::AudioBufferSourceOptions; use crate::dom::bindings::codegen::Bindings::AudioNodeBinding::{ AudioNodeOptions, ChannelCountMode, ChannelInterpretation, }; use crate::dom::bindings::codegen::Bindings::BaseAudioContextBinding::{ AudioContextState, BaseAudioContextMethods, DecodeErrorCallback, DecodeSuccessCallback, }; use crate::dom::bindings::codegen::Bindings::BiquadFilterNodeBinding::BiquadFilterOptions; use crate::dom::bindings::codegen::Bindings::ChannelMergerNodeBinding::ChannelMergerOptions; use crate::dom::bindings::codegen::Bindings::ChannelSplitterNodeBinding::ChannelSplitterOptions; use crate::dom::bindings::codegen::Bindings::ConstantSourceNodeBinding::ConstantSourceOptions; use crate::dom::bindings::codegen::Bindings::GainNodeBinding::GainOptions; use crate::dom::bindings::codegen::Bindings::IIRFilterNodeBinding::IIRFilterOptions; use crate::dom::bindings::codegen::Bindings::OscillatorNodeBinding::OscillatorOptions; use crate::dom::bindings::codegen::Bindings::PannerNodeBinding::PannerOptions; use crate::dom::bindings::codegen::Bindings::StereoPannerNodeBinding::StereoPannerOptions; use crate::dom::bindings::error::{Error, ErrorResult, Fallible}; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::num::Finite; use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::reflector::DomGlobal; use crate::dom::bindings::root::{DomRoot, MutNullableDom}; use crate::dom::domexception::{DOMErrorName, DOMException}; use crate::dom::eventtarget::EventTarget; use crate::dom::promise::Promise; pub(crate) enum BaseAudioContextOptions { AudioContext(RealTimeAudioContextOptions), OfflineAudioContext(OfflineAudioContextOptions), } #[derive(JSTraceable, MallocSizeOf)] struct DecodeResolver { #[conditional_malloc_size_of] pub(crate) promise: Rc<Promise>, #[conditional_malloc_size_of] pub(crate) success_callback: Option<Rc<DecodeSuccessCallback>>, #[conditional_malloc_size_of] pub(crate) error_callback: Option<Rc<DecodeErrorCallback>>, } type BoxedSliceOfPromises = Box<[Rc<Promise>]>; #[dom_struct] pub(crate) struct BaseAudioContext { eventtarget: EventTarget, #[ignore_malloc_size_of = "servo_media"] #[no_trace] audio_context_impl: Arc<Mutex<AudioContext>>, /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-destination> destination: MutNullableDom<AudioDestinationNode>, listener: MutNullableDom<AudioListener>, /// Resume promises which are soon to be fulfilled by a queued task. #[conditional_malloc_size_of] in_flight_resume_promises_queue: DomRefCell<VecDeque<(BoxedSliceOfPromises, ErrorResult)>>, /// <https://webaudio.github.io/web-audio-api/#pendingresumepromises> #[conditional_malloc_size_of] pending_resume_promises: DomRefCell<Vec<Rc<Promise>>>, decode_resolvers: DomRefCell<HashMap<String, DecodeResolver>>, /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-samplerate> sample_rate: f32, /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-state> /// Although servo-media already keeps track of the control thread state, /// we keep a state flag here as well. This is so that we can synchronously /// throw when trying to do things on the context when the context has just /// been "closed()". state: Cell<AudioContextState>, channel_count: u32, } impl BaseAudioContext { pub(crate) fn new_inherited( options: BaseAudioContextOptions, pipeline_id: PipelineId, ) -> Fallible<BaseAudioContext> { let (sample_rate, channel_count) = match options { BaseAudioContextOptions::AudioContext(ref opt) => (opt.sample_rate, 2), BaseAudioContextOptions::OfflineAudioContext(ref opt) => { (opt.sample_rate, opt.channels) }, }; let client_context_id = ClientContextId::build(pipeline_id.namespace_id.0, pipeline_id.index.0.get()); let audio_context_impl = ServoMedia::get() .create_audio_context(&client_context_id, options.convert()) .map_err(|_| Error::NotSupported(None))?; Ok(BaseAudioContext { eventtarget: EventTarget::new_inherited(), audio_context_impl, destination: Default::default(), listener: Default::default(), in_flight_resume_promises_queue: Default::default(), pending_resume_promises: Default::default(), decode_resolvers: Default::default(), sample_rate, state: Cell::new(AudioContextState::Suspended), channel_count: channel_count.into(), }) } /// Tells whether this is an OfflineAudioContext or not. pub(crate) fn is_offline(&self) -> bool { false } pub(crate) fn audio_context_impl(&self) -> Arc<Mutex<AudioContext>> { self.audio_context_impl.clone() } pub(crate) fn destination_node(&self) -> NodeId { self.audio_context_impl.lock().unwrap().dest_node() } pub(crate) fn listener(&self) -> NodeId { self.audio_context_impl.lock().unwrap().listener() } // https://webaudio.github.io/web-audio-api/#allowed-to-start pub(crate) fn is_allowed_to_start(&self) -> bool { self.state.get() == AudioContextState::Suspended } fn push_pending_resume_promise(&self, promise: &Rc<Promise>) { self.pending_resume_promises .borrow_mut() .push(promise.clone()); } /// Takes the pending resume promises. /// /// The result with which these promises will be fulfilled is passed here /// and this method returns nothing because we actually just move the /// current list of pending resume promises to the /// `in_flight_resume_promises_queue` field. /// /// Each call to this method must be followed by a call to /// `fulfill_in_flight_resume_promises`, to actually fulfill the promises /// which were taken and moved to the in-flight queue. fn take_pending_resume_promises(&self, result: ErrorResult) { let pending_resume_promises = std::mem::take(&mut *self.pending_resume_promises.borrow_mut()); self.in_flight_resume_promises_queue .borrow_mut() .push_back((pending_resume_promises.into(), result)); } /// Fulfills the next in-flight resume promises queue after running a closure. /// /// See the comment on `take_pending_resume_promises` for why this method /// does not take a list of promises to fulfill. Callers cannot just pop /// the front list off of `in_flight_resume_promises_queue` and later fulfill /// the promises because that would mean putting /// `#[cfg_attr(crown, expect(crown::unrooted_must_root))]` on even more functions, potentially /// hiding actual safety bugs. fn fulfill_in_flight_resume_promises<F>(&self, cx: &mut JSContext, f: F) where F: FnOnce(), { let (promises, result) = self .in_flight_resume_promises_queue .borrow_mut() .pop_front() .expect("there should be at least one list of in flight resume promises"); f(); for promise in &*promises { match result { Ok(ref value) => promise.resolve_native(cx, value), Err(ref error) => promise.reject_error(cx, error.clone()), } } } /// Control thread processing state pub(crate) fn control_thread_state(&self) -> ProcessingState { self.audio_context_impl.lock().unwrap().state() } /// Set audio context state pub(crate) fn set_state_attribute(&self, state: AudioContextState) { self.state.set(state); } pub(crate) fn resume(&self) { let this = Trusted::new(self); // Set the rendering thread state to 'running' and start // rendering the audio graph. match self.audio_context_impl.lock().unwrap().resume() { Some(()) => { self.take_pending_resume_promises(Ok(())); self.global().task_manager().dom_manipulation_task_source().queue( task!(resume_success: move |cx| { let this = this.root(); this.fulfill_in_flight_resume_promises(cx, || { if this.state.get() != AudioContextState::Running { this.state.set(AudioContextState::Running); this.global().task_manager().dom_manipulation_task_source().queue_simple_event( this.upcast(), atom!("statechange"), ); } }); }) ); }, None => { self.take_pending_resume_promises(Err(Error::Type( c"Something went wrong".to_owned(), ))); self.global() .task_manager() .dom_manipulation_task_source() .queue(task!(resume_error: move |cx| { this.root().fulfill_in_flight_resume_promises(cx, || {}) })); }, } } pub(crate) fn channel_count(&self) -> u32 { self.channel_count } } impl BaseAudioContextMethods<crate::DomTypeHolder> for BaseAudioContext { /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-samplerate> fn SampleRate(&self) -> Finite<f32> { Finite::wrap(self.sample_rate) } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-currenttime> fn CurrentTime(&self) -> Finite<f64> { let current_time = self.audio_context_impl.lock().unwrap().current_time(); Finite::wrap(current_time) } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-state> fn State(&self) -> AudioContextState { self.state.get() } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-resume> fn Resume(&self, cx: &mut CurrentRealm) -> Rc<Promise> { // Step 1. let promise = Promise::new_in_realm(cx); // Step 2. if self.audio_context_impl.lock().unwrap().state() == ProcessingState::Closed { promise.reject_error(cx, Error::InvalidState(None)); return promise; } // Step 3. if self.state.get() == AudioContextState::Running { promise.resolve_native(cx, &()); return promise; } self.push_pending_resume_promise(&promise); // Step 4. if !self.is_allowed_to_start() { return promise; } // Steps 5 and 6. self.resume(); // Step 7. promise } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-destination> fn Destination(&self, cx: &mut JSContext) -> DomRoot<AudioDestinationNode> { let global = self.global(); self.destination.or_init(|| { let mut options = AudioNodeOptions::empty(); options.channelCount = Some(self.channel_count); options.channelCountMode = Some(ChannelCountMode::Explicit); options.channelInterpretation = Some(ChannelInterpretation::Speakers); AudioDestinationNode::new(cx, &global, self, &options) }) } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-listener> fn Listener(&self, cx: &mut JSContext) -> DomRoot<AudioListener> { let global = self.global(); let window = global.as_window(); self.listener .or_init(|| AudioListener::new(cx, window, self)) } // https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-onstatechange event_handler!(statechange, GetOnstatechange, SetOnstatechange); /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createoscillator> fn CreateOscillator(&self, cx: &mut JSContext) -> Fallible<DomRoot<OscillatorNode>> { OscillatorNode::new( cx, self.global().as_window(), self, &OscillatorOptions::empty(), ) } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-creategain> fn CreateGain(&self, cx: &mut JSContext) -> Fallible<DomRoot<GainNode>> { GainNode::new(cx, self.global().as_window(), self, &GainOptions::empty()) } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createpanner> fn CreatePanner(&self, cx: &mut JSContext) -> Fallible<DomRoot<PannerNode>> { PannerNode::new(cx, self.global().as_window(), self, &PannerOptions::empty()) } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createanalyser> fn CreateAnalyser(&self, cx: &mut JSContext) -> Fallible<DomRoot<AnalyserNode>> { AnalyserNode::new( cx, self.global().as_window(), self, &AnalyserOptions::empty(), ) } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createbiquadfilter> fn CreateBiquadFilter(&self, cx: &mut JSContext) -> Fallible<DomRoot<BiquadFilterNode>> { BiquadFilterNode::new( cx, self.global().as_window(), self, &BiquadFilterOptions::empty(), ) } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createstereopanner> fn CreateStereoPanner(&self, cx: &mut JSContext) -> Fallible<DomRoot<StereoPannerNode>> { StereoPannerNode::new( cx, self.global().as_window(), self, &StereoPannerOptions::empty(), ) } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createconstantsource> fn CreateConstantSource(&self, cx: &mut JSContext) -> Fallible<DomRoot<ConstantSourceNode>> { ConstantSourceNode::new( cx, self.global().as_window(), self, &ConstantSourceOptions::empty(), ) } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createchannelmerger> fn CreateChannelMerger( &self, cx: &mut JSContext, count: u32, ) -> Fallible<DomRoot<ChannelMergerNode>> { let mut opts = ChannelMergerOptions::empty(); opts.numberOfInputs = count; ChannelMergerNode::new(cx, self.global().as_window(), self, &opts) } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createchannelsplitter> fn CreateChannelSplitter( &self, cx: &mut JSContext, count: u32, ) -> Fallible<DomRoot<ChannelSplitterNode>> { let mut opts = ChannelSplitterOptions::empty(); opts.numberOfOutputs = count; ChannelSplitterNode::new(cx, self.global().as_window(), self, &opts) } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createbuffer> fn CreateBuffer( &self, cx: &mut JSContext, number_of_channels: u32, length: u32, sample_rate: Finite<f32>, ) -> Fallible<DomRoot<AudioBuffer>> { if number_of_channels == 0 || number_of_channels > MAX_CHANNEL_COUNT || length == 0 || *sample_rate <= 0. { return Err(Error::NotSupported(None)); } Ok(AudioBuffer::new( cx, self.global().as_window(), number_of_channels, length, *sample_rate, None, )) } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createbuffersource> fn CreateBufferSource(&self, cx: &mut JSContext) -> Fallible<DomRoot<AudioBufferSourceNode>> { AudioBufferSourceNode::new( cx, self.global().as_window(), self, &AudioBufferSourceOptions::empty(), ) } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-decodeaudiodata> fn DecodeAudioData( &self, cx: &mut CurrentRealm, audio_data: CustomAutoRooterGuard<ArrayBuffer>, decode_success_callback: Option<Rc<DecodeSuccessCallback>>, decode_error_callback: Option<Rc<DecodeErrorCallback>>, ) -> Rc<Promise> { // Step 1. let promise = Promise::new_in_realm(cx); if let Some(audio_data) = audio_data.to_vec() { // Step 2. // XXX detach array buffer. let uuid = Uuid::new_v4().simple().to_string(); let uuid_ = uuid.clone(); self.decode_resolvers.safe_borrow_mut(cx.no_gc()).insert( uuid.clone(), DecodeResolver { promise: promise.clone(), success_callback: decode_success_callback, error_callback: decode_error_callback, }, ); let decoded_audio = Arc::new(Mutex::new(Vec::new())); let decoded_audio_ = decoded_audio.clone(); let decoded_audio__ = decoded_audio.clone(); // servo-media returns an audio channel position along // with the AudioDecoderCallback progress callback, which // may not be the same as the index of the decoded_audio // Vec. let channels = Arc::new(Mutex::new(HashMap::new())); let this = Trusted::new(self); let this_ = this.clone(); let task_source = self .global() .task_manager() .dom_manipulation_task_source() .to_sendable(); let task_source_clone = task_source.clone(); let callbacks = AudioDecoderCallbacksBuilder::default() .ready(move |channel_count| { decoded_audio .lock() .unwrap() .resize(channel_count as usize, Vec::new()); }) .progress(move |buffer, channel_pos_mask| { let mut decoded_audio = decoded_audio_.lock().unwrap(); let mut channels = channels.lock().unwrap(); let channel = match channels.entry(channel_pos_mask) { Entry::Occupied(entry) => *entry.get(), Entry::Vacant(entry) => { let x = (channel_pos_mask as f32).log2() as usize; *entry.insert(x) }, }; decoded_audio[channel].extend_from_slice((*buffer).as_ref()); }) .eos(move || { task_source.queue(task!(audio_decode_eos: move |cx| { let this = this.root(); let decoded_audio = decoded_audio__.lock().unwrap(); let length = if !decoded_audio.is_empty() { decoded_audio[0].len() } else { 0 }; let buffer = AudioBuffer::new( cx, this.global().as_window(), decoded_audio.len() as u32 /* number of channels */, length as u32, this.sample_rate, Some(decoded_audio.as_slice()), ); // Potential borrow hazard let resolver = { let mut resolvers = this.decode_resolvers.safe_borrow_mut(cx.no_gc()); assert!(resolvers.contains_key(&uuid_)); resolvers.remove(&uuid_).unwrap() }; if let Some(callback) = resolver.success_callback { let _ = callback.Call__(cx, &buffer, ExceptionHandling::Report); } resolver.promise.resolve_native(cx, &buffer); })); }) .error(move |error| { task_source_clone.queue(task!(audio_decode_eos: move |cx| { let this = this_.root(); // potential borrow hazard let resolver = { let mut resolvers = this.decode_resolvers.safe_borrow_mut(cx.no_gc()); assert!(resolvers.contains_key(&uuid)); resolvers.remove(&uuid).unwrap() }; if let Some(callback) = resolver.error_callback { let exception = DOMException::new(cx, &this.global(), DOMErrorName::DataCloneError, ); let _ = callback.Call__(cx, &exception, ExceptionHandling::Report); } let error = cformat!("Audio decode error {:?}", error); resolver.promise.reject_error(cx, Error::Type(error)); })); }) .build(); self.audio_context_impl .lock() .unwrap() .decode_audio_data(audio_data, callbacks); } else { // Step 3. promise.reject_error(cx, Error::DataClone(None)); return promise; } // Step 4. promise } /// <https://webaudio.github.io/web-audio-api/#dom-baseaudiocontext-createiirfilter> fn CreateIIRFilter( &self, cx: &mut JSContext, feedforward: Vec<Finite<f64>>, feedback: Vec<Finite<f64>>, ) -> Fallible<DomRoot<IIRFilterNode>> { let opts = IIRFilterOptions { parent: AudioNodeOptions::empty(), feedback, feedforward, }; IIRFilterNode::new(cx, self.global().as_window(), self, &opts) } } impl Convert<AudioContextOptions> for BaseAudioContextOptions { fn convert(self) -> AudioContextOptions { match self { BaseAudioContextOptions::AudioContext(options) => { AudioContextOptions::RealTimeAudioContext(options) }, BaseAudioContextOptions::OfflineAudioContext(options) => { AudioContextOptions::OfflineAudioContext(options) }, } } } impl Convert<AudioContextState> for ProcessingState { fn convert(self) -> AudioContextState { match self { ProcessingState::Suspended => AudioContextState::Suspended, ProcessingState::Running => AudioContextState::Running, ProcessingState::Closed => AudioContextState::Closed, } } }