gecko-dev/dom/media/mp4/Index.cpp

708 строки
24 KiB
C++

/* 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 "Index.h"
#include <algorithm>
#include <limits>
#include "BufferReader.h"
#include "mozilla/RefPtr.h"
#include "MP4Interval.h"
#include "MP4Metadata.h"
#include "nsAutoPtr.h"
#include "SinfParser.h"
using namespace mozilla::media;
namespace mozilla {
class MOZ_STACK_CLASS RangeFinder {
public:
// Given that we're processing this in order we don't use a binary search
// to find the apropriate time range. Instead we search linearly from the
// last used point.
explicit RangeFinder(const MediaByteRangeSet& ranges)
: mRanges(ranges), mIndex(0) {
// Ranges must be normalised for this to work
}
bool Contains(MediaByteRange aByteRange);
private:
const MediaByteRangeSet& mRanges;
size_t mIndex;
};
bool RangeFinder::Contains(MediaByteRange aByteRange) {
if (mRanges.IsEmpty()) {
return false;
}
if (mRanges[mIndex].ContainsStrict(aByteRange)) {
return true;
}
if (aByteRange.mStart < mRanges[mIndex].mStart) {
// Search backwards
do {
if (!mIndex) {
return false;
}
--mIndex;
if (mRanges[mIndex].ContainsStrict(aByteRange)) {
return true;
}
} while (aByteRange.mStart < mRanges[mIndex].mStart);
return false;
}
while (aByteRange.mEnd > mRanges[mIndex].mEnd) {
if (mIndex == mRanges.Length() - 1) {
return false;
}
++mIndex;
if (mRanges[mIndex].ContainsStrict(aByteRange)) {
return true;
}
}
return false;
}
SampleIterator::SampleIterator(Index* aIndex)
: mIndex(aIndex), mCurrentMoof(0), mCurrentSample(0) {
mIndex->RegisterIterator(this);
}
SampleIterator::~SampleIterator() { mIndex->UnregisterIterator(this); }
already_AddRefed<MediaRawData> SampleIterator::GetNext() {
Sample* s(Get());
if (!s) {
return nullptr;
}
int64_t length = std::numeric_limits<int64_t>::max();
mIndex->mSource->Length(&length);
if (s->mByteRange.mEnd > length) {
// We don't have this complete sample.
return nullptr;
}
RefPtr<MediaRawData> sample = new MediaRawData();
sample->mTimecode = TimeUnit::FromMicroseconds(s->mDecodeTime);
sample->mTime = TimeUnit::FromMicroseconds(s->mCompositionRange.start);
sample->mDuration = TimeUnit::FromMicroseconds(s->mCompositionRange.Length());
sample->mOffset = s->mByteRange.mStart;
sample->mKeyframe = s->mSync;
UniquePtr<MediaRawDataWriter> writer(sample->CreateWriter());
// Do the blocking read
if (!writer->SetSize(s->mByteRange.Length())) {
return nullptr;
}
size_t bytesRead;
if (!mIndex->mSource->ReadAt(sample->mOffset, writer->Data(), sample->Size(),
&bytesRead) ||
bytesRead != sample->Size()) {
return nullptr;
}
MoofParser* moofParser = mIndex->mMoofParser.get();
if (!moofParser) {
// File is not fragmented, we can't have crypto, just early return.
Next();
return sample.forget();
}
// We need to check if this moof has init data the CDM expects us to surface.
// This should happen when handling the first sample, even if that sample
// isn't encrypted (samples later in the moof may be).
if (mCurrentSample == 0) {
const nsTArray<Moof>& moofs = moofParser->Moofs();
const Moof* currentMoof = &moofs[mCurrentMoof];
if (!currentMoof->mPsshes.IsEmpty()) {
// This Moof contained crypto init data. Report that. We only report
// the init data on the Moof's first sample, to avoid reporting it more
// than once per Moof.
writer->mCrypto.mInitDatas.AppendElements(currentMoof->mPsshes);
writer->mCrypto.mInitDataType = NS_LITERAL_STRING("cenc");
}
}
auto cryptoSchemeResult = GetEncryptionScheme();
if (cryptoSchemeResult.isErr()) {
// Log the error here in future.
return nullptr;
}
CryptoScheme cryptoScheme = cryptoSchemeResult.unwrap();
if (cryptoScheme == CryptoScheme::None) {
// No crypto to handle, early return.
Next();
return sample.forget();
}
writer->mCrypto.mCryptoScheme = cryptoScheme;
MOZ_ASSERT(writer->mCrypto.mCryptoScheme != CryptoScheme::None,
"Should have early returned if we don't have a crypto scheme!");
MOZ_ASSERT(writer->mCrypto.mKeyId.IsEmpty(),
"Sample should not already have a key ID");
MOZ_ASSERT(writer->mCrypto.mConstantIV.IsEmpty(),
"Sample should not already have a constant IV");
CencSampleEncryptionInfoEntry* sampleInfo = GetSampleEncryptionEntry();
if (sampleInfo) {
// Use sample group information if present, this supersedes track level
// information.
writer->mCrypto.mKeyId.AppendElements(sampleInfo->mKeyId);
writer->mCrypto.mIVSize = sampleInfo->mIVSize;
writer->mCrypto.mCryptByteBlock = sampleInfo->mCryptByteBlock;
writer->mCrypto.mSkipByteBlock = sampleInfo->mSkipByteBlock;
writer->mCrypto.mConstantIV.AppendElements(sampleInfo->mConsantIV);
} else {
// Use the crypto info from track metadata
writer->mCrypto.mKeyId.AppendElements(moofParser->mSinf.mDefaultKeyID, 16);
writer->mCrypto.mIVSize = moofParser->mSinf.mDefaultIVSize;
writer->mCrypto.mCryptByteBlock = moofParser->mSinf.mDefaultCryptByteBlock;
writer->mCrypto.mSkipByteBlock = moofParser->mSinf.mDefaultSkipByteBlock;
writer->mCrypto.mConstantIV.AppendElements(
moofParser->mSinf.mDefaultConstantIV);
}
if ((writer->mCrypto.mIVSize == 0 && writer->mCrypto.mConstantIV.IsEmpty()) ||
(writer->mCrypto.mIVSize != 0 && s->mCencRange.IsEmpty())) {
// If mIVSize == 0, this indicates that a constant IV is in use, thus we
// should have a non empty constant IV. Alternatively if IV size is non
// zero, we should have an IV for this sample, which we need to look up
// in mCencRange (which must then be non empty). If neither of these are
// true we have bad crypto data, so bail.
return nullptr;
}
// Parse auxiliary information if present
if (!s->mCencRange.IsEmpty()) {
// The size comes from an 8 bit field
AutoTArray<uint8_t, 256> cencAuxInfo;
cencAuxInfo.SetLength(s->mCencRange.Length());
if (!mIndex->mSource->ReadAt(s->mCencRange.mStart, cencAuxInfo.Elements(),
cencAuxInfo.Length(), &bytesRead) ||
bytesRead != cencAuxInfo.Length()) {
return nullptr;
}
BufferReader reader(cencAuxInfo);
if (!reader.ReadArray(writer->mCrypto.mIV, writer->mCrypto.mIVSize)) {
return nullptr;
}
// Parse the auxiliary information for subsample information
auto res = reader.ReadU16();
if (res.isOk() && res.unwrap() > 0) {
uint16_t count = res.unwrap();
if (reader.Remaining() < count * 6) {
return nullptr;
}
for (size_t i = 0; i < count; i++) {
auto res_16 = reader.ReadU16();
auto res_32 = reader.ReadU32();
if (res_16.isErr() || res_32.isErr()) {
return nullptr;
}
writer->mCrypto.mPlainSizes.AppendElement(res_16.unwrap());
writer->mCrypto.mEncryptedSizes.AppendElement(res_32.unwrap());
}
} else {
// No subsample information means the entire sample is encrypted.
writer->mCrypto.mPlainSizes.AppendElement(0);
writer->mCrypto.mEncryptedSizes.AppendElement(sample->Size());
}
}
Next();
return sample.forget();
}
SampleDescriptionEntry* SampleIterator::GetSampleDescriptionEntry() {
nsTArray<Moof>& moofs = mIndex->mMoofParser->Moofs();
Moof& currentMoof = moofs[mCurrentMoof];
uint32_t sampleDescriptionIndex =
currentMoof.mTfhd.mDefaultSampleDescriptionIndex;
// Mp4 indices start at 1, shift down 1 so we index our array correctly.
sampleDescriptionIndex--;
FallibleTArray<SampleDescriptionEntry>& sampleDescriptions =
mIndex->mMoofParser->mSampleDescriptions;
if (sampleDescriptionIndex >= sampleDescriptions.Length()) {
// The sample description index is invalid, the mp4 is malformed. Bail out.
return nullptr;
}
return &sampleDescriptions[sampleDescriptionIndex];
}
CencSampleEncryptionInfoEntry* SampleIterator::GetSampleEncryptionEntry() {
nsTArray<Moof>& moofs = mIndex->mMoofParser->Moofs();
Moof* currentMoof = &moofs[mCurrentMoof];
SampleToGroupEntry* sampleToGroupEntry = nullptr;
// Default to using the sample to group entries for the fragment, otherwise
// fall back to the sample to group entries for the track.
FallibleTArray<SampleToGroupEntry>* sampleToGroupEntries =
currentMoof->mFragmentSampleToGroupEntries.Length() != 0
? &currentMoof->mFragmentSampleToGroupEntries
: &mIndex->mMoofParser->mTrackSampleToGroupEntries;
uint32_t seen = 0;
for (SampleToGroupEntry& entry : *sampleToGroupEntries) {
if (seen + entry.mSampleCount > mCurrentSample) {
sampleToGroupEntry = &entry;
break;
}
seen += entry.mSampleCount;
}
// ISO-14496-12 Section 8.9.2.3 and 8.9.4 : group description index
// (1) ranges from 1 to the number of sample group entries in the track
// level SampleGroupDescription Box, or (2) takes the value 0 to
// indicate that this sample is a member of no group, in this case, the
// sample is associated with the default values specified in
// TrackEncryption Box, or (3) starts at 0x10001, i.e. the index value
// 1, with the value 1 in the top 16 bits, to reference fragment-local
// SampleGroupDescription Box.
// According to the spec, ISO-14496-12, the sum of the sample counts in this
// box should be equal to the total number of samples, and, if less, the
// reader should behave as if an extra SampleToGroupEntry existed, with
// groupDescriptionIndex 0.
if (!sampleToGroupEntry || sampleToGroupEntry->mGroupDescriptionIndex == 0) {
return nullptr;
}
FallibleTArray<CencSampleEncryptionInfoEntry>* entries =
&mIndex->mMoofParser->mTrackSampleEncryptionInfoEntries;
uint32_t groupIndex = sampleToGroupEntry->mGroupDescriptionIndex;
// If the first bit is set to a one, then we should use the sample group
// descriptions from the fragment.
if (groupIndex > SampleToGroupEntry::kFragmentGroupDescriptionIndexBase) {
groupIndex -= SampleToGroupEntry::kFragmentGroupDescriptionIndexBase;
entries = &currentMoof->mFragmentSampleEncryptionInfoEntries;
}
// The group_index is one based.
return groupIndex > entries->Length() ? nullptr
: &entries->ElementAt(groupIndex - 1);
}
Result<CryptoScheme, const nsCString> SampleIterator::GetEncryptionScheme() {
// See ISO/IEC 23001-7 for information on the metadata being checked.
MoofParser* moofParser = mIndex->mMoofParser.get();
if (!moofParser) {
// This mp4 isn't fragmented so it can't be encrypted.
return CryptoScheme::None;
}
SampleDescriptionEntry* sampleDescriptionEntry = GetSampleDescriptionEntry();
if (!sampleDescriptionEntry) {
// For the file to be valid the tfhd must reference a sample description
// entry.
// If we encounter this error often, we may consider using the first
// sample description entry if the index is out of bounds.
return mozilla::Err(NS_LITERAL_CSTRING(
"Could not determine encryption scheme due to bad index for sample "
"description entry."));
}
if (!sampleDescriptionEntry->mIsEncryptedEntry) {
return CryptoScheme::None;
}
if (!moofParser->mSinf.IsValid()) {
// The sample description entry says this sample is encrypted, but we
// don't have a valid sinf box. This shouldn't happen as the sinf box is
// part of the sample description entry. Suggests a malformed file, bail.
return mozilla::Err(NS_LITERAL_CSTRING(
"Could not determine encryption scheme. Sample description entry "
"indicates encryption, but could not find associated sinf box."));
}
CencSampleEncryptionInfoEntry* sampleInfo = GetSampleEncryptionEntry();
if (sampleInfo && !sampleInfo->mIsEncrypted) {
// May not have sample encryption info, but if we do, it should match other
// metadata.
return mozilla::Err(NS_LITERAL_CSTRING(
"Could not determine encryption scheme. Sample description entry "
"indicates encryption, but sample encryption entry indicates sample is "
"not encrypted. These should be consistent."));
}
if (moofParser->mSinf.mDefaultEncryptionType == AtomType("cenc")) {
return CryptoScheme::Cenc;
} else if (moofParser->mSinf.mDefaultEncryptionType == AtomType("cbcs")) {
return CryptoScheme::Cbcs;
}
return mozilla::Err(NS_LITERAL_CSTRING(
"Could not determine encryption scheme. Sample description entry "
"reports sample is encrypted, but no scheme, or an unsupported scheme "
"is in use."));
}
Sample* SampleIterator::Get() {
if (!mIndex->mMoofParser) {
MOZ_ASSERT(!mCurrentMoof);
return mCurrentSample < mIndex->mIndex.Length()
? &mIndex->mIndex[mCurrentSample]
: nullptr;
}
nsTArray<Moof>& moofs = mIndex->mMoofParser->Moofs();
while (true) {
if (mCurrentMoof == moofs.Length()) {
if (!mIndex->mMoofParser->BlockingReadNextMoof()) {
return nullptr;
}
MOZ_ASSERT(mCurrentMoof < moofs.Length());
}
if (mCurrentSample < moofs[mCurrentMoof].mIndex.Length()) {
break;
}
mCurrentSample = 0;
++mCurrentMoof;
}
return &moofs[mCurrentMoof].mIndex[mCurrentSample];
}
void SampleIterator::Next() { ++mCurrentSample; }
void SampleIterator::Seek(Microseconds aTime) {
size_t syncMoof = 0;
size_t syncSample = 0;
mCurrentMoof = 0;
mCurrentSample = 0;
Sample* sample;
while (!!(sample = Get())) {
if (sample->mCompositionRange.start > aTime) {
break;
}
if (sample->mSync) {
syncMoof = mCurrentMoof;
syncSample = mCurrentSample;
}
if (sample->mCompositionRange.start == aTime) {
break;
}
Next();
}
mCurrentMoof = syncMoof;
mCurrentSample = syncSample;
}
Microseconds SampleIterator::GetNextKeyframeTime() {
SampleIterator itr(*this);
Sample* sample;
while (!!(sample = itr.Get())) {
if (sample->mSync) {
return sample->mCompositionRange.start;
}
itr.Next();
}
return -1;
}
Index::Index(const IndiceWrapper& aIndices, ByteStream* aSource,
uint32_t aTrackId, bool aIsAudio)
: mSource(aSource), mIsAudio(aIsAudio) {
if (!aIndices.Length()) {
mMoofParser = new MoofParser(aSource, AsVariant(aTrackId), aIsAudio);
} else {
if (!mIndex.SetCapacity(aIndices.Length(), fallible)) {
// OOM.
return;
}
media::IntervalSet<int64_t> intervalTime;
MediaByteRange intervalRange;
bool haveSync = false;
bool progressive = true;
int64_t lastOffset = 0;
for (size_t i = 0; i < aIndices.Length(); i++) {
Indice indice;
if (!aIndices.GetIndice(i, indice)) {
// Out of index?
return;
}
if (indice.sync || mIsAudio) {
haveSync = true;
}
if (!haveSync) {
continue;
}
Sample sample;
sample.mByteRange =
MediaByteRange(indice.start_offset, indice.end_offset);
sample.mCompositionRange = MP4Interval<Microseconds>(
indice.start_composition, indice.end_composition);
sample.mDecodeTime = indice.start_decode;
sample.mSync = indice.sync || mIsAudio;
// FIXME: Make this infallible after bug 968520 is done.
MOZ_ALWAYS_TRUE(mIndex.AppendElement(sample, fallible));
if (indice.start_offset < lastOffset) {
NS_WARNING("Chunks in MP4 out of order, expect slow down");
progressive = false;
}
lastOffset = indice.end_offset;
// Pack audio samples in group of 128.
if (sample.mSync && progressive && (!mIsAudio || !(i % 128))) {
if (mDataOffset.Length()) {
auto& last = mDataOffset.LastElement();
last.mEndOffset = intervalRange.mEnd;
NS_ASSERTION(intervalTime.Length() == 1,
"Discontinuous samples between keyframes");
last.mTime.start = intervalTime.GetStart();
last.mTime.end = intervalTime.GetEnd();
}
if (!mDataOffset.AppendElement(
MP4DataOffset(mIndex.Length() - 1, indice.start_offset),
fallible)) {
// OOM.
return;
}
intervalTime = media::IntervalSet<int64_t>();
intervalRange = MediaByteRange();
}
intervalTime += media::Interval<int64_t>(sample.mCompositionRange.start,
sample.mCompositionRange.end);
intervalRange = intervalRange.Span(sample.mByteRange);
}
if (mDataOffset.Length() && progressive) {
Indice indice;
if (!aIndices.GetIndice(aIndices.Length() - 1, indice)) {
return;
}
auto& last = mDataOffset.LastElement();
last.mEndOffset = indice.end_offset;
last.mTime =
MP4Interval<int64_t>(intervalTime.GetStart(), intervalTime.GetEnd());
} else {
mDataOffset.Clear();
}
}
}
Index::~Index() {}
void Index::UpdateMoofIndex(const MediaByteRangeSet& aByteRanges) {
UpdateMoofIndex(aByteRanges, false);
}
void Index::UpdateMoofIndex(const MediaByteRangeSet& aByteRanges,
bool aCanEvict) {
if (!mMoofParser) {
return;
}
size_t moofs = mMoofParser->Moofs().Length();
bool canEvict = aCanEvict && moofs > 1;
if (canEvict) {
// Check that we can trim the mMoofParser. We can only do so if all
// iterators have demuxed all possible samples.
for (const SampleIterator* iterator : mIterators) {
if ((iterator->mCurrentSample == 0 && iterator->mCurrentMoof == moofs) ||
iterator->mCurrentMoof == moofs - 1) {
continue;
}
canEvict = false;
break;
}
}
mMoofParser->RebuildFragmentedIndex(aByteRanges, &canEvict);
if (canEvict) {
// The moofparser got trimmed. Adjust all registered iterators.
for (SampleIterator* iterator : mIterators) {
iterator->mCurrentMoof -= moofs - 1;
}
}
}
Microseconds Index::GetEndCompositionIfBuffered(
const MediaByteRangeSet& aByteRanges) {
FallibleTArray<Sample>* index;
if (mMoofParser) {
if (!mMoofParser->ReachedEnd() || mMoofParser->Moofs().IsEmpty()) {
return 0;
}
index = &mMoofParser->Moofs().LastElement().mIndex;
} else {
index = &mIndex;
}
Microseconds lastComposition = 0;
RangeFinder rangeFinder(aByteRanges);
for (size_t i = index->Length(); i--;) {
const Sample& sample = (*index)[i];
if (!rangeFinder.Contains(sample.mByteRange)) {
return 0;
}
lastComposition = std::max(lastComposition, sample.mCompositionRange.end);
if (sample.mSync) {
return lastComposition;
}
}
return 0;
}
TimeIntervals Index::ConvertByteRangesToTimeRanges(
const MediaByteRangeSet& aByteRanges) {
if (aByteRanges == mLastCachedRanges) {
return mLastBufferedRanges;
}
mLastCachedRanges = aByteRanges;
if (mDataOffset.Length()) {
TimeIntervals timeRanges;
for (const auto& range : aByteRanges) {
uint32_t start = mDataOffset.IndexOfFirstElementGt(range.mStart - 1);
if (!mIsAudio && start == mDataOffset.Length()) {
continue;
}
uint32_t end = mDataOffset.IndexOfFirstElementGt(
range.mEnd, MP4DataOffset::EndOffsetComparator());
if (!mIsAudio && end < start) {
continue;
}
if (mIsAudio && start &&
range.Intersects(MediaByteRange(mDataOffset[start - 1].mStartOffset,
mDataOffset[start - 1].mEndOffset))) {
// Check if previous audio data block contains some available samples.
for (size_t i = mDataOffset[start - 1].mIndex; i < mIndex.Length();
i++) {
if (range.ContainsStrict(mIndex[i].mByteRange)) {
timeRanges += TimeInterval(
TimeUnit::FromMicroseconds(mIndex[i].mCompositionRange.start),
TimeUnit::FromMicroseconds(mIndex[i].mCompositionRange.end));
}
}
}
if (end > start) {
for (uint32_t i = start; i < end; i++) {
timeRanges += TimeInterval(
TimeUnit::FromMicroseconds(mDataOffset[i].mTime.start),
TimeUnit::FromMicroseconds(mDataOffset[i].mTime.end));
}
}
if (end < mDataOffset.Length()) {
// Find samples in partial block contained in the byte range.
for (size_t i = mDataOffset[end].mIndex;
i < mIndex.Length() && range.ContainsStrict(mIndex[i].mByteRange);
i++) {
timeRanges += TimeInterval(
TimeUnit::FromMicroseconds(mIndex[i].mCompositionRange.start),
TimeUnit::FromMicroseconds(mIndex[i].mCompositionRange.end));
}
}
}
mLastBufferedRanges = timeRanges;
return timeRanges;
}
RangeFinder rangeFinder(aByteRanges);
nsTArray<MP4Interval<Microseconds>> timeRanges;
nsTArray<FallibleTArray<Sample>*> indexes;
if (mMoofParser) {
// We take the index out of the moof parser and move it into a local
// variable so we don't get concurrency issues. It gets freed when we
// exit this function.
for (int i = 0; i < mMoofParser->Moofs().Length(); i++) {
Moof& moof = mMoofParser->Moofs()[i];
// We need the entire moof in order to play anything
if (rangeFinder.Contains(moof.mRange)) {
if (rangeFinder.Contains(moof.mMdatRange)) {
MP4Interval<Microseconds>::SemiNormalAppend(timeRanges,
moof.mTimeRange);
} else {
indexes.AppendElement(&moof.mIndex);
}
}
}
} else {
indexes.AppendElement(&mIndex);
}
bool hasSync = false;
for (size_t i = 0; i < indexes.Length(); i++) {
FallibleTArray<Sample>* index = indexes[i];
for (size_t j = 0; j < index->Length(); j++) {
const Sample& sample = (*index)[j];
if (!rangeFinder.Contains(sample.mByteRange)) {
// We process the index in decode order so we clear hasSync when we hit
// a range that isn't buffered.
hasSync = false;
continue;
}
hasSync |= sample.mSync;
if (!hasSync) {
continue;
}
MP4Interval<Microseconds>::SemiNormalAppend(timeRanges,
sample.mCompositionRange);
}
}
// This fixes up when the compositon order differs from the byte range order
nsTArray<MP4Interval<Microseconds>> timeRangesNormalized;
MP4Interval<Microseconds>::Normalize(timeRanges, &timeRangesNormalized);
// convert timeRanges.
media::TimeIntervals ranges;
for (size_t i = 0; i < timeRangesNormalized.Length(); i++) {
ranges += media::TimeInterval(
media::TimeUnit::FromMicroseconds(timeRangesNormalized[i].start),
media::TimeUnit::FromMicroseconds(timeRangesNormalized[i].end));
}
mLastBufferedRanges = ranges;
return ranges;
}
uint64_t Index::GetEvictionOffset(Microseconds aTime) {
uint64_t offset = std::numeric_limits<uint64_t>::max();
if (mMoofParser) {
// We need to keep the whole moof if we're keeping any of it because the
// parser doesn't keep parsed moofs.
for (int i = 0; i < mMoofParser->Moofs().Length(); i++) {
Moof& moof = mMoofParser->Moofs()[i];
if (moof.mTimeRange.Length() && moof.mTimeRange.end > aTime) {
offset = std::min(offset, uint64_t(std::min(moof.mRange.mStart,
moof.mMdatRange.mStart)));
}
}
} else {
// We've already parsed and stored the moov so we don't need to keep it.
// All we need to keep is the sample data itself.
for (size_t i = 0; i < mIndex.Length(); i++) {
const Sample& sample = mIndex[i];
if (aTime >= sample.mCompositionRange.end) {
offset = std::min(offset, uint64_t(sample.mByteRange.mEnd));
}
}
}
return offset;
}
void Index::RegisterIterator(SampleIterator* aIterator) {
mIterators.AppendElement(aIterator);
}
void Index::UnregisterIterator(SampleIterator* aIterator) {
mIterators.RemoveElement(aIterator);
}
} // namespace mozilla