AudioWorkletGlobalScope.cpp (12665B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "AudioWorkletGlobalScope.h" 8 9 #include "AudioNodeEngine.h" 10 #include "AudioNodeTrack.h" 11 #include "AudioWorkletImpl.h" 12 #include "Tracing.h" 13 #include "js/ForOfIterator.h" 14 #include "js/PropertyAndElement.h" // JS_GetProperty 15 #include "jsapi.h" 16 #include "mozilla/BasePrincipal.h" 17 #include "mozilla/dom/AudioParamDescriptorBinding.h" 18 #include "mozilla/dom/AudioWorkletGlobalScopeBinding.h" 19 #include "mozilla/dom/AudioWorkletProcessor.h" 20 #include "mozilla/dom/BindingCallContext.h" 21 #include "mozilla/dom/MessagePort.h" 22 #include "mozilla/dom/StructuredCloneHolder.h" 23 #include "nsPrintfCString.h" 24 #include "nsTHashSet.h" 25 26 namespace mozilla::dom { 27 28 NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioWorkletGlobalScope, WorkletGlobalScope, 29 mNameToProcessorMap, mPort); 30 31 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioWorkletGlobalScope) 32 NS_INTERFACE_MAP_END_INHERITING(WorkletGlobalScope) 33 34 NS_IMPL_ADDREF_INHERITED(AudioWorkletGlobalScope, WorkletGlobalScope) 35 NS_IMPL_RELEASE_INHERITED(AudioWorkletGlobalScope, WorkletGlobalScope) 36 37 AudioWorkletGlobalScope::AudioWorkletGlobalScope(AudioWorkletImpl* aImpl) 38 : WorkletGlobalScope(aImpl) {} 39 40 AudioWorkletImpl* AudioWorkletGlobalScope::Impl() const { 41 return static_cast<AudioWorkletImpl*>(mImpl.get()); 42 } 43 44 bool AudioWorkletGlobalScope::WrapGlobalObject( 45 JSContext* aCx, JS::MutableHandle<JSObject*> aReflector) { 46 // |this| is being exposed to JS and content script will soon be running. 47 // The graph needs a handle on the JSContext so it can interrupt JS. 48 Impl()->DestinationTrack()->Graph()->NotifyJSContext(aCx); 49 50 JS::RealmOptions options = CreateRealmOptions(); 51 return AudioWorkletGlobalScope_Binding::Wrap( 52 aCx, this, this, options, BasePrincipal::Cast(mImpl->Principal()), 53 aReflector); 54 } 55 56 void AudioWorkletGlobalScope::RegisterProcessor( 57 JSContext* aCx, const nsAString& aName, 58 AudioWorkletProcessorConstructor& aProcessorCtor, ErrorResult& aRv) { 59 TRACE_COMMENT("AudioWorkletGlobalScope::RegisterProcessor", "%s", 60 NS_ConvertUTF16toUTF8(aName).get()); 61 62 JS::Rooted<JSObject*> processorConstructor(aCx, 63 aProcessorCtor.CallableOrNull()); 64 65 /** 66 * 1. If the name is the empty string, throw a NotSupportedError 67 * exception and abort these steps because the empty string is not 68 * a valid key. 69 */ 70 if (aName.IsEmpty()) { 71 aRv.ThrowNotSupportedError("Argument 1 should not be an empty string."); 72 return; 73 } 74 75 /** 76 * 2. If the name exists as a key in the node name to processor 77 * definition map, throw a NotSupportedError exception and abort 78 * these steps because registering a definition with a duplicated 79 * key is not allowed. 80 */ 81 if (mNameToProcessorMap.GetWeak(aName)) { 82 // Duplicate names are not allowed 83 aRv.ThrowNotSupportedError( 84 "Argument 1 is invalid: a class with the same name is already " 85 "registered."); 86 return; 87 } 88 89 // We know processorConstructor is callable, so not a WindowProxy or Location. 90 JS::Rooted<JSObject*> constructorUnwrapped( 91 aCx, js::CheckedUnwrapStatic(processorConstructor)); 92 if (!constructorUnwrapped) { 93 // If the caller's compartment does not have permission to access the 94 // unwrapped constructor then throw. 95 aRv.ThrowSecurityError("Constructor cannot be called"); 96 return; 97 } 98 99 /** 100 * 3. If the result of IsConstructor(argument=processorCtor) is false, 101 * throw a TypeError and abort these steps. 102 */ 103 if (!JS::IsConstructor(constructorUnwrapped)) { 104 aRv.ThrowTypeError<MSG_NOT_CONSTRUCTOR>("Argument 2"); 105 return; 106 } 107 108 /** 109 * 4. Let prototype be the result of Get(O=processorCtor, P="prototype"). 110 */ 111 // The .prototype on the constructor passed could be an "expando" of a 112 // wrapper. So we should get it from wrapper instead of the underlying 113 // object. 114 JS::Rooted<JS::Value> prototype(aCx); 115 if (!JS_GetProperty(aCx, processorConstructor, "prototype", &prototype)) { 116 aRv.NoteJSContextException(aCx); 117 return; 118 } 119 120 /** 121 * 5. If the result of Type(argument=prototype) is not Object, throw a 122 * TypeError and abort all these steps. 123 */ 124 if (!prototype.isObject()) { 125 aRv.ThrowTypeError<MSG_NOT_OBJECT>("processorCtor.prototype"); 126 return; 127 } 128 /** 129 * 6. Let parameterDescriptorsValue be the result of Get(O=processorCtor, 130 * P="parameterDescriptors"). 131 */ 132 JS::Rooted<JS::Value> descriptors(aCx); 133 if (!JS_GetProperty(aCx, processorConstructor, "parameterDescriptors", 134 &descriptors)) { 135 aRv.NoteJSContextException(aCx); 136 return; 137 } 138 139 AudioParamDescriptorMap map; 140 /* 141 * 7. If parameterDescriptorsValue is not undefined 142 */ 143 if (!descriptors.isUndefined()) { 144 /* 145 * 7.1. Let parameterDescriptorSequence be the result of the conversion 146 * from parameterDescriptorsValue to an IDL value of type 147 * sequence<AudioParamDescriptor>. 148 */ 149 JS::Rooted<JS::Value> objectValue(aCx, descriptors); 150 JS::ForOfIterator iter(aCx); 151 if (!iter.init(objectValue, JS::ForOfIterator::AllowNonIterable)) { 152 aRv.NoteJSContextException(aCx); 153 return; 154 } 155 if (!iter.valueIsIterable()) { 156 aRv.ThrowTypeError<MSG_CONVERSION_ERROR>( 157 "AudioWorkletProcessor.parameterDescriptors", "sequence"); 158 return; 159 } 160 /* 161 * 7.2 and 7.3 (and substeps) 162 */ 163 map = DescriptorsFromJS(aCx, &iter, aRv); 164 if (aRv.Failed()) { 165 return; 166 } 167 } 168 169 /** 170 * 8. Append the key-value pair name → processorCtor to node name to processor 171 * constructor map of the associated AudioWorkletGlobalScope. 172 */ 173 if (!mNameToProcessorMap.InsertOrUpdate(aName, RefPtr{&aProcessorCtor}, 174 fallible)) { 175 aRv.Throw(NS_ERROR_OUT_OF_MEMORY); 176 return; 177 } 178 179 /** 180 * 9. Queue a task to the control thread to add the key-value pair 181 * (name - descriptors) to the node name to parameter descriptor 182 * map of the associated BaseAudioContext. 183 */ 184 NS_DispatchToMainThread(NS_NewRunnableFunction( 185 "AudioWorkletGlobalScope: parameter descriptors", 186 [impl = RefPtr{Impl()}, name = nsString(aName), 187 map = std::move(map)]() mutable { 188 AudioNode* destinationNode = 189 impl->DestinationTrack()->Engine()->NodeMainThread(); 190 if (!destinationNode) { 191 return; 192 } 193 destinationNode->Context()->SetParamMapForWorkletName(name, &map); 194 })); 195 } 196 197 uint64_t AudioWorkletGlobalScope::CurrentFrame() const { 198 AudioNodeTrack* destinationTrack = Impl()->DestinationTrack(); 199 GraphTime processedTime = destinationTrack->Graph()->ProcessedTime(); 200 return destinationTrack->GraphTimeToTrackTime(processedTime); 201 } 202 203 double AudioWorkletGlobalScope::CurrentTime() const { 204 return static_cast<double>(CurrentFrame()) / SampleRate(); 205 } 206 207 float AudioWorkletGlobalScope::SampleRate() const { 208 return static_cast<float>(Impl()->DestinationTrack()->mSampleRate); 209 } 210 211 AudioParamDescriptorMap AudioWorkletGlobalScope::DescriptorsFromJS( 212 JSContext* aCx, JS::ForOfIterator* aIter, ErrorResult& aRv) { 213 AudioParamDescriptorMap res; 214 // To check for duplicates 215 nsTHashSet<nsString> namesSet; 216 217 JS::Rooted<JS::Value> nextValue(aCx); 218 bool done = false; 219 size_t i = 0; 220 while (true) { 221 if (!aIter->next(&nextValue, &done)) { 222 aRv.NoteJSContextException(aCx); 223 return AudioParamDescriptorMap(); 224 } 225 if (done) { 226 break; 227 } 228 229 BindingCallContext callCx(aCx, "AudioWorkletGlobalScope.registerProcessor"); 230 nsPrintfCString sourceDescription("Element %zu in parameterDescriptors", i); 231 i++; 232 AudioParamDescriptor* descriptor = res.AppendElement(fallible); 233 if (!descriptor) { 234 aRv.Throw(NS_ERROR_OUT_OF_MEMORY); 235 return AudioParamDescriptorMap(); 236 } 237 if (!descriptor->Init(callCx, nextValue, sourceDescription.get())) { 238 aRv.NoteJSContextException(aCx); 239 return AudioParamDescriptorMap(); 240 } 241 } 242 243 for (const auto& descriptor : res) { 244 if (namesSet.Contains(descriptor.mName)) { 245 aRv.ThrowNotSupportedError("Duplicated name \""_ns + 246 NS_ConvertUTF16toUTF8(descriptor.mName) + 247 "\" in parameterDescriptors."_ns); 248 return AudioParamDescriptorMap(); 249 } 250 251 if (!namesSet.Insert(descriptor.mName, fallible)) { 252 aRv.Throw(NS_ERROR_OUT_OF_MEMORY); 253 return AudioParamDescriptorMap(); 254 } 255 256 if (descriptor.mMinValue > descriptor.mMaxValue) { 257 aRv.ThrowInvalidStateError( 258 "In parameterDescriptors, "_ns + 259 NS_ConvertUTF16toUTF8(descriptor.mName) + 260 " minValue should be smaller than maxValue."_ns); 261 return AudioParamDescriptorMap(); 262 } 263 264 if (descriptor.mDefaultValue < descriptor.mMinValue || 265 descriptor.mDefaultValue > descriptor.mMaxValue) { 266 aRv.ThrowInvalidStateError( 267 "In parameterDescriptors, "_ns + 268 NS_ConvertUTF16toUTF8(descriptor.mName) + 269 nsLiteralCString(" defaultValue is out of the range defined by " 270 "minValue and maxValue.")); 271 return AudioParamDescriptorMap(); 272 } 273 } 274 275 return res; 276 } 277 278 bool AudioWorkletGlobalScope::ConstructProcessor( 279 JSContext* aCx, const nsAString& aName, 280 NotNull<StructuredCloneHolder*> aSerializedOptions, 281 UniqueMessagePortId& aPortIdentifier, 282 JS::MutableHandle<JSObject*> aRetProcessor) { 283 TRACE_COMMENT("AudioWorkletProcessor::ConstructProcessor", "%s", 284 NS_ConvertUTF16toUTF8(aName).get()); 285 /** 286 * See 287 * https://webaudio.github.io/web-audio-api/#AudioWorkletProcessor-instantiation 288 */ 289 ErrorResult rv; 290 /** 291 * 4. Let deserializedPort be the result of 292 * StructuredDeserialize(serializedPort, the current Realm). 293 */ 294 RefPtr<MessagePort> deserializedPort = 295 MessagePort::Create(this, aPortIdentifier, rv); 296 if (NS_WARN_IF(rv.MaybeSetPendingException(aCx))) { 297 return false; 298 } 299 /** 300 * 5. Let deserializedOptions be the result of 301 * StructuredDeserialize(serializedOptions, the current Realm). 302 */ 303 JS::CloneDataPolicy cloneDataPolicy; 304 cloneDataPolicy.allowIntraClusterClonableSharedObjects(); 305 cloneDataPolicy.allowSharedMemoryObjects(); 306 307 JS::Rooted<JS::Value> deserializedOptions(aCx); 308 aSerializedOptions->Read(this, aCx, &deserializedOptions, cloneDataPolicy, 309 rv); 310 if (rv.MaybeSetPendingException(aCx)) { 311 return false; 312 } 313 /** 314 * 6. Let processorCtor be the result of looking up processorName on the 315 * AudioWorkletGlobalScope's node name to processor definition map. 316 */ 317 RefPtr<AudioWorkletProcessorConstructor> processorCtor = 318 mNameToProcessorMap.Get(aName); 319 // AudioWorkletNode has already checked the definition exists. 320 // See also https://github.com/WebAudio/web-audio-api/issues/1854 321 MOZ_ASSERT(processorCtor); 322 /** 323 * 7. Store nodeReference and deserializedPort to node reference and 324 * transferred port of this AudioWorkletGlobalScope's pending processor 325 * construction data respectively. 326 */ 327 // |nodeReference| is not required here because the "processorerror" event 328 // is thrown by WorkletNodeEngine::ConstructProcessor(). 329 mPortForProcessor = std::move(deserializedPort); 330 /** 331 * 8. Construct a callback function from processorCtor with the argument 332 * of deserializedOptions. 333 */ 334 // The options were an object before serialization and so will be an object 335 // if deserialization succeeded above. toObject() asserts. 336 JS::Rooted<JSObject*> options(aCx, &deserializedOptions.toObject()); 337 RefPtr<AudioWorkletProcessor> processor = processorCtor->Construct( 338 options, rv, "AudioWorkletProcessor construction", 339 CallbackFunction::eRethrowExceptions); 340 // https://github.com/WebAudio/web-audio-api/issues/2096 341 mPortForProcessor = nullptr; 342 if (rv.MaybeSetPendingException(aCx)) { 343 return false; 344 } 345 JS::Rooted<JS::Value> processorVal(aCx); 346 if (NS_WARN_IF(!ToJSValue(aCx, processor, &processorVal))) { 347 return false; 348 } 349 MOZ_ASSERT(processorVal.isObject()); 350 aRetProcessor.set(&processorVal.toObject()); 351 return true; 352 } 353 354 RefPtr<MessagePort> AudioWorkletGlobalScope::TakePortForProcessorCtor() { 355 return std::move(mPortForProcessor); 356 } 357 358 } // namespace mozilla::dom