Coverage Report

Created: 2018-09-25 14:53

/src/mozilla-central/dom/media/webspeech/synth/speechd/SpeechDispatcherService.cpp
Line
Count
Source (jump to first uncovered line)
1
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
3
/* This Source Code Form is subject to the terms of the Mozilla Public
4
 * License, v. 2.0. If a copy of the MPL was not distributed with this
5
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7
#include "SpeechDispatcherService.h"
8
9
#include "mozilla/dom/nsSpeechTask.h"
10
#include "mozilla/dom/nsSynthVoiceRegistry.h"
11
#include "mozilla/ClearOnShutdown.h"
12
#include "mozilla/Preferences.h"
13
#include "nsEscape.h"
14
#include "nsISupports.h"
15
#include "nsPrintfCString.h"
16
#include "nsReadableUtils.h"
17
#include "nsServiceManagerUtils.h"
18
#include "nsThreadUtils.h"
19
#include "prlink.h"
20
21
#include <math.h>
22
#include <stdlib.h>
23
24
0
#define URI_PREFIX "urn:moz-tts:speechd:"
25
26
0
#define MAX_RATE static_cast<float>(2.5)
27
0
#define MIN_RATE static_cast<float>(0.5)
28
29
// Some structures for libspeechd
30
typedef enum {
31
  SPD_EVENT_BEGIN,
32
  SPD_EVENT_END,
33
  SPD_EVENT_INDEX_MARK,
34
  SPD_EVENT_CANCEL,
35
  SPD_EVENT_PAUSE,
36
  SPD_EVENT_RESUME
37
} SPDNotificationType;
38
39
typedef enum {
40
  SPD_BEGIN = 1,
41
  SPD_END = 2,
42
  SPD_INDEX_MARKS = 4,
43
  SPD_CANCEL = 8,
44
  SPD_PAUSE = 16,
45
  SPD_RESUME = 32,
46
47
  SPD_ALL = 0x3f
48
} SPDNotification;
49
50
typedef enum {
51
  SPD_MODE_SINGLE = 0,
52
  SPD_MODE_THREADED = 1
53
} SPDConnectionMode;
54
55
typedef void (*SPDCallback) (size_t msg_id, size_t client_id,
56
                             SPDNotificationType state);
57
58
typedef void (*SPDCallbackIM) (size_t msg_id, size_t client_id,
59
                               SPDNotificationType state, char* index_mark);
60
61
struct SPDConnection
62
{
63
  SPDCallback callback_begin;
64
  SPDCallback callback_end;
65
  SPDCallback callback_cancel;
66
  SPDCallback callback_pause;
67
  SPDCallback callback_resume;
68
  SPDCallbackIM callback_im;
69
70
  /* partial, more private fields in structure */
71
};
72
73
struct SPDVoice
74
{
75
  char* name;
76
  char* language;
77
  char* variant;
78
};
79
80
typedef enum {
81
  SPD_IMPORTANT = 1,
82
  SPD_MESSAGE = 2,
83
  SPD_TEXT = 3,
84
  SPD_NOTIFICATION = 4,
85
  SPD_PROGRESS = 5
86
} SPDPriority;
87
88
#define SPEECHD_FUNCTIONS \
89
0
  FUNC(spd_open, SPDConnection*, (const char*, const char*, const char*, SPDConnectionMode)) \
90
0
  FUNC(spd_close, void, (SPDConnection*)) \
91
0
  FUNC(spd_list_synthesis_voices, SPDVoice**, (SPDConnection*)) \
92
0
  FUNC(spd_say, int, (SPDConnection*, SPDPriority, const char*)) \
93
0
  FUNC(spd_cancel, int, (SPDConnection*)) \
94
0
  FUNC(spd_set_volume, int, (SPDConnection*, int)) \
95
0
  FUNC(spd_set_voice_rate, int, (SPDConnection*, int)) \
96
0
  FUNC(spd_set_voice_pitch, int, (SPDConnection*, int)) \
97
0
  FUNC(spd_set_synthesis_voice, int, (SPDConnection*, const char*)) \
98
0
  FUNC(spd_set_notification_on, int, (SPDConnection*, SPDNotification))
