2015-11-27 07:40:30 +03:00
|
|
|
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
|
|
|
|
#include "ADTSDemuxer.h"
|
|
|
|
|
|
|
|
#include <inttypes.h>
|
|
|
|
|
2016-06-07 23:10:18 +03:00
|
|
|
#include "nsAutoPtr.h"
|
2015-11-27 07:40:30 +03:00
|
|
|
#include "VideoUtils.h"
|
|
|
|
#include "TimeUnits.h"
|
|
|
|
#include "prenv.h"
|
2016-12-16 06:16:31 +03:00
|
|
|
#include "mozilla/SizePrintfMacros.h"
|
2015-11-27 07:40:30 +03:00
|
|
|
|
|
|
|
#ifdef PR_LOGGING
|
2016-07-24 15:30:07 +03:00
|
|
|
extern mozilla::LazyLogModule gMediaDemuxerLog;
|
2015-11-27 07:40:30 +03:00
|
|
|
#define ADTSLOG(msg, ...) \
|
2016-07-24 15:30:07 +03:00
|
|
|
MOZ_LOG(gMediaDemuxerLog, LogLevel::Debug, ("ADTSDemuxer " msg, ##__VA_ARGS__))
|
2015-11-27 07:40:30 +03:00
|
|
|
#define ADTSLOGV(msg, ...) \
|
2016-07-24 15:30:07 +03:00
|
|
|
MOZ_LOG(gMediaDemuxerLog, LogLevel::Verbose, ("ADTSDemuxer " msg, ##__VA_ARGS__))
|
2015-11-27 07:40:30 +03:00
|
|
|
#else
|
2016-02-04 13:34:53 +03:00
|
|
|
#define ADTSLOG(msg, ...) do {} while (false)
|
|
|
|
#define ADTSLOGV(msg, ...) do {} while (false)
|
2015-11-27 07:40:30 +03:00
|
|
|
#endif
|
|
|
|
|
|
|
|
namespace mozilla {
|
|
|
|
namespace adts {
|
|
|
|
|
|
|
|
// adts::FrameHeader - Holds the ADTS frame header and its parsing
|
|
|
|
// state.
|
|
|
|
//
|
|
|
|
// ADTS Frame Structure
|
|
|
|
//
|
|
|
|
// 11111111 1111BCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP(QQQQQQQQ QQQQQQQQ)
|
|
|
|
//
|
|
|
|
// Header consists of 7 or 9 bytes(without or with CRC).
|
|
|
|
// Letter Length(bits) Description
|
|
|
|
// { sync } 12 syncword 0xFFF, all bits must be 1
|
|
|
|
// B 1 MPEG Version: 0 for MPEG-4, 1 for MPEG-2
|
|
|
|
// C 2 Layer: always 0
|
|
|
|
// D 1 protection absent, Warning, set to 1 if there is no
|
|
|
|
// CRC and 0 if there is CRC
|
|
|
|
// E 2 profile, the MPEG-4 Audio Object Type minus 1
|
|
|
|
// F 4 MPEG-4 Sampling Frequency Index (15 is forbidden)
|
|
|
|
// H 3 MPEG-4 Channel Configuration (in the case of 0, the
|
|
|
|
// channel configuration is sent via an in-band PCE)
|
|
|
|
// M 13 frame length, this value must include 7 or 9 bytes of
|
|
|
|
// header length: FrameLength =
|
|
|
|
// (ProtectionAbsent == 1 ? 7 : 9) + size(AACFrame)
|
|
|
|
// O 11 Buffer fullness
|
|
|
|
// P 2 Number of AAC frames(RDBs) in ADTS frame minus 1, for
|
|
|
|
// maximum compatibility always use 1 AAC frame per ADTS
|
|
|
|
// frame
|
|
|
|
// Q 16 CRC if protection absent is 0
|
2017-02-07 11:23:34 +03:00
|
|
|
class FrameHeader
|
|
|
|
{
|
2015-11-27 07:40:30 +03:00
|
|
|
public:
|
|
|
|
uint32_t mFrameLength;
|
|
|
|
uint32_t mSampleRate;
|
|
|
|
uint32_t mSamples;
|
|
|
|
uint32_t mChannels;
|
|
|
|
uint8_t mObjectType;
|
|
|
|
uint8_t mSamplingIndex;
|
|
|
|
uint8_t mChannelConfig;
|
|
|
|
uint8_t mNumAACFrames;
|
|
|
|
bool mHaveCrc;
|
|
|
|
|
|
|
|
// Returns whether aPtr matches a valid ADTS header sync marker
|
2017-02-07 11:23:34 +03:00
|
|
|
static bool MatchesSync(const uint8_t* aPtr)
|
|
|
|
{
|
2015-11-27 07:40:30 +03:00
|
|
|
return aPtr[0] == 0xFF && (aPtr[1] & 0xF6) == 0xF0;
|
|
|
|
}
|
|
|
|
|
|
|
|
FrameHeader() { Reset(); }
|
|
|
|
|
|
|
|
// Header size
|
|
|
|
size_t HeaderSize() const { return (mHaveCrc) ? 9 : 7; }
|
|
|
|
|
|
|
|
bool IsValid() const { return mFrameLength > 0; }
|
|
|
|
|
|
|
|
// Resets the state to allow for a new parsing session.
|
|
|
|
void Reset() { PodZero(this); }
|
|
|
|
|
|
|
|
// Returns whether the byte creates a valid sequence up to this point.
|
2017-02-07 11:23:34 +03:00
|
|
|
bool Parse(const uint8_t* aPtr)
|
|
|
|
{
|
2015-11-27 07:40:30 +03:00
|
|
|
const uint8_t* p = aPtr;
|
|
|
|
|
|
|
|
if (!MatchesSync(p)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// AAC has 1024 samples per frame per channel.
|
|
|
|
mSamples = 1024;
|
|
|
|
|
|
|
|
mHaveCrc = !(p[1] & 0x01);
|
|
|
|
mObjectType = ((p[2] & 0xC0) >> 6) + 1;
|
|
|
|
mSamplingIndex = (p[2] & 0x3C) >> 2;
|
|
|
|
mChannelConfig = (p[2] & 0x01) << 2 | (p[3] & 0xC0) >> 6;
|
2017-02-07 11:23:34 +03:00
|
|
|
mFrameLength =
|
|
|
|
(p[3] & 0x03) << 11 | (p[4] & 0xFF) << 3 | (p[5] & 0xE0) >> 5;
|
2015-11-27 07:40:30 +03:00
|
|
|
mNumAACFrames = (p[6] & 0x03) + 1;
|
|
|
|
|
|
|
|
static const int32_t SAMPLE_RATES[16] = {
|
|
|
|
96000, 88200, 64000, 48000,
|
|
|
|
44100, 32000, 24000, 22050,
|
|
|
|
16000, 12000, 11025, 8000,
|
|
|
|
7350
|
|
|
|
};
|
|
|
|
mSampleRate = SAMPLE_RATES[mSamplingIndex];
|
|
|
|
|
|
|
|
MOZ_ASSERT(mChannelConfig < 8);
|
|
|
|
mChannels = (mChannelConfig == 7) ? 8 : mChannelConfig;
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// adts::Frame - Frame meta container used to parse and hold a frame
|
|
|
|
// header and side info.
|
2017-02-07 11:23:34 +03:00
|
|
|
class Frame
|
|
|
|
{
|
2015-11-27 07:40:30 +03:00
|
|
|
public:
|
2017-02-07 11:23:34 +03:00
|
|
|
Frame() : mOffset(0), mHeader() { }
|
2015-11-27 07:40:30 +03:00
|
|
|
|
|
|
|
int64_t Offset() const { return mOffset; }
|
2017-02-07 11:23:34 +03:00
|
|
|
size_t Length() const
|
|
|
|
{
|
|
|
|
// TODO: If fields are zero'd when invalid, this check wouldn't be
|
|
|
|
// necessary.
|
2015-11-27 07:40:30 +03:00
|
|
|
if (!mHeader.IsValid()) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return mHeader.mFrameLength;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns the offset to the start of frame's raw data.
|
2017-02-07 11:23:34 +03:00
|
|
|
int64_t PayloadOffset() const { return mOffset + mHeader.HeaderSize(); }
|
2015-11-27 07:40:30 +03:00
|
|
|
|
|
|
|
// Returns the length of the frame's raw data (excluding the header) in bytes.
|
2017-02-07 11:23:34 +03:00
|
|
|
size_t PayloadLength() const
|
|
|
|
{
|
|
|
|
// TODO: If fields are zero'd when invalid, this check wouldn't be
|
|
|
|
// necessary.
|
2015-11-27 07:40:30 +03:00
|
|
|
if (!mHeader.IsValid()) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return mHeader.mFrameLength - mHeader.HeaderSize();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns the parsed frame header.
|
2017-02-07 11:23:34 +03:00
|
|
|
const FrameHeader& Header() const { return mHeader; }
|
2015-11-27 07:40:30 +03:00
|
|
|
|
2017-02-07 11:23:34 +03:00
|
|
|
bool IsValid() const { return mHeader.IsValid(); }
|
2015-11-27 07:40:30 +03:00
|
|
|
|
|
|
|
// Resets the frame header and data.
|
2017-02-07 11:23:34 +03:00
|
|
|
void Reset()
|
|
|
|
{
|
2015-11-27 07:40:30 +03:00
|
|
|
mHeader.Reset();
|
|
|
|
mOffset = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns whether the valid
|
|
|
|
bool Parse(int64_t aOffset, uint8_t* aStart, uint8_t* aEnd) {
|
|
|
|
MOZ_ASSERT(aStart && aEnd);
|
|
|
|
|
|
|
|
bool found = false;
|
|
|
|
uint8_t* ptr = aStart;
|
|
|
|
// Require at least 7 bytes of data at the end of the buffer for the minimum
|
|
|
|
// ADTS frame header.
|
|
|
|
while (ptr < aEnd - 7 && !found) {
|
|
|
|
found = mHeader.Parse(ptr);
|
|
|
|
ptr++;
|
|
|
|
}
|
|
|
|
|
|
|
|
mOffset = aOffset + (ptr - aStart) - 1;
|
|
|
|
|
|
|
|
return found;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
// The offset to the start of the header.
|
|
|
|
int64_t mOffset;
|
|
|
|
|
|
|
|
// The currently parsed frame header.
|
|
|
|
FrameHeader mHeader;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2017-02-07 11:23:34 +03:00
|
|
|
class FrameParser
|
|
|
|
{
|
2015-11-27 07:40:30 +03:00
|
|
|
public:
|
|
|
|
|
|
|
|
// Returns the currently parsed frame. Reset via Reset or EndFrameSession.
|
|
|
|
const Frame& CurrentFrame() const { return mFrame; }
|
|
|
|
|
|
|
|
|
|
|
|
// Returns the first parsed frame. Reset via Reset.
|
|
|
|
const Frame& FirstFrame() const { return mFirstFrame; }
|
|
|
|
|
|
|
|
// Resets the parser. Don't use between frames as first frame data is reset.
|
2017-02-07 11:23:34 +03:00
|
|
|
void Reset()
|
|
|
|
{
|
2015-11-27 07:40:30 +03:00
|
|
|
EndFrameSession();
|
|
|
|
mFirstFrame.Reset();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clear the last parsed frame to allow for next frame parsing, i.e.:
|
|
|
|
// - sets PrevFrame to CurrentFrame
|
|
|
|
// - resets the CurrentFrame
|
|
|
|
// - resets ID3Header if no valid header was parsed yet
|
2017-02-07 11:23:34 +03:00
|
|
|
void EndFrameSession()
|
|
|
|
{
|
2015-11-27 07:40:30 +03:00
|
|
|
mFrame.Reset();
|
|
|
|
}
|
|
|
|
|
2017-02-07 11:23:34 +03:00
|
|
|
// Parses contents of given ByteReader for a valid frame header and returns
|
|
|
|
// true if one was found. After returning, the variable passed to
|
|
|
|
// 'aBytesToSkip' holds the amount of bytes to be skipped (if any) in order to
|
|
|
|
// jump across a large ID3v2 tag spanning multiple buffers.
|
|
|
|
bool Parse(int64_t aOffset, uint8_t* aStart, uint8_t* aEnd)
|
|
|
|
{
|
2015-11-27 07:40:30 +03:00
|
|
|
const bool found = mFrame.Parse(aOffset, aStart, aEnd);
|
|
|
|
|
|
|
|
if (mFrame.Length() && !mFirstFrame.Length()) {
|
|
|
|
mFirstFrame = mFrame;
|
|
|
|
}
|
|
|
|
|
|
|
|
return found;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
// We keep the first parsed frame around for static info access, the
|
|
|
|
// previously parsed frame for debugging and the currently parsed frame.
|
|
|
|
Frame mFirstFrame;
|
|
|
|
Frame mFrame;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Return the AAC Profile Level Indication based upon sample rate and channels
|
|
|
|
// Information based upon table 1.10 from ISO/IEC 14496-3:2005(E)
|
|
|
|
static int8_t
|
|
|
|
ProfileLevelIndication(const Frame& frame)
|
|
|
|
{
|
|
|
|
const FrameHeader& header = frame.Header();
|
|
|
|
MOZ_ASSERT(header.IsValid());
|
|
|
|
|
|
|
|
if (!header.IsValid()) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
const int channels = header.mChannels;
|
|
|
|
const int sampleRate = header.mSampleRate;
|
|
|
|
|
|
|
|
if (channels <= 2) {
|
|
|
|
if (sampleRate <= 24000) {
|
|
|
|
// AAC Profile L1
|
|
|
|
return 0x28;
|
|
|
|
}
|
|
|
|
else if (sampleRate <= 48000) {
|
|
|
|
// AAC Profile L2
|
|
|
|
return 0x29;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (channels <= 5) {
|
|
|
|
if (sampleRate <= 48000) {
|
|
|
|
// AAC Profile L4
|
|
|
|
return 0x2A;
|
|
|
|
}
|
|
|
|
else if (sampleRate <= 96000) {
|
|
|
|
// AAC Profile L5
|
|
|
|
return 0x2B;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Should this be 0xFE for 'no audio profile specified'?
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Initialize the AAC AudioSpecificConfig.
|
|
|
|
// Only handles two-byte version for AAC-LC.
|
|
|
|
static void
|
|
|
|
InitAudioSpecificConfig(const Frame& frame,
|
|
|
|
MediaByteBuffer* aBuffer)
|
|
|
|
{
|
|
|
|
const FrameHeader& header = frame.Header();
|
|
|
|
MOZ_ASSERT(header.IsValid());
|
|
|
|
|
|
|
|
int audioObjectType = header.mObjectType;
|
|
|
|
int samplingFrequencyIndex = header.mSamplingIndex;
|
|
|
|
int channelConfig = header.mChannelConfig;
|
|
|
|
|
|
|
|
uint8_t asc[2];
|
|
|
|
asc[0] = (audioObjectType & 0x1F) << 3 | (samplingFrequencyIndex & 0x0E) >> 1;
|
|
|
|
asc[1] = (samplingFrequencyIndex & 0x01) << 7 | (channelConfig & 0x0F) << 3;
|
|
|
|
|
|
|
|
aBuffer->AppendElements(asc, 2);
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace adts
|
|
|
|
|
|
|
|
// ADTSDemuxer
|
|
|
|
|
|
|
|
ADTSDemuxer::ADTSDemuxer(MediaResource* aSource)
|
|
|
|
: mSource(aSource)
|
2017-02-07 11:23:34 +03:00
|
|
|
{
|
|
|
|
}
|
2015-11-27 07:40:30 +03:00
|
|
|
|
|
|
|
bool
|
|
|
|
ADTSDemuxer::InitInternal()
|
|
|
|
{
|
|
|
|
if (!mTrackDemuxer) {
|
|
|
|
mTrackDemuxer = new ADTSTrackDemuxer(mSource);
|
|
|
|
}
|
|
|
|
return mTrackDemuxer->Init();
|
|
|
|
}
|
|
|
|
|
|
|
|
RefPtr<ADTSDemuxer::InitPromise>
|
|
|
|
ADTSDemuxer::Init()
|
|
|
|
{
|
|
|
|
if (!InitInternal()) {
|
|
|
|
ADTSLOG("Init() failure: waiting for data");
|
|
|
|
|
|
|
|
return InitPromise::CreateAndReject(
|
2016-09-12 05:22:20 +03:00
|
|
|
NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
|
2015-11-27 07:40:30 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
ADTSLOG("Init() successful");
|
|
|
|
return InitPromise::CreateAndResolve(NS_OK, __func__);
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
ADTSDemuxer::HasTrackType(TrackInfo::TrackType aType) const
|
|
|
|
{
|
|
|
|
return aType == TrackInfo::kAudioTrack;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint32_t
|
|
|
|
ADTSDemuxer::GetNumberTracks(TrackInfo::TrackType aType) const
|
|
|
|
{
|
|
|
|
return (aType == TrackInfo::kAudioTrack) ? 1 : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
already_AddRefed<MediaTrackDemuxer>
|
|
|
|
ADTSDemuxer::GetTrackDemuxer(TrackInfo::TrackType aType, uint32_t aTrackNumber)
|
|
|
|
{
|
|
|
|
if (!mTrackDemuxer) {
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
return RefPtr<ADTSTrackDemuxer>(mTrackDemuxer).forget();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
ADTSDemuxer::IsSeekable() const
|
|
|
|
{
|
|
|
|
int64_t length = mSource->GetLength();
|
|
|
|
if (length > -1)
|
|
|
|
return true;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ADTSTrackDemuxer
|
|
|
|
ADTSTrackDemuxer::ADTSTrackDemuxer(MediaResource* aSource)
|
|
|
|
: mSource(aSource)
|
|
|
|
, mParser(new adts::FrameParser())
|
|
|
|
, mOffset(0)
|
|
|
|
, mNumParsedFrames(0)
|
|
|
|
, mFrameIndex(0)
|
|
|
|
, mTotalFrameLen(0)
|
|
|
|
, mSamplesPerFrame(0)
|
|
|
|
, mSamplesPerSecond(0)
|
|
|
|
, mChannels(0)
|
|
|
|
{
|
|
|
|
Reset();
|
|
|
|
}
|
|
|
|
|
|
|
|
ADTSTrackDemuxer::~ADTSTrackDemuxer()
|
|
|
|
{
|
|
|
|
delete mParser;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
ADTSTrackDemuxer::Init()
|
|
|
|
{
|
|
|
|
FastSeek(media::TimeUnit());
|
|
|
|
// Read the first frame to fetch sample rate and other meta data.
|
|
|
|
RefPtr<MediaRawData> frame(GetNextFrame(FindNextFrame(true)));
|
|
|
|
|
|
|
|
ADTSLOG("Init StreamLength()=%" PRId64 " first-frame-found=%d",
|
|
|
|
StreamLength(), !!frame);
|
|
|
|
|
|
|
|
if (!frame) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Rewind back to the stream begin to avoid dropping the first frame.
|
|
|
|
FastSeek(media::TimeUnit());
|
|
|
|
|
|
|
|
if (!mInfo) {
|
|
|
|
mInfo = MakeUnique<AudioInfo>();
|
|
|
|
}
|
|
|
|
|
|
|
|
mInfo->mRate = mSamplesPerSecond;
|
|
|
|
mInfo->mChannels = mChannels;
|
|
|
|
mInfo->mBitDepth = 16;
|
|
|
|
mInfo->mDuration = Duration().ToMicroseconds();
|
|
|
|
|
|
|
|
// AAC Specific information
|
|
|
|
mInfo->mMimeType = "audio/mp4a-latm";
|
|
|
|
|
|
|
|
// Configure AAC codec-specific values.
|
|
|
|
|
|
|
|
// According to
|
|
|
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd742784%28v=vs.85%29.aspx,
|
|
|
|
// wAudioProfileLevelIndication, which is passed mInfo->mProfile, is
|
|
|
|
// a value from Table 1.12 -- audioProfileLevelIndication values, ISO/IEC 14496-3.
|
|
|
|
mInfo->mProfile = ProfileLevelIndication(mParser->FirstFrame());
|
|
|
|
// For AAC, mExtendedProfile contains the audioObjectType from Table
|
|
|
|
// 1.3 -- Audio Profile definition, ISO/IEC 14496-3. Eg. 2 == AAC LC
|
|
|
|
mInfo->mExtendedProfile = mParser->FirstFrame().Header().mObjectType;
|
|
|
|
InitAudioSpecificConfig(mParser->FirstFrame(), mInfo->mCodecSpecificConfig);
|
|
|
|
|
2017-02-07 11:23:34 +03:00
|
|
|
ADTSLOG("Init mInfo={mRate=%u mChannels=%u mBitDepth=%u mDuration=%" PRId64
|
|
|
|
"}",
|
2015-11-27 07:40:30 +03:00
|
|
|
mInfo->mRate, mInfo->mChannels, mInfo->mBitDepth, mInfo->mDuration);
|
|
|
|
|
|
|
|
return mSamplesPerSecond && mChannels;
|
|
|
|
}
|
|
|
|
|
|
|
|
UniquePtr<TrackInfo>
|
|
|
|
ADTSTrackDemuxer::GetInfo() const
|
|
|
|
{
|
|
|
|
return mInfo->Clone();
|
|
|
|
}
|
|
|
|
|
|
|
|
RefPtr<ADTSTrackDemuxer::SeekPromise>
|
2016-11-13 07:13:51 +03:00
|
|
|
ADTSTrackDemuxer::Seek(const media::TimeUnit& aTime)
|
2015-11-27 07:40:30 +03:00
|
|
|
{
|
|
|
|
// Efficiently seek to the position.
|
|
|
|
FastSeek(aTime);
|
|
|
|
// Correct seek position by scanning the next frames.
|
|
|
|
const media::TimeUnit seekTime = ScanUntil(aTime);
|
|
|
|
|
|
|
|
return SeekPromise::CreateAndResolve(seekTime, __func__);
|
|
|
|
}
|
|
|
|
|
|
|
|
media::TimeUnit
|
|
|
|
ADTSTrackDemuxer::FastSeek(const media::TimeUnit& aTime)
|
|
|
|
{
|
|
|
|
ADTSLOG("FastSeek(%" PRId64 ") avgFrameLen=%f mNumParsedFrames=%" PRIu64
|
|
|
|
" mFrameIndex=%" PRId64 " mOffset=%" PRIu64,
|
2015-12-23 05:44:31 +03:00
|
|
|
aTime.ToMicroseconds(), AverageFrameLength(), mNumParsedFrames,
|
|
|
|
mFrameIndex, mOffset);
|
2015-11-27 07:40:30 +03:00
|
|
|
|
|
|
|
const int64_t firstFrameOffset = mParser->FirstFrame().Offset();
|
|
|
|
if (!aTime.ToMicroseconds()) {
|
|
|
|
// Quick seek to the beginning of the stream.
|
|
|
|
mOffset = firstFrameOffset;
|
|
|
|
} else if (AverageFrameLength() > 0) {
|
|
|
|
mOffset = firstFrameOffset + FrameIndexFromTime(aTime) *
|
|
|
|
AverageFrameLength();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mOffset > firstFrameOffset && StreamLength() > 0) {
|
|
|
|
mOffset = std::min(StreamLength() - 1, mOffset);
|
|
|
|
}
|
|
|
|
|
|
|
|
mFrameIndex = FrameIndexFromOffset(mOffset);
|
|
|
|
mParser->EndFrameSession();
|
|
|
|
|
|
|
|
ADTSLOG("FastSeek End avgFrameLen=%f mNumParsedFrames=%" PRIu64
|
2016-12-16 06:16:31 +03:00
|
|
|
" mFrameIndex=%" PRId64 " mFirstFrameOffset=%" PRIu64 " mOffset=%" PRIu64
|
|
|
|
" SL=%" PRIu64 "",
|
2015-11-27 07:40:30 +03:00
|
|
|
AverageFrameLength(), mNumParsedFrames, mFrameIndex,
|
|
|
|
firstFrameOffset, mOffset, StreamLength());
|
|
|
|
|
|
|
|
return Duration(mFrameIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
media::TimeUnit
|
|
|
|
ADTSTrackDemuxer::ScanUntil(const media::TimeUnit& aTime)
|
|
|
|
{
|
|
|
|
ADTSLOG("ScanUntil(%" PRId64 ") avgFrameLen=%f mNumParsedFrames=%" PRIu64
|
|
|
|
" mFrameIndex=%" PRId64 " mOffset=%" PRIu64,
|
2015-12-23 05:44:31 +03:00
|
|
|
aTime.ToMicroseconds(), AverageFrameLength(), mNumParsedFrames,
|
|
|
|
mFrameIndex, mOffset);
|
2015-11-27 07:40:30 +03:00
|
|
|
|
|
|
|
if (!aTime.ToMicroseconds()) {
|
|
|
|
return FastSeek(aTime);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Duration(mFrameIndex) > aTime) {
|
|
|
|
FastSeek(aTime);
|
|
|
|
}
|
|
|
|
|
|
|
|
while (SkipNextFrame(FindNextFrame()) && Duration(mFrameIndex + 1) < aTime) {
|
|
|
|
ADTSLOGV("ScanUntil* avgFrameLen=%f mNumParsedFrames=%" PRIu64
|
|
|
|
" mFrameIndex=%" PRId64 " mOffset=%" PRIu64 " Duration=%" PRId64,
|
2015-12-23 05:44:31 +03:00
|
|
|
AverageFrameLength(), mNumParsedFrames, mFrameIndex,
|
|
|
|
mOffset, Duration(mFrameIndex + 1).ToMicroseconds());
|
2015-11-27 07:40:30 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
ADTSLOG("ScanUntil End avgFrameLen=%f mNumParsedFrames=%" PRIu64
|
|
|
|
" mFrameIndex=%" PRId64 " mOffset=%" PRIu64,
|
2015-12-23 05:44:31 +03:00
|
|
|
AverageFrameLength(), mNumParsedFrames, mFrameIndex, mOffset);
|
2015-11-27 07:40:30 +03:00
|
|
|
|
|
|
|
return Duration(mFrameIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
RefPtr<ADTSTrackDemuxer::SamplesPromise>
|
|
|
|
ADTSTrackDemuxer::GetSamples(int32_t aNumSamples)
|
|
|
|
{
|
|
|
|
ADTSLOGV("GetSamples(%d) Begin mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
|
2017-02-07 11:23:34 +03:00
|
|
|
" mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
|
|
|
|
" mSamplesPerFrame=%d "
|
|
|
|
"mSamplesPerSecond=%d mChannels=%d",
|
|
|
|
aNumSamples, mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
|
|
|
|
mSamplesPerFrame, mSamplesPerSecond, mChannels);
|
2015-11-27 07:40:30 +03:00
|
|
|
|
2016-09-12 05:22:20 +03:00
|
|
|
MOZ_ASSERT(aNumSamples);
|
2015-11-27 07:40:30 +03:00
|
|
|
|
|
|
|
RefPtr<SamplesHolder> frames = new SamplesHolder();
|
|
|
|
|
|
|
|
while (aNumSamples--) {
|
|
|
|
RefPtr<MediaRawData> frame(GetNextFrame(FindNextFrame()));
|
|
|
|
if (!frame)
|
|
|
|
break;
|
|
|
|
|
|
|
|
frames->mSamples.AppendElement(frame);
|
|
|
|
}
|
|
|
|
|
2016-12-16 06:16:31 +03:00
|
|
|
ADTSLOGV("GetSamples() End mSamples.Size()=%" PRIuSIZE " aNumSamples=%d mOffset=%" PRIu64
|
2017-02-07 11:23:34 +03:00
|
|
|
" mNumParsedFrames=%" PRIu64 " mFrameIndex=%" PRId64
|
|
|
|
" mTotalFrameLen=%" PRIu64
|
|
|
|
" mSamplesPerFrame=%d mSamplesPerSecond=%d "
|
|
|
|
"mChannels=%d",
|
|
|
|
frames->mSamples.Length(), aNumSamples, mOffset, mNumParsedFrames,
|
|
|
|
mFrameIndex, mTotalFrameLen, mSamplesPerFrame, mSamplesPerSecond,
|
|
|
|
mChannels);
|
2015-11-27 07:40:30 +03:00
|
|
|
|
|
|
|
if (frames->mSamples.IsEmpty()) {
|
|
|
|
return SamplesPromise::CreateAndReject(
|
2016-09-12 05:22:20 +03:00
|
|
|
NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
|
2015-11-27 07:40:30 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return SamplesPromise::CreateAndResolve(frames, __func__);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
ADTSTrackDemuxer::Reset()
|
|
|
|
{
|
|
|
|
ADTSLOG("Reset()");
|
|
|
|
MOZ_ASSERT(mParser);
|
|
|
|
if (mParser) {
|
|
|
|
mParser->Reset();
|
|
|
|
}
|
|
|
|
FastSeek(media::TimeUnit());
|
|
|
|
}
|
|
|
|
|
|
|
|
RefPtr<ADTSTrackDemuxer::SkipAccessPointPromise>
|
2017-02-07 11:23:34 +03:00
|
|
|
ADTSTrackDemuxer::SkipToNextRandomAccessPoint(
|
|
|
|
const media::TimeUnit& aTimeThreshold)
|
2015-11-27 07:40:30 +03:00
|
|
|
{
|
|
|
|
// Will not be called for audio-only resources.
|
|
|
|
return SkipAccessPointPromise::CreateAndReject(
|
2016-09-12 05:22:20 +03:00
|
|
|
SkipFailureHolder(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, 0), __func__);
|
2015-11-27 07:40:30 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
int64_t
|
|
|
|
ADTSTrackDemuxer::GetResourceOffset() const
|
|
|
|
{
|
|
|
|
return mOffset;
|
|
|
|
}
|
|
|
|
|
|
|
|
media::TimeIntervals
|
|
|
|
ADTSTrackDemuxer::GetBuffered()
|
|
|
|
{
|
|
|
|
media::TimeUnit duration = Duration();
|
|
|
|
|
|
|
|
if (duration <= media::TimeUnit()) {
|
|
|
|
return media::TimeIntervals();
|
|
|
|
}
|
|
|
|
|
|
|
|
AutoPinned<MediaResource> stream(mSource.GetResource());
|
|
|
|
return GetEstimatedBufferedTimeRanges(stream, duration.ToMicroseconds());
|
|
|
|
}
|
|
|
|
|
|
|
|
int64_t
|
|
|
|
ADTSTrackDemuxer::StreamLength() const
|
|
|
|
{
|
|
|
|
return mSource.GetLength();
|
|
|
|
}
|
|
|
|
|
|
|
|
media::TimeUnit
|
|
|
|
ADTSTrackDemuxer::Duration() const
|
|
|
|
{
|
|
|
|
if (!mNumParsedFrames) {
|
|
|
|
return media::TimeUnit::FromMicroseconds(-1);
|
|
|
|
}
|
|
|
|
|
|
|
|
const int64_t streamLen = StreamLength();
|
|
|
|
if (streamLen < 0) {
|
|
|
|
// Unknown length, we can't estimate duration.
|
|
|
|
return media::TimeUnit::FromMicroseconds(-1);
|
|
|
|
}
|
|
|
|
const int64_t firstFrameOffset = mParser->FirstFrame().Offset();
|
|
|
|
int64_t numFrames = (streamLen - firstFrameOffset) / AverageFrameLength();
|
|
|
|
return Duration(numFrames);
|
|
|
|
}
|
|
|
|
|
|
|
|
media::TimeUnit
|
|
|
|
ADTSTrackDemuxer::Duration(int64_t aNumFrames) const
|
|
|
|
{
|
|
|
|
if (!mSamplesPerSecond) {
|
|
|
|
return media::TimeUnit::FromMicroseconds(-1);
|
|
|
|
}
|
|
|
|
|
2016-09-09 15:38:04 +03:00
|
|
|
return FramesToTimeUnit(aNumFrames * mSamplesPerFrame, mSamplesPerSecond);
|
2015-11-27 07:40:30 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
const adts::Frame&
|
|
|
|
ADTSTrackDemuxer::FindNextFrame(bool findFirstFrame /*= false*/)
|
|
|
|
{
|
|
|
|
static const int BUFFER_SIZE = 4096;
|
|
|
|
static const int MAX_SKIPPED_BYTES = 10 * BUFFER_SIZE;
|
|
|
|
|
|
|
|
ADTSLOGV("FindNext() Begin mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
|
|
|
|
" mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
|
|
|
|
" mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d",
|
|
|
|
mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
|
|
|
|
mSamplesPerFrame, mSamplesPerSecond, mChannels);
|
|
|
|
|
|
|
|
uint8_t buffer[BUFFER_SIZE];
|
|
|
|
int32_t read = 0;
|
|
|
|
|
|
|
|
bool foundFrame = false;
|
|
|
|
int64_t frameHeaderOffset = mOffset;
|
|
|
|
|
|
|
|
// Prepare the parser for the next frame parsing session.
|
|
|
|
mParser->EndFrameSession();
|
|
|
|
|
|
|
|
// Check whether we've found a valid ADTS frame.
|
|
|
|
while (!foundFrame) {
|
|
|
|
if ((read = Read(buffer, frameHeaderOffset, BUFFER_SIZE)) == 0) {
|
|
|
|
ADTSLOG("FindNext() EOS without a frame");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (frameHeaderOffset - mOffset > MAX_SKIPPED_BYTES) {
|
|
|
|
ADTSLOG("FindNext() exceeded MAX_SKIPPED_BYTES without a frame");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
const adts::Frame& currentFrame = mParser->CurrentFrame();
|
|
|
|
foundFrame = mParser->Parse(frameHeaderOffset, buffer, buffer + read);
|
|
|
|
if (findFirstFrame && foundFrame) {
|
|
|
|
// Check for sync marker after the found frame, since it's
|
|
|
|
// possible to find sync marker in AAC data. If sync marker
|
|
|
|
// exists after the current frame then we've found a frame
|
|
|
|
// header.
|
2017-02-07 11:23:34 +03:00
|
|
|
int64_t nextFrameHeaderOffset =
|
|
|
|
currentFrame.Offset() + currentFrame.Length();
|
2015-11-27 07:40:30 +03:00
|
|
|
int32_t read = Read(buffer, nextFrameHeaderOffset, 2);
|
|
|
|
if (read != 2 || !adts::FrameHeader::MatchesSync(buffer)) {
|
|
|
|
frameHeaderOffset = currentFrame.Offset() + 1;
|
|
|
|
mParser->Reset();
|
|
|
|
foundFrame = false;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (foundFrame) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Minimum header size is 7 bytes.
|
|
|
|
int64_t advance = read - 7;
|
|
|
|
|
|
|
|
// Check for offset overflow.
|
|
|
|
if (frameHeaderOffset + advance <= frameHeaderOffset) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
frameHeaderOffset += advance;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!foundFrame || !mParser->CurrentFrame().Length()) {
|
2016-12-16 06:16:31 +03:00
|
|
|
ADTSLOG("FindNext() Exit foundFrame=%d mParser->CurrentFrame().Length()=%" PRIuSIZE " ",
|
2015-11-27 07:40:30 +03:00
|
|
|
foundFrame, mParser->CurrentFrame().Length());
|
|
|
|
mParser->Reset();
|
|
|
|
return mParser->CurrentFrame();
|
|
|
|
}
|
|
|
|
|
|
|
|
ADTSLOGV("FindNext() End mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
|
2016-12-16 06:16:31 +03:00
|
|
|
" mFrameIndex=%" PRId64 " frameHeaderOffset=%" PRId64
|
2015-11-27 07:40:30 +03:00
|
|
|
" mTotalFrameLen=%" PRIu64 " mSamplesPerFrame=%d mSamplesPerSecond=%d"
|
|
|
|
" mChannels=%d",
|
|
|
|
mOffset, mNumParsedFrames, mFrameIndex, frameHeaderOffset,
|
|
|
|
mTotalFrameLen, mSamplesPerFrame, mSamplesPerSecond, mChannels);
|
|
|
|
|
|
|
|
return mParser->CurrentFrame();
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
ADTSTrackDemuxer::SkipNextFrame(const adts::Frame& aFrame)
|
|
|
|
{
|
|
|
|
if (!mNumParsedFrames || !aFrame.Length()) {
|
|
|
|
RefPtr<MediaRawData> frame(GetNextFrame(aFrame));
|
|
|
|
return frame;
|
|
|
|
}
|
|
|
|
|
|
|
|
UpdateState(aFrame);
|
|
|
|
|
|
|
|
ADTSLOGV("SkipNext() End mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
|
|
|
|
" mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
|
|
|
|
" mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d",
|
|
|
|
mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
|
|
|
|
mSamplesPerFrame, mSamplesPerSecond, mChannels);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
already_AddRefed<MediaRawData>
|
|
|
|
ADTSTrackDemuxer::GetNextFrame(const adts::Frame& aFrame)
|
|
|
|
{
|
2016-12-16 06:16:31 +03:00
|
|
|
ADTSLOG("GetNext() Begin({mOffset=%" PRId64 " HeaderSize()=%" PRIuSIZE
|
|
|
|
" Length()=%" PRIuSIZE "})",
|
2015-11-27 07:40:30 +03:00
|
|
|
aFrame.Offset(), aFrame.Header().HeaderSize(), aFrame.PayloadLength());
|
|
|
|
if (!aFrame.IsValid())
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
const int64_t offset = aFrame.PayloadOffset();
|
|
|
|
const uint32_t length = aFrame.PayloadLength();
|
|
|
|
|
|
|
|
RefPtr<MediaRawData> frame = new MediaRawData();
|
|
|
|
frame->mOffset = offset;
|
|
|
|
|
|
|
|
nsAutoPtr<MediaRawDataWriter> frameWriter(frame->CreateWriter());
|
|
|
|
if (!frameWriter->SetSize(length)) {
|
|
|
|
ADTSLOG("GetNext() Exit failed to allocated media buffer");
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
const uint32_t read = Read(frameWriter->Data(), offset, length);
|
|
|
|
if (read != length) {
|
2016-12-16 06:16:31 +03:00
|
|
|
ADTSLOG("GetNext() Exit read=%u frame->Size()=%" PRIuSIZE, read, frame->Size());
|
2015-11-27 07:40:30 +03:00
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
UpdateState(aFrame);
|
|
|
|
|
|
|
|
frame->mTime = Duration(mFrameIndex - 1).ToMicroseconds();
|
|
|
|
frame->mDuration = Duration(1).ToMicroseconds();
|
|
|
|
frame->mTimecode = frame->mTime;
|
|
|
|
frame->mKeyframe = true;
|
|
|
|
|
|
|
|
MOZ_ASSERT(frame->mTime >= 0);
|
|
|
|
MOZ_ASSERT(frame->mDuration > 0);
|
|
|
|
|
|
|
|
ADTSLOGV("GetNext() End mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
|
2017-02-07 11:23:34 +03:00
|
|
|
" mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
|
|
|
|
" mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d",
|
|
|
|
mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
|
|
|
|
mSamplesPerFrame, mSamplesPerSecond, mChannels);
|
2015-11-27 07:40:30 +03:00
|
|
|
|
|
|
|
return frame.forget();
|
|
|
|
}
|
|
|
|
|
|
|
|
int64_t
|
|
|
|
ADTSTrackDemuxer::FrameIndexFromOffset(int64_t aOffset) const
|
|
|
|
{
|
|
|
|
int64_t frameIndex = 0;
|
|
|
|
|
|
|
|
if (AverageFrameLength() > 0) {
|
2017-02-07 11:23:34 +03:00
|
|
|
frameIndex =
|
|
|
|
(aOffset - mParser->FirstFrame().Offset()) / AverageFrameLength();
|
2015-11-27 07:40:30 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
ADTSLOGV("FrameIndexFromOffset(%" PRId64 ") -> %" PRId64, aOffset, frameIndex);
|
|
|
|
return std::max<int64_t>(0, frameIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
int64_t
|
|
|
|
ADTSTrackDemuxer::FrameIndexFromTime(const media::TimeUnit& aTime) const
|
|
|
|
{
|
|
|
|
int64_t frameIndex = 0;
|
|
|
|
if (mSamplesPerSecond > 0 && mSamplesPerFrame > 0) {
|
|
|
|
frameIndex = aTime.ToSeconds() * mSamplesPerSecond / mSamplesPerFrame - 1;
|
|
|
|
}
|
|
|
|
|
2017-02-07 11:23:34 +03:00
|
|
|
ADTSLOGV("FrameIndexFromOffset(%fs) -> %" PRId64,
|
|
|
|
aTime.ToSeconds(), frameIndex);
|
2015-11-27 07:40:30 +03:00
|
|
|
return std::max<int64_t>(0, frameIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
ADTSTrackDemuxer::UpdateState(const adts::Frame& aFrame)
|
|
|
|
{
|
|
|
|
int32_t frameLength = aFrame.Length();
|
|
|
|
// Prevent overflow.
|
|
|
|
if (mTotalFrameLen + frameLength < mTotalFrameLen) {
|
|
|
|
// These variables have a linear dependency and are only used to derive the
|
|
|
|
// average frame length.
|
|
|
|
mTotalFrameLen /= 2;
|
|
|
|
mNumParsedFrames /= 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Full frame parsed, move offset to its end.
|
|
|
|
mOffset = aFrame.Offset() + frameLength;
|
|
|
|
mTotalFrameLen += frameLength;
|
|
|
|
|
|
|
|
if (!mSamplesPerFrame) {
|
|
|
|
const adts::FrameHeader& header = aFrame.Header();
|
|
|
|
mSamplesPerFrame = header.mSamples;
|
|
|
|
mSamplesPerSecond = header.mSampleRate;
|
|
|
|
mChannels = header.mChannels;
|
|
|
|
}
|
|
|
|
|
|
|
|
++mNumParsedFrames;
|
|
|
|
++mFrameIndex;
|
|
|
|
MOZ_ASSERT(mFrameIndex > 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
int32_t
|
|
|
|
ADTSTrackDemuxer::Read(uint8_t* aBuffer, int64_t aOffset, int32_t aSize)
|
|
|
|
{
|
2017-02-07 11:23:34 +03:00
|
|
|
ADTSLOGV("ADTSTrackDemuxer::Read(%p %" PRId64 " %d)",
|
|
|
|
aBuffer, aOffset, aSize);
|
2015-11-27 07:40:30 +03:00
|
|
|
|
|
|
|
const int64_t streamLen = StreamLength();
|
|
|
|
if (mInfo && streamLen > 0) {
|
|
|
|
// Prevent blocking reads after successful initialization.
|
|
|
|
aSize = std::min<int64_t>(aSize, streamLen - aOffset);
|
|
|
|
}
|
|
|
|
|
|
|
|
uint32_t read = 0;
|
|
|
|
ADTSLOGV("ADTSTrackDemuxer::Read -> ReadAt(%d)", aSize);
|
|
|
|
const nsresult rv = mSource.ReadAt(aOffset, reinterpret_cast<char*>(aBuffer),
|
|
|
|
static_cast<uint32_t>(aSize), &read);
|
|
|
|
NS_ENSURE_SUCCESS(rv, 0);
|
|
|
|
return static_cast<int32_t>(read);
|
|
|
|
}
|
|
|
|
|
|
|
|
double
|
|
|
|
ADTSTrackDemuxer::AverageFrameLength() const
|
|
|
|
{
|
|
|
|
if (mNumParsedFrames) {
|
|
|
|
return static_cast<double>(mTotalFrameLen) / mNumParsedFrames;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0.0;
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace mozilla
|