1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
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 "MediaBufferDecoder.h"
8 #include "mozilla/dom/AudioContextBinding.h"
9 #include "mozilla/dom/BaseAudioContextBinding.h"
10 #include "mozilla/dom/DOMException.h"
11 #include "mozilla/dom/ScriptSettings.h"
12 #include "mozilla/AbstractThread.h"
13 #include <speex/speex_resampler.h>
14 #include "nsXPCOMCIDInternal.h"
15 #include "nsComponentManagerUtils.h"
16 #include "MediaFormatReader.h"
17 #include "MediaQueue.h"
18 #include "BufferMediaResource.h"
19 #include "DecoderTraits.h"
20 #include "AudioContext.h"
21 #include "AudioBuffer.h"
22 #include "MediaContainerType.h"
23 #include "nsContentUtils.h"
24 #include "nsIScriptObjectPrincipal.h"
25 #include "nsIScriptError.h"
26 #include "nsMimeTypes.h"
27 #include "VideoUtils.h"
28 #include "WebAudioUtils.h"
29 #include "mozilla/dom/Promise.h"
30 #include "mozilla/Telemetry.h"
31 #include "nsPrintfCString.h"
32 #include "AudioNodeEngine.h"
33
34 namespace mozilla {
35
36 extern LazyLogModule gMediaDecoderLog;
37
38 using namespace dom;
39
40 class ReportResultTask final : public Runnable {
41 public:
ReportResultTask(WebAudioDecodeJob & aDecodeJob,WebAudioDecodeJob::ResultFn aFunction,WebAudioDecodeJob::ErrorCode aErrorCode)42 ReportResultTask(WebAudioDecodeJob& aDecodeJob,
43 WebAudioDecodeJob::ResultFn aFunction,
44 WebAudioDecodeJob::ErrorCode aErrorCode)
45 : Runnable("ReportResultTask"),
46 mDecodeJob(aDecodeJob),
47 mFunction(aFunction),
48 mErrorCode(aErrorCode) {
49 MOZ_ASSERT(aFunction);
50 }
51
Run()52 NS_IMETHOD Run() override {
53 MOZ_ASSERT(NS_IsMainThread());
54
55 (mDecodeJob.*mFunction)(mErrorCode);
56
57 return NS_OK;
58 }
59
60 private:
61 // Note that the mDecodeJob member will probably die when mFunction is run.
62 // Therefore, it is not safe to do anything fancy with it in this class.
63 // Really, this class is only used because nsRunnableMethod doesn't support
64 // methods accepting arguments.
65 WebAudioDecodeJob& mDecodeJob;
66 WebAudioDecodeJob::ResultFn mFunction;
67 WebAudioDecodeJob::ErrorCode mErrorCode;
68 };
69
70 enum class PhaseEnum : int { Decode, AllocateBuffer, Done };
71
72 class MediaDecodeTask final : public Runnable {
73 public:
MediaDecodeTask(const MediaContainerType & aContainerType,uint8_t * aBuffer,uint32_t aLength,WebAudioDecodeJob & aDecodeJob)74 MediaDecodeTask(const MediaContainerType& aContainerType, uint8_t* aBuffer,
75 uint32_t aLength, WebAudioDecodeJob& aDecodeJob)
76 : Runnable("MediaDecodeTask"),
77 mContainerType(aContainerType),
78 mBuffer(aBuffer),
79 mLength(aLength),
80 mDecodeJob(aDecodeJob),
81 mPhase(PhaseEnum::Decode),
82 mFirstFrameDecoded(false) {
83 MOZ_ASSERT(aBuffer);
84 MOZ_ASSERT(NS_IsMainThread());
85 }
86
87 NS_IMETHOD Run() override;
88 bool CreateReader();
Reader()89 MediaFormatReader* Reader() {
90 MOZ_ASSERT(mDecoderReader);
91 return mDecoderReader;
92 }
93
94 private:
ReportFailureOnMainThread(WebAudioDecodeJob::ErrorCode aErrorCode)95 void ReportFailureOnMainThread(WebAudioDecodeJob::ErrorCode aErrorCode) {
96 if (NS_IsMainThread()) {
97 Cleanup();
98 mDecodeJob.OnFailure(aErrorCode);
99 } else {
100 // Take extra care to cleanup on the main thread
101 mMainThread->Dispatch(NewRunnableMethod("MediaDecodeTask::Cleanup", this,
102 &MediaDecodeTask::Cleanup));
103
104 nsCOMPtr<nsIRunnable> event = new ReportResultTask(
105 mDecodeJob, &WebAudioDecodeJob::OnFailure, aErrorCode);
106 mMainThread->Dispatch(event.forget());
107 }
108 }
109
110 void Decode();
111 void OnMetadataRead(MetadataHolder&& aMetadata);
112 void OnMetadataNotRead(const MediaResult& aError);
113 void RequestSample();
114 void SampleDecoded(RefPtr<AudioData> aData);
115 void SampleNotDecoded(const MediaResult& aError);
116 void FinishDecode();
117 void AllocateBuffer();
118 void CallbackTheResult();
119
Cleanup()120 void Cleanup() {
121 MOZ_ASSERT(NS_IsMainThread());
122 mDecoderReader = nullptr;
123 JS_free(nullptr, mBuffer);
124 }
125
126 private:
127 MediaContainerType mContainerType;
128 uint8_t* mBuffer;
129 uint32_t mLength;
130 WebAudioDecodeJob& mDecodeJob;
131 PhaseEnum mPhase;
132 RefPtr<MediaFormatReader> mDecoderReader;
133 MediaInfo mMediaInfo;
134 MediaQueue<AudioData> mAudioQueue;
135 RefPtr<AbstractThread> mMainThread;
136 bool mFirstFrameDecoded;
137 };
138
139 NS_IMETHODIMP
Run()140 MediaDecodeTask::Run() {
141 MOZ_ASSERT(mDecoderReader);
142 switch (mPhase) {
143 case PhaseEnum::Decode:
144 Decode();
145 break;
146 case PhaseEnum::AllocateBuffer:
147 AllocateBuffer();
148 break;
149 case PhaseEnum::Done:
150 break;
151 }
152
153 return NS_OK;
154 }
155
CreateReader()156 bool MediaDecodeTask::CreateReader() {
157 MOZ_ASSERT(NS_IsMainThread());
158
159 RefPtr<BufferMediaResource> resource =
160 new BufferMediaResource(static_cast<uint8_t*>(mBuffer), mLength);
161
162 mMainThread = mDecodeJob.mContext->GetOwnerGlobal()->AbstractMainThreadFor(
163 TaskCategory::Other);
164
165 // If you change this list to add support for new decoders, please consider
166 // updating HTMLMediaElement::CreateDecoder as well.
167
168 MediaFormatReaderInit init;
169 init.mResource = resource;
170 mDecoderReader = DecoderTraits::CreateReader(mContainerType, init);
171
172 if (!mDecoderReader) {
173 return false;
174 }
175
176 nsresult rv = mDecoderReader->Init();
177 if (NS_FAILED(rv)) {
178 return false;
179 }
180
181 return true;
182 }
183
184 class AutoResampler final {
185 public:
AutoResampler()186 AutoResampler() : mResampler(nullptr) {}
~AutoResampler()187 ~AutoResampler() {
188 if (mResampler) {
189 speex_resampler_destroy(mResampler);
190 }
191 }
operator SpeexResamplerState*() const192 operator SpeexResamplerState*() const {
193 MOZ_ASSERT(mResampler);
194 return mResampler;
195 }
operator =(SpeexResamplerState * aResampler)196 void operator=(SpeexResamplerState* aResampler) { mResampler = aResampler; }
197
198 private:
199 SpeexResamplerState* mResampler;
200 };
201
Decode()202 void MediaDecodeTask::Decode() {
203 MOZ_ASSERT(!NS_IsMainThread());
204
205 mDecoderReader->AsyncReadMetadata()->Then(
206 mDecoderReader->OwnerThread(), __func__, this,
207 &MediaDecodeTask::OnMetadataRead, &MediaDecodeTask::OnMetadataNotRead);
208 }
209
OnMetadataRead(MetadataHolder && aMetadata)210 void MediaDecodeTask::OnMetadataRead(MetadataHolder&& aMetadata) {
211 mMediaInfo = *aMetadata.mInfo;
212 if (!mMediaInfo.HasAudio()) {
213 mDecoderReader->Shutdown();
214 ReportFailureOnMainThread(WebAudioDecodeJob::NoAudio);
215 return;
216 }
217
218 nsCString codec;
219 if (!mMediaInfo.mAudio.GetAsAudioInfo()->mMimeType.IsEmpty()) {
220 codec = nsPrintfCString(
221 "webaudio; %s", mMediaInfo.mAudio.GetAsAudioInfo()->mMimeType.get());
222 } else {
223 codec = nsPrintfCString("webaudio;resource; %s",
224 mContainerType.Type().AsString().Data());
225 }
226
227 nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction(
228 "MediaDecodeTask::OnMetadataRead", [codec]() -> void {
229 MOZ_ASSERT(!codec.IsEmpty());
230 MOZ_LOG(gMediaDecoderLog, LogLevel::Debug,
231 ("Telemetry (WebAudio) MEDIA_CODEC_USED= '%s'", codec.get()));
232 Telemetry::Accumulate(Telemetry::HistogramID::MEDIA_CODEC_USED, codec);
233 });
234 SystemGroup::Dispatch(TaskCategory::Other, task.forget());
235
236 RequestSample();
237 }
238
OnMetadataNotRead(const MediaResult & aReason)239 void MediaDecodeTask::OnMetadataNotRead(const MediaResult& aReason) {
240 mDecoderReader->Shutdown();
241 ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent);
242 }
243
RequestSample()244 void MediaDecodeTask::RequestSample() {
245 mDecoderReader->RequestAudioData()->Then(
246 mDecoderReader->OwnerThread(), __func__, this,
247 &MediaDecodeTask::SampleDecoded, &MediaDecodeTask::SampleNotDecoded);
248 }
249
SampleDecoded(RefPtr<AudioData> aData)250 void MediaDecodeTask::SampleDecoded(RefPtr<AudioData> aData) {
251 MOZ_ASSERT(!NS_IsMainThread());
252 mAudioQueue.Push(aData);
253 if (!mFirstFrameDecoded) {
254 mDecoderReader->ReadUpdatedMetadata(&mMediaInfo);
255 mFirstFrameDecoded = true;
256 }
257 RequestSample();
258 }
259
SampleNotDecoded(const MediaResult & aError)260 void MediaDecodeTask::SampleNotDecoded(const MediaResult& aError) {
261 MOZ_ASSERT(!NS_IsMainThread());
262 if (aError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
263 FinishDecode();
264 } else {
265 mDecoderReader->Shutdown();
266 ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent);
267 }
268 }
269
FinishDecode()270 void MediaDecodeTask::FinishDecode() {
271 mDecoderReader->Shutdown();
272
273 uint32_t frameCount = mAudioQueue.FrameCount();
274 uint32_t channelCount = mMediaInfo.mAudio.mChannels;
275 uint32_t sampleRate = mMediaInfo.mAudio.mRate;
276
277 if (!frameCount || !channelCount || !sampleRate) {
278 ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent);
279 return;
280 }
281
282 const uint32_t destSampleRate = mDecodeJob.mContext->SampleRate();
283 AutoResampler resampler;
284
285 uint32_t resampledFrames = frameCount;
286 if (sampleRate != destSampleRate) {
287 resampledFrames = static_cast<uint32_t>(
288 static_cast<uint64_t>(destSampleRate) *
289 static_cast<uint64_t>(frameCount) / static_cast<uint64_t>(sampleRate));
290
291 resampler = speex_resampler_init(channelCount, sampleRate, destSampleRate,
292 SPEEX_RESAMPLER_QUALITY_DEFAULT, nullptr);
293 speex_resampler_skip_zeros(resampler);
294 resampledFrames += speex_resampler_get_output_latency(resampler);
295 }
296
297 // Allocate contiguous channel buffers. Note that if we end up resampling,
298 // we may write fewer bytes than mResampledFrames to the output buffer, in
299 // which case writeIndex will tell us how many valid samples we have.
300 mDecodeJob.mBuffer.mChannelData.SetLength(channelCount);
301 #if AUDIO_OUTPUT_FORMAT == AUDIO_FORMAT_FLOAT32
302 // This buffer has separate channel arrays that could be transferred to
303 // JS_NewArrayBufferWithContents(), but AudioBuffer::RestoreJSChannelData()
304 // does not yet take advantage of this.
305 RefPtr<ThreadSharedFloatArrayBufferList> buffer =
306 ThreadSharedFloatArrayBufferList::Create(channelCount, resampledFrames,
307 fallible);
308 if (!buffer) {
309 ReportFailureOnMainThread(WebAudioDecodeJob::UnknownError);
310 return;
311 }
312 for (uint32_t i = 0; i < channelCount; ++i) {
313 mDecodeJob.mBuffer.mChannelData[i] = buffer->GetData(i);
314 }
315 #else
316 RefPtr<SharedBuffer> buffer = SharedBuffer::Create(
317 sizeof(AudioDataValue) * resampledFrames * channelCount);
318 if (!buffer) {
319 ReportFailureOnMainThread(WebAudioDecodeJob::UnknownError);
320 return;
321 }
322 auto data = static_cast<AudioDataValue*>(floatBuffer->Data());
323 for (uint32_t i = 0; i < channelCount; ++i) {
324 mDecodeJob.mBuffer.mChannelData[i] = data;
325 data += resampledFrames;
326 }
327 #endif
328 mDecodeJob.mBuffer.mBuffer = buffer.forget();
329 mDecodeJob.mBuffer.mVolume = 1.0f;
330 mDecodeJob.mBuffer.mBufferFormat = AUDIO_OUTPUT_FORMAT;
331
332 uint32_t writeIndex = 0;
333 RefPtr<AudioData> audioData;
334 while ((audioData = mAudioQueue.PopFront())) {
335 audioData->EnsureAudioBuffer(); // could lead to a copy :(
336 const AudioDataValue* bufferData =
337 static_cast<AudioDataValue*>(audioData->mAudioBuffer->Data());
338
339 if (sampleRate != destSampleRate) {
340 const uint32_t maxOutSamples = resampledFrames - writeIndex;
341
342 for (uint32_t i = 0; i < audioData->mChannels; ++i) {
343 uint32_t inSamples = audioData->mFrames;
344 uint32_t outSamples = maxOutSamples;
345 AudioDataValue* outData =
346 mDecodeJob.mBuffer.ChannelDataForWrite<AudioDataValue>(i) +
347 writeIndex;
348
349 WebAudioUtils::SpeexResamplerProcess(
350 resampler, i, &bufferData[i * audioData->mFrames], &inSamples,
351 outData, &outSamples);
352
353 if (i == audioData->mChannels - 1) {
354 writeIndex += outSamples;
355 MOZ_ASSERT(writeIndex <= resampledFrames);
356 MOZ_ASSERT(inSamples == audioData->mFrames);
357 }
358 }
359 } else {
360 for (uint32_t i = 0; i < audioData->mChannels; ++i) {
361 AudioDataValue* outData =
362 mDecodeJob.mBuffer.ChannelDataForWrite<AudioDataValue>(i) +
363 writeIndex;
364 PodCopy(outData, &bufferData[i * audioData->mFrames],
365 audioData->mFrames);
366
367 if (i == audioData->mChannels - 1) {
368 writeIndex += audioData->mFrames;
369 }
370 }
371 }
372 }
373
374 if (sampleRate != destSampleRate) {
375 uint32_t inputLatency = speex_resampler_get_input_latency(resampler);
376 const uint32_t maxOutSamples = resampledFrames - writeIndex;
377 for (uint32_t i = 0; i < channelCount; ++i) {
378 uint32_t inSamples = inputLatency;
379 uint32_t outSamples = maxOutSamples;
380 AudioDataValue* outData =
381 mDecodeJob.mBuffer.ChannelDataForWrite<AudioDataValue>(i) +
382 writeIndex;
383
384 WebAudioUtils::SpeexResamplerProcess(resampler, i,
385 (AudioDataValue*)nullptr, &inSamples,
386 outData, &outSamples);
387
388 if (i == channelCount - 1) {
389 writeIndex += outSamples;
390 MOZ_ASSERT(writeIndex <= resampledFrames);
391 MOZ_ASSERT(inSamples == inputLatency);
392 }
393 }
394 }
395
396 mDecodeJob.mBuffer.mDuration = writeIndex;
397 mPhase = PhaseEnum::AllocateBuffer;
398 mMainThread->Dispatch(do_AddRef(this));
399 }
400
AllocateBuffer()401 void MediaDecodeTask::AllocateBuffer() {
402 MOZ_ASSERT(NS_IsMainThread());
403
404 if (!mDecodeJob.AllocateBuffer()) {
405 ReportFailureOnMainThread(WebAudioDecodeJob::UnknownError);
406 return;
407 }
408
409 mPhase = PhaseEnum::Done;
410 CallbackTheResult();
411 }
412
CallbackTheResult()413 void MediaDecodeTask::CallbackTheResult() {
414 MOZ_ASSERT(NS_IsMainThread());
415
416 Cleanup();
417
418 // Now, we're ready to call the script back with the resulting buffer
419 mDecodeJob.OnSuccess(WebAudioDecodeJob::NoError);
420 }
421
AllocateBuffer()422 bool WebAudioDecodeJob::AllocateBuffer() {
423 MOZ_ASSERT(!mOutput);
424 MOZ_ASSERT(NS_IsMainThread());
425
426 // Now create the AudioBuffer
427 mOutput = AudioBuffer::Create(mContext->GetOwner(), mContext->SampleRate(),
428 Move(mBuffer));
429 return mOutput != nullptr;
430 }
431
AsyncDecodeWebAudio(const char * aContentType,uint8_t * aBuffer,uint32_t aLength,WebAudioDecodeJob & aDecodeJob)432 void AsyncDecodeWebAudio(const char* aContentType, uint8_t* aBuffer,
433 uint32_t aLength, WebAudioDecodeJob& aDecodeJob) {
434 Maybe<MediaContainerType> containerType =
435 MakeMediaContainerType(aContentType);
436 // Do not attempt to decode the media if we were not successful at sniffing
437 // the container type.
438 if (!*aContentType || strcmp(aContentType, APPLICATION_OCTET_STREAM) == 0 ||
439 !containerType) {
440 nsCOMPtr<nsIRunnable> event =
441 new ReportResultTask(aDecodeJob, &WebAudioDecodeJob::OnFailure,
442 WebAudioDecodeJob::UnknownContent);
443 JS_free(nullptr, aBuffer);
444 aDecodeJob.mContext->Dispatch(event.forget());
445 return;
446 }
447
448 RefPtr<MediaDecodeTask> task =
449 new MediaDecodeTask(*containerType, aBuffer, aLength, aDecodeJob);
450 if (!task->CreateReader()) {
451 nsCOMPtr<nsIRunnable> event =
452 new ReportResultTask(aDecodeJob, &WebAudioDecodeJob::OnFailure,
453 WebAudioDecodeJob::UnknownError);
454 aDecodeJob.mContext->Dispatch(event.forget());
455 } else {
456 // If we did this without a temporary:
457 // task->Reader()->OwnerThread()->Dispatch(task.forget())
458 // we might evaluate the task.forget() before calling Reader(). Enforce
459 // a non-crashy order-of-operations.
460 TaskQueue* taskQueue = task->Reader()->OwnerThread();
461 nsresult rv = taskQueue->Dispatch(task.forget());
462 MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
463 Unused << rv;
464 }
465 }
466
WebAudioDecodeJob(AudioContext * aContext,Promise * aPromise,DecodeSuccessCallback * aSuccessCallback,DecodeErrorCallback * aFailureCallback)467 WebAudioDecodeJob::WebAudioDecodeJob(AudioContext* aContext, Promise* aPromise,
468 DecodeSuccessCallback* aSuccessCallback,
469 DecodeErrorCallback* aFailureCallback)
470 : mContext(aContext),
471 mPromise(aPromise),
472 mSuccessCallback(aSuccessCallback),
473 mFailureCallback(aFailureCallback) {
474 MOZ_ASSERT(aContext);
475 MOZ_ASSERT(NS_IsMainThread());
476 MOZ_COUNT_CTOR(WebAudioDecodeJob);
477 }
478
~WebAudioDecodeJob()479 WebAudioDecodeJob::~WebAudioDecodeJob() {
480 MOZ_ASSERT(NS_IsMainThread());
481 MOZ_COUNT_DTOR(WebAudioDecodeJob);
482 }
483
OnSuccess(ErrorCode aErrorCode)484 void WebAudioDecodeJob::OnSuccess(ErrorCode aErrorCode) {
485 MOZ_ASSERT(NS_IsMainThread());
486 MOZ_ASSERT(aErrorCode == NoError);
487
488 if (mSuccessCallback) {
489 ErrorResult rv;
490 mSuccessCallback->Call(*mOutput, rv);
491 // Ignore errors in calling the callback, since there is not much that we
492 // can do about it here.
493 rv.SuppressException();
494 }
495 mPromise->MaybeResolve(mOutput);
496
497 mContext->RemoveFromDecodeQueue(this);
498 }
499
OnFailure(ErrorCode aErrorCode)500 void WebAudioDecodeJob::OnFailure(ErrorCode aErrorCode) {
501 MOZ_ASSERT(NS_IsMainThread());
502
503 const char* errorMessage;
504 switch (aErrorCode) {
505 case NoError:
506 MOZ_FALLTHROUGH_ASSERT("Who passed NoError to OnFailure?");
507 // Fall through to get some sort of a sane error message if this actually
508 // happens at runtime.
509 case UnknownError:
510 errorMessage = "MediaDecodeAudioDataUnknownError";
511 break;
512 case UnknownContent:
513 errorMessage = "MediaDecodeAudioDataUnknownContentType";
514 break;
515 case InvalidContent:
516 errorMessage = "MediaDecodeAudioDataInvalidContent";
517 break;
518 case NoAudio:
519 errorMessage = "MediaDecodeAudioDataNoAudio";
520 break;
521 }
522
523 nsIDocument* doc = nullptr;
524 if (nsPIDOMWindowInner* pWindow = mContext->GetParentObject()) {
525 doc = pWindow->GetExtantDoc();
526 }
527 nsContentUtils::ReportToConsole(
528 nsIScriptError::errorFlag, NS_LITERAL_CSTRING("Media"), doc,
529 nsContentUtils::eDOM_PROPERTIES, errorMessage);
530
531 // Ignore errors in calling the callback, since there is not much that we can
532 // do about it here.
533 if (mFailureCallback) {
534 nsAutoCString errorString(errorMessage);
535 RefPtr<DOMException> exception = DOMException::Create(
536 NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR, errorString);
537 mFailureCallback->Call(*exception);
538 }
539
540 mPromise->MaybeReject(NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR);
541
542 mContext->RemoveFromDecodeQueue(this);
543 }
544
SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const545 size_t WebAudioDecodeJob::SizeOfExcludingThis(
546 MallocSizeOf aMallocSizeOf) const {
547 size_t amount = 0;
548 if (mSuccessCallback) {
549 amount += mSuccessCallback->SizeOfIncludingThis(aMallocSizeOf);
550 }
551 if (mFailureCallback) {
552 amount += mFailureCallback->SizeOfIncludingThis(aMallocSizeOf);
553 }
554 if (mOutput) {
555 amount += mOutput->SizeOfIncludingThis(aMallocSizeOf);
556 }
557 amount += mBuffer.SizeOfExcludingThis(aMallocSizeOf, false);
558 return amount;
559 }
560
SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const561 size_t WebAudioDecodeJob::SizeOfIncludingThis(
562 MallocSizeOf aMallocSizeOf) const {
563 return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
564 }
565
566 } // namespace mozilla
567