99
100
#define FUNC(name, type, params) \
101
  typedef type (*_##name##_fn) params; \
102
  static _##name##_fn _##name;
103
104
SPEECHD_FUNCTIONS
105
106
#undef FUNC
107
108
0
#define spd_open _spd_open
109
0
#define spd_close _spd_close
110
0
#define spd_list_synthesis_voices _spd_list_synthesis_voices
111
0
#define spd_say _spd_say
112
0
#define spd_cancel _spd_cancel
113
0
#define spd_set_volume _spd_set_volume
114
0
#define spd_set_voice_rate _spd_set_voice_rate
115
0
#define spd_set_voice_pitch _spd_set_voice_pitch
116
0
#define spd_set_synthesis_voice _spd_set_synthesis_voice
117
0
#define spd_set_notification_on _spd_set_notification_on
118
119
static PRLibrary* speechdLib = nullptr;
120
121
typedef void (*nsSpeechDispatcherFunc)();
122
struct nsSpeechDispatcherDynamicFunction
123
{
124
  const char* functionName;
125
  nsSpeechDispatcherFunc* function;
126
};
127
128
namespace mozilla {
129
namespace dom {
130
131
StaticRefPtr<SpeechDispatcherService> SpeechDispatcherService::sSingleton;
132
133
class SpeechDispatcherVoice
134
{
135
public:
136
137
  SpeechDispatcherVoice(const nsAString& aName, const nsAString& aLanguage)
138
0
    : mName(aName), mLanguage(aLanguage) {}
139
140
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SpeechDispatcherVoice)
141
142
  // Voice name
143
  nsString mName;
144
145
  // Voice language, in BCP-47 syntax
146
  nsString mLanguage;
147
148
private:
149
0
  ~SpeechDispatcherVoice() {}
150
};
151
152
153
class SpeechDispatcherCallback final : public nsISpeechTaskCallback
154
{
155
public:
156
  SpeechDispatcherCallback(nsISpeechTask* aTask, SpeechDispatcherService* aService)
157
    : mTask(aTask)
158
0
    , mService(aService) {}
159
160
  NS_DECL_CYCLE_COLLECTING_ISUPPORTS
161
  NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(SpeechDispatcherCallback, nsISpeechTaskCallback)
162
163
  NS_DECL_NSISPEECHTASKCALLBACK
164
165
  bool OnSpeechEvent(SPDNotificationType state);
166
167
private:
168
0
  ~SpeechDispatcherCallback() { }
169
170
  // This pointer is used to dispatch events
171
  nsCOMPtr<nsISpeechTask> mTask;
172
173
  // By holding a strong reference to the service we guarantee that it won't be
174
  // destroyed before this runnable.
175
  RefPtr<SpeechDispatcherService> mService;
176
177
  TimeStamp mStartTime;
178
};
179
180
NS_IMPL_CYCLE_COLLECTION(SpeechDispatcherCallback, mTask);
181
182
0
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechDispatcherCallback)
183
0
  NS_INTERFACE_MAP_ENTRY(nsISpeechTaskCallback)
184
0
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpeechTaskCallback)
185
0
NS_INTERFACE_MAP_END
186
187
NS_IMPL_CYCLE_COLLECTING_ADDREF(SpeechDispatcherCallback)
188
NS_IMPL_CYCLE_COLLECTING_RELEASE(SpeechDispatcherCallback)
189
190
NS_IMETHODIMP
191
SpeechDispatcherCallback::OnPause()
192
0
{
193
0
  // XXX: Speech dispatcher does not pause immediately, but waits for the speech
194
0
  // to reach an index mark so that it could resume from that offset.
195
0
  // There is no support for word or sentence boundaries, so index marks would
196
0
  // only occur in explicit SSML marks, and we don't support that yet.
197
0
  // What in actuality happens, is that if you call spd_pause(), it will speak
198
0
  // the utterance in its entirety, dispatch an end event, and then put speechd
199
0
  // in a 'paused' state. Since it is after the utterance ended, we don't get
200
0
  // that state change, and our speech api is in an unrecoverable state.
201
0
  // So, since it is useless anyway, I am not implementing pause.
202
0
  return NS_OK;
203
0
}
204
205
NS_IMETHODIMP
206
SpeechDispatcherCallback::OnResume()
207
0
{
208
0
  // XXX: Unsupported, see OnPause().
209
0
  return NS_OK;
210
0
}
211
212
NS_IMETHODIMP
213
SpeechDispatcherCallback::OnCancel()
214
0
{
215
0
  if (spd_cancel(mService->mSpeechdClient) < 0) {
216
0
    return NS_ERROR_FAILURE;
217
0
  }
218
0
219
0
  return NS_OK;
220
0
}
221
222
NS_IMETHODIMP
223
SpeechDispatcherCallback::OnVolumeChanged(float aVolume)
224
0
{
225
0
  // XXX: This currently does not change the volume mid-utterance, but it
226
0
  // doesn't do anything bad either. So we could put this here with the hopes
227
0
  // that speechd supports this in the future.
228
0
  if (spd_set_volume(mService->mSpeechdClient, static_cast<int>(aVolume * 100)) < 0) {
229
0
    return NS_ERROR_FAILURE;
230
0
  }
231
0
232
0
  return NS_OK;
233
0
}
234
235
bool
236
SpeechDispatcherCallback::OnSpeechEvent(SPDNotificationType state)
237
0
{
238
0
  bool remove = false;
239
0
240
0
  switch (state) {
241
0
    case SPD_EVENT_BEGIN:
242
0
      mStartTime = TimeStamp::Now();
243
0
      mTask->DispatchStart();
244
0
      break;
245
0
246
0
    case SPD_EVENT_PAUSE:
247
0
      mTask->DispatchPause((TimeStamp::Now() - mStartTime).ToSeconds(), 0);
248
0
      break;
249
0
250
0
    case SPD_EVENT_RESUME:
251
0
      mTask->DispatchResume((TimeStamp::Now() - mStartTime).ToSeconds(), 0);
252
0
      break;
253
0
254
0
    case SPD_EVENT_CANCEL:
255
0
    case SPD_EVENT_END:
256
0
      mTask->DispatchEnd((TimeStamp::Now() - mStartTime).ToSeconds(), 0);
257
0
      remove = true;
258
0
      break;
259
0
260
0
    case SPD_EVENT_INDEX_MARK:
261
0
      // Not yet supported
262
0
      break;
263
0
264
0
    default:
265
0
      break;
266
0
  }
267
0
268
0
  return remove;
269
0
}
270
271
static void
272
speechd_cb(size_t msg_id, size_t client_id, SPDNotificationType state)
273
0
{
274
0
  SpeechDispatcherService* service = SpeechDispatcherService::GetInstance(false);
275
0
276
0
  if (service) {
277
0
    NS_DispatchToMainThread(NewRunnableMethod<uint32_t, SPDNotificationType>(
278
0
      "dom::SpeechDispatcherService::EventNotify",
279
0
      service,
280
0
      &SpeechDispatcherService::EventNotify,
281
0
      static_cast<uint32_t>(msg_id),
282
0
      state));
283
0
  }
284
0
}
285
286
287
0
NS_INTERFACE_MAP_BEGIN(SpeechDispatcherService)
288
0
  NS_INTERFACE_MAP_ENTRY(nsISpeechService)
