Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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 */
#include "MediaCache.h"
#include "ChannelMediaResource.h"
#include "FileBlockCache.h"
#include "MediaBlockCacheBase.h"
#include "MediaResource.h"
#include "MemoryBlockCache.h"
#include "mozilla/Attributes.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/ErrorNames.h"
#include "mozilla/Logging.h"
#include "mozilla/Monitor.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/StaticPtr.h"
#include "mozilla/StaticPrefs_browser.h"
#include "mozilla/StaticPrefs_media.h"
#include "mozilla/Telemetry.h"
#include "nsContentUtils.h"
#include "nsINetworkLinkService.h"
#include "nsIObserverService.h"
#include "nsPrintfCString.h"
#include "nsProxyRelease.h"
#include "nsTHashSet.h"
#include "nsThreadUtils.h"
#include "prio.h"
#include "VideoUtils.h"
#include <algorithm>
namespace mozilla {
#undef LOG
#undef LOGI
#undef LOGE
LazyLogModule gMediaCacheLog("MediaCache");
#define LOG(...) MOZ_LOG(gMediaCacheLog, LogLevel::Debug, (__VA_ARGS__))
#define LOGI(...) MOZ_LOG(gMediaCacheLog, LogLevel::Info, (__VA_ARGS__))
#define LOGE(...) \
NS_DebugBreak(NS_DEBUG_WARNING, nsPrintfCString(__VA_ARGS__).get(), nullptr, \
__FILE__, __LINE__)
// For HTTP seeking, if number of bytes needing to be
// seeked forward is less than this value then a read is
// done rather than a byte range request.
// If we assume a 100Mbit connection, and assume reissuing an HTTP seek causes
// a delay of 200ms, then in that 200ms we could have simply read ahead 2MB. So
// setting SEEK_VS_READ_THRESHOLD to 1MB sounds reasonable.
static const int64_t SEEK_VS_READ_THRESHOLD = 1 * 1024 * 1024;
// Readahead blocks for non-seekable streams will be limited to this
// fraction of the cache space. We don't normally evict such blocks
// because replacing them requires a seek, but we need to make sure
// they don't monopolize the cache.
static const double NONSEEKABLE_READAHEAD_MAX = 0.5;
// Data N seconds before the current playback position is given the same
// priority as data REPLAY_PENALTY_FACTOR*N seconds ahead of the current
// playback position. REPLAY_PENALTY_FACTOR is greater than 1 to reflect that
// data in the past is less likely to be played again than data in the future.
// We want to give data just behind the current playback position reasonably
// high priority in case codecs need to retrieve that data (e.g. because
// tracks haven't been muxed well or are being decoded at uneven rates).
// 1/REPLAY_PENALTY_FACTOR as much data will be kept behind the
// current playback position as will be kept ahead of the current playback
// position.
static const uint32_t REPLAY_PENALTY_FACTOR = 3;
// When looking for a reusable block, scan forward this many blocks
// from the desired "best" block location to look for free blocks,
// before we resort to scanning the whole cache. The idea is to try to
// store runs of stream blocks close-to-consecutively in the cache if we
// can.
static const uint32_t FREE_BLOCK_SCAN_LIMIT = 16;
#ifdef DEBUG
// Turn this on to do very expensive cache state validation
class MediaCacheFlusher final : public nsIObserver,
public nsSupportsWeakReference {
static void RegisterMediaCache(MediaCache* aMediaCache);
static void UnregisterMediaCache(MediaCache* aMediaCache);
MediaCacheFlusher() = default;
~MediaCacheFlusher() = default;
// Singleton instance created when a first MediaCache is registered, and
// released when the last MediaCache is unregistered.
// The observer service will keep a weak reference to it, for notifications.
static StaticRefPtr<MediaCacheFlusher> gMediaCacheFlusher;
nsTArray<MediaCache*> mMediaCaches;
/* static */
StaticRefPtr<MediaCacheFlusher> MediaCacheFlusher::gMediaCacheFlusher;
NS_IMPL_ISUPPORTS(MediaCacheFlusher, nsIObserver, nsISupportsWeakReference)
/* static */
void MediaCacheFlusher::RegisterMediaCache(MediaCache* aMediaCache) {
NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
if (!gMediaCacheFlusher) {
gMediaCacheFlusher = new MediaCacheFlusher();
nsCOMPtr<nsIObserverService> observerService =
if (observerService) {
observerService->AddObserver(gMediaCacheFlusher, "last-pb-context-exited",
"cacheservice:empty-cache", true);
gMediaCacheFlusher, "contentchild:network-link-type-changed", true);
/* static */
void MediaCacheFlusher::UnregisterMediaCache(MediaCache* aMediaCache) {
NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
if (gMediaCacheFlusher->mMediaCaches.Length() == 0) {
gMediaCacheFlusher = nullptr;
class MediaCache {
using AutoLock = MonitorAutoLock;
friend class MediaCacheStream::BlockList;
typedef MediaCacheStream::BlockList BlockList;
static const int64_t BLOCK_SIZE = MediaCacheStream::BLOCK_SIZE;
// Get an instance of a MediaCache (or nullptr if initialization failed).
// aContentLength is the content length if known already, otherwise -1.
// If the length is known and considered small enough, a discrete MediaCache
// with memory backing will be given. Otherwise the one MediaCache with
// file backing will be provided.
// If aIsPrivateBrowsing is true, only initialization of a memory backed
// MediaCache will be attempted, returning nullptr if that fails.
static RefPtr<MediaCache> GetMediaCache(int64_t aContentLength,
bool aIsPrivateBrowsing);
nsISerialEventTarget* OwnerThread() const { return sThread; }
// Brutally flush the cache contents. Main thread only.
void Flush();
// Close all streams associated with private browsing windows. This will
// also remove the blocks from the cache since we don't want to leave any
// traces when PB is done.
void CloseStreamsForPrivateBrowsing();
// Cache-file access methods. These are the lowest-level cache methods.
// mMonitor must be held; these can be called on any thread.
// This can return partial reads.
// Note mMonitor will be dropped while doing IO. The caller need
// to handle changes happening when the monitor is not held.
nsresult ReadCacheFile(AutoLock&, int64_t aOffset, void* aData,
int32_t aLength, int32_t* aBytes);
// The generated IDs are always positive.
int64_t AllocateResourceID(AutoLock&) { return ++mNextResourceID; }
// mMonitor must be held, called on main thread.
// These methods are used by the stream to set up and tear down streams,
// and to handle reads and writes.
// Add aStream to the list of streams.
void OpenStream(AutoLock&, MediaCacheStream* aStream, bool aIsClone = false);
// Remove aStream from the list of streams.
void ReleaseStream(AutoLock&, MediaCacheStream* aStream);
// Free all blocks belonging to aStream.
void ReleaseStreamBlocks(AutoLock&, MediaCacheStream* aStream);
// Find a cache entry for this data, and write the data into it
void AllocateAndWriteBlock(
AutoLock&, MediaCacheStream* aStream, int32_t aStreamBlockIndex,
Span<const uint8_t> aData1,
Span<const uint8_t> aData2 = Span<const uint8_t>());
// mMonitor must be held; can be called on any thread
// Notify the cache that a seek has been requested. Some blocks may
// need to change their class between PLAYED_BLOCK and READAHEAD_BLOCK.
// This does not trigger channel seeks directly, the next Update()
// will do that if necessary. The caller will call QueueUpdate().
void NoteSeek(AutoLock&, MediaCacheStream* aStream, int64_t aOldOffset);
// Notify the cache that a block has been read from. This is used
// to update last-use times. The block may not actually have a
// cache entry yet since Read can read data from a stream's
// in-memory mPartialBlockBuffer while the block is only partly full,
// and thus hasn't yet been committed to the cache. The caller will
// call QueueUpdate().
void NoteBlockUsage(AutoLock&, MediaCacheStream* aStream, int32_t aBlockIndex,
int64_t aStreamOffset, MediaCacheStream::ReadMode aMode,
TimeStamp aNow);
// Mark aStream as having the block, adding it as an owner.
void AddBlockOwnerAsReadahead(AutoLock&, int32_t aBlockIndex,
MediaCacheStream* aStream,
int32_t aStreamBlockIndex);
// This queues a call to Update() on the media cache thread.
void QueueUpdate(AutoLock&);
// Notify all streams for the resource ID that the suspended status changed
// at the end of MediaCache::Update.
void QueueSuspendedStatusUpdate(AutoLock&, int64_t aResourceID);
// Updates the cache state asynchronously on the media cache thread:
// -- try to trim the cache back to its desired size, if necessary
// -- suspend channels that are going to read data that's lower priority
// than anything currently cached
// -- resume channels that are going to read data that's higher priority
// than something currently cached
// -- seek channels that need to seek to a new location
void Update();
// Verify invariants, especially block list invariants
void Verify(AutoLock&);
void Verify(AutoLock&) {}
mozilla::Monitor& Monitor() {
// This method should only be called outside the main thread.
// The MOZ_DIAGNOSTIC_ASSERT(!NS_IsMainThread()) assertion should be
// re-added as part of bug 1464045
return mMonitor;
// Polls whether we're on a cellular network connection, and posts a task
// to the MediaCache thread to set the value of MediaCache::sOnCellular.
// Call on main thread only.
static void UpdateOnCellular();
* An iterator that makes it easy to iterate through all streams that
* have a given resource ID and are not closed.
* Must be used while holding the media cache lock.
class ResourceStreamIterator {
ResourceStreamIterator(MediaCache* aMediaCache, int64_t aResourceID)
: mMediaCache(aMediaCache), mResourceID(aResourceID), mNext(0) {
MediaCacheStream* Next(AutoLock& aLock) {
while (mNext < mMediaCache->mStreams.Length()) {
MediaCacheStream* stream = mMediaCache->mStreams[mNext];
if (stream->GetResourceID() == mResourceID && !stream->IsClosed(aLock))
return stream;
return nullptr;
MediaCache* mMediaCache;
int64_t mResourceID;
uint32_t mNext;
explicit MediaCache(MediaBlockCacheBase* aCache)
: mMonitor("MediaCache.mMonitor"),
#ifdef DEBUG
NS_ASSERTION(NS_IsMainThread(), "Only construct MediaCache on main thread");
~MediaCache() {
NS_ASSERTION(NS_IsMainThread(), "Only destroy MediaCache on main thread");
if (this == gMediaCache) {
LOG("~MediaCache(Global file-backed MediaCache)");
// This is the file-backed MediaCache, reset the global pointer.
gMediaCache = nullptr;
} else {
LOG("~MediaCache(Memory-backed MediaCache %p)", this);
NS_ASSERTION(mStreams.IsEmpty(), "Stream(s) still open!");
NS_ASSERTION(mIndex.Length() == 0, "Blocks leaked?");
static size_t CacheSize() {
return sOnCellular ? StaticPrefs::media_cache_size_cellular()
: StaticPrefs::media_cache_size();
static size_t ReadaheadLimit() {
return sOnCellular ? StaticPrefs::media_cache_readahead_limit_cellular()
: StaticPrefs::media_cache_readahead_limit();
static size_t ResumeThreshold() {
return sOnCellular ? StaticPrefs::media_cache_resume_threshold_cellular()
: StaticPrefs::media_cache_resume_threshold();
// Find a free or reusable block and return its index. If there are no
// free blocks and no reusable blocks, add a new block to the cache
// and return it. Can return -1 on OOM.
int32_t FindBlockForIncomingData(AutoLock&, TimeStamp aNow,
MediaCacheStream* aStream,
int32_t aStreamBlockIndex);
// Find a reusable block --- a free block, if there is one, otherwise
// the reusable block with the latest predicted-next-use, or -1 if
// there aren't any freeable blocks. Only block indices less than
// aMaxSearchBlockIndex are considered. If aForStream is non-null,
// then aForStream and aForStreamBlock indicate what media data will
// be placed; FindReusableBlock will favour returning free blocks
// near other blocks for that point in the stream.
int32_t FindReusableBlock(AutoLock&, TimeStamp aNow,
MediaCacheStream* aForStream,
int32_t aForStreamBlock,
int32_t aMaxSearchBlockIndex);
bool BlockIsReusable(AutoLock&, int32_t aBlockIndex);
// Given a list of blocks sorted with the most reusable blocks at the
// end, find the last block whose stream is not pinned (if any)
// and whose cache entry index is less than aBlockIndexLimit
// and append it to aResult.
void AppendMostReusableBlock(AutoLock&, BlockList* aBlockList,
nsTArray<uint32_t>* aResult,
int32_t aBlockIndexLimit);
enum BlockClass {
// block belongs to mMetadataBlockList because data has been consumed
// from it in "metadata mode" --- in particular blocks read during
// Ogg seeks go into this class. These blocks may have played data
// in them too.
// block belongs to mPlayedBlockList because its offset is
// less than the stream's current reader position
// block belongs to the stream's mReadaheadBlockList because its
// offset is greater than or equal to the stream's current
// reader position
struct BlockOwner {
constexpr BlockOwner() = default;
// The stream that owns this block, or null if the block is free.
MediaCacheStream* mStream = nullptr;
// The block index in the stream. Valid only if mStream is non-null.
// Initialized to an insane value to highlight misuse.
uint32_t mStreamBlock = UINT32_MAX;
// Time at which this block was last used. Valid only if
TimeStamp mLastUseTime;
BlockClass mClass = READAHEAD_BLOCK;
struct Block {
// Free blocks have an empty mOwners array
nsTArray<BlockOwner> mOwners;
// Get the BlockList that the block should belong to given its
// current owner
BlockList* GetListForBlock(AutoLock&, BlockOwner* aBlock);
// Get the BlockOwner for the given block index and owning stream
// (returns null if the stream does not own the block)
BlockOwner* GetBlockOwner(AutoLock&, int32_t aBlockIndex,
MediaCacheStream* aStream);
// Returns true iff the block is free
bool IsBlockFree(int32_t aBlockIndex) {
return mIndex[aBlockIndex].mOwners.IsEmpty();
// Add the block to the free list and mark its streams as not having
// the block in cache
void FreeBlock(AutoLock&, int32_t aBlock);
// Mark aStream as not having the block, removing it as an owner. If
// the block has no more owners it's added to the free list.
void RemoveBlockOwner(AutoLock&, int32_t aBlockIndex,
MediaCacheStream* aStream);
// Swap all metadata associated with the two blocks. The caller
// is responsible for swapping up any cache file state.
void SwapBlocks(AutoLock&, int32_t aBlockIndex1, int32_t aBlockIndex2);
// Insert the block into the readahead block list for the stream
// at the right point in the list.
void InsertReadaheadBlock(AutoLock&, BlockOwner* aBlockOwner,
int32_t aBlockIndex);
// Guess the duration until block aBlock will be next used
TimeDuration PredictNextUse(AutoLock&, TimeStamp aNow, int32_t aBlock);
// Guess the duration until the next incoming data on aStream will be used
TimeDuration PredictNextUseForIncomingData(AutoLock&,
MediaCacheStream* aStream);
// Truncate the file and index array if there are free blocks at the
// end
void Truncate();
void FlushInternal(AutoLock&);
// There is at most one file-backed media cache.
// It is owned by all MediaCacheStreams that use it.
// This is a raw pointer set by GetMediaCache(), and reset by ~MediaCache(),
// both on the main thread; and is not accessed anywhere else.
static inline MediaCache* gMediaCache = nullptr;
// This member is main-thread only. It's used to allocate unique
// resource IDs to streams.
int64_t mNextResourceID = 0;
// The monitor protects all the data members here. Also, off-main-thread
// readers that need to block will Wait() on this monitor. When new
// data becomes available in the cache, we NotifyAll() on this monitor.
mozilla::Monitor mMonitor MOZ_UNANNOTATED;
// This must always be accessed when the monitor is held.
nsTArray<MediaCacheStream*> mStreams;
// The Blocks describing the cache entries.
nsTArray<Block> mIndex;
RefPtr<MediaBlockCacheBase> mBlockCache;
// The list of free blocks; they are not ordered.
BlockList mFreeBlocks;
// True if an event to run Update() has been queued but not processed
bool mUpdateQueued;
#ifdef DEBUG
bool mInUpdate;
// A list of resource IDs to notify about the change in suspended status.
nsTArray<int64_t> mSuspendedStatusToNotify;
// The thread on which we will run data callbacks from the channels.
// Note this thread is shared among all MediaCache instances.
static inline StaticRefPtr<nsIThread> sThread;
// True if we've tried to init sThread. Note we try once only so it is safe
// to access sThread on all threads.
static inline bool sThreadInit = false;
// MediaCache thread only. True if we're on a cellular network connection.
static inline bool sOnCellular = false;
// Try to trim the cache back to its desired size, if necessary. Return the
// amount of free block counts after trimming.
int32_t TrimCacheIfNeeded(AutoLock& aLock, const TimeStamp& aNow);
struct StreamAction {
// Members for 'SEEK' only.
bool mResume = false;
int64_t mSeekTarget = -1;
// In each update, media cache would determine an action for each stream,
// possible actions are: keeping the stream unchanged, seeking to the new
// position, resuming its channel or suspending its channel. The action would
// be determined by considering a lot of different factors, eg. stream's data
// offset and length, how many free or reusable blocks are avaliable, the
// predicted time for the next block...e.t.c. This function will write the
// corresponding action for each stream in `mStreams` into `aActions`.
void DetermineActionsForStreams(AutoLock& aLock, const TimeStamp& aNow,
nsTArray<StreamAction>& aActions,
int32_t aFreeBlockCount);
// Used by MediaCacheStream::GetDebugInfo() only for debugging.
// Don't add new callers to this function.
friend void MediaCacheStream::GetDebugInfo(
dom::MediaCacheStreamDebugInfo& aInfo);
mozilla::Monitor& GetMonitorOnTheMainThread() {
return mMonitor;
void MediaCache::UpdateOnCellular() {
"Only call on main thread"); // JNI required on Android...
bool onCellular = OnCellularConnection();
LOG("MediaCache::UpdateOnCellular() onCellular=%d", onCellular);
nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
"MediaCache::UpdateOnCellular", [=]() { sOnCellular = onCellular; });
MediaCacheFlusher::Observe(nsISupports* aSubject, char const* aTopic,
char16_t const* aData) {
NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
if (strcmp(aTopic, "last-pb-context-exited") == 0) {
for (MediaCache* mc : mMediaCaches) {
return NS_OK;
if (strcmp(aTopic, "cacheservice:empty-cache") == 0) {
for (MediaCache* mc : mMediaCaches) {
return NS_OK;
if (strcmp(aTopic, "contentchild:network-link-type-changed") == 0 ||
strcmp(aTopic, NS_NETWORK_LINK_TYPE_TOPIC) == 0) {
return NS_OK;
MediaCacheStream::MediaCacheStream(ChannelMediaResource* aClient,
bool aIsPrivateBrowsing)
: mMediaCache(nullptr),
mIsPrivateBrowsing(aIsPrivateBrowsing) {}
size_t MediaCacheStream::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
AutoLock lock(mMediaCache->Monitor());
// Looks like these are not owned:
// - mClient
size_t size = mBlocks.ShallowSizeOfExcludingThis(aMallocSizeOf);
size += mReadaheadBlocks.SizeOfExcludingThis(aMallocSizeOf);
size += mMetadataBlocks.SizeOfExcludingThis(aMallocSizeOf);
size += mPlayedBlocks.SizeOfExcludingThis(aMallocSizeOf);
size += aMallocSizeOf(mPartialBlockBuffer.get());
return size;
size_t MediaCacheStream::BlockList::SizeOfExcludingThis(
MallocSizeOf aMallocSizeOf) const {
return mEntries.ShallowSizeOfExcludingThis(aMallocSizeOf);
void MediaCacheStream::BlockList::AddFirstBlock(int32_t aBlock) {
NS_ASSERTION(!mEntries.GetEntry(aBlock), "Block already in list");
Entry* entry = mEntries.PutEntry(aBlock);
if (mFirstBlock < 0) {
entry->mNextBlock = entry->mPrevBlock = aBlock;
} else {
entry->mNextBlock = mFirstBlock;
entry->mPrevBlock = mEntries.GetEntry(mFirstBlock)->mPrevBlock;
mEntries.GetEntry(entry->mNextBlock)->mPrevBlock = aBlock;
mEntries.GetEntry(entry->mPrevBlock)->mNextBlock = aBlock;
mFirstBlock = aBlock;
void MediaCacheStream::BlockList::AddAfter(int32_t aBlock, int32_t aBefore) {
NS_ASSERTION(!mEntries.GetEntry(aBlock), "Block already in list");
Entry* entry = mEntries.PutEntry(aBlock);
Entry* addAfter = mEntries.GetEntry(aBefore);
NS_ASSERTION(addAfter, "aBefore not in list");
entry->mNextBlock = addAfter->mNextBlock;
entry->mPrevBlock = aBefore;
mEntries.GetEntry(entry->mNextBlock)->mPrevBlock = aBlock;
mEntries.GetEntry(entry->mPrevBlock)->mNextBlock = aBlock;
void MediaCacheStream::BlockList::RemoveBlock(int32_t aBlock) {
Entry* entry = mEntries.GetEntry(aBlock);
MOZ_DIAGNOSTIC_ASSERT(entry, "Block not in list");
if (entry->mNextBlock == aBlock) {
MOZ_DIAGNOSTIC_ASSERT(entry->mPrevBlock == aBlock,
"Linked list inconsistency");
MOZ_DIAGNOSTIC_ASSERT(mFirstBlock == aBlock, "Linked list inconsistency");
mFirstBlock = -1;
} else {
if (mFirstBlock == aBlock) {
mFirstBlock = entry->mNextBlock;
mEntries.GetEntry(entry->mNextBlock)->mPrevBlock = entry->mPrevBlock;
mEntries.GetEntry(entry->mPrevBlock)->mNextBlock = entry->mNextBlock;
int32_t MediaCacheStream::BlockList::GetLastBlock() const {
if (mFirstBlock < 0) return -1;
return mEntries.GetEntry(mFirstBlock)->mPrevBlock;
int32_t MediaCacheStream::BlockList::GetNextBlock(int32_t aBlock) const {
int32_t block = mEntries.GetEntry(aBlock)->mNextBlock;
if (block == mFirstBlock) return -1;
return block;
int32_t MediaCacheStream::BlockList::GetPrevBlock(int32_t aBlock) const {
if (aBlock == mFirstBlock) return -1;
return mEntries.GetEntry(aBlock)->mPrevBlock;
#ifdef DEBUG
void MediaCacheStream::BlockList::Verify() {
int32_t count = 0;
if (mFirstBlock >= 0) {
int32_t block = mFirstBlock;
do {
Entry* entry = mEntries.GetEntry(block);
NS_ASSERTION(mEntries.GetEntry(entry->mNextBlock)->mPrevBlock == block,
"Bad prev link");
NS_ASSERTION(mEntries.GetEntry(entry->mPrevBlock)->mNextBlock == block,
"Bad next link");
block = entry->mNextBlock;
} while (block != mFirstBlock);
NS_ASSERTION(count == mCount, "Bad count");
static void UpdateSwappedBlockIndex(int32_t* aBlockIndex, int32_t aBlock1Index,
int32_t aBlock2Index) {
int32_t index = *aBlockIndex;
if (index == aBlock1Index) {
*aBlockIndex = aBlock2Index;
} else if (index == aBlock2Index) {
*aBlockIndex = aBlock1Index;
void MediaCacheStream::BlockList::NotifyBlockSwapped(int32_t aBlockIndex1,
int32_t aBlockIndex2) {
Entry* e1 = mEntries.GetEntry(aBlockIndex1);
Entry* e2 = mEntries.GetEntry(aBlockIndex2);
int32_t e1Prev = -1, e1Next = -1, e2Prev = -1, e2Next = -1;
// Fix mFirstBlock
UpdateSwappedBlockIndex(&mFirstBlock, aBlockIndex1, aBlockIndex2);
// Fix mNextBlock/mPrevBlock links. First capture previous/next links
// so we don't get confused due to aliasing.
if (e1) {
e1Prev = e1->mPrevBlock;
e1Next = e1->mNextBlock;
if (e2) {
e2Prev = e2->mPrevBlock;
e2Next = e2->mNextBlock;
// Update the entries.
if (e1) {
mEntries.GetEntry(e1Prev)->mNextBlock = aBlockIndex2;
mEntries.GetEntry(e1Next)->mPrevBlock = aBlockIndex2;
if (e2) {
mEntries.GetEntry(e2Prev)->mNextBlock = aBlockIndex1;
mEntries.GetEntry(e2Next)->mPrevBlock = aBlockIndex1;
// Fix hashtable keys. First remove stale entries.
if (e1) {
e1Prev = e1->mPrevBlock;
e1Next = e1->mNextBlock;
// Refresh pointer after hashtable mutation.
e2 = mEntries.GetEntry(aBlockIndex2);
if (e2) {
e2Prev = e2->mPrevBlock;
e2Next = e2->mNextBlock;
// Put new entries back.
if (e1) {
e1 = mEntries.PutEntry(aBlockIndex2);
e1->mNextBlock = e1Next;
e1->mPrevBlock = e1Prev;
if (e2) {
e2 = mEntries.PutEntry(aBlockIndex1);
e2->mNextBlock = e2Next;
e2->mPrevBlock = e2Prev;
void MediaCache::FlushInternal(AutoLock& aLock) {
for (uint32_t blockIndex = 0; blockIndex < mIndex.Length(); ++blockIndex) {
FreeBlock(aLock, blockIndex);
// Truncate index array.
NS_ASSERTION(mIndex.Length() == 0, "Blocks leaked?");
// Reset block cache to its pristine state.
void MediaCache::Flush() {
nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
"MediaCache::Flush", [self = RefPtr<MediaCache>(this)]() mutable {
AutoLock lock(self->mMonitor);
// Ensure MediaCache is deleted on the main thread.
NS_ReleaseOnMainThread("MediaCache::Flush", self.forget());
void MediaCache::CloseStreamsForPrivateBrowsing() {
[self = RefPtr<MediaCache>(this)]() mutable {
AutoLock lock(self->mMonitor);
// Copy mStreams since CloseInternal() will change the array.
for (MediaCacheStream* s : self->mStreams.Clone()) {
if (s->mIsPrivateBrowsing) {
// Ensure MediaCache is deleted on the main thread.
/* static */
RefPtr<MediaCache> MediaCache::GetMediaCache(int64_t aContentLength,
bool aIsPrivateBrowsing) {
NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
if (!sThreadInit) {
sThreadInit = true;
nsCOMPtr<nsIThread> thread;
nsresult rv = NS_NewNamedThread("MediaCache", getter_AddRefs(thread));
if (NS_FAILED(rv)) {
NS_WARNING("Failed to create a thread for MediaCache.");
return nullptr;
sThread = ToRefPtr(std::move(thread));
static struct ClearThread {
// Called during shutdown to clear sThread.
void operator=(std::nullptr_t) {
MOZ_ASSERT(sThread, "We should only clear sThread once.");
sThread = nullptr;
} sClearThread;
ClearOnShutdown(&sClearThread, ShutdownPhase::XPCOMShutdownThreads);
if (!sThread) {
return nullptr;
const int64_t mediaMemoryCacheMaxSize =
static_cast<int64_t>(StaticPrefs::media_memory_cache_max_size()) * 1024;
// Force usage of in-memory cache if we are in private browsing mode
// and the forceMediaMemoryCache pref is set
// We will not attempt to create an on-disk cache if this is the case
const bool forceMediaMemoryCache =
aIsPrivateBrowsing &&
// Alternatively, use an in-memory cache if the media will fit entirely
// in memory
// aContentLength < 0 indicates we do not know content's actual size
const bool contentFitsInMediaMemoryCache =
(aContentLength > 0) && (aContentLength <= mediaMemoryCacheMaxSize);
// Try to allocate a memory cache for our content
if (contentFitsInMediaMemoryCache || forceMediaMemoryCache) {
// Figure out how large our cache should be
int64_t cacheSize = 0;
if (contentFitsInMediaMemoryCache) {
cacheSize = aContentLength;
} else if (forceMediaMemoryCache) {
// Unknown content length, we'll give the maximum allowed cache size
// just to be sure.
if (aContentLength < 0) {
cacheSize = mediaMemoryCacheMaxSize;
} else {
// If the content length is less than the maximum allowed cache size,
// use that, otherwise we cap it to max size.
cacheSize = std::min(aContentLength, mediaMemoryCacheMaxSize);
RefPtr<MediaBlockCacheBase> bc = new MemoryBlockCache(cacheSize);
nsresult rv = bc->Init();
if (NS_SUCCEEDED(rv)) {
RefPtr<MediaCache> mc = new MediaCache(bc);
LOG("GetMediaCache(%" PRIi64 ") -> Memory MediaCache %p", aContentLength,
return mc;
// MemoryBlockCache initialization failed.
// If we require use of a memory media cache, we will bail here.
// Otherwise use a file-backed MediaCache below.
if (forceMediaMemoryCache) {
return nullptr;
if (gMediaCache) {
LOG("GetMediaCache(%" PRIi64 ") -> Existing file-backed MediaCache",
return gMediaCache;
RefPtr<MediaBlockCacheBase> bc = new FileBlockCache();
nsresult rv = bc->Init();
if (NS_SUCCEEDED(rv)) {
gMediaCache = new MediaCache(bc);
LOG("GetMediaCache(%" PRIi64 ") -> Created file-backed MediaCache",
} else {
LOG("GetMediaCache(%" PRIi64 ") -> Failed to create file-backed MediaCache",
return gMediaCache;
nsresult MediaCache::ReadCacheFile(AutoLock&, int64_t aOffset, void* aData,
int32_t aLength, int32_t* aBytes) {
if (!mBlockCache) {
return mBlockCache->Read(aOffset, reinterpret_cast<uint8_t*>(aData), aLength,
// Allowed range is whatever can be accessed with an int32_t block index.
static bool IsOffsetAllowed(int64_t aOffset) {
return aOffset < (int64_t(INT32_MAX) + 1) * MediaCache::BLOCK_SIZE &&
aOffset >= 0;
// Convert 64-bit offset to 32-bit block index.
// Assumes offset range-check was already done.
static int32_t OffsetToBlockIndexUnchecked(int64_t aOffset) {
// Still check for allowed range in debug builds, to catch out-of-range
// issues early during development.
return int32_t(aOffset / MediaCache::BLOCK_SIZE);
// Convert 64-bit offset to 32-bit block index. -1 if out of allowed range.
static int32_t OffsetToBlockIndex(int64_t aOffset) {
return IsOffsetAllowed(aOffset) ? OffsetToBlockIndexUnchecked(aOffset) : -1;
// Convert 64-bit offset to 32-bit offset inside a block.
// Will not fail (even if offset is outside allowed range), so there is no
// need to check for errors.
static int32_t OffsetInBlock(int64_t aOffset) {
// Still check for allowed range in debug builds, to catch out-of-range
// issues early during development.
return int32_t(aOffset % MediaCache::BLOCK_SIZE);
int32_t MediaCache::FindBlockForIncomingData(AutoLock& aLock, TimeStamp aNow,
MediaCacheStream* aStream,
int32_t aStreamBlockIndex) {
int32_t blockIndex =
FindReusableBlock(aLock, aNow, aStream, aStreamBlockIndex, INT32_MAX);
if (blockIndex < 0 || !IsBlockFree(blockIndex)) {
// The block returned is already allocated.
// Don't reuse it if a) there's room to expand the cache or
// b) the data we're going to store in the free block is not higher
// priority than the data already stored in the free block.
// The latter can lead us to go over the cache limit a bit.
if ((mIndex.Length() <
uint32_t(mBlockCache->GetMaxBlocks(MediaCache::CacheSize())) ||
blockIndex < 0 ||
PredictNextUseForIncomingData(aLock, aStream) >=
PredictNextUse(aLock, aNow, blockIndex))) {
blockIndex = mIndex.Length();
// XXX(Bug 1631371) Check if this should use a fallible operation as it
// pretended earlier.
return blockIndex;
return blockIndex;
bool MediaCache::BlockIsReusable(AutoLock&, int32_t aBlockIndex) {
Block* block = &mIndex[aBlockIndex];
for (uint32_t i = 0; i < block->mOwners.Length(); ++i) {
MediaCacheStream* stream = block->mOwners[i].mStream;
if (stream->mPinCount > 0 ||
uint32_t(OffsetToBlockIndex(stream->mStreamOffset)) ==
block->mOwners[i].mStreamBlock) {
return false;
return true;
void MediaCache::AppendMostReusableBlock(AutoLock& aLock, BlockList* aBlockList,
nsTArray<uint32_t>* aResult,
int32_t aBlockIndexLimit) {
int32_t blockIndex = aBlockList->GetLastBlock();
if (blockIndex < 0) return;
do {
// Don't consider blocks for pinned streams, or blocks that are
// beyond the specified limit, or a block that contains a stream's
// current read position (such a block contains both played data
// and readahead data)
if (blockIndex < aBlockIndexLimit && BlockIsReusable(aLock, blockIndex)) {
blockIndex = aBlockList->GetPrevBlock(blockIndex);
} while (blockIndex >= 0);
int32_t MediaCache::FindReusableBlock(AutoLock& aLock, TimeStamp aNow,
MediaCacheStream* aForStream,
int32_t aForStreamBlock,
int32_t aMaxSearchBlockIndex) {
uint32_t length =
std::min(uint32_t(aMaxSearchBlockIndex), uint32_t(mIndex.Length()));
if (aForStream && aForStreamBlock > 0 &&
uint32_t(aForStreamBlock) <= aForStream->mBlocks.Length()) {
int32_t prevCacheBlock = aForStream->mBlocks[aForStreamBlock - 1];
if (prevCacheBlock >= 0) {
uint32_t freeBlockScanEnd =
std::min(length, prevCacheBlock + FREE_BLOCK_SCAN_LIMIT);
for (uint32_t i = prevCacheBlock; i < freeBlockScanEnd; ++i) {
if (IsBlockFree(i)) return i;
if (!mFreeBlocks.IsEmpty()) {
int32_t blockIndex = mFreeBlocks.GetFirstBlock();
do {
if (blockIndex < aMaxSearchBlockIndex) return blockIndex;
blockIndex = mFreeBlocks.GetNextBlock(blockIndex);
} while (blockIndex >= 0);
// Build a list of the blocks we should consider for the "latest
// predicted time of next use". We can exploit the fact that the block
// linked lists are ordered by increasing time of next use. This is
// actually the whole point of having the linked lists.
AutoTArray<uint32_t, 8> candidates;
for (uint32_t i = 0; i < mStreams.Length(); ++i) {
MediaCacheStream* stream = mStreams[i];
if (stream->mPinCount > 0) {
// No point in even looking at this stream's blocks
AppendMostReusableBlock(aLock, &stream->mMetadataBlocks, &candidates,
AppendMostReusableBlock(aLock, &stream->mPlayedBlocks, &candidates, length);
// Don't consider readahead blocks in non-seekable streams. If we
// remove the block we won't be able to seek back to read it later.
if (stream->mIsTransportSeekable) {
AppendMostReusableBlock(aLock, &stream->mReadaheadBlocks, &candidates,
TimeDuration latestUse;
int32_t latestUseBlock = -1;
for (uint32_t i = 0; i < candidates.Length(); ++i) {
TimeDuration nextUse = PredictNextUse(aLock, aNow, candidates[i]);
if (nextUse > latestUse) {
latestUse = nextUse;
latestUseBlock = candidates[i];
return latestUseBlock;
MediaCache::BlockList* MediaCache::GetListForBlock(AutoLock&,
BlockOwner* aBlock) {
switch (aBlock->mClass) {
NS_ASSERTION(aBlock->mStream, "Metadata block has no stream?");
return &aBlock->mStream->mMetadataBlocks;
NS_ASSERTION(aBlock->mStream, "Metadata block has no stream?");
return &aBlock->mStream->mPlayedBlocks;
NS_ASSERTION(aBlock->mStream, "Readahead block has no stream?");
return &aBlock->mStream->mReadaheadBlocks;
NS_ERROR("Invalid block class");
return nullptr;
MediaCache::BlockOwner* MediaCache::GetBlockOwner(AutoLock&,
int32_t aBlockIndex,
MediaCacheStream* aStream) {
Block* block = &mIndex[aBlockIndex];
for (uint32_t i = 0; i < block->mOwners.Length(); ++i) {
if (block->mOwners[i].mStream == aStream) return &block->mOwners[i];
return nullptr;
void MediaCache::SwapBlocks(AutoLock& aLock, int32_t aBlockIndex1,
int32_t aBlockIndex2) {
Block* block1 = &mIndex[aBlockIndex1];
Block* block2 = &mIndex[aBlockIndex2];
// Now all references to block1 have to be replaced with block2 and
// vice versa.
// First update stream references to blocks via mBlocks.
const Block* blocks[] = {block1, block2};
int32_t blockIndices[] = {aBlockIndex1, aBlockIndex2};
for (int32_t i = 0; i < 2; ++i) {
for (uint32_t j = 0; j < blocks[i]->mOwners.Length(); ++j) {
const BlockOwner* b = &blocks[i]->mOwners[j];
b->mStream->mBlocks[b->mStreamBlock] = blockIndices[i];
// Now update references to blocks in block lists.
mFreeBlocks.NotifyBlockSwapped(aBlockIndex1, aBlockIndex2);
nsTHashSet<MediaCacheStream*> visitedStreams;
for (int32_t i = 0; i < 2; ++i) {
for (uint32_t j = 0; j < blocks[i]->mOwners.Length(); ++j) {
MediaCacheStream* stream = blocks[i]->mOwners[j].mStream;
// Make sure that we don't update the same stream twice --- that
// would result in swapping the block references back again!
if (!visitedStreams.EnsureInserted(stream)) continue;
stream->mReadaheadBlocks.NotifyBlockSwapped(aBlockIndex1, aBlockIndex2);
stream->mPlayedBlocks.NotifyBlockSwapped(aBlockIndex1, aBlockIndex2);
stream->mMetadataBlocks.NotifyBlockSwapped(aBlockIndex1, aBlockIndex2);
void MediaCache::RemoveBlockOwner(AutoLock& aLock, int32_t aBlockIndex,
MediaCacheStream* aStream) {
Block* block = &mIndex[aBlockIndex];
for (uint32_t i = 0; i < block->mOwners.Length(); ++i) {
BlockOwner* bo = &block->mOwners[i];
if (bo->mStream == aStream) {
GetListForBlock(aLock, bo)->RemoveBlock(aBlockIndex);
bo->mStream->mBlocks[bo->mStreamBlock] = -1;
if (block->mOwners.IsEmpty()) {
void MediaCache::AddBlockOwnerAsReadahead(AutoLock& aLock, int32_t aBlockIndex,
MediaCacheStream* aStream,
int32_t aStreamBlockIndex) {
Block* block = &mIndex[aBlockIndex];
if (block->mOwners.IsEmpty()) {
BlockOwner* bo = block->mOwners.AppendElement();
bo->mStream = aStream;
bo->mStreamBlock = aStreamBlockIndex;
aStream->mBlocks[aStreamBlockIndex] = aBlockIndex;
InsertReadaheadBlock(aLock, bo, aBlockIndex);
void MediaCache::FreeBlock(AutoLock& aLock, int32_t aBlock) {
Block* block = &mIndex[aBlock];
if (block->mOwners.IsEmpty()) {
// already free
LOG("Released block %d", aBlock);
for (uint32_t i = 0; i < block->mOwners.Length(); ++i) {
BlockOwner* bo = &block->mOwners[i];
GetListForBlock(aLock, bo)->RemoveBlock(aBlock);
bo->mStream->mBlocks[bo->mStreamBlock] = -1;
TimeDuration MediaCache::PredictNextUse(AutoLock&, TimeStamp aNow,
int32_t aBlock) {
NS_ASSERTION(!IsBlockFree(aBlock), "aBlock is free");
Block* block = &mIndex[aBlock];
// Blocks can be belong to multiple streams. The predicted next use
// time is the earliest time predicted by any of the streams.
TimeDuration result;
for (uint32_t i = 0; i < block->mOwners.Length(); ++i) {
BlockOwner* bo = &block->mOwners[i];
TimeDuration prediction;
switch (bo->mClass) {
// This block should be managed in LRU mode. For metadata we predict
// that the time until the next use is the time since the last use.
prediction = aNow - bo->mLastUseTime;
// This block should be managed in LRU mode, and we should impose
// a "replay delay" to reflect the likelihood of replay happening
NS_ASSERTION(static_cast<int64_t>(bo->mStreamBlock) * BLOCK_SIZE <
"Played block after the current stream position?");
int64_t bytesBehind =
bo->mStream->mStreamOffset -
static_cast<int64_t>(bo->mStreamBlock) * BLOCK_SIZE;
int64_t millisecondsBehind =
bytesBehind * 1000 / bo->mStream->mPlaybackBytesPerSecond;
prediction = TimeDuration::FromMilliseconds(std::min<int64_t>(
millisecondsBehind * REPLAY_PENALTY_FACTOR, INT32_MAX));
int64_t bytesAhead =
static_cast<int64_t>(bo->mStreamBlock) * BLOCK_SIZE -
NS_ASSERTION(bytesAhead >= 0,
"Readahead block before the current stream position?");
int64_t millisecondsAhead =
bytesAhead * 1000 / bo->mStream->mPlaybackBytesPerSecond;
prediction = TimeDuration::FromMilliseconds(
std::min<int64_t>(millisecondsAhead, INT32_MAX));
NS_ERROR("Invalid class for predicting next use");
return TimeDuration(0);
if (i == 0 || prediction < result) {
result = prediction;
return result;
TimeDuration MediaCache::PredictNextUseForIncomingData(
AutoLock&, MediaCacheStream* aStream) {
int64_t bytesAhead = aStream->mChannelOffset - aStream->mStreamOffset;
if (bytesAhead <= -BLOCK_SIZE) {
// Hmm, no idea when data behind us will be used. Guess 24 hours.
return TimeDuration::FromSeconds(24 * 60 * 60);
if (bytesAhead <= 0) return TimeDuration(0);
int64_t millisecondsAhead =
bytesAhead * 1000 / aStream->mPlaybackBytesPerSecond;
return TimeDuration::FromMilliseconds(
std::min<int64_t>(millisecondsAhead, INT32_MAX));
void MediaCache::Update() {
AutoLock lock(mMonitor);
mUpdateQueued = false;
#ifdef DEBUG
mInUpdate = true;
const TimeStamp now = TimeStamp::Now();
const int32_t freeBlockCount = TrimCacheIfNeeded(lock, now);
// The action to use for each stream. We store these so we can make
// decisions while holding the cache lock but implement those decisions
// without holding the cache lock, since we need to call out to
// stream, decoder and element code.
AutoTArray<StreamAction, 10> actions;
DetermineActionsForStreams(lock, now, actions, freeBlockCount);
#ifdef DEBUG
mInUpdate = false;
// First, update the mCacheSuspended/mCacheEnded flags so that they're all
// correct when we fire our CacheClient commands below. Those commands can
// rely on these flags being set correctly for all streams.
for (uint32_t i = 0; i < mStreams.Length(); ++i) {
MediaCacheStream* stream = mStreams[i];
switch (actions[i].mTag) {
case StreamAction::SEEK:
stream->mCacheSuspended = false;
stream->mChannelEnded = false;
case StreamAction::RESUME:
stream->mCacheSuspended = false;
case StreamAction::SUSPEND:
stream->mCacheSuspended = true;
for (uint32_t i = 0; i < mStreams.Length(); ++i) {
MediaCacheStream* stream = mStreams[i];
switch (actions[i].mTag) {
case StreamAction::SEEK:
LOG("Stream %p CacheSeek to %" PRId64 " (resume=%d)", stream,
actions[i].mSeekTarget, actions[i].mResume);
case StreamAction::RESUME:
LOG("Stream %p Resumed", stream);
QueueSuspendedStatusUpdate(lock, stream->mResourceID);
case StreamAction::SUSPEND:
LOG("Stream %p Suspended", stream);
QueueSuspendedStatusUpdate(lock, stream->mResourceID);
// Notify streams about the suspended status changes.
for (uint32_t i = 0; i < mSuspendedStatusToNotify.Length(); ++i) {
MediaCache::ResourceStreamIterator iter(this, mSuspendedStatusToNotify[i]);
while (MediaCacheStream* stream = iter.Next(lock)) {
int32_t MediaCache::TrimCacheIfNeeded(AutoLock& aLock, const TimeStamp& aNow) {
const int32_t maxBlocks = mBlockCache->GetMaxBlocks(MediaCache::CacheSize());
int32_t freeBlockCount = mFreeBlocks.GetCount();
TimeDuration latestPredictedUseForOverflow = 0;
if (mIndex.Length() > uint32_t(maxBlocks)) {
// Try to trim back the cache to its desired maximum size. The cache may
// have overflowed simply due to data being received when we have
// no blocks in the main part of the cache that are free or lower
// priority than the new data. The cache can also be overflowing because
// the media.cache_size preference was reduced.
// First, figure out what the least valuable block in the cache overflow
// is. We don't want to replace any blocks in the main part of the
// cache whose expected time of next use is earlier or equal to that.
// If we allow that, we can effectively end up discarding overflowing
// blocks (by moving an overflowing block to the main part of the cache,
// and then overwriting it with another overflowing block), and we try
// to avoid that since it requires HTTP seeks.
// We also use this loop to eliminate overflowing blocks from
// freeBlockCount.
for (int32_t blockIndex = mIndex.Length() - 1; blockIndex >= maxBlocks;
--blockIndex) {
if (IsBlockFree(blockIndex)) {
// Don't count overflowing free blocks in our free block count
TimeDuration predictedUse = PredictNextUse(aLock, aNow, blockIndex);
latestPredictedUseForOverflow =
std::max(latestPredictedUseForOverflow, predictedUse);
} else {
freeBlockCount += maxBlocks - mIndex.Length();
// Now try to move overflowing blocks to the main part of the cache.
for (int32_t blockIndex = mIndex.Length() - 1; blockIndex >= maxBlocks;
--blockIndex) {
if (IsBlockFree(blockIndex)) continue;
Block* block = &mIndex[blockIndex];
// Try to relocate the block close to other blocks for the first stream.
// There is no point in trying to make it close to other blocks in
// *all* the streams it might belong to.
int32_t destinationBlockIndex =
FindReusableBlock(aLock, aNow, block->mOwners[0].mStream,
block->mOwners[0].mStreamBlock, maxBlocks);
if (destinationBlockIndex < 0) {
// Nowhere to place this overflow block. We won't be able to
// place any more overflow blocks.
// Don't evict |destinationBlockIndex| if it is within [cur, end) otherwise
// a new channel will be opened to download this block again which is bad.
bool inCurrentCachedRange = false;
for (BlockOwner& owner : mIndex[destinationBlockIndex].mOwners) {
MediaCacheStream* stream = owner.mStream;
int64_t end = OffsetToBlockIndexUnchecked(
stream->GetCachedDataEndInternal(aLock, stream->mStreamOffset));
int64_t cur = OffsetToBlockIndexUnchecked(stream->mStreamOffset);
if (cur <= owner.mStreamBlock && owner.mStreamBlock < end) {
inCurrentCachedRange = true;
if (inCurrentCachedRange) {
if (IsBlockFree(destinationBlockIndex) ||
PredictNextUse(aLock, aNow, destinationBlockIndex) >
latestPredictedUseForOverflow) {
// Reuse blocks in the main part of the cache that are less useful than
// the least useful overflow blocks
nsresult rv = mBlockCache->MoveBlock(blockIndex, destinationBlockIndex);
if (NS_SUCCEEDED(rv)) {
// We successfully copied the file data.
LOG("Swapping blocks %d and %d (trimming cache)", blockIndex,
// Swapping the block metadata here lets us maintain the
// correct positions in the linked lists
SwapBlocks(aLock, blockIndex, destinationBlockIndex);
// Free the overflowing block even if the copy failed.
LOG("Released block %d (trimming cache)", blockIndex);
FreeBlock(aLock, blockIndex);
} else {
LOG("Could not trim cache block %d (destination %d, "
"predicted next use %f, latest predicted use for overflow %f",
blockIndex, destinationBlockIndex,
PredictNextUse(aLock, aNow, destinationBlockIndex).ToSeconds(),
// Try chopping back the array of cache entries and the cache file.
return freeBlockCount;
void MediaCache::DetermineActionsForStreams(AutoLock& aLock,
const TimeStamp& aNow,
nsTArray<StreamAction>& aActions,
int32_t aFreeBlockCount) {
// Count the blocks allocated for readahead of non-seekable streams
// (these blocks can't be freed but we don't want them to monopolize the
// cache)
int32_t nonSeekableReadaheadBlockCount = 0;
for (uint32_t i = 0; i < mStreams.Length(); ++i) {
MediaCacheStream* stream = mStreams[i];
if (!stream->mIsTransportSeekable) {
nonSeekableReadaheadBlockCount += stream->mReadaheadBlocks.GetCount();
// If freeBlockCount is zero, then compute the latest of
// the predicted next-uses for all blocks
TimeDuration latestNextUse;
const int32_t maxBlocks = mBlockCache->GetMaxBlocks(MediaCache::CacheSize());
if (aFreeBlockCount == 0) {
const int32_t reusableBlock =
FindReusableBlock(aLock, aNow, nullptr, 0, maxBlocks);
if (reusableBlock >= 0) {
latestNextUse = PredictNextUse(aLock, aNow, reusableBlock);
for (uint32_t i = 0; i < mStreams.Length(); ++i) {
MediaCacheStream* stream = mStreams[i];
if (stream->mClosed) {
LOG("Stream %p closed", stream);
// We make decisions based on mSeekTarget when there is a pending seek.
// Otherwise we will keep issuing seek requests until mChannelOffset
// is changed by NotifyDataStarted() which is bad.
const int64_t channelOffset = stream->mSeekTarget != -1
? stream->mSeekTarget
: stream->mChannelOffset;
// Figure out where we should be reading from. It's the first
// uncached byte after the current mStreamOffset.
const int64_t dataOffset =
stream->GetCachedDataEndInternal(aLock, stream->mStreamOffset);
MOZ_ASSERT(dataOffset >= 0);
// Compute where we'd actually seek to to read at readOffset
int64_t desiredOffset = dataOffset;
if (stream->mIsTransportSeekable) {
if (desiredOffset > channelOffset &&
desiredOffset <= channelOffset + SEEK_VS_READ_THRESHOLD) {
// Assume it's more efficient to just keep reading up to the
// desired position instead of trying to seek
desiredOffset = channelOffset;
} else {
// We can't seek directly to the desired offset...
if (channelOffset > desiredOffset) {
// Reading forward won't get us anywhere, we need to go backwards.
// Seek back to 0 (the client will reopen the stream) and then
// read forward.
NS_WARNING("Can't seek backwards, so seeking to 0");
desiredOffset = 0;
// Flush cached blocks out, since if this is a live stream
// the cached data may be completely different next time we
// read it. We have to assume that live streams don't
// advertise themselves as being seekable...
ReleaseStreamBlocks(aLock, stream);
} else {
// otherwise reading forward is looking good, so just stay where we
// are and don't trigger a channel seek!
desiredOffset = channelOffset;
// Figure out if we should be reading data now or not. It's amazing
// how complex this is, but each decision is simple enough.
bool enableReading;
if (stream->mStreamLength >= 0 && dataOffset >= stream->mStreamLength) {
// We want data at the end of the stream, where there's nothing to
// read. We don't want to try to read if we're suspended, because that
// might create a new channel and seek unnecessarily (and incorrectly,
// since HTTP doesn't allow seeking to the actual EOF), and we don't want
// to suspend if we're not suspended and already reading at the end of
// the stream, since there just might be more data than the server
// advertised with Content-Length, and we may as well keep reading.
// But we don't want to seek to the end of the stream if we're not
// already there.
LOG("Stream %p at end of stream", stream);
enableReading =
!stream->mCacheSuspended && stream->mStreamLength == channelOffset;
} else if (desiredOffset < stream->mStreamOffset) {
// We're reading to try to catch up to where the current stream
// reader wants to be. Better not stop.
LOG("Stream %p catching up", stream);
enableReading = true;
} else if (desiredOffset < stream->mStreamOffset + BLOCK_SIZE) {
// The stream reader is waiting for us, or nearly so. Better feed it.
LOG("Stream %p feeding reader", stream);
enableReading = true;
} else if (!stream->mIsTransportSeekable &&
nonSeekableReadaheadBlockCount >=
// This stream is not seekable and there are already too many blocks
// being cached for readahead for nonseekable streams (which we can't
// free). So stop reading ahead now.
LOG("Stream %p throttling non-seekable readahead", stream);
enableReading = false;
} else if (mIndex.Length() > uint32_t(maxBlocks)) {
// We're in the process of bringing the cache size back to the
// desired limit, so don't bring in more data yet
LOG("Stream %p throttling to reduce cache size", stream);