summaryrefslogtreecommitdiffstats
path: root/dom/midi
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /dom/midi
parentInitial commit. (diff)
downloadfirefox-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 '')
-rw-r--r--dom/midi/MIDIAccess.cpp221
-rw-r--r--dom/midi/MIDIAccess.h116
-rw-r--r--dom/midi/MIDIAccessManager.cpp149
-rw-r--r--dom/midi/MIDIAccessManager.h75
-rw-r--r--dom/midi/MIDIInput.cpp63
-rw-r--r--dom/midi/MIDIInput.h53
-rw-r--r--dom/midi/MIDIInputMap.cpp29
-rw-r--r--dom/midi/MIDIInputMap.h40
-rw-r--r--dom/midi/MIDIManagerChild.cpp36
-rw-r--r--dom/midi/MIDIManagerChild.h39
-rw-r--r--dom/midi/MIDIManagerParent.cpp26
-rw-r--r--dom/midi/MIDIManagerParent.h36
-rw-r--r--dom/midi/MIDIMessageEvent.cpp106
-rw-r--r--dom/midi/MIDIMessageEvent.h64
-rw-r--r--dom/midi/MIDIMessageQueue.cpp73
-rw-r--r--dom/midi/MIDIMessageQueue.h58
-rw-r--r--dom/midi/MIDIOutput.cpp100
-rw-r--r--dom/midi/MIDIOutput.h52
-rw-r--r--dom/midi/MIDIOutputMap.cpp29
-rw-r--r--dom/midi/MIDIOutputMap.h43
-rw-r--r--dom/midi/MIDIPermissionRequest.cpp98
-rw-r--r--dom/midi/MIDIPermissionRequest.h48
-rw-r--r--dom/midi/MIDIPlatformRunnables.cpp49
-rw-r--r--dom/midi/MIDIPlatformRunnables.h127
-rw-r--r--dom/midi/MIDIPlatformService.cpp261
-rw-r--r--dom/midi/MIDIPlatformService.h162
-rw-r--r--dom/midi/MIDIPort.cpp197
-rw-r--r--dom/midi/MIDIPort.h97
-rw-r--r--dom/midi/MIDIPortChild.cpp57
-rw-r--r--dom/midi/MIDIPortChild.h52
-rw-r--r--dom/midi/MIDIPortInterface.cpp27
-rw-r--r--dom/midi/MIDIPortInterface.h50
-rw-r--r--dom/midi/MIDIPortParent.cpp108
-rw-r--r--dom/midi/MIDIPortParent.h51
-rw-r--r--dom/midi/MIDITypes.ipdlh31
-rw-r--r--dom/midi/MIDIUtils.cpp124
-rw-r--r--dom/midi/MIDIUtils.h29
-rw-r--r--dom/midi/PMIDIManager.ipdl25
-rw-r--r--dom/midi/PMIDIPort.ipdl31
-rw-r--r--dom/midi/TestMIDIPlatformService.cpp244
-rw-r--r--dom/midi/TestMIDIPlatformService.h64
-rw-r--r--dom/midi/moz.build63
-rw-r--r--dom/midi/tests/.eslintrc.js5
-rw-r--r--dom/midi/tests/MIDITestUtils.js63
-rw-r--r--dom/midi/tests/mochitest.ini19
-rw-r--r--dom/midi/tests/test_midi_device_connect_disconnect.html54
-rw-r--r--dom/midi/tests/test_midi_device_enumeration.html46
-rw-r--r--dom/midi/tests/test_midi_device_explicit_open_close.html95
-rw-r--r--dom/midi/tests/test_midi_device_implicit_open_close.html67
-rw-r--r--dom/midi/tests/test_midi_device_pending.html122
-rw-r--r--dom/midi/tests/test_midi_device_sysex.html57
-rw-r--r--dom/midi/tests/test_midi_device_system_rt.html39
-rw-r--r--dom/midi/tests/test_midi_packet_timing_sorting.html48
-rw-r--r--dom/midi/tests/test_midi_permission_allow.html26
-rw-r--r--dom/midi/tests/test_midi_permission_deny.html26
-rw-r--r--dom/midi/tests/test_midi_permission_prompt.html24
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>