289
0
  NS_INTERFACE_MAP_ENTRY(nsIObserver)
290
0
  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
291
0
NS_INTERFACE_MAP_END
292
293
NS_IMPL_ADDREF(SpeechDispatcherService)
294
NS_IMPL_RELEASE(SpeechDispatcherService)
295
296
SpeechDispatcherService::SpeechDispatcherService()
297
  : mInitialized(false)
298
  , mSpeechdClient(nullptr)
299
0
{
300
0
}
301
302
void
303
SpeechDispatcherService::Init()
304
0
{
305
0
  if (!Preferences::GetBool("media.webspeech.synth.enabled") ||
306
0
      Preferences::GetBool("media.webspeech.synth.test")) {
307
0
    return;
308
0
  }
309
0
310
0
  // While speech dispatcher has a "threaded" mode, only spd_say() is async.
311
0
  // Since synchronous socket i/o could impact startup time, we do
312
0
  // initialization in a separate thread.
313
0
  DebugOnly<nsresult> rv = NS_NewNamedThread("speechd init",
314
0
                                             getter_AddRefs(mInitThread));
315
0
  MOZ_ASSERT(NS_SUCCEEDED(rv));
316
0
  rv = mInitThread->Dispatch(
317
0
    NewRunnableMethod("dom::SpeechDispatcherService::Setup",
318
0
                      this,
319
0
                      &SpeechDispatcherService::Setup),
320
0
    NS_DISPATCH_NORMAL);
321
0
  MOZ_ASSERT(NS_SUCCEEDED(rv));
322
0
}
323
324
SpeechDispatcherService::~SpeechDispatcherService()
325
0
{
326
0
  if (mInitThread) {
327
0
    mInitThread->Shutdown();
328
0
  }
329
0
330
0
  if (mSpeechdClient) {
331
0
    spd_close(mSpeechdClient);
332
0
  }
333
0
}
334
335
void
336
SpeechDispatcherService::Setup()
337
0
{
338
0
#define FUNC(name, type, params) { #name, (nsSpeechDispatcherFunc *)&_##name },
339
0
  static const nsSpeechDispatcherDynamicFunction kSpeechDispatcherSymbols[] = {
340
0
    SPEECHD_FUNCTIONS
341
0
  };
342
0
#undef FUNC
343
0
344
0
  MOZ_ASSERT(!mInitialized);
345
0
346
0
  speechdLib = PR_LoadLibrary("libspeechd.so.2");
347
0
348
0
  if (!speechdLib) {
349
0
    NS_WARNING("Failed to load speechd library");
350
0
    return;
351
0
  }
352
0
353
0
  if (!PR_FindFunctionSymbol(speechdLib, "spd_get_volume")) {
354
0
    // There is no version getter function, so we rely on a symbol that was
355
0
    // introduced in release 0.8.2 in order to check for ABI compatibility.
356
0
    NS_WARNING("Unsupported version of speechd detected");
357
0
    return;
358
0
  }
359
0
360
0
  for (uint32_t i = 0; i < ArrayLength(kSpeechDispatcherSymbols); i++) {
361
0
    *kSpeechDispatcherSymbols[i].function =
362
0
      PR_FindFunctionSymbol(speechdLib, kSpeechDispatcherSymbols[i].functionName);
363
0
364
0
    if (!*kSpeechDispatcherSymbols[i].function) {
365
0
      NS_WARNING(nsPrintfCString("Failed to find speechd symbol for'%s'",
366
0
                                 kSpeechDispatcherSymbols[i].functionName).get());
367
0
      return;
368
0
    }
369
0
  }
370
0
371
0
  mSpeechdClient = spd_open("firefox", "web speech api", "who", SPD_MODE_THREADED);
372
0
  if (!mSpeechdClient) {
373
0
    NS_WARNING("Failed to call spd_open");
374
0
    return;
375
0
  }
376
0
377
0
  // Get all the voices from sapi and register in the SynthVoiceRegistry
378
0
  SPDVoice** list = spd_list_synthesis_voices(mSpeechdClient);
379
0
380
0
  mSpeechdClient->callback_begin = speechd_cb;
381
0
  mSpeechdClient->callback_end = speechd_cb;
382
0
  mSpeechdClient->callback_cancel = speechd_cb;
383
0
  mSpeechdClient->callback_pause = speechd_cb;
384
0
  mSpeechdClient->callback_resume = speechd_cb;
385
0
386
0
  spd_set_notification_on(mSpeechdClient, SPD_BEGIN);
387
0
  spd_set_notification_on(mSpeechdClient, SPD_END);
388
0
  spd_set_notification_on(mSpeechdClient, SPD_CANCEL);
389
0
390
0
  if (list != NULL) {
391
0
    for (int i = 0; list[i]; i++) {
392
0
      nsAutoString uri;
393
0
394
0
      uri.AssignLiteral(URI_PREFIX);
395
0
      nsAutoCString name;
396
0
      NS_EscapeURL(list[i]->name, -1, esc_OnlyNonASCII | esc_Spaces | esc_AlwaysCopy, name);
397
0
      uri.Append(NS_ConvertUTF8toUTF16(name));;
398
0
      uri.AppendLiteral("?");
399
0
400
0
      nsAutoCString lang(list[i]->language);
401
0
402
0
      if (strcmp(list[i]->variant, "none") != 0) {
403
0
        // In speech dispatcher, the variant will usually be the locale subtag
404
0
        // with another, non-standard suptag after it. We keep the first one
405
0
        // and convert it to uppercase.
406
0
        const char* v = list[i]->variant;
407
0
        const char* hyphen = strchr(v, '-');
408
0
        nsDependentCSubstring variant(v, hyphen ? hyphen - v : strlen(v));
409
0
        ToUpperCase(variant);
410
0
411
0
        // eSpeak uses UK which is not a valid region subtag in BCP47.
412
0
        if (variant.EqualsLiteral("UK")) {
413
0
          variant.AssignLiteral("GB");
414
0
        }
415
0
416
0
        lang.AppendLiteral("-");
417
0
        lang.Append(variant);
418
0
      }
419
0
420
0
      uri.Append(NS_ConvertUTF8toUTF16(lang));
421
0
422
0
      mVoices.Put(uri, new SpeechDispatcherVoice(
423
0
                    NS_ConvertUTF8toUTF16(list[i]->name),
424
0
                    NS_ConvertUTF8toUTF16(lang)));
425
0
    }
426
0
  }
427
0
428
0
  NS_DispatchToMainThread(
429
0
    NewRunnableMethod("dom::SpeechDispatcherService::RegisterVoices",
430
0
                      this,
431
0
                      &SpeechDispatcherService::RegisterVoices));
432
0
433
0
  //mInitialized = true;
434
0
}
435
436
// private methods
437
438
void
439
SpeechDispatcherService::RegisterVoices()
440
0
{
441
0
  RefPtr<nsSynthVoiceRegistry> registry = nsSynthVoiceRegistry::GetInstance();
442
0
  for (auto iter = mVoices.Iter(); !iter.Done(); iter.Next()) {
443
0
    RefPtr<SpeechDispatcherVoice>& voice = iter.Data();
444
0
445
0
    // This service can only speak one utterance at a time, so we set
446
0
    // aQueuesUtterances to true in order to track global state and schedule
447
0
    // access to this service.
448
0
    DebugOnly<nsresult> rv =
449
0
      registry->AddVoice(this, iter.Key(), voice->mName, voice->mLanguage,
450
0
                         voice->mName.EqualsLiteral("default"), true);
451
0
452
0
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to add voice");
453
0
  }
454
0
455
0
  mInitThread->Shutdown();
456
0
  mInitThread = nullptr;
457
0
458
0
  mInitialized = true;
459
0
460
0
  registry->NotifyVoicesChanged();
461
0
}
462
463
// nsIObserver
464
465
NS_IMETHODIMP
466
SpeechDispatcherService::Observe(nsISupports* aSubject, const char* aTopic,
467
                                 const char16_t* aData)
