1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/content/media/apple/AppleMP3Reader.cpp Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,546 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +#include "AppleMP3Reader.h" 1.9 + 1.10 +#include "nsISeekableStream.h" 1.11 +#include "MediaDecoder.h" 1.12 + 1.13 +// Number of bytes we will read and pass to the audio parser in each 1.14 +// |DecodeAudioData| call. 1.15 +#define AUDIO_READ_BYTES 4096 1.16 + 1.17 +// Maximum number of audio frames we will accept from the audio decoder in one 1.18 +// go. Carefully select this to work well with both the mp3 1152 max frames 1.19 +// per block and power-of-2 allocation sizes. Since we must pre-allocate the 1.20 +// buffer we cannot use AudioCompactor without paying for an additional 1.21 +// allocation and copy. Therefore, choosing a value that divides exactly into 1.22 +// 1152 is most memory efficient. 1.23 +#define MAX_AUDIO_FRAMES 128 1.24 + 1.25 +namespace mozilla { 1.26 + 1.27 +#ifdef PR_LOGGING 1.28 +extern PRLogModuleInfo* gMediaDecoderLog; 1.29 +#define LOGE(...) PR_LOG(gMediaDecoderLog, PR_LOG_ERROR, (__VA_ARGS__)) 1.30 +#define LOGW(...) PR_LOG(gMediaDecoderLog, PR_LOG_WARNING, (__VA_ARGS__)) 1.31 +#define LOGD(...) PR_LOG(gMediaDecoderLog, PR_LOG_DEBUG, (__VA_ARGS__)) 1.32 +#else 1.33 +#define LOGE(...) 1.34 +#define LOGW(...) 1.35 +#define LOGD(...) 1.36 +#endif 1.37 + 1.38 +#define PROPERTY_ID_FORMAT "%c%c%c%c" 1.39 +#define PROPERTY_ID_PRINT(x) ((x) >> 24), \ 1.40 + ((x) >> 16) & 0xff, \ 1.41 + ((x) >> 8) & 0xff, \ 1.42 + (x) & 0xff 1.43 + 1.44 +AppleMP3Reader::AppleMP3Reader(AbstractMediaDecoder *aDecoder) 1.45 + : MediaDecoderReader(aDecoder) 1.46 + , mStreamReady(false) 1.47 + , mAudioFramesPerCompressedPacket(0) 1.48 + , mCurrentAudioFrame(0) 1.49 + , mAudioChannels(0) 1.50 + , mAudioSampleRate(0) 1.51 + , mAudioFileStream(nullptr) 1.52 + , mAudioConverter(nullptr) 1.53 + , mMP3FrameParser(mDecoder->GetResource()->GetLength()) 1.54 +{ 1.55 + MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread"); 1.56 +} 1.57 + 1.58 +AppleMP3Reader::~AppleMP3Reader() 1.59 +{ 1.60 + MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread"); 1.61 +} 1.62 + 1.63 + 1.64 +/* 1.65 + * The Apple audio decoding APIs are very callback-happy. When the parser has 1.66 + * some metadata, it will call back to here. 1.67 + */ 1.68 +static void _AudioMetadataCallback(void *aThis, 1.69 + AudioFileStreamID aFileStream, 1.70 + AudioFileStreamPropertyID aPropertyID, 1.71 + UInt32 *aFlags) 1.72 +{ 1.73 + ((AppleMP3Reader*)aThis)->AudioMetadataCallback(aFileStream, aPropertyID, 1.74 + aFlags); 1.75 +} 1.76 + 1.77 +/* 1.78 + * Similar to above, this is called when the parser has enough data to parse 1.79 + * one or more samples. 1.80 + */ 1.81 +static void _AudioSampleCallback(void *aThis, 1.82 + UInt32 aNumBytes, UInt32 aNumPackets, 1.83 + const void *aData, 1.84 + AudioStreamPacketDescription *aPackets) 1.85 +{ 1.86 + ((AppleMP3Reader*)aThis)->AudioSampleCallback(aNumBytes, aNumPackets, 1.87 + aData, aPackets); 1.88 +} 1.89 + 1.90 + 1.91 +/* 1.92 + * If we're not at end of stream, read |aNumBytes| from the media resource, 1.93 + * put it in |aData|, and return true. 1.94 + * Otherwise, put as much data as is left into |aData|, set |aNumBytes| to the 1.95 + * amount of data we have left, and return false. 1.96 + */ 1.97 +nsresult 1.98 +AppleMP3Reader::Read(uint32_t *aNumBytes, char *aData) 1.99 +{ 1.100 + MediaResource *resource = mDecoder->GetResource(); 1.101 + 1.102 + // Loop until we have all the data asked for, or we've reached EOS 1.103 + uint32_t totalBytes = 0; 1.104 + uint32_t numBytes; 1.105 + do { 1.106 + uint32_t bytesWanted = *aNumBytes - totalBytes; 1.107 + nsresult rv = resource->Read(aData + totalBytes, bytesWanted, &numBytes); 1.108 + totalBytes += numBytes; 1.109 + 1.110 + if (NS_FAILED(rv)) { 1.111 + *aNumBytes = 0; 1.112 + return NS_ERROR_FAILURE; 1.113 + } 1.114 + } while(totalBytes < *aNumBytes && numBytes); 1.115 + 1.116 + *aNumBytes = totalBytes; 1.117 + 1.118 + // We will have read some data in the last iteration iff we filled the buffer. 1.119 + // XXX Maybe return a better value than NS_ERROR_FAILURE? 1.120 + return numBytes ? NS_OK : NS_ERROR_FAILURE; 1.121 +} 1.122 + 1.123 +nsresult 1.124 +AppleMP3Reader::Init(MediaDecoderReader* aCloneDonor) 1.125 +{ 1.126 + AudioFileTypeID fileType = kAudioFileMP3Type; 1.127 + 1.128 + OSStatus rv = AudioFileStreamOpen(this, 1.129 + _AudioMetadataCallback, 1.130 + _AudioSampleCallback, 1.131 + fileType, 1.132 + &mAudioFileStream); 1.133 + 1.134 + if (rv) { 1.135 + return NS_ERROR_FAILURE; 1.136 + } 1.137 + 1.138 + return NS_OK; 1.139 +} 1.140 + 1.141 + 1.142 +struct PassthroughUserData { 1.143 + AppleMP3Reader *mReader; 1.144 + UInt32 mNumPackets; 1.145 + UInt32 mDataSize; 1.146 + const void *mData; 1.147 + AudioStreamPacketDescription *mPacketDesc; 1.148 + bool mDone; 1.149 +}; 1.150 + 1.151 +// Error value we pass through the decoder to signal that nothing has gone wrong 1.152 +// during decoding, but more data is needed. 1.153 +const UInt32 kNeedMoreData = 'MOAR'; 1.154 + 1.155 +/* 1.156 + * This function is called from |AudioConverterFillComplexBuffer|, which is 1.157 + * called from |AudioSampleCallback| below, which in turn is called by 1.158 + * |AudioFileStreamParseBytes|, which is called by |DecodeAudioData|. 1.159 + * 1.160 + * Mercifully, this is all synchronous. 1.161 + * 1.162 + * This callback is run when the AudioConverter (decoder) wants more MP3 packets 1.163 + * to decode. 1.164 + */ 1.165 +/* static */ OSStatus 1.166 +AppleMP3Reader::PassthroughInputDataCallback(AudioConverterRef aAudioConverter, 1.167 + UInt32 *aNumDataPackets /* in/out */, 1.168 + AudioBufferList *aData /* in/out */, 1.169 + AudioStreamPacketDescription **aPacketDesc, 1.170 + void *aUserData) 1.171 +{ 1.172 + PassthroughUserData *userData = (PassthroughUserData *)aUserData; 1.173 + if (userData->mDone) { 1.174 + // We make sure this callback is run _once_, with all the data we received 1.175 + // from |AudioFileStreamParseBytes|. When we return an error, the decoder 1.176 + // simply passes the return value on to the calling method, 1.177 + // |AudioSampleCallback|; and flushes all of the audio frames it had 1.178 + // buffered. It does not change the decoder's state. 1.179 + LOGD("requested too much data; returning\n"); 1.180 + *aNumDataPackets = 0; 1.181 + return kNeedMoreData; 1.182 + } 1.183 + 1.184 + userData->mDone = true; 1.185 + 1.186 + LOGD("AudioConverter wants %u packets of audio data\n", *aNumDataPackets); 1.187 + 1.188 + *aNumDataPackets = userData->mNumPackets; 1.189 + *aPacketDesc = userData->mPacketDesc; 1.190 + 1.191 + aData->mBuffers[0].mNumberChannels = userData->mReader->mAudioChannels; 1.192 + aData->mBuffers[0].mDataByteSize = userData->mDataSize; 1.193 + aData->mBuffers[0].mData = const_cast<void *>(userData->mData); 1.194 + 1.195 + return 0; 1.196 +} 1.197 + 1.198 +/* 1.199 + * This callback is called when |AudioFileStreamParseBytes| has enough data to 1.200 + * extract one or more MP3 packets. 1.201 + */ 1.202 +void 1.203 +AppleMP3Reader::AudioSampleCallback(UInt32 aNumBytes, 1.204 + UInt32 aNumPackets, 1.205 + const void *aData, 1.206 + AudioStreamPacketDescription *aPackets) 1.207 +{ 1.208 + LOGD("got %u bytes, %u packets\n", aNumBytes, aNumPackets); 1.209 + 1.210 + // 1 frame per packet * num channels * 32-bit float 1.211 + uint32_t decodedSize = MAX_AUDIO_FRAMES * mAudioChannels * 1.212 + sizeof(AudioDataValue); 1.213 + 1.214 + // descriptions for _decompressed_ audio packets. ignored. 1.215 + nsAutoArrayPtr<AudioStreamPacketDescription> 1.216 + packets(new AudioStreamPacketDescription[MAX_AUDIO_FRAMES]); 1.217 + 1.218 + // This API insists on having MP3 packets spoon-fed to it from a callback. 1.219 + // This structure exists only to pass our state and the result of the parser 1.220 + // on to the callback above. 1.221 + PassthroughUserData userData = { this, aNumPackets, aNumBytes, aData, aPackets, false }; 1.222 + 1.223 + do { 1.224 + // Decompressed audio buffer 1.225 + nsAutoArrayPtr<uint8_t> decoded(new uint8_t[decodedSize]); 1.226 + 1.227 + AudioBufferList decBuffer; 1.228 + decBuffer.mNumberBuffers = 1; 1.229 + decBuffer.mBuffers[0].mNumberChannels = mAudioChannels; 1.230 + decBuffer.mBuffers[0].mDataByteSize = decodedSize; 1.231 + decBuffer.mBuffers[0].mData = decoded.get(); 1.232 + 1.233 + // in: the max number of packets we can handle from the decoder. 1.234 + // out: the number of packets the decoder is actually returning. 1.235 + UInt32 numFrames = MAX_AUDIO_FRAMES; 1.236 + 1.237 + OSStatus rv = AudioConverterFillComplexBuffer(mAudioConverter, 1.238 + PassthroughInputDataCallback, 1.239 + &userData, 1.240 + &numFrames /* in/out */, 1.241 + &decBuffer, 1.242 + packets.get()); 1.243 + 1.244 + if (rv && rv != kNeedMoreData) { 1.245 + LOGE("Error decoding audio stream: %x\n", rv); 1.246 + break; 1.247 + } 1.248 + 1.249 + // If we decoded zero frames then AudiOConverterFillComplexBuffer is out 1.250 + // of data to provide. We drained its internal buffer completely on the 1.251 + // last pass. 1.252 + if (numFrames == 0 && rv == kNeedMoreData) { 1.253 + LOGD("FillComplexBuffer out of data exactly\n"); 1.254 + break; 1.255 + } 1.256 + 1.257 + int64_t time = FramesToUsecs(mCurrentAudioFrame, mAudioSampleRate).value(); 1.258 + int64_t duration = FramesToUsecs(numFrames, mAudioSampleRate).value(); 1.259 + 1.260 + LOGD("pushed audio at time %lfs; duration %lfs\n", 1.261 + (double)time / USECS_PER_S, (double)duration / USECS_PER_S); 1.262 + 1.263 + AudioData *audio = new AudioData(mDecoder->GetResource()->Tell(), 1.264 + time, duration, numFrames, 1.265 + reinterpret_cast<AudioDataValue *>(decoded.forget()), 1.266 + mAudioChannels); 1.267 + mAudioQueue.Push(audio); 1.268 + 1.269 + mCurrentAudioFrame += numFrames; 1.270 + 1.271 + if (rv == kNeedMoreData) { 1.272 + // No error; we just need more data. 1.273 + LOGD("FillComplexBuffer out of data\n"); 1.274 + break; 1.275 + } 1.276 + } while (true); 1.277 +} 1.278 + 1.279 +bool 1.280 +AppleMP3Reader::DecodeAudioData() 1.281 +{ 1.282 + MOZ_ASSERT(mDecoder->OnDecodeThread(), "Should be on decode thread"); 1.283 + 1.284 + // Read AUDIO_READ_BYTES if we can 1.285 + char bytes[AUDIO_READ_BYTES]; 1.286 + uint32_t numBytes = AUDIO_READ_BYTES; 1.287 + 1.288 + nsresult readrv = Read(&numBytes, bytes); 1.289 + 1.290 + // This function calls |AudioSampleCallback| above, synchronously, when it 1.291 + // finds compressed MP3 frame. 1.292 + OSStatus rv = AudioFileStreamParseBytes(mAudioFileStream, 1.293 + numBytes, 1.294 + bytes, 1.295 + 0 /* flags */); 1.296 + 1.297 + if (NS_FAILED(readrv)) { 1.298 + mAudioQueue.Finish(); 1.299 + return false; 1.300 + } 1.301 + 1.302 + // DataUnavailable just means there wasn't enough data to demux anything. 1.303 + // We should have more to push into the demuxer next time we're called. 1.304 + if (rv && rv != kAudioFileStreamError_DataUnavailable) { 1.305 + LOGE("AudioFileStreamParseBytes returned unknown error %x", rv); 1.306 + return false; 1.307 + } 1.308 + 1.309 + return true; 1.310 +} 1.311 + 1.312 +bool 1.313 +AppleMP3Reader::DecodeVideoFrame(bool &aKeyframeSkip, 1.314 + int64_t aTimeThreshold) 1.315 +{ 1.316 + MOZ_ASSERT(mDecoder->OnDecodeThread(), "Should be on decode thread"); 1.317 + return false; 1.318 +} 1.319 + 1.320 + 1.321 +bool 1.322 +AppleMP3Reader::HasAudio() 1.323 +{ 1.324 + MOZ_ASSERT(mDecoder->OnDecodeThread(), "Should be on decode thread"); 1.325 + return mStreamReady; 1.326 +} 1.327 + 1.328 +bool 1.329 +AppleMP3Reader::HasVideo() 1.330 +{ 1.331 + MOZ_ASSERT(mDecoder->OnDecodeThread(), "Should be on decode thread"); 1.332 + return false; 1.333 +} 1.334 + 1.335 + 1.336 +/* 1.337 + * Query the MP3 parser for a piece of metadata. 1.338 + */ 1.339 +static nsresult 1.340 +GetProperty(AudioFileStreamID aAudioFileStream, 1.341 + AudioFileStreamPropertyID aPropertyID, void *aData) 1.342 +{ 1.343 + UInt32 size; 1.344 + Boolean writeable; 1.345 + OSStatus rv = AudioFileStreamGetPropertyInfo(aAudioFileStream, aPropertyID, 1.346 + &size, &writeable); 1.347 + 1.348 + if (rv) { 1.349 + LOGW("Couldn't get property " PROPERTY_ID_FORMAT "\n", 1.350 + PROPERTY_ID_PRINT(aPropertyID)); 1.351 + return NS_ERROR_FAILURE; 1.352 + } 1.353 + 1.354 + rv = AudioFileStreamGetProperty(aAudioFileStream, aPropertyID, 1.355 + &size, aData); 1.356 + 1.357 + return NS_OK; 1.358 +} 1.359 + 1.360 + 1.361 +nsresult 1.362 +AppleMP3Reader::ReadMetadata(MediaInfo* aInfo, 1.363 + MetadataTags** aTags) 1.364 +{ 1.365 + MOZ_ASSERT(mDecoder->OnDecodeThread(), "Should be on decode thread"); 1.366 + 1.367 + *aTags = nullptr; 1.368 + 1.369 + /* 1.370 + * Feed bytes into the parser until we have all the metadata we need to 1.371 + * set up the decoder. When the parser has enough data, it will 1.372 + * synchronously call back to |AudioMetadataCallback| below. 1.373 + */ 1.374 + OSStatus rv; 1.375 + nsresult readrv; 1.376 + uint32_t offset = 0; 1.377 + do { 1.378 + char bytes[AUDIO_READ_BYTES]; 1.379 + uint32_t numBytes = AUDIO_READ_BYTES; 1.380 + readrv = Read(&numBytes, bytes); 1.381 + 1.382 + rv = AudioFileStreamParseBytes(mAudioFileStream, 1.383 + numBytes, 1.384 + bytes, 1.385 + 0 /* flags */); 1.386 + 1.387 + mMP3FrameParser.Parse(bytes, numBytes, offset); 1.388 + 1.389 + offset += numBytes; 1.390 + 1.391 + // We have to do our decoder setup from the callback. When it's done it will 1.392 + // set mStreamReady. 1.393 + } while (!mStreamReady && !rv && NS_SUCCEEDED(readrv)); 1.394 + 1.395 + if (rv) { 1.396 + LOGE("Error decoding audio stream metadata\n"); 1.397 + return NS_ERROR_FAILURE; 1.398 + } 1.399 + 1.400 + if (!mAudioConverter) { 1.401 + LOGE("Failed to setup the AudioToolbox audio decoder\n"); 1.402 + return NS_ERROR_FAILURE; 1.403 + } 1.404 + 1.405 + if (!mMP3FrameParser.IsMP3()) { 1.406 + LOGE("Frame parser failed to parse MP3 stream\n"); 1.407 + return NS_ERROR_FAILURE; 1.408 + } 1.409 + 1.410 + aInfo->mAudio.mRate = mAudioSampleRate; 1.411 + aInfo->mAudio.mChannels = mAudioChannels; 1.412 + aInfo->mAudio.mHasAudio = mStreamReady; 1.413 + 1.414 + { 1.415 + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); 1.416 + mDuration = mMP3FrameParser.GetDuration(); 1.417 + mDecoder->SetMediaDuration(mDuration); 1.418 + } 1.419 + 1.420 + return NS_OK; 1.421 +} 1.422 + 1.423 + 1.424 +void 1.425 +AppleMP3Reader::AudioMetadataCallback(AudioFileStreamID aFileStream, 1.426 + AudioFileStreamPropertyID aPropertyID, 1.427 + UInt32 *aFlags) 1.428 +{ 1.429 + if (aPropertyID == kAudioFileStreamProperty_ReadyToProducePackets) { 1.430 + /* 1.431 + * The parser is ready to send us packets of MP3 audio. 1.432 + * 1.433 + * We need to set the decoder up here, because if 1.434 + * |AudioFileStreamParseBytes| has enough audio data, then it will call 1.435 + * |AudioSampleCallback| before we get back to |ReadMetadata|. 1.436 + */ 1.437 + SetupDecoder(); 1.438 + mStreamReady = true; 1.439 + } 1.440 +} 1.441 + 1.442 + 1.443 +void 1.444 +AppleMP3Reader::SetupDecoder() 1.445 +{ 1.446 + // Get input format description from demuxer 1.447 + AudioStreamBasicDescription inputFormat, outputFormat; 1.448 + GetProperty(mAudioFileStream, kAudioFileStreamProperty_DataFormat, &inputFormat); 1.449 + 1.450 + memset(&outputFormat, 0, sizeof(outputFormat)); 1.451 + 1.452 + // Set output format 1.453 +#if defined(MOZ_SAMPLE_TYPE_FLOAT32) 1.454 + outputFormat.mBitsPerChannel = 32; 1.455 + outputFormat.mFormatFlags = 1.456 + kLinearPCMFormatFlagIsFloat | 1.457 + 0; 1.458 +#else 1.459 +#error Unknown audio sample type 1.460 +#endif 1.461 + 1.462 + mAudioSampleRate = outputFormat.mSampleRate = inputFormat.mSampleRate; 1.463 + mAudioChannels 1.464 + = outputFormat.mChannelsPerFrame = inputFormat.mChannelsPerFrame; 1.465 + mAudioFramesPerCompressedPacket = inputFormat.mFramesPerPacket; 1.466 + 1.467 + outputFormat.mFormatID = kAudioFormatLinearPCM; 1.468 + 1.469 + // Set up the decoder so it gives us one sample per frame; this way, it will 1.470 + // pass us all the samples it has in one go. Also makes it much easier to 1.471 + // deinterlace. 1.472 + outputFormat.mFramesPerPacket = 1; 1.473 + outputFormat.mBytesPerPacket = outputFormat.mBytesPerFrame 1.474 + = outputFormat.mChannelsPerFrame * outputFormat.mBitsPerChannel / 8; 1.475 + 1.476 + OSStatus rv = AudioConverterNew(&inputFormat, 1.477 + &outputFormat, 1.478 + &mAudioConverter); 1.479 + 1.480 + if (rv) { 1.481 + LOGE("Error constructing audio format converter: %x\n", rv); 1.482 + mAudioConverter = nullptr; 1.483 + return; 1.484 + } 1.485 +} 1.486 + 1.487 + 1.488 +nsresult 1.489 +AppleMP3Reader::Seek(int64_t aTime, 1.490 + int64_t aStartTime, 1.491 + int64_t aEndTime, 1.492 + int64_t aCurrentTime) 1.493 +{ 1.494 + MOZ_ASSERT(mDecoder->OnDecodeThread(), "Should be on decode thread"); 1.495 + NS_ASSERTION(aStartTime < aEndTime, 1.496 + "Seeking should happen over a positive range"); 1.497 + 1.498 + // Find the exact frame/packet that contains |aTime|. 1.499 + mCurrentAudioFrame = aTime * mAudioSampleRate / USECS_PER_S; 1.500 + SInt64 packet = mCurrentAudioFrame / mAudioFramesPerCompressedPacket; 1.501 + 1.502 + // |AudioFileStreamSeek| will pass back through |byteOffset| the byte offset 1.503 + // into the stream it expects next time it reads. 1.504 + SInt64 byteOffset; 1.505 + UInt32 flags = 0; 1.506 + 1.507 + OSStatus rv = AudioFileStreamSeek(mAudioFileStream, 1.508 + packet, 1.509 + &byteOffset, 1.510 + &flags); 1.511 + 1.512 + if (rv) { 1.513 + LOGE("Couldn't seek demuxer. Error code %x\n", rv); 1.514 + return NS_ERROR_FAILURE; 1.515 + } 1.516 + 1.517 + LOGD("computed byte offset = %lld; estimated = %s\n", 1.518 + byteOffset, 1.519 + (flags & kAudioFileStreamSeekFlag_OffsetIsEstimated) ? "YES" : "NO"); 1.520 + 1.521 + mDecoder->GetResource()->Seek(nsISeekableStream::NS_SEEK_SET, byteOffset); 1.522 + 1.523 + ResetDecode(); 1.524 + 1.525 + return NS_OK; 1.526 +} 1.527 + 1.528 +void 1.529 +AppleMP3Reader::NotifyDataArrived(const char* aBuffer, 1.530 + uint32_t aLength, 1.531 + int64_t aOffset) 1.532 +{ 1.533 + MOZ_ASSERT(NS_IsMainThread()); 1.534 + if (!mMP3FrameParser.NeedsData()) { 1.535 + return; 1.536 + } 1.537 + 1.538 + mMP3FrameParser.Parse(aBuffer, aLength, aOffset); 1.539 + 1.540 + uint64_t duration = mMP3FrameParser.GetDuration(); 1.541 + if (duration != mDuration) { 1.542 + LOGD("Updating media duration to %lluus\n", duration); 1.543 + mDuration = duration; 1.544 + ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); 1.545 + mDecoder->UpdateEstimatedMediaDuration(duration); 1.546 + } 1.547 +} 1.548 + 1.549 +} // namespace mozilla