summaryrefslogtreecommitdiffstats
path: root/dom/media/MediaResource.cpp
blob: feb0b130bf706196344a10d6a19aa47428824c04 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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 "MediaResource.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/Logging.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/ErrorNames.h"
#include "mozilla/SchedulerGroup.h"

using mozilla::media::TimeUnit;

#undef ILOG

mozilla::LazyLogModule gMediaResourceIndexLog("MediaResourceIndex");
// Debug logging macro with object pointer and class name.
#define ILOG(msg, ...)                                             \
  DDMOZ_LOG(gMediaResourceIndexLog, mozilla::LogLevel::Debug, msg, \
            ##__VA_ARGS__)

namespace mozilla {

static const uint32_t kMediaResourceIndexCacheSize = 8192;
static_assert(IsPowerOfTwo(kMediaResourceIndexCacheSize),
              "kMediaResourceIndexCacheSize cache size must be a power of 2");

MediaResourceIndex::MediaResourceIndex(MediaResource* aResource)
    : mResource(aResource),
      mOffset(0),
      mCacheBlockSize(
          aResource->ShouldCacheReads() ? kMediaResourceIndexCacheSize : 0),
      mCachedOffset(0),
      mCachedBytes(0),
      mCachedBlock(MakeUnique<char[]>(mCacheBlockSize)) {
  DDLINKCHILD("resource", aResource);
}

nsresult MediaResourceIndex::Read(char* aBuffer, uint32_t aCount,
                                  uint32_t* aBytes) {
  NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread");

  // We purposefuly don't check that we may attempt to read past
  // mResource->GetLength() as the resource's length may change over time.

  nsresult rv = ReadAt(mOffset, aBuffer, aCount, aBytes);
  if (NS_FAILED(rv)) {
    return rv;
  }
  mOffset += *aBytes;
  if (mOffset < 0) {
    // Very unlikely overflow; just return to position 0.
    mOffset = 0;
  }
  return NS_OK;
}

static nsCString ResultName(nsresult aResult) {
  nsCString name;
  GetErrorName(aResult, name);
  return name;
}

nsresult MediaResourceIndex::ReadAt(int64_t aOffset, char* aBuffer,
                                    uint32_t aCount, uint32_t* aBytes) {
  if (mCacheBlockSize == 0) {
    return UncachedReadAt(aOffset, aBuffer, aCount, aBytes);
  }

  *aBytes = 0;

  if (aCount == 0) {
    return NS_OK;
  }

  const int64_t endOffset = aOffset + aCount;
  if (aOffset < 0 || endOffset < aOffset) {
    return NS_ERROR_ILLEGAL_VALUE;
  }

  const int64_t lastBlockOffset = CacheOffsetContaining(endOffset - 1);

  if (mCachedBytes != 0 && mCachedOffset + mCachedBytes >= aOffset &&
      mCachedOffset < endOffset) {
    // There is data in the cache that is not completely before aOffset and not
    // completely after endOffset, so it could be usable (with potential
    // top-up).
    if (aOffset < mCachedOffset) {
      // We need to read before the cached data.
      const uint32_t toRead = uint32_t(mCachedOffset - aOffset);
      MOZ_ASSERT(toRead > 0);
      MOZ_ASSERT(toRead < aCount);
      uint32_t read = 0;
      nsresult rv = UncachedReadAt(aOffset, aBuffer, toRead, &read);
      if (NS_FAILED(rv)) {
        ILOG("ReadAt(%" PRIu32 "@%" PRId64
             ") uncached read before cache -> %s, %" PRIu32,
             aCount, aOffset, ResultName(rv).get(), *aBytes);
        return rv;
      }
      *aBytes = read;
      if (read < toRead) {
        // Could not read everything we wanted, we're done.
        ILOG("ReadAt(%" PRIu32 "@%" PRId64
             ") uncached read before cache, incomplete -> OK, %" PRIu32,
             aCount, aOffset, *aBytes);
        return NS_OK;
      }
      ILOG("ReadAt(%" PRIu32 "@%" PRId64
           ") uncached read before cache: %" PRIu32 ", remaining: %" PRIu32
           "@%" PRId64 "...",
           aCount, aOffset, read, aCount - read, aOffset + read);
      aOffset += read;
      aBuffer += read;
      aCount -= read;
      // We should have reached the cache.
      MOZ_ASSERT(aOffset == mCachedOffset);
    }
    MOZ_ASSERT(aOffset >= mCachedOffset);

    // We've reached our cache.
    const uint32_t toCopy =
        std::min(aCount, uint32_t(mCachedOffset + mCachedBytes - aOffset));
    // Note that we could in fact be just after the last byte of the cache, in
    // which case we can't actually read from it! (But we will top-up next.)
    if (toCopy != 0) {
      memcpy(aBuffer, &mCachedBlock[IndexInCache(aOffset)], toCopy);
      *aBytes += toCopy;
      aCount -= toCopy;
      if (aCount == 0) {
        // All done!
        ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") copied everything (%" PRIu32
             ") from cache(%" PRIu32 "@%" PRId64 ") :-D -> OK, %" PRIu32,
             aCount, aOffset, toCopy, mCachedBytes, mCachedOffset, *aBytes);
        return NS_OK;
      }
      aOffset += toCopy;
      aBuffer += toCopy;
      ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") copied %" PRIu32
           " from cache(%" PRIu32 "@%" PRId64 ") :-), remaining: %" PRIu32
           "@%" PRId64 "...",
           aCount + toCopy, aOffset - toCopy, toCopy, mCachedBytes,
           mCachedOffset, aCount, aOffset);
    }

    if (aOffset - 1 >= lastBlockOffset) {
      // We were already reading cached data from the last block, we need more
      // from it -> try to top-up, read what we can, and we'll be done.
      MOZ_ASSERT(aOffset == mCachedOffset + mCachedBytes);
      MOZ_ASSERT(endOffset <= lastBlockOffset + mCacheBlockSize);
      return CacheOrReadAt(aOffset, aBuffer, aCount, aBytes);
    }

    // We were not in the last block (but we may just have crossed the line now)
    MOZ_ASSERT(aOffset <= lastBlockOffset);
    // Continue below...
  } else if (aOffset >= lastBlockOffset) {
    // There was nothing we could get from the cache.
    // But we're already in the last block -> Cache or read what we can.
    // Make sure to invalidate the cache first.
    mCachedBytes = 0;
    return CacheOrReadAt(aOffset, aBuffer, aCount, aBytes);
  }

  // If we're here, either there was nothing usable in the cache, or we've just
  // read what was in the cache but there's still more to read.

  if (aOffset < lastBlockOffset) {
    // We need to read before the last block.
    // Start with an uncached read up to the last block.
    const uint32_t toRead = uint32_t(lastBlockOffset - aOffset);
    MOZ_ASSERT(toRead > 0);
    MOZ_ASSERT(toRead < aCount);
    uint32_t read = 0;
    nsresult rv = UncachedReadAt(aOffset, aBuffer, toRead, &read);
    if (NS_FAILED(rv)) {
      ILOG("ReadAt(%" PRIu32 "@%" PRId64
           ") uncached read before last block failed -> %s, %" PRIu32,
           aCount, aOffset, ResultName(rv).get(), *aBytes);
      return rv;
    }
    if (read == 0) {
      ILOG("ReadAt(%" PRIu32 "@%" PRId64
           ") uncached read 0 before last block -> OK, %" PRIu32,
           aCount, aOffset, *aBytes);
      return NS_OK;
    }
    *aBytes += read;
    if (read < toRead) {
      // Could not read everything we wanted, we're done.
      ILOG("ReadAt(%" PRIu32 "@%" PRId64
           ") uncached read before last block, incomplete -> OK, %" PRIu32,
           aCount, aOffset, *aBytes);
      return NS_OK;
    }
    ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") read %" PRIu32
         " before last block, remaining: %" PRIu32 "@%" PRId64 "...",
         aCount, aOffset, read, aCount - read, aOffset + read);
    aOffset += read;
    aBuffer += read;
    aCount -= read;
  }

  // We should just have reached the start of the last block.
  MOZ_ASSERT(aOffset == lastBlockOffset);
  MOZ_ASSERT(aCount <= mCacheBlockSize);
  // Make sure to invalidate the cache first.
  mCachedBytes = 0;
  return CacheOrReadAt(aOffset, aBuffer, aCount, aBytes);
}

