diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /dom/midi | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/midi')
56 files changed, 4094 insertions, 0 deletions
diff --git a/dom/midi/MIDIAccess.cpp b/dom/midi/MIDIAccess.cpp new file mode 100644 index 0000000000..698473c20a --- /dev/null +++ b/dom/midi/MIDIAccess.cpp @@ -0,0 +1,221 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/MIDIAccess.h" +#include "mozilla/dom/MIDIAccessManager.h" +#include "mozilla/dom/MIDIPort.h" +#include "mozilla/dom/MIDIAccessBinding.h" +#include "mozilla/dom/MIDIConnectionEvent.h" +#include "mozilla/dom/MIDIOptionsBinding.h" +#include "mozilla/dom/MIDIOutputMapBinding.h" +#include "mozilla/dom/MIDIInputMapBinding.h" +#include "mozilla/dom/MIDIOutputMap.h" +#include "mozilla/dom/MIDIInputMap.h" +#include "mozilla/dom/MIDIOutput.h" +#include "mozilla/dom/MIDIInput.h" +#include "mozilla/dom/MIDITypes.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PContent.h" +#include "mozilla/dom/Document.h" +#include "nsPIDOMWindow.h" +#include "nsContentPermissionHelper.h" +#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, MOZ_COUNT_DTOR +#include "IPCMessageUtils.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(MIDIAccess) +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MIDIAccess, DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MIDIAccess, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputMap) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutputMap) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessPromise) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MIDIAccess, + DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputMap) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputMap) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAccessPromise) + tmp->Shutdown(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MIDIAccess) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(MIDIAccess, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(MIDIAccess, DOMEventTargetHelper) + +MIDIAccess::MIDIAccess(nsPIDOMWindowInner* aWindow, bool aSysexEnabled, + Promise* aPromise) + : DOMEventTargetHelper(aWindow), + mInputMap(new MIDIInputMap(aWindow)), + mOutputMap(new MIDIOutputMap(aWindow)), + mSysexEnabled(aSysexEnabled), + mAccessPromise(aPromise), + mHasShutdown(false) { + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aPromise); +} + +MIDIAccess::~MIDIAccess() { Shutdown(); } + +void MIDIAccess::Shutdown() { + if (mHasShutdown) { + return; + } + mDestructionObservers.Broadcast(void_t()); + if (MIDIAccessManager::IsRunning()) { + MIDIAccessManager::Get()->RemoveObserver(this); + } + mHasShutdown = true; +} + +void MIDIAccess::FireConnectionEvent(MIDIPort* aPort) { + MOZ_ASSERT(aPort); + MIDIConnectionEventInit init; + init.mPort = aPort; + nsAutoString id; + aPort->GetId(id); + ErrorResult rv; + if (aPort->State() == MIDIPortDeviceState::Disconnected) { + if (aPort->Type() == MIDIPortType::Input && + MIDIInputMap_Binding::MaplikeHelpers::Has(mInputMap, id, rv)) { + MIDIInputMap_Binding::MaplikeHelpers::Delete(mInputMap, id, rv); + } else if (aPort->Type() == MIDIPortType::Output && + MIDIOutputMap_Binding::MaplikeHelpers::Has(mOutputMap, id, rv)) { + MIDIOutputMap_Binding::MaplikeHelpers::Delete(mOutputMap, id, rv); + } + // Check to make sure Has()/Delete() calls haven't failed. + if (NS_WARN_IF(rv.Failed())) { + return; + } + } else { + // If we receive an event from a port that is not in one of our port maps, + // this means a port that was disconnected has been reconnected, with the + // port owner holding the object during that time, and we should add that + // port object to our maps again. + if (aPort->Type() == MIDIPortType::Input && + !MIDIInputMap_Binding::MaplikeHelpers::Has(mInputMap, id, rv)) { + if (NS_WARN_IF(rv.Failed())) { + return; + } + MIDIInputMap_Binding::MaplikeHelpers::Set( + mInputMap, id, *(static_cast<MIDIInput*>(aPort)), rv); + if (NS_WARN_IF(rv.Failed())) { + return; + } + } else if (aPort->Type() == MIDIPortType::Output && + !MIDIOutputMap_Binding::MaplikeHelpers::Has(mOutputMap, id, + rv)) { + if (NS_WARN_IF(rv.Failed())) { + return; + } + MIDIOutputMap_Binding::MaplikeHelpers::Set( + mOutputMap, id, *(static_cast<MIDIOutput*>(aPort)), rv); + if (NS_WARN_IF(rv.Failed())) { + return; + } + } + } + RefPtr<MIDIConnectionEvent> event = + MIDIConnectionEvent::Constructor(this, u"statechange"_ns, init); + DispatchTrustedEvent(event); +} + +void MIDIAccess::MaybeCreateMIDIPort(const MIDIPortInfo& aInfo, + ErrorResult& aRv) { + nsAutoString id(aInfo.id()); + MIDIPortType type = static_cast<MIDIPortType>(aInfo.type()); + RefPtr<MIDIPort> port; + if (type == MIDIPortType::Input) { + bool hasPort = + MIDIInputMap_Binding::MaplikeHelpers::Has(mInputMap, id, aRv); + if (hasPort || NS_WARN_IF(aRv.Failed())) { + // We already have the port in our map. + return; + } + port = MIDIInput::Create(GetOwner(), this, aInfo, mSysexEnabled); + if (NS_WARN_IF(!port)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + MIDIInputMap_Binding::MaplikeHelpers::Set( + mInputMap, id, *(static_cast<MIDIInput*>(port.get())), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } else if (type == MIDIPortType::Output) { + bool hasPort = + MIDIOutputMap_Binding::MaplikeHelpers::Has(mOutputMap, id, aRv); + if (hasPort || NS_WARN_IF(aRv.Failed())) { + // We already have the port in our map. + return; + } + port = MIDIOutput::Create(GetOwner(), this, aInfo, mSysexEnabled); + if (NS_WARN_IF(!port)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + MIDIOutputMap_Binding::MaplikeHelpers::Set( + mOutputMap, id, *(static_cast<MIDIOutput*>(port.get())), aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } else { + // If we hit this, then we have some port that is neither input nor output. + // That is bad. + MOZ_CRASH("We shouldn't be here!"); + } + // Set up port to listen for destruction of this access object. + mDestructionObservers.AddObserver(port); + + // If we haven't resolved the promise for handing the MIDIAccess object to + // content, this means we're still populating the list of already connected + // devices. Don't fire events yet. + if (!mAccessPromise) { + FireConnectionEvent(port); + } +} + +// For the MIDIAccess object, only worry about new connections, where we create +// MIDIPort objects. When a port is removed and the MIDIPortRemove event is +// received, that will be handled by the MIDIPort object itself, and it will +// request removal from MIDIAccess's maps. +void MIDIAccess::Notify(const MIDIPortList& aEvent) { + for (auto& port : aEvent.ports()) { + // Something went very wrong. Warn and return. + ErrorResult rv; + MaybeCreateMIDIPort(port, rv); + if (rv.Failed()) { + if (!mAccessPromise) { + return; + } + mAccessPromise->MaybeReject(std::move(rv)); + mAccessPromise = nullptr; + } + } + if (!mAccessPromise) { + return; + } + mAccessPromise->MaybeResolve(this); + mAccessPromise = nullptr; +} + +JSObject* MIDIAccess::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return MIDIAccess_Binding::Wrap(aCx, this, aGivenProto); +} + +void MIDIAccess::RemovePortListener(MIDIAccessDestructionObserver* aObs) { + mDestructionObservers.RemoveObserver(aObs); +} + +} // namespace mozilla::dom diff --git a/dom/midi/MIDIAccess.h b/dom/midi/MIDIAccess.h new file mode 100644 index 0000000000..99071aa526 --- /dev/null +++ b/dom/midi/MIDIAccess.h @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIAccess_h +#define mozilla_dom_MIDIAccess_h + +#include "mozilla/Attributes.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/Observer.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +struct JSContext; + +namespace mozilla { +class ErrorResult; + +// Predeclare void_t here, as including IPCMessageUtils brings in windows.h and +// causes binding compilation problems. +struct void_t; + +namespace dom { + +class MIDIAccessManager; +class MIDIInputMap; +struct MIDIOptions; +class MIDIOutputMap; +class MIDIPermissionRequest; +class MIDIPort; +class MIDIPortChangeEvent; +class MIDIPortInfo; +class MIDIPortList; +class Promise; + +typedef Observer<void_t> MIDIAccessDestructionObserver; + +/** + * MIDIAccess is the DOM object that is handed to the user upon MIDI permissions + * being successfully granted. It manages access to MIDI ports, and fires events + * for device connection and disconnection. + * + * New MIDIAccess objects are created every time RequestMIDIAccess is called. + * MIDIAccess objects are managed via MIDIAccessManager. + */ +class MIDIAccess final : public DOMEventTargetHelper, + public Observer<MIDIPortList> { + // Use the Permission Request class in MIDIAccessManager for creating + // MIDIAccess objects. + friend class MIDIPermissionRequest; + friend class MIDIAccessManager; + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(MIDIAccess, + DOMEventTargetHelper) + public: + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // Return map of MIDI Input Ports + MIDIInputMap* Inputs() const { return mInputMap; } + + // Return map of MIDI Output Ports + MIDIOutputMap* Outputs() const { return mOutputMap; } + + // Returns true if sysex permissions were given + bool SysexEnabled() const { return mSysexEnabled; } + + // Observer implementation for receiving port connection updates + void Notify(const MIDIPortList& aEvent) override; + + // All MIDIPort objects observe destruction of the MIDIAccess object that + // created them, as the port object receives disconnection events which then + // must be passed up to the MIDIAccess object. If the Port object dies before + // the MIDIAccess object, it needs to be removed from the observer list. + void RemovePortListener(MIDIAccessDestructionObserver* aPort); + + // Fires DOM event on port connection/disconnection + void FireConnectionEvent(MIDIPort* aPort); + + // Notify all MIDIPorts that were created by this MIDIAccess and are still + // alive, and detach from the MIDIAccessManager. + void Shutdown(); + IMPL_EVENT_HANDLER(statechange); + + private: + MIDIAccess(nsPIDOMWindowInner* aWindow, bool aSysexEnabled, + Promise* aAccessPromise); + ~MIDIAccess(); + + // On receiving a connection event from MIDIAccessManager, create a + // corresponding MIDIPort object if we don't already have one. + void MaybeCreateMIDIPort(const MIDIPortInfo& aInfo, ErrorResult& aRv); + + // Stores all known MIDIInput Ports + RefPtr<MIDIInputMap> mInputMap; + // Stores all known MIDIOutput Ports + RefPtr<MIDIOutputMap> mOutputMap; + // List of MIDIPort observers that need to be updated on destruction. + ObserverList<void_t> mDestructionObservers; + // True if user gave permissions for sysex usage to this object. + bool mSysexEnabled; + // Promise created by RequestMIDIAccess call, to be resolved after port + // populating is finished. + RefPtr<Promise> mAccessPromise; + // True if shutdown process has started, so we don't try to add more ports. + bool mHasShutdown; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MIDIAccess_h diff --git a/dom/midi/MIDIAccessManager.cpp b/dom/midi/MIDIAccessManager.cpp new file mode 100644 index 0000000000..65c5588cd4 --- /dev/null +++ b/dom/midi/MIDIAccessManager.cpp @@ -0,0 +1,149 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/MIDIAccessManager.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/MIDIAccess.h" +#include "mozilla/dom/MIDIManagerChild.h" +#include "mozilla/dom/MIDIPermissionRequest.h" +#include "mozilla/dom/FeaturePolicyUtils.h" +#include "mozilla/dom/Promise.h" +#include "nsIGlobalObject.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/Preferences.h" + +using namespace mozilla::ipc; + +namespace mozilla::dom { + +namespace { +// Singleton object for MIDIAccessManager +StaticRefPtr<MIDIAccessManager> gMIDIAccessManager; +} // namespace + +MIDIAccessManager::MIDIAccessManager() : mHasPortList(false), mChild(nullptr) {} + +MIDIAccessManager::~MIDIAccessManager() = default; + +// static +MIDIAccessManager* MIDIAccessManager::Get() { + if (!gMIDIAccessManager) { + gMIDIAccessManager = new MIDIAccessManager(); + ClearOnShutdown(&gMIDIAccessManager); + } + return gMIDIAccessManager; +} + +// static +bool MIDIAccessManager::IsRunning() { return !!gMIDIAccessManager; } + +already_AddRefed<Promise> MIDIAccessManager::RequestMIDIAccess( + nsPIDOMWindowInner* aWindow, const MIDIOptions& aOptions, + ErrorResult& aRv) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aWindow); + nsCOMPtr<nsIGlobalObject> go = do_QueryInterface(aWindow); + RefPtr<Promise> p = Promise::Create(go, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + nsCOMPtr<Document> doc = aWindow->GetDoc(); + if (NS_WARN_IF(!doc)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + if (!FeaturePolicyUtils::IsFeatureAllowed(doc, u"midi"_ns)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + nsCOMPtr<nsIRunnable> permRunnable = + new MIDIPermissionRequest(aWindow, p, aOptions); + aRv = NS_DispatchToMainThread(permRunnable); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + return p.forget(); +} + +bool MIDIAccessManager::AddObserver(Observer<MIDIPortList>* aObserver) { + // Add observer before we start the service, otherwise we can end up with + // device lists being received before we have observers to send them to. + mChangeObservers.AddObserver(aObserver); + // If we don't currently have a port list, that means this is a new + // AccessManager and we possibly need to start the MIDI Service. + if (!mChild) { + // Otherwise we must begin the PBackground initialization process and + // wait for the async ActorCreated() callback. + MOZ_ASSERT(NS_IsMainThread()); + ::mozilla::ipc::PBackgroundChild* actor = + BackgroundChild::GetOrCreateForCurrentThread(); + if (NS_WARN_IF(!actor)) { + return false; + } + RefPtr<MIDIManagerChild> mgr(new MIDIManagerChild()); + PMIDIManagerChild* constructedMgr = actor->SendPMIDIManagerConstructor(mgr); + + if (NS_WARN_IF(!constructedMgr)) { + return false; + } + MOZ_ASSERT(constructedMgr == mgr); + mChild = std::move(mgr); + // Add a ref to mChild here, that will be deref'd by + // BackgroundChildImpl::DeallocPMIDIManagerChild on IPC cleanup. + mChild->SetActorAlive(); + } + return true; +} + +void MIDIAccessManager::RemoveObserver(Observer<MIDIPortList>* aObserver) { + mChangeObservers.RemoveObserver(aObserver); + if (mChangeObservers.Length() == 0) { + // If we're out of listeners, go ahead and shut down. Make sure to cleanup + // the IPDL protocol also. + if (mChild) { + mChild->Shutdown(); + mChild = nullptr; + } + gMIDIAccessManager = nullptr; + } +} + +void MIDIAccessManager::CreateMIDIAccess(nsPIDOMWindowInner* aWindow, + bool aNeedsSysex, Promise* aPromise) { + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aPromise); + RefPtr<MIDIAccess> a(new MIDIAccess(aWindow, aNeedsSysex, aPromise)); + if (NS_WARN_IF(!AddObserver(a))) { + aPromise->MaybeReject(NS_ERROR_FAILURE); + return; + } + if (!mHasPortList) { + // Hold the access object until we get a connected device list. + mAccessHolder.AppendElement(a); + } else { + // If we already have a port list, just send it to the MIDIAccess object now + // so it can prepopulate its device list and resolve the promise. + a->Notify(mPortList); + } +} + +void MIDIAccessManager::Update(const MIDIPortList& aPortList) { + mPortList = aPortList; + mChangeObservers.Broadcast(aPortList); + if (!mHasPortList) { + mHasPortList = true; + // Now that we've broadcast the already-connected port list, content + // should manage the lifetime of the MIDIAccess object, so we can clear the + // keep-alive array. + mAccessHolder.Clear(); + } +} + +} // namespace mozilla::dom diff --git a/dom/midi/MIDIAccessManager.h b/dom/midi/MIDIAccessManager.h new file mode 100644 index 0000000000..2ffb7e6f53 --- /dev/null +++ b/dom/midi/MIDIAccessManager.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIAccessManager_h +#define mozilla_dom_MIDIAccessManager_h + +#include "nsPIDOMWindow.h" +#include "mozilla/dom/MIDITypes.h" +#include "mozilla/Observer.h" + +namespace mozilla { +namespace dom { + +class MIDIAccess; +class MIDIManagerChild; +struct MIDIOptions; +class MIDIPortChangeEvent; +class MIDIPortInfo; +class Promise; + +/** + * MIDIAccessManager manages creation and lifetime of MIDIAccess objects for the + * process it lives in. It is in charge of dealing with permission requests, + * creating new MIDIAccess objects, and updating live MIDIAccess objects with + * new device listings. + * + * While a process/window can have many MIDIAccess objects, there is only one + * MIDIAccessManager for any one process. + */ +class MIDIAccessManager final { + public: + NS_INLINE_DECL_REFCOUNTING(MIDIAccessManager); + // Handles requests from Navigator for MIDI permissions and MIDIAccess + // creation. + already_AddRefed<Promise> RequestMIDIAccess(nsPIDOMWindowInner* aWindow, + const MIDIOptions& aOptions, + ErrorResult& aRv); + // Creates a new MIDIAccess object + void CreateMIDIAccess(nsPIDOMWindowInner* aWindow, bool aNeedsSysex, + Promise* aPromise); + // Getter for manager singleton + static MIDIAccessManager* Get(); + // True if manager singleton has been created + static bool IsRunning(); + // Send device connection updates to all known MIDIAccess objects. + void Update(const MIDIPortList& aEvent); + // Adds a device update observer (usually a MIDIAccess object) + bool AddObserver(Observer<MIDIPortList>* aObserver); + // Removes a device update observer (usually a MIDIAccess object) + void RemoveObserver(Observer<MIDIPortList>* aObserver); + + private: + MIDIAccessManager(); + ~MIDIAccessManager(); + // True if object has received a device list from the MIDI platform service. + bool mHasPortList; + // List of known ports for the system. + MIDIPortList mPortList; + // Holds MIDIAccess objects until we've received the first list of devices + // from the MIDI Service. + nsTArray<RefPtr<MIDIAccess>> mAccessHolder; + // Device state update observers (usually MIDIAccess objects) + ObserverList<MIDIPortList> mChangeObservers; + // IPC Object for MIDIManager. Created on first MIDIAccess object creation, + // destroyed on last MIDIAccess object destruction. + RefPtr<MIDIManagerChild> mChild; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MIDIAccessManager_h diff --git a/dom/midi/MIDIInput.cpp b/dom/midi/MIDIInput.cpp new file mode 100644 index 0000000000..5947f08f41 --- /dev/null +++ b/dom/midi/MIDIInput.cpp @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/MIDIInput.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/MIDIPortChild.h" +#include "mozilla/dom/MIDIInputBinding.h" +#include "mozilla/dom/MIDIMessageEvent.h" +#include "mozilla/dom/MIDIMessageEventBinding.h" +#include "nsDOMNavigationTiming.h" + +namespace mozilla::dom { + +MIDIInput::MIDIInput(nsPIDOMWindowInner* aWindow, MIDIAccess* aMIDIAccessParent) + : MIDIPort(aWindow, aMIDIAccessParent) {} + +// static +MIDIInput* MIDIInput::Create(nsPIDOMWindowInner* aWindow, + MIDIAccess* aMIDIAccessParent, + const MIDIPortInfo& aPortInfo, + const bool aSysexEnabled) { + MOZ_ASSERT(static_cast<MIDIPortType>(aPortInfo.type()) == + MIDIPortType::Input); + auto port = new MIDIInput(aWindow, aMIDIAccessParent); + if (!port->Initialize(aPortInfo, aSysexEnabled)) { + return nullptr; + } + return port; +} + +JSObject* MIDIInput::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return MIDIInput_Binding::Wrap(aCx, this, aGivenProto); +} + +void MIDIInput::Receive(const nsTArray<MIDIMessage>& aMsgs) { + nsCOMPtr<Document> doc = GetOwner() ? GetOwner()->GetDoc() : nullptr; + if (!doc) { + NS_WARNING("No document available to send MIDIMessageEvent to!"); + return; + } + for (auto& msg : aMsgs) { + RefPtr<MIDIMessageEvent> event( + MIDIMessageEvent::Constructor(this, msg.timestamp(), msg.data())); + DispatchTrustedEvent(event); + } +} + +EventHandlerNonNull* MIDIInput::GetOnmidimessage() { + return GetEventHandler(nsGkAtoms::onmidimessage); +} + +void MIDIInput::SetOnmidimessage(EventHandlerNonNull* aCallback) { + SetEventHandler(nsGkAtoms::onmidimessage, aCallback); + if (mPort->ConnectionState() != MIDIPortConnectionState::Open) { + mPort->SendOpen(); + } +} + +} // namespace mozilla::dom diff --git a/dom/midi/MIDIInput.h b/dom/midi/MIDIInput.h new file mode 100644 index 0000000000..df4247ffa8 --- /dev/null +++ b/dom/midi/MIDIInput.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIInput_h +#define mozilla_dom_MIDIInput_h + +#include "mozilla/dom/MIDIPort.h" + +struct JSContext; + +namespace mozilla { +namespace dom { + +class MIDIPortInfo; + +/** + * Represents a MIDI Input Port, handles generating incoming message events. + * + */ +class MIDIInput final : public MIDIPort { + public: + static MIDIInput* Create(nsPIDOMWindowInner* aWindow, + MIDIAccess* aMIDIAccessParent, + const MIDIPortInfo& aPortInfo, + const bool aSysexEnabled); + ~MIDIInput() = default; + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // Since we need to be able to open the port on event handler assignment, we + // can't use IMPL_EVENT_HANDLER. We have to implement the event handler + // functions ourselves. + + // Getter for the event handler callback + EventHandlerNonNull* GetOnmidimessage(); + // Setter for the event handler callback + void SetOnmidimessage(EventHandlerNonNull* aCallback); + + private: + MIDIInput(nsPIDOMWindowInner* aWindow, MIDIAccess* aMIDIAccessParent); + // Takes an array of IPC MIDIMessage objects and turns them into + // MIDIMessageEvents, which it then fires. + void Receive(const nsTArray<MIDIMessage>& aMsgs) override; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MIDIInput_h diff --git a/dom/midi/MIDIInputMap.cpp b/dom/midi/MIDIInputMap.cpp new file mode 100644 index 0000000000..aa4f57a7d3 --- /dev/null +++ b/dom/midi/MIDIInputMap.cpp @@ -0,0 +1,29 @@ +/* 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 "mozilla/dom/MIDIInputMap.h" +#include "mozilla/dom/MIDIInputMapBinding.h" +#include "nsPIDOMWindow.h" +#include "mozilla/dom/BindingUtils.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MIDIInputMap, mParent) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(MIDIInputMap) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MIDIInputMap) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MIDIInputMap) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +MIDIInputMap::MIDIInputMap(nsPIDOMWindowInner* aParent) : mParent(aParent) {} + +JSObject* MIDIInputMap::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return MIDIInputMap_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/midi/MIDIInputMap.h b/dom/midi/MIDIInputMap.h new file mode 100644 index 0000000000..ee9b749678 --- /dev/null +++ b/dom/midi/MIDIInputMap.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIInputMap_h +#define mozilla_dom_MIDIInputMap_h + +#include "nsCOMPtr.h" +#include "nsWrapperCache.h" + +class nsPIDOMWindowInner; + +namespace mozilla { +namespace dom { + +/** + * Maplike DOM object that holds a list of all MIDI input ports available for + * access. Almost all functions are implemented automatically by WebIDL. + */ +class MIDIInputMap final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(MIDIInputMap) + nsPIDOMWindowInner* GetParentObject() const { return mParent; } + + explicit MIDIInputMap(nsPIDOMWindowInner* aParent); + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + private: + ~MIDIInputMap() = default; + nsCOMPtr<nsPIDOMWindowInner> mParent; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MIDIInputMap_h diff --git a/dom/midi/MIDIManagerChild.cpp b/dom/midi/MIDIManagerChild.cpp new file mode 100644 index 0000000000..258a16bae3 --- /dev/null +++ b/dom/midi/MIDIManagerChild.cpp @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "MIDIManagerChild.h" +#include "mozilla/dom/MIDIAccessManager.h" + +using namespace mozilla::dom; + +MIDIManagerChild::MIDIManagerChild() : mShutdown(false) {} + +mozilla::ipc::IPCResult MIDIManagerChild::RecvMIDIPortListUpdate( + const MIDIPortList& aPortList) { + MOZ_ASSERT(NS_IsMainThread()); + if (mShutdown) { + return IPC_OK(); + } + MOZ_ASSERT(MIDIAccessManager::IsRunning()); + MIDIAccessManager::Get()->Update(aPortList); + return IPC_OK(); +} + +void MIDIManagerChild::SetActorAlive() { + // IPC Channels for MIDIManagers are created and managed by MIDIAccessManager, + // so once the actor is created, we'll need to add a reference to keep it + // alive until BackgroundChildImpl kills it. + AddRef(); +} + +void MIDIManagerChild::Shutdown() { + MOZ_ASSERT(!mShutdown); + mShutdown = true; + SendShutdown(); +} diff --git a/dom/midi/MIDIManagerChild.h b/dom/midi/MIDIManagerChild.h new file mode 100644 index 0000000000..eaf6f99d02 --- /dev/null +++ b/dom/midi/MIDIManagerChild.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIManagerChild_h +#define mozilla_dom_MIDIManagerChild_h + +#include "mozilla/dom/PMIDIManagerChild.h" + +namespace mozilla { +namespace dom { + +/** + * Actor implementation for the Child side of MIDIManager (represented in DOM by + * MIDIAccess). Manages actor lifetime so that we know to shut down services + * when all MIDIManagers are gone. Also receives port list update on MIDIAccess + * object creation. + * + */ +class MIDIManagerChild final : public PMIDIManagerChild { + public: + NS_INLINE_DECL_REFCOUNTING(MIDIManagerChild) + + MIDIManagerChild(); + mozilla::ipc::IPCResult RecvMIDIPortListUpdate(const MIDIPortList& aPortList); + void SetActorAlive(); + void Shutdown(); + + private: + ~MIDIManagerChild() = default; + bool mShutdown; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MIDIManagerChild_h diff --git a/dom/midi/MIDIManagerParent.cpp b/dom/midi/MIDIManagerParent.cpp new file mode 100644 index 0000000000..a0190ff643 --- /dev/null +++ b/dom/midi/MIDIManagerParent.cpp @@ -0,0 +1,26 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/MIDIManagerParent.h" +#include "mozilla/dom/MIDIPlatformService.h" + +namespace mozilla::dom { + +void MIDIManagerParent::ActorDestroy(ActorDestroyReason aWhy) {} + +void MIDIManagerParent::Teardown() { + if (MIDIPlatformService::IsRunning()) { + MIDIPlatformService::Get()->RemoveManager(this); + } +} + +mozilla::ipc::IPCResult MIDIManagerParent::RecvShutdown() { + Teardown(); + Unused << Send__delete__(this); + return IPC_OK(); +} + +} // namespace mozilla::dom diff --git a/dom/midi/MIDIManagerParent.h b/dom/midi/MIDIManagerParent.h new file mode 100644 index 0000000000..6714ed6ab4 --- /dev/null +++ b/dom/midi/MIDIManagerParent.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIManagerParent_h +#define mozilla_dom_MIDIManagerParent_h + +#include "mozilla/dom/PMIDIManagerParent.h" + +namespace mozilla { +namespace dom { + +/** + * Actor implementation for the Parent (PBackground thread) side of MIDIManager + * (represented in DOM by MIDIAccess). Manages actor lifetime so that we know + * to shut down services when all MIDIManagers are gone. + * + */ +class MIDIManagerParent final : public PMIDIManagerParent { + public: + NS_INLINE_DECL_REFCOUNTING(MIDIManagerParent); + MIDIManagerParent() = default; + mozilla::ipc::IPCResult RecvShutdown(); + void Teardown(); + void ActorDestroy(ActorDestroyReason aWhy) override; + + private: + ~MIDIManagerParent() = default; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MIDIManagerParent_h diff --git a/dom/midi/MIDIMessageEvent.cpp b/dom/midi/MIDIMessageEvent.cpp new file mode 100644 index 0000000000..b06b852dc5 --- /dev/null +++ b/dom/midi/MIDIMessageEvent.cpp @@ -0,0 +1,106 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/EventBinding.h" +#include "mozilla/dom/MIDIMessageEvent.h" +#include "mozilla/dom/MIDIMessageEventBinding.h" +#include "js/GCAPI.h" +#include "jsfriendapi.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/PrimitiveConversions.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/dom/Performance.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(MIDIMessageEvent) + +NS_IMPL_ADDREF_INHERITED(MIDIMessageEvent, Event) +NS_IMPL_RELEASE_INHERITED(MIDIMessageEvent, Event) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MIDIMessageEvent, Event) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MIDIMessageEvent, Event) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mData) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MIDIMessageEvent, Event) + tmp->mData = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MIDIMessageEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +MIDIMessageEvent::MIDIMessageEvent(mozilla::dom::EventTarget* aOwner) + : Event(aOwner, nullptr, nullptr) { + mozilla::HoldJSObjects(this); +} + +MIDIMessageEvent::~MIDIMessageEvent() { + mData = nullptr; + mozilla::DropJSObjects(this); +} + +JSObject* MIDIMessageEvent::WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return MIDIMessageEvent_Binding::Wrap(aCx, this, aGivenProto); +} + +MIDIMessageEvent* MIDIMessageEvent::AsMIDIMessageEvent() { return this; } + +already_AddRefed<MIDIMessageEvent> MIDIMessageEvent::Constructor( + EventTarget* aOwner, const class TimeStamp& aReceivedTime, + const nsTArray<uint8_t>& aData) { + MOZ_ASSERT(aOwner); + RefPtr<MIDIMessageEvent> e = new MIDIMessageEvent(aOwner); + e->InitEvent(u"midimessage"_ns, false, false); + e->mEvent->mTimeStamp = aReceivedTime; + e->mRawData = aData.Clone(); + e->SetTrusted(true); + return e.forget(); +} + +already_AddRefed<MIDIMessageEvent> MIDIMessageEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const MIDIMessageEventInit& aEventInitDict, ErrorResult& aRv) { + nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<MIDIMessageEvent> e = new MIDIMessageEvent(owner); + bool trusted = e->Init(owner); + e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable); + // Set data for event. Timestamp will always be set to Now() (default for + // event) using this constructor. + const auto& a = aEventInitDict.mData.Value(); + a.ComputeState(); + e->mData = Uint8Array::Create(aGlobal.Context(), owner, a.Length(), a.Data()); + if (NS_WARN_IF(!e->mData)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + e->SetTrusted(trusted); + mozilla::HoldJSObjects(e.get()); + return e.forget(); +} + +void MIDIMessageEvent::GetData(JSContext* cx, + JS::MutableHandle<JSObject*> aData, + ErrorResult& aRv) { + if (!mData) { + mData = + Uint8Array::Create(cx, this, mRawData.Length(), mRawData.Elements()); + if (!mData) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + mRawData.Clear(); + } + aData.set(mData); +} + +} // namespace mozilla::dom diff --git a/dom/midi/MIDIMessageEvent.h b/dom/midi/MIDIMessageEvent.h new file mode 100644 index 0000000000..6422f8872a --- /dev/null +++ b/dom/midi/MIDIMessageEvent.h @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIMessageEvent_h +#define mozilla_dom_MIDIMessageEvent_h + +#include <cstdint> +#include "js/RootingAPI.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/Event.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsTArray.h" + +struct JSContext; +namespace mozilla { +namespace dom { +struct MIDIMessageEventInit; + +/** + * Event that fires whenever a MIDI message is received by the MIDIInput object. + * + */ +class MIDIMessageEvent final : public Event { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(MIDIMessageEvent, + Event) + protected: + explicit MIDIMessageEvent(mozilla::dom::EventTarget* aOwner); + + JS::Heap<JSObject*> mData; + + public: + virtual MIDIMessageEvent* AsMIDIMessageEvent(); + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + static already_AddRefed<MIDIMessageEvent> Constructor( + EventTarget* aOwner, const class TimeStamp& aReceivedTime, + const nsTArray<uint8_t>& aData); + + static already_AddRefed<MIDIMessageEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const MIDIMessageEventInit& aEventInitDict, ErrorResult& aRv); + + // Getter for message data + void GetData(JSContext* cx, JS::MutableHandle<JSObject*> aData, + ErrorResult& aRv); + + private: + ~MIDIMessageEvent(); + nsTArray<uint8_t> mRawData; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MIDIMessageEvent_h diff --git a/dom/midi/MIDIMessageQueue.cpp b/dom/midi/MIDIMessageQueue.cpp new file mode 100644 index 0000000000..3006b225c0 --- /dev/null +++ b/dom/midi/MIDIMessageQueue.cpp @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "MIDIMessageQueue.h" +#include "mozilla/dom/MIDITypes.h" + +namespace mozilla::dom { + +MIDIMessageQueue::MIDIMessageQueue() : mMutex("MIDIMessageQueue::mMutex") {} + +class MIDIMessageTimestampComparator { + public: + bool Equals(const MIDIMessage& a, const MIDIMessage& b) const { + return a.timestamp() == b.timestamp(); + } + bool LessThan(const MIDIMessage& a, const MIDIMessage& b) const { + return a.timestamp() < b.timestamp(); + } +}; + +void MIDIMessageQueue::Add(nsTArray<MIDIMessage>& aMsg) { + MutexAutoLock lock(mMutex); + for (auto msg : aMsg) { + mMessageQueue.InsertElementSorted(msg, MIDIMessageTimestampComparator()); + } +} + +void MIDIMessageQueue::GetMessagesBefore(TimeStamp aTimestamp, + nsTArray<MIDIMessage>& aMsgQueue) { + MutexAutoLock lock(mMutex); + int i = 0; + for (auto msg : mMessageQueue) { + if (aTimestamp < msg.timestamp()) { + break; + } + aMsgQueue.AppendElement(msg); + i++; + } + if (i > 0) { + mMessageQueue.RemoveElementsAt(0, i); + } +} + +void MIDIMessageQueue::GetMessages(nsTArray<MIDIMessage>& aMsgQueue) { + MutexAutoLock lock(mMutex); + aMsgQueue.AppendElements(mMessageQueue); + mMessageQueue.Clear(); +} + +void MIDIMessageQueue::Clear() { + MutexAutoLock lock(mMutex); + mMessageQueue.Clear(); +} + +void MIDIMessageQueue::ClearAfterNow() { + MutexAutoLock lock(mMutex); + TimeStamp now = TimeStamp::Now(); + int i = 0; + for (auto msg : mMessageQueue) { + if (now < msg.timestamp()) { + break; + } + i++; + } + if (i > 0) { + mMessageQueue.RemoveElementsAt(0, i); + } +} + +} // namespace mozilla::dom diff --git a/dom/midi/MIDIMessageQueue.h b/dom/midi/MIDIMessageQueue.h new file mode 100644 index 0000000000..edee2fd19b --- /dev/null +++ b/dom/midi/MIDIMessageQueue.h @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIMessageQueue_h +#define mozilla_dom_MIDIMessageQueue_h + +#include "mozilla/Mutex.h" +#include "nsTArray.h" + +// XXX Avoid including this here by moving function implementations to the cpp +// file. +#include "mozilla/dom/MIDITypes.h" + +namespace mozilla { + +class TimeStamp; + +namespace dom { + +class MIDIMessage; + +/** + * Since some MIDI Messages can be scheduled to be sent in the future, the + * MIDIMessageQueue is responsible for making sure all MIDI messages are + * scheduled and sent in order. + */ +class MIDIMessageQueue { + public: + MIDIMessageQueue(); + ~MIDIMessageQueue() = default; + // Adds an array of possibly out-of-order messages to our queue. + void Add(nsTArray<MIDIMessage>& aMsg); + // Retrieve all pending messages before the time specified. + void GetMessagesBefore(TimeStamp aTimestamp, + nsTArray<MIDIMessage>& aMsgArray); + // Get all pending messages. + void GetMessages(nsTArray<MIDIMessage>& aMsgArray); + // Erase all pending messages. + void Clear(); + // Erase all pending messages that would be sent in the future. Useful for + // when ports are closed, as we must send all messages with timestamps in the + // past. + void ClearAfterNow(); + + private: + // Array of messages to be sent. + nsTArray<MIDIMessage> mMessageQueue; + // Mutex for coordinating cross thread array access. + Mutex mMutex; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MIDIMessageQueue_h diff --git a/dom/midi/MIDIOutput.cpp b/dom/midi/MIDIOutput.cpp new file mode 100644 index 0000000000..c7148e174c --- /dev/null +++ b/dom/midi/MIDIOutput.cpp @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/MIDIOutput.h" +#include "mozilla/dom/MIDIPortChild.h" +#include "mozilla/dom/MIDITypes.h" +#include "mozilla/dom/MIDIOutputBinding.h" +#include "mozilla/dom/MIDIUtils.h" +#include "nsDOMNavigationTiming.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Performance.h" + +using namespace mozilla; +using namespace mozilla::dom; + +MIDIOutput::MIDIOutput(nsPIDOMWindowInner* aWindow, + MIDIAccess* aMIDIAccessParent) + : MIDIPort(aWindow, aMIDIAccessParent) {} + +// static +MIDIOutput* MIDIOutput::Create(nsPIDOMWindowInner* aWindow, + MIDIAccess* aMIDIAccessParent, + const MIDIPortInfo& aPortInfo, + const bool aSysexEnabled) { + MOZ_ASSERT(static_cast<MIDIPortType>(aPortInfo.type()) == + MIDIPortType::Output); + auto port = new MIDIOutput(aWindow, aMIDIAccessParent); + if (NS_WARN_IF(!port->Initialize(aPortInfo, aSysexEnabled))) { + return nullptr; + } + return port; +} + +JSObject* MIDIOutput::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return MIDIOutput_Binding::Wrap(aCx, this, aGivenProto); +} + +void MIDIOutput::Send(const Sequence<uint8_t>& aData, + const Optional<double>& aTimestamp, ErrorResult& aRv) { + if (mPort->DeviceState() == MIDIPortDeviceState::Disconnected) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + // The timestamp passed to us is a DOMHighResTimestamp, which is in relation + // to the start of navigation timing. This needs to be turned into a + // TimeStamp before it hits the platform specific MIDI service. + // + // If timestamp is either not set or zero, set timestamp to now and send the + // message ASAP. + TimeStamp timestamp; + if (aTimestamp.WasPassed() && aTimestamp.Value() != 0) { + nsCOMPtr<Document> doc = GetOwner()->GetDoc(); + if (!doc) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + TimeDuration ts_diff = TimeDuration::FromMilliseconds(aTimestamp.Value()); + timestamp = GetOwner() + ->GetPerformance() + ->GetDOMTiming() + ->GetNavigationStartTimeStamp() + + ts_diff; + } else { + timestamp = TimeStamp::Now(); + } + + nsTArray<MIDIMessage> msgArray; + MIDIUtils::ParseMessages(aData, timestamp, msgArray); + // Our translation of the spec is that invalid messages in a multi-message + // sequence will be thrown out, but that valid messages will still be used. + if (msgArray.IsEmpty()) { + aRv.ThrowTypeError("Empty message array"); + return; + } + + // TODO Move this check back to parse message so we don't have to iterate + // twice. + if (!SysexEnabled()) { + for (auto& msg : msgArray) { + if (MIDIUtils::IsSysexMessage(msg)) { + aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR); + return; + } + } + } + mPort->SendSend(msgArray); +} + +void MIDIOutput::Clear() { + if (mPort->ConnectionState() == MIDIPortConnectionState::Closed) { + return; + } + mPort->SendClear(); +} diff --git a/dom/midi/MIDIOutput.h b/dom/midi/MIDIOutput.h new file mode 100644 index 0000000000..72d0efb55e --- /dev/null +++ b/dom/midi/MIDIOutput.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIOutput_h +#define mozilla_dom_MIDIOutput_h + +#include "mozilla/dom/MIDIPort.h" +#include "mozilla/Attributes.h" +#include "nsWrapperCache.h" + +struct JSContext; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class MIDIPortInfo; +class MIDIMessage; + +/** + * Represents a MIDI Output Port, handles sending message to devices. + * + */ +class MIDIOutput final : public MIDIPort { + public: + static MIDIOutput* Create(nsPIDOMWindowInner* aWindow, + MIDIAccess* aMIDIAccessParent, + const MIDIPortInfo& aPortInfo, + const bool aSysexEnabled); + ~MIDIOutput() = default; + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // Send a message to an output port + void Send(const Sequence<uint8_t>& aData, const Optional<double>& aTimestamp, + ErrorResult& aRv); + // Clear any partially sent messages from the send queue + void Clear(); + + private: + MIDIOutput(nsPIDOMWindowInner* aWindow, MIDIAccess* aMIDIAccessParent); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MIDIOutput_h diff --git a/dom/midi/MIDIOutputMap.cpp b/dom/midi/MIDIOutputMap.cpp new file mode 100644 index 0000000000..d7840827f2 --- /dev/null +++ b/dom/midi/MIDIOutputMap.cpp @@ -0,0 +1,29 @@ +/* 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 "mozilla/dom/MIDIOutputMap.h" +#include "mozilla/dom/MIDIOutputMapBinding.h" +#include "nsPIDOMWindow.h" +#include "mozilla/dom/BindingUtils.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MIDIOutputMap, mParent) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(MIDIOutputMap) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MIDIOutputMap) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MIDIOutputMap) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +MIDIOutputMap::MIDIOutputMap(nsPIDOMWindowInner* aParent) : mParent(aParent) {} + +JSObject* MIDIOutputMap::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return MIDIOutputMap_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/midi/MIDIOutputMap.h b/dom/midi/MIDIOutputMap.h new file mode 100644 index 0000000000..16c8957669 --- /dev/null +++ b/dom/midi/MIDIOutputMap.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIOutputMap_h +#define mozilla_dom_MIDIOutputMap_h + +#include "nsCOMPtr.h" +#include "nsWrapperCache.h" + +class nsPIDOMWindowInner; + +namespace mozilla { +namespace dom { + +/** + * Maplike DOM object that holds a list of all MIDI output ports available for + * access. Almost all functions are implemented automatically by WebIDL. + * + */ +class MIDIOutputMap final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(MIDIOutputMap) + + explicit MIDIOutputMap(nsPIDOMWindowInner* aParent); + + nsPIDOMWindowInner* GetParentObject() const { return mParent; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + private: + ~MIDIOutputMap() = default; + nsCOMPtr<nsPIDOMWindowInner> mParent; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MIDIOutputMap_h diff --git a/dom/midi/MIDIPermissionRequest.cpp b/dom/midi/MIDIPermissionRequest.cpp new file mode 100644 index 0000000000..a26c730aff --- /dev/null +++ b/dom/midi/MIDIPermissionRequest.cpp @@ -0,0 +1,98 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/MIDIPermissionRequest.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/MIDIAccessManager.h" +#include "mozilla/dom/MIDIOptionsBinding.h" +#include "nsIGlobalObject.h" +#include "mozilla/Preferences.h" +#include "nsContentUtils.h" + +//------------------------------------------------- +// MIDI Permission Requests +//------------------------------------------------- + +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(MIDIPermissionRequest, + ContentPermissionRequestBase, mPromise) + +NS_IMPL_QUERY_INTERFACE_CYCLE_COLLECTION_INHERITED(MIDIPermissionRequest, + ContentPermissionRequestBase, + nsIRunnable) + +NS_IMPL_ADDREF_INHERITED(MIDIPermissionRequest, ContentPermissionRequestBase) +NS_IMPL_RELEASE_INHERITED(MIDIPermissionRequest, ContentPermissionRequestBase) + +MIDIPermissionRequest::MIDIPermissionRequest(nsPIDOMWindowInner* aWindow, + Promise* aPromise, + const MIDIOptions& aOptions) + : ContentPermissionRequestBase( + aWindow->GetDoc()->NodePrincipal(), aWindow, + ""_ns, // We check prefs in a custom way here + "midi"_ns), + mPromise(aPromise), + mNeedsSysex(aOptions.mSysex) { + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aPromise, "aPromise should not be null!"); + MOZ_ASSERT(aWindow->GetDoc()); + mPrincipal = aWindow->GetDoc()->NodePrincipal(); + MOZ_ASSERT(mPrincipal); +} + +NS_IMETHODIMP +MIDIPermissionRequest::GetTypes(nsIArray** aTypes) { + NS_ENSURE_ARG_POINTER(aTypes); + nsTArray<nsString> options; + if (mNeedsSysex) { + options.AppendElement(u"sysex"_ns); + } + return nsContentPermissionUtils::CreatePermissionArray(mType, options, + aTypes); +} + +NS_IMETHODIMP +MIDIPermissionRequest::Cancel() { + mPromise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR); + return NS_OK; +} + +NS_IMETHODIMP +MIDIPermissionRequest::Allow(JS::HandleValue aChoices) { + MOZ_ASSERT(aChoices.isUndefined()); + MIDIAccessManager* mgr = MIDIAccessManager::Get(); + mgr->CreateMIDIAccess(mWindow, mNeedsSysex, mPromise); + return NS_OK; +} + +NS_IMETHODIMP +MIDIPermissionRequest::Run() { + // If the testing flag is true, skip dialog + if (Preferences::GetBool("midi.prompt.testing", false)) { + bool allow = + Preferences::GetBool("media.navigator.permission.disabled", false); + if (allow) { + Allow(JS::UndefinedHandleValue); + } else { + Cancel(); + } + return NS_OK; + } + + // If we already have sysex perms, allow. + if (nsContentUtils::IsExactSitePermAllow(mPrincipal, "midi-sysex"_ns)) { + Allow(JS::UndefinedHandleValue); + return NS_OK; + } + + // If we have no perms, or only have midi and are asking for sysex, pop dialog + if (NS_FAILED(nsContentPermissionUtils::AskPermission(this, mWindow))) { + Cancel(); + return NS_ERROR_FAILURE; + } + return NS_OK; +} diff --git a/dom/midi/MIDIPermissionRequest.h b/dom/midi/MIDIPermissionRequest.h new file mode 100644 index 0000000000..558f14bcf0 --- /dev/null +++ b/dom/midi/MIDIPermissionRequest.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIPermissionRequest_h +#define mozilla_dom_MIDIPermissionRequest_h + +#include "mozilla/dom/Promise.h" +#include "nsContentPermissionHelper.h" + +namespace mozilla { +namespace dom { + +struct MIDIOptions; + +/** + * Handles permission dialog management when requesting MIDI permissions. + */ +class MIDIPermissionRequest final : public ContentPermissionRequestBase, + public nsIRunnable { + public: + MIDIPermissionRequest(nsPIDOMWindowInner* aWindow, Promise* aPromise, + const MIDIOptions& aOptions); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLE + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MIDIPermissionRequest, + ContentPermissionRequestBase) + // nsIContentPermissionRequest + NS_IMETHOD Cancel(void) override; + NS_IMETHOD Allow(JS::HandleValue choices) override; + NS_IMETHOD GetTypes(nsIArray** aTypes) override; + + private: + ~MIDIPermissionRequest() = default; + + // Promise for returning MIDIAccess on request success + RefPtr<Promise> mPromise; + // True if sysex permissions should be requested + bool mNeedsSysex; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MIDIPermissionRequest_h diff --git a/dom/midi/MIDIPlatformRunnables.cpp b/dom/midi/MIDIPlatformRunnables.cpp new file mode 100644 index 0000000000..debc555e74 --- /dev/null +++ b/dom/midi/MIDIPlatformRunnables.cpp @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/MIDIPlatformRunnables.h" +#include "mozilla/dom/MIDIPlatformService.h" +#include "mozilla/dom/MIDIPortParent.h" +#include "mozilla/ipc/BackgroundParent.h" + +namespace mozilla::dom { + +NS_IMETHODIMP +MIDIBackgroundRunnable::Run() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + if (!MIDIPlatformService::IsRunning()) { + return NS_OK; + } + RunInternal(); + return NS_OK; +} + +void ReceiveRunnable::RunInternal() { + MIDIPlatformService::Get()->CheckAndReceive(mPortId, mMsgs); +} + +void AddPortRunnable::RunInternal() { + MIDIPlatformService::Get()->AddPortInfo(mPortInfo); +} + +void RemovePortRunnable::RunInternal() { + MIDIPlatformService::Get()->RemovePortInfo(mPortInfo); +} + +void SetStatusRunnable::RunInternal() { + MIDIPlatformService::Get()->UpdateStatus(mPortId, mState, mConnection); +} + +void SendPortListRunnable::RunInternal() { + // Unlike other runnables, SendPortListRunnable should just exit quietly if + // the service has died. + if (!MIDIPlatformService::IsRunning()) { + return; + } + MIDIPlatformService::Get()->SendPortList(); +} + +} // namespace mozilla::dom diff --git a/dom/midi/MIDIPlatformRunnables.h b/dom/midi/MIDIPlatformRunnables.h new file mode 100644 index 0000000000..0a5e1d0583 --- /dev/null +++ b/dom/midi/MIDIPlatformRunnables.h @@ -0,0 +1,127 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIPlatformRunnables_h +#define mozilla_dom_MIDIPlatformRunnables_h + +#include "mozilla/dom/MIDITypes.h" + +namespace mozilla { +namespace dom { + +enum class MIDIPortConnectionState : uint8_t; +enum class MIDIPortDeviceState : uint8_t; + +class MIDIPortParent; +class MIDIMessage; +class MIDIPortInfo; + +/** + * Base class for runnables to be fired to the platform-specific MIDI service + * thread in PBackground. + */ +class MIDIBackgroundRunnable : public Runnable { + public: + MIDIBackgroundRunnable(const char* aName) : Runnable(aName) {} + virtual ~MIDIBackgroundRunnable() = default; + NS_IMETHOD Run() override; + virtual void RunInternal() = 0; +}; + +/** + * Runnable fired from platform-specific MIDI service thread to PBackground + * Thread whenever messages need to be sent to a MIDI device. + * + */ +class ReceiveRunnable final : public MIDIBackgroundRunnable { + public: + ReceiveRunnable(const nsAString& aPortId, const nsTArray<MIDIMessage>& aMsgs) + : MIDIBackgroundRunnable("ReceiveRunnable"), + mMsgs(aMsgs.Clone()), + mPortId(aPortId) {} + // Used in tests + ReceiveRunnable(const nsAString& aPortId, const MIDIMessage& aMsgs) + : MIDIBackgroundRunnable("ReceiveRunnable"), mPortId(aPortId) { + mMsgs.AppendElement(aMsgs); + } + ~ReceiveRunnable() = default; + void RunInternal() override; + + private: + nsTArray<MIDIMessage> mMsgs; + nsString mPortId; +}; + +/** + * Runnable fired from platform-specific MIDI service thread to PBackground + * Thread whenever a device is connected. + * + */ +class AddPortRunnable final : public MIDIBackgroundRunnable { + public: + explicit AddPortRunnable(const MIDIPortInfo& aPortInfo) + : MIDIBackgroundRunnable("AddPortRunnable"), mPortInfo(aPortInfo) {} + ~AddPortRunnable() = default; + void RunInternal() override; + + private: + MIDIPortInfo mPortInfo; +}; + +/** + * Runnable fired from platform-specific MIDI service thread to PBackground + * Thread whenever a device is disconnected. + * + */ +class RemovePortRunnable final : public MIDIBackgroundRunnable { + public: + explicit RemovePortRunnable(const MIDIPortInfo& aPortInfo) + : MIDIBackgroundRunnable("RemovePortRunnable"), mPortInfo(aPortInfo) {} + ~RemovePortRunnable() = default; + void RunInternal() override; + + private: + MIDIPortInfo mPortInfo; +}; + +/** + * Runnable used to delay calls to SendPortList, which is requires to make sure + * MIDIManager actor initialization happens correctly. Also used for testing. + * + */ +class SendPortListRunnable final : public MIDIBackgroundRunnable { + public: + SendPortListRunnable() : MIDIBackgroundRunnable("SendPortListRunnable") {} + ~SendPortListRunnable() = default; + void RunInternal() override; +}; + +/** + * Runnable fired from platform-specific MIDI service thread to PBackground + * Thread whenever a device is disconnected. + * + */ +class SetStatusRunnable final : public MIDIBackgroundRunnable { + public: + SetStatusRunnable(const nsAString& aPortId, MIDIPortDeviceState aState, + MIDIPortConnectionState aConnection) + : MIDIBackgroundRunnable("SetStatusRunnable"), + mPortId(aPortId), + mState(aState), + mConnection(aConnection) {} + ~SetStatusRunnable() = default; + void RunInternal() override; + + private: + nsString mPortId; + MIDIPortDeviceState mState; + MIDIPortConnectionState mConnection; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MIDIPlatformRunnables_h diff --git a/dom/midi/MIDIPlatformService.cpp b/dom/midi/MIDIPlatformService.cpp new file mode 100644 index 0000000000..940cdf8541 --- /dev/null +++ b/dom/midi/MIDIPlatformService.cpp @@ -0,0 +1,261 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "MIDIPlatformService.h" +#include "MIDIMessageQueue.h" +#include "TestMIDIPlatformService.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/MIDIManagerParent.h" +#include "mozilla/dom/MIDIPlatformRunnables.h" +#include "mozilla/dom/MIDIUtils.h" +#include "mozilla/dom/PMIDIManagerParent.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/dom/MIDIPortParent.h" + +using namespace mozilla; +using namespace mozilla::dom; + +MIDIPlatformService::MIDIPlatformService() + : mHasSentPortList(false), + mMessageQueueMutex("MIDIPlatformServce::mMessageQueueMutex") {} + +MIDIPlatformService::~MIDIPlatformService() = default; + +void MIDIPlatformService::CheckAndReceive(const nsAString& aPortId, + const nsTArray<MIDIMessage>& aMsgs) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + for (auto& port : mPorts) { + // TODO Clean this up when we split input/output port arrays + if (port->MIDIPortInterface::Id() != aPortId || + port->Type() != MIDIPortType::Input || + port->ConnectionState() != MIDIPortConnectionState::Open) { + continue; + } + if (!port->SysexEnabled()) { + nsTArray<MIDIMessage> msgs; + for (auto& msg : aMsgs) { + if (!MIDIUtils::IsSysexMessage(msg)) { + msgs.AppendElement(msg); + } + } + Unused << port->SendReceive(msgs); + } else { + Unused << port->SendReceive(aMsgs); + } + } +} + +void MIDIPlatformService::AddPort(MIDIPortParent* aPort) { + MOZ_ASSERT(aPort); + ::mozilla::ipc::AssertIsOnBackgroundThread(); + mPorts.AppendElement(aPort); +} + +void MIDIPlatformService::RemovePort(MIDIPortParent* aPort) { + // This should only be called from the background thread, when a MIDIPort + // actor has been destroyed. + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPort); + mPorts.RemoveElement(aPort); + MaybeStop(); +} + +void MIDIPlatformService::BroadcastState(const MIDIPortInfo& aPortInfo, + const MIDIPortDeviceState& aState) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + for (auto& p : mPorts) { + if (p->MIDIPortInterface::Id() == aPortInfo.id() && + p->DeviceState() != aState) { + p->SendUpdateStatus(aState, p->ConnectionState()); + } + } +} + +void MIDIPlatformService::QueueMessages(const nsAString& aId, + nsTArray<MIDIMessage>& aMsgs) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + { + MutexAutoLock lock(mMessageQueueMutex); + MIDIMessageQueue* msgQueue = mMessageQueues.LookupOrAdd(aId); + msgQueue->Add(aMsgs); + ScheduleSend(aId); + } +} + +void MIDIPlatformService::SendPortList() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + mHasSentPortList = true; + MIDIPortList l; + for (auto& el : mPortInfo) { + l.ports().AppendElement(el); + } + for (auto& mgr : mManagers) { + Unused << mgr->SendMIDIPortListUpdate(l); + } +} + +void MIDIPlatformService::Clear(MIDIPortParent* aPort) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(aPort); + { + MutexAutoLock lock(mMessageQueueMutex); + MIDIMessageQueue* msgQueue = + mMessageQueues.Get(aPort->MIDIPortInterface::Id()); + if (msgQueue) { + msgQueue->Clear(); + } + } +} + +void MIDIPlatformService::AddPortInfo(MIDIPortInfo& aPortInfo) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(XRE_IsParentProcess()); + + mPortInfo.AppendElement(aPortInfo); + + // ORDER MATTERS HERE. + // + // When MIDI hardware is disconnected, all open MIDIPort objects revert to a + // "pending" state, and they are removed from the port maps of MIDIAccess + // objects. We need to send connection updates to all living ports first, THEN + // we can send port list updates to all of the live MIDIAccess objects. We + // have to go in this order because if a port object is still held live but is + // disconnected, it needs to readd itself to its originating MIDIAccess + // object. Running SendPortList first would cause MIDIAccess to create a new + // MIDIPort object, which would conflict (i.e. old disconnected object != new + // object in port map, which is against spec). + for (auto& port : mPorts) { + if (port->MIDIPortInterface::Id() == aPortInfo.id()) { + port->SendUpdateStatus(MIDIPortDeviceState::Connected, + port->ConnectionState()); + } + } + if (mHasSentPortList) { + SendPortList(); + } +} + +void MIDIPlatformService::RemovePortInfo(MIDIPortInfo& aPortInfo) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + mPortInfo.RemoveElement(aPortInfo); + BroadcastState(aPortInfo, MIDIPortDeviceState::Disconnected); + if (mHasSentPortList) { + SendPortList(); + } +} + +StaticRefPtr<MIDIPlatformService> gMIDIPlatformService; + +// static +bool MIDIPlatformService::IsRunning() { + return gMIDIPlatformService != nullptr; +} + +void MIDIPlatformService::Close(mozilla::dom::MIDIPortParent* aPort) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + { + MutexAutoLock lock(mMessageQueueMutex); + MIDIMessageQueue* msgQueue = + mMessageQueues.Get(aPort->MIDIPortInterface::Id()); + if (msgQueue) { + msgQueue->ClearAfterNow(); + } + } + // Send all messages before sending a close request + ScheduleSend(aPort->MIDIPortInterface::Id()); + // TODO We should probably have the send function schedule closing + ScheduleClose(aPort); +} + +// static +MIDIPlatformService* MIDIPlatformService::Get() { + // We should never touch the platform service in a child process. + MOZ_ASSERT(XRE_IsParentProcess()); + ::mozilla::ipc::AssertIsOnBackgroundThread(); + if (!IsRunning()) { + ErrorResult rv; + // Uncomment once we have an actual platform library to test. + // + // bool useTestService = false; + // rv = Preferences::GetRootBranch()->GetBoolPref("midi.testing", + // &useTestService); + gMIDIPlatformService = new TestMIDIPlatformService(); + gMIDIPlatformService->Init(); + } + return gMIDIPlatformService; +} + +void MIDIPlatformService::MaybeStop() { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + if (!IsRunning()) { + // Service already stopped or never started. Exit. + return; + } + // If we have any ports or managers left, we should still be alive. + if (!mPorts.IsEmpty() || !mManagers.IsEmpty()) { + return; + } + Stop(); + gMIDIPlatformService = nullptr; +} + +void MIDIPlatformService::AddManager(MIDIManagerParent* aManager) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + mManagers.AppendElement(aManager); + // Managers add themselves during construction. We have to wait for the + // protocol construction to finish before we send them a port list. The + // runnable calls SendPortList, which iterates through the live manager list, + // so this saves us from having to worry about Manager pointer validity at + // time of runnable execution. + nsCOMPtr<nsIRunnable> r(new SendPortListRunnable()); + NS_DispatchToCurrentThread(r); +} + +void MIDIPlatformService::RemoveManager(MIDIManagerParent* aManager) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + mManagers.RemoveElement(aManager); + MaybeStop(); +} + +void MIDIPlatformService::UpdateStatus( + const nsAString& aPortId, const MIDIPortDeviceState& aDeviceState, + const MIDIPortConnectionState& aConnectionState) { + ::mozilla::ipc::AssertIsOnBackgroundThread(); + for (auto port : mPorts) { + if (port->MIDIPortInterface::Id() == aPortId) { + port->SendUpdateStatus(aDeviceState, aConnectionState); + } + } +} + +void MIDIPlatformService::GetMessages(const nsAString& aPortId, + nsTArray<MIDIMessage>& aMsgs) { + // Can run on either background thread or platform specific IO Thread. + { + MutexAutoLock lock(mMessageQueueMutex); + MIDIMessageQueue* msgQueue; + if (!mMessageQueues.Get(aPortId, &msgQueue)) { + return; + } + msgQueue->GetMessages(aMsgs); + } +} + +void MIDIPlatformService::GetMessagesBefore(const nsAString& aPortId, + const TimeStamp& aTimeStamp, + nsTArray<MIDIMessage>& aMsgs) { + // Can run on either background thread or platform specific IO Thread. + { + MutexAutoLock lock(mMessageQueueMutex); + MIDIMessageQueue* msgQueue; + if (!mMessageQueues.Get(aPortId, &msgQueue)) { + return; + } + msgQueue->GetMessagesBefore(aTimeStamp, aMsgs); + } +} diff --git a/dom/midi/MIDIPlatformService.h b/dom/midi/MIDIPlatformService.h new file mode 100644 index 0000000000..74ca04b3b8 --- /dev/null +++ b/dom/midi/MIDIPlatformService.h @@ -0,0 +1,162 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIPlatformService_h +#define mozilla_dom_MIDIPlatformService_h + +#include "nsClassHashtable.h" +#include "mozilla/Mutex.h" +#include "mozilla/dom/MIDIPortBinding.h" +#include "nsHashKeys.h" + +// XXX Avoid including this here by moving function implementations to the cpp +// file. +#include "mozilla/dom/MIDIMessageQueue.h" + +namespace mozilla { +namespace dom { + +class MIDIManagerParent; +class MIDIPortParent; +class MIDIMessage; +class MIDIMessageQueue; +class MIDIPortInfo; + +/** + * Base class for platform specific MIDI implementations. Handles aggregation of + * IPC service objects, as well as sending/receiving updates about port + * connection events and messages. + * + */ +class MIDIPlatformService { + public: + NS_INLINE_DECL_REFCOUNTING(MIDIPlatformService); + // Adds info about MIDI Port that has been connected. + void AddPortInfo(MIDIPortInfo& aPortInfo); + + // Removes info of MIDI Port that has been disconnected. + void RemovePortInfo(MIDIPortInfo& aPortInfo); + + // Adds a newly created manager protocol object to manager array. + void AddManager(MIDIManagerParent* aParent); + + // Removes a deleted manager protocol object from manager array. + void RemoveManager(MIDIManagerParent* aParent); + + // Adds a newly created port protocol object to port array. + void AddPort(MIDIPortParent* aPort); + + // Removes a deleted port protocol object from port array. + void RemovePort(MIDIPortParent* aPort); + + // Platform specific init function. + virtual void Init() = 0; + + // Platform specific MIDI port opening function. + virtual void Open(MIDIPortParent* aPort) = 0; + + // Clears all queued MIDI messages for a port. + void Clear(MIDIPortParent* aPort); + + // Puts in a request to destroy the singleton MIDIPlatformService object. + // Object will only be destroyed if there are no more MIDIManager and MIDIPort + // protocols left to communicate with. + void MaybeStop(); + + // True if service is live. + static bool IsRunning(); + + // Returns a pointer to the MIDIPlatformService object, creating it and + // starting the platform specific service if it is not currently running. + static MIDIPlatformService* Get(); + + // Sends a list of all currently connected ports in order to populate a new + // MIDIAccess object. + void SendPortList(); + + // Receives a new set of messages from an MIDI Input Port, and checks their + // validity. + void CheckAndReceive(const nsAString& aPortID, + const nsTArray<MIDIMessage>& aMsgs); + + // Sends connection/disconnect/open/closed/etc status updates about a MIDI + // Port to all port listeners. + void UpdateStatus(MIDIPortParent* aPort, + const MIDIPortDeviceState& aDeviceState, + const MIDIPortConnectionState& aConnectionState); + void UpdateStatus(const nsAString& aPortId, + const MIDIPortDeviceState& aDeviceState, + const MIDIPortConnectionState& aConnectionState); + + // Adds outgoing messages to the sorted message queue, for sending later. + void QueueMessages(const nsAString& aId, nsTArray<MIDIMessage>& aMsgs); + + // Clears all messages later than now, sends all outgoing message scheduled + // before/at now, and schedules MIDI Port connection closing. + void Close(MIDIPortParent* aPort); + + protected: + MIDIPlatformService(); + virtual ~MIDIPlatformService(); + // Platform specific MIDI service shutdown method. + virtual void Stop() = 0; + + // When device state of a MIDI Port changes, broadcast to all IPC port + // objects. + void BroadcastState(const MIDIPortInfo& aPortInfo, + const MIDIPortDeviceState& aState); + + // Platform specific MIDI port closing function. Named "Schedule" due to the + // fact that it needs to happen in the context of the I/O thread for the + // platform MIDI implementation, and therefore will happen async. + virtual void ScheduleClose(MIDIPortParent* aPort) = 0; + + // Platform specific MIDI message sending function. Named "Schedule" due to + // the fact that it needs to happen in the context of the I/O thread for the + // platform MIDI implementation, and therefore will happen async. + virtual void ScheduleSend(const nsAString& aPortId) = 0; + + // Allows platform specific IO Threads to retrieve all messages to be sent. + // Handles mutex locking. + void GetMessages(const nsAString& aPortId, nsTArray<MIDIMessage>& aMsgs); + + // Allows platform specific IO Threads to retrieve all messages to be sent + // before a certain timestamp. Handles mutex locking. + void GetMessagesBefore(const nsAString& aPortId, const TimeStamp& aTimeStamp, + nsTArray<MIDIMessage>& aMsgs); + + private: + // When the MIDIPlatformService is created, we need to know whether or not the + // corresponding IPC MIDIManager objects have received the MIDIPort list after + // it is populated. This is set to True when that is done, so we don't + // constantly spam MIDIManagers with port lists. + bool mHasSentPortList; + + // Array of MIDIManager IPC objects. This array manages the lifetime of + // MIDIManager objects in the parent process, and IPC will call + // RemoveManager() end lifetime when IPC channel is destroyed. + nsTArray<RefPtr<MIDIManagerParent>> mManagers; + + // Array of information for currently connected Ports + nsTArray<MIDIPortInfo> mPortInfo; + + // Array of MIDIPort IPC objects. May contain ports not listed in mPortInfo, + // as we can hold port objects even after they are disconnected. + // + // TODO Split this into input and output ports. Will make life easier. + nsTArray<RefPtr<MIDIPortParent>> mPorts; + + // Per-port message queue hashtable. Handles scheduling messages for sending. + nsClassHashtable<nsStringHashKey, MIDIMessageQueue> mMessageQueues; + + // Mutex for managing access to message queue objects. + Mutex mMessageQueueMutex; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MIDIPlatformService_h diff --git a/dom/midi/MIDIPort.cpp b/dom/midi/MIDIPort.cpp new file mode 100644 index 0000000000..662c7ca7b1 --- /dev/null +++ b/dom/midi/MIDIPort.cpp @@ -0,0 +1,197 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/MIDIPort.h" +#include "mozilla/dom/MIDIConnectionEvent.h" +#include "mozilla/dom/MIDIPortChild.h" +#include "mozilla/dom/MIDIAccess.h" +#include "mozilla/dom/MIDITypes.h" +#include "mozilla/ipc/PBackgroundChild.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/MIDITypes.h" +#include "mozilla/Unused.h" +#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, MOZ_COUNT_DTOR + +using namespace mozilla::ipc; + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(MIDIPort, DOMEventTargetHelper, + mOpeningPromise, mClosingPromise) + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MIDIPort, DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MIDIPort) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(MIDIPort, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(MIDIPort, DOMEventTargetHelper) + +MIDIPort::MIDIPort(nsPIDOMWindowInner* aWindow, MIDIAccess* aMIDIAccessParent) + : DOMEventTargetHelper(aWindow), mMIDIAccessParent(aMIDIAccessParent) { + MOZ_ASSERT(aWindow); + MOZ_ASSERT(aMIDIAccessParent); +} + +MIDIPort::~MIDIPort() { + if (mMIDIAccessParent) { + mMIDIAccessParent->RemovePortListener(this); + mMIDIAccessParent = nullptr; + } + if (mPort) { + // If the IPC port channel is still alive at this point, it means we're + // probably CC'ing this port object. Send the shutdown message to also clean + // up the IPC channel. + mPort->SendShutdown(); + // This will unset the IPC Port pointer. Don't call anything after this. + mPort->Teardown(); + } +} + +bool MIDIPort::Initialize(const MIDIPortInfo& aPortInfo, bool aSysexEnabled) { + RefPtr<MIDIPortChild> port = + new MIDIPortChild(aPortInfo, aSysexEnabled, this); + PBackgroundChild* b = BackgroundChild::GetForCurrentThread(); + MOZ_ASSERT(b, + "Should always have a valid BackgroundChild when creating a port " + "object!"); + if (!b->SendPMIDIPortConstructor(port, aPortInfo, aSysexEnabled)) { + return false; + } + mPort = port; + // Make sure to increase the ref count for the port, so it can be cleaned up + // by the IPC manager. + mPort->SetActorAlive(); + return true; +} + +void MIDIPort::UnsetIPCPort() { mPort = nullptr; } + +void MIDIPort::GetId(nsString& aRetVal) const { + MOZ_ASSERT(mPort); + aRetVal = mPort->MIDIPortInterface::Id(); +} + +void MIDIPort::GetManufacturer(nsString& aRetVal) const { + MOZ_ASSERT(mPort); + aRetVal = mPort->Manufacturer(); +} + +void MIDIPort::GetName(nsString& aRetVal) const { + MOZ_ASSERT(mPort); + aRetVal = mPort->Name(); +} + +void MIDIPort::GetVersion(nsString& aRetVal) const { + MOZ_ASSERT(mPort); + aRetVal = mPort->Version(); +} + +MIDIPortType MIDIPort::Type() const { + MOZ_ASSERT(mPort); + return mPort->Type(); +} + +MIDIPortConnectionState MIDIPort::Connection() const { + MOZ_ASSERT(mPort); + return mPort->ConnectionState(); +} + +MIDIPortDeviceState MIDIPort::State() const { + MOZ_ASSERT(mPort); + return mPort->DeviceState(); +} + +bool MIDIPort::SysexEnabled() const { + MOZ_ASSERT(mPort); + return mPort->SysexEnabled(); +} + +already_AddRefed<Promise> MIDIPort::Open() { + MOZ_ASSERT(mPort); + RefPtr<Promise> p; + if (mOpeningPromise) { + p = mOpeningPromise; + return p.forget(); + } + ErrorResult rv; + nsCOMPtr<nsIGlobalObject> go = do_QueryInterface(GetOwner()); + p = Promise::Create(go, rv); + if (rv.Failed()) { + return nullptr; + } + mOpeningPromise = p; + mPort->SendOpen(); + return p.forget(); +} + +already_AddRefed<Promise> MIDIPort::Close() { + MOZ_ASSERT(mPort); + RefPtr<Promise> p; + if (mClosingPromise) { + p = mClosingPromise; + return p.forget(); + } + ErrorResult rv; + nsCOMPtr<nsIGlobalObject> go = do_QueryInterface(GetOwner()); + p = Promise::Create(go, rv); + if (rv.Failed()) { + return nullptr; + } + mClosingPromise = p; + mPort->SendClose(); + return p.forget(); +} + +void MIDIPort::Notify(const void_t& aVoid) { + // If we're getting notified, it means the MIDIAccess parent object is dead. + // Nullify our copy. + mMIDIAccessParent = nullptr; +} + +void MIDIPort::FireStateChangeEvent() { + MOZ_ASSERT(mPort); + if (mPort->ConnectionState() == MIDIPortConnectionState::Open || + mPort->ConnectionState() == MIDIPortConnectionState::Pending) { + if (mOpeningPromise) { + mOpeningPromise->MaybeResolve(this); + mOpeningPromise = nullptr; + } + } else if (mPort->ConnectionState() == MIDIPortConnectionState::Closed) { + if (mOpeningPromise) { + mOpeningPromise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR); + mOpeningPromise = nullptr; + } + if (mClosingPromise) { + mClosingPromise->MaybeResolve(this); + mClosingPromise = nullptr; + } + } + if (mPort->DeviceState() == MIDIPortDeviceState::Connected && + mPort->ConnectionState() == MIDIPortConnectionState::Pending) { + mPort->SendOpen(); + } + // Fire MIDIAccess events first so that the port is no longer in the port + // maps. + if (mMIDIAccessParent) { + mMIDIAccessParent->FireConnectionEvent(this); + } + + MIDIConnectionEventInit init; + init.mPort = this; + RefPtr<MIDIConnectionEvent> event( + MIDIConnectionEvent::Constructor(this, u"statechange"_ns, init)); + DispatchTrustedEvent(event); +} + +void MIDIPort::Receive(const nsTArray<MIDIMessage>& aMsg) { + MOZ_CRASH("We should never get here!"); +} + +} // namespace mozilla::dom diff --git a/dom/midi/MIDIPort.h b/dom/midi/MIDIPort.h new file mode 100644 index 0000000000..1d12030a71 --- /dev/null +++ b/dom/midi/MIDIPort.h @@ -0,0 +1,97 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIPort_h +#define mozilla_dom_MIDIPort_h + +#include "nsWrapperCache.h" +#include "mozilla/Attributes.h" +#include "mozilla/Observer.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/dom/MIDIAccess.h" +#include "mozilla/dom/MIDIPortInterface.h" + +struct JSContext; + +namespace mozilla { +namespace dom { + +class Promise; +class MIDIPortInfo; +class MIDIAccess; +class MIDIPortChangeEvent; +class MIDIPortChild; +class MIDIMessage; + +/** + * Implementation of WebIDL DOM MIDIPort class. Handles all port representation + * and communication. + * + */ +class MIDIPort : public DOMEventTargetHelper, + public MIDIAccessDestructionObserver { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(MIDIPort, + DOMEventTargetHelper) + protected: + MIDIPort(nsPIDOMWindowInner* aWindow, MIDIAccess* aMIDIAccessParent); + bool Initialize(const MIDIPortInfo& aPortInfo, bool aSysexEnabled); + virtual ~MIDIPort(); + + public: + nsPIDOMWindowInner* GetParentObject() const { return GetOwner(); } + + // Getters + void GetId(nsString& aRetVal) const; + void GetManufacturer(nsString& aRetVal) const; + void GetName(nsString& aRetVal) const; + void GetVersion(nsString& aRetVal) const; + MIDIPortType Type() const; + MIDIPortConnectionState Connection() const; + MIDIPortDeviceState State() const; + bool SysexEnabled() const; + + already_AddRefed<Promise> Open(); + already_AddRefed<Promise> Close(); + + // MIDIPorts observe the death of their parent MIDIAccess object, and delete + // their reference accordingly. + virtual void Notify(const void_t& aVoid) override; + + void FireStateChangeEvent(); + + virtual void Receive(const nsTArray<MIDIMessage>& aMsg); + + // This object holds a pointer to its corresponding IPC MIDIPortChild actor. + // If the IPC actor is deleted, it cleans itself up via this method. + void UnsetIPCPort(); + + IMPL_EVENT_HANDLER(statechange) + protected: + // IPC Actor corresponding to this class + RefPtr<MIDIPortChild> mPort; + + private: + // MIDIAccess object that created this MIDIPort object, which we need for + // firing port connection events. There is a chance this MIDIPort object can + // outlive its parent MIDIAccess object, so this is a weak reference that must + // be handled properly. It is set on construction of the MIDIPort object, and + // set to null when the parent MIDIAccess object is destroyed, which fires an + // notification we observe. + MIDIAccess* mMIDIAccessParent; + // Promise object generated on Open() call, that needs to be resolved once the + // platform specific Open() function has completed. + RefPtr<Promise> mOpeningPromise; + // Promise object generated on Close() call, that needs to be resolved once + // the platform specific Close() function has completed. + RefPtr<Promise> mClosingPromise; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MIDIPort_h diff --git a/dom/midi/MIDIPortChild.cpp b/dom/midi/MIDIPortChild.cpp new file mode 100644 index 0000000000..1540c8cae4 --- /dev/null +++ b/dom/midi/MIDIPortChild.cpp @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/MIDIPortChild.h" +#include "mozilla/dom/MIDIPort.h" +#include "mozilla/dom/MIDIPortInterface.h" + +using namespace mozilla; +using namespace mozilla::dom; + +MIDIPortChild::MIDIPortChild(const MIDIPortInfo& aPortInfo, bool aSysexEnabled, + MIDIPort* aPort) + : MIDIPortInterface(aPortInfo, aSysexEnabled), + mDOMPort(aPort), + mActorWasAlive(false) {} + +void MIDIPortChild::Teardown() { + if (mDOMPort) { + mDOMPort->UnsetIPCPort(); + mDOMPort = nullptr; + } + MIDIPortInterface::Shutdown(); +} + +void MIDIPortChild::ActorDestroy(ActorDestroyReason aWhy) {} + +mozilla::ipc::IPCResult MIDIPortChild::RecvReceive( + nsTArray<MIDIMessage>&& aMsgs) { + if (mDOMPort) { + mDOMPort->Receive(aMsgs); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult MIDIPortChild::RecvUpdateStatus( + const uint32_t& aDeviceState, const uint32_t& aConnectionState) { + // Either a device is connected, and can have any connection state, or a + // device is disconnected, and can only be closed or pending. + MOZ_ASSERT(mDeviceState == MIDIPortDeviceState::Connected || + (mConnectionState == MIDIPortConnectionState::Closed || + mConnectionState == MIDIPortConnectionState::Pending)); + mDeviceState = static_cast<MIDIPortDeviceState>(aDeviceState); + mConnectionState = static_cast<MIDIPortConnectionState>(aConnectionState); + if (mDOMPort) { + mDOMPort->FireStateChangeEvent(); + } + return IPC_OK(); +} + +void MIDIPortChild::SetActorAlive() { + MOZ_ASSERT(!mActorWasAlive); + mActorWasAlive = true; + AddRef(); +} diff --git a/dom/midi/MIDIPortChild.h b/dom/midi/MIDIPortChild.h new file mode 100644 index 0000000000..e0d1e5a247 --- /dev/null +++ b/dom/midi/MIDIPortChild.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIPortChild_h +#define mozilla_dom_MIDIPortChild_h + +#include "mozilla/dom/PMIDIPortChild.h" +#include "mozilla/dom/MIDIPortInterface.h" + +namespace mozilla { +namespace dom { + +class MIDIPort; +class MIDIPortInfo; + +/** + * Child actor for a MIDIPort object. Each MIDIPort DOM object in JS has a its + * own child actor. The lifetime of the actor object is dependent on the + * lifetime of the JS object. + * + */ +class MIDIPortChild final : public PMIDIPortChild, public MIDIPortInterface { + public: + NS_INLINE_DECL_REFCOUNTING(MIDIPortChild); + mozilla::ipc::IPCResult RecvReceive(nsTArray<MIDIMessage>&& aMsgs); + + virtual void ActorDestroy(ActorDestroyReason aWhy) override; + + mozilla::ipc::IPCResult RecvUpdateStatus(const uint32_t& aDeviceState, + const uint32_t& aConnectionState); + + MIDIPortChild(const MIDIPortInfo& aPortInfo, bool aSysexEnabled, + MIDIPort* aPort); + // virtual void Shutdown() override; + void SetActorAlive(); + + void Teardown(); + + private: + ~MIDIPortChild() = default; + // Pointer to the DOM object this actor represents. The actor cannot outlive + // the DOM object. + MIDIPort* mDOMPort; + bool mActorWasAlive; +}; +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/midi/MIDIPortInterface.cpp b/dom/midi/MIDIPortInterface.cpp new file mode 100644 index 0000000000..42411b81a8 --- /dev/null +++ b/dom/midi/MIDIPortInterface.cpp @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/MIDIPortInterface.h" +#include "mozilla/dom/MIDIPlatformService.h" +#include "mozilla/dom/MIDITypes.h" + +mozilla::dom::MIDIPortInterface::MIDIPortInterface( + const MIDIPortInfo& aPortInfo, bool aSysexEnabled) + : mId(aPortInfo.id()), + mName(aPortInfo.name()), + mManufacturer(aPortInfo.manufacturer()), + mVersion(aPortInfo.version()), + mSysexEnabled(aSysexEnabled), + mType((MIDIPortType)aPortInfo.type()), + // We'll never initialize a port object that's not connected + mDeviceState(MIDIPortDeviceState::Connected), + // Open everything on connection + mConnectionState(MIDIPortConnectionState::Open), + mShuttingDown(false) {} + +mozilla::dom::MIDIPortInterface::~MIDIPortInterface() { Shutdown(); } + +void mozilla::dom::MIDIPortInterface::Shutdown() { mShuttingDown = true; } diff --git a/dom/midi/MIDIPortInterface.h b/dom/midi/MIDIPortInterface.h new file mode 100644 index 0000000000..dbbea382ed --- /dev/null +++ b/dom/midi/MIDIPortInterface.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIPortInterface_h +#define mozilla_dom_MIDIPortInterface_h + +#include "mozilla/dom/MIDIPortBinding.h" + +namespace mozilla { +namespace dom { +class MIDIPortInfo; +/** + * Base class for MIDIPort Parent/Child Actors. Makes sure both sides of the + * MIDIPort IPC connection need to a synchronized set of info/state. + * + */ +class MIDIPortInterface { + public: + MIDIPortInterface(const MIDIPortInfo& aPortInfo, bool aSysexEnabled); + const nsString& Id() const { return mId; } + const nsString& Name() const { return mName; } + const nsString& Manufacturer() const { return mManufacturer; } + const nsString& Version() const { return mVersion; } + bool SysexEnabled() const { return mSysexEnabled; } + MIDIPortType Type() const { return mType; } + MIDIPortDeviceState DeviceState() const { return mDeviceState; } + MIDIPortConnectionState ConnectionState() const { return mConnectionState; } + bool IsShutdown() const { return mShuttingDown; } + virtual void Shutdown(); + + protected: + virtual ~MIDIPortInterface(); + nsString mId; + nsString mName; + nsString mManufacturer; + nsString mVersion; + bool mSysexEnabled; + MIDIPortType mType; + MIDIPortDeviceState mDeviceState; + MIDIPortConnectionState mConnectionState; + bool mShuttingDown; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MIDIPortInterface_h diff --git a/dom/midi/MIDIPortParent.cpp b/dom/midi/MIDIPortParent.cpp new file mode 100644 index 0000000000..2da91d7653 --- /dev/null +++ b/dom/midi/MIDIPortParent.cpp @@ -0,0 +1,108 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/MIDIPortParent.h" +#include "mozilla/dom/MIDIPlatformService.h" +#include "nsContentUtils.h" + +// C++ file contents +namespace mozilla::dom { + +// Keep an internal ID that we can use for passing information about specific +// MIDI ports back and forth to the Rust libraries. +static uint32_t gId = 0; + +mozilla::ipc::IPCResult MIDIPortParent::RecvSend( + nsTArray<MIDIMessage>&& aMsgs) { + if (mConnectionState != MIDIPortConnectionState::Open) { + mMessageQueue.AppendElements(aMsgs); + if (MIDIPlatformService::IsRunning()) { + MIDIPlatformService::Get()->Open(this); + } + return IPC_OK(); + } + if (MIDIPlatformService::IsRunning()) { + MIDIPlatformService::Get()->QueueMessages(MIDIPortInterface::mId, aMsgs); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult MIDIPortParent::RecvOpen() { + if (MIDIPlatformService::IsRunning()) { + MIDIPlatformService::Get()->Open(this); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult MIDIPortParent::RecvClose() { + if (mConnectionState != MIDIPortConnectionState::Closed) { + if (MIDIPlatformService::IsRunning()) { + MIDIPlatformService::Get()->Close(this); + } + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult MIDIPortParent::RecvClear() { + if (MIDIPlatformService::IsRunning()) { + MIDIPlatformService::Get()->Clear(this); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult MIDIPortParent::RecvShutdown() { + if (mShuttingDown) { + return IPC_OK(); + } + Teardown(); + Unused << Send__delete__(this); + return IPC_OK(); +} + +void MIDIPortParent::Teardown() { + mMessageQueue.Clear(); + MIDIPortInterface::Shutdown(); + if (MIDIPlatformService::IsRunning()) { + MIDIPlatformService::Get()->RemovePort(this); + } +} + +void MIDIPortParent::ActorDestroy(ActorDestroyReason) {} + +bool MIDIPortParent::SendUpdateStatus( + const MIDIPortDeviceState& aDeviceState, + const MIDIPortConnectionState& aConnectionState) { + if (mShuttingDown) { + return true; + } + mDeviceState = aDeviceState; + mConnectionState = aConnectionState; + if (aConnectionState == MIDIPortConnectionState::Open && + aDeviceState == MIDIPortDeviceState::Disconnected) { + mConnectionState = MIDIPortConnectionState::Pending; + } else if (aConnectionState == MIDIPortConnectionState::Open && + aDeviceState == MIDIPortDeviceState::Connected && + !mMessageQueue.IsEmpty()) { + if (MIDIPlatformService::IsRunning()) { + MIDIPlatformService::Get()->QueueMessages(MIDIPortInterface::mId, + mMessageQueue); + } + mMessageQueue.Clear(); + } + return PMIDIPortParent::SendUpdateStatus( + static_cast<uint32_t>(mDeviceState), + static_cast<uint32_t>(mConnectionState)); +} + +MIDIPortParent::MIDIPortParent(const MIDIPortInfo& aPortInfo, + const bool aSysexEnabled) + : MIDIPortInterface(aPortInfo, aSysexEnabled), mInternalId(++gId) { + MOZ_ASSERT(MIDIPlatformService::IsRunning(), + "Shouldn't be able to add MIDI port without MIDI service!"); + MIDIPlatformService::Get()->AddPort(this); +} + +} // namespace mozilla::dom diff --git a/dom/midi/MIDIPortParent.h b/dom/midi/MIDIPortParent.h new file mode 100644 index 0000000000..946e457928 --- /dev/null +++ b/dom/midi/MIDIPortParent.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_MIDIPortParent_h +#define mozilla_dom_MIDIPortParent_h + +#include "mozilla/dom/PMIDIPortParent.h" +#include "mozilla/dom/MIDIPortBinding.h" +#include "mozilla/dom/MIDIPortInterface.h" + +// Header file contents +namespace mozilla { +namespace dom { + +/** + * Actor representing the parent (PBackground thread) side of a MIDIPort object. + * + */ +class MIDIPortParent final : public PMIDIPortParent, public MIDIPortInterface { + public: + NS_INLINE_DECL_REFCOUNTING(MIDIPortParent); + void ActorDestroy(ActorDestroyReason) override; + mozilla::ipc::IPCResult RecvSend(nsTArray<MIDIMessage>&& aMsg); + mozilla::ipc::IPCResult RecvOpen(); + mozilla::ipc::IPCResult RecvClose(); + mozilla::ipc::IPCResult RecvClear(); + mozilla::ipc::IPCResult RecvShutdown(); + MOZ_IMPLICIT MIDIPortParent(const MIDIPortInfo& aPortInfo, + const bool aSysexEnabled); + // Sends the current port status to the child actor. May also send message + // buffer if required. + bool SendUpdateStatus(const MIDIPortDeviceState& aState, + const MIDIPortConnectionState& aConnection); + uint32_t GetInternalId() const { return mInternalId; } + void Teardown(); + + protected: + ~MIDIPortParent() = default; + // Queue of messages that needs to be sent. Since sending a message on a + // closed port opens it, we sometimes have to buffer messages from the time + // Send() is called until the time we get a device state change to Opened. + nsTArray<MIDIMessage> mMessageQueue; + const uint32_t mInternalId; +}; +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/midi/MIDITypes.ipdlh b/dom/midi/MIDITypes.ipdlh new file mode 100644 index 0000000000..9694ca12c3 --- /dev/null +++ b/dom/midi/MIDITypes.ipdlh @@ -0,0 +1,31 @@ +/* -*- Mode: C++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 8 -*- */ +/* vim: set sw=4 ts=8 et tw=80 ft=cpp : */ +/* 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/. */ + +using mozilla::TimeStamp from "mozilla/TimeStamp.h"; + +namespace mozilla { +namespace dom { + +comparable struct MIDIPortInfo { + nsString id; + nsString name; + nsString manufacturer; + nsString version; + //Actually a MIDIPortType enum + uint32_t type; +}; + +struct MIDIMessage { + uint8_t[] data; + TimeStamp timestamp; +}; + +struct MIDIPortList { + MIDIPortInfo[] ports; +}; + +} // namespace dom +} // namespace mozilla diff --git a/dom/midi/MIDIUtils.cpp b/dom/midi/MIDIUtils.cpp new file mode 100644 index 0000000000..6e2a7f75af --- /dev/null +++ b/dom/midi/MIDIUtils.cpp @@ -0,0 +1,124 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/MIDITypes.h" +#include "mozilla/dom/MIDIUtils.h" +#include "mozilla/UniquePtr.h" + +// Taken from MIDI IMPLEMENTATION CHART INSTRUCTIONS, MIDI Spec v1.0, Pg. 97 +static const uint8_t kCommandByte = 0x80; +static const uint8_t kSysexMessageStart = 0xF0; +static const uint8_t kSystemMessage = 0xF0; +static const uint8_t kSysexMessageEnd = 0xF7; +static const uint8_t kSystemRealtimeMessage = 0xF8; +// Represents the length of all possible command messages. +// Taken from MIDI Spec, Pg. 101v 1.0, Table 2 +static const uint8_t kCommandLengths[] = {3, 3, 3, 3, 2, 2, 3}; +// Represents the length of all possible system messages. The length of sysex +// messages is variable, so we just put zero since it won't be checked anyways. +// Taken from MIDI Spec v1.0, Pg. 105, Table 5 +static const uint8_t kSystemLengths[] = {0, 2, 3, 2, 1, 1, 1, 1}; + +namespace mozilla::dom::MIDIUtils { + +// Checks validity of MIDIMessage passed to it. Throws debug warnings and +// returns false if message is not valid. +bool IsValidMessage(const MIDIMessage* aMsg) { + if (NS_WARN_IF(!aMsg)) { + return false; + } + // Assert on parser problems + MOZ_ASSERT(aMsg->data().Length() > 0, + "Created a MIDI Message of Length 0. This should never happen!"); + uint8_t cmd = aMsg->data()[0]; + // If first byte isn't a command, something is definitely wrong. + MOZ_ASSERT((cmd & kCommandByte) == kCommandByte, + "Constructed a MIDI packet where first byte is not command!"); + if (cmd == kSysexMessageStart) { + // All we can do with sysex is make sure it starts and ends with the + // correct command bytes. + if (aMsg->data()[aMsg->data().Length() - 1] != kSysexMessageEnd) { + NS_WARNING("Last byte of Sysex Message not 0xF7!"); + return false; + } + return true; + } + // For system realtime messages, the length should always be 1. + if ((cmd & kSystemRealtimeMessage) == kSystemRealtimeMessage) { + return aMsg->data().Length() == 1; + } + // Otherwise, just use the correct array for testing lengths. We can't tell + // much about message validity other than that. + if ((cmd & kSystemMessage) == kSystemMessage) { + if (cmd - kSystemMessage >= + static_cast<uint8_t>(ArrayLength(kSystemLengths))) { + NS_WARNING("System Message Command byte not valid!"); + return false; + } + return aMsg->data().Length() == kSystemLengths[cmd - kSystemMessage]; + } + // For non system commands, we only care about differences in the high nibble + // of the first byte. Shift this down to give the index of the expected packet + // length. + uint8_t cmdIndex = (cmd - kCommandByte) >> 4; + if (cmdIndex >= ArrayLength(kCommandLengths)) { + // If our index is bigger than our array length, command byte is unknown; + NS_WARNING("Unknown MIDI command!"); + return false; + } + return aMsg->data().Length() == kCommandLengths[cmdIndex]; +} + +uint32_t ParseMessages(const nsTArray<uint8_t>& aByteBuffer, + const TimeStamp& aTimestamp, + nsTArray<MIDIMessage>& aMsgArray) { + uint32_t bytesRead = 0; + bool inSysexMessage = false; + UniquePtr<MIDIMessage> currentMsg; + for (auto& byte : aByteBuffer) { + bytesRead++; + if ((byte & kSystemRealtimeMessage) == kSystemRealtimeMessage) { + MIDIMessage rt_msg; + rt_msg.data().AppendElement(byte); + rt_msg.timestamp() = aTimestamp; + aMsgArray.AppendElement(rt_msg); + continue; + } + if (byte == kSysexMessageEnd) { + if (!inSysexMessage) { + MOZ_ASSERT(inSysexMessage); + NS_WARNING( + "Got sysex message end with no sysex message being processed!"); + } + inSysexMessage = false; + } else if (byte & kCommandByte) { + if (currentMsg && IsValidMessage(currentMsg.get())) { + aMsgArray.AppendElement(*currentMsg); + } + currentMsg = MakeUnique<MIDIMessage>(); + currentMsg->timestamp() = aTimestamp; + } + currentMsg->data().AppendElement(byte); + if (byte == kSysexMessageStart) { + inSysexMessage = true; + } + } + if (currentMsg && IsValidMessage(currentMsg.get())) { + aMsgArray.AppendElement(*currentMsg); + } + return bytesRead; +} + +bool IsSysexMessage(const MIDIMessage& aMsg) { + if (aMsg.data().Length() == 0) { + return false; + } + if (aMsg.data()[0] == kSysexMessageStart) { + return true; + } + return false; +} +} // namespace mozilla::dom::MIDIUtils diff --git a/dom/midi/MIDIUtils.h b/dom/midi/MIDIUtils.h new file mode 100644 index 0000000000..43cc294b90 --- /dev/null +++ b/dom/midi/MIDIUtils.h @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "nsTArray.h" +#include "mozilla/TimeStamp.h" + +namespace mozilla { +namespace dom { +class MIDIMessage; + +/** + * Set of utility functions for dealing with MIDI Messages. + * + */ +namespace MIDIUtils { + +// Takes a nsTArray of bytes and parses it into zero or more MIDI messages. +// Returns number of bytes parsed. +uint32_t ParseMessages(const nsTArray<uint8_t>& aByteBuffer, + const TimeStamp& aTimestamp, + nsTArray<MIDIMessage>& aMsgArray); +// Returns true if a message is a sysex message. +bool IsSysexMessage(const MIDIMessage& a); +} // namespace MIDIUtils +} // namespace dom +} // namespace mozilla diff --git a/dom/midi/PMIDIManager.ipdl b/dom/midi/PMIDIManager.ipdl new file mode 100644 index 0000000000..eb5f363fd4 --- /dev/null +++ b/dom/midi/PMIDIManager.ipdl @@ -0,0 +1,25 @@ +/* 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 protocol PBackground; +include MIDITypes; + +namespace mozilla { +namespace dom { + +async protocol PMIDIManager +{ + manager PBackground; +parent: + async Shutdown(); +child: + /* + * Send an updated list of MIDI ports to the child + */ + async MIDIPortListUpdate(MIDIPortList aPortList); + async __delete__(); +}; + +} // namespace ipc +} // namespace mozilla diff --git a/dom/midi/PMIDIPort.ipdl b/dom/midi/PMIDIPort.ipdl new file mode 100644 index 0000000000..ebdb9c644d --- /dev/null +++ b/dom/midi/PMIDIPort.ipdl @@ -0,0 +1,31 @@ +/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ +/* vim: set ts=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 protocol PBackground; +include MIDITypes; + +namespace mozilla { +namespace dom { + +async protocol PMIDIPort +{ + manager PBackground; +parent: + async Shutdown(); + async Send(MIDIMessage[] msg); + async Open(); + async Close(); + async Clear(); +child: + async Receive(MIDIMessage[] msg); + // Actually takes a MIDIDeviceConnectionState and MIDIPortConnectionState + // respectively. + async UpdateStatus(uint32_t deviceState, uint32_t connectionState); + async __delete__(); +}; + +} +} diff --git a/dom/midi/TestMIDIPlatformService.cpp b/dom/midi/TestMIDIPlatformService.cpp new file mode 100644 index 0000000000..22cd57a2e4 --- /dev/null +++ b/dom/midi/TestMIDIPlatformService.cpp @@ -0,0 +1,244 @@ +/* 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 "TestMIDIPlatformService.h" +#include "mozilla/dom/MIDIPort.h" +#include "mozilla/dom/MIDITypes.h" +#include "mozilla/dom/MIDIPortInterface.h" +#include "mozilla/dom/MIDIPortParent.h" +#include "mozilla/dom/MIDIPlatformRunnables.h" +#include "mozilla/dom/MIDIUtils.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/Unused.h" +#include "nsIThread.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::ipc; + +/** + * Runnable used for making sure ProcessMessages only happens on the IO thread. + * + */ +class ProcessMessagesRunnable : public mozilla::Runnable { + public: + explicit ProcessMessagesRunnable(const nsAString& aPortID) + : Runnable("ProcessMessagesRunnable"), mPortID(aPortID) {} + ~ProcessMessagesRunnable() = default; + NS_IMETHOD Run() { + // If service is no longer running, just exist without processing. + if (!MIDIPlatformService::IsRunning()) { + return NS_OK; + } + TestMIDIPlatformService* srv = + static_cast<TestMIDIPlatformService*>(MIDIPlatformService::Get()); + srv->ProcessMessages(mPortID); + return NS_OK; + } + + private: + nsString mPortID; +}; + +/** + * Runnable used for allowing IO thread to queue more messages for processing, + * since it can't access the service object directly. + * + */ +class QueueMessagesRunnable : public MIDIBackgroundRunnable { + public: + QueueMessagesRunnable(const nsAString& aPortID, + const nsTArray<MIDIMessage>& aMsgs) + : MIDIBackgroundRunnable("QueueMessagesRunnable"), + mPortID(aPortID), + mMsgs(aMsgs.Clone()) {} + ~QueueMessagesRunnable() = default; + virtual void RunInternal() { + AssertIsOnBackgroundThread(); + MIDIPlatformService::Get()->QueueMessages(mPortID, mMsgs); + } + + private: + nsString mPortID; + nsTArray<MIDIMessage> mMsgs; +}; + +TestMIDIPlatformService::TestMIDIPlatformService() + : mBackgroundThread(NS_GetCurrentThread()), + mControlInputPort(u"b744eebe-f7d8-499b-872b-958f63c8f522"_ns, + u"Test Control MIDI Device Input Port"_ns, + u"Test Manufacturer"_ns, u"1.0.0"_ns, + static_cast<uint32_t>(MIDIPortType::Input)), + mControlOutputPort(u"ab8e7fe8-c4de-436a-a960-30898a7c9a3d"_ns, + u"Test Control MIDI Device Output Port"_ns, + u"Test Manufacturer"_ns, u"1.0.0"_ns, + static_cast<uint32_t>(MIDIPortType::Output)), + mStateTestInputPort(u"a9329677-8588-4460-a091-9d4a7f629a48"_ns, + u"Test State MIDI Device Input Port"_ns, + u"Test Manufacturer"_ns, u"1.0.0"_ns, + static_cast<uint32_t>(MIDIPortType::Input)), + mStateTestOutputPort(u"478fa225-b5fc-4fa6-a543-d32d9cb651e7"_ns, + u"Test State MIDI Device Output Port"_ns, + u"Test Manufacturer"_ns, u"1.0.0"_ns, + static_cast<uint32_t>(MIDIPortType::Output)), + mAlwaysClosedTestOutputPort(u"f87d0c76-3c68-49a9-a44f-700f1125c07a"_ns, + u"Always Closed MIDI Device Output Port"_ns, + u"Test Manufacturer"_ns, u"1.0.0"_ns, + static_cast<uint32_t>(MIDIPortType::Output)), + mIsInitialized(false) { + AssertIsOnBackgroundThread(); +} + +TestMIDIPlatformService::~TestMIDIPlatformService() { + AssertIsOnBackgroundThread(); +} + +void TestMIDIPlatformService::Init() { + AssertIsOnBackgroundThread(); + + if (mIsInitialized) { + return; + } + mIsInitialized = true; + + // Treat all of our special ports as always connected. When the service comes + // up, prepopulate the port list with them. + MIDIPlatformService::Get()->AddPortInfo(mControlInputPort); + MIDIPlatformService::Get()->AddPortInfo(mControlOutputPort); + MIDIPlatformService::Get()->AddPortInfo(mAlwaysClosedTestOutputPort); + nsCOMPtr<nsIRunnable> r(new SendPortListRunnable()); + + // Start the IO Thread. + NS_DispatchToCurrentThread(r); +} + +void TestMIDIPlatformService::Open(MIDIPortParent* aPort) { + MOZ_ASSERT(aPort); + MIDIPortConnectionState s = MIDIPortConnectionState::Open; + if (aPort->MIDIPortInterface::Id() == mAlwaysClosedTestOutputPort.id()) { + // If it's the always closed testing port, act like it's already opened + // exclusively elsewhere. + s = MIDIPortConnectionState::Closed; + } + // Connection events are just simulated on the background thread, no need to + // push to IO thread. + nsCOMPtr<nsIRunnable> r(new SetStatusRunnable(aPort->MIDIPortInterface::Id(), + aPort->DeviceState(), s)); + NS_DispatchToCurrentThread(r); +} + +void TestMIDIPlatformService::ScheduleClose(MIDIPortParent* aPort) { + MOZ_ASSERT(aPort); + if (aPort->ConnectionState() == MIDIPortConnectionState::Open) { + // Connection events are just simulated on the background thread, no need to + // push to IO thread. + nsCOMPtr<nsIRunnable> r(new SetStatusRunnable( + aPort->MIDIPortInterface::Id(), aPort->DeviceState(), + MIDIPortConnectionState::Closed)); + NS_DispatchToCurrentThread(r); + } +} + +void TestMIDIPlatformService::Stop() { AssertIsOnBackgroundThread(); } + +void TestMIDIPlatformService::ScheduleSend(const nsAString& aPortId) { + nsCOMPtr<nsIRunnable> r(new ProcessMessagesRunnable(aPortId)); + NS_DispatchToCurrentThread(r); +} + +void TestMIDIPlatformService::ProcessMessages(const nsAString& aPortId) { + nsTArray<MIDIMessage> msgs; + GetMessagesBefore(aPortId, TimeStamp::Now(), msgs); + + for (MIDIMessage msg : msgs) { + // receiving message from test control port + if (aPortId == mControlOutputPort.id()) { + switch (msg.data()[0]) { + // Hit a note, get a test! + case 0x90: + switch (msg.data()[1]) { + // Echo data/timestamp back through output port + case 0x00: { + nsCOMPtr<nsIRunnable> r( + new ReceiveRunnable(mControlInputPort.id(), msg)); + mBackgroundThread->Dispatch(r, NS_DISPATCH_NORMAL); + break; + } + // Cause control test ports to connect + case 0x01: { + nsCOMPtr<nsIRunnable> r1( + new AddPortRunnable(mStateTestInputPort)); + mBackgroundThread->Dispatch(r1, NS_DISPATCH_NORMAL); + break; + } + // Cause control test ports to disconnect + case 0x02: { + nsCOMPtr<nsIRunnable> r1( + new RemovePortRunnable(mStateTestInputPort)); + mBackgroundThread->Dispatch(r1, NS_DISPATCH_NORMAL); + break; + } + // Test for packet timing + case 0x03: { + // Append a few echo command packets in reverse timing order, + // should come out in correct order on other end. + nsTArray<MIDIMessage> newMsgs; + nsTArray<uint8_t> msg; + msg.AppendElement(0x90); + msg.AppendElement(0x00); + msg.AppendElement(0x00); + // PR_Now() returns nanosecods, and we need a double with + // fractional milliseconds. + TimeStamp currentTime = TimeStamp::Now(); + for (int i = 0; i <= 5; ++i) { + // Insert messages with timestamps in reverse order, to make + // sure we're sorting correctly. + newMsgs.AppendElement(MIDIMessage( + msg, currentTime - TimeDuration::FromMilliseconds(i * 2))); + } + nsCOMPtr<nsIRunnable> r( + new QueueMessagesRunnable(aPortId, newMsgs)); + mBackgroundThread->Dispatch(r, NS_DISPATCH_NORMAL); + break; + } + default: + NS_WARNING("Unknown Test MIDI message received!"); + } + break; + // Sysex tests + case 0xF0: + switch (msg.data()[1]) { + // Echo data/timestamp back through output port + case 0x00: { + nsCOMPtr<nsIRunnable> r( + new ReceiveRunnable(mControlInputPort.id(), msg)); + mBackgroundThread->Dispatch(r, NS_DISPATCH_NORMAL); + break; + } + // Test for system real time messages in the middle of sysex + // messages. + case 0x01: { + nsTArray<uint8_t> msgs; + const uint8_t msg[] = {0xF0, 0x01, 0xF8, 0x02, 0x03, + 0x04, 0xF9, 0x05, 0xF7}; + // Can't use AppendElements on an array here, so just do range + // based loading. + for (auto& s : msg) { + msgs.AppendElement(s); + } + nsTArray<MIDIMessage> newMsgs; + MIDIUtils::ParseMessages(msgs, TimeStamp::Now(), newMsgs); + nsCOMPtr<nsIRunnable> r( + new ReceiveRunnable(mControlInputPort.id(), newMsgs)); + mBackgroundThread->Dispatch(r, NS_DISPATCH_NORMAL); + break; + } + default: + NS_WARNING("Unknown Test Sysex MIDI message received!"); + } + break; + } + } + } +} diff --git a/dom/midi/TestMIDIPlatformService.h b/dom/midi/TestMIDIPlatformService.h new file mode 100644 index 0000000000..984afce032 --- /dev/null +++ b/dom/midi/TestMIDIPlatformService.h @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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_TestMIDIPlatformService_h +#define mozilla_dom_TestMIDIPlatformService_h + +#include "mozilla/dom/MIDIPlatformService.h" +#include "mozilla/dom/MIDITypes.h" + +class nsIThread; + +namespace mozilla { +namespace dom { + +class MIDIPortInterface; + +/** + * Platform service implementation used for mochitests. Emulates what a real + * platform service should look like, including using an internal IO thread for + * message IO. + * + */ +class TestMIDIPlatformService : public MIDIPlatformService { + public: + TestMIDIPlatformService(); + virtual void Init() override; + virtual void Open(MIDIPortParent* aPort) override; + virtual void Stop() override; + virtual void ScheduleSend(const nsAString& aPort) override; + virtual void ScheduleClose(MIDIPortParent* aPort) override; + // MIDI Service simulation function. Can take specially formed sysex messages + // in order to trigger device connection events and state changes, + // interrupting messages for high priority sysex sends, etc... + void ProcessMessages(const nsAString& aPort); + + private: + virtual ~TestMIDIPlatformService(); + // Convenience object for sending runnables to the background thread. All + // runnables are pushed to the background thread, and check for existence of a + // manager object on the thread before running. + nsCOMPtr<nsIThread> mBackgroundThread; + // Port that takes test control messages + MIDIPortInfo mControlInputPort; + // Port that returns test status messages + MIDIPortInfo mControlOutputPort; + // Used for testing input connection/disconnection + MIDIPortInfo mStateTestInputPort; + // Used for testing output connection/disconnection + MIDIPortInfo mStateTestOutputPort; + // Used for testing open() call failures + MIDIPortInfo mAlwaysClosedTestOutputPort; + // IO Simulation thread. Runs all instances of ProcessMessages(). + nsCOMPtr<nsIThread> mClientThread; + // True if server has been brought up already. + bool mIsInitialized; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_TestMIDIPlatformService_h diff --git a/dom/midi/moz.build b/dom/midi/moz.build new file mode 100644 index 0000000000..11723dcfad --- /dev/null +++ b/dom/midi/moz.build @@ -0,0 +1,63 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +IPDL_SOURCES += [ + "MIDITypes.ipdlh", + "PMIDIManager.ipdl", + "PMIDIPort.ipdl", +] + +EXPORTS.mozilla.dom += [ + "MIDIAccess.h", + "MIDIAccessManager.h", + "MIDIInput.h", + "MIDIInputMap.h", + "MIDIManagerChild.h", + "MIDIManagerParent.h", + "MIDIMessageEvent.h", + "MIDIMessageQueue.h", + "MIDIOutput.h", + "MIDIOutputMap.h", + "MIDIPermissionRequest.h", + "MIDIPlatformRunnables.h", + "MIDIPlatformService.h", + "MIDIPort.h", + "MIDIPortChild.h", + "MIDIPortInterface.h", + "MIDIPortParent.h", + "MIDIUtils.h", +] + +UNIFIED_SOURCES = [ + "MIDIAccess.cpp", + "MIDIAccessManager.cpp", + "MIDIInput.cpp", + "MIDIInputMap.cpp", + "MIDIManagerChild.cpp", + "MIDIManagerParent.cpp", + "MIDIMessageEvent.cpp", + "MIDIMessageQueue.cpp", + "MIDIOutput.cpp", + "MIDIOutputMap.cpp", + "MIDIPermissionRequest.cpp", + "MIDIPlatformRunnables.cpp", + "MIDIPlatformService.cpp", + "MIDIPort.cpp", + "MIDIPortChild.cpp", + "MIDIPortInterface.cpp", + "MIDIPortParent.cpp", + "MIDIUtils.cpp", + "TestMIDIPlatformService.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "/dom/base", +] + +MOCHITEST_MANIFESTS += ["tests/mochitest.ini"] diff --git a/dom/midi/tests/.eslintrc.js b/dom/midi/tests/.eslintrc.js new file mode 100644 index 0000000000..845ed3f013 --- /dev/null +++ b/dom/midi/tests/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/mochitest-test"], +}; diff --git a/dom/midi/tests/MIDITestUtils.js b/dom/midi/tests/MIDITestUtils.js new file mode 100644 index 0000000000..4d516fc2e4 --- /dev/null +++ b/dom/midi/tests/MIDITestUtils.js @@ -0,0 +1,63 @@ +/* eslint-env mozilla/frame-script */ +var MIDITestUtils = { + permissionSetup: allow => { + let permPromiseRes; + let permPromise = new Promise((res, rej) => { + permPromiseRes = res; + }); + SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.webmidi.enabled", true], + ["midi.testing", true], + ["midi.prompt.testing", true], + ["media.navigator.permission.disabled", allow], + ], + }, + () => { + permPromiseRes(); + } + ); + return permPromise; + }, + // This list needs to stay synced with the ports in + // dom/midi/TestMIDIPlatformService. + inputInfo: { + id: "b744eebe-f7d8-499b-872b-958f63c8f522", + name: "Test Control MIDI Device Input Port", + manufacturer: "Test Manufacturer", + version: "1.0.0", + }, + outputInfo: { + id: "ab8e7fe8-c4de-436a-a960-30898a7c9a3d", + name: "Test Control MIDI Device Output Port", + manufacturer: "Test Manufacturer", + version: "1.0.0", + }, + stateTestInputInfo: { + id: "a9329677-8588-4460-a091-9d4a7f629a48", + name: "Test State MIDI Device Input Port", + manufacturer: "Test Manufacturer", + version: "1.0.0", + }, + stateTestOutputInfo: { + id: "478fa225-b5fc-4fa6-a543-d32d9cb651e7", + name: "Test State MIDI Device Output Port", + manufacturer: "Test Manufacturer", + version: "1.0.0", + }, + alwaysClosedTestOutputInfo: { + id: "f87d0c76-3c68-49a9-a44f-700f1125c07a", + name: "Always Closed MIDI Device Output Port", + manufacturer: "Test Manufacturer", + version: "1.0.0", + }, + checkPacket: (expected, actual) => { + if (expected.length != actual.length) { + ok(false, "Packet " + expected + " length not same as packet " + actual); + } + for (var i = 0; i < expected.length; ++i) { + is(expected[i], actual[i], "Packet value " + expected[i] + " matches."); + } + }, +}; diff --git a/dom/midi/tests/mochitest.ini b/dom/midi/tests/mochitest.ini new file mode 100644 index 0000000000..6682a30c9f --- /dev/null +++ b/dom/midi/tests/mochitest.ini @@ -0,0 +1,19 @@ +[DEFAULT] +support-files = + MIDITestUtils.js +scheme = https + + +[test_midi_permission_prompt.html] +[test_midi_permission_allow.html] +[test_midi_permission_deny.html] +[test_midi_device_enumeration.html] +[test_midi_device_implicit_open_close.html] +[test_midi_device_explicit_open_close.html] +[test_midi_device_sysex.html] +[test_midi_device_system_rt.html] +[test_midi_packet_timing_sorting.html] +[test_midi_device_connect_disconnect.html] +disabled = Bug 1437204 +[test_midi_device_pending.html] +disabled = Bug 1437204 diff --git a/dom/midi/tests/test_midi_device_connect_disconnect.html b/dom/midi/tests/test_midi_device_connect_disconnect.html new file mode 100644 index 0000000000..9b5143921d --- /dev/null +++ b/dom/midi/tests/test_midi_device_connect_disconnect.html @@ -0,0 +1,54 @@ +<html> + <head> + <title>WebMIDI Listener Test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="application/javascript" src="MIDITestUtils.js"></script> + </head> + + <body onload="runTests()"> + <script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + async function runTests() { + await MIDITestUtils.permissionSetup(true); + let output; + + let midi_access; + try { + midi_access = await navigator.requestMIDIAccess({ "sysex": false }); + ok(true, "MIDI Access Request successful"); + } catch (e) { + ok(false, "MIDI Access Request failed!"); + SimpleTest.finish(); + return; + } + is(midi_access.sysexEnabled, false, "Sysex should be false"); + output = midi_access.outputs.get(MIDITestUtils.outputInfo.id); + let statePromiseRes; + let statePromise = new Promise((res) => { statePromiseRes = res; }); + await output.open(); + let stateChangeHandler = (event) => { + if (event.port == output) { + return; + } + statePromiseRes(event.port); + }; + midi_access.addEventListener("statechange", stateChangeHandler); + // Send command to connect new port. + output.send([0x90, 0x01, 0x00]); + let p = await statePromise; + is(p.state, "connected", "Device " + p.name + " connected"); + + // Rebuild our promise, we'll need to await another one. + statePromise = new Promise((res) => { statePromiseRes = res; }); + output.send([0x90, 0x02, 0x00]); + p = await statePromise; + is(p.state, "disconnected", "Device " + p.name + " disconnected"); + midi_access.removeEventListener("statechange", stateChangeHandler); + SimpleTest.finish(); + } + </script> + </body> +</html> diff --git a/dom/midi/tests/test_midi_device_enumeration.html b/dom/midi/tests/test_midi_device_enumeration.html new file mode 100644 index 0000000000..0702228c73 --- /dev/null +++ b/dom/midi/tests/test_midi_device_enumeration.html @@ -0,0 +1,46 @@ +<html> + <head> + <title>WebMIDI Listener Test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="application/javascript" src="MIDITestUtils.js"></script> + </head> + + <body onload="runTests()"> + <script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + let objectCompare = (type, props, obj) => { + for (var prop in props) { + is(props[prop], obj[prop], type + " property value " + prop + " is " + props[prop]); + } + }; + let failOnCall = (event) => { + ok(false, "No connect/state events should be received on startup!"); + }; + async function runTests () { + await MIDITestUtils.permissionSetup(true); + // Request access without sysex. + let access = await navigator.requestMIDIAccess({ "sysex": false }); + ok(true, "MIDI Access Request successful"); + is(access.sysexEnabled, false, "Sysex should be false"); + access.addEventListener("statechange", failOnCall); + var input_id = MIDITestUtils.inputInfo.id; + var output_id = MIDITestUtils.outputInfo.id; + var inputs = access.inputs; + var outputs = access.outputs; + is(inputs.size, 1, "Should have one input"); + is(outputs.size, 2, "Should have two outputs"); + ok(inputs.has(input_id), "input list should contain input id"); + ok(outputs.has(output_id), "output list should contain output id"); + var input = access.inputs.get(input_id); + var output = access.outputs.get(output_id); + objectCompare("input", MIDITestUtils.inputInfo, input); + objectCompare("output", MIDITestUtils.outputInfo, output); + access.removeEventListener("statechange", failOnCall); + SimpleTest.finish(); + }; + </script> + </body> +</html> diff --git a/dom/midi/tests/test_midi_device_explicit_open_close.html b/dom/midi/tests/test_midi_device_explicit_open_close.html new file mode 100644 index 0000000000..db4e17f619 --- /dev/null +++ b/dom/midi/tests/test_midi_device_explicit_open_close.html @@ -0,0 +1,95 @@ +<html> + <head> + <title>WebMIDI Device Open/Close Test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="application/javascript" src="MIDITestUtils.js"></script> + </head> + + <body onload="runTests()"> + <script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + async function runTests() { + await MIDITestUtils.permissionSetup(true); + + let access; + try { + access = await navigator.requestMIDIAccess({ "sysex": false }) + } catch (e) { + ok(false, "MIDI Access Request Failed!"); + SimpleTest.finish(); + } + + ok(true, "MIDI Access Request successful"); + let input = access.inputs.get(MIDITestUtils.inputInfo.id); + let portEventRes; + let accessEventRes; + let portEventPromise = new Promise((resolve, reject) => { portEventRes = resolve; }); + let accessEventPromise = new Promise((resolve, reject) => { accessEventRes = resolve; }); + let shouldClose = false; + let checkPort = (event) => { + ok(input === event.port, "input port object and event port object are same object"); + ok(true, "port connection event fired"); + ok(event.port.connection === (!shouldClose ? "open" : "closed"), "connection registered correctly"); + }; + let inputEventHandler = (event) => { + checkPort(event); + portEventRes(); + }; + let accessEventHandler = (event) => { + checkPort(event); + accessEventRes(); + }; + input.addEventListener("statechange", inputEventHandler); + access.addEventListener("statechange", accessEventHandler); + await input.open(); + ok(true, "connection successful"); + ok(input.connection === "open", "connection registered as open"); + await Promise.all([portEventPromise, accessEventPromise]); + input.removeEventListener("statechange", inputEventHandler); + access.removeEventListener("statechange", accessEventHandler); + ok(true, "MIDI Port Open Test finished."); + ok(true, "Testing open failure"); + let out_access; + try { + out_access = await navigator.requestMIDIAccess({ "sysex": false }); + } catch (e) { + ok(false, "MIDI Access Request Failed!"); + SimpleTest.finish(); + } + let outputEventRes; + let outputEventHandler = (event) => { + ok(output_opened === event.port, "output port object and event port object are same object"); + ok(true, "access connection event fired"); + ok(event.port.connection === "closed", "connection registered as closed"); + }; + out_access.addEventListener("statechange", outputEventHandler); + let output_opened = out_access.outputs.get(MIDITestUtils.alwaysClosedTestOutputInfo.id); + try { + await output_opened.open(); + ok(false, "Should've failed to open port!"); + } catch(err) { + is(err.name, "InvalidAccessError", "error name " + err.name + " should be InvalidAccessError"); + ok(output_opened.connection == "closed", "connection registered as closed"); + ok(true, "Port not opened, test succeeded"); + } finally { + out_access.removeEventListener("statechange", outputEventHandler); + } + ok(true, "Starting MIDI port closing test"); + portEventPromise = new Promise((resolve, reject) => { portEventRes = resolve; }); + accessEventPromise = new Promise((resolve, reject) => { accessEventRes = resolve; }); + input.addEventListener("statechange", inputEventHandler); + access.addEventListener("statechange", accessEventHandler); + shouldClose = true; + await input.close(); + ok(input.connection === "closed", "connection registered as closed"); + await Promise.all([portEventPromise, accessEventPromise]); + input.removeEventListener("statechange", inputEventHandler); + access.removeEventListener("statechange", accessEventHandler); + SimpleTest.finish(); + } + </script> + </body> +</html> diff --git a/dom/midi/tests/test_midi_device_implicit_open_close.html b/dom/midi/tests/test_midi_device_implicit_open_close.html new file mode 100644 index 0000000000..f2c064dc6c --- /dev/null +++ b/dom/midi/tests/test_midi_device_implicit_open_close.html @@ -0,0 +1,67 @@ +<html> + <head> + <title>WebMIDI Listener Test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="application/javascript" src="MIDITestUtils.js"></script> + </head> + + <body onload="runTests()"> + <script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + async function runTests() { + await MIDITestUtils.permissionSetup(true); + let access = await navigator.requestMIDIAccess({ "sysex": false }); + ok(true, "MIDI Access Request successful"); + is(access.sysexEnabled, false, "Sysex should be false"); + + var checkCount = 0; + var reopened = false; + var input; + var output; + function checkCallbacks(port) { + ok(true, "Got port " + port.connection + " for " + port.name); + if (port.connection == "open") { + checkCount++; + } else { + if (!reopened) { + reopened = true; + // Ports are closed. Fire rest of tests. + input.onmidimessage = checkReturn; + output.send([0x90, 0x00, 0x7F]); + } + } + if (checkCount == 3) { + input.onstatechange = undefined; + output.onstatechange = undefined; + input.close(); + output.close(); + SimpleTest.finish(); + } + } + function checkReturn(event) { + checkCount++; + ok(true, "Got echo message back"); + MIDITestUtils.checkPacket(event.data, [0x90, 0x00, 0x7f]); + if (checkCount == 3) { + input.onstatechange = undefined; + output.onstatechange = undefined; + input.close(); + output.close(); + SimpleTest.finish(); + } + } + + input = access.inputs.get(MIDITestUtils.inputInfo.id); + output = access.outputs.get(MIDITestUtils.outputInfo.id); + // We automatically open ports, so close them first. + input.onstatechange = (event) => { checkCallbacks(event.port); }; + output.onstatechange = (event) => { checkCallbacks(event.port); }; + input.close(); + output.close(); + } + </script> + </body> +</html> diff --git a/dom/midi/tests/test_midi_device_pending.html b/dom/midi/tests/test_midi_device_pending.html new file mode 100644 index 0000000000..cc179cfdcc --- /dev/null +++ b/dom/midi/tests/test_midi_device_pending.html @@ -0,0 +1,122 @@ +<html> + <head> + <title>WebMIDI Listener Test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="application/javascript" src="MIDITestUtils.js"></script> + </head> + + <body onload="runTests()"> + <script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + async function runTests() { + await MIDITestUtils.permissionSetup(true); + + + var checkCount = 0; + var state = "connecting"; + var output; + var test_ports = []; + let access; + + let accessRes; + let accessRej; + let accessPromise; + let portRes; + let portRej; + let portPromise; + + function resetPromises() { + accessPromise = new Promise((res, rej) => { accessRes = res; accessRej = rej; }); + portPromise = new Promise((res, rej) => { portRes = res; portRej = rej; }); + } + + function accessStateChangeHandler(event) { + var p = event.port; + // We'll get an open event for the output control port. Ignore it. + if (p.name == MIDITestUtils.outputInfo.name) { + return; + } + accessRes(event); + } + + function portStateChangeHandler(event) { + var p = event.port; + // We'll get an open event for the output control port. Ignore it. + if (p.name == MIDITestUtils.outputInfo.name) { + return; + } + portRes(event); + } + + // Part 1: Create MIDIAccess object, attach state change listener to list for new connections + access = await navigator.requestMIDIAccess({ "sysex": false }); + ok(true, "MIDI Access Request successful"); + is(access.sysexEnabled, false, "Sysex should be false"); + access.addEventListener("statechange", accessStateChangeHandler); + + // Part 2: open test device, make sure it connects, attach event handler to device object + output = access.outputs.get(MIDITestUtils.outputInfo.id); + resetPromises(); + output.send([0x90, 0x01, 0x00]); + let accessEvent = await accessPromise; + let testPort = accessEvent.port; + test_ports.push(testPort); + testPort.addEventListener("statechange", portStateChangeHandler); + is(testPort.state, "connected", "Device " + testPort.name + " connected"); + + // Part 3: Listen for port status change on open as both an access event + // and a port event. + resetPromises(); + testPort.open(); + accessEvent = await accessPromise; + is(testPort.connection, "open", "Connection " + testPort.name + " opened"); + let portEvent = await portPromise; + is(testPort.connection, "open", "Connection " + testPort.name + " opened"); + + // Part 4: Disconnect port but don't close, check status to make sure we're pending. + resetPromises(); + output.send([0x90, 0x02, 0x00]); + accessEvent = await accessPromise; + is(testPort.connection, "pending", "Connection " + testPort.name + " pending"); + is(access.inputs.has(testPort.id), false, "port removed from input map while pending"); + portEvent = await portPromise; + is(testPort.connection, "pending", "Connection " + testPort.name + " pending"); + + // Part 5: Connect ports again, make sure we return to the right status. The events will + // fire because the device has been readded to the device maps in the access object. + resetPromises(); + output.send([0x90, 0x01, 0x00]); + accessEvent = await accessPromise; + var port = access.inputs.get(testPort.id); + is(port, accessEvent.port, "port in map and port in event should be the same"); + is(testPort.connection, "pending", "Connection " + testPort.name + " pending"); + portEvent = await portPromise; + is(testPort.connection, "pending", "Connection " + testPort.name + " pending"); + + // Part 6: Close out everything and clean up. + resetPromises(); + accessEvent = await accessPromise; + is(accessEvent.port.connection, "open", "Connection " + testPort.name + " opened"); + portEvent = await portPromise; + is(portEvent.port.connection, "open", "Connection " + testPort.name + " opened"); + + /* for (let port of test_ports) { + * port.removeEventListener("statechange", checkDevices); + * } + * access.removeEventListener("statechange", checkDevices);*/ + output.send([0x90, 0x02, 0x00]); + testPort.removeEventListener("statechange", portStateChangeHandler); + access.removeEventListener("statechange", accessStateChangeHandler); + access = undefined; + output = undefined; + testPort = undefined; + accessEvent = undefined; + portEvent = undefined; + SimpleTest.finish(); + } + </script> + </body> +</html> diff --git a/dom/midi/tests/test_midi_device_sysex.html b/dom/midi/tests/test_midi_device_sysex.html new file mode 100644 index 0000000000..60ee741e60 --- /dev/null +++ b/dom/midi/tests/test_midi_device_sysex.html @@ -0,0 +1,57 @@ +<html> + <head> + <title>WebMIDI Listener Test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="application/javascript" src="MIDITestUtils.js"></script> + </head> + + <body onload="runTests()"> + <script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + async function runTests() { + await MIDITestUtils.permissionSetup(true); + var sysexCheckCount = 0; + var checkCount = 0; + var input; + var output; + function checkSysexReceive(event) { + checkCount++; + sysexCheckCount++; + if (sysexCheckCount == 1) { + is(event.data[0], 0xF0, "Echoed sysex message via sysex port"); + } else { + is(event.data[0], 0x90, "Echoed regular message via sysex port"); + } + if (checkCount == 5) { + SimpleTest.finish(); + } + } + + function checkNoSysexReceive(event) { + checkCount++; + is(event.data[0], 0x90, "Echoed regular message via non-sysex port"); + if (checkCount == 5) { + SimpleTest.finish() + } + } + + // Request access without sysex. + let access_regular = await navigator.requestMIDIAccess({ "sysex": false }); + let access_sysex = await navigator.requestMIDIAccess({ "sysex": true }); + ok(true, "MIDI Access Request successful"); + ok(true, "Check for sysex message drop"); + input = access_regular.inputs.get(MIDITestUtils.inputInfo.id); + output = access_sysex.outputs.get(MIDITestUtils.outputInfo.id); + input_sysex = access_sysex.inputs.get(MIDITestUtils.inputInfo.id); + input_sysex.onmidimessage = checkSysexReceive; + input.onmidimessage = checkNoSysexReceive; + output.send([0xF0, 0x00, 0xF7]); + output.send([0x90, 0x00, 0x01]); + output.send([0x90, 0x00, 0x01]); + } + </script> + </body> +</html> diff --git a/dom/midi/tests/test_midi_device_system_rt.html b/dom/midi/tests/test_midi_device_system_rt.html new file mode 100644 index 0000000000..399f42f915 --- /dev/null +++ b/dom/midi/tests/test_midi_device_system_rt.html @@ -0,0 +1,39 @@ +<html> + <head> + <title>WebMIDI Listener Test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="application/javascript" src="MIDITestUtils.js"></script> + </head> + + <body onload="runTests()"> + <script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + async function runTests() { + await MIDITestUtils.permissionSetup(true); + var checkCount = 0; + + function checkReturn(msg) { + checkCount++; + if (checkCount == 1) { + MIDITestUtils.checkPacket(msg.data, [0xF8]); + } else if (checkCount == 2) { + MIDITestUtils.checkPacket(msg.data, [0xF9]); + } else if (checkCount == 3) { + MIDITestUtils.checkPacket(msg.data, [0xF0, 0x01, 0x02, 0x03, 0x04, 0x05, 0xF7]); + SimpleTest.finish(); + } + } + + // Request access without sysex. + let access_sysex = await navigator.requestMIDIAccess({ "sysex": true }); + let input_sysex = access_sysex.inputs.get(MIDITestUtils.inputInfo.id); + input_sysex.onmidimessage = checkReturn; + let output_sysex = access_sysex.outputs.get(MIDITestUtils.outputInfo.id); + output_sysex.send([0xF0, 0x01, 0xF7]); + } + </script> + </body> +</html> diff --git a/dom/midi/tests/test_midi_packet_timing_sorting.html b/dom/midi/tests/test_midi_packet_timing_sorting.html new file mode 100644 index 0000000000..904929e532 --- /dev/null +++ b/dom/midi/tests/test_midi_packet_timing_sorting.html @@ -0,0 +1,48 @@ +<html> + <head> + <title>WebMIDI Listener Test</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="application/javascript" src="MIDITestUtils.js"></script> + </head> + + <body onload="runTests()"> + <script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + async function runTests() { + await MIDITestUtils.permissionSetup(true); + await SpecialPowers.pushPrefEnv({"set": [["privacy.resistFingerprinting.reduceTimerPrecision.jitter", false]]}); + var checkCount = 0; + var lastTime = 0; + var reopened = false; + var input; + var output; + function checkReturn(event) { + ok(event.timeStamp > lastTime, "Received timestamp " + event.timeStamp + " should be greater than " + lastTime); + lastTime = event.timeStamp; + checkCount++; + + if (checkCount == 6) { + input.close(); + output.close(); + SimpleTest.finish(); + } + } + ok("Testing MIDI packet reordering based on timestamps"); + // Request access without sysex. + let access = await navigator.requestMIDIAccess({ "sysex": false }); + ok(true, "MIDI Access Request successful"); + is(access.sysexEnabled, false, "Sysex should be false"); + + input = access.inputs.get(MIDITestUtils.inputInfo.id); + output = access.outputs.get(MIDITestUtils.outputInfo.id); + input.onmidimessage = checkReturn; + // trigger the packet timing sorting tests + output.send([0x90, 0x03, 0x00], 0); + ok(true, "Waiting on packets"); + } + </script> + </body> +</html> diff --git a/dom/midi/tests/test_midi_permission_allow.html b/dom/midi/tests/test_midi_permission_allow.html new file mode 100644 index 0000000000..84578cfeae --- /dev/null +++ b/dom/midi/tests/test_midi_permission_allow.html @@ -0,0 +1,26 @@ +<html> + <head> + <title>WebMIDI Listener Test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="MIDITestUtils.js"></script> + </head> + + <body onload="runTests()"> + <script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + async function runTests() { + await MIDITestUtils.permissionSetup(true); + // Request access without sysex. + try { + await navigator.requestMIDIAccess({ "sysex": false }) + ok(true, "MIDI Access Request successful"); + SimpleTest.finish(); + } catch (ex) { + ok(false, "MIDI Access Request Failed!"); + SimpleTest.finish(); + } + } + </script> + </body> +</html> diff --git a/dom/midi/tests/test_midi_permission_deny.html b/dom/midi/tests/test_midi_permission_deny.html new file mode 100644 index 0000000000..8e3043a49a --- /dev/null +++ b/dom/midi/tests/test_midi_permission_deny.html @@ -0,0 +1,26 @@ +<html> + <head> + <title>WebMIDI Listener Test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="MIDITestUtils.js"></script> + </head> + + <body onload="runTests()"> + <script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + async function runTests() { + await MIDITestUtils.permissionSetup(false); + // Request access without sysex. + try { + await navigator.requestMIDIAccess({ "sysex": false }); + ok(false, "MIDI Access Request Deny failed"); + SimpleTest.finish(); + } catch (ex) { + ok(true, "MIDI Access Request Deny successful!"); + SimpleTest.finish(); + } + } + </script> + </body> +</html> diff --git a/dom/midi/tests/test_midi_permission_prompt.html b/dom/midi/tests/test_midi_permission_prompt.html new file mode 100644 index 0000000000..26a6b3d789 --- /dev/null +++ b/dom/midi/tests/test_midi_permission_prompt.html @@ -0,0 +1,24 @@ +<html> + <head> + <title>WebMIDI Listener Test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="MIDITestUtils.js"></script> + </head> + + <body onload="runTests()"> + <script class="testbody" type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + async function runTests() { + await MIDITestUtils.permissionSetup(true); + try { + await navigator.requestMIDIAccess({ "sysex": false }); + ok(true, "Prompting for permissions succeeded!"); + } catch (e) { + ok(false, "Prompting for permissions failed!"); + } + SimpleTest.finish(); + } + </script> + </body> +</html> |