Bug 1620308: Move RTCDTMFSender logic to a single c++ class. r=smaug,jib,mjf
authorByron Campen [:bwc] <docfaraday@gmail.com>
Wed, 25 Mar 2020 18:46:37 +0000
changeset 520429 8d0e79c4d8c41b49e5f5ce68d701503924aa4d47
parent 520428 e2ebf5fb7ae6ea2893a082b8022943d94735d176
child 520430 df3cdb2a806fb023ecd28cde98b4ec0f5ccbafcf
push id37250
push userdvarga@mozilla.com
push dateThu, 26 Mar 2020 04:04:15 +0000
treeherdermozilla-central@85bae8580dde [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, jib, mjf
bugs1620308
milestone76.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1620308: Move RTCDTMFSender logic to a single c++ class. r=smaug,jib,mjf Differential Revision: https://phabricator.services.mozilla.com/D67513
dom/bindings/Bindings.conf
dom/media/PeerConnection.jsm
dom/webidl/PeerConnectionImpl.webidl
dom/webidl/PeerConnectionObserver.webidl
dom/webidl/RTCDTMFSender.webidl
dom/webidl/RTCRtpTransceiver.webidl
dom/webidl/TransceiverImpl.webidl
media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
media/webrtc/signaling/src/peerconnection/RTCDTMFSender.cpp
media/webrtc/signaling/src/peerconnection/RTCDTMFSender.h
media/webrtc/signaling/src/peerconnection/TransceiverImpl.cpp
media/webrtc/signaling/src/peerconnection/TransceiverImpl.h
media/webrtc/signaling/src/peerconnection/moz.build
xpcom/ds/StaticAtoms.py
--- a/dom/bindings/Bindings.conf
+++ b/dom/bindings/Bindings.conf
@@ -679,16 +679,20 @@ DOMInterfaces = {
     'implicitJSContext': [ 'arrayBuffer', 'blob', 'formData', 'json', 'text',
                            'clone', 'cloneUnfiltered' ],
 },
 
 'RTCDataChannel': {
     'nativeType': 'nsDOMDataChannel',
 },
 
+'RTCDTMFSender': {
+    'headerFile': 'RTCDTMFSender.h'
+},
+
 'RTCRtpReceiver': {
     'headerFile': 'RTCRtpReceiver.h'
 },
 
 'RTCStatsReport': {
     'headerFile': 'RTCStatsReport.h'
 },
 
--- a/dom/media/PeerConnection.jsm
+++ b/dom/media/PeerConnection.jsm
@@ -15,34 +15,30 @@ ChromeUtils.defineModuleGetter(
 const PC_CONTRACT = "@mozilla.org/dom/peerconnection;1";
 const PC_OBS_CONTRACT = "@mozilla.org/dom/peerconnectionobserver;1";
 const PC_ICE_CONTRACT = "@mozilla.org/dom/rtcicecandidate;1";
 const PC_SESSION_CONTRACT = "@mozilla.org/dom/rtcsessiondescription;1";
 const PC_STATIC_CONTRACT = "@mozilla.org/dom/peerconnectionstatic;1";
 const PC_SENDER_CONTRACT = "@mozilla.org/dom/rtpsender;1";
 const PC_TRANSCEIVER_CONTRACT = "@mozilla.org/dom/rtptransceiver;1";
 const PC_COREQUEST_CONTRACT = "@mozilla.org/dom/createofferrequest;1";
-const PC_DTMF_SENDER_CONTRACT = "@mozilla.org/dom/rtcdtmfsender;1";
 
 const PC_CID = Components.ID("{bdc2e533-b308-4708-ac8e-a8bfade6d851}");
 const PC_OBS_CID = Components.ID("{d1748d4c-7f6a-4dc5-add6-d55b7678537e}");
 const PC_ICE_CID = Components.ID("{02b9970c-433d-4cc2-923d-f7028ac66073}");
 const PC_SESSION_CID = Components.ID("{1775081b-b62d-4954-8ffe-a067bbf508a7}");
 const PC_MANAGER_CID = Components.ID("{7293e901-2be3-4c02-b4bd-cbef6fc24f78}");
 const PC_STATIC_CID = Components.ID("{0fb47c47-a205-4583-a9fc-cbadf8c95880}");
 const PC_SENDER_CID = Components.ID("{4fff5d46-d827-4cd4-a970-8fd53977440e}");
 const PC_TRANSCEIVER_CID = Components.ID(
   "{09475754-103a-41f5-a2d0-e1f27eb0b537}"
 );
 const PC_COREQUEST_CID = Components.ID(
   "{74b2122d-65a8-4824-aa9e-3d664cb75dc2}"
 );
-const PC_DTMF_SENDER_CID = Components.ID(
-  "{3610C242-654E-11E6-8EC0-6D1BE389A607}"
-);
 
 function logMsg(msg, file, line, flag, winID) {
   let scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
   let scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
   scriptError.initWithWindowID(
     msg,
     file,
     null,
@@ -1477,29 +1473,16 @@ class RTCPeerConnection {
         }
 
         this._negotiationNeeded = true;
         this.dispatchEvent(new this._win.Event("negotiationneeded"));
       });
     });
   }
 
-  _insertDTMF(transceiverImpl, tones, duration, interToneGap) {
-    return this._impl.insertDTMF(
-      transceiverImpl,
-      tones,
-      duration,
-      interToneGap
-    );
-  }
-
-  _getDTMFToneBuffer(sender) {
-    return this._impl.getDTMFToneBuffer(sender.__DOM_IMPL__);
-  }
-
   _replaceTrackNoRenegotiation(transceiverImpl, withTrack) {
     this._impl.replaceTrackNoRenegotiation(transceiverImpl, withTrack);
   }
 
   close() {
     if (this._closed) {
       return;
     }
@@ -1983,24 +1966,16 @@ class PeerConnectionObserver {
     );
   }
 
   fireStreamEvent(stream) {
     const ev = new this._win.MediaStreamEvent("addstream", { stream });
     this.dispatchEvent(ev);
   }
 
