Line data Source code
1 : /* This Source Code Form is subject to the terms of the Mozilla Public
2 : * License, v. 2.0. If a copy of the MPL was not distributed with this
3 : * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 :
5 : #include "mp4_demuxer/ByteReader.h"
6 : #include "mp4_demuxer/Index.h"
7 : #include "mp4_demuxer/Interval.h"
8 : #include "mp4_demuxer/MP4Metadata.h"
9 : #include "mp4_demuxer/SinfParser.h"
10 : #include "nsAutoPtr.h"
11 : #include "mozilla/RefPtr.h"
12 :
13 : #include <algorithm>
14 : #include <limits>
15 :
16 : using namespace stagefright;
17 : using namespace mozilla;
18 : using namespace mozilla::media;
19 :
20 : namespace mp4_demuxer
21 : {
22 :
23 : class MOZ_STACK_CLASS RangeFinder
24 : {
25 : public:
26 : // Given that we're processing this in order we don't use a binary search
27 : // to find the apropriate time range. Instead we search linearly from the
28 : // last used point.
29 0 : explicit RangeFinder(const MediaByteRangeSet& ranges)
30 0 : : mRanges(ranges), mIndex(0)
31 : {
32 : // Ranges must be normalised for this to work
33 0 : }
34 :
35 : bool Contains(MediaByteRange aByteRange);
36 :
37 : private:
38 : const MediaByteRangeSet& mRanges;
39 : size_t mIndex;
40 : };
41 :
42 : bool
43 0 : RangeFinder::Contains(MediaByteRange aByteRange)
44 : {
45 0 : if (!mRanges.Length()) {
46 0 : return false;
47 : }
48 :
49 0 : if (mRanges[mIndex].ContainsStrict(aByteRange)) {
50 0 : return true;
51 : }
52 :
53 0 : if (aByteRange.mStart < mRanges[mIndex].mStart) {
54 : // Search backwards
55 0 : do {
56 0 : if (!mIndex) {
57 0 : return false;
58 : }
59 0 : --mIndex;
60 0 : if (mRanges[mIndex].ContainsStrict(aByteRange)) {
61 0 : return true;
62 : }
63 0 : } while (aByteRange.mStart < mRanges[mIndex].mStart);
64 :
65 0 : return false;
66 : }
67 :
68 0 : while (aByteRange.mEnd > mRanges[mIndex].mEnd) {
69 0 : if (mIndex == mRanges.Length() - 1) {
70 0 : return false;
71 : }
72 0 : ++mIndex;
73 0 : if (mRanges[mIndex].ContainsStrict(aByteRange)) {
74 0 : return true;
75 : }
76 : }
77 :
78 0 : return false;
79 : }
80 :
81 0 : SampleIterator::SampleIterator(Index* aIndex)
82 : : mIndex(aIndex)
83 : , mCurrentMoof(0)
84 0 : , mCurrentSample(0)
85 : {
86 0 : mIndex->RegisterIterator(this);
87 0 : }
88 :
89 0 : SampleIterator::~SampleIterator()
90 : {
91 0 : mIndex->UnregisterIterator(this);
92 0 : }
93 :
94 0 : already_AddRefed<MediaRawData> SampleIterator::GetNext()
95 : {
96 0 : Sample* s(Get());
97 0 : if (!s) {
98 0 : return nullptr;
99 : }
100 :
101 0 : int64_t length = std::numeric_limits<int64_t>::max();
102 0 : mIndex->mSource->Length(&length);
103 0 : if (s->mByteRange.mEnd > length) {
104 : // We don't have this complete sample.
105 0 : return nullptr;
106 : }
107 :
108 0 : RefPtr<MediaRawData> sample = new MediaRawData();
109 0 : sample->mTimecode= TimeUnit::FromMicroseconds(s->mDecodeTime);
110 0 : sample->mTime = TimeUnit::FromMicroseconds(s->mCompositionRange.start);
111 0 : sample->mDuration = TimeUnit::FromMicroseconds(s->mCompositionRange.Length());
112 0 : sample->mOffset = s->mByteRange.mStart;
113 0 : sample->mKeyframe = s->mSync;
114 :
115 0 : nsAutoPtr<MediaRawDataWriter> writer(sample->CreateWriter());
116 : // Do the blocking read
117 0 : if (!writer->SetSize(s->mByteRange.Length())) {
118 0 : return nullptr;
119 : }
120 :
121 : size_t bytesRead;
122 0 : if (!mIndex->mSource->ReadAt(sample->mOffset, writer->Data(), sample->Size(),
123 0 : &bytesRead) || bytesRead != sample->Size()) {
124 0 : return nullptr;
125 : }
126 :
127 0 : if (!s->mCencRange.IsEmpty()) {
128 0 : MoofParser* parser = mIndex->mMoofParser.get();
129 :
130 0 : if (!parser || !parser->mSinf.IsValid()) {
131 0 : return nullptr;
132 : }
133 :
134 0 : uint8_t ivSize = parser->mSinf.mDefaultIVSize;
135 :
136 : // The size comes from an 8 bit field
137 0 : AutoTArray<uint8_t, 256> cenc;
138 0 : cenc.SetLength(s->mCencRange.Length());
139 0 : if (!mIndex->mSource->ReadAt(s->mCencRange.mStart, cenc.Elements(), cenc.Length(),
140 0 : &bytesRead) || bytesRead != cenc.Length()) {
141 0 : return nullptr;
142 : }
143 0 : ByteReader reader(cenc);
144 0 : writer->mCrypto.mValid = true;
145 0 : writer->mCrypto.mIVSize = ivSize;
146 :
147 0 : CencSampleEncryptionInfoEntry* sampleInfo = GetSampleEncryptionEntry();
148 0 : if (sampleInfo) {
149 0 : writer->mCrypto.mKeyId.AppendElements(sampleInfo->mKeyId);
150 : }
151 :
152 0 : if (!reader.ReadArray(writer->mCrypto.mIV, ivSize)) {
153 0 : return nullptr;
154 : }
155 :
156 0 : if (reader.CanRead16()) {
157 0 : uint16_t count = reader.ReadU16();
158 :
159 0 : if (reader.Remaining() < count * 6) {
160 0 : return nullptr;
161 : }
162 :
163 0 : for (size_t i = 0; i < count; i++) {
164 0 : writer->mCrypto.mPlainSizes.AppendElement(reader.ReadU16());
165 0 : writer->mCrypto.mEncryptedSizes.AppendElement(reader.ReadU32());
166 : }
167 : } else {
168 : // No subsample information means the entire sample is encrypted.
169 0 : writer->mCrypto.mPlainSizes.AppendElement(0);
170 0 : writer->mCrypto.mEncryptedSizes.AppendElement(sample->Size());
171 : }
172 : }
173 :
174 0 : Next();
175 :
176 0 : return sample.forget();
177 : }
178 :
179 0 : CencSampleEncryptionInfoEntry* SampleIterator::GetSampleEncryptionEntry()
180 : {
181 0 : nsTArray<Moof>& moofs = mIndex->mMoofParser->Moofs();
182 0 : Moof* currentMoof = &moofs[mCurrentMoof];
183 0 : SampleToGroupEntry* sampleToGroupEntry = nullptr;
184 :
185 : // Default to using the sample to group entries for the fragment, otherwise
186 : // fall back to the sample to group entries for the track.
187 : nsTArray<SampleToGroupEntry>* sampleToGroupEntries =
188 0 : currentMoof->mFragmentSampleToGroupEntries.Length() != 0
189 0 : ? ¤tMoof->mFragmentSampleToGroupEntries
190 0 : : &mIndex->mMoofParser->mTrackSampleToGroupEntries;
191 :
192 0 : uint32_t seen = 0;
193 :
194 0 : for (SampleToGroupEntry& entry : *sampleToGroupEntries) {
195 0 : if (seen + entry.mSampleCount > mCurrentSample) {
196 0 : sampleToGroupEntry = &entry;
197 0 : break;
198 : }
199 0 : seen += entry.mSampleCount;
200 : }
201 :
202 : // ISO-14496-12 Section 8.9.2.3 and 8.9.4 : group description index
203 : // (1) ranges from 1 to the number of sample group entries in the track
204 : // level SampleGroupDescription Box, or (2) takes the value 0 to
205 : // indicate that this sample is a member of no group, in this case, the
206 : // sample is associated with the default values specified in
207 : // TrackEncryption Box, or (3) starts at 0x10001, i.e. the index value
208 : // 1, with the value 1 in the top 16 bits, to reference fragment-local
209 : // SampleGroupDescription Box.
210 :
211 : // According to the spec, ISO-14496-12, the sum of the sample counts in this
212 : // box should be equal to the total number of samples, and, if less, the
213 : // reader should behave as if an extra SampleToGroupEntry existed, with
214 : // groupDescriptionIndex 0.
215 :
216 0 : if (!sampleToGroupEntry || sampleToGroupEntry->mGroupDescriptionIndex == 0) {
217 0 : return nullptr;
218 : }
219 :
220 : nsTArray<CencSampleEncryptionInfoEntry>* entries =
221 0 : &mIndex->mMoofParser->mTrackSampleEncryptionInfoEntries;
222 :
223 0 : uint32_t groupIndex = sampleToGroupEntry->mGroupDescriptionIndex;
224 :
225 : // If the first bit is set to a one, then we should use the sample group
226 : // descriptions from the fragment.
227 0 : if (groupIndex > SampleToGroupEntry::kFragmentGroupDescriptionIndexBase) {
228 0 : groupIndex -= SampleToGroupEntry::kFragmentGroupDescriptionIndexBase;
229 0 : entries = ¤tMoof->mFragmentSampleEncryptionInfoEntries;
230 : }
231 :
232 : // The group_index is one based.
233 0 : return groupIndex > entries->Length()
234 0 : ? nullptr
235 0 : : &entries->ElementAt(groupIndex - 1);
236 : }
237 :
238 0 : Sample* SampleIterator::Get()
239 : {
240 0 : if (!mIndex->mMoofParser) {
241 0 : MOZ_ASSERT(!mCurrentMoof);
242 0 : return mCurrentSample < mIndex->mIndex.Length()
243 0 : ? &mIndex->mIndex[mCurrentSample]
244 0 : : nullptr;
245 : }
246 :
247 0 : nsTArray<Moof>& moofs = mIndex->mMoofParser->Moofs();
248 : while (true) {
249 0 : if (mCurrentMoof == moofs.Length()) {
250 0 : if (!mIndex->mMoofParser->BlockingReadNextMoof()) {
251 0 : return nullptr;
252 : }
253 0 : MOZ_ASSERT(mCurrentMoof < moofs.Length());
254 : }
255 0 : if (mCurrentSample < moofs[mCurrentMoof].mIndex.Length()) {
256 0 : break;
257 : }
258 0 : mCurrentSample = 0;
259 0 : ++mCurrentMoof;
260 : }
261 0 : return &moofs[mCurrentMoof].mIndex[mCurrentSample];
262 : }
263 :
264 0 : void SampleIterator::Next()
265 : {
266 0 : ++mCurrentSample;
267 0 : }
268 :
269 0 : void SampleIterator::Seek(Microseconds aTime)
270 : {
271 0 : size_t syncMoof = 0;
272 0 : size_t syncSample = 0;
273 0 : mCurrentMoof = 0;
274 0 : mCurrentSample = 0;
275 : Sample* sample;
276 0 : while (!!(sample = Get())) {
277 0 : if (sample->mCompositionRange.start > aTime) {
278 0 : break;
279 : }
280 0 : if (sample->mSync) {
281 0 : syncMoof = mCurrentMoof;
282 0 : syncSample = mCurrentSample;
283 : }
284 0 : if (sample->mCompositionRange.start == aTime) {
285 0 : break;
286 : }
287 0 : Next();
288 : }
289 0 : mCurrentMoof = syncMoof;
290 0 : mCurrentSample = syncSample;
291 0 : }
292 :
293 : Microseconds
294 0 : SampleIterator::GetNextKeyframeTime()
295 : {
296 0 : SampleIterator itr(*this);
297 : Sample* sample;
298 0 : while (!!(sample = itr.Get())) {
299 0 : if (sample->mSync) {
300 0 : return sample->mCompositionRange.start;
301 : }
302 0 : itr.Next();
303 : }
304 0 : return -1;
305 : }
306 :
307 0 : Index::Index(const IndiceWrapper& aIndices,
308 : Stream* aSource,
309 : uint32_t aTrackId,
310 0 : bool aIsAudio)
311 : : mSource(aSource)
312 0 : , mIsAudio(aIsAudio)
313 : {
314 0 : if (!aIndices.Length()) {
315 0 : mMoofParser = new MoofParser(aSource, aTrackId, aIsAudio);
316 : } else {
317 0 : if (!mIndex.SetCapacity(aIndices.Length(), fallible)) {
318 : // OOM.
319 0 : return;
320 : }
321 0 : media::IntervalSet<int64_t> intervalTime;
322 0 : MediaByteRange intervalRange;
323 0 : bool haveSync = false;
324 0 : bool progressive = true;
325 0 : int64_t lastOffset = 0;
326 0 : for (size_t i = 0; i < aIndices.Length(); i++) {
327 : Indice indice;
328 0 : if (!aIndices.GetIndice(i, indice)) {
329 : // Out of index?
330 0 : return;
331 : }
332 0 : if (indice.sync || mIsAudio) {
333 0 : haveSync = true;
334 : }
335 0 : if (!haveSync) {
336 0 : continue;
337 : }
338 :
339 0 : Sample sample;
340 0 : sample.mByteRange = MediaByteRange(indice.start_offset,
341 0 : indice.end_offset);
342 0 : sample.mCompositionRange = Interval<Microseconds>(indice.start_composition,
343 0 : indice.end_composition);
344 0 : sample.mDecodeTime = indice.start_decode;
345 0 : sample.mSync = indice.sync || mIsAudio;
346 : // FIXME: Make this infallible after bug 968520 is done.
347 0 : MOZ_ALWAYS_TRUE(mIndex.AppendElement(sample, fallible));
348 0 : if (indice.start_offset < lastOffset) {
349 0 : NS_WARNING("Chunks in MP4 out of order, expect slow down");
350 0 : progressive = false;
351 : }
352 0 : lastOffset = indice.end_offset;
353 :
354 : // Pack audio samples in group of 128.
355 0 : if (sample.mSync && progressive && (!mIsAudio || !(i % 128))) {
356 0 : if (mDataOffset.Length()) {
357 0 : auto& last = mDataOffset.LastElement();
358 0 : last.mEndOffset = intervalRange.mEnd;
359 0 : NS_ASSERTION(intervalTime.Length() == 1, "Discontinuous samples between keyframes");
360 0 : last.mTime.start = intervalTime.GetStart();
361 0 : last.mTime.end = intervalTime.GetEnd();
362 : }
363 0 : if (!mDataOffset.AppendElement(MP4DataOffset(mIndex.Length() - 1,
364 0 : indice.start_offset),
365 : fallible)) {
366 : // OOM.
367 0 : return;
368 : }
369 0 : intervalTime = media::IntervalSet<int64_t>();
370 0 : intervalRange = MediaByteRange();
371 : }
372 0 : intervalTime += media::Interval<int64_t>(sample.mCompositionRange.start,
373 0 : sample.mCompositionRange.end);
374 0 : intervalRange = intervalRange.Span(sample.mByteRange);
375 : }
376 :
377 0 : if (mDataOffset.Length() && progressive) {
378 : Indice indice;
379 0 : if (!aIndices.GetIndice(aIndices.Length() - 1, indice)) {
380 0 : return;
381 : }
382 0 : auto& last = mDataOffset.LastElement();
383 0 : last.mEndOffset = indice.end_offset;
384 0 : last.mTime = Interval<int64_t>(intervalTime.GetStart(), intervalTime.GetEnd());
385 : } else {
386 0 : mDataOffset.Clear();
387 : }
388 : }
389 : }
390 :
391 0 : Index::~Index() {}
392 :
393 : void
394 0 : Index::UpdateMoofIndex(const MediaByteRangeSet& aByteRanges)
395 : {
396 0 : UpdateMoofIndex(aByteRanges, false);
397 0 : }
398 :
399 : void
400 0 : Index::UpdateMoofIndex(const MediaByteRangeSet& aByteRanges, bool aCanEvict)
401 : {
402 0 : if (!mMoofParser) {
403 0 : return;
404 : }
405 0 : size_t moofs = mMoofParser->Moofs().Length();
406 0 : bool canEvict = aCanEvict && moofs > 1;
407 0 : if (canEvict) {
408 : // Check that we can trim the mMoofParser. We can only do so if all
409 : // iterators have demuxed all possible samples.
410 0 : for (const SampleIterator* iterator : mIterators) {
411 0 : if ((iterator->mCurrentSample == 0 && iterator->mCurrentMoof == moofs) ||
412 0 : iterator->mCurrentMoof == moofs - 1) {
413 0 : continue;
414 : }
415 0 : canEvict = false;
416 0 : break;
417 : }
418 : }
419 0 : mMoofParser->RebuildFragmentedIndex(aByteRanges, &canEvict);
420 0 : if (canEvict) {
421 : // The moofparser got trimmed. Adjust all registered iterators.
422 0 : for (SampleIterator* iterator : mIterators) {
423 0 : iterator->mCurrentMoof -= moofs - 1;
424 : }
425 : }
426 : }
427 :
428 : Microseconds
429 0 : Index::GetEndCompositionIfBuffered(const MediaByteRangeSet& aByteRanges)
430 : {
431 : FallibleTArray<Sample>* index;
432 0 : if (mMoofParser) {
433 0 : if (!mMoofParser->ReachedEnd() || mMoofParser->Moofs().IsEmpty()) {
434 0 : return 0;
435 : }
436 0 : index = &mMoofParser->Moofs().LastElement().mIndex;
437 : } else {
438 0 : index = &mIndex;
439 : }
440 :
441 0 : Microseconds lastComposition = 0;
442 0 : RangeFinder rangeFinder(aByteRanges);
443 0 : for (size_t i = index->Length(); i--;) {
444 0 : const Sample& sample = (*index)[i];
445 0 : if (!rangeFinder.Contains(sample.mByteRange)) {
446 0 : return 0;
447 : }
448 0 : lastComposition = std::max(lastComposition, sample.mCompositionRange.end);
449 0 : if (sample.mSync) {
450 0 : return lastComposition;
451 : }
452 : }
453 0 : return 0;
454 : }
455 :
456 : TimeIntervals
457 0 : Index::ConvertByteRangesToTimeRanges(const MediaByteRangeSet& aByteRanges)
458 : {
459 0 : if (aByteRanges == mLastCachedRanges) {
460 0 : return mLastBufferedRanges;
461 : }
462 0 : mLastCachedRanges = aByteRanges;
463 :
464 0 : if (mDataOffset.Length()) {
465 0 : TimeIntervals timeRanges;
466 0 : for (const auto& range : aByteRanges) {
467 0 : uint32_t start = mDataOffset.IndexOfFirstElementGt(range.mStart - 1);
468 0 : if (!mIsAudio && start == mDataOffset.Length()) {
469 0 : continue;
470 : }
471 0 : uint32_t end = mDataOffset.IndexOfFirstElementGt(range.mEnd, MP4DataOffset::EndOffsetComparator());
472 0 : if (!mIsAudio && end < start) {
473 0 : continue;
474 : }
475 0 : if (mIsAudio && start &&
476 0 : range.Intersects(MediaByteRange(mDataOffset[start-1].mStartOffset,
477 0 : mDataOffset[start-1].mEndOffset))) {
478 : // Check if previous audio data block contains some available samples.
479 0 : for (size_t i = mDataOffset[start-1].mIndex; i < mIndex.Length(); i++) {
480 0 : if (range.ContainsStrict(mIndex[i].mByteRange)) {
481 : timeRanges +=
482 0 : TimeInterval(TimeUnit::FromMicroseconds(mIndex[i].mCompositionRange.start),
483 0 : TimeUnit::FromMicroseconds(mIndex[i].mCompositionRange.end));
484 : }
485 : }
486 : }
487 0 : if (end > start) {
488 : timeRanges +=
489 0 : TimeInterval(TimeUnit::FromMicroseconds(mDataOffset[start].mTime.start),
490 0 : TimeUnit::FromMicroseconds(mDataOffset[end-1].mTime.end));
491 : }
492 0 : if (end < mDataOffset.Length()) {
493 : // Find samples in partial block contained in the byte range.
494 0 : for (size_t i = mDataOffset[end].mIndex;
495 0 : i < mIndex.Length() && range.ContainsStrict(mIndex[i].mByteRange);
496 : i++) {
497 : timeRanges +=
498 0 : TimeInterval(TimeUnit::FromMicroseconds(mIndex[i].mCompositionRange.start),
499 0 : TimeUnit::FromMicroseconds(mIndex[i].mCompositionRange.end));
500 : }
501 : }
502 : }
503 0 : mLastBufferedRanges = timeRanges;
504 0 : return timeRanges;
505 : }
506 :
507 0 : RangeFinder rangeFinder(aByteRanges);
508 0 : nsTArray<Interval<Microseconds>> timeRanges;
509 0 : nsTArray<FallibleTArray<Sample>*> indexes;
510 0 : if (mMoofParser) {
511 : // We take the index out of the moof parser and move it into a local
512 : // variable so we don't get concurrency issues. It gets freed when we
513 : // exit this function.
514 0 : for (int i = 0; i < mMoofParser->Moofs().Length(); i++) {
515 0 : Moof& moof = mMoofParser->Moofs()[i];
516 :
517 : // We need the entire moof in order to play anything
518 0 : if (rangeFinder.Contains(moof.mRange)) {
519 0 : if (rangeFinder.Contains(moof.mMdatRange)) {
520 0 : Interval<Microseconds>::SemiNormalAppend(timeRanges, moof.mTimeRange);
521 : } else {
522 0 : indexes.AppendElement(&moof.mIndex);
523 : }
524 : }
525 : }
526 : } else {
527 0 : indexes.AppendElement(&mIndex);
528 : }
529 :
530 0 : bool hasSync = false;
531 0 : for (size_t i = 0; i < indexes.Length(); i++) {
532 0 : FallibleTArray<Sample>* index = indexes[i];
533 0 : for (size_t j = 0; j < index->Length(); j++) {
534 0 : const Sample& sample = (*index)[j];
535 0 : if (!rangeFinder.Contains(sample.mByteRange)) {
536 : // We process the index in decode order so we clear hasSync when we hit
537 : // a range that isn't buffered.
538 0 : hasSync = false;
539 0 : continue;
540 : }
541 :
542 0 : hasSync |= sample.mSync;
543 0 : if (!hasSync) {
544 0 : continue;
545 : }
546 :
547 : Interval<Microseconds>::SemiNormalAppend(timeRanges,
548 0 : sample.mCompositionRange);
549 : }
550 : }
551 :
552 : // This fixes up when the compositon order differs from the byte range order
553 0 : nsTArray<Interval<Microseconds>> timeRangesNormalized;
554 0 : Interval<Microseconds>::Normalize(timeRanges, &timeRangesNormalized);
555 : // convert timeRanges.
556 0 : media::TimeIntervals ranges;
557 0 : for (size_t i = 0; i < timeRangesNormalized.Length(); i++) {
558 : ranges +=
559 0 : media::TimeInterval(media::TimeUnit::FromMicroseconds(timeRangesNormalized[i].start),
560 0 : media::TimeUnit::FromMicroseconds(timeRangesNormalized[i].end));
561 : }
562 0 : mLastBufferedRanges = ranges;
563 0 : return ranges;
564 : }
565 :
566 : uint64_t
567 0 : Index::GetEvictionOffset(Microseconds aTime)
568 : {
569 0 : uint64_t offset = std::numeric_limits<uint64_t>::max();
570 0 : if (mMoofParser) {
571 : // We need to keep the whole moof if we're keeping any of it because the
572 : // parser doesn't keep parsed moofs.
573 0 : for (int i = 0; i < mMoofParser->Moofs().Length(); i++) {
574 0 : Moof& moof = mMoofParser->Moofs()[i];
575 :
576 0 : if (moof.mTimeRange.Length() && moof.mTimeRange.end > aTime) {
577 0 : offset = std::min(offset, uint64_t(std::min(moof.mRange.mStart,
578 0 : moof.mMdatRange.mStart)));
579 : }
580 : }
581 : } else {
582 : // We've already parsed and stored the moov so we don't need to keep it.
583 : // All we need to keep is the sample data itself.
584 0 : for (size_t i = 0; i < mIndex.Length(); i++) {
585 0 : const Sample& sample = mIndex[i];
586 0 : if (aTime >= sample.mCompositionRange.end) {
587 0 : offset = std::min(offset, uint64_t(sample.mByteRange.mEnd));
588 : }
589 : }
590 : }
591 0 : return offset;
592 : }
593 :
594 : void
595 0 : Index::RegisterIterator(SampleIterator* aIterator)
596 : {
597 0 : mIterators.AppendElement(aIterator);
598 0 : }
599 :
600 : void
601 0 : Index::UnregisterIterator(SampleIterator* aIterator)
602 : {
603 0 : mIterators.RemoveElement(aIterator);
604 0 : }
605 :
606 : }
|