Bug 1951240 - Implement AudioWorklet.port, r=padenot,webidl,saschanaz
Differential Revision: https://phabricator.services.mozilla.com/D240104
This commit is contained in:
@@ -60,10 +60,6 @@ DOMInterfaces = {
|
||||
'implicitJSContext': [ 'buffer' ],
|
||||
},
|
||||
|
||||
'AudioWorklet': {
|
||||
'nativeType': 'mozilla::dom::Worklet',
|
||||
},
|
||||
|
||||
'AudioWorkletGlobalScope': {
|
||||
'implicitJSContext': [ 'registerProcessor' ],
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "mozilla/dom/AnalyserNodeBinding.h"
|
||||
#include "mozilla/dom/AudioBufferSourceNodeBinding.h"
|
||||
#include "mozilla/dom/AudioContextBinding.h"
|
||||
#include "mozilla/dom/AudioWorklet.h"
|
||||
#include "mozilla/dom/BaseAudioContextBinding.h"
|
||||
#include "mozilla/dom/BiquadFilterNodeBinding.h"
|
||||
#include "mozilla/dom/BrowsingContext.h"
|
||||
@@ -43,7 +44,6 @@
|
||||
#include "mozilla/dom/Promise.h"
|
||||
#include "mozilla/dom/StereoPannerNodeBinding.h"
|
||||
#include "mozilla/dom/WaveShaperNodeBinding.h"
|
||||
#include "mozilla/dom/Worklet.h"
|
||||
|
||||
#include "AudioBuffer.h"
|
||||
#include "AudioBufferSourceNode.h"
|
||||
@@ -594,7 +594,7 @@ void AudioContext::GetOutputTimestamp(AudioTimestamp& aTimeStamp) {
|
||||
}
|
||||
}
|
||||
|
||||
Worklet* AudioContext::GetAudioWorklet(ErrorResult& aRv) {
|
||||
AudioWorklet* AudioContext::GetAudioWorklet(ErrorResult& aRv) {
|
||||
if (!mWorklet) {
|
||||
mWorklet = AudioWorkletImpl::CreateWorklet(this, aRv);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ class AudioBufferSourceNode;
|
||||
class AudioDestinationNode;
|
||||
class AudioListener;
|
||||
class AudioNode;
|
||||
class AudioWorklet;
|
||||
class BiquadFilterNode;
|
||||
class BrowsingContext;
|
||||
class ChannelMergerNode;
|
||||
@@ -72,7 +73,6 @@ class PannerNode;
|
||||
class ScriptProcessorNode;
|
||||
class StereoPannerNode;
|
||||
class WaveShaperNode;
|
||||
class Worklet;
|
||||
class PeriodicWave;
|
||||
struct PeriodicWaveConstraints;
|
||||
class Promise;
|
||||
@@ -202,7 +202,7 @@ class AudioContext final : public DOMEventTargetHelper,
|
||||
|
||||
void GetOutputTimestamp(AudioTimestamp& aTimeStamp);
|
||||
|
||||
Worklet* GetAudioWorklet(ErrorResult& aRv);
|
||||
AudioWorklet* GetAudioWorklet(ErrorResult& aRv);
|
||||
|
||||
bool IsRunning() const;
|
||||
|
||||
@@ -393,7 +393,7 @@ class AudioContext final : public DOMEventTargetHelper,
|
||||
AudioContextState mAudioContextState;
|
||||
RefPtr<AudioDestinationNode> mDestination;
|
||||
RefPtr<AudioListener> mListener;
|
||||
RefPtr<Worklet> mWorklet;
|
||||
RefPtr<AudioWorklet> mWorklet;
|
||||
nsTArray<UniquePtr<WebAudioDecodeJob>> mDecodeJobs;
|
||||
// This array is used to keep the suspend/close promises alive until
|
||||
// they are resolved, so we can safely pass them accross threads.
|
||||
|
||||
36
dom/media/webaudio/AudioWorklet.cpp
Normal file
36
dom/media/webaudio/AudioWorklet.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* 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 http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "AudioWorklet.h"
|
||||
#include "mozilla/dom/MessagePort.h"
|
||||
#include "mozilla/dom/WorkletImpl.h"
|
||||
|
||||
namespace mozilla::dom {
|
||||
|
||||
NS_IMPL_ADDREF_INHERITED(AudioWorklet, Worklet)
|
||||
NS_IMPL_RELEASE_INHERITED(AudioWorklet, Worklet)
|
||||
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioWorklet)
|
||||
NS_INTERFACE_MAP_END_INHERITING(Worklet)
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_CLASS(AudioWorklet)
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(AudioWorklet, Worklet)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPort)
|
||||
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(AudioWorklet, Worklet)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK(mPort)
|
||||
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
||||
|
||||
AudioWorklet::AudioWorklet(nsPIDOMWindowInner* aWindow,
|
||||
RefPtr<WorkletImpl> aImpl, nsISupports* aOwnedObject,
|
||||
MessagePort* aPort)
|
||||
: Worklet(aWindow, aImpl, aOwnedObject), mPort(aPort) {
|
||||
MOZ_ASSERT(aPort);
|
||||
}
|
||||
|
||||
} // namespace mozilla::dom
|
||||
34
dom/media/webaudio/AudioWorklet.h
Normal file
34
dom/media/webaudio/AudioWorklet.h
Normal file
@@ -0,0 +1,34 @@
|
||||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* 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 http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef mozilla_dom_AudioWorklet_h
|
||||
#define mozilla_dom_AudioWorklet_h
|
||||
|
||||
#include "mozilla/dom/Worklet.h"
|
||||
|
||||
namespace mozilla::dom {
|
||||
|
||||
class MessagePort;
|
||||
|
||||
class AudioWorklet final : public Worklet {
|
||||
public:
|
||||
NS_DECL_ISUPPORTS_INHERITED
|
||||
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioWorklet, Worklet)
|
||||
|
||||
AudioWorklet(nsPIDOMWindowInner* aWindow, RefPtr<WorkletImpl> aImpl,
|
||||
nsISupports* aOwnedObject, MessagePort* aPort);
|
||||
|
||||
MessagePort* Port() const { return mPort; };
|
||||
|
||||
private:
|
||||
~AudioWorklet() = default;
|
||||
|
||||
RefPtr<MessagePort> mPort;
|
||||
};
|
||||
|
||||
} // namespace mozilla::dom
|
||||
|
||||
#endif // mozilla_dom_AudioWorklet_h
|
||||
@@ -26,7 +26,7 @@
|
||||
namespace mozilla::dom {
|
||||
|
||||
NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioWorkletGlobalScope, WorkletGlobalScope,
|
||||
mNameToProcessorMap);
|
||||
mNameToProcessorMap, mPort);
|
||||
|
||||
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioWorkletGlobalScope)
|
||||
NS_INTERFACE_MAP_END_INHERITING(WorkletGlobalScope)
|
||||
|
||||
@@ -47,6 +47,10 @@ class AudioWorkletGlobalScope final : public WorkletGlobalScope {
|
||||
|
||||
float SampleRate() const;
|
||||
|
||||
MessagePort* Port() const { return mPort; };
|
||||
|
||||
void SetPort(MessagePort* aPort) { mPort = aPort; }
|
||||
|
||||
// If successful, returns true and sets aRetProcessor, which will be in the
|
||||
// compartment for the realm of this global. Returns false on failure.
|
||||
MOZ_CAN_RUN_SCRIPT
|
||||
@@ -76,6 +80,8 @@ class AudioWorkletGlobalScope final : public WorkletGlobalScope {
|
||||
// This does not need to be traversed during cycle-collection because it is
|
||||
// only set while this AudioWorkletGlobalScope is on the stack.
|
||||
RefPtr<MessagePort> mPortForProcessor;
|
||||
|
||||
RefPtr<MessagePort> mPort;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
|
||||
@@ -8,17 +8,19 @@
|
||||
|
||||
#include "AudioContext.h"
|
||||
#include "AudioNodeTrack.h"
|
||||
#include "AudioWorklet.h"
|
||||
#include "GeckoProfiler.h"
|
||||
#include "mozilla/dom/AudioWorkletBinding.h"
|
||||
#include "mozilla/dom/AudioWorkletGlobalScope.h"
|
||||
#include "mozilla/dom/Worklet.h"
|
||||
#include "mozilla/dom/MessageChannel.h"
|
||||
#include "mozilla/dom/MessagePort.h"
|
||||
#include "mozilla/dom/WorkletThread.h"
|
||||
#include "nsGlobalWindowInner.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
/* static */ already_AddRefed<dom::Worklet> AudioWorkletImpl::CreateWorklet(
|
||||
dom::AudioContext* aContext, ErrorResult& aRv) {
|
||||
/* static */ already_AddRefed<dom::AudioWorklet>
|
||||
AudioWorkletImpl::CreateWorklet(dom::AudioContext* aContext, ErrorResult& aRv) {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
nsGlobalWindowInner* window = aContext->GetOwnerWindow();
|
||||
@@ -32,27 +34,41 @@ namespace mozilla {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefPtr<dom::MessageChannel> messageChannel =
|
||||
dom::MessageChannel::Constructor(window, aRv);
|
||||
if (NS_WARN_IF(aRv.Failed())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
dom::UniqueMessagePortId globalScopePortId;
|
||||
messageChannel->Port2()->CloneAndDisentangle(globalScopePortId);
|
||||
|
||||
RefPtr<AudioWorkletImpl> impl =
|
||||
new AudioWorkletImpl(window, principal, aContext->DestinationTrack());
|
||||
new AudioWorkletImpl(window, principal, aContext->DestinationTrack(),
|
||||
std::move(globalScopePortId));
|
||||
|
||||
// The Worklet owns a reference to the AudioContext so as to keep the graph
|
||||
// thread running as long as the Worklet is alive by keeping the
|
||||
// AudioDestinationNode alive.
|
||||
return MakeAndAddRef<dom::Worklet>(window, std::move(impl),
|
||||
ToSupports(aContext));
|
||||
return MakeAndAddRef<dom::AudioWorklet>(
|
||||
window, std::move(impl), ToSupports(aContext), messageChannel->Port1());
|
||||
}
|
||||
|
||||
AudioWorkletImpl::AudioWorkletImpl(nsPIDOMWindowInner* aWindow,
|
||||
nsIPrincipal* aPrincipal,
|
||||
AudioNodeTrack* aDestinationTrack)
|
||||
: WorkletImpl(aWindow, aPrincipal), mDestinationTrack(aDestinationTrack) {}
|
||||
AudioNodeTrack* aDestinationTrack,
|
||||
dom::UniqueMessagePortId&& aPortIdentifier)
|
||||
: WorkletImpl(aWindow, aPrincipal),
|
||||
mDestinationTrack(aDestinationTrack),
|
||||
mGlobalScopePortIdentifier(std::move(aPortIdentifier)) {}
|
||||
|
||||
AudioWorkletImpl::~AudioWorkletImpl() = default;
|
||||
|
||||
JSObject* AudioWorkletImpl::WrapWorklet(JSContext* aCx, dom::Worklet* aWorklet,
|
||||
JS::Handle<JSObject*> aGivenProto) {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
return dom::AudioWorklet_Binding::Wrap(aCx, aWorklet, aGivenProto);
|
||||
return dom::AudioWorklet_Binding::Wrap(
|
||||
aCx, static_cast<dom::AudioWorklet*>(aWorklet), aGivenProto);
|
||||
}
|
||||
|
||||
nsresult AudioWorkletImpl::SendControlMessage(
|
||||
@@ -78,10 +94,23 @@ void AudioWorkletImpl::OnAddModulePromiseSettled() const {
|
||||
}
|
||||
|
||||
already_AddRefed<dom::WorkletGlobalScope>
|
||||
AudioWorkletImpl::ConstructGlobalScope() {
|
||||
AudioWorkletImpl::ConstructGlobalScope(JSContext* aCx) {
|
||||
dom::WorkletThread::AssertIsOnWorkletThread();
|
||||
|
||||
return MakeAndAddRef<dom::AudioWorkletGlobalScope>(this);
|
||||
RefPtr<dom::AudioWorkletGlobalScope> globalScope =
|
||||
new dom::AudioWorkletGlobalScope(this);
|
||||
|
||||
ErrorResult rv;
|
||||
RefPtr<dom::MessagePort> deserializedPort =
|
||||
dom::MessagePort::Create(globalScope, mGlobalScopePortIdentifier, rv);
|
||||
if (NS_WARN_IF(rv.MaybeSetPendingException(aCx))) {
|
||||
// The exception will be propagated into the global
|
||||
return globalScope.forget();
|
||||
}
|
||||
|
||||
globalScope->SetPort(deserializedPort);
|
||||
|
||||
return globalScope.forget();
|
||||
}
|
||||
|
||||
} // namespace mozilla
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "mozilla/dom/WorkletImpl.h"
|
||||
#include "mozilla/dom/AudioWorkletGlobalScope.h"
|
||||
#include "mozilla/dom/MessagePort.h"
|
||||
|
||||
namespace mozilla {
|
||||
|
||||
@@ -16,13 +17,14 @@ class AudioNodeTrack;
|
||||
|
||||
namespace dom {
|
||||
class AudioContext;
|
||||
}
|
||||
class AudioWorklet;
|
||||
} // namespace dom
|
||||
|
||||
class AudioWorkletImpl final : public WorkletImpl {
|
||||
public:
|
||||
// Methods for parent thread only:
|
||||
|
||||
static already_AddRefed<dom::Worklet> CreateWorklet(
|
||||
static already_AddRefed<dom::AudioWorklet> CreateWorklet(
|
||||
dom::AudioContext* aContext, ErrorResult& aRv);
|
||||
|
||||
JSObject* WrapWorklet(JSContext* aCx, dom::Worklet* aWorklet,
|
||||
@@ -48,14 +50,17 @@ class AudioWorkletImpl final : public WorkletImpl {
|
||||
|
||||
protected:
|
||||
// Execution thread only.
|
||||
already_AddRefed<dom::WorkletGlobalScope> ConstructGlobalScope() override;
|
||||
already_AddRefed<dom::WorkletGlobalScope> ConstructGlobalScope(
|
||||
JSContext* aCx) override;
|
||||
|
||||
private:
|
||||
AudioWorkletImpl(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal,
|
||||
AudioNodeTrack* aDestinationTrack);
|
||||
AudioNodeTrack* aDestinationTrack,
|
||||
dom::UniqueMessagePortId&& aPortIdentifier);
|
||||
~AudioWorkletImpl();
|
||||
|
||||
const RefPtr<AudioNodeTrack> mDestinationTrack;
|
||||
dom::UniqueMessagePortId mGlobalScopePortIdentifier;
|
||||
};
|
||||
|
||||
} // namespace mozilla
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include "AudioNodeEngine.h"
|
||||
#include "AudioParamMap.h"
|
||||
#include "AudioWorklet.h"
|
||||
#include "AudioWorkletImpl.h"
|
||||
#include "js/Array.h" // JS::{Get,Set}ArrayLength, JS::NewArrayLength
|
||||
#include "js/CallAndConstruct.h" // JS::Call, JS::IsCallable
|
||||
|
||||
@@ -54,6 +54,7 @@ EXPORTS.mozilla.dom += [
|
||||
"AudioParamMap.h",
|
||||
"AudioProcessingEvent.h",
|
||||
"AudioScheduledSourceNode.h",
|
||||
"AudioWorklet.h",
|
||||
"AudioWorkletGlobalScope.h",
|
||||
"AudioWorkletNode.h",
|
||||
"AudioWorkletProcessor.h",
|
||||
@@ -95,6 +96,7 @@ UNIFIED_SOURCES += [
|
||||
"AudioParamMap.cpp",
|
||||
"AudioProcessingEvent.cpp",
|
||||
"AudioScheduledSourceNode.cpp",
|
||||
"AudioWorklet.cpp",
|
||||
"AudioWorkletGlobalScope.cpp",
|
||||
"AudioWorkletImpl.cpp",
|
||||
"AudioWorkletNode.cpp",
|
||||
|
||||
@@ -821,7 +821,8 @@ void MessagePort::ForceClose(const MessagePortIdentifier& aIdentifier) {
|
||||
mozilla::ipc::PBackgroundChild* actorChild =
|
||||
mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
|
||||
if (NS_WARN_IF(!actorChild)) {
|
||||
MOZ_CRASH("Failed to create a PBackgroundChild actor!");
|
||||
// We are shutting down this process. This port will be leaked.
|
||||
return;
|
||||
}
|
||||
|
||||
Unused << actorChild->SendMessagePortForceClose(aIdentifier.uuid(),
|
||||
|
||||
@@ -12,4 +12,5 @@
|
||||
|
||||
[Exposed=Window, SecureContext]
|
||||
interface AudioWorklet : Worklet {
|
||||
readonly attribute MessagePort port;
|
||||
};
|
||||
|
||||
@@ -16,4 +16,5 @@ interface AudioWorkletGlobalScope : WorkletGlobalScope {
|
||||
readonly attribute unsigned long long currentFrame;
|
||||
readonly attribute double currentTime;
|
||||
readonly attribute float sampleRate;
|
||||
readonly attribute MessagePort port;
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ class WorkletScriptHandler;
|
||||
struct WorkletOptions;
|
||||
enum class CallerType : uint32_t;
|
||||
|
||||
class Worklet final : public nsISupports, public nsWrapperCache {
|
||||
class Worklet : public nsISupports, public nsWrapperCache {
|
||||
public:
|
||||
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
|
||||
NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Worklet)
|
||||
@@ -54,9 +54,10 @@ class Worklet final : public nsISupports, public nsWrapperCache {
|
||||
return mLocalizedStrings;
|
||||
}
|
||||
|
||||
private:
|
||||
~Worklet();
|
||||
protected:
|
||||
virtual ~Worklet();
|
||||
|
||||
private:
|
||||
WorkletFetchHandler* GetImportFetchHandler(const nsACString& aURI);
|
||||
|
||||
void AddImportFetchHandler(const nsACString& aURI,
|
||||
|
||||
@@ -91,7 +91,7 @@ dom::WorkletGlobalScope* WorkletImpl::GetGlobalScope() {
|
||||
jsapi.Init();
|
||||
JSContext* cx = jsapi.cx();
|
||||
|
||||
mGlobalScope = ConstructGlobalScope();
|
||||
mGlobalScope = ConstructGlobalScope(cx);
|
||||
|
||||
JS::Rooted<JSObject*> global(cx);
|
||||
NS_ENSURE_TRUE(mGlobalScope->WrapGlobalObject(cx, &global), nullptr);
|
||||
|
||||
@@ -103,7 +103,8 @@ class WorkletImpl {
|
||||
WorkletImpl(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal);
|
||||
virtual ~WorkletImpl();
|
||||
|
||||
virtual already_AddRefed<dom::WorkletGlobalScope> ConstructGlobalScope() = 0;
|
||||
virtual already_AddRefed<dom::WorkletGlobalScope> ConstructGlobalScope(
|
||||
JSContext* aCx) = 0;
|
||||
|
||||
// Modified only in constructor.
|
||||
ipc::PrincipalInfo mPrincipalInfo;
|
||||
|
||||
@@ -31,7 +31,7 @@ PaintWorkletImpl::PaintWorkletImpl(nsPIDOMWindowInner* aWindow,
|
||||
PaintWorkletImpl::~PaintWorkletImpl() = default;
|
||||
|
||||
already_AddRefed<dom::WorkletGlobalScope>
|
||||
PaintWorkletImpl::ConstructGlobalScope() {
|
||||
PaintWorkletImpl::ConstructGlobalScope(JSContext* aCx) {
|
||||
dom::WorkletThread::AssertIsOnWorkletThread();
|
||||
|
||||
return MakeAndAddRef<dom::PaintWorkletGlobalScope>(this);
|
||||
|
||||
@@ -24,7 +24,8 @@ class PaintWorkletImpl final : public WorkletImpl {
|
||||
|
||||
protected:
|
||||
// Execution thread only.
|
||||
already_AddRefed<dom::WorkletGlobalScope> ConstructGlobalScope() override;
|
||||
already_AddRefed<dom::WorkletGlobalScope> ConstructGlobalScope(
|
||||
JSContext* aCx) override;
|
||||
|
||||
private:
|
||||
PaintWorkletImpl(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal);
|
||||
|
||||
@@ -167,12 +167,6 @@
|
||||
[AudioRenderCapacityEvent interface: attribute underrunRatio]
|
||||
expected: FAIL
|
||||
|
||||
[AudioWorklet interface: attribute port]
|
||||
expected: FAIL
|
||||
|
||||
[AudioWorklet interface: context.audioWorklet must inherit property "port" with the proper type]
|
||||
expected: FAIL
|
||||
|
||||
[AudioContext interface: attribute sinkId]
|
||||
expected: FAIL
|
||||
|
||||
|
||||
@@ -16,6 +16,19 @@
|
||||
|
||||
let filePath = 'processors/port-processor.js';
|
||||
|
||||
// AudioWorket to global communication.
|
||||
audit.define(
|
||||
'Test postMessage from AudioWorklet to AudioWorkletGlobalScope',
|
||||
(task, should) => {
|
||||
context.audioWorklet.port.onmessage = (event) => {
|
||||
should(event.data,
|
||||
'The response from AudioWorkletGlobalscope')
|
||||
.beEqualTo('hello world');
|
||||
task.done();
|
||||
};
|
||||
context.audioWorklet.port.postMessage("hello world");
|
||||
});
|
||||
|
||||
// Creates an AudioWorkletNode and sets an EventHandler on MessagePort
|
||||
// object. The associated PortProcessor will post a message upon its
|
||||
// construction. Test if the message is received correctly.
|
||||
|
||||
@@ -32,3 +32,5 @@ class PortProcessor extends AudioWorkletProcessor {
|
||||
}
|
||||
|
||||
registerProcessor('port-processor', PortProcessor);
|
||||
|
||||
port.onmessage = (event) => port.postMessage(event.data);
|
||||
|
||||
Reference in New Issue
Block a user