nsresult MediaResourceIndex::CacheOrReadAt(int64_t aOffset, char* aBuffer,
                                           uint32_t aCount, uint32_t* aBytes) {
  // We should be here because there is more data to read.
  MOZ_ASSERT(aCount > 0);
  // We should be in the last block, so we shouldn't try to read past it.
  MOZ_ASSERT(IndexInCache(aOffset) + aCount <= mCacheBlockSize);

  const int64_t length = GetLength();
  // If length is unknown (-1), look at resource-cached data.
  // If length is known and equal or greater than requested, also look at
  // resource-cached data.
  // Otherwise, if length is known but same, or less than(!?), requested, don't
  // attempt to access resource-cached data, as we're not expecting it to ever
  // be greater than the length.
  if (length < 0 || length >= aOffset + aCount) {
    // Is there cached data covering at least the requested range?
    const int64_t cachedDataEnd = mResource->GetCachedDataEnd(aOffset);
    if (cachedDataEnd >= aOffset + aCount) {
      // Try to read as much resource-cached data as can fill our local cache.
      // Assume we can read as much as is cached without blocking.
      const uint32_t cacheIndex = IndexInCache(aOffset);
      const uint32_t toRead = uint32_t(std::min(
          cachedDataEnd - aOffset, int64_t(mCacheBlockSize - cacheIndex)));
      MOZ_ASSERT(toRead >= aCount);
      uint32_t read = 0;
      // We would like `toRead` if possible, but ok with at least `aCount`.
      nsresult rv = UncachedRangedReadAt(aOffset, &mCachedBlock[cacheIndex],
                                         aCount, toRead - aCount, &read);
      if (NS_SUCCEEDED(rv)) {
        if (read == 0) {
          ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") - UncachedRangedReadAt(%" PRIu32
               "..%" PRIu32 "@%" PRId64
               ") to top-up succeeded but read nothing -> OK anyway",
               aCount, aOffset, aCount, toRead, aOffset);
          // Couldn't actually read anything, but didn't error out, so count
          // that as success.
          return NS_OK;
        }
        if (mCachedOffset + mCachedBytes == aOffset) {
          // We were topping-up the cache, just update its size.
          ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") - UncachedRangedReadAt(%" PRIu32
               "..%" PRIu32 "@%" PRId64 ") to top-up succeeded to read %" PRIu32
               "...",
               aCount, aOffset, aCount, toRead, aOffset, read);
          mCachedBytes += read;
        } else {
          // We were filling the cache from scratch, save new cache information.
          ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") - UncachedRangedReadAt(%" PRIu32
               "..%" PRIu32 "@%" PRId64
               ") to fill cache succeeded to read %" PRIu32 "...",
               aCount, aOffset, aCount, toRead, aOffset, read);
          mCachedOffset = aOffset;
          mCachedBytes = read;
        }
        // Copy relevant part into output.
        uint32_t toCopy = std::min(aCount, read);
        memcpy(aBuffer, &mCachedBlock[cacheIndex], toCopy);
        *aBytes += toCopy;
        ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") - copied %" PRIu32 "@%" PRId64
             " -> OK, %" PRIu32,
             aCount, aOffset, toCopy, aOffset, *aBytes);
        // We may not have read all that was requested, but we got everything
        // we could get, so we're done.
        return NS_OK;
      }
      ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") - UncachedRangedReadAt(%" PRIu32
           "..%" PRIu32 "@%" PRId64
           ") failed: %s, will fallback to blocking read...",
           aCount, aOffset, aCount, toRead, aOffset, ResultName(rv).get());
      // Failure during reading. Note that this may be due to the cache
      // changing between `GetCachedDataEnd` and `ReadAt`, so it's not
      // totally unexpected, just hopefully rare; but we do need to handle it.

      // Invalidate part of cache that may have been partially overridden.
      if (mCachedOffset + mCachedBytes == aOffset) {
        // We were topping-up the cache, just keep the old untouched data.
        // (i.e., nothing to do here.)
      } else {
        // We were filling the cache from scratch, invalidate cache.
        mCachedBytes = 0;
      }
    } else {
      ILOG("ReadAt(%" PRIu32 "@%" PRId64
           ") - no cached data, will fallback to blocking read...",
           aCount, aOffset);
    }
  } else {
    ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") - length is %" PRId64
         " (%s), will fallback to blocking read as the caller requested...",
         aCount, aOffset, length, length < 0 ? "unknown" : "too short!");
  }
  uint32_t read = 0;
  nsresult rv = UncachedReadAt(aOffset, aBuffer, aCount, &read);
  if (NS_SUCCEEDED(rv)) {
    *aBytes += read;
    ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") - fallback uncached read got %" PRIu32
         " bytes -> %s, %" PRIu32,
         aCount, aOffset, read, ResultName(rv).get(), *aBytes);
  } else {
    ILOG("ReadAt(%" PRIu32 "@%" PRId64
         ") - fallback uncached read failed -> %s, %" PRIu32,
         aCount, aOffset, ResultName(rv).get(), *aBytes);
  }
  return rv;
}

