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
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
|
/* -*- 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/. */
#ifndef MediaCache_h_
#define MediaCache_h_
#include "DecoderDoctorLogger.h"
#include "Intervals.h"
#include "mozilla/Monitor.h"
#include "mozilla/Result.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/dom/MediaDebugInfoBinding.h"
#include "nsCOMPtr.h"
#include "nsHashKeys.h"
#include "nsTArray.h"
#include "nsTHashtable.h"
#include "MediaChannelStatistics.h"
class nsIEventTarget;
class nsIPrincipal;
namespace mozilla {
// defined in MediaResource.h
class ChannelMediaResource;
typedef media::IntervalSet<int64_t> MediaByteRangeSet;
class MediaResource;
/**
* Media applications want fast, "on demand" random access to media data,
* for pausing, seeking, etc. But we are primarily interested
* in transporting media data using HTTP over the Internet, which has
* high latency to open a connection, requires a new connection for every
* seek, may not even support seeking on some connections (especially
* live streams), and uses a push model --- data comes from the server
* and you don't have much control over the rate. Also, transferring data
* over the Internet can be slow and/or unpredictable, so we want to read
* ahead to buffer and cache as much data as possible.
*
* The job of the media cache is to resolve this impedance mismatch.
* The media cache reads data from Necko channels into file-backed storage,
* and offers a random-access file-like API to the stream data
* (MediaCacheStream). Along the way it solves several problems:
* -- The cache intelligently reads ahead to prefetch data that may be
* needed in the future
* -- The size of the cache is bounded so that we don't fill up
* storage with read-ahead data
* -- Cache replacement is managed globally so that the most valuable
* data (across all streams) is retained
* -- The cache can suspend Necko channels temporarily when their data is
* not wanted (yet)
* -- The cache translates file-like seek requests to HTTP seeks,
* including optimizations like not triggering a new seek if it would
* be faster to just keep reading until we reach the seek point. The
* "seek to EOF" idiom to determine file size is also handled efficiently
* (seeking to EOF and then seeking back to the previous offset does not
* trigger any Necko activity)
* -- The cache also handles the case where the server does not support
* seeking
* -- Necko can only send data to the main thread, but MediaCacheStream
* can distribute data to any thread
* -- The cache exposes APIs so clients can detect what data is
* currently held
*
* Note that although HTTP is the most important transport and we only
* support transport-level seeking via HTTP byte-ranges, the media cache
* works with any kind of Necko channels and provides random access to
* cached data even for, e.g., FTP streams.
*
* The media cache is not persistent. It does not currently allow
* data from one load to be used by other loads, either within the same
* browser session or across browser sessions. The media cache file
* is marked "delete on close" so it will automatically disappear in the
* event of a browser crash or shutdown.
*
* The media cache is block-based. Streams are divided into blocks of a
* fixed size (currently 4K) and we cache blocks. A single cache contains
* blocks for all streams.
*
* The cache size is controlled by the media.cache_size preference
* (which is in KB). The default size is 500MB.
*
* The replacement policy predicts a "time of next use" for each block
* in the cache. When we need to free a block, the block with the latest
* "time of next use" will be evicted. Blocks are divided into
* different classes, each class having its own predictor:
* FREE_BLOCK: these blocks are effectively infinitely far in the future;
* a free block will always be chosen for replacement before other classes
* of blocks.
* METADATA_BLOCK: these are blocks that contain data that has been read
* by the decoder in "metadata mode", e.g. while the decoder is searching
* the stream during a seek operation. These blocks are managed with an
* LRU policy; the "time of next use" is predicted to be as far in the
* future as the last use was in the past.
* PLAYED_BLOCK: these are blocks that have not been read in "metadata
* mode", and contain data behind the current decoder read point. (They
* may not actually have been read by the decoder, if the decoder seeked
* forward.) These blocks are managed with an LRU policy except that we add
* REPLAY_DELAY seconds of penalty to their predicted "time of next use",
* to reflect the uncertainty about whether replay will actually happen
* or not.
* READAHEAD_BLOCK: these are blocks that have not been read in
* "metadata mode" and that are entirely ahead of the current decoder
* read point. (They may actually have been read by the decoder in the
* past if the decoder has since seeked backward.) We predict the
* time of next use for these blocks by assuming steady playback and
* dividing the number of bytes between the block and the current decoder
* read point by the decoder's estimate of its playback rate in bytes
* per second. This ensures that the blocks farthest ahead are considered
* least valuable.
* For efficient prediction of the "latest time of next use", we maintain
* linked lists of blocks in each class, ordering blocks by time of
* next use. READAHEAD_BLOCKS have one linked list per stream, since their
* time of next use depends on stream parameters, but the other lists
* are global.
*
* A block containing a current decoder read point can contain data
* both behind and ahead of the read point. It will be classified as a
* PLAYED_BLOCK but we will give it special treatment so it is never
* evicted --- it actually contains the highest-priority readahead data
* as well as played data.
*
* "Time of next use" estimates are also used for flow control. When
* reading ahead we can predict the time of next use for the data that
* will be read. If the predicted time of next use is later then the
* prediction for all currently cached blocks, and the cache is full, then
* we should suspend reading from the Necko channel.
*
* Unfortunately suspending the Necko channel can't immediately stop the
* flow of data from the server. First our desire to suspend has to be
* transmitted to the server (in practice, Necko stops reading from the
* socket, which causes the kernel to shrink its advertised TCP receive
* window size to zero). Then the server can stop sending the data, but
* we will receive data roughly corresponding to the product of the link
* bandwidth multiplied by the round-trip latency. We deal with this by
* letting the cache overflow temporarily and then trimming it back by
* moving overflowing blocks back into the body of the cache, replacing
* less valuable blocks as they become available. We try to avoid simply
* discarding overflowing readahead data.
*
* All changes to the actual contents of the cache happen on the main
* thread, since that's where Necko's notifications happen.
*
* The media cache maintains at most one Necko channel for each stream.
* (In the future it might be advantageous to relax this, e.g. so that a
* seek to near the end of the file can happen without disturbing
* the loading of data from the beginning of the file.) The Necko channel
* is managed through ChannelMediaResource; MediaCache does not
* depend on Necko directly.
*
* Every time something changes that might affect whether we want to
* read from a Necko channel, or whether we want to seek on the Necko
* channel --- such as data arriving or data being consumed by the
* decoder --- we asynchronously trigger MediaCache::Update on the main
* thread. That method implements most cache policy. It evaluates for
* each stream whether we want to suspend or resume the stream and what
* offset we should seek to, if any. It is also responsible for trimming
* back the cache size to its desired limit by moving overflowing blocks
* into the main part of the cache.
*
* Streams can be opened in non-seekable mode. In non-seekable mode,
* the cache will only call ChannelMediaResource::CacheClientSeek with
* a 0 offset. The cache tries hard not to discard readahead data
* for non-seekable streams, since that could trigger a potentially
* disastrous re-read of the entire stream. It's up to cache clients
* to try to avoid requesting seeks on such streams.
*
* MediaCache has a single internal monitor for all synchronization.
* This is treated as the lowest level monitor in the media code. So,
* we must not acquire any MediaDecoder locks or MediaResource locks
* while holding the MediaCache lock. But it's OK to hold those locks
* and then get the MediaCache lock.
*
* MediaCache associates a principal with each stream. CacheClientSeek
* can trigger new HTTP requests; due to redirects to other domains,
* each HTTP load can return data with a different principal. This
* principal must be passed to NotifyDataReceived, and MediaCache
* will detect when different principals are associated with data in the
* same stream, and replace them with a null principal.
*/
class MediaCache;
DDLoggedTypeDeclName(MediaCacheStream);
/**
* If the cache fails to initialize then Init will fail, so nonstatic
* methods of this class can assume gMediaCache is non-null.
*
* This class can be directly embedded as a value.
*/
class MediaCacheStream : public DecoderDoctorLifeLogger<MediaCacheStream> {
using AutoLock = MonitorAutoLock;
public:
// This needs to be a power of two
static const int64_t BLOCK_SIZE = 32768;
enum ReadMode { MODE_METADATA, MODE_PLAYBACK };
// aClient provides the underlying transport that cache will use to read
// data for this stream.
MediaCacheStream(ChannelMediaResource* aClient, bool aIsPrivateBrowsing);
~MediaCacheStream();
// Set up this stream with the cache. Can fail on OOM.
// aContentLength is the content length if known, otherwise -1.
// Exactly one of InitAsClone or Init must be called before any other method
// on this class. Does nothing if already initialized.
nsresult Init(int64_t aContentLength);
// Set up this stream with the cache, assuming it's for the same data
// as the aOriginal stream.
// Exactly one of InitAsClone or Init must be called before any other method
// on this class.
void InitAsClone(MediaCacheStream* aOriginal);
nsISerialEventTarget* OwnerThread() const;
// These are called on the main thread.
// This must be called (and resolve) before the ChannelMediaResource
// used to create this MediaCacheStream is deleted.
RefPtr<GenericPromise> Close();
// This returns true when the stream has been closed.
bool IsClosed(AutoLock&) const { return mClosed; }
// Returns true when this stream is can be shared by a new resource load.
// Called on the main thread only.
bool IsAvailableForSharing() const { return !mIsPrivateBrowsing; }
// These callbacks are called on the main thread by the client
// when data has been received via the channel.
// Notifies the cache that a load has begun. We pass the offset
// because in some cases the offset might not be what the cache
// requested. In particular we might unexpectedly start providing
// data at offset 0. This need not be called if the offset is the
// offset that the cache requested in
// ChannelMediaResource::CacheClientSeek. This can be called at any
// time by the client, not just after a CacheClientSeek.
//
// aSeekable tells us whether the stream is seekable or not. Non-seekable
// streams will always pass 0 for aOffset to CacheClientSeek. This should only
// be called while the stream is at channel offset 0. Seekability can
// change during the lifetime of the MediaCacheStream --- every time
// we do an HTTP load the seekability may be different (and sometimes
// is, in practice, due to the effects of caching proxies).
//
// aLength tells the cache what the server said the data length is going to
// be. The actual data length may be greater (we receive more data than
// specified) or smaller (the stream ends before we reach the given
// length), because servers can lie. The server's reported data length
// *and* the actual data length can even vary over time because a
// misbehaving server may feed us a different stream after each seek
// operation. So this is really just a hint. The cache may however
// stop reading (suspend the channel) when it thinks we've read all the
// data available based on an incorrect reported length. Seeks relative
// EOF also depend on the reported length if we haven't managed to
// read the whole stream yet.
void NotifyDataStarted(uint32_t aLoadID, int64_t aOffset, bool aSeekable,
int64_t aLength);
// Notifies the cache that data has been received. The stream already
// knows the offset because data is received in sequence and
// the starting offset is known via NotifyDataStarted or because
// the cache requested the offset in
// ChannelMediaResource::CacheClientSeek, or because it defaulted to 0.
void NotifyDataReceived(uint32_t aLoadID, uint32_t aCount,
const uint8_t* aData);
// Set the load ID so the following NotifyDataEnded() call can work properly.
// Used in some rare cases where NotifyDataEnded() is called without the
// preceding NotifyDataStarted().
void NotifyLoadID(uint32_t aLoadID);
// Notifies the cache that the channel has closed with the given status.
void NotifyDataEnded(uint32_t aLoadID, nsresult aStatus);
// Notifies the stream that the suspend status of the client has changed.
// Main thread only.
void NotifyClientSuspended(bool aSuspended);
// Notifies the stream to resume download at the current offset.
void NotifyResume();
// These methods can be called on any thread.
// Cached blocks associated with this stream will not be evicted
// while the stream is pinned.
void Pin();
void Unpin();
// See comments above for NotifyDataStarted about how the length
// can vary over time. Returns -1 if no length is known. Returns the
// reported length if we haven't got any better information. If
// the stream ended normally we return the length we actually got.
// If we've successfully read data beyond the originally reported length,
// we return the end of the data we've read.
int64_t GetLength() const;
// Return the length and offset where next channel data will write to. Main
// thread only.
// This method should be removed as part of bug 1464045.
struct LengthAndOffset {
int64_t mLength;
int64_t mOffset;
};
LengthAndOffset GetLengthAndOffset() const;
// Returns the unique resource ID. Call only on the main thread or while
// holding the media cache lock.
int64_t GetResourceID() { return mResourceID; }
// Returns the end of the bytes starting at the given offset
// which are in cache.
int64_t GetCachedDataEnd(int64_t aOffset);
// Returns the offset of the first byte of cached data at or after aOffset,
// or -1 if there is no such cached data.
int64_t GetNextCachedData(int64_t aOffset);
// Fills aRanges with the ByteRanges representing the data which is currently
// cached. Locks the media cache while running, to prevent any ranges
// growing. The stream should be pinned while this runs and while its results
// are used, to ensure no data is evicted.
nsresult GetCachedRanges(MediaByteRangeSet& aRanges);
double GetDownloadRate(bool* aIsReliable);
// Reads from buffered data only. Will fail if not all data to be read is
// in the cache. Will not mark blocks as read. Can be called from the main
// thread. It's the caller's responsibility to wrap the call in a pin/unpin,
// and also to check that the range they want is cached before calling this.
nsresult ReadFromCache(char* aBuffer, int64_t aOffset, uint32_t aCount);
// IsDataCachedToEndOfStream returns true if all the data from
// aOffset to the end of the stream (the server-reported end, if the
// real end is not known) is in cache. If we know nothing about the
// end of the stream, this returns false.
bool IsDataCachedToEndOfStream(int64_t aOffset);
// The mode is initially MODE_METADATA.
void SetReadMode(ReadMode aMode);
// This is the client's estimate of the playback rate assuming
// the media plays continuously. The cache can't guess this itself
// because it doesn't know when the decoder was paused, buffering, etc.
// Do not pass zero.
void SetPlaybackRate(uint32_t aBytesPerSecond);
// Returns true when all streams for this resource are suspended or their
// channel has ended.
bool AreAllStreamsForResourceSuspended(AutoLock&);
// These methods must be called on a different thread from the main
// thread. They should always be called on the same thread for a given
// stream.
// *aBytes gets the number of bytes that were actually read. This can
// be less than aCount. If the first byte of data is not in the cache,
// this will block until the data is available or the stream is
// closed, otherwise it won't block.
nsresult Read(AutoLock&, char* aBuffer, uint32_t aCount, uint32_t* aBytes);
// Seeks to aOffset in the stream then performs a Read operation. See
// 'Read' for argument and return details.
nsresult ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount,
uint32_t* aBytes);
void ThrottleReadahead(bool bThrottle);
size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const;
void GetDebugInfo(dom::MediaCacheStreamDebugInfo& aInfo);
private:
friend class MediaCache;
/**
* A doubly-linked list of blocks. Add/Remove/Get methods are all
* constant time. We declare this here so that a stream can contain a
* BlockList of its read-ahead blocks. Blocks are referred to by index
* into the MediaCache::mIndex array.
*
* Blocks can belong to more than one list at the same time, because
* the next/prev pointers are not stored in the block.
*/
class BlockList {
public:
BlockList() : mFirstBlock(-1), mCount(0) {}
~BlockList() {
NS_ASSERTION(mFirstBlock == -1 && mCount == 0,
"Destroying non-empty block list");
}
void AddFirstBlock(int32_t aBlock);
void AddAfter(int32_t aBlock, int32_t aBefore);
void RemoveBlock(int32_t aBlock);
// Returns the first block in the list, or -1 if empty
int32_t GetFirstBlock() const { return mFirstBlock; }
// Returns the last block in the list, or -1 if empty
int32_t GetLastBlock() const;
// Returns the next block in the list after aBlock or -1 if
// aBlock is the last block
int32_t GetNextBlock(int32_t aBlock) const;
// Returns the previous block in the list before aBlock or -1 if
// aBlock is the first block
int32_t GetPrevBlock(int32_t aBlock) const;
bool IsEmpty() const { return mFirstBlock < 0; }
int32_t GetCount() const { return mCount; }
// The contents of aBlockIndex1 and aBlockIndex2 have been swapped
void NotifyBlockSwapped(int32_t aBlockIndex1, int32_t aBlockIndex2);
#ifdef DEBUG
// Verify linked-list invariants
void Verify();
#else
void Verify() {}
#endif
size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
private:
struct Entry : public nsUint32HashKey {
explicit Entry(KeyTypePointer aKey)
: nsUint32HashKey(aKey), mNextBlock(0), mPrevBlock(0) {}
Entry(const Entry& toCopy)
: nsUint32HashKey(&toCopy.GetKey()),
mNextBlock(toCopy.mNextBlock),
mPrevBlock(toCopy.mPrevBlock) {}
int32_t mNextBlock;
int32_t mPrevBlock;
};
nsTHashtable<Entry> mEntries;
// The index of the first block in the list, or -1 if the list is empty.
int32_t mFirstBlock;
// The number of blocks in the list.
int32_t mCount;
};
// Read data from the partial block and return the number of bytes read
// successfully. 0 if aOffset is not an offset in the partial block or there
// is nothing to read.
uint32_t ReadPartialBlock(AutoLock&, int64_t aOffset, Span<char> aBuffer);
// Read data from the cache block specified by aOffset. Return the number of
// bytes read successfully or an error code if any failure.
Result<uint32_t, nsresult> ReadBlockFromCache(AutoLock&, int64_t aOffset,
Span<char> aBuffer,
bool aNoteBlockUsage = false);
// Non-main thread only.
nsresult Seek(AutoLock&, int64_t aOffset);
// Returns the end of the bytes starting at the given offset
// which are in cache.
// This method assumes that the cache monitor is held and can be called on
// any thread.
int64_t GetCachedDataEndInternal(AutoLock&, int64_t aOffset);
// Returns the offset of the first byte of cached data at or after aOffset,
// or -1 if there is no such cached data.
// This method assumes that the cache monitor is held and can be called on
// any thread.
int64_t GetNextCachedDataInternal(AutoLock&, int64_t aOffset);
// Used by |NotifyDataEnded| to write |mPartialBlock| to disk.
// If |aNotifyAll| is true, this function will wake up readers who may be
// waiting on the media cache monitor. Called on the media cache thread only.
void FlushPartialBlockInternal(AutoLock&, bool aNotifyAll);
void NotifyDataStartedInternal(uint32_t aLoadID, int64_t aOffset,
bool aSeekable, int64_t aLength);
void NotifyDataEndedInternal(uint32_t aLoadID, nsresult aStatus);
void UpdateDownloadStatistics(AutoLock&);
void CloseInternal(AutoLock&);
void InitAsCloneInternal(MediaCacheStream* aOriginal);
// Instance of MediaCache to use with this MediaCacheStream.
RefPtr<MediaCache> mMediaCache;
ChannelMediaResource* const mClient;
// The following fields must be written holding the cache's monitor and
// only on the main thread, thus can be read either on the main thread
// or while holding the cache's monitor.
// Set to true when the stream has been closed either explicitly or
// due to an internal cache error
bool mClosed = false;
// This is a unique ID representing the resource we're loading.
// All streams with the same mResourceID are loading the same
// underlying resource and should share data.
// Initialized to 0 as invalid. Will be allocated a valid ID (always positive)
// from the cache.
int64_t mResourceID = 0;
// The last reported seekability state for the underlying channel
bool mIsTransportSeekable;
// True if the cache has suspended our channel because the cache is
// full and the priority of the data that would be received is lower
// than the priority of the data already in the cache
bool mCacheSuspended;
// True if the channel ended and we haven't seeked it again.
bool mChannelEnded;
// The following fields are protected by the cache's monitor and can be
// written by any thread.
// The reported or discovered length of the data, or -1 if nothing is known
int64_t mStreamLength = -1;
// The offset where the next data from the channel will arrive
int64_t mChannelOffset = 0;
// The offset where the reader is positioned in the stream
int64_t mStreamOffset;
// For each block in the stream data, maps to the cache entry for the
// block, or -1 if the block is not cached.
nsTArray<int32_t> mBlocks;
// The list of read-ahead blocks, ordered by stream offset; the first
// block is the earliest in the stream (so the last block will be the
// least valuable).
BlockList mReadaheadBlocks;
// The list of metadata blocks; the first block is the most recently used
BlockList mMetadataBlocks;
// The list of played-back blocks; the first block is the most recently used
BlockList mPlayedBlocks;
// The last reported estimate of the decoder's playback rate
uint32_t mPlaybackBytesPerSecond;
// The number of times this stream has been Pinned without a
// corresponding Unpin
uint32_t mPinCount;
// True if CacheClientNotifyDataEnded has been called for this stream.
bool mDidNotifyDataEnded = false;
// The status used when we did CacheClientNotifyDataEnded. Only valid
// when mDidNotifyDataEnded is true.
nsresult mNotifyDataEndedStatus;
// The last reported read mode
ReadMode mCurrentMode = MODE_METADATA;
// The load ID of the current channel. Used to check whether the data is
// coming from an old channel and should be discarded.
uint32_t mLoadID = 0;
// The seek target initiated by MediaCache. -1 if no seek is going on.
int64_t mSeekTarget = -1;
bool mThrottleReadahead = false;
// Data received for the block containing mChannelOffset. Data needs
// to wait here so we can write back a complete block. The first
// mChannelOffset%BLOCK_SIZE bytes have been filled in with good data,
// the rest are garbage.
// Heap allocate this buffer since the exact power-of-2 will cause allocation
// slop when combined with the rest of the object members.
// This partial buffer should always be read/write within the cache's monitor.
const UniquePtr<uint8_t[]> mPartialBlockBuffer =
MakeUnique<uint8_t[]>(BLOCK_SIZE);
// True if associated with a private browsing window.
const bool mIsPrivateBrowsing;
// True if the client is suspended. Accessed on the owner thread only.
bool mClientSuspended = false;
MediaChannelStatistics mDownloadStatistics;
};
} // namespace mozilla
#endif
|