summaryrefslogtreecommitdiffstats
path: root/dom/midi
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/midi/AlsaCompatibility.cpp32
-rw-r--r--dom/midi/MIDIAccess.cpp250
-rw-r--r--dom/midi/MIDIAccess.h118
-rw-r--r--dom/midi/MIDIAccessManager.cpp176
-rw-r--r--dom/midi/MIDIAccessManager.h76
-rw-r--r--dom/midi/MIDIInput.cpp101
-rw-r--r--dom/midi/MIDIInput.h54
-rw-r--r--dom/midi/MIDIInputMap.cpp29
-rw-r--r--dom/midi/MIDIInputMap.h46
-rw-r--r--dom/midi/MIDILog.cpp46
-rw-r--r--dom/midi/MIDILog.h26
-rw-r--r--dom/midi/MIDIManagerChild.cpp29
-rw-r--r--dom/midi/MIDIManagerChild.h36
-rw-r--r--dom/midi/MIDIManagerParent.cpp34
-rw-r--r--dom/midi/MIDIManagerParent.h34
-rw-r--r--dom/midi/MIDIMessageEvent.cpp95
-rw-r--r--dom/midi/MIDIMessageEvent.h62
-rw-r--r--dom/midi/MIDIMessageQueue.cpp73
-rw-r--r--dom/midi/MIDIMessageQueue.h58
-rw-r--r--dom/midi/MIDIOutput.cpp102
-rw-r--r--dom/midi/MIDIOutput.h50
-rw-r--r--dom/midi/MIDIOutputMap.cpp29
-rw-r--r--dom/midi/MIDIOutputMap.h49
-rw-r--r--dom/midi/MIDIPermissionRequest.cpp202
-rw-r--r--dom/midi/MIDIPermissionRequest.h52
-rw-r--r--dom/midi/MIDIPlatformRunnables.cpp49
-rw-r--r--dom/midi/MIDIPlatformRunnables.h125
-rw-r--r--dom/midi/MIDIPlatformService.cpp279
-rw-r--r--dom/midi/MIDIPlatformService.h174
-rw-r--r--dom/midi/MIDIPort.cpp283
-rw-r--r--dom/midi/MIDIPort.h126
-rw-r--r--dom/midi/MIDIPortChild.cpp59
-rw-r--r--dom/midi/MIDIPortChild.h50
-rw-r--r--dom/midi/MIDIPortInterface.cpp26
-rw-r--r--dom/midi/MIDIPortInterface.h48
-rw-r--r--dom/midi/MIDIPortParent.cpp106
-rw-r--r--dom/midi/MIDIPortParent.h49
-rw-r--r--dom/midi/MIDITypes.ipdlh31
-rw-r--r--dom/midi/MIDIUtils.cpp172
-rw-r--r--dom/midi/MIDIUtils.h27
-rw-r--r--dom/midi/PMIDIManager.ipdl24
-rw-r--r--dom/midi/PMIDIPort.ipdl29
-rw-r--r--dom/midi/TestMIDIPlatformService.cpp258
-rw-r--r--dom/midi/TestMIDIPlatformService.h61
-rw-r--r--dom/midi/crashtests/1851829.html31
-rw-r--r--dom/midi/crashtests/crashtests.list1
-rw-r--r--dom/midi/midirMIDIPlatformService.cpp193
-rw-r--r--dom/midi/midirMIDIPlatformService.h64
-rw-r--r--dom/midi/midir_impl/Cargo.toml14
-rw-r--r--dom/midi/midir_impl/cbindgen.toml18
-rw-r--r--dom/midi/midir_impl/moz.build17
-rw-r--r--dom/midi/midir_impl/src/lib.rs418
-rw-r--r--dom/midi/moz.build76
-rw-r--r--dom/midi/tests/MIDITestUtils.js94
-rw-r--r--dom/midi/tests/blank.html5
-rw-r--r--dom/midi/tests/browser.toml23
-rw-r--r--dom/midi/tests/browser_midi_permission_gated.js839
-rw-r--r--dom/midi/tests/browser_refresh_port_list.js71
-rw-r--r--dom/midi/tests/browser_stable_midi_port_ids.js52
-rw-r--r--dom/midi/tests/file_midi_permission_gated.html15
-rw-r--r--dom/midi/tests/mochitest.toml41
-rw-r--r--dom/midi/tests/port_ids_page_1.html17
-rw-r--r--dom/midi/tests/port_ids_page_2.html17
-rw-r--r--dom/midi/tests/refresh_port_list.html49
-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.html94
-rw-r--r--dom/midi/tests/test_midi_device_implicit_open_close.html54
-rw-r--r--dom/midi/tests/test_midi_device_pending.html118
-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_message_event.html45
-rw-r--r--dom/midi/tests/test_midi_packet_timing_sorting.html47
-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_gated.html181
-rw-r--r--dom/midi/tests/test_midi_permission_prompt.html24
-rw-r--r--dom/midi/tests/test_midi_send_messages.html112
78 files changed, 6813 insertions, 0 deletions
diff --git a/dom/midi/AlsaCompatibility.cpp b/dom/midi/AlsaCompatibility.cpp
new file mode 100644
index 0000000000..9e219d4999
--- /dev/null
+++ b/dom/midi/AlsaCompatibility.cpp
@@ -0,0 +1,32 @@
+/* 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/Assertions.h"
+
+// The code in this file is a workaround for building with ALSA versions prior
+// to 1.1. Various functions that the alsa crate (a dependency of the midir
+// crate) use are missing from those versions. The functions are not actually
+// used so we provide dummy implementations that return an error. This file
+// can be safely removed when the Linux sysroot will be updated to Debian 9
+// (or higher)
+#include <alsa/asoundlib.h>
+
+extern "C" {
+
+#define ALSA_DIVERT(func) \
+ int func(void) { \
+ MOZ_CRASH(#func "should never be called."); \
+ return -1; \
+ }
+
+#if (SND_LIB_MAJOR == 1) && (SND_LIB_MINOR == 0) && (SND_LIB_SUBMINOR < 29)
+ALSA_DIVERT(snd_pcm_sw_params_set_tstamp_type)
+ALSA_DIVERT(snd_pcm_sw_params_get_tstamp_type)
+#endif
+
+#if (SND_LIB_MAJOR == 1) && (SND_LIB_MINOR < 1)
+ALSA_DIVERT(snd_pcm_hw_params_supports_audio_ts_type)
+ALSA_DIVERT(snd_pcm_status_set_audio_htstamp_config)
+#endif
+}
diff --git a/dom/midi/MIDIAccess.cpp b/dom/midi/MIDIAccess.cpp
new file mode 100644
index 0000000000..fa8ae514c2
--- /dev/null
+++ b/dom/midi/MIDIAccess.cpp
@@ -0,0 +1,250 @@
+/* -*- 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 "ipc/IPCMessageUtils.h"
+#include "MIDILog.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* aAccessPromise)
+ : DOMEventTargetHelper(aWindow),
+ mInputMap(new MIDIInputMap(aWindow)),
+ mOutputMap(new MIDIOutputMap(aWindow)),
+ mSysexEnabled(aSysexEnabled),
+ mAccessPromise(aAccessPromise),
+ mHasShutdown(false) {
+ MOZ_ASSERT(aWindow);
+ MOZ_ASSERT(aAccessPromise);
+ KeepAliveIfHasListenersFor(nsGkAtoms::onstatechange);
+}
+
+MIDIAccess::~MIDIAccess() { Shutdown(); }
+
+void MIDIAccess::Shutdown() {
+ LOG("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 && mInputMap->Has(id)) {
+ MIDIInputMap_Binding::MaplikeHelpers::Delete(mInputMap, aPort->StableId(),
+ rv);
+ mInputMap->Remove(id);
+ } else if (aPort->Type() == MIDIPortType::Output && mOutputMap->Has(id)) {
+ MIDIOutputMap_Binding::MaplikeHelpers::Delete(mOutputMap,
+ aPort->StableId(), rv);
+ mOutputMap->Remove(id);
+ }
+ // Check to make sure Has()/Delete() calls haven't failed.
+ if (NS_WARN_IF(rv.Failed())) {
+ LOG("Inconsistency during FireConnectionEvent");
+ 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 && !mInputMap->Has(id)) {
+ if (NS_WARN_IF(rv.Failed())) {
+ LOG("Input port not found");
+ return;
+ }
+ MIDIInputMap_Binding::MaplikeHelpers::Set(
+ mInputMap, aPort->StableId(), *(static_cast<MIDIInput*>(aPort)), rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ LOG("Map Set failed for input port");
+ return;
+ }
+ mInputMap->Insert(id, aPort);
+ } else if (aPort->Type() == MIDIPortType::Output && mOutputMap->Has(id)) {
+ if (NS_WARN_IF(rv.Failed())) {
+ LOG("Output port not found");
+ return;
+ }
+ MIDIOutputMap_Binding::MaplikeHelpers::Set(
+ mOutputMap, aPort->StableId(), *(static_cast<MIDIOutput*>(aPort)),
+ rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ LOG("Map set failed for output port");
+ return;
+ }
+ mOutputMap->Insert(id, aPort);
+ }
+ }
+ 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) {
+ if (mInputMap->Has(id) || 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)) {
+ LOG("Couldn't create input port");
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ MIDIInputMap_Binding::MaplikeHelpers::Set(
+ mInputMap, port->StableId(), *(static_cast<MIDIInput*>(port.get())),
+ aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ LOG("Coudld't set input port in map");
+ return;
+ }
+ mInputMap->Insert(id, port);
+ } else if (type == MIDIPortType::Output) {
+ if (mOutputMap->Has(id) || 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)) {
+ LOG("Couldn't create output port");
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+ MIDIOutputMap_Binding::MaplikeHelpers::Set(
+ mOutputMap, port->StableId(), *(static_cast<MIDIOutput*>(port.get())),
+ aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ LOG("Coudld't set output port in map");
+ return;
+ }
+ mOutputMap->Insert(id, port);
+ } 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) {
+ LOG("MIDIAcess::Notify");
+ if (!GetOwner()) {
+ // Do nothing if we've already been disconnected from the document.
+ return;
+ }
+
+ for (const auto& port : aEvent.ports()) {
+ // Something went very wrong. Warn and return.
+ ErrorResult rv;
+ MaybeCreateMIDIPort(port, rv);
+ if (rv.Failed()) {
+ if (!mAccessPromise) {
+ // We can't reject the promise so let's suppress the error instead
+ rv.SuppressException();
+ 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);
+}
+
+void MIDIAccess::DisconnectFromOwner() {
+ IgnoreKeepAliveIfHasListenersFor(nsGkAtoms::onstatechange);
+
+ DOMEventTargetHelper::DisconnectFromOwner();
+ MIDIAccessManager::Get()->SendRefresh();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/midi/MIDIAccess.h b/dom/midi/MIDIAccess.h
new file mode 100644
index 0000000000..9b3a860f02
--- /dev/null
+++ b/dom/midi/MIDIAccess.h
@@ -0,0 +1,118 @@
+/* -*- 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;
+
+using MIDIAccessDestructionObserver = Observer<void_t>;
+
+/**
+ * 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* aObs);
+
+ // 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);
+
+ void DisconnectFromOwner() override;
+
+ 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..c0556f1ffd
--- /dev/null
+++ b/dom/midi/MIDIAccessManager.cpp
@@ -0,0 +1,176 @@
+/* -*- 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/Endpoint.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/StaticPrefs_midi.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;
+ }
+
+#ifndef MOZ_WEBMIDI_MIDIR_IMPL
+ if (!StaticPrefs::midi_testing()) {
+ // If we don't have a MIDI implementation and testing is disabled we can't
+ // allow accessing WebMIDI. However we don't want to return something
+ // different from a normal rejection because we don't want websites to use
+ // the error as a way to fingerprint users, so we throw a security error
+ // as if the request had been rejected by the user.
+ aRv.ThrowSecurityError("Access not allowed");
+ return nullptr;
+ }
+#endif
+
+ 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 (!mChild) {
+ StartActor();
+ } else {
+ mChild->SendRefresh();
+ }
+
+ return true;
+}
+
+// Sets up the actor to talk to the parent.
+//
+// We Bootstrap the actor manually rather than using a constructor so that
+// we can bind the parent endpoint to a dedicated task queue.
+void MIDIAccessManager::StartActor() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mChild);
+
+ // Grab PBackground.
+ ::mozilla::ipc::PBackgroundChild* pBackground =
+ BackgroundChild::GetOrCreateForCurrentThread();
+
+ // Create the endpoints and bind the one on the child side.
+ Endpoint<PMIDIManagerParent> parentEndpoint;
+ Endpoint<PMIDIManagerChild> childEndpoint;
+ MOZ_ALWAYS_SUCCEEDS(
+ PMIDIManager::CreateEndpoints(&parentEndpoint, &childEndpoint));
+ mChild = new MIDIManagerChild();
+ MOZ_ALWAYS_TRUE(childEndpoint.Bind(mChild));
+
+ // Kick over to the parent to connect things over there.
+ pBackground->SendCreateMIDIManager(std::move(parentEndpoint));
+}
+
+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::SendRefresh() {
+ if (mChild) {
+ mChild->SendRefresh();
+ }
+}
+
+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..606c73d268
--- /dev/null
+++ b/dom/midi/MIDIAccessManager.h
@@ -0,0 +1,76 @@
+/* -*- 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::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& aPortList);
+ // 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);
+ // Requests the service to update the list of devices
+ void SendRefresh();
+
+ private:
+ MIDIAccessManager();
+ ~MIDIAccessManager();
+ void StartActor();
+ // 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 mozilla::dom
+
+#endif // mozilla_dom_MIDIAccessManager_h
diff --git a/dom/midi/MIDIInput.cpp b/dom/midi/MIDIInput.cpp
new file mode 100644
index 0000000000..b9202ac578
--- /dev/null
+++ b/dom/midi/MIDIInput.cpp
@@ -0,0 +1,101 @@
+/* -*- 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 "MIDILog.h"
+
+namespace mozilla::dom {
+
+MIDIInput::MIDIInput(nsPIDOMWindowInner* aWindow)
+ : MIDIPort(aWindow), mKeepAlive(false) {}
+
+// static
+RefPtr<MIDIInput> MIDIInput::Create(nsPIDOMWindowInner* aWindow,
+ MIDIAccess* aMIDIAccessParent,
+ const MIDIPortInfo& aPortInfo,
+ const bool aSysexEnabled) {
+ MOZ_ASSERT(static_cast<MIDIPortType>(aPortInfo.type()) ==
+ MIDIPortType::Input);
+ RefPtr<MIDIInput> port = new MIDIInput(aWindow);
+ if (!port->Initialize(aPortInfo, aSysexEnabled, aMIDIAccessParent)) {
+ 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) {
+ if (!GetOwner()) {
+ return; // Ignore messages once we've been disconnected from the owner
+ }
+
+ nsCOMPtr<Document> doc = GetOwner()->GetDoc();
+ if (!doc) {
+ NS_WARNING("No document available to send MIDIMessageEvent to!");
+ return;
+ }
+ for (const auto& msg : aMsgs) {
+ RefPtr<MIDIMessageEvent> event(
+ MIDIMessageEvent::Constructor(this, msg.timestamp(), msg.data()));
+ DispatchTrustedEvent(event);
+ }
+}
+
+void MIDIInput::StateChange() {
+ if (Port()->ConnectionState() == MIDIPortConnectionState::Open ||
+ (Port()->DeviceState() == MIDIPortDeviceState::Connected &&
+ Port()->ConnectionState() == MIDIPortConnectionState::Pending)) {
+ KeepAliveOnMidimessage();
+ } else {
+ DontKeepAliveOnMidimessage();
+ }
+}
+
+void MIDIInput::EventListenerAdded(nsAtom* aType) {
+ if (aType == nsGkAtoms::onmidimessage) {
+ // HACK: the Web MIDI spec states that we should open a port only when
+ // setting the midimessage event handler but Chrome does it even when
+ // adding event listeners hence this.
+ if (Port()->ConnectionState() != MIDIPortConnectionState::Open) {
+ LOG("onmidimessage event listener added, sending implicit Open");
+ Port()->SendOpen();
+ }
+ }
+
+ DOMEventTargetHelper::EventListenerAdded(aType);
+}
+
+void MIDIInput::DisconnectFromOwner() {
+ DontKeepAliveOnMidimessage();
+
+ MIDIPort::DisconnectFromOwner();
+}
+
+void MIDIInput::KeepAliveOnMidimessage() {
+ if (!mKeepAlive) {
+ mKeepAlive = true;
+ KeepAliveIfHasListenersFor(nsGkAtoms::onmidimessage);
+ }
+}
+
+void MIDIInput::DontKeepAliveOnMidimessage() {
+ if (mKeepAlive) {
+ IgnoreKeepAliveIfHasListenersFor(nsGkAtoms::onmidimessage);
+ mKeepAlive = false;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/midi/MIDIInput.h b/dom/midi/MIDIInput.h
new file mode 100644
index 0000000000..72458457a0
--- /dev/null
+++ b/dom/midi/MIDIInput.h
@@ -0,0 +1,54 @@
+/* -*- 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::dom {
+
+class MIDIPortInfo;
+
+/**
+ * Represents a MIDI Input Port, handles generating incoming message events.
+ *
+ */
+class MIDIInput final : public MIDIPort {
+ public:
+ static RefPtr<MIDIInput> Create(nsPIDOMWindowInner* aWindow,
+ MIDIAccess* aMIDIAccessParent,
+ const MIDIPortInfo& aPortInfo,
+ const bool aSysexEnabled);
+ ~MIDIInput() = default;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ IMPL_EVENT_HANDLER(midimessage);
+
+ void StateChange() override;
+ void EventListenerAdded(nsAtom* aType) override;
+ void DisconnectFromOwner() override;
+
+ private:
+ explicit MIDIInput(nsPIDOMWindowInner* aWindow);
+ // Takes an array of IPC MIDIMessage objects and turns them into
+ // MIDIMessageEvents, which it then fires.
+ void Receive(const nsTArray<MIDIMessage>& aMsgs) override;
+
+ void KeepAliveOnMidimessage();
+ void DontKeepAliveOnMidimessage();
+
+ // If true this object will be kept alive even without direct JS references
+ bool mKeepAlive;
+};
+
+} // namespace mozilla::dom
+
+#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..5826290dbc
--- /dev/null
+++ b/dom/midi/MIDIInputMap.h
@@ -0,0 +1,46 @@
+/* -*- 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 "mozilla/dom/MIDIPort.h"
+#include "nsCOMPtr.h"
+#include "nsTHashMap.h"
+#include "nsWrapperCache.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla::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_WRAPPERCACHE_CLASS(MIDIInputMap)
+ nsPIDOMWindowInner* GetParentObject() const { return mParent; }
+
+ explicit MIDIInputMap(nsPIDOMWindowInner* aParent);
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ bool Has(nsAString& aId) { return mPorts.Get(aId) != nullptr; }
+ void Insert(nsAString& aId, RefPtr<MIDIPort> aPort) {
+ mPorts.InsertOrUpdate(aId, aPort);
+ }
+ void Remove(nsAString& aId) { mPorts.Remove(aId); }
+
+ private:
+ ~MIDIInputMap() = default;
+ nsTHashMap<nsString, RefPtr<MIDIPort>> mPorts;
+ nsCOMPtr<nsPIDOMWindowInner> mParent;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_MIDIInputMap_h
diff --git a/dom/midi/MIDILog.cpp b/dom/midi/MIDILog.cpp
new file mode 100644
index 0000000000..cc9945ed58
--- /dev/null
+++ b/dom/midi/MIDILog.cpp
@@ -0,0 +1,46 @@
+/* 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 "MIDILog.h"
+
+#include "mozilla/dom/MIDITypes.h"
+#include "mozilla/dom/MIDIPortBinding.h"
+
+mozilla::LazyLogModule gWebMIDILog("WebMIDI");
+
+void LogMIDIMessage(const mozilla::dom::MIDIMessage& aMessage,
+ const nsAString& aPortId,
+ mozilla::dom::MIDIPortType aDirection) {
+ if (MOZ_LOG_TEST(gWebMIDILog, mozilla::LogLevel::Debug)) {
+ if (MOZ_LOG_TEST(gWebMIDILog, mozilla::LogLevel::Verbose)) {
+ uint32_t byteCount = aMessage.data().Length();
+ nsAutoCString logMessage;
+ // Log long messages inline with the timestamp and the length, log
+ // longer messages a bit like xxd
+ logMessage.AppendPrintf(
+ "%s %s length=%u", NS_ConvertUTF16toUTF8(aPortId).get(),
+ aDirection == mozilla::dom::MIDIPortType::Input ? "->" : "<-",
+ byteCount);
+
+ if (byteCount <= 3) {
+ logMessage.AppendPrintf(" [");
+ // Regular messages
+ for (uint32_t i = 0; i < byteCount - 1; i++) {
+ logMessage.AppendPrintf("%x ", aMessage.data()[i]);
+ }
+ logMessage.AppendPrintf("%x]", aMessage.data()[byteCount - 1]);
+ } else {
+ // Longer messages
+ for (uint32_t i = 0; i < byteCount; i++) {
+ if (!(i % 8)) {
+ logMessage.AppendPrintf("\n%08u:\t", i);
+ }
+ logMessage.AppendPrintf("%x ", aMessage.data()[i]);
+ }
+ }
+ MOZ_LOG(gWebMIDILog, mozilla::LogLevel::Verbose,
+ ("%s", logMessage.get()));
+ }
+ }
+}
diff --git a/dom/midi/MIDILog.h b/dom/midi/MIDILog.h
new file mode 100644
index 0000000000..ee2cae3dfc
--- /dev/null
+++ b/dom/midi/MIDILog.h
@@ -0,0 +1,26 @@
+/* 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_MIDILog_h
+#define mozilla_dom_MIDILog_h
+
+#include <mozilla/Logging.h>
+#include <nsStringFwd.h>
+
+namespace mozilla::dom {
+class MIDIMessage;
+enum class MIDIPortType : uint8_t;
+} // namespace mozilla::dom
+
+extern mozilla::LazyLogModule gWebMIDILog;
+
+#define LOG(...) MOZ_LOG(gWebMIDILog, mozilla::LogLevel::Debug, (__VA_ARGS__));
+#define LOGV(x, ...) \
+ MOZ_LOG(gWebMIDILog, mozilla::LogLevel::Verbose, (__VA_ARGS__));
+
+void LogMIDIMessage(const mozilla::dom::MIDIMessage& aMessage,
+ const nsAString& aPortId,
+ mozilla::dom::MIDIPortType aDirection);
+
+#endif // mozilla_dom_MIDILog_h
diff --git a/dom/midi/MIDIManagerChild.cpp b/dom/midi/MIDIManagerChild.cpp
new file mode 100644
index 0000000000..cb0dd6a793
--- /dev/null
+++ b/dom/midi/MIDIManagerChild.cpp
@@ -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 "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::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..610105de9c
--- /dev/null
+++ b/dom/midi/MIDIManagerChild.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_MIDIManagerChild_h
+#define mozilla_dom_MIDIManagerChild_h
+
+#include "mozilla/dom/PMIDIManagerChild.h"
+
+namespace mozilla::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 Shutdown();
+
+ private:
+ ~MIDIManagerChild() = default;
+ bool mShutdown;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_MIDIManagerChild_h
diff --git a/dom/midi/MIDIManagerParent.cpp b/dom/midi/MIDIManagerParent.cpp
new file mode 100644
index 0000000000..ef574f5b04
--- /dev/null
+++ b/dom/midi/MIDIManagerParent.cpp
@@ -0,0 +1,34 @@
+/* -*- 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) {
+ if (MIDIPlatformService::IsRunning()) {
+ MIDIPlatformService::Get()->RemoveManager(this);
+ }
+}
+
+mozilla::ipc::IPCResult MIDIManagerParent::RecvRefresh() {
+ MIDIPlatformService::Get()->Refresh();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MIDIManagerParent::RecvShutdown() {
+ // The two-step shutdown process here is the standard way to ensure that the
+ // child receives any messages sent by the server (since either sending or
+ // receiving __delete__ prevents any further messages from being received).
+ // This was necessary before bug 1547085 when discarded messages would
+ // trigger a crash, and is probably unnecessary now, but we leave it in place
+ // just in case.
+ Close();
+ return IPC_OK();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/midi/MIDIManagerParent.h b/dom/midi/MIDIManagerParent.h
new file mode 100644
index 0000000000..a8ff0648da
--- /dev/null
+++ b/dom/midi/MIDIManagerParent.h
@@ -0,0 +1,34 @@
+/* -*- 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::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_THREADSAFE_REFCOUNTING(MIDIManagerParent, override)
+ MIDIManagerParent() = default;
+ mozilla::ipc::IPCResult RecvRefresh();
+ mozilla::ipc::IPCResult RecvShutdown();
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ private:
+ ~MIDIManagerParent() = default;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_MIDIManagerParent_h
diff --git a/dom/midi/MIDIMessageEvent.cpp b/dom/midi/MIDIMessageEvent.cpp
new file mode 100644
index 0000000000..793cb4eeca
--- /dev/null
+++ b/dom/midi/MIDIMessageEvent.cpp
@@ -0,0 +1,95 @@
+/* -*- 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 "js/Realm.h"
+#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_INHERITED_WITH_JS_MEMBERS(MIDIMessageEvent, Event, (),
+ (mData))
+
+NS_IMPL_ADDREF_INHERITED(MIDIMessageEvent, Event)
+NS_IMPL_RELEASE_INHERITED(MIDIMessageEvent, Event)
+
+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() { 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.
+ if (aEventInitDict.mData.WasPassed()) {
+ JSAutoRealm ar(aGlobal.Context(), aGlobal.Get());
+ JS::Rooted<JSObject*> data(aGlobal.Context(),
+ aEventInitDict.mData.Value().Obj());
+ e->mData = JS_NewUint8ArrayFromArray(aGlobal.Context(), 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, aRv);
+ if (aRv.Failed()) {
+ 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..d5bc0b710a
--- /dev/null
+++ b/dom/midi/MIDIMessageEvent.h
@@ -0,0 +1,62 @@
+/* -*- 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::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 mozilla::dom
+
+#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..cadfc1b249
--- /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>& aMsgQueue);
+ // Get all pending messages.
+ void GetMessages(nsTArray<MIDIMessage>& aMsgQueue);
+ // 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 MOZ_UNANNOTATED;
+};
+
+} // 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..3cdd7d308d
--- /dev/null
+++ b/dom/midi/MIDIOutput.cpp
@@ -0,0 +1,102 @@
+/* -*- 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) : MIDIPort(aWindow) {}
+
+// static
+RefPtr<MIDIOutput> MIDIOutput::Create(nsPIDOMWindowInner* aWindow,
+ MIDIAccess* aMIDIAccessParent,
+ const MIDIPortInfo& aPortInfo,
+ const bool aSysexEnabled) {
+ MOZ_ASSERT(static_cast<MIDIPortType>(aPortInfo.type()) ==
+ MIDIPortType::Output);
+ RefPtr<MIDIOutput> port = new MIDIOutput(aWindow);
+ if (NS_WARN_IF(
+ !port->Initialize(aPortInfo, aSysexEnabled, aMIDIAccessParent))) {
+ 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 (Port()->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;
+ bool ret = MIDIUtils::ParseMessages(aData, timestamp, msgArray);
+ if (!ret) {
+ aRv.ThrowTypeError("Invalid MIDI message");
+ return;
+ }
+
+ 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;
+ }
+ }
+ }
+ Port()->SendSend(msgArray);
+}
+
+void MIDIOutput::Clear() {
+ if (Port()->ConnectionState() == MIDIPortConnectionState::Closed) {
+ return;
+ }
+ Port()->SendClear();
+}
diff --git a/dom/midi/MIDIOutput.h b/dom/midi/MIDIOutput.h
new file mode 100644
index 0000000000..7c39d7a4a9
--- /dev/null
+++ b/dom/midi/MIDIOutput.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_MIDIOutput_h
+#define mozilla_dom_MIDIOutput_h
+
+#include "mozilla/dom/MIDIPort.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 RefPtr<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:
+ explicit MIDIOutput(nsPIDOMWindowInner* aWindow);
+};
+
+} // 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..2e33e8b2a7
--- /dev/null
+++ b/dom/midi/MIDIOutputMap.h
@@ -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/. */
+
+#ifndef mozilla_dom_MIDIOutputMap_h
+#define mozilla_dom_MIDIOutputMap_h
+
+#include "mozilla/dom/MIDIPort.h"
+#include "nsCOMPtr.h"
+#include "nsTHashMap.h"
+#include "nsWrapperCache.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla::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_WRAPPERCACHE_CLASS(MIDIOutputMap)
+
+ explicit MIDIOutputMap(nsPIDOMWindowInner* aParent);
+
+ nsPIDOMWindowInner* GetParentObject() const { return mParent; }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ bool Has(nsAString& aId) { return mPorts.Get(aId) != nullptr; }
+ void Insert(nsAString& aId, RefPtr<MIDIPort> aPort) {
+ mPorts.InsertOrUpdate(aId, aPort);
+ }
+ void Remove(nsAString& aId) { mPorts.Remove(aId); }
+
+ private:
+ ~MIDIOutputMap() = default;
+ nsTHashMap<nsString, RefPtr<MIDIPort>> mPorts;
+ nsCOMPtr<nsPIDOMWindowInner> mParent;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_MIDIOutputMap_h
diff --git a/dom/midi/MIDIPermissionRequest.cpp b/dom/midi/MIDIPermissionRequest.cpp
new file mode 100644
index 0000000000..1eed95f177
--- /dev/null
+++ b/dom/midi/MIDIPermissionRequest.cpp
@@ -0,0 +1,202 @@
+/* -*- 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 "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/RandomNum.h"
+#include "mozilla/StaticPrefs_dom.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;
+
+ // The previous implementation made no differences between midi and
+ // midi-sysex. The check on the SitePermsAddonProvider pref should be removed
+ // at the same time as the old implementation.
+ if (mNeedsSysex || !StaticPrefs::dom_sitepermsaddon_provider_enabled()) {
+ options.AppendElement(u"sysex"_ns);
+ }
+ return nsContentPermissionUtils::CreatePermissionArray(mType, options,
+ aTypes);
+}
+
+NS_IMETHODIMP
+MIDIPermissionRequest::Cancel() {
+ mCancelTimer = nullptr;
+
+ if (StaticPrefs::dom_sitepermsaddon_provider_enabled()) {
+ mPromise->MaybeRejectWithSecurityError(
+ "WebMIDI requires a site permission add-on to activate");
+ } else {
+ // This message is used for the initial XPIProvider-based implementation
+ // of Site Permissions.
+ // It should be removed as part of Bug 1789718.
+ mPromise->MaybeRejectWithSecurityError(
+ "WebMIDI requires a site permission add-on to activate — see "
+ "https://extensionworkshop.com/documentation/publish/"
+ "site-permission-add-on/ for details.");
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MIDIPermissionRequest::Allow(JS::Handle<JS::Value> 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;
+ }
+
+ nsCString permName = "midi"_ns;
+ // The previous implementation made no differences between midi and
+ // midi-sysex. The check on the SitePermsAddonProvider pref should be removed
+ // at the same time as the old implementation.
+ if (mNeedsSysex || !StaticPrefs::dom_sitepermsaddon_provider_enabled()) {
+ permName.Append("-sysex");
+ }
+
+ // First, check for an explicit allow/deny. Note that we want to support
+ // granting a permission on the base domain and then using it on a subdomain,
+ // which is why we use the non-"Exact" variants of these APIs. See bug
+ // 1757218.
+ if (nsContentUtils::IsSitePermAllow(mPrincipal, permName)) {
+ Allow(JS::UndefinedHandleValue);
+ return NS_OK;
+ }
+
+ if (nsContentUtils::IsSitePermDeny(mPrincipal, permName)) {
+ CancelWithRandomizedDelay();
+ return NS_OK;
+ }
+
+ // If the add-on is not installed, and sitepermsaddon provider not enabled,
+ // auto-deny (except for localhost).
+ if (StaticPrefs::dom_webmidi_gated() &&
+ !StaticPrefs::dom_sitepermsaddon_provider_enabled() &&
+ !nsContentUtils::HasSitePerm(mPrincipal, permName) &&
+ !mPrincipal->GetIsLoopbackHost()) {
+ CancelWithRandomizedDelay();
+ return NS_OK;
+ }
+
+ // If sitepermsaddon provider is enabled and user denied install,
+ // auto-deny (except for localhost, where we use a regular permission flow).
+ if (StaticPrefs::dom_sitepermsaddon_provider_enabled() &&
+ nsContentUtils::IsSitePermDeny(mPrincipal, "install"_ns) &&
+ !mPrincipal->GetIsLoopbackHost()) {
+ CancelWithRandomizedDelay();
+ return NS_OK;
+ }
+
+ // Before we bother the user with a prompt, see if they have any devices. If
+ // they don't, just report denial.
+ MOZ_ASSERT(NS_IsMainThread());
+ mozilla::ipc::PBackgroundChild* actor =
+ mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!actor)) {
+ return NS_ERROR_FAILURE;
+ }
+ RefPtr<MIDIPermissionRequest> self = this;
+ actor->SendHasMIDIDevice(
+ [=](bool aHasDevices) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aHasDevices) {
+ self->DoPrompt();
+ } else {
+ nsContentUtils::ReportToConsoleNonLocalized(
+ u"Silently denying site request for MIDI access because no devices were detected. You may need to restart your browser after connecting a new device."_ns,
+ nsIScriptError::infoFlag, "WebMIDI"_ns, mWindow->GetDoc());
+ self->CancelWithRandomizedDelay();
+ }
+ },
+ [=](auto) { self->CancelWithRandomizedDelay(); });
+
+ return NS_OK;
+}
+
+// If the user has no MIDI devices, we automatically deny the request. To
+// prevent sites from using timing attack to discern the existence of MIDI
+// devices, we instrument silent denials with a randomized delay between 3
+// and 13 seconds, which is intended to model the time the user might spend
+// considering a prompt before denying it.
+//
+// Note that we set the random component of the delay to zero in automation
+// to avoid unnecessarily increasing test end-to-end time.
+void MIDIPermissionRequest::CancelWithRandomizedDelay() {
+ MOZ_ASSERT(NS_IsMainThread());
+ uint32_t baseDelayMS = 3 * 1000;
+ uint32_t randomDelayMS =
+ xpc::IsInAutomation() ? 0 : RandomUint64OrDie() % (10 * 1000);
+ auto delay = TimeDuration::FromMilliseconds(baseDelayMS + randomDelayMS);
+ RefPtr<MIDIPermissionRequest> self = this;
+ NS_NewTimerWithCallback(
+ getter_AddRefs(mCancelTimer), [=](auto) { self->Cancel(); }, delay,
+ nsITimer::TYPE_ONE_SHOT, __func__);
+}
+
+nsresult MIDIPermissionRequest::DoPrompt() {
+ 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..935abf580a
--- /dev/null
+++ b/dom/midi/MIDIPermissionRequest.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_MIDIPermissionRequest_h
+#define mozilla_dom_MIDIPermissionRequest_h
+
+#include "mozilla/dom/Promise.h"
+#include "nsContentPermissionHelper.h"
+
+namespace mozilla::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::Handle<JS::Value> choices) override;
+ NS_IMETHOD GetTypes(nsIArray** aTypes) override;
+
+ private:
+ ~MIDIPermissionRequest() = default;
+ nsresult DoPrompt();
+ void CancelWithRandomizedDelay();
+
+ // If we're canceling on a timer, we need to hold a strong ref while it's
+ // outstanding.
+ nsCOMPtr<nsITimer> mCancelTimer;
+
+ // Promise for returning MIDIAccess on request success
+ RefPtr<Promise> mPromise;
+ // True if sysex permissions should be requested
+ bool mNeedsSysex;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_MIDIPermissionRequest_h
diff --git a/dom/midi/MIDIPlatformRunnables.cpp b/dom/midi/MIDIPlatformRunnables.cpp
new file mode 100644
index 0000000000..355f90b9f3
--- /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() {
+ MIDIPlatformService::AssertThread();
+ 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(mPort, 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..15727a99cc
--- /dev/null
+++ b/dom/midi/MIDIPlatformRunnables.h
@@ -0,0 +1,125 @@
+/* -*- 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::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(MIDIPortParent* aPort, MIDIPortDeviceState aState,
+ MIDIPortConnectionState aConnection)
+ : MIDIBackgroundRunnable("SetStatusRunnable"),
+ mPort(aPort),
+ mState(aState),
+ mConnection(aConnection) {}
+ ~SetStatusRunnable() = default;
+ void RunInternal() override;
+
+ private:
+ RefPtr<MIDIPortParent> mPort;
+ MIDIPortDeviceState mState;
+ MIDIPortConnectionState mConnection;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_MIDIPlatformRunnables_h
diff --git a/dom/midi/MIDIPlatformService.cpp b/dom/midi/MIDIPlatformService.cpp
new file mode 100644
index 0000000000..a3ef5911a7
--- /dev/null
+++ b/dom/midi/MIDIPlatformService.cpp
@@ -0,0 +1,279 @@
+/* -*- 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"
+#ifdef MOZ_WEBMIDI_MIDIR_IMPL
+# include "midirMIDIPlatformService.h"
+#endif // MOZ_WEBMIDI_MIDIR_IMPL
+#include "mozilla/ErrorResult.h"
+#include "mozilla/StaticPrefs_midi.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) {
+ AssertThread();
+ 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 (const 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);
+ AssertThread();
+ mPorts.AppendElement(aPort);
+}
+
+void MIDIPlatformService::RemovePort(MIDIPortParent* aPort) {
+ // This should only be called from the background thread, when a MIDIPort
+ // actor has been destroyed.
+ AssertThread();
+ MOZ_ASSERT(aPort);
+ mPorts.RemoveElement(aPort);
+ MaybeStop();
+}
+
+void MIDIPlatformService::BroadcastState(const MIDIPortInfo& aPortInfo,
+ const MIDIPortDeviceState& aState) {
+ AssertThread();
+ 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) {
+ AssertThread();
+ {
+ MutexAutoLock lock(mMessageQueueMutex);
+ MIDIMessageQueue* msgQueue = mMessageQueues.GetOrInsertNew(aId);
+ msgQueue->Add(aMsgs);
+ }
+
+ ScheduleSend(aId);
+}
+
+void MIDIPlatformService::SendPortList() {
+ AssertThread();
+ 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) {
+ AssertThread();
+ MOZ_ASSERT(aPort);
+ {
+ MutexAutoLock lock(mMessageQueueMutex);
+ MIDIMessageQueue* msgQueue =
+ mMessageQueues.Get(aPort->MIDIPortInterface::Id());
+ if (msgQueue) {
+ msgQueue->Clear();
+ }
+ }
+}
+
+void MIDIPlatformService::AddPortInfo(MIDIPortInfo& aPortInfo) {
+ AssertThread();
+ 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) {
+ AssertThread();
+ mPortInfo.RemoveElement(aPortInfo);
+ BroadcastState(aPortInfo, MIDIPortDeviceState::Disconnected);
+ if (mHasSentPortList) {
+ SendPortList();
+ }
+}
+
+StaticRefPtr<nsISerialEventTarget> gMIDITaskQueue;
+
+// static
+void MIDIPlatformService::InitStatics() {
+ nsCOMPtr<nsISerialEventTarget> queue;
+ MOZ_ALWAYS_SUCCEEDS(
+ NS_CreateBackgroundTaskQueue("MIDITaskQueue", getter_AddRefs(queue)));
+ gMIDITaskQueue = queue.forget();
+ ClearOnShutdown(&gMIDITaskQueue);
+}
+
+// static
+nsISerialEventTarget* MIDIPlatformService::OwnerThread() {
+ return gMIDITaskQueue;
+}
+
+StaticRefPtr<MIDIPlatformService> gMIDIPlatformService;
+
+// static
+bool MIDIPlatformService::IsRunning() {
+ return gMIDIPlatformService != nullptr;
+}
+
+void MIDIPlatformService::Close(mozilla::dom::MIDIPortParent* aPort) {
+ AssertThread();
+ {
+ 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());
+ AssertThread();
+ if (!IsRunning()) {
+ if (StaticPrefs::midi_testing()) {
+ gMIDIPlatformService = new TestMIDIPlatformService();
+ }
+#ifdef MOZ_WEBMIDI_MIDIR_IMPL
+ else {
+ gMIDIPlatformService = new midirMIDIPlatformService();
+ }
+#endif // MOZ_WEBMIDI_MIDIR_IMPL
+ gMIDIPlatformService->Init();
+ }
+ return gMIDIPlatformService;
+}
+
+void MIDIPlatformService::MaybeStop() {
+ AssertThread();
+ 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) {
+ AssertThread();
+ 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());
+ OwnerThread()->Dispatch(r.forget());
+}
+
+void MIDIPlatformService::RemoveManager(MIDIManagerParent* aManager) {
+ AssertThread();
+ mManagers.RemoveElement(aManager);
+ MaybeStop();
+}
+
+void MIDIPlatformService::UpdateStatus(
+ MIDIPortParent* aPort, const MIDIPortDeviceState& aDeviceState,
+ const MIDIPortConnectionState& aConnectionState) {
+ AssertThread();
+ aPort->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..36a1ea1296
--- /dev/null
+++ b/dom/midi/MIDIPlatformService.h
@@ -0,0 +1,174 @@
+/* -*- 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::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_THREADSAFE_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* aManager);
+
+ // Removes a deleted manager protocol object from manager array.
+ void RemoveManager(MIDIManagerParent* aManager);
+
+ // 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;
+
+ // Forces the implementation to refresh the port list.
+ virtual void Refresh() = 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();
+
+ // Initializes statics on startup.
+ static void InitStatics();
+
+ // Returns the MIDI Task Queue.
+ static nsISerialEventTarget* OwnerThread();
+
+ // Asserts that we're on the above task queue.
+ static void AssertThread() {
+ MOZ_DIAGNOSTIC_ASSERT(OwnerThread()->IsOnCurrentThread());
+ }
+
+ // 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);
+
+ // 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);
+
+ // Returns whether there are currently any MIDI devices.
+ bool HasDevice() { return !mPortInfo.IsEmpty(); }
+
+ 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 MOZ_UNANNOTATED;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_MIDIPlatformService_h
diff --git a/dom/midi/MIDIPort.cpp b/dom/midi/MIDIPort.cpp
new file mode 100644
index 0000000000..3d75bf2d63
--- /dev/null
+++ b/dom/midi/MIDIPort.cpp
@@ -0,0 +1,283 @@
+/* -*- 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/Endpoint.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Promise.h"
+#include "nsContentUtils.h"
+#include "nsISupportsImpl.h" // for MOZ_COUNT_CTOR, MOZ_COUNT_DTOR
+#include "MIDILog.h"
+
+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)
+ : DOMEventTargetHelper(aWindow),
+ mMIDIAccessParent(nullptr),
+ mKeepAlive(false) {
+ MOZ_ASSERT(aWindow);
+
+ Document* aDoc = GetOwner()->GetExtantDoc();
+ if (aDoc) {
+ aDoc->DisallowBFCaching();
+ }
+}
+
+MIDIPort::~MIDIPort() {
+ if (mMIDIAccessParent) {
+ mMIDIAccessParent->RemovePortListener(this);
+ mMIDIAccessParent = nullptr;
+ }
+ if (Port()) {
+ // 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.
+ Port()->SendShutdown();
+ }
+}
+
+bool MIDIPort::Initialize(const MIDIPortInfo& aPortInfo, bool aSysexEnabled,
+ MIDIAccess* aMIDIAccessParent) {
+ MOZ_ASSERT(aMIDIAccessParent);
+ nsCOMPtr<Document> document = GetDocumentIfCurrent();
+ if (!document) {
+ return false;
+ }
+
+ nsCOMPtr<nsIURI> uri = document->GetDocumentURI();
+ if (!uri) {
+ return false;
+ }
+
+ nsAutoCString origin;
+ nsresult rv = nsContentUtils::GetWebExposedOriginSerialization(uri, origin);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ RefPtr<MIDIPortChild> port =
+ new MIDIPortChild(aPortInfo, aSysexEnabled, this);
+ if (NS_FAILED(port->GenerateStableId(origin))) {
+ return false;
+ }
+ PBackgroundChild* b = BackgroundChild::GetForCurrentThread();
+ MOZ_ASSERT(b,
+ "Should always have a valid BackgroundChild when creating a port "
+ "object!");
+
+ // Create the endpoints and bind the one on the child side.
+ Endpoint<PMIDIPortParent> parentEndpoint;
+ Endpoint<PMIDIPortChild> childEndpoint;
+ MOZ_ALWAYS_SUCCEEDS(
+ PMIDIPort::CreateEndpoints(&parentEndpoint, &childEndpoint));
+ MOZ_ALWAYS_TRUE(childEndpoint.Bind(port));
+
+ if (!b->SendCreateMIDIPort(std::move(parentEndpoint), aPortInfo,
+ aSysexEnabled)) {
+ return false;
+ }
+
+ mMIDIAccessParent = aMIDIAccessParent;
+ mPortHolder.Init(port.forget());
+ LOG("MIDIPort::Initialize (%s, %s)",
+ NS_ConvertUTF16toUTF8(Port()->Name()).get(),
+ MIDIPortTypeValues::strings[uint32_t(Port()->Type())].value);
+ return true;
+}
+
+void MIDIPort::UnsetIPCPort() {
+ LOG("MIDIPort::UnsetIPCPort (%s, %s)",
+ NS_ConvertUTF16toUTF8(Port()->Name()).get(),
+ MIDIPortTypeValues::strings[uint32_t(Port()->Type())].value);
+ mPortHolder.Clear();
+}
+
+void MIDIPort::GetId(nsString& aRetVal) const {
+ MOZ_ASSERT(Port());
+ aRetVal = Port()->StableId();
+}
+
+void MIDIPort::GetManufacturer(nsString& aRetVal) const {
+ MOZ_ASSERT(Port());
+ aRetVal = Port()->Manufacturer();
+}
+
+void MIDIPort::GetName(nsString& aRetVal) const {
+ MOZ_ASSERT(Port());
+ aRetVal = Port()->Name();
+}
+
+void MIDIPort::GetVersion(nsString& aRetVal) const {
+ MOZ_ASSERT(Port());
+ aRetVal = Port()->Version();
+}
+
+MIDIPortType MIDIPort::Type() const {
+ MOZ_ASSERT(Port());
+ return Port()->Type();
+}
+
+MIDIPortConnectionState MIDIPort::Connection() const {
+ MOZ_ASSERT(Port());
+ return Port()->ConnectionState();
+}
+
+MIDIPortDeviceState MIDIPort::State() const {
+ MOZ_ASSERT(Port());
+ return Port()->DeviceState();
+}
+
+bool MIDIPort::SysexEnabled() const {
+ MOZ_ASSERT(Port());
+ return Port()->SysexEnabled();
+}
+
+already_AddRefed<Promise> MIDIPort::Open(ErrorResult& aError) {
+ LOG("MIDIPort::Open");
+ MOZ_ASSERT(Port());
+ RefPtr<Promise> p;
+ if (mOpeningPromise) {
+ p = mOpeningPromise;
+ return p.forget();
+ }
+ nsCOMPtr<nsIGlobalObject> go = do_QueryInterface(GetOwner());
+ p = Promise::Create(go, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ mOpeningPromise = p;
+ Port()->SendOpen();
+ return p.forget();
+}
+
+already_AddRefed<Promise> MIDIPort::Close(ErrorResult& aError) {
+ LOG("MIDIPort::Close");
+ MOZ_ASSERT(Port());
+ RefPtr<Promise> p;
+ if (mClosingPromise) {
+ p = mClosingPromise;
+ return p.forget();
+ }
+ nsCOMPtr<nsIGlobalObject> go = do_QueryInterface(GetOwner());
+ p = Promise::Create(go, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ mClosingPromise = p;
+ Port()->SendClose();
+ return p.forget();
+}
+
+void MIDIPort::Notify(const void_t& aVoid) {
+ LOG("MIDIPort::notify MIDIAccess shutting down, dropping reference.");
+ // If we're getting notified, it means the MIDIAccess parent object is dead.
+ // Nullify our copy.
+ mMIDIAccessParent = nullptr;
+}
+
+void MIDIPort::FireStateChangeEvent() {
+ if (!GetOwner()) {
+ return; // Ignore changes once we've been disconnected from the owner
+ }
+
+ StateChange();
+
+ MOZ_ASSERT(Port());
+ if (Port()->ConnectionState() == MIDIPortConnectionState::Open ||
+ Port()->ConnectionState() == MIDIPortConnectionState::Pending) {
+ if (mOpeningPromise) {
+ mOpeningPromise->MaybeResolve(this);
+ mOpeningPromise = nullptr;
+ }
+ } else if (Port()->ConnectionState() == MIDIPortConnectionState::Closed) {
+ if (mOpeningPromise) {
+ mOpeningPromise->MaybeReject(NS_ERROR_DOM_INVALID_ACCESS_ERR);
+ mOpeningPromise = nullptr;
+ }
+ if (mClosingPromise) {
+ mClosingPromise->MaybeResolve(this);
+ mClosingPromise = nullptr;
+ }
+ }
+
+ if (Port()->DeviceState() == MIDIPortDeviceState::Connected &&
+ Port()->ConnectionState() == MIDIPortConnectionState::Pending) {
+ Port()->SendOpen();
+ }
+
+ if (Port()->ConnectionState() == MIDIPortConnectionState::Open ||
+ (Port()->DeviceState() == MIDIPortDeviceState::Connected &&
+ Port()->ConnectionState() == MIDIPortConnectionState::Pending)) {
+ KeepAliveOnStatechange();
+ } else {
+ DontKeepAliveOnStatechange();
+ }
+
+ // 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::StateChange() {}
+
+void MIDIPort::Receive(const nsTArray<MIDIMessage>& aMsg) {
+ MOZ_CRASH("We should never get here!");
+}
+
+void MIDIPort::DisconnectFromOwner() {
+ if (Port()) {
+ Port()->SendClose();
+ }
+ DontKeepAliveOnStatechange();
+
+ DOMEventTargetHelper::DisconnectFromOwner();
+}
+
+void MIDIPort::KeepAliveOnStatechange() {
+ if (!mKeepAlive) {
+ mKeepAlive = true;
+ KeepAliveIfHasListenersFor(nsGkAtoms::onstatechange);
+ }
+}
+
+void MIDIPort::DontKeepAliveOnStatechange() {
+ if (mKeepAlive) {
+ IgnoreKeepAliveIfHasListenersFor(nsGkAtoms::onstatechange);
+ mKeepAlive = false;
+ }
+}
+
+const nsString& MIDIPort::StableId() { return Port()->StableId(); }
+
+} // namespace mozilla::dom
diff --git a/dom/midi/MIDIPort.h b/dom/midi/MIDIPort.h
new file mode 100644
index 0000000000..76e039ab65
--- /dev/null
+++ b/dom/midi/MIDIPort.h
@@ -0,0 +1,126 @@
+/* -*- 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 "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/dom/MIDIAccess.h"
+#include "mozilla/dom/MIDIPortChild.h"
+#include "mozilla/dom/MIDIPortInterface.h"
+
+struct JSContext;
+
+namespace mozilla::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:
+ explicit MIDIPort(nsPIDOMWindowInner* aWindow);
+ bool Initialize(const MIDIPortInfo& aPortInfo, bool aSysexEnabled,
+ MIDIAccess* aMIDIAccessParent);
+ 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(ErrorResult& aError);
+ already_AddRefed<Promise> Close(ErrorResult& aError);
+
+ // 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 StateChange();
+ 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)
+
+ void DisconnectFromOwner() override;
+ const nsString& StableId();
+
+ protected:
+ // Helper class to ensure we always call DetachOwner when we drop the
+ // reference to the the port.
+ class PortHolder {
+ public:
+ void Init(already_AddRefed<MIDIPortChild> aArg) {
+ MOZ_ASSERT(!mInner);
+ mInner = aArg;
+ }
+ void Clear() {
+ if (mInner) {
+ mInner->DetachOwner();
+ mInner = nullptr;
+ }
+ }
+ ~PortHolder() { Clear(); }
+ MIDIPortChild* Get() const { return mInner; }
+
+ private:
+ RefPtr<MIDIPortChild> mInner;
+ };
+
+ // IPC Actor corresponding to this class.
+ PortHolder mPortHolder;
+ MIDIPortChild* Port() const { return mPortHolder.Get(); }
+
+ private:
+ void KeepAliveOnStatechange();
+ void DontKeepAliveOnStatechange();
+
+ // 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;
+ // If true this object will be kept alive even without direct JS references
+ bool mKeepAlive;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_MIDIPort_h
diff --git a/dom/midi/MIDIPortChild.cpp b/dom/midi/MIDIPortChild.cpp
new file mode 100644
index 0000000000..d649d5540e
--- /dev/null
+++ b/dom/midi/MIDIPortChild.cpp
@@ -0,0 +1,59 @@
+/* -*- 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"
+#include "nsContentUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+MIDIPortChild::MIDIPortChild(const MIDIPortInfo& aPortInfo, bool aSysexEnabled,
+ MIDIPort* aPort)
+ : MIDIPortInterface(aPortInfo, aSysexEnabled), mDOMPort(aPort) {}
+
+void MIDIPortChild::ActorDestroy(ActorDestroyReason aWhy) {
+ if (mDOMPort) {
+ mDOMPort->UnsetIPCPort();
+ MOZ_ASSERT(!mDOMPort);
+ }
+ MIDIPortInterface::Shutdown();
+}
+
+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();
+}
+
+nsresult MIDIPortChild::GenerateStableId(const nsACString& aOrigin) {
+ const size_t kIdLength = 64;
+ mStableId.SetCapacity(kIdLength);
+ mStableId.Append(Name());
+ mStableId.Append(Manufacturer());
+ mStableId.Append(Version());
+ nsContentUtils::AnonymizeId(mStableId, aOrigin,
+ nsContentUtils::OriginFormat::Plain);
+ return NS_OK;
+}
diff --git a/dom/midi/MIDIPortChild.h b/dom/midi/MIDIPortChild.h
new file mode 100644
index 0000000000..2ef1059c78
--- /dev/null
+++ b/dom/midi/MIDIPortChild.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_MIDIPortChild_h
+#define mozilla_dom_MIDIPortChild_h
+
+#include "mozilla/dom/MIDIPortInterface.h"
+#include "mozilla/dom/PMIDIPortChild.h"
+
+namespace mozilla::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, override);
+ 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);
+ nsresult GenerateStableId(const nsACString& aOrigin);
+ const nsString& StableId() { return mStableId; };
+
+ void DetachOwner() { mDOMPort = nullptr; }
+
+ private:
+ ~MIDIPortChild() = default;
+ // Pointer to the DOM object this actor represents. The actor cannot outlive
+ // the DOM object.
+ MIDIPort* mDOMPort;
+ nsString mStableId;
+};
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/midi/MIDIPortInterface.cpp b/dom/midi/MIDIPortInterface.cpp
new file mode 100644
index 0000000000..9d186fa3f4
--- /dev/null
+++ b/dom/midi/MIDIPortInterface.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/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),
+ mConnectionState(MIDIPortConnectionState::Closed),
+ 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..360750e6e5
--- /dev/null
+++ b/dom/midi/MIDIPortInterface.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_MIDIPortInterface_h
+#define mozilla_dom_MIDIPortInterface_h
+
+#include "mozilla/dom/MIDIPortBinding.h"
+
+namespace mozilla::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 mozilla::dom
+
+#endif // mozilla_dom_MIDIPortInterface_h
diff --git a/dom/midi/MIDIPortParent.cpp b/dom/midi/MIDIPortParent.cpp
new file mode 100644
index 0000000000..0a01784945
--- /dev/null
+++ b/dom/midi/MIDIPortParent.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/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() &&
+ mConnectionState == MIDIPortConnectionState::Closed) {
+ 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();
+ }
+ Close();
+ return IPC_OK();
+}
+
+void MIDIPortParent::ActorDestroy(ActorDestroyReason) {
+ mMessageQueue.Clear();
+ MIDIPortInterface::Shutdown();
+ if (MIDIPlatformService::IsRunning()) {
+ MIDIPlatformService::Get()->RemovePort(this);
+ }
+}
+
+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..527abddd00
--- /dev/null
+++ b/dom/midi/MIDIPortParent.h
@@ -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/. */
+
+#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::dom {
+
+/**
+ * Actor representing the parent (PBackground thread) side of a MIDIPort object.
+ *
+ */
+class MIDIPortParent final : public PMIDIPortParent, public MIDIPortInterface {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MIDIPortParent, override);
+ 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& aDeviceState,
+ const MIDIPortConnectionState& aConnectionState);
+ uint32_t GetInternalId() const { return mInternalId; }
+
+ 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 mozilla::dom
+
+#endif
diff --git a/dom/midi/MIDITypes.ipdlh b/dom/midi/MIDITypes.ipdlh
new file mode 100644
index 0000000000..0508f171a8
--- /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..645889a167
--- /dev/null
+++ b/dom/midi/MIDIUtils.cpp
@@ -0,0 +1,172 @@
+/* -*- 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};
+static const uint8_t kReservedStatuses[] = {0xf4, 0xf5, 0xf9, 0xfd};
+
+namespace mozilla::dom::MIDIUtils {
+
+static bool IsSystemRealtimeMessage(uint8_t aByte) {
+ return (aByte & kSystemRealtimeMessage) == kSystemRealtimeMessage;
+}
+
+static bool IsCommandByte(uint8_t aByte) {
+ return (aByte & kCommandByte) == kCommandByte;
+}
+
+static bool IsReservedStatus(uint8_t aByte) {
+ for (const auto& msg : kReservedStatuses) {
+ if (aByte == msg) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// Checks validity of MIDIMessage passed to it. Throws debug warnings and
+// returns false if message is not valid.
+bool IsValidMessage(const MIDIMessage* aMsg) {
+ if (aMsg->data().Length() == 0) {
+ return false;
+ }
+
+ uint8_t cmd = aMsg->data()[0];
+ // If first byte isn't a command, something is definitely wrong.
+ if (!IsCommandByte(cmd)) {
+ NS_WARNING("Constructed a MIDI packet where first byte is not command!");
+ return false;
+ }
+
+ if (IsReservedStatus(cmd)) {
+ NS_WARNING("Using a reserved message");
+ return false;
+ }
+
+ if (cmd == kSysexMessageStart) {
+ // All we can do with sysex is make sure it starts and ends with the
+ // correct command bytes and that it does not contain other command bytes.
+ if (aMsg->data()[aMsg->data().Length() - 1] != kSysexMessageEnd) {
+ NS_WARNING("Last byte of Sysex Message not 0xF7!");
+ return false;
+ }
+
+ for (size_t i = 1; i < aMsg->data().Length() - 2; i++) {
+ if (IsCommandByte(aMsg->data()[i])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ // For system realtime messages, the length should always be 1.
+ if (IsSystemRealtimeMessage(cmd)) {
+ 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];
+}
+
+bool ParseMessages(const nsTArray<uint8_t>& aByteBuffer,
+ const TimeStamp& aTimestamp,
+ nsTArray<MIDIMessage>& aMsgArray) {
+ bool inSysexMessage = false;
+ UniquePtr<MIDIMessage> currentMsg = nullptr;
+ for (const auto& byte : aByteBuffer) {
+ if (IsSystemRealtimeMessage(byte)) {
+ MIDIMessage rt_msg;
+ rt_msg.data().AppendElement(byte);
+ rt_msg.timestamp() = aTimestamp;
+ if (!IsValidMessage(&rt_msg)) {
+ return false;
+ }
+ aMsgArray.AppendElement(rt_msg);
+ continue;
+ }
+
+ if (byte == kSysexMessageEnd) {
+ if (!inSysexMessage) {
+ NS_WARNING(
+ "Got sysex message end with no sysex message being processed!");
+ return false;
+ }
+ inSysexMessage = false;
+ } else if (IsCommandByte(byte)) {
+ if (currentMsg) {
+ if (!IsValidMessage(currentMsg.get())) {
+ return false;
+ }
+
+ aMsgArray.AppendElement(*currentMsg);
+ }
+
+ currentMsg = MakeUnique<MIDIMessage>();
+ currentMsg->timestamp() = aTimestamp;
+ }
+
+ if (!currentMsg) {
+ NS_WARNING("No command byte has been encountered yet!");
+ return false;
+ }
+
+ currentMsg->data().AppendElement(byte);
+
+ if (byte == kSysexMessageStart) {
+ inSysexMessage = true;
+ }
+ }
+
+ if (currentMsg) {
+ if (!IsValidMessage(currentMsg.get())) {
+ return false;
+ }
+ aMsgArray.AppendElement(*currentMsg);
+ }
+
+ return true;
+}
+
+bool IsSysexMessage(const MIDIMessage& aMsg) {
+ if (aMsg.data().Length() == 0) {
+ return false;
+ }
+ return aMsg.data()[0] == kSysexMessageStart;
+}
+} // namespace mozilla::dom::MIDIUtils
diff --git a/dom/midi/MIDIUtils.h b/dom/midi/MIDIUtils.h
new file mode 100644
index 0000000000..c5559dce7e
--- /dev/null
+++ b/dom/midi/MIDIUtils.h
@@ -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 "nsTArray.h"
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla::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 true if no errors were encountered, false otherwise.
+bool 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 mozilla::dom
diff --git a/dom/midi/PMIDIManager.ipdl b/dom/midi/PMIDIManager.ipdl
new file mode 100644
index 0000000000..42698a9189
--- /dev/null
+++ b/dom/midi/PMIDIManager.ipdl
@@ -0,0 +1,24 @@
+/* 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 MIDITypes;
+
+namespace mozilla {
+namespace dom {
+
+[ChildProc=anydom]
+async protocol PMIDIManager
+{
+parent:
+ async Refresh();
+ async Shutdown();
+child:
+ /*
+ * Send an updated list of MIDI ports to the child
+ */
+ async MIDIPortListUpdate(MIDIPortList aPortList);
+};
+
+} // namespace ipc
+} // namespace mozilla
diff --git a/dom/midi/PMIDIPort.ipdl b/dom/midi/PMIDIPort.ipdl
new file mode 100644
index 0000000000..917518e919
--- /dev/null
+++ b/dom/midi/PMIDIPort.ipdl
@@ -0,0 +1,29 @@
+/* -*- 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 MIDITypes;
+
+namespace mozilla {
+namespace dom {
+
+[ChildProc=anydom]
+async protocol PMIDIPort
+{
+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);
+};
+
+}
+}
diff --git a/dom/midi/TestMIDIPlatformService.cpp b/dom/midi/TestMIDIPlatformService.cpp
new file mode 100644
index 0000000000..cbc11a7474
--- /dev/null
+++ b/dom/midi/TestMIDIPlatformService.cpp
@@ -0,0 +1,258 @@
+/* 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() override {
+ // 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() {
+ MIDIPlatformService::AssertThread();
+ MIDIPlatformService::Get()->QueueMessages(mPortID, mMsgs);
+ }
+
+ private:
+ nsString mPortID;
+ nsTArray<MIDIMessage> mMsgs;
+};
+
+TestMIDIPlatformService::TestMIDIPlatformService()
+ : 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)),
+ mDoRefresh(false),
+ mIsInitialized(false) {
+ MIDIPlatformService::AssertThread();
+}
+
+TestMIDIPlatformService::~TestMIDIPlatformService() {
+ MIDIPlatformService::AssertThread();
+}
+
+void TestMIDIPlatformService::Init() {
+ MIDIPlatformService::AssertThread();
+
+ 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);
+ MIDIPlatformService::Get()->AddPortInfo(mStateTestOutputPort);
+ nsCOMPtr<nsIRunnable> r(new SendPortListRunnable());
+
+ // Start the IO Thread.
+ OwnerThread()->Dispatch(r.forget());
+}
+
+void TestMIDIPlatformService::Refresh() {
+ if (mDoRefresh) {
+ AddPortInfo(mStateTestInputPort);
+ mDoRefresh = false;
+ }
+}
+
+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, aPort->DeviceState(), s));
+ OwnerThread()->Dispatch(r.forget());
+}
+
+void TestMIDIPlatformService::ScheduleClose(MIDIPortParent* aPort) {
+ AssertThread();
+ 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, aPort->DeviceState(), MIDIPortConnectionState::Closed));
+ OwnerThread()->Dispatch(r.forget());
+ }
+}
+
+void TestMIDIPlatformService::Stop() { MIDIPlatformService::AssertThread(); }
+
+void TestMIDIPlatformService::ScheduleSend(const nsAString& aPortId) {
+ AssertThread();
+ nsCOMPtr<nsIRunnable> r(new ProcessMessagesRunnable(aPortId));
+ OwnerThread()->Dispatch(r.forget());
+}
+
+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));
+ OwnerThread()->Dispatch(r, NS_DISPATCH_NORMAL);
+ break;
+ }
+ // Cause control test ports to connect
+ case 0x01: {
+ nsCOMPtr<nsIRunnable> r1(
+ new AddPortRunnable(mStateTestInputPort));
+ OwnerThread()->Dispatch(r1, NS_DISPATCH_NORMAL);
+ break;
+ }
+ // Cause control test ports to disconnect
+ case 0x02: {
+ nsCOMPtr<nsIRunnable> r1(
+ new RemovePortRunnable(mStateTestInputPort));
+ OwnerThread()->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));
+ OwnerThread()->Dispatch(r, NS_DISPATCH_NORMAL);
+ break;
+ }
+ // Causes the next refresh to add new ports to the list
+ case 0x04: {
+ mDoRefresh = true;
+ 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));
+ OwnerThread()->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, 0xFA, 0x02, 0x03,
+ 0x04, 0xF8, 0x05, 0xF7};
+ // Can't use AppendElements on an array here, so just do range
+ // based loading.
+ for (const auto& s : msg) {
+ msgs.AppendElement(s);
+ }
+ nsTArray<MIDIMessage> newMsgs;
+ MIDIUtils::ParseMessages(msgs, TimeStamp::Now(), newMsgs);
+ nsCOMPtr<nsIRunnable> r(
+ new ReceiveRunnable(mControlInputPort.id(), newMsgs));
+ OwnerThread()->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..29423a02a4
--- /dev/null
+++ b/dom/midi/TestMIDIPlatformService.h
@@ -0,0 +1,61 @@
+/* -*- 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::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 Refresh() 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();
+ // 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;
+ // When true calling Refresh() will add new ports.
+ bool mDoRefresh;
+ // True if server has been brought up already.
+ bool mIsInitialized;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_TestMIDIPlatformService_h
diff --git a/dom/midi/crashtests/1851829.html b/dom/midi/crashtests/1851829.html
new file mode 100644
index 0000000000..f14d22e42f
--- /dev/null
+++ b/dom/midi/crashtests/1851829.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1851829
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1851829</title>
+ <script>
+ async function timeout (cmd) {
+ const timer = new Promise((resolve, reject) => {
+ const id = setTimeout(() => {
+ clearTimeout(id)
+ reject(new Error('Promise timed out!'))
+ }, 750)
+ })
+ return Promise.race([cmd, timer])
+ }
+
+ window.addEventListener('load', async () => {
+ // <script>window.close()<\/script>
+ const tab = window.open('data:text/plain;charset=utf-8;base64,PHNjcmlwdD53aW5kb3cuY2xvc2UoKTwvc2NyaXB0Pg==')
+ setTimeout(async () => {
+ try { await timeout(tab.navigator.requestMIDIAccess({})) } catch (e) {}
+ window.close()
+ document.documentElement.classList.remove("reftest-wait");
+ }, 400)
+ })
+ </script>
+</head>
+</html>
diff --git a/dom/midi/crashtests/crashtests.list b/dom/midi/crashtests/crashtests.list
new file mode 100644
index 0000000000..6b22018eb6
--- /dev/null
+++ b/dom/midi/crashtests/crashtests.list
@@ -0,0 +1 @@
+skip-if(Android) test-pref(app.normandy.enabled,false) test-pref(app.update.auto,false) test-pref(app.update.staging.enabled,false) test-pref(app.update.url.android,'') test-pref(apz.wr.activate_all_scroll_frames,true) test-pref(browser.EULA.override,true) test-pref(browser.cache.disk.enable,false) test-pref(browser.cache.disk_cache_ssl,false) test-pref(browser.cache.memory.enable,false) test-pref(browser.cache.offline.enable,false) test-pref(browser.chrome.site_icons,false) test-pref(browser.chrome.toolbar_tips,false) test-pref(browser.dom.window.dump.enabled,true) test-pref(browser.newtabpage.enabled,false) test-pref(browser.pagethumbnails.capturing_disabled,true) test-pref(browser.reader.detectedFirstArticle,true) test-pref(browser.safebrowsing.blockedURIs.enabled,false) test-pref(browser.safebrowsing.downloads.enabled,false) test-pref(browser.safebrowsing.downloads.remote.enabled,false) test-pref(browser.safebrowsing.enabled,false) test-pref(browser.safebrowsing.malware.enabled,false) test-pref(browser.safebrowsing.phishing.enabled,false) test-pref(browser.search.geoip.url,'') test-pref(browser.search.region,'US') test-pref(browser.search.suggest.enabled,false) test-pref(browser.search.suggest.prompted,true) test-pref(browser.search.update,false) test-pref(browser.sessionstore.resume_from_crash,false) test-pref(browser.shell.checkDefaultBrowser,false) test-pref(browser.ssl_override_behavior,1) test-pref(browser.startup.homepage,'about:blank') test-pref(browser.startup.homepage_override.mstone,'ignore') test-pref(browser.startup.page,0) test-pref(browser.tabs.warnOnClose,false) test-pref(browser.tabs.warnOnCloseOtherTabs,false) test-pref(browser.warnOnQuit,false) test-pref(canvas.hitregions.enabled,true) test-pref(captivedetect.canonicalURL,'') test-pref(clipboard.autocopy,true) test-pref(csp.skip_about_page_has_csp_assert,true) test-pref(datareporting.healthreport.uploadEnabled,false) test-pref(datareporting.policy.dataSubmissionEnabled,false) test-pref(datareporting.policy.dataSubmissionPolicyAcceptedVersion,2) test-pref(datareporting.policy.dataSubmissionPolicyBypassNotification,true) test-pref(datareporting.policy.firstRunURL,'') test-pref(device.sensors.ambientLight.enabled,true) test-pref(device.sensors.proximity.enabled,true) test-pref(devtools.selfxss.count,999) test-pref(dom.allow_scripts_to_close_windows,true) test-pref(dom.always_stop_slow_scripts,true) test-pref(dom.caches.testing.enabled,true) test-pref(dom.css_pseudo_element.enabled,true) test-pref(dom.dialog_element.enabled,true) test-pref(dom.disable_open_during_load,false) test-pref(dom.disable_window_flip,false) test-pref(dom.element.popover.enabled,true) test-pref(dom.fetchObserver.enabled,true) test-pref(dom.forms.datetime.others,true) test-pref(dom.gamepad.extensions.lightindicator,false) test-pref(dom.gamepad.extensions.multitouch,false) test-pref(dom.gamepad.test.enabled,false) test-pref(dom.image-lazy-loading.enabled,false) test-pref(dom.indexedDB.experimental,true) test-pref(dom.input.dirpicker,true) test-pref(dom.input_events.beforeinput.enabled,true) test-pref(dom.max_chrome_script_run_time,0) test-pref(dom.max_script_run_time,0) test-pref(dom.payments.request.enabled,true) test-pref(dom.presentation.controller.enabled,true) test-pref(dom.presentation.enabled,true) test-pref(dom.presentation.receiver.enabled,true) test-pref(dom.push.testing.ignorePermission,true) test-pref(dom.security.featurePolicy.webidl.enabled,true) test-pref(dom.security.sanitizer.enabled,true) test-pref(dom.security.setHTML.enabled,true) test-pref(dom.send_after_paint_to_content,true) test-pref(dom.serviceWorkers.testing.enabled,true) test-pref(dom.successive_dialog_time_limit,0) test-pref(dom.textMetrics.baselines.enabled,true) test-pref(dom.textMetrics.emHeight.enabled,true) test-pref(dom.textMetrics.fontBoundingBox.enabled,true) test-pref(dom.visualviewport.enabled,true) test-pref(dom.vr.external.notdetected.timeout,0) test-pref(dom.vr.external.quit.timeout,0) test-pref(dom.vr.poseprediction.enabled,false) test-pref(dom.vr.puppet.enabled,false) test-pref(dom.vr.require-gesture,false) test-pref(dom.vr.webxr.enabled,false) test-pref(dom.webgpu.enabled,true) test-pref(dom.weblocks.enabled,true) test-pref(dom.webmidi.enabled,true) test-pref(dom.window_print.fuzzing.block_while_printing,true) test-pref(extensions.autoDisableScopes,0) test-pref(extensions.blocklist.enabled,false) test-pref(extensions.enabledScopes,5) test-pref(extensions.getAddons.cache.enabled,false) test-pref(extensions.installDistroAddons,false) test-pref(extensions.showMismatchUI,false) test-pref(extensions.update.enabled,false) test-pref(extensions.update.notifyUser,false) test-pref(full-screen-api.allow-trusted-requests-only,false) test-pref(full-screen-api.warning.timeout,500) test-pref(fuzzing.enabled,true) test-pref(general.useragent.updates.enabled,false) test-pref(general.warnOnAboutConfig,false) test-pref(geo.enabled,false) test-pref(gfx.color_management.mode,1) test-pref(gfx.downloadable_fonts.disable_cache,true) test-pref(gfx.downloadable_fonts.otl_validation,false) test-pref(gfx.downloadable_fonts.sanitize_omt,false) test-pref(gfx.downloadable_fonts.validate_variation_tables,false) test-pref(gfx.offscreencanvas.enabled,true) test-pref(gfx.webrender.all,true) test-pref(gfx.webrender.debug.restrict-blob-size,true) test-pref(image.animated.decode-on-demand.batch-size,1) test-pref(image.animated.decode-on-demand.threshold-kb,0) test-pref(image.avif.sequence.enabled,true) test-pref(image.multithreaded_decoding.limit,1) test-pref(layout.accessiblecaret.enabled,true) test-pref(layout.css.backdrop-filter.enabled,true) test-pref(layout.css.constructable-stylesheets.enabled,true) test-pref(layout.css.container-queries.enabled,true) test-pref(layout.css.content-visibility.enabled,true) test-pref(layout.css.initial-letter.enabled,true) test-pref(layout.css.moz-control-character-visibility.enabled,true) test-pref(layout.css.moz-document.content.enabled,true) test-pref(layout.css.overflow-clip-box.enabled,true) test-pref(layout.css.scroll-linked-animations.enabled,true) test-pref(layout.css.zoom-transform-hack.enabled,true) test-pref(media.autoplay.default,0) test-pref(media.autoplay.enabled.user-gestures-needed,false) test-pref(media.eme.enabled,true) test-pref(media.eme.hdcp-policy-check.enabled,true) test-pref(media.gmp-manager.url.override,'http://127.0.0.1:6/dummy-gmp-manager.xml') test-pref(media.mediasource.webm.enabled,true) test-pref(media.navigator.permission.disabled,true) test-pref(media.navigator.video.red_ulpfec_enabled,true) test-pref(media.setsinkid.enabled,true) test-pref(midi.prompt.testing,true) test-pref(midi.testing,true) test-pref(network.captive-portal-service.enabled,false) test-pref(network.connectivity-service.enabled,false) test-pref(network.http.response.timeout,1) test-pref(network.http.spdy.enabled,false) test-pref(network.manage-offline-status,false) test-pref(network.prefetch-next,false) test-pref(network.protocol-handler.external.mailto,false) test-pref(network.proxy.allow_bypass,false) test-pref(network.proxy.autoconfig_url,"data:text/base64,ZnVuY3Rpb24gRmluZFByb3h5Rm9yVVJMKHVybCwgaG9zdCkgeyBpZiAoaG9zdCA9PSAnbG9jYWxob3N0JyB8fCBob3N0ID09ICcxMjcuMC4wLjEnKSB7IHJldHVybiAnRElSRUNUJzsgfSBlbHNlIHsgcmV0dXJuICdQUk9YWSAxMjcuMC4wLjE6Nic7IH0gfQ==") test-pref(network.proxy.failover_direct,false) test-pref(network.proxy.share_proxy_settings,true) test-pref(network.proxy.type,2) test-pref(network.websocket.allowInsecureFromHTTPS,true) test-pref(network.websocket.delay-failed-reconnects,false) test-pref(nglayout.debug.disable_xul_cache,false) test-pref(notification.prompt.testing,true) test-pref(notification.prompt.testing.allow,true) test-pref(pdfjs.firstRun,false) test-pref(pdfjs.previousHandler.alwaysAskBeforeHandling,true) test-pref(pdfjs.previousHandler.preferredAction,4) test-pref(permissions.default.camera,1) test-pref(permissions.default.geo,1) test-pref(permissions.default.microphone,1) test-pref(plugin.disable,true) test-pref(print.always_print_silent,true) test-pref(print.print_to_file,true) test-pref(print.show_print_progress,true) test-pref(security.OCSP.enabled,0) test-pref(security.data_uri.unique_opaque_origin,false) test-pref(security.default_personal_cert,'Select\u0020Automatically') test-pref(security.fileuri.strict_origin_policy,false) test-pref(security.webauth.webauthn_enable_softtoken,true) test-pref(security.webauth.webauthn_enable_usbtoken,false) test-pref(svg.context-properties.content.enabled,true) test-pref(toolkit.cosmeticAnimations.enabled,false) test-pref(toolkit.startup.max_resumed_crashes,-1) test-pref(toolkit.telemetry.enabled,false) test-pref(toolkit.telemetry.server,'') test-pref(webgl.enable-privileged-extensions,true) test-pref(webgl.max-warnings-per-context,0) test-pref(webgl.prefer-native-gl,false) load 1851829.html # WebMIDI is not supported on Android
diff --git a/dom/midi/midirMIDIPlatformService.cpp b/dom/midi/midirMIDIPlatformService.cpp
new file mode 100644
index 0000000000..842eb51dcd
--- /dev/null
+++ b/dom/midi/midirMIDIPlatformService.cpp
@@ -0,0 +1,193 @@
+/* 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 "midirMIDIPlatformService.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/TimeStamp.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/dom/midi/midir_impl_ffi_generated.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/Unused.h"
+#include "nsIThread.h"
+#include "mozilla/Logging.h"
+#include "MIDILog.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::ipc;
+
+static_assert(sizeof(TimeStamp) == sizeof(GeckoTimeStamp));
+
+/**
+ * Runnable used for to send messages asynchronously on the I/O thread.
+ */
+class SendRunnable : public MIDIBackgroundRunnable {
+ public:
+ explicit SendRunnable(const nsAString& aPortID, const MIDIMessage& aMessage)
+ : MIDIBackgroundRunnable("SendRunnable"),
+ mPortID(aPortID),
+ mMessage(aMessage) {}
+ ~SendRunnable() = default;
+ virtual void RunInternal() {
+ MIDIPlatformService::AssertThread();
+ if (!MIDIPlatformService::IsRunning()) {
+ // Some send operations might outlive the service, bail out and do nothing
+ return;
+ }
+ midirMIDIPlatformService* srv =
+ static_cast<midirMIDIPlatformService*>(MIDIPlatformService::Get());
+ srv->SendMessage(mPortID, mMessage);
+ }
+
+ private:
+ nsString mPortID;
+ MIDIMessage mMessage;
+};
+
+// static
+StaticMutex midirMIDIPlatformService::gOwnerThreadMutex;
+
+// static
+nsCOMPtr<nsISerialEventTarget> midirMIDIPlatformService::gOwnerThread;
+
+midirMIDIPlatformService::midirMIDIPlatformService()
+ : mImplementation(nullptr) {
+ StaticMutexAutoLock lock(gOwnerThreadMutex);
+ gOwnerThread = OwnerThread();
+}
+
+midirMIDIPlatformService::~midirMIDIPlatformService() {
+ LOG("midir_impl_shutdown");
+ if (mImplementation) {
+ midir_impl_shutdown(mImplementation);
+ }
+ StaticMutexAutoLock lock(gOwnerThreadMutex);
+ gOwnerThread = nullptr;
+}
+
+// static
+void midirMIDIPlatformService::AddPort(const nsString* aId,
+ const nsString* aName, bool aInput) {
+ MIDIPortType type = aInput ? MIDIPortType::Input : MIDIPortType::Output;
+ MIDIPortInfo port(*aId, *aName, u""_ns, u""_ns, static_cast<uint32_t>(type));
+ MIDIPlatformService::Get()->AddPortInfo(port);
+}
+
+// static
+void midirMIDIPlatformService::RemovePort(const nsString* aId,
+ const nsString* aName, bool aInput) {
+ MIDIPortType type = aInput ? MIDIPortType::Input : MIDIPortType::Output;
+ MIDIPortInfo port(*aId, *aName, u""_ns, u""_ns, static_cast<uint32_t>(type));
+ MIDIPlatformService::Get()->RemovePortInfo(port);
+}
+
+void midirMIDIPlatformService::Init() {
+ if (mImplementation) {
+ return;
+ }
+
+ mImplementation = midir_impl_init(AddPort);
+
+ if (mImplementation) {
+ MIDIPlatformService::Get()->SendPortList();
+ } else {
+ LOG("midir_impl_init failure");
+ }
+}
+
+// static
+void midirMIDIPlatformService::CheckAndReceive(const nsString* aId,
+ const uint8_t* aData,
+ size_t aLength,
+ const GeckoTimeStamp* aTimeStamp,
+ uint64_t aMicros) {
+ nsTArray<uint8_t> data;
+ data.AppendElements(aData, aLength);
+ const TimeStamp* openTime = reinterpret_cast<const TimeStamp*>(aTimeStamp);
+ TimeStamp timestamp =
+ *openTime + TimeDuration::FromMicroseconds(static_cast<double>(aMicros));
+ MIDIMessage message(data, timestamp);
+ LogMIDIMessage(message, *aId, MIDIPortType::Input);
+ nsTArray<MIDIMessage> messages;
+ messages.AppendElement(message);
+
+ nsCOMPtr<nsIRunnable> r(new ReceiveRunnable(*aId, messages));
+ StaticMutexAutoLock lock(gOwnerThreadMutex);
+ if (gOwnerThread) {
+ gOwnerThread->Dispatch(r, NS_DISPATCH_NORMAL);
+ }
+}
+
+void midirMIDIPlatformService::Refresh() {
+ midir_impl_refresh(mImplementation, AddPort, RemovePort);
+}
+
+void midirMIDIPlatformService::Open(MIDIPortParent* aPort) {
+ AssertThread();
+ MOZ_ASSERT(aPort);
+ nsString id = aPort->MIDIPortInterface::Id();
+ TimeStamp openTimeStamp = TimeStamp::Now();
+ if (midir_impl_open_port(mImplementation, &id,
+ reinterpret_cast<GeckoTimeStamp*>(&openTimeStamp),
+ CheckAndReceive)) {
+ LOG("MIDI port open: %s at t=%lf", NS_ConvertUTF16toUTF8(id).get(),
+ (openTimeStamp - TimeStamp::ProcessCreation()).ToSeconds());
+ nsCOMPtr<nsIRunnable> r(new SetStatusRunnable(
+ aPort, aPort->DeviceState(), MIDIPortConnectionState::Open));
+ OwnerThread()->Dispatch(r.forget());
+ } else {
+ LOG("MIDI port open failed: %s", NS_ConvertUTF16toUTF8(id).get());
+ }
+}
+
+void midirMIDIPlatformService::Stop() {
+ // Nothing to do here AFAIK
+}
+
+void midirMIDIPlatformService::ScheduleSend(const nsAString& aPortId) {
+ AssertThread();
+ LOG("MIDI port schedule send %s", NS_ConvertUTF16toUTF8(aPortId).get());
+ nsTArray<MIDIMessage> messages;
+ GetMessages(aPortId, messages);
+ TimeStamp now = TimeStamp::Now();
+ for (const auto& message : messages) {
+ if (message.timestamp().IsNull()) {
+ SendMessage(aPortId, message);
+ } else {
+ double delay = (message.timestamp() - now).ToMilliseconds();
+ if (delay < 1.0) {
+ SendMessage(aPortId, message);
+ } else {
+ nsCOMPtr<nsIRunnable> r(new SendRunnable(aPortId, message));
+ OwnerThread()->DelayedDispatch(r.forget(),
+ static_cast<uint32_t>(delay));
+ }
+ }
+ }
+}
+
+void midirMIDIPlatformService::ScheduleClose(MIDIPortParent* aPort) {
+ AssertThread();
+ MOZ_ASSERT(aPort);
+ nsString id = aPort->MIDIPortInterface::Id();
+ LOG("MIDI port schedule close %s", NS_ConvertUTF16toUTF8(id).get());
+ if (aPort->ConnectionState() == MIDIPortConnectionState::Open) {
+ midir_impl_close_port(mImplementation, &id);
+ nsCOMPtr<nsIRunnable> r(new SetStatusRunnable(
+ aPort, aPort->DeviceState(), MIDIPortConnectionState::Closed));
+ OwnerThread()->Dispatch(r.forget());
+ }
+}
+
+void midirMIDIPlatformService::SendMessage(const nsAString& aPortId,
+ const MIDIMessage& aMessage) {
+ LOG("MIDI send message on %s", NS_ConvertUTF16toUTF8(aPortId).get());
+ LogMIDIMessage(aMessage, aPortId, MIDIPortType::Output);
+ midir_impl_send(mImplementation, &aPortId, &aMessage.data());
+}
diff --git a/dom/midi/midirMIDIPlatformService.h b/dom/midi/midirMIDIPlatformService.h
new file mode 100644
index 0000000000..e204b5576a
--- /dev/null
+++ b/dom/midi/midirMIDIPlatformService.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_midirMIDIPlatformService_h
+#define mozilla_dom_midirMIDIPlatformService_h
+
+#include "mozilla/StaticMutex.h"
+#include "mozilla/dom/MIDIPlatformService.h"
+#include "mozilla/dom/MIDITypes.h"
+#include "mozilla/dom/midi/midir_impl_ffi_generated.h"
+
+class nsIThread;
+struct MidirWrapper;
+
+namespace mozilla::dom {
+
+class MIDIPortInterface;
+
+/**
+ * Platform service implementation using the midir crate.
+ */
+class midirMIDIPlatformService : public MIDIPlatformService {
+ public:
+ midirMIDIPlatformService();
+ virtual void Init() override;
+ virtual void Refresh() override;
+ virtual void Open(MIDIPortParent* aPort) override;
+ virtual void Stop() override;
+ virtual void ScheduleSend(const nsAString& aPort) override;
+ virtual void ScheduleClose(MIDIPortParent* aPort) override;
+
+ void SendMessage(const nsAString& aPort, const MIDIMessage& aMessage);
+
+ private:
+ virtual ~midirMIDIPlatformService();
+
+ static void AddPort(const nsString* aId, const nsString* aName, bool aInput);
+ static void RemovePort(const nsString* aId, const nsString* aName,
+ bool aInput);
+ static void CheckAndReceive(const nsString* aId, const uint8_t* aData,
+ size_t aLength, const GeckoTimeStamp* aTimeStamp,
+ uint64_t aMicros);
+
+ // Wrapper around the midir Rust implementation.
+ MidirWrapper* mImplementation;
+
+ // The midir backends can invoke CheckAndReceive on arbitrary background
+ // threads, and so we dispatch events from there to the owner task queue.
+ // It's a little ambiguous whether midir can ever invoke CheckAndReceive
+ // on one of its platform-specific background threads after we've dropped
+ // the main instance. Just in case, we use a static mutex to avoid a potential
+ // race with dropping the primary reference to the task queue via
+ // ClearOnShutdown.
+ static StaticMutex gOwnerThreadMutex;
+ static nsCOMPtr<nsISerialEventTarget> gOwnerThread
+ MOZ_GUARDED_BY(gOwnerThreadMutex);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_midirMIDIPlatformService_h
diff --git a/dom/midi/midir_impl/Cargo.toml b/dom/midi/midir_impl/Cargo.toml
new file mode 100644
index 0000000000..1e6d4030bf
--- /dev/null
+++ b/dom/midi/midir_impl/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "midir_impl"
+version = "0.1.0"
+authors = ["Gabriele Svelto"]
+edition = "2018"
+license = "MPL-2.0"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+midir = "0.7.0"
+nsstring = { path = "../../../xpcom/rust/nsstring/" }
+uuid = { version = "1.0", features = ["v4"] }
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
diff --git a/dom/midi/midir_impl/cbindgen.toml b/dom/midi/midir_impl/cbindgen.toml
new file mode 100644
index 0000000000..1b80a85e45
--- /dev/null
+++ b/dom/midi/midir_impl/cbindgen.toml
@@ -0,0 +1,18 @@
+header = """/* 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/. */"""
+autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */
+"""
+include_version = true
+braces = "SameLine"
+line_length = 100
+tab_width = 2
+language = "C++"
+include_guard = "midir_impl_ffi_generated_h"
+includes = ["nsStringFwd.h", "nsTArrayForwardDeclare.h"]
+
+[defines]
+"target_os = windows" = "XP_WIN"
+
+[export.rename]
+"ThinVec" = "nsTArray"
diff --git a/dom/midi/midir_impl/moz.build b/dom/midi/midir_impl/moz.build
new file mode 100644
index 0000000000..f079022906
--- /dev/null
+++ b/dom/midi/midir_impl/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; 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/.
+
+if CONFIG["COMPILE_ENVIRONMENT"]:
+ # This tells mach to run cbindgen and that this header-file should be created
+ CbindgenHeader(
+ "midir_impl_ffi_generated.h",
+ inputs=["/dom/midi/midir_impl"],
+ )
+
+ # This tells mach to copy that generated file to obj/dist/includes/mozilla/dom/midi
+ EXPORTS.mozilla.dom.midi += [
+ "!midir_impl_ffi_generated.h",
+ ]
diff --git a/dom/midi/midir_impl/src/lib.rs b/dom/midi/midir_impl/src/lib.rs
new file mode 100644
index 0000000000..de47fbeb11
--- /dev/null
+++ b/dom/midi/midir_impl/src/lib.rs
@@ -0,0 +1,418 @@
+extern crate thin_vec;
+
+use midir::{
+ InitError, MidiInput, MidiInputConnection, MidiInputPort, MidiOutput, MidiOutputConnection,
+ MidiOutputPort,
+};
+use nsstring::{nsAString, nsString};
+use std::boxed::Box;
+use std::ptr;
+use thin_vec::ThinVec;
+use uuid::Uuid;
+
+/* 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/. */
+extern crate midir;
+
+#[cfg(target_os = "windows")]
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct GeckoTimeStamp {
+ gtc: u64,
+ qpc: u64,
+
+ is_null: u8,
+ has_qpc: u8,
+}
+
+#[cfg(not(target_os = "windows"))]
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct GeckoTimeStamp {
+ value: u64,
+}
+
+enum MidiConnection {
+ Input(MidiInputConnection<CallbackData>),
+ Output(MidiOutputConnection),
+}
+
+struct MidiConnectionWrapper {
+ id: String,
+ connection: MidiConnection,
+}
+
+enum MidiPort {
+ Input(MidiInputPort),
+ Output(MidiOutputPort),
+}
+
+struct MidiPortWrapper {
+ id: String,
+ name: String,
+ port: MidiPort,
+ open_count: u32,
+}
+
+impl MidiPortWrapper {
+ fn input(self: &MidiPortWrapper) -> bool {
+ match self.port {
+ MidiPort::Input(_) => true,
+ MidiPort::Output(_) => false,
+ }
+ }
+}
+
+pub struct MidirWrapper {
+ ports: Vec<MidiPortWrapper>,
+ connections: Vec<MidiConnectionWrapper>,
+}
+
+struct CallbackData {
+ nsid: nsString,
+ open_timestamp: GeckoTimeStamp,
+}
+
+type AddCallback = unsafe extern "C" fn(id: &nsString, name: &nsString, input: bool);
+type RemoveCallback = AddCallback;
+
+impl MidirWrapper {
+ fn refresh(
+ self: &mut MidirWrapper,
+ add_callback: AddCallback,
+ remove_callback: Option<RemoveCallback>,
+ ) {
+ if let Ok(ports) = collect_ports() {
+ if let Some(remove_callback) = remove_callback {
+ self.remove_missing_ports(&ports, remove_callback);
+ }
+
+ self.add_new_ports(ports, add_callback);
+ }
+ }
+
+ fn remove_missing_ports(
+ self: &mut MidirWrapper,
+ ports: &Vec<MidiPortWrapper>,
+ remove_callback: RemoveCallback,
+ ) {
+ let old_ports = &mut self.ports;
+ let mut i = 0;
+ while i < old_ports.len() {
+ if !ports
+ .iter()
+ .any(|p| p.name == old_ports[i].name && p.input() == old_ports[i].input())
+ {
+ let port = old_ports.remove(i);
+ let id = nsString::from(&port.id);
+ let name = nsString::from(&port.name);
+ unsafe { remove_callback(&id, &name, port.input()) };
+ } else {
+ i += 1;
+ }
+ }
+ }
+
+ fn add_new_ports(
+ self: &mut MidirWrapper,
+ ports: Vec<MidiPortWrapper>,
+ add_callback: AddCallback,
+ ) {
+ for port in ports {
+ if !self.is_port_present(&port) && !Self::is_microsoft_synth_output(&port) {
+ let id = nsString::from(&port.id);
+ let name = nsString::from(&port.name);
+ unsafe { add_callback(&id, &name, port.input()) };
+ self.ports.push(port);
+ }
+ }
+ }
+
+ fn is_port_present(self: &MidirWrapper, port: &MidiPortWrapper) -> bool {
+ self.ports
+ .iter()
+ .any(|p| p.name == port.name && p.input() == port.input())
+ }
+
+ // We explicitly disable Microsoft's soft synthesizer, see bug 1798097
+ fn is_microsoft_synth_output(port: &MidiPortWrapper) -> bool {
+ (port.input() == false) && (port.name == "Microsoft GS Wavetable Synth")
+ }
+
+ fn open_port(
+ self: &mut MidirWrapper,
+ nsid: &nsString,
+ timestamp: GeckoTimeStamp,
+ callback: unsafe extern "C" fn(
+ id: &nsString,
+ data: *const u8,
+ length: usize,
+ timestamp: &GeckoTimeStamp,
+ micros: u64,
+ ),
+ ) -> Result<(), ()> {
+ let id = nsid.to_string();
+ let connections = &mut self.connections;
+ let port = self.ports.iter_mut().find(|e| e.id.eq(&id));
+ if let Some(port) = port {
+ if port.open_count == 0 {
+ let connection = match &port.port {
+ MidiPort::Input(port) => {
+ let input = MidiInput::new("WebMIDI input").map_err(|_err| ())?;
+ let data = CallbackData {
+ nsid: nsid.clone(),
+ open_timestamp: timestamp,
+ };
+ let connection = input
+ .connect(
+ port,
+ "Input connection",
+ move |stamp, message, data| unsafe {
+ callback(
+ &data.nsid,
+ message.as_ptr(),
+ message.len(),
+ &data.open_timestamp,
+ stamp,
+ );
+ },
+ data,
+ )
+ .map_err(|_err| ())?;
+ MidiConnectionWrapper {
+ id: id.clone(),
+ connection: MidiConnection::Input(connection),
+ }
+ }
+ MidiPort::Output(port) => {
+ let output = MidiOutput::new("WebMIDI output").map_err(|_err| ())?;
+ let connection = output
+ .connect(port, "Output connection")
+ .map_err(|_err| ())?;
+ MidiConnectionWrapper {
+ connection: MidiConnection::Output(connection),
+ id: id.clone(),
+ }
+ }
+ };
+
+ connections.push(connection);
+ }
+
+ port.open_count += 1;
+ return Ok(());
+ }
+
+ Err(())
+ }
+
+ fn close_port(self: &mut MidirWrapper, id: &str) {
+ let port = self.ports.iter_mut().find(|e| e.id.eq(&id)).unwrap();
+ port.open_count -= 1;
+
+ if port.open_count > 0 {
+ return;
+ }
+
+ let connections = &mut self.connections;
+ let index = connections.iter().position(|e| e.id.eq(id)).unwrap();
+ let connection_wrapper = connections.remove(index);
+
+ match connection_wrapper.connection {
+ MidiConnection::Input(connection) => {
+ connection.close();
+ }
+ MidiConnection::Output(connection) => {
+ connection.close();
+ }
+ }
+ }
+
+ fn send(self: &mut MidirWrapper, id: &str, data: &[u8]) -> Result<(), ()> {
+ let connections = &mut self.connections;
+ let index = connections.iter().position(|e| e.id.eq(id)).ok_or(())?;
+ let connection_wrapper = connections.get_mut(index).unwrap();
+
+ match &mut connection_wrapper.connection {
+ MidiConnection::Output(connection) => {
+ connection.send(data).map_err(|_err| ())?;
+ }
+ _ => {
+ panic!("Sending on an input port!");
+ }
+ }
+
+ Ok(())
+ }
+}
+
+fn collect_ports() -> Result<Vec<MidiPortWrapper>, InitError> {
+ let input = MidiInput::new("WebMIDI input")?;
+ let output = MidiOutput::new("WebMIDI output")?;
+ let mut ports = Vec::<MidiPortWrapper>::new();
+ collect_input_ports(&input, &mut ports);
+ collect_output_ports(&output, &mut ports);
+ Ok(ports)
+}
+
+impl MidirWrapper {
+ fn new() -> Result<MidirWrapper, InitError> {
+ let ports = Vec::new();
+ let connections: Vec<MidiConnectionWrapper> = Vec::new();
+ Ok(MidirWrapper { ports, connections })
+ }
+}
+
+/// Create the C++ wrapper that will be used to talk with midir.
+///
+/// This function will be exposed to C++
+///
+/// # Safety
+///
+/// This function deliberately leaks the wrapper because ownership is
+/// transfered to the C++ code. Use [midir_impl_shutdown()] to free it.
+#[no_mangle]
+pub unsafe extern "C" fn midir_impl_init(callback: AddCallback) -> *mut MidirWrapper {
+ if let Ok(mut midir_impl) = MidirWrapper::new() {
+ midir_impl.refresh(callback, None);
+
+ // Gecko invokes this initialization on a separate thread from all the
+ // other operations, so make it clear to Rust this needs to be Send.
+ fn assert_send<T: Send>(_: &T) {}
+ assert_send(&midir_impl);
+
+ let midir_box = Box::new(midir_impl);
+ // Leak the object as it will be owned by the C++ code from now on
+ Box::leak(midir_box) as *mut _
+ } else {
+ ptr::null_mut()
+ }
+}
+
+/// Refresh the list of ports.
+///
+/// This function will be exposed to C++
+///
+/// # Safety
+///
+/// `wrapper` must be the pointer returned by [midir_impl_init()].
+#[no_mangle]
+pub unsafe extern "C" fn midir_impl_refresh(
+ wrapper: *mut MidirWrapper,
+ add_callback: AddCallback,
+ remove_callback: RemoveCallback,
+) {
+ (*wrapper).refresh(add_callback, Some(remove_callback))
+}
+
+/// Shutdown midir and free the C++ wrapper.
+///
+/// This function will be exposed to C++
+///
+/// # Safety
+///
+/// `wrapper` must be the pointer returned by [midir_impl_init()]. After this
+/// has been called the wrapper object will be destoyed and cannot be accessed
+/// anymore.
+#[no_mangle]
+pub unsafe extern "C" fn midir_impl_shutdown(wrapper: *mut MidirWrapper) {
+ // The MidirImpl object will be automatically destroyed when the contents
+ // of this box are automatically dropped at the end of the function
+ let _midir_box = Box::from_raw(wrapper);
+}
+
+/// Open a MIDI port.
+///
+/// This function will be exposed to C++
+///
+/// # Safety
+///
+/// `wrapper` must be the pointer returned by [midir_impl_init()].
+#[no_mangle]
+pub unsafe extern "C" fn midir_impl_open_port(
+ wrapper: *mut MidirWrapper,
+ nsid: *mut nsString,
+ timestamp: *mut GeckoTimeStamp,
+ callback: unsafe extern "C" fn(
+ id: &nsString,
+ data: *const u8,
+ length: usize,
+ timestamp: &GeckoTimeStamp,
+ micros: u64,
+ ),
+) -> bool {
+ (*wrapper)
+ .open_port(nsid.as_ref().unwrap(), *timestamp, callback)
+ .is_ok()
+}
+
+/// Close a MIDI port.
+///
+/// This function will be exposed to C++
+///
+/// # Safety
+///
+/// `wrapper` must be the pointer returned by [midir_impl_init()].
+#[no_mangle]
+pub unsafe extern "C" fn midir_impl_close_port(wrapper: *mut MidirWrapper, id: *mut nsString) {
+ (*wrapper).close_port(&(*id).to_string());
+}
+
+/// Send a message over a MIDI output port.
+///
+/// This function will be exposed to C++
+///
+/// # Safety
+///
+/// `wrapper` must be the pointer returned by [midir_impl_init()].
+#[no_mangle]
+pub unsafe extern "C" fn midir_impl_send(
+ wrapper: *mut MidirWrapper,
+ id: *const nsAString,
+ data: *const ThinVec<u8>,
+) -> bool {
+ (*wrapper)
+ .send(&(*id).to_string(), (*data).as_slice())
+ .is_ok()
+}
+
+fn collect_input_ports(input: &MidiInput, wrappers: &mut Vec<MidiPortWrapper>) {
+ let ports = input.ports();
+ for port in ports {
+ let id = Uuid::new_v4()
+ .as_hyphenated()
+ .encode_lower(&mut Uuid::encode_buffer())
+ .to_owned();
+ let name = input
+ .port_name(&port)
+ .unwrap_or_else(|_| "unknown input port".to_string());
+ let port = MidiPortWrapper {
+ id,
+ name,
+ port: MidiPort::Input(port),
+ open_count: 0,
+ };
+ wrappers.push(port);
+ }
+}
+
+fn collect_output_ports(output: &MidiOutput, wrappers: &mut Vec<MidiPortWrapper>) {
+ let ports = output.ports();
+ for port in ports {
+ let id = Uuid::new_v4()
+ .as_hyphenated()
+ .encode_lower(&mut Uuid::encode_buffer())
+ .to_owned();
+ let name = output
+ .port_name(&port)
+ .unwrap_or_else(|_| "unknown input port".to_string());
+ let port = MidiPortWrapper {
+ id,
+ name,
+ port: MidiPort::Output(port),
+ open_count: 0,
+ };
+ wrappers.push(port);
+ }
+}
diff --git a/dom/midi/moz.build b/dom/midi/moz.build
new file mode 100644
index 0000000000..80ec11b68c
--- /dev/null
+++ b/dom/midi/moz.build
@@ -0,0 +1,76 @@
+# -*- 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",
+ "MIDILog.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")
+
+if CONFIG["MOZ_WEBMIDI_MIDIR_IMPL"]:
+ DEFINES["MOZ_WEBMIDI_MIDIR_IMPL"] = True
+ DIRS += ["midir_impl"]
+ UNIFIED_SOURCES += [
+ "midirMIDIPlatformService.cpp",
+ ]
+
+ if CONFIG["OS_TARGET"] == "Linux":
+ OS_LIBS += ["asound"] # Required by midir
+ UNIFIED_SOURCES += ["AlsaCompatibility.cpp"]
+
+FINAL_LIBRARY = "xul"
+LOCAL_INCLUDES += [
+ "/dom/base",
+]
+
+MOCHITEST_MANIFESTS += ["tests/mochitest.toml"]
+BROWSER_CHROME_MANIFESTS += ["tests/browser.toml"]
diff --git a/dom/midi/tests/MIDITestUtils.js b/dom/midi/tests/MIDITestUtils.js
new file mode 100644
index 0000000000..779a961991
--- /dev/null
+++ b/dom/midi/tests/MIDITestUtils.js
@@ -0,0 +1,94 @@
+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: {
+ get id() {
+ return MIDITestUtils.stableId(this);
+ },
+ name: "Test Control MIDI Device Input Port",
+ manufacturer: "Test Manufacturer",
+ version: "1.0.0",
+ },
+ outputInfo: {
+ get id() {
+ return MIDITestUtils.stableId(this);
+ },
+ name: "Test Control MIDI Device Output Port",
+ manufacturer: "Test Manufacturer",
+ version: "1.0.0",
+ },
+ stateTestInputInfo: {
+ get id() {
+ return MIDITestUtils.stableId(this);
+ },
+ name: "Test State MIDI Device Input Port",
+ manufacturer: "Test Manufacturer",
+ version: "1.0.0",
+ },
+ stateTestOutputInfo: {
+ get id() {
+ return MIDITestUtils.stableId(this);
+ },
+ name: "Test State MIDI Device Output Port",
+ manufacturer: "Test Manufacturer",
+ version: "1.0.0",
+ },
+ alwaysClosedTestOutputInfo: {
+ get id() {
+ return MIDITestUtils.stableId(this);
+ },
+ 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.");
+ }
+ },
+ stableId: async info => {
+ // This computes the stable ID of a MIDI port according to the logic we
+ // use in the Web MIDI implementation. See MIDIPortChild::GenerateStableId()
+ // and nsContentUtils::AnonymizeId().
+ const id = info.name + info.manufacturer + info.version;
+ const encoder = new TextEncoder();
+ const data = encoder.encode(id);
+ const keyBytes = encoder.encode(self.origin);
+ const key = await crypto.subtle.importKey(
+ "raw",
+ keyBytes,
+ { name: "HMAC", hash: "SHA-256" },
+ false,
+ ["sign"]
+ );
+ const result = new Uint8Array(await crypto.subtle.sign("HMAC", key, data));
+ let resultString = "";
+ for (let i = 0; i < result.length; i++) {
+ resultString += String.fromCharCode(result[i]);
+ }
+ return btoa(resultString);
+ },
+};
diff --git a/dom/midi/tests/blank.html b/dom/midi/tests/blank.html
new file mode 100644
index 0000000000..7fdd0621f6
--- /dev/null
+++ b/dom/midi/tests/blank.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html>
+<meta charset="utf8">
+<html>
+<body></body>
+</html>
diff --git a/dom/midi/tests/browser.toml b/dom/midi/tests/browser.toml
new file mode 100644
index 0000000000..1741a9e25f
--- /dev/null
+++ b/dom/midi/tests/browser.toml
@@ -0,0 +1,23 @@
+[DEFAULT]
+prefs = [
+ "dom.webmidi.enabled=true",
+ "midi.testing=true",
+ "midi.prompt.testing=true",
+ "media.navigator.permission.disabled=true",
+ "dom.sitepermsaddon-provider.enabled=true",
+]
+
+["browser_midi_permission_gated.js"]
+support-files = ["blank.html"]
+skip-if = ["a11y_checks"] # Bug 1858041 clicked popup-notification-primary-button may not be focusable, intermittent results (passes on Try, fails on Autoland)
+
+["browser_refresh_port_list.js"]
+run-if = ["os != 'android'"]
+support-files = ["refresh_port_list.html"]
+
+["browser_stable_midi_port_ids.js"]
+run-if = ["os != 'android'"]
+support-files = [
+ "port_ids_page_1.html",
+ "port_ids_page_2.html",
+]
diff --git a/dom/midi/tests/browser_midi_permission_gated.js b/dom/midi/tests/browser_midi_permission_gated.js
new file mode 100644
index 0000000000..2bdce51a2d
--- /dev/null
+++ b/dom/midi/tests/browser_midi_permission_gated.js
@@ -0,0 +1,839 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const EXAMPLE_COM_URL =
+ "https://example.com/document-builder.sjs?html=<h1>Test midi permission with synthetic site permission addon</h1>";
+const PAGE_WITH_IFRAMES_URL = `https://example.org/document-builder.sjs?html=
+ <h1>Test midi permission with synthetic site permission addon in iframes</h1>
+ <iframe id=sameOrigin src="${encodeURIComponent(
+ 'https://example.org/document-builder.sjs?html=SameOrigin"'
+ )}"></iframe>
+ <iframe id=crossOrigin src="${encodeURIComponent(
+ 'https://example.net/document-builder.sjs?html=CrossOrigin"'
+ )}"></iframe>`;
+
+const l10n = new Localization(
+ [
+ "browser/addonNotifications.ftl",
+ "toolkit/global/extensions.ftl",
+ "toolkit/global/extensionPermissions.ftl",
+ "branding/brand.ftl",
+ ],
+ true
+);
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+ChromeUtils.defineESModuleGetters(this, {
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+});
+
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["midi.prompt.testing", false]],
+ });
+
+ AddonTestUtils.initMochitest(this);
+ AddonTestUtils.hookAMTelemetryEvents();
+
+ // Once the addon is installed, a dialog is displayed as a confirmation.
+ // This could interfere with tests running after this one, so we set up a listener
+ // that will always accept post install dialogs so we don't have to deal with them in
+ // the test.
+ alwaysAcceptAddonPostInstallDialogs();
+
+ registerCleanupFunction(async () => {
+ // Remove the permission.
+ await SpecialPowers.removePermission("midi-sysex", {
+ url: EXAMPLE_COM_URL,
+ });
+ await SpecialPowers.removePermission("midi-sysex", {
+ url: PAGE_WITH_IFRAMES_URL,
+ });
+ await SpecialPowers.removePermission("midi", {
+ url: EXAMPLE_COM_URL,
+ });
+ await SpecialPowers.removePermission("midi", {
+ url: PAGE_WITH_IFRAMES_URL,
+ });
+ await SpecialPowers.removePermission("install", {
+ url: EXAMPLE_COM_URL,
+ });
+
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ });
+});
+
+add_task(async function testRequestMIDIAccess() {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, EXAMPLE_COM_URL);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ const testPageHost = gBrowser.selectedTab.linkedBrowser.documentURI.host;
+ Services.fog.testResetFOG();
+
+ info("Check that midi-sysex isn't set");
+ ok(
+ await SpecialPowers.testPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.UNKNOWN_ACTION,
+ { url: EXAMPLE_COM_URL }
+ ),
+ "midi-sysex value should have UNKNOWN permission"
+ );
+
+ info("Request midi-sysex access");
+ let onAddonInstallBlockedNotification = waitForNotification(
+ "addon-install-blocked"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
+ sysex: true,
+ });
+ });
+
+ info("Deny site permission addon install in first popup");
+ let addonInstallPanel = await onAddonInstallBlockedNotification;
+ const [installPopupHeader, installPopupMessage] =
+ addonInstallPanel.querySelectorAll(
+ "description.popup-notification-description"
+ );
+ is(
+ installPopupHeader.textContent,
+ l10n.formatValueSync("site-permission-install-first-prompt-midi-header"),
+ "First popup has expected header text"
+ );
+ is(
+ installPopupMessage.textContent,
+ l10n.formatValueSync("site-permission-install-first-prompt-midi-message"),
+ "First popup has expected message"
+ );
+
+ let notification = addonInstallPanel.childNodes[0];
+ // secondaryButton is the "Don't allow" button
+ notification.secondaryButton.click();
+
+ let rejectionMessage = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ let errorMessage;
+ try {
+ await content.midiAccessRequestPromise;
+ } catch (e) {
+ errorMessage = `${e.name}: ${e.message}`;
+ }
+
+ delete content.midiAccessRequestPromise;
+ return errorMessage;
+ }
+ );
+ is(
+ rejectionMessage,
+ "SecurityError: WebMIDI requires a site permission add-on to activate"
+ );
+
+ assertSitePermissionInstallTelemetryEvents(["site_warning", "cancelled"]);
+
+ info("Deny site permission addon install in second popup");
+ onAddonInstallBlockedNotification = waitForNotification(
+ "addon-install-blocked"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
+ sysex: true,
+ });
+ });
+ addonInstallPanel = await onAddonInstallBlockedNotification;
+ notification = addonInstallPanel.childNodes[0];
+ let dialogPromise = waitForInstallDialog();
+ notification.button.click();
+ let installDialog = await dialogPromise;
+ is(
+ installDialog.querySelector(".popup-notification-description").textContent,
+ l10n.formatValueSync(
+ "webext-site-perms-header-with-gated-perms-midi-sysex",
+ { hostname: testPageHost }
+ ),
+ "Install dialog has expected header text"
+ );
+ is(
+ installDialog.querySelector("popupnotificationcontent description")
+ .textContent,
+ l10n.formatValueSync("webext-site-perms-description-gated-perms-midi"),
+ "Install dialog has expected description"
+ );
+
+ // secondaryButton is the "Cancel" button
+ installDialog.secondaryButton.click();
+
+ rejectionMessage = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ let errorMessage;
+ try {
+ await content.midiAccessRequestPromise;
+ } catch (e) {
+ errorMessage = `${e.name}: ${e.message}`;
+ }
+
+ delete content.midiAccessRequestPromise;
+ return errorMessage;
+ }
+ );
+ is(
+ rejectionMessage,
+ "SecurityError: WebMIDI requires a site permission add-on to activate"
+ );
+
+ assertSitePermissionInstallTelemetryEvents([
+ "site_warning",
+ "permissions_prompt",
+ "cancelled",
+ ]);
+
+ info("Request midi-sysex access again");
+ onAddonInstallBlockedNotification = waitForNotification(
+ "addon-install-blocked"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
+ sysex: true,
+ });
+ });
+
+ info("Accept site permission addon install");
+ addonInstallPanel = await onAddonInstallBlockedNotification;
+ notification = addonInstallPanel.childNodes[0];
+ dialogPromise = waitForInstallDialog();
+ notification.button.click();
+ installDialog = await dialogPromise;
+ installDialog.button.click();
+
+ info("Wait for the midi-sysex access request promise to resolve");
+ let accessGranted = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ try {
+ await content.midiAccessRequestPromise;
+ return true;
+ } catch (e) {}
+
+ delete content.midiAccessRequestPromise;
+ return false;
+ }
+ );
+ ok(accessGranted, "requestMIDIAccess resolved");
+
+ info("Check that midi-sysex is now set");
+ ok(
+ await SpecialPowers.testPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.ALLOW_ACTION,
+ { url: EXAMPLE_COM_URL }
+ ),
+ "midi-sysex value should have ALLOW permission"
+ );
+ ok(
+ await SpecialPowers.testPermission(
+ "midi",
+ SpecialPowers.Services.perms.UNKNOWN_ACTION,
+ { url: EXAMPLE_COM_URL }
+ ),
+ "but midi should have UNKNOWN permission"
+ );
+
+ info("Check that we don't prompt user again once they installed the addon");
+ const accessPromiseState = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ return content.navigator
+ .requestMIDIAccess({ sysex: true })
+ .then(() => "resolved");
+ }
+ );
+ is(
+ accessPromiseState,
+ "resolved",
+ "requestMIDIAccess resolved without user prompt"
+ );
+
+ assertSitePermissionInstallTelemetryEvents([
+ "site_warning",
+ "permissions_prompt",
+ "completed",
+ ]);
+
+ info("Request midi access without sysex");
+ onAddonInstallBlockedNotification = waitForNotification(
+ "addon-install-blocked"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.midiNoSysexAccessRequestPromise =
+ content.navigator.requestMIDIAccess();
+ });
+
+ info("Accept site permission addon install");
+ addonInstallPanel = await onAddonInstallBlockedNotification;
+ notification = addonInstallPanel.childNodes[0];
+
+ is(
+ notification
+ .querySelector("#addon-install-blocked-info")
+ .getAttribute("href"),
+ Services.urlFormatter.formatURLPref("app.support.baseURL") +
+ "site-permission-addons",
+ "Got the expected SUMO page as a learn more link in the addon-install-blocked panel"
+ );
+
+ dialogPromise = waitForInstallDialog();
+ notification.button.click();
+ installDialog = await dialogPromise;
+
+ is(
+ installDialog.querySelector(".popup-notification-description").textContent,
+ l10n.formatValueSync("webext-site-perms-header-with-gated-perms-midi", {
+ hostname: testPageHost,
+ }),
+ "Install dialog has expected header text"
+ );
+ is(
+ installDialog.querySelector("popupnotificationcontent description")
+ .textContent,
+ l10n.formatValueSync("webext-site-perms-description-gated-perms-midi"),
+ "Install dialog has expected description"
+ );
+
+ installDialog.button.click();
+
+ info("Wait for the midi access request promise to resolve");
+ accessGranted = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ try {
+ await content.midiNoSysexAccessRequestPromise;
+ return true;
+ } catch (e) {}
+
+ delete content.midiNoSysexAccessRequestPromise;
+ return false;
+ }
+ );
+ ok(accessGranted, "requestMIDIAccess resolved");
+
+ info("Check that both midi-sysex and midi are now set");
+ ok(
+ await SpecialPowers.testPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.ALLOW_ACTION,
+ { url: EXAMPLE_COM_URL }
+ ),
+ "midi-sysex value should have ALLOW permission"
+ );
+ ok(
+ await SpecialPowers.testPermission(
+ "midi",
+ SpecialPowers.Services.perms.ALLOW_ACTION,
+ { url: EXAMPLE_COM_URL }
+ ),
+ "and midi value should also have ALLOW permission"
+ );
+
+ assertSitePermissionInstallTelemetryEvents([
+ "site_warning",
+ "permissions_prompt",
+ "completed",
+ ]);
+
+ info("Check that we don't prompt user again when they perm denied");
+ // remove permission to have a clean state
+ await SpecialPowers.removePermission("midi-sysex", {
+ url: EXAMPLE_COM_URL,
+ });
+
+ onAddonInstallBlockedNotification = waitForNotification(
+ "addon-install-blocked"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
+ sysex: true,
+ });
+ });
+
+ info("Perm-deny site permission addon install");
+ addonInstallPanel = await onAddonInstallBlockedNotification;
+ // Click the "Report Suspicious Site" menuitem, which has the same effect as
+ // "Never Allow" and also submits a telemetry event (which we check below).
+ notification.menupopup.querySelectorAll("menuitem")[1].click();
+
+ rejectionMessage = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ let errorMessage;
+ try {
+ await content.midiAccessRequestPromise;
+ } catch (e) {
+ errorMessage = e.name;
+ }
+
+ delete content.midiAccessRequestPromise;
+ return errorMessage;
+ }
+ );
+ is(rejectionMessage, "SecurityError", "requestMIDIAccess was rejected");
+
+ info("Request midi-sysex access again");
+ let denyIntervalStart = performance.now();
+ rejectionMessage = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ let errorMessage;
+ try {
+ await content.navigator.requestMIDIAccess({
+ sysex: true,
+ });
+ } catch (e) {
+ errorMessage = e.name;
+ }
+ return errorMessage;
+ }
+ );
+ is(
+ rejectionMessage,
+ "SecurityError",
+ "requestMIDIAccess was rejected without user prompt"
+ );
+ let denyIntervalElapsed = performance.now() - denyIntervalStart;
+ Assert.greaterOrEqual(
+ denyIntervalElapsed,
+ 3000,
+ `Rejection should be delayed by a randomized interval no less than 3 seconds (got ${
+ denyIntervalElapsed / 1000
+ } seconds)`
+ );
+
+ Assert.deepEqual(
+ [{ suspicious_site: "example.com" }],
+ AddonTestUtils.getAMGleanEvents("reportSuspiciousSite"),
+ "Expected Glean event recorded."
+ );
+
+ // Invoking getAMTelemetryEvents resets the mocked event array, and we want
+ // to test two different things here, so we cache it.
+ let events = AddonTestUtils.getAMTelemetryEvents();
+ Assert.deepEqual(
+ events.filter(evt => evt.method == "reportSuspiciousSite")[0],
+ {
+ method: "reportSuspiciousSite",
+ object: "suspiciousSite",
+ value: "example.com",
+ extra: undefined,
+ }
+ );
+ assertSitePermissionInstallTelemetryEvents(
+ ["site_warning", "cancelled"],
+ events
+ );
+});
+
+add_task(async function testIframeRequestMIDIAccess() {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ PAGE_WITH_IFRAMES_URL
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ info("Check that midi-sysex isn't set");
+ ok(
+ await SpecialPowers.testPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.UNKNOWN_ACTION,
+ { url: PAGE_WITH_IFRAMES_URL }
+ ),
+ "midi-sysex value should have UNKNOWN permission"
+ );
+
+ info("Request midi-sysex access from the same-origin iframe");
+ const sameOriginIframeBrowsingContext = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ return content.document.getElementById("sameOrigin").browsingContext;
+ }
+ );
+
+ let onAddonInstallBlockedNotification = waitForNotification(
+ "addon-install-blocked"
+ );
+ await SpecialPowers.spawn(sameOriginIframeBrowsingContext, [], () => {
+ content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
+ sysex: true,
+ });
+ });
+
+ info("Accept site permission addon install");
+ const addonInstallPanel = await onAddonInstallBlockedNotification;
+ const notification = addonInstallPanel.childNodes[0];
+ const dialogPromise = waitForInstallDialog();
+ notification.button.click();
+ let installDialog = await dialogPromise;
+ installDialog.button.click();
+
+ info("Wait for the midi-sysex access request promise to resolve");
+ const accessGranted = await SpecialPowers.spawn(
+ sameOriginIframeBrowsingContext,
+ [],
+ async () => {
+ try {
+ await content.midiAccessRequestPromise;
+ return true;
+ } catch (e) {}
+
+ delete content.midiAccessRequestPromise;
+ return false;
+ }
+ );
+ ok(accessGranted, "requestMIDIAccess resolved");
+
+ info("Check that midi-sysex is now set");
+ ok(
+ await SpecialPowers.testPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.ALLOW_ACTION,
+ { url: PAGE_WITH_IFRAMES_URL }
+ ),
+ "midi-sysex value should have ALLOW permission"
+ );
+
+ info(
+ "Check that we don't prompt user again once they installed the addon from the same-origin iframe"
+ );
+ const accessPromiseState = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ return content.navigator
+ .requestMIDIAccess({ sysex: true })
+ .then(() => "resolved");
+ }
+ );
+ is(
+ accessPromiseState,
+ "resolved",
+ "requestMIDIAccess resolved without user prompt"
+ );
+
+ assertSitePermissionInstallTelemetryEvents([
+ "site_warning",
+ "permissions_prompt",
+ "completed",
+ ]);
+
+ info("Check that request is rejected when done from a cross-origin iframe");
+ const crossOriginIframeBrowsingContext = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ return content.document.getElementById("crossOrigin").browsingContext;
+ }
+ );
+
+ const onConsoleErrorMessage = new Promise(resolve => {
+ const errorListener = {
+ observe(error) {
+ if (error.message.includes("WebMIDI access request was denied")) {
+ resolve(error);
+ Services.console.unregisterListener(errorListener);
+ }
+ },
+ };
+ Services.console.registerListener(errorListener);
+ });
+
+ const rejectionMessage = await SpecialPowers.spawn(
+ crossOriginIframeBrowsingContext,
+ [],
+ async () => {
+ let errorName;
+ try {
+ await content.navigator.requestMIDIAccess({
+ sysex: true,
+ });
+ } catch (e) {
+ errorName = e.name;
+ }
+ return errorName;
+ }
+ );
+
+ is(
+ rejectionMessage,
+ "SecurityError",
+ "requestMIDIAccess from the remote iframe was rejected"
+ );
+
+ const consoleErrorMessage = await onConsoleErrorMessage;
+ ok(
+ consoleErrorMessage.message.includes(
+ `WebMIDI access request was denied: ❝SitePermsAddons can't be installed from cross origin subframes❞`,
+ "an error message is sent to the console"
+ )
+ );
+ assertSitePermissionInstallTelemetryEvents([]);
+});
+
+add_task(async function testRequestMIDIAccessLocalhost() {
+ const httpServer = new HttpServer();
+ httpServer.start(-1);
+ httpServer.registerPathHandler(`/test`, function (request, response) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(`
+ <!DOCTYPE html>
+ <meta charset=utf8>
+ <h1>Test requestMIDIAccess on lcoalhost</h1>`);
+ });
+ const localHostTestUrl = `http://localhost:${httpServer.identity.primaryPort}/test`;
+
+ registerCleanupFunction(async function cleanup() {
+ await new Promise(resolve => httpServer.stop(resolve));
+ });
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, localHostTestUrl);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ info("Check that midi-sysex isn't set");
+ ok(
+ await SpecialPowers.testPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.UNKNOWN_ACTION,
+ { url: localHostTestUrl }
+ ),
+ "midi-sysex value should have UNKNOWN permission"
+ );
+
+ info(
+ "Request midi-sysex access should not prompt for addon install on locahost, but for permission"
+ );
+ let popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.midiAccessRequestPromise = content.navigator.requestMIDIAccess({
+ sysex: true,
+ });
+ });
+ await popupShown;
+ is(
+ PopupNotifications.panel.querySelector("popupnotification").id,
+ "midi-notification",
+ "midi notification was displayed"
+ );
+
+ info("Accept permission");
+ PopupNotifications.panel
+ .querySelector(".popup-notification-primary-button")
+ .click();
+
+ info("Wait for the midi-sysex access request promise to resolve");
+ const accessGranted = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async () => {
+ try {
+ await content.midiAccessRequestPromise;
+ return true;
+ } catch (e) {}
+
+ delete content.midiAccessRequestPromise;
+ return false;
+ }
+ );
+ ok(accessGranted, "requestMIDIAccess resolved");
+
+ info("Check that we prompt user again even if they accepted before");
+ popupShown = BrowserTestUtils.waitForEvent(
+ PopupNotifications.panel,
+ "popupshown"
+ );
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async () => {
+ content.navigator.requestMIDIAccess({ sysex: true });
+ });
+ await popupShown;
+ is(
+ PopupNotifications.panel.querySelector("popupnotification").id,
+ "midi-notification",
+ "midi notification was displayed again"
+ );
+
+ assertSitePermissionInstallTelemetryEvents([]);
+});
+
+add_task(async function testDisabledRequestMIDIAccessFile() {
+ let dir = getChromeDir(getResolvedURI(gTestPath));
+ dir.append("blank.html");
+ const fileSchemeTestUri = Services.io.newFileURI(dir).spec;
+
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, fileSchemeTestUri);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ info("Check that requestMIDIAccess isn't set on navigator on file scheme");
+ const isRequestMIDIAccessDefined = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ () => {
+ return "requestMIDIAccess" in content.wrappedJSObject.navigator;
+ }
+ );
+ is(
+ isRequestMIDIAccessDefined,
+ false,
+ "navigator.requestMIDIAccess is not defined on file scheme"
+ );
+});
+
+// Ignore any additional telemetry events collected in this file.
+// Unfortunately it doesn't work to have this in a cleanup function.
+// Keep this as the last task done.
+add_task(function teardown_telemetry_events() {
+ AddonTestUtils.getAMTelemetryEvents();
+});
+
+/**
+ * Check that the expected sitepermission install events are recorded.
+ *
+ * @param {Array<String>} expectedSteps: An array of the expected extra.step values recorded.
+ */
+function assertSitePermissionInstallTelemetryEvents(
+ expectedSteps,
+ events = null
+) {
+ let amInstallEvents = (events ?? AddonTestUtils.getAMTelemetryEvents())
+ .filter(evt => evt.method === "install" && evt.object === "sitepermission")
+ .map(evt => evt.extra.step);
+
+ Assert.deepEqual(amInstallEvents, expectedSteps);
+}
+
+async function waitForInstallDialog(id = "addon-webext-permissions") {
+ let panel = await waitForNotification(id);
+ return panel.childNodes[0];
+}
+
+/**
+ * Adds an event listener that will listen for post-install dialog event and automatically
+ * close the dialogs.
+ */
+function alwaysAcceptAddonPostInstallDialogs() {
+ // Once the addon is installed, a dialog is displayed as a confirmation.
+ // This could interfere with tests running after this one, so we set up a listener
+ // that will always accept post install dialogs so we don't have to deal with them in
+ // the test.
+ const abortController = new AbortController();
+
+ const { AppMenuNotifications } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppMenuNotifications.sys.mjs"
+ );
+ info("Start listening and accept addon post-install notifications");
+ PanelUI.notificationPanel.addEventListener(
+ "popupshown",
+ async function popupshown() {
+ let notification = AppMenuNotifications.activeNotification;
+ if (!notification || notification.id !== "addon-installed") {
+ return;
+ }
+
+ let popupnotificationID = PanelUI._getPopupId(notification);
+ if (popupnotificationID) {
+ info("Accept post-install dialog");
+ let popupnotification = document.getElementById(popupnotificationID);
+ popupnotification?.button.click();
+ }
+ },
+ {
+ signal: abortController.signal,
+ }
+ );
+
+ registerCleanupFunction(async () => {
+ // Clear the listener at the end of the test file, to prevent it to stay
+ // around when the same browser instance may be running other unrelated
+ // test files.
+ abortController.abort();
+ });
+}
+
+const PROGRESS_NOTIFICATION = "addon-progress";
+async function waitForNotification(notificationId) {
+ info(`Waiting for ${notificationId} notification`);
+
+ let topic = getObserverTopic(notificationId);
+
+ let observerPromise;
+ if (notificationId !== "addon-webext-permissions") {
+ observerPromise = new Promise(resolve => {
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ // Ignore the progress notification unless that is the notification we want
+ if (
+ notificationId != PROGRESS_NOTIFICATION &&
+ aTopic == getObserverTopic(PROGRESS_NOTIFICATION)
+ ) {
+ return;
+ }
+ Services.obs.removeObserver(observer, topic);
+ resolve();
+ }, topic);
+ });
+ }
+
+ let panelEventPromise = new Promise(resolve => {
+ window.PopupNotifications.panel.addEventListener(
+ "PanelUpdated",
+ function eventListener(e) {
+ // Skip notifications that are not the one that we are supposed to be looking for
+ if (!e.detail.includes(notificationId)) {
+ return;
+ }
+ window.PopupNotifications.panel.removeEventListener(
+ "PanelUpdated",
+ eventListener
+ );
+ resolve();
+ }
+ );
+ });
+
+ await observerPromise;
+ await panelEventPromise;
+ await waitForTick();
+
+ info(`Saw a ${notificationId} notification`);
+ await SimpleTest.promiseFocus(window.PopupNotifications.window);
+ return window.PopupNotifications.panel;
+}
+
+// This function is similar to the one in
+// toolkit/mozapps/extensions/test/xpinstall/browser_doorhanger_installs.js,
+// please keep both in sync!
+function getObserverTopic(aNotificationId) {
+ let topic = aNotificationId;
+ if (topic == "xpinstall-disabled") {
+ topic = "addon-install-disabled";
+ } else if (topic == "addon-progress") {
+ topic = "addon-install-started";
+ } else if (topic == "addon-installed") {
+ topic = "webextension-install-notify";
+ }
+ return topic;
+}
+
+function waitForTick() {
+ return new Promise(resolve => executeSoon(resolve));
+}
diff --git a/dom/midi/tests/browser_refresh_port_list.js b/dom/midi/tests/browser_refresh_port_list.js
new file mode 100644
index 0000000000..152b067254
--- /dev/null
+++ b/dom/midi/tests/browser_refresh_port_list.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const EXAMPLE_ORG_URL = "https://example.org/browser/dom/midi/tests/";
+const PAGE = "refresh_port_list.html";
+
+async function get_access(browser) {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ return content.wrappedJSObject.get_access();
+ });
+}
+
+async function reset_access(browser) {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ return content.wrappedJSObject.reset_access();
+ });
+}
+
+async function get_num_ports(browser) {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], function () {
+ return content.wrappedJSObject.get_num_ports();
+ });
+}
+
+async function add_port(browser) {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ return content.wrappedJSObject.add_port();
+ });
+}
+
+async function remove_port(browser) {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ return content.wrappedJSObject.remove_port();
+ });
+}
+
+async function force_refresh(browser) {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ return content.wrappedJSObject.force_refresh();
+ });
+}
+
+add_task(async function () {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(
+ gBrowser,
+ EXAMPLE_ORG_URL + PAGE
+ );
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ await get_access(gBrowser.selectedBrowser);
+ let ports_num = await get_num_ports(gBrowser.selectedBrowser);
+ Assert.equal(ports_num, 4, "We start with four ports");
+ await add_port(gBrowser.selectedBrowser);
+ ports_num = await get_num_ports(gBrowser.selectedBrowser);
+ Assert.equal(ports_num, 5, "One port is added manually");
+ // This causes the test service to refresh the ports the next time a refresh
+ // is requested, it will happen after we reload the tab later on and will add
+ // back the port that we're removing on the next line.
+ await force_refresh(gBrowser.selectedBrowser);
+ await remove_port(gBrowser.selectedBrowser);
+ ports_num = await get_num_ports(gBrowser.selectedBrowser);
+ Assert.equal(ports_num, 4, "One port is removed manually");
+
+ await BrowserTestUtils.reloadTab(gBrowser.selectedTab);
+
+ await get_access(gBrowser.selectedBrowser);
+ let refreshed_ports_num = await get_num_ports(gBrowser.selectedBrowser);
+ Assert.equal(refreshed_ports_num, 5, "One port is added by the refresh");
+
+ gBrowser.removeTab(gBrowser.selectedTab);
+});
diff --git a/dom/midi/tests/browser_stable_midi_port_ids.js b/dom/midi/tests/browser_stable_midi_port_ids.js
new file mode 100644
index 0000000000..e7d3056160
--- /dev/null
+++ b/dom/midi/tests/browser_stable_midi_port_ids.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const EXAMPLE_COM_URL = "https://example.com/browser/dom/midi/tests/";
+const EXAMPLE_ORG_URL = "https://example.org/browser/dom/midi/tests/";
+const PAGE1 = "port_ids_page_1.html";
+const PAGE2 = "port_ids_page_2.html";
+
+// Return the MIDI port id of the first input port for the given URL and page
+function id_for_tab(url, page) {
+ return BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: url + page,
+ waitForLoad: true,
+ },
+ async function (browser) {
+ return SpecialPowers.spawn(browser, [""], function () {
+ return content.wrappedJSObject.get_first_input_id();
+ });
+ }
+ );
+}
+
+add_task(async function () {
+ let com_page1;
+ let com_page1_reload;
+ let org_page1;
+ let org_page2;
+
+ [com_page1, com_page1_reload, org_page1, org_page2] = await Promise.all([
+ id_for_tab(EXAMPLE_COM_URL, PAGE1),
+ id_for_tab(EXAMPLE_COM_URL, PAGE1),
+ id_for_tab(EXAMPLE_ORG_URL, PAGE1),
+ id_for_tab(EXAMPLE_ORG_URL, PAGE2),
+ ]);
+ Assert.equal(
+ com_page1,
+ com_page1_reload,
+ "MIDI port ids should be the same when reloading the same page"
+ );
+ Assert.notEqual(
+ com_page1,
+ org_page1,
+ "MIDI port ids should be different in different origins"
+ );
+ Assert.equal(
+ org_page1,
+ org_page2,
+ "MIDI port ids should be the same in the same origin"
+ );
+});
diff --git a/dom/midi/tests/file_midi_permission_gated.html b/dom/midi/tests/file_midi_permission_gated.html
new file mode 100644
index 0000000000..8e3ed4d625
--- /dev/null
+++ b/dom/midi/tests/file_midi_permission_gated.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+ window.addEventListener("message", async (evt) => {
+ try {
+ await navigator.requestMIDIAccess({sysex: evt.data});
+ parent.postMessage("succeeded", "*");
+ } catch (ex) {
+ parent.postMessage("failed", "*");
+ }
+ });
+</script>
+<body></body>
+</html>
diff --git a/dom/midi/tests/mochitest.toml b/dom/midi/tests/mochitest.toml
new file mode 100644
index 0000000000..93a34003af
--- /dev/null
+++ b/dom/midi/tests/mochitest.toml
@@ -0,0 +1,41 @@
+[DEFAULT]
+support-files = [
+ "MIDITestUtils.js",
+ "file_midi_permission_gated.html",
+]
+scheme = "https"
+
+["test_midi_device_connect_disconnect.html"]
+disabled = "Bug 1437204"
+
+["test_midi_device_enumeration.html"]
+
+["test_midi_device_explicit_open_close.html"]
+
+["test_midi_device_implicit_open_close.html"]
+
+["test_midi_device_pending.html"]
+disabled = "Bug 1437204"
+
+["test_midi_device_sysex.html"]
+
+["test_midi_device_system_rt.html"]
+
+["test_midi_message_event.html"]
+
+["test_midi_packet_timing_sorting.html"]
+
+["test_midi_permission_allow.html"]
+
+["test_midi_permission_deny.html"]
+
+["test_midi_permission_gated.html"]
+skip-if = [
+ "os == 'android'", #Bug 1747637
+ "http3",
+ "http2",
+]
+
+["test_midi_permission_prompt.html"]
+
+["test_midi_send_messages.html"]
diff --git a/dom/midi/tests/port_ids_page_1.html b/dom/midi/tests/port_ids_page_1.html
new file mode 100644
index 0000000000..31dadad4a5
--- /dev/null
+++ b/dom/midi/tests/port_ids_page_1.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Stable MIDI port id test</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<script>
+ async function get_first_input_id() {
+ let access = await navigator.requestMIDIAccess({ sysex: false });
+ const inputs = access.inputs.values();
+ const input = inputs.next();
+ return input.value.id;
+ }
+</script>
+</body>
+</html>
diff --git a/dom/midi/tests/port_ids_page_2.html b/dom/midi/tests/port_ids_page_2.html
new file mode 100644
index 0000000000..8c313b04da
--- /dev/null
+++ b/dom/midi/tests/port_ids_page_2.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Stable MIDI port id test</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<script>
+ async function get_first_input_id() {
+ let access = await navigator.requestMIDIAccess({ sysex: false });
+ const inputs = access.inputs.values();
+ const input = inputs.next();
+ return input.value.id;
+}
+</script>
+</body>
+</html>
diff --git a/dom/midi/tests/refresh_port_list.html b/dom/midi/tests/refresh_port_list.html
new file mode 100644
index 0000000000..96e4a7a309
--- /dev/null
+++ b/dom/midi/tests/refresh_port_list.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Refresh MIDI port list test</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<script>
+ var access = null;
+ async function get_access() {
+ access = await navigator.requestMIDIAccess({ sysex: true });
+ }
+
+ async function reset_access() {
+ access = null;
+ }
+
+ async function get_num_ports() {
+ return access.inputs.size + access.outputs.size;
+ }
+
+ async function add_port() {
+ let addPortPromise = new Promise(resolve => {
+ access.addEventListener("statechange", (event) => { dump("***** 1 event.port.name = " + event.port.name + "event.connection = " + event.port.connection + "\n"); if (event.port.connection != "open") { resolve(); } });
+ });
+ const outputs = access.outputs.values();
+ const output = outputs.next().value;
+ output.send([0x90, 0x01, 0x00]);
+ await addPortPromise;
+ }
+
+ async function remove_port() {
+ let removePortPromise = new Promise(resolve => {
+ access.addEventListener("statechange", (event) => { dump("***** 2 event.port.name = " + event.port.name + "event.connection = " + event.port.connection + "\n"); if (event.port.connection != "open") { resolve(); } });
+ });
+ const outputs = access.outputs.values();
+ const output = outputs.next().value;
+ output.send([0x90, 0x02, 0x00]);
+ await removePortPromise;
+ }
+
+ async function force_refresh() {
+ const outputs = access.outputs.values();
+ const output = outputs.next().value;
+ output.send([0x90, 0x04, 0x00]);
+ }
+</script>
+</body>
+</html>
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..338d1de55d
--- /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(await 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..1dab1c8cf7
--- /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 = async (type, props, obj) => {
+ for (var prop in props) {
+ is(await 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 = await MIDITestUtils.inputInfo.id;
+ var output_id = await MIDITestUtils.outputInfo.id;
+ var inputs = access.inputs;
+ var outputs = access.outputs;
+ is(inputs.size, 1, "Should have one input");
+ is(outputs.size, 3, "Should have three 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);
+ await objectCompare("input", MIDITestUtils.inputInfo, input);
+ await 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..d3ed910a55
--- /dev/null
+++ b/dom/midi/tests/test_midi_device_explicit_open_close.html
@@ -0,0 +1,94 @@
+<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(await 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 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(await 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..cddbaf26c2
--- /dev/null
+++ b/dom/midi/tests/test_midi_device_implicit_open_close.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 access = await navigator.requestMIDIAccess({ "sysex": false });
+ ok(true, "MIDI Access Request successful");
+ is(access.sysexEnabled, false, "Sysex should be false");
+
+ var checkCount = 0;
+ var input;
+ var output;
+ function checkCallbacks(port) {
+ if (checkCount < 2) {
+ ok(port.connection === "open", "Got port " + port.connection + " for " + port.name);
+ } else {
+ ok(port.connection === "closed", "Got port " + port.connection + " for " + port.name);
+ }
+
+ checkCount++;
+ if (checkCount == 4) {
+ input.onstatechange = undefined;
+ output.onstatechange = undefined;
+ SimpleTest.finish();
+ }
+ }
+ function checkReturn(event) {
+ ok(true, "Got echo message back");
+ MIDITestUtils.checkPacket(event.data, [0x90, 0x00, 0x7f]);
+ input.close();
+ output.close();
+ }
+
+ input = access.inputs.get(await MIDITestUtils.inputInfo.id);
+ output = access.outputs.get(await MIDITestUtils.outputInfo.id);
+ input.onstatechange = (event) => { checkCallbacks(event.port); };
+ output.onstatechange = (event) => { checkCallbacks(event.port); };
+ // Ports are closed. Fire rest of tests.
+ input.onmidimessage = checkReturn;
+ output.send([0x90, 0x00, 0x7F]);
+ }
+ </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..2e6bd08420
--- /dev/null
+++ b/dom/midi/tests/test_midi_device_pending.html
@@ -0,0 +1,118 @@
+<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 output;
+ var test_ports = [];
+ let access;
+
+ let accessRes;
+ let accessPromise;
+ let portRes;
+ let portPromise;
+
+ function resetPromises() {
+ accessPromise = new Promise((res, rej) => { accessRes = res; });
+ portPromise = new Promise((res, rej) => { portRes = res; });
+ }
+
+ 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(await 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..618f54ac8a
--- /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(await MIDITestUtils.inputInfo.id);
+ output = access_sysex.outputs.get(await MIDITestUtils.outputInfo.id);
+ let input_sysex = access_sysex.inputs.get(await 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..81de0c3a94
--- /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, [0xFA]);
+ } else if (checkCount == 2) {
+ MIDITestUtils.checkPacket(msg.data, [0xF8]);
+ } 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(await MIDITestUtils.inputInfo.id);
+ input_sysex.onmidimessage = checkReturn;
+ let output_sysex = access_sysex.outputs.get(await MIDITestUtils.outputInfo.id);
+ output_sysex.send([0xF0, 0x01, 0xF7]);
+ }
+ </script>
+ </body>
+</html>
diff --git a/dom/midi/tests/test_midi_message_event.html b/dom/midi/tests/test_midi_message_event.html
new file mode 100644
index 0000000000..098b033008
--- /dev/null
+++ b/dom/midi/tests/test_midi_message_event.html
@@ -0,0 +1,45 @@
+<html>
+
+<head>
+ <title>WebMIDI MIDIMessageEvent 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>
+ <script class="testbody" type="application/javascript">
+ add_task(async () => {
+ await MIDITestUtils.permissionSetup(true);
+
+ is(new MIDIMessageEvent('eventType').bubbles, false, "bubbles field is false by default");
+ is(new MIDIMessageEvent('eventType').cancelable, false, "cancelable field is false by default");
+ isDeeply(new MIDIMessageEvent('eventType').data, [], "The default message is empty");
+
+ is(new MIDIMessageEvent('eventType', { bubbles: false }).bubbles, false, "bubbles is passed");
+ is(new MIDIMessageEvent('eventType', { bubbles: true }).bubbles, true, "bubbles is passed");
+
+ is(new MIDIMessageEvent('eventType', { cancelable: false }).cancelable, false, "cancelable is passed");
+ is(new MIDIMessageEvent('eventType', { cancelable: true }).cancelable, true, "cancelable is passed");
+
+ var data = new Uint8Array(16);
+ isDeeply(new MIDIMessageEvent('eventType', { data }).data, data, "data is passed");
+
+ // All initializers are passed.
+ data = new Uint8Array(3);
+ is(new MIDIMessageEvent('eventType', { bubbles: true, cancelable: true, data }).bubbles, true, "all initializers are passed");
+ is(new MIDIMessageEvent('eventType', { bubbles: true, cancelable: true, data }).cancelable, true, "all initializers are passed");
+ isDeeply(new MIDIMessageEvent('eventType', { bubbles: true, cancelable: true, data }).data, data, "all initializers are passed");
+
+ if (window.SharedArrayBuffer) {
+ data = new Uint8Array(new SharedArrayBuffer(3));
+ SimpleTest.doesThrow(() => { new MIDIMessageEvent('eventType', { data }); }, "shared array buffers are rejected");
+ } else {
+ todo(false, 'SharedArrayBuffer is unavailable.');
+ }
+ });
+ </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..3c344066ac
--- /dev/null
+++ b/dom/midi/tests/test_midi_packet_timing_sorting.html
@@ -0,0 +1,47 @@
+<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 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(await MIDITestUtils.inputInfo.id);
+ output = access.outputs.get(await 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_gated.html b/dom/midi/tests/test_midi_permission_gated.html
new file mode 100644
index 0000000000..0e85e99e9c
--- /dev/null
+++ b/dom/midi/tests/test_midi_permission_gated.html
@@ -0,0 +1,181 @@
+<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()">
+ <iframe id="subdomain"></iframe>
+ <iframe id="localhost"></iframe>
+ <script class="testbody" type="application/javascript">
+ SimpleTest.waitForExplicitFinish();
+ const filePath = "/tests/dom/midi/tests/file_midi_permission_gated.html";
+ // Generally this runs on example.com but with --enable-xorigin-tests it runs
+ // on example.org.
+ let subdomainURL = "https://test1." + location.host + filePath;
+ $("subdomain").src = subdomainURL;
+ // For some reason the mochitest server returns "Bad request" with localhost,
+ // but permits the loopback address. That's good enough for testing purposes.
+ $("localhost").src = "http://127.0.0.1:8888" + filePath;
+
+ function waitForMessage() {
+ return new Promise((resolve) => {
+ window.addEventListener("message", (e) => resolve(e.data), {once: true});
+ });
+ }
+
+ async function runTests() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.webmidi.enabled", true],
+ ["midi.testing", true],
+ ],
+ });
+ ok(
+ await SpecialPowers.testPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.UNKNOWN_ACTION,
+ document
+ ),
+ "midi-sysex value should have UNKNOWN permission"
+ );
+ ok(
+ await SpecialPowers.testPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.UNKNOWN_ACTION,
+ subdomainURL
+ ),
+ "permission should also not be set for subdomain"
+ );
+
+ let onChangeCalled = 0;
+ let onChangeCalledWithSysex = 0;
+ // We expect the same states with and without sysex support.
+ const expectedChangedStates = ["denied", "granted", "prompt"];
+
+ const results = [];
+ for (let sysex of [false, true]) {
+ let result = await navigator.permissions.query({ name: "midi", sysex });
+ is(result?.state, "prompt", "expected 'prompt' permission status");
+ // Register two unique listeners that should be invoked every time we
+ // change permissions in the rest of this test case: one with sysex
+ // support, and the other one without.
+ if (sysex) {
+ result.onchange = () => {
+ is(
+ result.state,
+ expectedChangedStates[onChangeCalledWithSysex++],
+ "expected change event with sysex support"
+ );
+ };
+ results.push(result);
+ } else {
+ result.onchange = () => {
+ is(
+ result.state,
+ expectedChangedStates[onChangeCalled++],
+ "expected change event"
+ );
+ };
+ results.push(result);
+ }
+ }
+
+ // Explicitly set the permission as blocked, and expect the
+ // `requestMIDIAccess` call to be automatically rejected (not having any
+ // permission set would trigger the synthetic addon install provided by
+ // AddonManager and SitePermsAddonProvider).
+ await SpecialPowers.addPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.DENY_ACTION,
+ document
+ );
+ await SpecialPowers.addPermission(
+ "midi",
+ SpecialPowers.Services.perms.DENY_ACTION,
+ document
+ );
+ for (let sysex of [false, true]) {
+ try {
+ await navigator.requestMIDIAccess({ sysex });
+ ok(false, "MIDI Access Request gate allowed but expected to be denied");
+ } catch (ex) {
+ ok(true, "MIDI Access Request denied by default");
+ }
+
+ let result = await navigator.permissions.query({ name: "midi", sysex });
+ // We expect "denied" because that's what has been set above (with
+ // `SpecialPowers.addPermission()`). In practice, this state should
+ // never be returned since explicit rejection is handled at the add-on
+ // installation level.
+ is(result?.state, "denied", "expected 'denied' permission status");
+ }
+
+ // Gated permission should prompt for localhost.
+ //
+ // Note: We don't appear to have good test machinery anymore for
+ // navigating prompts from a plain mochitest. If you uncomment the lines
+ // below and run the test interactively, it should pass. Given that this
+ // is a niche feature that's unlikely to break, it doesn't seem worth
+ // investing in complicated test infrastructure to check it in automation.
+ // for (let sysex of [false, true]) {
+ // $("localhost").contentWindow.postMessage(sysex, "*");
+ // let response = await waitForMessage();
+ // is(response, "succeeded", "MIDI Access Request allowed for localhost");
+ // }
+
+ // When an addon is installed, the permission is inserted. Test
+ // that the request succeeds after we insert the permission.
+ await SpecialPowers.addPermission(
+ "midi-sysex",
+ SpecialPowers.Services.perms.ALLOW_ACTION,
+ document
+ );
+ await SpecialPowers.addPermission(
+ "midi",
+ SpecialPowers.Services.perms.ALLOW_ACTION,
+ document
+ );
+ // Gated permission should allow access after addon inserted permission.
+ for (let sysex of [false, true]) {
+ try {
+ await navigator.requestMIDIAccess({ sysex });
+ ok(true, "MIDI Access Request allowed");
+ } catch (ex) {
+ ok(false, "MIDI Access Request failed");
+ }
+
+ let result = await navigator.permissions.query({ name: "midi", sysex });
+ is(result?.state, "granted", "expected 'granted' permission status");
+ }
+
+ // Gated permission should also apply to subdomains.
+ for (let sysex of [false, true]) {
+ $("subdomain").contentWindow.postMessage(sysex, "*");
+ let response = await waitForMessage();
+ is(response, "succeeded", "MIDI Access Request allowed for subdomain");
+ }
+
+ is(
+ onChangeCalled,
+ expectedChangedStates.length - 1,
+ `expected onchange listener to have been called ${expectedChangedStates.length - 1} times`
+ );
+ is(
+ onChangeCalledWithSysex,
+ expectedChangedStates.length - 1,
+ `expected onchange listener to have been called ${expectedChangedStates.length - 1} times (sysex)`
+ );
+
+ // Remove the permission.
+ await SpecialPowers.removePermission("midi-sysex", document);
+ await SpecialPowers.removePermission("midi", document);
+
+ results.forEach(result => result.onchange = null);
+
+ 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>
diff --git a/dom/midi/tests/test_midi_send_messages.html b/dom/midi/tests/test_midi_send_messages.html
new file mode 100644
index 0000000000..1d78709877
--- /dev/null
+++ b/dom/midi/tests/test_midi_send_messages.html
@@ -0,0 +1,112 @@
+<html>
+
+<head>
+ <title>WebMIDI Send 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);
+ const access = await navigator.requestMIDIAccess({ sysex: true });
+ const output = access.outputs.get(await MIDITestUtils.stateTestOutputInfo.id);
+
+
+ // Note on(off).
+ output.send([0xff, 0x90, 0x00, 0x00, 0x90, 0x07, 0x00]);
+
+ try {
+ output.send([0x00, 0x01])
+ } catch (ex) {
+ ok(true, "Caught exception");
+ }
+
+ // Running status is not allowed in Web MIDI API.
+ SimpleTest.doesThrow(() => output.send([0x00, 0x01]), "Running status is not allowed in Web MIDI API.");
+
+ // Unexpected End of Sysex.
+ SimpleTest.doesThrow(() => output.send([0xf7]), "Unexpected End of Sysex.");
+
+ // Unexpected reserved status bytes.
+ SimpleTest.doesThrow(() => output.send([0xf4]), "Unexpected reserved status byte 0xf4.");
+ SimpleTest.doesThrow(() => output.send([0xf5]), "Unexpected reserved status byte 0xf5.");
+ SimpleTest.doesThrow(() => output.send([0xf9]), "Unexpected reserved status byte 0xf9.");
+ SimpleTest.doesThrow(() => output.send([0xfd]), "Unexpected reserved status byte 0xfd.");
+
+ // Incomplete channel messages.
+ SimpleTest.doesThrow(() => output.send([0x80]), "Incomplete channel message.");
+ SimpleTest.doesThrow(() => output.send([0x80, 0x00]), "Incomplete channel message.");
+ SimpleTest.doesThrow(() => output.send([0x90]), "Incomplete channel message.");
+ SimpleTest.doesThrow(() => output.send([0x90, 0x00]), "Incomplete channel message.");
+ SimpleTest.doesThrow(() => output.send([0xa0]), "Incomplete channel message.");
+ SimpleTest.doesThrow(() => output.send([0xa0, 0x00]), "Incomplete channel message.");
+ SimpleTest.doesThrow(() => output.send([0xb0]), "Incomplete channel message.");
+ SimpleTest.doesThrow(() => output.send([0xb0, 0x00]), "Incomplete channel message.");
+ SimpleTest.doesThrow(() => output.send([0xc0]), "Incomplete channel message.");
+ SimpleTest.doesThrow(() => output.send([0xd0]), "Incomplete channel message.");
+ SimpleTest.doesThrow(() => output.send([0xe0]), "Incomplete channel message.");
+ SimpleTest.doesThrow(() => output.send([0xe0, 0x00]), "Incomplete channel message.");
+
+ // Incomplete system messages.
+ SimpleTest.doesThrow(() => output.send([0xf1]), "Incomplete system message.");
+ SimpleTest.doesThrow(() => output.send([0xf2]), "Incomplete system message.");
+ SimpleTest.doesThrow(() => output.send([0xf2, 0x00]), "Incomplete system message.");
+ SimpleTest.doesThrow(() => output.send([0xf3]), "Incomplete system message.");
+
+ // Invalid data bytes.
+ SimpleTest.doesThrow(() => output.send([0x80, 0x80, 0x00]), "Incomplete system message.");
+ SimpleTest.doesThrow(() => output.send([0x80, 0x00, 0x80]), "Incomplete system message.");
+
+ // Complete messages.
+ output.send([0x80, 0x00, 0x00]);
+ output.send([0x90, 0x00, 0x00]);
+ output.send([0xa0, 0x00, 0x00]);
+ output.send([0xb0, 0x00, 0x00]);
+ output.send([0xc0, 0x00]);
+ output.send([0xd0, 0x00]);
+ output.send([0xe0, 0x00, 0x00]);
+
+ // Real-Time messages.
+ output.send([0xf8]);
+ output.send([0xfa]);
+ output.send([0xfb]);
+ output.send([0xfc]);
+ output.send([0xfe]);
+ output.send([0xff]);
+
+ // Valid messages with Real-Time messages.
+ output.send([0x90, 0xff, 0xff, 0x00, 0xff, 0x01, 0xff, 0x80, 0xff, 0x00,
+ 0xff, 0xff, 0x00, 0xff, 0xff]);
+
+ // Sysex messages.
+ output.send([0xf0, 0x00, 0x01, 0x02, 0x03, 0xf7]);
+ output.send([0xf0, 0xf8, 0xf7, 0xff]);
+ SimpleTest.doesThrow(() => output.send([0xf0, 0x80, 0xf7]), "Invalid sysex message.");
+ SimpleTest.doesThrow(() => output.send([0xf0, 0xf0, 0xf7]), "Double begin sysex message.");
+ SimpleTest.doesThrow(() => output.send([0xf0, 0xff, 0xf7, 0xf7]), "Double end sysex message.");
+
+ // Reserved status bytes.
+ SimpleTest.doesThrow(() => output.send([0xf4, 0x80, 0x00, 0x00]), "Reserved status byte.");
+ SimpleTest.doesThrow(() => output.send([0x80, 0xf4, 0x00, 0x00]), "Reserved status byte.");
+ SimpleTest.doesThrow(() => output.send([0x80, 0x00, 0xf4, 0x00]), "Reserved status byte.");
+ SimpleTest.doesThrow(() => output.send([0x80, 0x00, 0x00, 0xf4]), "Reserved status byte.");
+ SimpleTest.doesThrow(() => output.send([0xf0, 0xff, 0xf4, 0xf7]), "Reserved status byte.");
+
+ // Invalid timestamps.
+ SimpleTest.doesThrow(() => output.send([], NaN), "NaN timestamp.");
+ SimpleTest.doesThrow(() => output.send([], Infinity), "Infinity timestamp.");
+ SimpleTest.doesThrow(() => output.send(new Uint8Array(), NaN), "NaN timestamp.");
+ SimpleTest.doesThrow(() => output.send(new Uint8Array(), Infinity), "Infinity timestamp.");
+
+ SimpleTest.finish();
+ }
+ </script>
+</body>
+
+</html>