nsresult MediaResourceIndex::UncachedReadAt(int64_t aOffset, char* aBuffer,
                                            uint32_t aCount,
                                            uint32_t* aBytes) const {
  if (aOffset < 0) {
    return NS_ERROR_ILLEGAL_VALUE;
  }
  if (aCount == 0) {
    *aBytes = 0;
    return NS_OK;
  }
  return mResource->ReadAt(aOffset, aBuffer, aCount, aBytes);
}

nsresult MediaResourceIndex::UncachedRangedReadAt(int64_t aOffset,
                                                  char* aBuffer,
                                                  uint32_t aRequestedCount,
                                                  uint32_t aExtraCount,
                                                  uint32_t* aBytes) const {
  uint32_t count = aRequestedCount + aExtraCount;
  if (aOffset < 0 || count < aRequestedCount) {
    return NS_ERROR_ILLEGAL_VALUE;
  }
  if (count == 0) {
    *aBytes = 0;
    return NS_OK;
  }
  return mResource->ReadAt(aOffset, aBuffer, count, aBytes);
}

nsresult MediaResourceIndex::Seek(int32_t aWhence, int64_t aOffset) {
  switch (aWhence) {
    case SEEK_SET:
      break;
    case SEEK_CUR:
      aOffset += mOffset;
      break;
    case SEEK_END: {
      int64_t length = mResource->GetLength();
      if (length == -1 || length - aOffset < 0) {
        return NS_ERROR_FAILURE;
      }
      aOffset = mResource->GetLength() - aOffset;
    } break;
    default:
      return NS_ERROR_FAILURE;
  }

  if (aOffset < 0) {
    return NS_ERROR_ILLEGAL_VALUE;
  }
  mOffset = aOffset;

  return NS_OK;
}