-  onDTMFToneChange(track, tone) {
-    var pc = this._dompc;
-    var sender = pc.getSenders().find(sender => sender.track == track);
-    sender.dtmf.dispatchEvent(
-      new pc._win.RTCDTMFToneChangeEvent("tonechange", { tone })
-    );
-  }
-
   onPacket(level, type, sending, packet) {
     var pc = this._dompc;
     if (pc._onPacket) {
       pc._onPacket(level, type, sending, packet);
     }
   }
 
   syncTransceivers() {
@@ -2023,49 +1998,21 @@ class RTCPeerConnectionStatic {
   }
 }
 setupPrototype(RTCPeerConnectionStatic, {
   classID: PC_STATIC_CID,
   contractID: PC_STATIC_CONTRACT,
   QueryInterface: ChromeUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer]),
 });
 
-class RTCDTMFSender {
-  constructor(sender) {
-    this._sender = sender;
-  }
-
-  get toneBuffer() {
-    return this._sender._pc._getDTMFToneBuffer(this._sender);
-  }
-
-  get ontonechange() {
-    return this.__DOM_IMPL__.getEventHandler("ontonechange");
-  }
-
-  set ontonechange(handler) {
-    this.__DOM_IMPL__.setEventHandler("ontonechange", handler);
-  }
-
-  insertDTMF(tones, duration, interToneGap) {
-    this._sender._pc._checkClosed();
-    this._sender._transceiver.insertDTMF(tones, duration, interToneGap);
-  }
-}
-setupPrototype(RTCDTMFSender, {
-  classID: PC_DTMF_SENDER_CID,
-  contractID: PC_DTMF_SENDER_CONTRACT,
-  QueryInterface: ChromeUtils.generateQI([]),
-});
-
 class RTCRtpSender {
   constructor(pc, transceiverImpl, transceiver, track, kind, streams) {
     let dtmf = null;
     if (kind == "audio") {
-      dtmf = pc._win.RTCDTMFSender._create(pc._win, new RTCDTMFSender(this));
+      dtmf = transceiverImpl.dtmf;
     }
 
     Object.assign(this, {
       _pc: pc,
       _transceiverImpl: transceiverImpl,
       _transceiver: transceiver,
       track,
       _streams: streams,
@@ -2316,50 +2263,16 @@ class RTCRtpTransceiver {
   setMid(mid) {
     this.mid = mid;
   }
 
   // Used by _transceiverImpl.syncWithJS, don't call sync again!
   unsetMid() {
     this.mid = null;
   }
-
-  insertDTMF(tones, duration, interToneGap) {
-    if (this.stopped) {
-      throw new this._pc._win.DOMException(
-        "Transceiver is stopped!",
-        "InvalidStateError"
-      );
-    }
-
-    if (!this.sender.track) {
-      throw new this._pc._win.DOMException(
-        "RTCRtpSender has no track",
-        "InvalidStateError"
-      );
-    }
-
-    duration = Math.max(40, Math.min(duration, 6000));
-    if (interToneGap < 30) {
-      interToneGap = 30;
-    }
-
-    tones = tones.toUpperCase();
-
-    if (tones.match(/[^0-9A-D#*,]/)) {
-      throw new this._pc._win.DOMException(
-        "Invalid DTMF characters",
-        "InvalidCharacterError"
-      );
-    }
-
-    // TODO (bug 1401983): Move this API to TransceiverImpl so we don't need the
-    // extra hops through RTCPeerConnection and PeerConnectionImpl
-    this._pc._insertDTMF(this._transceiverImpl, tones, duration, interToneGap);
-  }
 }
 
 setupPrototype(RTCRtpTransceiver, {
   classID: PC_TRANSCEIVER_CID,
   contractID: PC_TRANSCEIVER_CONTRACT,
   QueryInterface: ChromeUtils.generateQI([]),
 });
 
@@ -2371,17 +2284,16 @@ class CreateOfferRequest {
 setupPrototype(CreateOfferRequest, {
   classID: PC_COREQUEST_CID,
   contractID: PC_COREQUEST_CONTRACT,
   QueryInterface: ChromeUtils.generateQI([]),
 });
 
 var EXPORTED_SYMBOLS = [
   "GlobalPCList",
-  "RTCDTMFSender",
   "RTCIceCandidate",
   "RTCSessionDescription",
   "RTCPeerConnection",
   "RTCPeerConnectionStatic",
   "RTCRtpSender",
   "RTCRtpTransceiver",
   "PeerConnectionObserver",
   "CreateOfferRequest",
--- a/dom/webidl/PeerConnectionImpl.webidl
+++ b/dom/webidl/PeerConnectionImpl.webidl
@@ -42,22 +42,16 @@ interface PeerConnectionImpl  {
   sequence<MediaStream> getRemoteStreams();
 
   /* Adds the tracks created by GetUserMedia */
   [Throws]
   TransceiverImpl createTransceiverImpl(DOMString kind,
                                         MediaStreamTrack? track);
   [Throws]
   boolean checkNegotiationNeeded();
-  [Throws]
-  void insertDTMF(TransceiverImpl transceiver, DOMString tones,
-                  optional unsigned long duration = 100,
-                  optional unsigned long interToneGap = 70);
-  [Throws]
-  DOMString getDTMFToneBuffer(RTCRtpSender sender);
 
   [Throws]
   void replaceTrackNoRenegotiation(TransceiverImpl transceiverImpl,
                                    MediaStreamTrack? withTrack);
   [Throws]
   void closeStreams();
 
   [Throws]
--- a/dom/webidl/PeerConnectionObserver.webidl
+++ b/dom/webidl/PeerConnectionObserver.webidl
@@ -47,18 +47,15 @@ interface PeerConnectionObserver
   */
   void fireTrackEvent(RTCRtpReceiver receiver, sequence<MediaStream> streams);
 
   /*
     Lets PeerConnectionImpl fire addstream events on the RTCPeerConnection
   */
   void fireStreamEvent(MediaStream stream);
 
-  /* DTMF callback */
-  void onDTMFToneChange(MediaStreamTrack track, DOMString tone);
-
   /* Packet dump callback */
   void onPacket(unsigned long level, mozPacketDumpType type, boolean sending,
                 ArrayBuffer packet);
 
   /* Transceiver sync */
   void syncTransceivers();
 };
--- a/dom/webidl/RTCDTMFSender.webidl
+++ b/dom/webidl/RTCDTMFSender.webidl
@@ -2,17 +2,17 @@
 /* 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/.
  *
  * The origin of this IDL file is
  * https://www.w3.org/TR/webrtc/#rtcdtmfsender
  */
 
-[JSImplementation="@mozilla.org/dom/rtcdtmfsender;1",
- Exposed=Window]
+[Exposed=Window]
 interface RTCDTMFSender : EventTarget {
+    [Throws]
     void insertDTMF(DOMString tones,
                     optional unsigned long duration = 100,
                     optional unsigned long interToneGap = 70);
-             attribute EventHandler  ontonechange;
-    readonly attribute DOMString     toneBuffer;
+    attribute EventHandler  ontonechange;
+    readonly attribute DOMString toneBuffer;
 };
--- a/dom/webidl/RTCRtpTransceiver.webidl
+++ b/dom/webidl/RTCRtpTransceiver.webidl
@@ -56,15 +56,10 @@ interface RTCRtpTransceiver {
     void setStopped();
 
     [ChromeOnly]
     DOMString getKind();
     [ChromeOnly]
     boolean hasBeenUsedToSend();
     [ChromeOnly]
     void sync();
-
-    [ChromeOnly]
-    void insertDTMF(DOMString tones,
-                    optional unsigned long duration = 100,
-                    optional unsigned long interToneGap = 70);
 };
 
--- a/dom/webidl/TransceiverImpl.webidl
+++ b/dom/webidl/TransceiverImpl.webidl
@@ -15,10 +15,13 @@
 
 // Constructed by PeerConnectionImpl::CreateTransceiverImpl.
 [ChromeOnly,
  Exposed=Window]
 interface TransceiverImpl {
   [Throws]
   void syncWithJS(RTCRtpTransceiver transceiver);
   readonly attribute RTCRtpReceiver receiver;
+  // TODO(bug 1616937): We won't need this once we implement RTCRtpSender in c++
+  [ChromeOnly]
+  readonly attribute RTCDTMFSender? dtmf;
 };
 
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
@@ -72,18 +72,16 @@
 #include "nsPrintfCString.h"
 #include "nsURLHelper.h"
 #include "nsNetUtil.h"
 #include "js/ArrayBuffer.h"    // JS::NewArrayBufferWithContents
 #include "js/GCAnnotations.h"  // JS_HAZ_ROOTED
 #include "js/RootingAPI.h"     // JS::{{,Mutable}Handle,Rooted}
 #include "mozilla/PeerIdentity.h"
 #include "mozilla/dom/RTCCertificate.h"
-#include "mozilla/dom/RTCDTMFSenderBinding.h"
-#include "mozilla/dom/RTCDTMFToneChangeEvent.h"
 #include "mozilla/dom/RTCRtpReceiverBinding.h"
 #include "mozilla/dom/RTCRtpSenderBinding.h"
 #include "mozilla/dom/RTCStatsReportBinding.h"
 #include "mozilla/dom/RTCPeerConnectionBinding.h"
 #include "mozilla/dom/PeerConnectionImplBinding.h"
 #include "mozilla/dom/RTCDataChannelBinding.h"
 #include "mozilla/dom/PluginCrashedEvent.h"
 #include "MediaStreamTrack.h"
@@ -1726,95 +1724,16 @@ nsresult PeerConnectionImpl::DisablePack
   MutexAutoLock lock(mPacketDumpFlagsMutex);
   if (level < packetDumpFlags->size()) {
     (*packetDumpFlags)[level] &= ~flag;
   }
 
   return NS_OK;
 }
 
-static int GetDTMFToneCode(uint16_t c) {
-  const char* DTMF_TONECODES = "0123456789*#ABCD";
-
-  if (c == ',') {
-    // , is a special character indicating a 2 second delay
-    return -1;
-  }
-
-  const char* i = strchr(DTMF_TONECODES, c);
-  MOZ_ASSERT(i);
-  return i - DTMF_TONECODES;
-}
-
-NS_IMETHODIMP
-PeerConnectionImpl::InsertDTMF(TransceiverImpl& transceiver,
-                               const nsAString& tones, uint32_t duration,
-                               uint32_t interToneGap) {
-  PC_AUTO_ENTER_API_CALL(false);
-
-  // Check values passed in from PeerConnection.js
-  MOZ_ASSERT(duration >= 40, "duration must be at least 40");
-  MOZ_ASSERT(duration <= 6000, "duration must be at most 6000");
-  MOZ_ASSERT(interToneGap >= 30, "interToneGap must be at least 30");
-
-  JSErrorResult jrv;
-
-  // TODO(bug 1401983): Move DTMF stuff to TransceiverImpl
-  // Attempt to locate state for the DTMFSender
-  RefPtr<DTMFState> state;
-  for (auto& dtmfState : mDTMFStates) {
-    if (dtmfState->mTransceiver.get() == &transceiver) {
-      state = dtmfState;
-      break;
-    }
-  }
-
-  // No state yet, create a new one
-  if (!state) {
-    state = *mDTMFStates.AppendElement(new DTMFState);
-    state->mPCObserver = mPCObserver;
-    state->mTransceiver = &transceiver;
-  }
-  MOZ_ASSERT(state);
-
-  state->mTones = tones;
-  state->mDuration = duration;
-  state->mInterToneGap = interToneGap;
-  if (!state->mTones.IsEmpty()) {
-    state->StartPlayout(0);
-  }
-  return NS_OK;
-}
-
-NS_IMETHODIMP
-PeerConnectionImpl::GetDTMFToneBuffer(mozilla::dom::RTCRtpSender& sender,
-                                      nsAString& outToneBuffer) {
-  PC_AUTO_ENTER_API_CALL(false);
-
-  JSErrorResult jrv;
-
-  // Retrieve track
-  RefPtr<MediaStreamTrack> mst = sender.GetTrack(jrv);
-  if (jrv.Failed()) {
-    NS_WARNING("Failed to retrieve track for RTCRtpSender!");
-    return jrv.StealNSResult();
-  }
-
-  // TODO(bug 1401983): Move DTMF stuff to TransceiverImpl
-  // Attempt to locate state for the DTMFSender
-  for (auto& dtmfState : mDTMFStates) {
-    if (dtmfState->mTransceiver->HasSendTrack(mst)) {
-      outToneBuffer = dtmfState->mTones;
-      break;
-    }
-  }
-
-  return NS_OK;
-}
-
 NS_IMETHODIMP
 PeerConnectionImpl::ReplaceTrackNoRenegotiation(TransceiverImpl& aTransceiver,
                                                 MediaStreamTrack* aWithTrack) {
   PC_AUTO_ENTER_API_CALL(true);
 
   RefPtr<dom::MediaStreamTrack> oldSendTrack(aTransceiver.GetSendTrack());
   if (oldSendTrack) {
     oldSendTrack->RemovePrincipalChangeObserver(this);
@@ -1823,25 +1742,16 @@ PeerConnectionImpl::ReplaceTrackNoRenego
   nsresult rv = aTransceiver.UpdateSendTrack(aWithTrack);
 
   if (NS_FAILED(rv)) {
     CSFLogError(LOGTAG, "Failed to update transceiver: %d",
                 static_cast<int>(rv));
     return rv;
   }
 
-  // TODO(bug 1401983): Move DTMF stuff to TransceiverImpl
-  for (size_t i = 0; i < mDTMFStates.Length(); ++i) {
-    if (mDTMFStates[i]->mTransceiver.get() == &aTransceiver) {
-      mDTMFStates[i]->StopPlayout();
-      mDTMFStates.RemoveElementAt(i);
-      break;
-    }
-  }
-
   if (aWithTrack) {
     aWithTrack->AddPrincipalChangeObserver(this);
     PrincipalChanged(aWithTrack);
   }
 
   if (aTransceiver.IsVideo()) {
     // We update the media pipelines here so we can apply different codec
     // settings for different sources (e.g. screensharing as opposed to camera.)
@@ -2085,21 +1995,16 @@ void PeerConnectionImpl::RecordEndOfCall
       }
     }
   }
 }
 
 nsresult PeerConnectionImpl::CloseInt() {
   PC_AUTO_ENTER_API_CALL_NO_CHECK();
 
-  // TODO(bug 1401983): Move DTMF stuff to TransceiverImpl
-  for (auto& dtmfState : mDTMFStates) {
-    dtmfState->StopPlayout();
-  }
-
   // We do this at the end of the call because we want to make sure we've waited
   // for all trickle ICE candidates to come in; this can happen well after we've
   // transitioned to connected. As a bonus, this allows us to detect race
   // conditions where a stats dispatch happens right as the PC closes.
   if (!mPrivateWindow) {
     RecordLongtermICEStatistics();
   }
   RecordEndOfCallTelemetry();
@@ -2871,71 +2776,10 @@ void PeerConnectionImpl::startCallTelem(
     }
   }
 
   // Increment session call counter
   // If we want to track Loop calls independently here, we need two histograms.
   Telemetry::Accumulate(Telemetry::WEBRTC_CALL_COUNT_2, 1);
 }
 
-void PeerConnectionImpl::DTMFState::StopPlayout() {
-  if (mSendTimer) {
-    mSendTimer->Cancel();
-    mSendTimer = nullptr;
-  }
-}
-
-void PeerConnectionImpl::DTMFState::StartPlayout(uint32_t aDelay) {
-  if (!mSendTimer) {
-    mSendTimer = NS_NewTimer();
-    mSendTimer->InitWithCallback(this, aDelay, nsITimer::TYPE_ONE_SHOT);
-  }
-}
-
-nsresult PeerConnectionImpl::DTMFState::Notify(nsITimer* timer) {
-  MOZ_ASSERT(NS_IsMainThread());
-  StopPlayout();
-
-  if (!mTransceiver->IsSending()) {
-    return NS_OK;
-  }
-
-  nsString eventTone;
-  if (!mTones.IsEmpty()) {
-    uint16_t toneChar = mTones.CharAt(0);
-    int tone = GetDTMFToneCode(toneChar);
-
-    eventTone.Assign(toneChar);
-
-    mTones.Cut(0, 1);
-
-    if (tone == -1) {
-      StartPlayout(2000);
-    } else {
-      // Reset delay if necessary
-      StartPlayout(mDuration + mInterToneGap);
-      mTransceiver->InsertDTMFTone(tone, mDuration);
-    }
-  }
-
-  RefPtr<dom::MediaStreamTrack> sendTrack = mTransceiver->GetSendTrack();
-  if (!sendTrack) {
-    NS_WARNING("Failed to dispatch the RTCDTMFToneChange event!");
-    return NS_OK;  // Return is ignored anyhow
-  }
-
-  JSErrorResult jrv;
-  mPCObserver->OnDTMFToneChange(*sendTrack, eventTone, jrv);
-
-  if (jrv.Failed()) {
-    NS_WARNING("Failed to dispatch the RTCDTMFToneChange event!");
-  }
-
-  return NS_OK;
-}
-
-PeerConnectionImpl::DTMFState::DTMFState() = default;
-PeerConnectionImpl::DTMFState::~DTMFState() { StopPlayout(); }
-
-NS_IMPL_ISUPPORTS(PeerConnectionImpl::DTMFState, nsITimerCallback)
-
 std::map<std::string, PeerConnectionAutoTimer> PeerConnectionImpl::mAutoTimers;
 }  // namespace mozilla
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
@@ -58,17 +58,16 @@ class MediaPipeline;
 class MediaPipelineReceive;
 class MediaPipelineTransmit;
 class TransceiverImpl;
 
 namespace dom {
 class RTCCertificate;
 struct RTCConfiguration;
 struct RTCRtpSourceEntry;
-class RTCDTMFSender;
 struct RTCIceServer;
 struct RTCOfferOptions;
 struct RTCRtpParameters;
 class RTCRtpSender;
 class MediaStreamTrack;
 
 #ifdef USE_FAKE_PCOBSERVER
 typedef test::AFakePCObserver PeerConnectionObserver;
@@ -282,29 +281,16 @@ class PeerConnectionImpl final
   void CloseStreams(ErrorResult& rv) { rv = CloseStreams(); }
 
   already_AddRefed<TransceiverImpl> CreateTransceiverImpl(
       const nsAString& aKind, dom::MediaStreamTrack* aSendTrack,
       ErrorResult& rv);
 
   bool CheckNegotiationNeeded(ErrorResult& rv);
 
-  NS_IMETHODIMP_TO_ERRORRESULT(InsertDTMF, ErrorResult& rv,
-                               TransceiverImpl& transceiver,
-                               const nsAString& tones, uint32_t duration,
-                               uint32_t interToneGap) {
-    rv = InsertDTMF(transceiver, tones, duration, interToneGap);
-  }
-
-  NS_IMETHODIMP_TO_ERRORRESULT(GetDTMFToneBuffer, ErrorResult& rv,
-                               dom::RTCRtpSender& sender,
-                               nsAString& outToneBuffer) {
-    rv = GetDTMFToneBuffer(sender, outToneBuffer);
-  }
-
   NS_IMETHODIMP_TO_ERRORRESULT(ReplaceTrackNoRenegotiation, ErrorResult& rv,
                                TransceiverImpl& aTransceiver,
                                mozilla::dom::MediaStreamTrack* aWithTrack) {
     rv = ReplaceTrackNoRenegotiation(aTransceiver, aWithTrack);
   }
 
   // test-only
   NS_IMETHODIMP_TO_ERRORRESULT(EnablePacketDump, ErrorResult& rv,
@@ -596,40 +582,16 @@ class PeerConnectionImpl final
 
   // Whether this PeerConnection is being counted as active by mWindow
   bool mActiveOnWindow;
 
   // storage for Telemetry data
   uint16_t mMaxReceiving[SdpMediaSection::kMediaTypes];
   uint16_t mMaxSending[SdpMediaSection::kMediaTypes];
 
-  // DTMF
-  class DTMFState : public nsITimerCallback {
-    virtual ~DTMFState();
-
-   public:
-    DTMFState();
-
-    NS_DECL_NSITIMERCALLBACK
-    NS_DECL_THREADSAFE_ISUPPORTS
-
-    void StopPlayout();
-    void StartPlayout(uint32_t aDelay);
-
-    RefPtr<PeerConnectionObserver> mPCObserver;
-    RefPtr<TransceiverImpl> mTransceiver;
-    nsCOMPtr<nsITimer> mSendTimer;
-    nsString mTones;
-    uint32_t mDuration;
-    uint32_t mInterToneGap;
-  };
-
-  // TODO(bug 1401983): Move DTMF stuff to TransceiverImpl
-  nsTArray<RefPtr<DTMFState>> mDTMFStates;
-
   std::vector<unsigned> mSendPacketDumpFlags;
   std::vector<unsigned> mRecvPacketDumpFlags;
   Atomic<bool> mPacketDumpEnabled;
   mutable Mutex mPacketDumpFlagsMutex;
 
   // used to store the raw trickle candidate string for display
   // on the about:webrtc raw candidates table.
   std::vector<std::string> mRawTrickledCandidates;
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/peerconnection/RTCDTMFSender.cpp
@@ -0,0 +1,153 @@
+/* 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 "RTCDTMFSender.h"
+#include "MediaConduitInterface.h"
+#include "logging.h"
+#include "TransceiverImpl.h"
+#include "nsITimer.h"
+#include "mozilla/dom/RTCDTMFSenderBinding.h"
+#include "mozilla/dom/RTCDTMFToneChangeEvent.h"
+#include <algorithm>
+#include <bitset>
+
+namespace mozilla {
+
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(RTCDTMFSender, DOMEventTargetHelper, mWindow,
+                                   mTransceiver, mSendTimer)
+
+NS_IMPL_ADDREF_INHERITED(RTCDTMFSender, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(RTCDTMFSender, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RTCDTMFSender)
+  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+  NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+LazyLogModule gDtmfLog("RTCDTMFSender");
+
+RTCDTMFSender::RTCDTMFSender(nsPIDOMWindowInner* aWindow,
+                             TransceiverImpl* aTransceiver,
+                             AudioSessionConduit* aConduit)
+    : mWindow(aWindow), mTransceiver(aTransceiver), mConduit(aConduit) {}
+
+JSObject* RTCDTMFSender::WrapObject(JSContext* aCx,
+                                    JS::Handle<JSObject*> aGivenProto) {
+  return RTCDTMFSender_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+static int GetDTMFToneCode(uint16_t c) {
+  const char* DTMF_TONECODES = "0123456789*#ABCD";
+
+  if (c == ',') {
+    // , is a special character indicating a 2 second delay
+    return -1;
+  }
+
+  const char* i = strchr(DTMF_TONECODES, c);
+  MOZ_ASSERT(i);
+  return static_cast<int>(i - DTMF_TONECODES);
+}
+
+static std::bitset<256> GetCharacterBitset(const std::string& aCharsInSet) {
+  std::bitset<256> result;
+  for (auto c : aCharsInSet) {
+    result[c] = true;
+  }
+  return result;
+}
+
+static bool IsUnrecognizedChar(const char c) {
+  static const std::bitset<256> recognized =
+      GetCharacterBitset("0123456789ABCD#*,");
+  return !recognized[c];
+}
+
+void RTCDTMFSender::InsertDTMF(const nsAString& aTones, uint32_t aDuration,
+                               uint32_t aInterToneGap, ErrorResult& aRv) {
+  if (!mTransceiver->CanSendDTMF()) {
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+    return;
+  }
+
+  std::string utf8Tones = NS_ConvertUTF16toUTF8(aTones).get();
+
+  std::transform(utf8Tones.begin(), utf8Tones.end(), utf8Tones.begin(),
+                 [](const char c) { return std::toupper(c); });
+
+  if (std::any_of(utf8Tones.begin(), utf8Tones.end(), IsUnrecognizedChar)) {
+    aRv.Throw(NS_ERROR_DOM_INVALID_CHARACTER_ERR);
+    return;
+  }
+
+  mToneBuffer = NS_ConvertUTF8toUTF16(utf8Tones.c_str());
+  mDuration = std::clamp(aDuration, 40U, 6000U);
+  mInterToneGap = std::clamp(aInterToneGap, 30U, 6000U);
+
+  if (mToneBuffer.Length()) {
+    StartPlayout(0);
+  }
+}
+
+void RTCDTMFSender::StopPlayout() {
+  if (mSendTimer) {
+    mSendTimer->Cancel();
+    mSendTimer = nullptr;
+  }
+}
+
+void RTCDTMFSender::StartPlayout(uint32_t aDelay) {
+  if (!mSendTimer) {
+    mSendTimer = NS_NewTimer();
+    mSendTimer->InitWithCallback(this, aDelay, nsITimer::TYPE_ONE_SHOT);
+  }
+}
+
+nsresult RTCDTMFSender::Notify(nsITimer* timer) {
+  MOZ_ASSERT(NS_IsMainThread());
+  StopPlayout();
+
+  if (!mTransceiver->IsSending()) {
+    return NS_OK;
+  }
+
+  RTCDTMFToneChangeEventInit init;
+  if (!mToneBuffer.IsEmpty()) {
+    uint16_t toneChar = mToneBuffer.CharAt(0);
+    int tone = GetDTMFToneCode(toneChar);
+
+    init.mTone.Assign(toneChar);
+
+    mToneBuffer.Cut(0, 1);
+
+    if (tone == -1) {
+      StartPlayout(2000);
+    } else {
+      // Reset delay if necessary
+      StartPlayout(mDuration + mInterToneGap);
+      // Note: We default to channel 0, not inband, and 6dB attenuation.
+      //      here. We might want to revisit these choices in the future.
+      mConduit->InsertDTMFTone(0, tone, true, mDuration, 6);
+    }
+  }
+
+  RefPtr<RTCDTMFToneChangeEvent> event = RTCDTMFToneChangeEvent::Constructor(
+      this, NS_LITERAL_STRING("tonechange"), init);
+  DispatchTrustedEvent(event);
+
+  return NS_OK;
+}
+
+void RTCDTMFSender::GetToneBuffer(nsAString& aOutToneBuffer) {
+  aOutToneBuffer = mToneBuffer;
+}
+
+nsPIDOMWindowInner* RTCDTMFSender::GetParentObject() const { return mWindow; }
+
+}  // namespace dom
+}  // namespace mozilla
+
+#undef LOGTAG
new file mode 100644
--- /dev/null
+++ b/media/webrtc/signaling/src/peerconnection/RTCDTMFSender.h
@@ -0,0 +1,60 @@
+/* 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 _RTCDTMFSender_h_
+#define _RTCDTMFSender_h_
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/RefPtr.h"
+#include "js/RootingAPI.h"
+
+class nsPIDOMWindowInner;
+class nsITimer;
+
+namespace mozilla {
+class AudioSessionConduit;
+class TransceiverImpl;
+
+namespace dom {
+
+class RTCDTMFSender : public DOMEventTargetHelper, public nsITimerCallback {
+ public:
+  explicit RTCDTMFSender(nsPIDOMWindowInner* aWindow,
+                         TransceiverImpl* aTransceiver,
+                         AudioSessionConduit* aConduit);
+
+  // nsISupports
+  NS_DECL_NSITIMERCALLBACK
+  NS_DECL_ISUPPORTS_INHERITED
+  NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(RTCDTMFSender, DOMEventTargetHelper)
+
+  // webidl
+  JSObject* WrapObject(JSContext* aCx,
+                       JS::Handle<JSObject*> aGivenProto) override;
+  void InsertDTMF(const nsAString& aTones, uint32_t aDuration,
+                  uint32_t aInterToneGap, ErrorResult& aRv);
+  void GetToneBuffer(nsAString& aOutToneBuffer);
+  IMPL_EVENT_HANDLER(tonechange)
+
+  nsPIDOMWindowInner* GetParentObject() const;
+
+  void StopPlayout();
+
+ private:
+  virtual ~RTCDTMFSender() = default;
+
+  void StartPlayout(uint32_t aDelay);
+
+  nsCOMPtr<nsPIDOMWindowInner> mWindow;
+  RefPtr<TransceiverImpl> mTransceiver;
+  RefPtr<AudioSessionConduit> mConduit;
+  nsString mToneBuffer;
+  uint32_t mDuration = 0;
+  uint32_t mInterToneGap = 0;
+  nsCOMPtr<nsITimer> mSendTimer;
+};
+
+}  // namespace dom
+}  // namespace mozilla
+#endif  // _RTCDTMFSender_h_
--- a/media/webrtc/signaling/src/peerconnection/TransceiverImpl.cpp
+++ b/media/webrtc/signaling/src/peerconnection/TransceiverImpl.cpp
@@ -20,25 +20,26 @@
 #include "RemoteTrackSource.h"
 #include "MediaConduitInterface.h"
 #include "MediaTransportHandler.h"
 #include "mozilla/dom/RTCRtpReceiverBinding.h"
 #include "mozilla/dom/RTCRtpSenderBinding.h"
 #include "mozilla/dom/RTCRtpTransceiverBinding.h"
 #include "mozilla/dom/TransceiverImplBinding.h"
 #include "RTCRtpReceiver.h"
+#include "RTCDTMFSender.h"
 
 namespace mozilla {
 
 MOZ_MTLOG_MODULE("transceiverimpl")
 
 using LocalDirection = MediaSessionConduitLocalDirection;
 
 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TransceiverImpl, mWindow, mSendTrack,
-                                      mReceiver)
+                                      mReceiver, mDtmf)
 NS_IMPL_CYCLE_COLLECTING_ADDREF(TransceiverImpl)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(TransceiverImpl)
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TransceiverImpl)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
 TransceiverImpl::TransceiverImpl(
@@ -67,16 +68,21 @@ TransceiverImpl::TransceiverImpl(
   }
 
   mConduit->SetPCHandle(mPCHandle);
 
   mReceiver =
       new RTCRtpReceiver(aWindow, aPrivacyNeeded, aPCHandle, aTransportHandler,
                          aJsepTransceiver, aMainThread, aStsThread, mConduit);
 
+  if (!IsVideo()) {
+    mDtmf = new RTCDTMFSender(
+        aWindow, this, static_cast<AudioSessionConduit*>(mConduit.get()));
+  }
+
   mTransmitPipeline =
       new MediaPipelineTransmit(mPCHandle, mTransportHandler, mMainThread.get(),
                                 mStsThread.get(), IsVideo(), mConduit);
 
   mTransmitPipeline->SetTrack(mSendTrack);
 }
 
 TransceiverImpl::~TransceiverImpl() = default;
@@ -423,26 +429,26 @@ void TransceiverImpl::SyncWithJS(dom::RT
 
   if (aRv.Failed()) {
     return;
   }
 
   // currentDirection from JSEP, but not if "this transceiver has never been
   // represented in an offer/answer exchange"
   if (mJsepTransceiver->HasLevel() && mJsepTransceiver->IsNegotiated()) {
-    if (mJsepTransceiver->mRecvTrack.GetActive()) {
-      if (mJsepTransceiver->mSendTrack.GetActive()) {
+    if (IsReceiving()) {
+      if (IsSending()) {
         aJsTransceiver.SetCurrentDirection(
             dom::RTCRtpTransceiverDirection::Sendrecv, aRv);
       } else {
         aJsTransceiver.SetCurrentDirection(
             dom::RTCRtpTransceiverDirection::Recvonly, aRv);
       }
     } else {
-      if (mJsepTransceiver->mSendTrack.GetActive()) {
+      if (IsSending()) {
         aJsTransceiver.SetCurrentDirection(
             dom::RTCRtpTransceiverDirection::Sendonly, aRv);
       } else {
         aJsTransceiver.SetCurrentDirection(
             dom::RTCRtpTransceiverDirection::Inactive, aRv);
       }
     }
 
@@ -460,28 +466,49 @@ void TransceiverImpl::SyncWithJS(dom::RT
     return;
   }
 
   if (mJsepTransceiver->IsRemoved()) {
     aJsTransceiver.SetShouldRemove(true, aRv);
   }
 }
 
-void TransceiverImpl::InsertDTMFTone(int tone, uint32_t duration) {
-  if (mJsepTransceiver->IsStopped()) {
-    return;
+bool TransceiverImpl::CanSendDTMF() const {
+  // Spec says: "If connection's RTCPeerConnectionState is not "connected"
+  // return false." We don't support that right now. This is supposed to be
+  // true once ICE is complete, and _all_ DTLS handshakes are also complete. We
+  // don't really have access to the state of _all_ of our DTLS states either.
+  // Our pipeline _does_ know whether SRTP/SRTCP is ready, which happens
+  // immediately after our transport finishes DTLS (unless there was an error),
+  // so this is pretty close.
+  // TODO (bug 1265827): Base this on RTCPeerConnectionState instead.
+  // TODO (bug 1623193): Tighten this up
+  if (!IsSending() || !mSendTrack) {
+    return false;
   }
 
-  MOZ_ASSERT(mConduit->type() == MediaSessionConduit::AUDIO);
+  // Ok, it looks like the connection is up and sending. Did we negotiate
+  // telephone-event?
+  JsepTrackNegotiatedDetails* details =
+      mJsepTransceiver->mSendTrack.GetNegotiatedDetails();
+  if (NS_WARN_IF(!details || !details->GetEncodingCount())) {
+    // What?
+    return false;
+  }
 
-  RefPtr<AudioSessionConduit> conduit(
-      static_cast<AudioSessionConduit*>(mConduit.get()));
-  // Note: We default to channel 0, not inband, and 6dB attenuation.
-  //      here. We might want to revisit these choices in the future.
-  conduit->InsertDTMFTone(0, tone, true, duration, 6);
+  for (size_t i = 0; i < details->GetEncodingCount(); ++i) {
+    const auto& encoding = details->GetEncoding(i);
+    for (const auto& codec : encoding.GetCodecs()) {
+      if (codec->mName == "telephone-event") {
+        return true;
+      }
+    }
+  }
+
+  return false;
 }
 
 JSObject* TransceiverImpl::WrapObject(JSContext* aCx,
                                       JS::Handle<JSObject*> aGivenProto) {
   return dom::TransceiverImpl_Binding::Wrap(aCx, this, aGivenProto);
 }
 
 nsPIDOMWindowInner* TransceiverImpl::GetParentObject() const { return mWindow; }
@@ -779,15 +806,19 @@ void TransceiverImpl::Stop() {
   mReceiver->Shutdown();
   // Make sure that stats queries stop working on this transceiver.
   UpdateSendTrack(nullptr);
 
   if (mConduit) {
     mConduit->DeleteStreams();
   }
   mConduit = nullptr;
+
+  if (mDtmf) {
+    mDtmf->StopPlayout();
+  }
 }
 
 bool TransceiverImpl::IsVideo() const {
   return mJsepTransceiver->GetMediaType() == SdpMediaSection::MediaType::kVideo;
 }
 
 }  // namespace mozilla
--- a/media/webrtc/signaling/src/peerconnection/TransceiverImpl.h
+++ b/media/webrtc/signaling/src/peerconnection/TransceiverImpl.h
@@ -27,16 +27,17 @@ class VideoCodecConfig;  // Why is this 
 class MediaPipelineTransmit;
 class MediaPipeline;
 class MediaPipelineFilter;
 class MediaTransportHandler;
 class WebRtcCallWrapper;
 class JsepTrackNegotiatedDetails;
 
 namespace dom {
+class RTCDTMFSender;
 class RTCRtpTransceiver;
 struct RTCRtpSourceEntry;
 class RTCRtpReceiver;
 }  // namespace dom
 
 /**
  * This is what ties all the various pieces that make up a transceiver
  * together. This includes:
@@ -88,33 +89,39 @@ class TransceiverImpl : public nsISuppor
   RefPtr<dom::MediaStreamTrack> GetSendTrack() { return mSendTrack; }
 
   // for webidl
   JSObject* WrapObject(JSContext* aCx,
                        JS::Handle<JSObject*> aGivenProto) override;
   nsPIDOMWindowInner* GetParentObject() const;
   void SyncWithJS(dom::RTCRtpTransceiver& aJsTransceiver, ErrorResult& aRv);
   dom::RTCRtpReceiver* Receiver() const { return mReceiver; }
+  dom::RTCDTMFSender* GetDtmf() const { return mDtmf; }
 
-  void InsertDTMFTone(int tone, uint32_t duration);
+  bool CanSendDTMF() const;
 
   // TODO: These are for stats; try to find a cleaner way.
   RefPtr<MediaPipelineTransmit> GetSendPipeline();
 
   std::string GetTransportId() const {
     return mJsepTransceiver->mTransport.mTransportId;
   }
 
   bool IsVideo() const;
 
   bool IsSending() const {
     return !mJsepTransceiver->IsStopped() &&
            mJsepTransceiver->mSendTrack.GetActive();
   }
 
+  bool IsReceiving() const {
+    return !mJsepTransceiver->IsStopped() &&
+           mJsepTransceiver->mRecvTrack.GetActive();
+  }
+
   void GetRtpSources(const int64_t aTimeNow,
                      nsTArray<dom::RTCRtpSourceEntry>& outSources) const;
 
   MediaSessionConduit* GetConduit() const { return mConduit; }
 
   // nsISupports
   NS_DECL_CYCLE_COLLECTING_ISUPPORTS
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(TransceiverImpl)
@@ -149,13 +156,15 @@ class TransceiverImpl : public nsISuppor
   nsCOMPtr<nsISerialEventTarget> mMainThread;
   nsCOMPtr<nsISerialEventTarget> mStsThread;
   RefPtr<dom::MediaStreamTrack> mSendTrack;
   // state for webrtc.org that is shared between all transceivers
   RefPtr<WebRtcCallWrapper> mCallWrapper;
   RefPtr<MediaSessionConduit> mConduit;
   RefPtr<MediaPipelineTransmit> mTransmitPipeline;
   RefPtr<dom::RTCRtpReceiver> mReceiver;
+  // TODO(bug 1616937): Move this to RTCRtpSender
+  RefPtr<dom::RTCDTMFSender> mDtmf;
 };
 
 }  // namespace mozilla
 
 #endif  // _TRANSCEIVERIMPL_H_
--- a/media/webrtc/signaling/src/peerconnection/moz.build
+++ b/media/webrtc/signaling/src/peerconnection/moz.build
@@ -25,16 +25,17 @@ LOCAL_INCLUDES += [
 UNIFIED_SOURCES += [
     'MediaTransportHandler.cpp',
     'MediaTransportHandlerIPC.cpp',
     'MediaTransportParent.cpp',
     'PacketDumper.cpp',
     'PeerConnectionCtx.cpp',
     'PeerConnectionImpl.cpp',
     'PeerConnectionMedia.cpp',
+    'RTCDTMFSender.cpp',
     'RTCRtpReceiver.cpp',
     'RTCStatsIdGenerator.cpp',
     'RTCStatsReport.cpp',
     'TransceiverImpl.cpp',
     'WebrtcGlobalInformation.cpp',
 ]
 
 FINAL_LIBRARY = 'xul'
--- a/xpcom/ds/StaticAtoms.py
+++ b/xpcom/ds/StaticAtoms.py
@@ -887,16 +887,17 @@ STATIC_ATOMS = [
     Atom("onstatechange", "onstatechange"),
     Atom("onstorage", "onstorage"),
     Atom("onsubmit", "onsubmit"),
     Atom("onsuccess", "onsuccess"),
     Atom("ontypechange", "ontypechange"),
     Atom("onterminate", "onterminate"),
     Atom("ontext", "ontext"),
     Atom("ontoggle", "ontoggle"),
+    Atom("ontonechange", "ontonechange"),
     Atom("ontouchstart", "ontouchstart"),
     Atom("ontouchend", "ontouchend"),
     Atom("ontouchmove", "ontouchmove"),
     Atom("ontouchcancel", "ontouchcancel"),
     Atom("ontransitioncancel", "ontransitioncancel"),
     Atom("ontransitionend", "ontransitionend"),
     Atom("ontransitionrun", "ontransitionrun"),
     Atom("ontransitionstart", "ontransitionstart"),