468
0
{
469
0
  return NS_OK;
470
0
}
471
472
// nsISpeechService
473
474
// TODO: Support SSML
475
NS_IMETHODIMP
476
SpeechDispatcherService::Speak(const nsAString& aText, const nsAString& aUri,
477
                               float aVolume, float aRate, float aPitch,
478
                               nsISpeechTask* aTask)
479
0
{
480
0
  if (NS_WARN_IF(!mInitialized)) {
481
0
    return NS_ERROR_NOT_AVAILABLE;
482
0
  }
483
0
484
0
  RefPtr<SpeechDispatcherCallback> callback =
485
0
    new SpeechDispatcherCallback(aTask, this);
486
0
487
0
  bool found = false;
488
0
  SpeechDispatcherVoice* voice = mVoices.GetWeak(aUri, &found);
489
0
490
0
  if(NS_WARN_IF(!(found))) {
491
0
    return NS_ERROR_NOT_AVAILABLE;
492
0
  }
493
0
494
0
  spd_set_synthesis_voice(mSpeechdClient,
495
0
                          NS_ConvertUTF16toUTF8(voice->mName).get());
496
0
497
0
  // We provide a volume of 0.0 to 1.0, speech-dispatcher expects 0 - 100.
498
0
  spd_set_volume(mSpeechdClient, static_cast<int>(aVolume * 100));
499
0
500
0
  // aRate is a value of 0.1 (0.1x) to 10 (10x) with 1 (1x) being normal rate.
501
0
  // speechd expects -100 to 100 with 0 being normal rate.
502
0
  float rate = 0;
503
0
  if (aRate > 1) {
504
0
    // Each step to 100 is logarithmically distributed up to 2.5x.
505
0
    rate = log10(std::min(aRate, MAX_RATE)) / log10(MAX_RATE) * 100;
506
0
  } else if (aRate < 1) {
507
0
    // Each step to -100 is logarithmically distributed down to 0.5x.
508
0
    rate = log10(std::max(aRate, MIN_RATE)) / log10(MIN_RATE) * -100;
509
0
  }
510
0
511
0
  spd_set_voice_rate(mSpeechdClient, static_cast<int>(rate));
512
0
513
0
  // We provide a pitch of 0 to 2 with 1 being the default.
514
0
  // speech-dispatcher expects -100 to 100 with 0 being default.
515
0
  spd_set_voice_pitch(mSpeechdClient, static_cast<int>((aPitch - 1) * 100));
516
0
517
0
  nsresult rv = aTask->Setup(callback);
518
0
519
0
  if (NS_FAILED(rv)) {
520
0
    return rv;
521
0
  }
522
0
523
0
  if (aText.Length()) {
524
0
    int msg_id = spd_say(
525
0
      mSpeechdClient, SPD_MESSAGE, NS_ConvertUTF16toUTF8(aText).get());
526
0
527
0
    if (msg_id < 0) {
528
0
      return NS_ERROR_FAILURE;
529
0
    }
530
0
531
0
    mCallbacks.Put(msg_id, callback);
532
0
  } else {
533
0
    // Speech dispatcher does not work well with empty strings.
534
0
    // In that case, don't send empty string to speechd,
535
0
    // and just emulate a speechd start and end event.
536
0
    NS_DispatchToMainThread(NewRunnableMethod<SPDNotificationType>(
537
0
      "dom::SpeechDispatcherCallback::OnSpeechEvent",
538
0
      callback,
539
0
      &SpeechDispatcherCallback::OnSpeechEvent,
540
0
      SPD_EVENT_BEGIN));
541
0
542
0
    NS_DispatchToMainThread(NewRunnableMethod<SPDNotificationType>(
543
0
      "dom::SpeechDispatcherCallback::OnSpeechEvent",
544
0
      callback,
545
0
      &SpeechDispatcherCallback::OnSpeechEvent,
546
0
      SPD_EVENT_END));
547
0
  }
548
0
549
0
  return NS_OK;
550
0
}
551
552
SpeechDispatcherService*
553
SpeechDispatcherService::GetInstance(bool create)
554
0
{
555
0
  if (XRE_GetProcessType() != GeckoProcessType_Default) {
556
0
    MOZ_ASSERT(false,
557
0
               "SpeechDispatcherService can only be started on main gecko process");
558
0
    return nullptr;
559
0
  }
560
0
561
0
  if (!sSingleton && create) {
562
0
    sSingleton = new SpeechDispatcherService();
563
0
    sSingleton->Init();
564
0
    ClearOnShutdown(&sSingleton);
565
0
  }
566
0
567
0
  return sSingleton;
568
0
}
569
570
already_AddRefed<SpeechDispatcherService>
571
SpeechDispatcherService::GetInstanceForService()
572
0
{
573
0
  MOZ_ASSERT(NS_IsMainThread());
574
0
  RefPtr<SpeechDispatcherService> sapiService = GetInstance();
575
0
  return sapiService.forget();
576
0
}
577
578
void
579
SpeechDispatcherService::EventNotify(uint32_t aMsgId, uint32_t aState)
580
0
{
581
0
  SpeechDispatcherCallback* callback = mCallbacks.GetWeak(aMsgId);
582
0
583
0
  if (callback) {
584
0
    if (callback->OnSpeechEvent((SPDNotificationType)aState)) {
585
0
      mCallbacks.Remove(aMsgId);
586
0
    }
587
0
  }
588
0
}
589
590
} // namespace dom
591
} // namespace mozilla