already_AddRefed<MediaByteBuffer> MediaResourceIndex::MediaReadAt(
    int64_t aOffset, uint32_t aCount) const {
  NS_ENSURE_TRUE(aOffset >= 0, nullptr);
  RefPtr<MediaByteBuffer> bytes = new MediaByteBuffer();
  bool ok = bytes->SetLength(aCount, fallible);
  NS_ENSURE_TRUE(ok, nullptr);

  uint32_t bytesRead = 0;
  nsresult rv = mResource->ReadAt(
      aOffset, reinterpret_cast<char*>(bytes->Elements()), aCount, &bytesRead);
  NS_ENSURE_SUCCESS(rv, nullptr);

  bytes->SetLength(bytesRead);
  return bytes.forget();
}

already_AddRefed<MediaByteBuffer> MediaResourceIndex::CachedMediaReadAt(
    int64_t aOffset, uint32_t aCount) const {
  RefPtr<MediaByteBuffer> bytes = new MediaByteBuffer();
  bool ok = bytes->SetLength(aCount, fallible);
  NS_ENSURE_TRUE(ok, nullptr);
  char* curr = reinterpret_cast<char*>(bytes->Elements());
  nsresult rv = mResource->ReadFromCache(curr, aOffset, aCount);
  NS_ENSURE_SUCCESS(rv, nullptr);
  return bytes.forget();
}

// Get the length of the stream in bytes. Returns -1 if not known.
// This can change over time; after a seek operation, a misbehaving
// server may give us a resource of a different length to what it had
// reported previously --- or it may just lie in its Content-Length
// header and give us more or less data than it reported. We will adjust
// the result of GetLength to reflect the data that's actually arriving.
int64_t MediaResourceIndex::GetLength() const { return mResource->GetLength(); }

uint32_t MediaResourceIndex::IndexInCache(int64_t aOffsetInFile) const {
  const uint32_t index = uint32_t(aOffsetInFile) & (mCacheBlockSize - 1);
  MOZ_ASSERT(index == aOffsetInFile % mCacheBlockSize);
  return index;
}

int64_t MediaResourceIndex::CacheOffsetContaining(int64_t aOffsetInFile) const {
  const int64_t offset = aOffsetInFile & ~(int64_t(mCacheBlockSize) - 1);
  MOZ_ASSERT(offset == aOffsetInFile - IndexInCache(aOffsetInFile));
  return offset;
}

}  // namespace mozilla

// avoid redefined macro in unified build
#undef ILOG