mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
557 lines
16 KiB
C++
557 lines
16 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
/* 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 "TrackBuffer.h"
|
|
|
|
#include "ContainerParser.h"
|
|
#include "MediaSourceDecoder.h"
|
|
#include "SharedThreadPool.h"
|
|
#include "MediaTaskQueue.h"
|
|
#include "SourceBufferDecoder.h"
|
|
#include "SourceBufferResource.h"
|
|
#include "VideoUtils.h"
|
|
#include "mozilla/dom/TimeRanges.h"
|
|
#include "nsError.h"
|
|
#include "nsIRunnable.h"
|
|
#include "nsThreadUtils.h"
|
|
#include "prlog.h"
|
|
|
|
#ifdef PR_LOGGING
|
|
extern PRLogModuleInfo* GetMediaSourceLog();
|
|
extern PRLogModuleInfo* GetMediaSourceAPILog();
|
|
|
|
#define MSE_DEBUG(...) PR_LOG(GetMediaSourceLog(), PR_LOG_DEBUG, (__VA_ARGS__))
|
|
#define MSE_DEBUGV(...) PR_LOG(GetMediaSourceLog(), PR_LOG_DEBUG+1, (__VA_ARGS__))
|
|
#define MSE_API(...) PR_LOG(GetMediaSourceAPILog(), PR_LOG_DEBUG, (__VA_ARGS__))
|
|
#else
|
|
#define MSE_DEBUG(...)
|
|
#define MSE_DEBUGV(...)
|
|
#define MSE_API(...)
|
|
#endif
|
|
|
|
namespace mozilla {
|
|
|
|
TrackBuffer::TrackBuffer(MediaSourceDecoder* aParentDecoder, const nsACString& aType)
|
|
: mParentDecoder(aParentDecoder)
|
|
, mType(aType)
|
|
, mLastStartTimestamp(0)
|
|
, mLastEndTimestamp(0)
|
|
{
|
|
MOZ_COUNT_CTOR(TrackBuffer);
|
|
mParser = ContainerParser::CreateForMIMEType(aType);
|
|
mTaskQueue = new MediaTaskQueue(GetMediaDecodeThreadPool());
|
|
aParentDecoder->AddTrackBuffer(this);
|
|
}
|
|
|
|
TrackBuffer::~TrackBuffer()
|
|
{
|
|
MOZ_COUNT_DTOR(TrackBuffer);
|
|
}
|
|
|
|
class ReleaseDecoderTask : public nsRunnable {
|
|
public:
|
|
explicit ReleaseDecoderTask(SourceBufferDecoder* aDecoder)
|
|
: mDecoder(aDecoder)
|
|
{
|
|
}
|
|
|
|
NS_IMETHOD Run() MOZ_OVERRIDE MOZ_FINAL {
|
|
mDecoder->GetReader()->BreakCycles();
|
|
mDecoder = nullptr;
|
|
return NS_OK;
|
|
}
|
|
|
|
private:
|
|
nsRefPtr<SourceBufferDecoder> mDecoder;
|
|
};
|
|
|
|
MOZ_STACK_CLASS class DecodersToInitialize MOZ_FINAL {
|
|
public:
|
|
explicit DecodersToInitialize(TrackBuffer* aOwner)
|
|
: mOwner(aOwner)
|
|
{
|
|
}
|
|
|
|
~DecodersToInitialize()
|
|
{
|
|
for (size_t i = 0; i < mDecoders.Length(); i++) {
|
|
mOwner->QueueInitializeDecoder(mDecoders[i]);
|
|
}
|
|
}
|
|
|
|
bool NewDecoder()
|
|
{
|
|
nsRefPtr<SourceBufferDecoder> decoder = mOwner->NewDecoder();
|
|
if (!decoder) {
|
|
return false;
|
|
}
|
|
mDecoders.AppendElement(decoder);
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
TrackBuffer* mOwner;
|
|
nsAutoTArray<nsRefPtr<SourceBufferDecoder>,2> mDecoders;
|
|
};
|
|
|
|
void
|
|
TrackBuffer::Shutdown()
|
|
{
|
|
// Finish any decoder initialization, which may add to mInitializedDecoders.
|
|
// Shutdown waits for any pending events, which may require the monitor,
|
|
// so we must not hold the monitor during this call.
|
|
mParentDecoder->GetReentrantMonitor().AssertNotCurrentThreadIn();
|
|
mTaskQueue->Shutdown();
|
|
mTaskQueue = nullptr;
|
|
|
|
ReentrantMonitorAutoEnter mon(mParentDecoder->GetReentrantMonitor());
|
|
for (uint32_t i = 0; i < mDecoders.Length(); ++i) {
|
|
mDecoders[i]->GetReader()->Shutdown();
|
|
}
|
|
mInitializedDecoders.Clear();
|
|
mParentDecoder = nullptr;
|
|
}
|
|
|
|
bool
|
|
TrackBuffer::AppendData(const uint8_t* aData, uint32_t aLength)
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
DecodersToInitialize decoders(this);
|
|
// TODO: Run more of the buffer append algorithm asynchronously.
|
|
if (mParser->IsInitSegmentPresent(aData, aLength)) {
|
|
MSE_DEBUG("TrackBuffer(%p)::AppendData: New initialization segment.", this);
|
|
if (!decoders.NewDecoder()) {
|
|
return false;
|
|
}
|
|
} else if (!mParser->HasInitData()) {
|
|
MSE_DEBUG("TrackBuffer(%p)::AppendData: Non-init segment appended during initialization.", this);
|
|
return false;
|
|
}
|
|
|
|
int64_t start, end;
|
|
if (mParser->ParseStartAndEndTimestamps(aData, aLength, start, end)) {
|
|
if (mParser->IsMediaSegmentPresent(aData, aLength) &&
|
|
!mParser->TimestampsFuzzyEqual(start, mLastEndTimestamp)) {
|
|
MSE_DEBUG("TrackBuffer(%p)::AppendData: Data last=[%lld, %lld] overlaps [%lld, %lld]",
|
|
this, mLastStartTimestamp, mLastEndTimestamp, start, end);
|
|
|
|
// This data is earlier in the timeline than data we have already
|
|
// processed, so we must create a new decoder to handle the decoding.
|
|
if (!decoders.NewDecoder()) {
|
|
return false;
|
|
}
|
|
MSE_DEBUG("TrackBuffer(%p)::AppendData: Decoder marked as initialized.", this);
|
|
const nsTArray<uint8_t>& initData = mParser->InitData();
|
|
AppendDataToCurrentResource(initData.Elements(), initData.Length());
|
|
mLastStartTimestamp = start;
|
|
}
|
|
mLastEndTimestamp = end;
|
|
MSE_DEBUG("TrackBuffer(%p)::AppendData: Segment last=[%lld, %lld] [%lld, %lld]",
|
|
this, mLastStartTimestamp, mLastEndTimestamp, start, end);
|
|
}
|
|
|
|
if (!AppendDataToCurrentResource(aData, aLength)) {
|
|
return false;
|
|
}
|
|
|
|
// Schedule the state machine thread to ensure playback starts if required
|
|
// when data is appended.
|
|
mParentDecoder->ScheduleStateMachineThread();
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
TrackBuffer::AppendDataToCurrentResource(const uint8_t* aData, uint32_t aLength)
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
if (!mCurrentDecoder) {
|
|
return false;
|
|
}
|
|
|
|
SourceBufferResource* resource = mCurrentDecoder->GetResource();
|
|
int64_t appendOffset = resource->GetLength();
|
|
resource->AppendData(aData, aLength);
|
|
// XXX: For future reference: NDA call must run on the main thread.
|
|
mCurrentDecoder->NotifyDataArrived(reinterpret_cast<const char*>(aData),
|
|
aLength, appendOffset);
|
|
mParentDecoder->NotifyTimeRangesChanged();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
TrackBuffer::EvictData(uint32_t aThreshold)
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
ReentrantMonitorAutoEnter mon(mParentDecoder->GetReentrantMonitor());
|
|
|
|
int64_t totalSize = 0;
|
|
for (uint32_t i = 0; i < mDecoders.Length(); ++i) {
|
|
totalSize += mDecoders[i]->GetResource()->GetSize();
|
|
}
|
|
|
|
int64_t toEvict = totalSize - aThreshold;
|
|
if (toEvict <= 0) {
|
|
return false;
|
|
}
|
|
|
|
for (uint32_t i = 0; i < mInitializedDecoders.Length(); ++i) {
|
|
MSE_DEBUG("TrackBuffer(%p)::EvictData decoder=%u threshold=%u toEvict=%lld",
|
|
this, i, aThreshold, toEvict);
|
|
toEvict -= mInitializedDecoders[i]->GetResource()->EvictData(toEvict);
|
|
if (!mInitializedDecoders[i]->GetResource()->GetSize() &&
|
|
mInitializedDecoders[i] != mCurrentDecoder) {
|
|
RemoveDecoder(mInitializedDecoders[i]);
|
|
}
|
|
}
|
|
return toEvict < (totalSize - aThreshold);
|
|
}
|
|
|
|
void
|
|
TrackBuffer::EvictBefore(double aTime)
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
ReentrantMonitorAutoEnter mon(mParentDecoder->GetReentrantMonitor());
|
|
for (uint32_t i = 0; i < mInitializedDecoders.Length(); ++i) {
|
|
int64_t endOffset = mInitializedDecoders[i]->ConvertToByteOffset(aTime);
|
|
if (endOffset > 0) {
|
|
MSE_DEBUG("TrackBuffer(%p)::EvictBefore decoder=%u offset=%lld", this, i, endOffset);
|
|
mInitializedDecoders[i]->GetResource()->EvictBefore(endOffset);
|
|
if (!mInitializedDecoders[i]->GetResource()->GetSize() &&
|
|
mInitializedDecoders[i] != mCurrentDecoder) {
|
|
RemoveDecoder(mInitializedDecoders[i]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
double
|
|
TrackBuffer::Buffered(dom::TimeRanges* aRanges)
|
|
{
|
|
ReentrantMonitorAutoEnter mon(mParentDecoder->GetReentrantMonitor());
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
|
|
double highestEndTime = 0;
|
|
|
|
for (uint32_t i = 0; i < mDecoders.Length(); ++i) {
|
|
nsRefPtr<dom::TimeRanges> r = new dom::TimeRanges();
|
|
mDecoders[i]->GetBuffered(r);
|
|
if (r->Length() > 0) {
|
|
highestEndTime = std::max(highestEndTime, r->GetEndTime());
|
|
aRanges->Union(r);
|
|
}
|
|
}
|
|
|
|
return highestEndTime;
|
|
}
|
|
|
|
already_AddRefed<SourceBufferDecoder>
|
|
TrackBuffer::NewDecoder()
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
MOZ_ASSERT(mParentDecoder);
|
|
|
|
DiscardDecoder();
|
|
|
|
nsRefPtr<SourceBufferDecoder> decoder = mParentDecoder->CreateSubDecoder(mType);
|
|
if (!decoder) {
|
|
return nullptr;
|
|
}
|
|
ReentrantMonitorAutoEnter mon(mParentDecoder->GetReentrantMonitor());
|
|
mCurrentDecoder = decoder;
|
|
mDecoders.AppendElement(decoder);
|
|
|
|
mLastStartTimestamp = 0;
|
|
mLastEndTimestamp = 0;
|
|
|
|
decoder->SetTaskQueue(mTaskQueue);
|
|
return decoder.forget();
|
|
}
|
|
|
|
bool
|
|
TrackBuffer::QueueInitializeDecoder(SourceBufferDecoder* aDecoder)
|
|
{
|
|
RefPtr<nsIRunnable> task =
|
|
NS_NewRunnableMethodWithArg<SourceBufferDecoder*>(this,
|
|
&TrackBuffer::InitializeDecoder,
|
|
aDecoder);
|
|
if (NS_FAILED(mTaskQueue->Dispatch(task))) {
|
|
MSE_DEBUG("MediaSourceReader(%p): Failed to enqueue decoder initialization task", this);
|
|
RemoveDecoder(aDecoder);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void
|
|
TrackBuffer::InitializeDecoder(SourceBufferDecoder* aDecoder)
|
|
{
|
|
MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
|
|
|
|
// ReadMetadata may block the thread waiting on data, so it must not be
|
|
// called with the monitor held.
|
|
mParentDecoder->GetReentrantMonitor().AssertNotCurrentThreadIn();
|
|
|
|
MediaDecoderReader* reader = aDecoder->GetReader();
|
|
MSE_DEBUG("TrackBuffer(%p): Initializing subdecoder %p reader %p",
|
|
this, aDecoder, reader);
|
|
|
|
MediaInfo mi;
|
|
nsAutoPtr<MetadataTags> tags; // TODO: Handle metadata.
|
|
nsresult rv = reader->ReadMetadata(&mi, getter_Transfers(tags));
|
|
reader->SetIdle();
|
|
|
|
if (NS_SUCCEEDED(rv) && reader->IsWaitingOnCDMResource()) {
|
|
ReentrantMonitorAutoEnter mon(mParentDecoder->GetReentrantMonitor());
|
|
mWaitingDecoders.AppendElement(aDecoder);
|
|
return;
|
|
}
|
|
|
|
aDecoder->SetTaskQueue(nullptr);
|
|
|
|
if (NS_FAILED(rv) || (!mi.HasVideo() && !mi.HasAudio())) {
|
|
// XXX: Need to signal error back to owning SourceBuffer.
|
|
MSE_DEBUG("TrackBuffer(%p): Reader %p failed to initialize rv=%x audio=%d video=%d",
|
|
this, reader, rv, mi.HasAudio(), mi.HasVideo());
|
|
RemoveDecoder(aDecoder);
|
|
return;
|
|
}
|
|
|
|
if (mi.HasVideo()) {
|
|
MSE_DEBUG("TrackBuffer(%p): Reader %p video resolution=%dx%d",
|
|
this, reader, mi.mVideo.mDisplay.width, mi.mVideo.mDisplay.height);
|
|
}
|
|
if (mi.HasAudio()) {
|
|
MSE_DEBUG("TrackBuffer(%p): Reader %p audio sampleRate=%d channels=%d",
|
|
this, reader, mi.mAudio.mRate, mi.mAudio.mChannels);
|
|
}
|
|
|
|
if (!RegisterDecoder(aDecoder)) {
|
|
// XXX: Need to signal error back to owning SourceBuffer.
|
|
MSE_DEBUG("TrackBuffer(%p): Reader %p not activated", this, reader);
|
|
RemoveDecoder(aDecoder);
|
|
return;
|
|
}
|
|
MSE_DEBUG("TrackBuffer(%p): Reader %p activated", this, reader);
|
|
}
|
|
|
|
bool
|
|
TrackBuffer::ValidateTrackFormats(const MediaInfo& aInfo)
|
|
{
|
|
if (mInfo.HasAudio() != aInfo.HasAudio() ||
|
|
mInfo.HasVideo() != aInfo.HasVideo()) {
|
|
MSE_DEBUG("TrackBuffer(%p)::ValidateTrackFormats audio/video track mismatch", this);
|
|
return false;
|
|
}
|
|
|
|
// TODO: Support dynamic audio format changes.
|
|
if (mInfo.HasAudio() &&
|
|
(mInfo.mAudio.mRate != aInfo.mAudio.mRate ||
|
|
mInfo.mAudio.mChannels != aInfo.mAudio.mChannels)) {
|
|
MSE_DEBUG("TrackBuffer(%p)::ValidateTrackFormats audio format mismatch", this);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
TrackBuffer::RegisterDecoder(SourceBufferDecoder* aDecoder)
|
|
{
|
|
ReentrantMonitorAutoEnter mon(mParentDecoder->GetReentrantMonitor());
|
|
const MediaInfo& info = aDecoder->GetReader()->GetMediaInfo();
|
|
// Initialize the track info since this is the first decoder.
|
|
if (mInitializedDecoders.IsEmpty()) {
|
|
mInfo = info;
|
|
mParentDecoder->OnTrackBufferConfigured(this, mInfo);
|
|
}
|
|
if (!ValidateTrackFormats(info)) {
|
|
MSE_DEBUG("TrackBuffer(%p)::RegisterDecoder with mismatched audio/video tracks", this);
|
|
return false;
|
|
}
|
|
mInitializedDecoders.AppendElement(aDecoder);
|
|
mParentDecoder->NotifyTimeRangesChanged();
|
|
return true;
|
|
}
|
|
|
|
void
|
|
TrackBuffer::DiscardDecoder()
|
|
{
|
|
ReentrantMonitorAutoEnter mon(mParentDecoder->GetReentrantMonitor());
|
|
if (mCurrentDecoder) {
|
|
mCurrentDecoder->GetResource()->Ended();
|
|
}
|
|
mCurrentDecoder = nullptr;
|
|
}
|
|
|
|
void
|
|
TrackBuffer::Detach()
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
if (mCurrentDecoder) {
|
|
DiscardDecoder();
|
|
}
|
|
}
|
|
|
|
bool
|
|
TrackBuffer::HasInitSegment()
|
|
{
|
|
ReentrantMonitorAutoEnter mon(mParentDecoder->GetReentrantMonitor());
|
|
return mParser->HasInitData();
|
|
}
|
|
|
|
bool
|
|
TrackBuffer::IsReady()
|
|
{
|
|
ReentrantMonitorAutoEnter mon(mParentDecoder->GetReentrantMonitor());
|
|
MOZ_ASSERT((mInfo.HasAudio() || mInfo.HasVideo()) || mInitializedDecoders.IsEmpty());
|
|
return mParser->HasInitData() && (mInfo.HasAudio() || mInfo.HasVideo());
|
|
}
|
|
|
|
bool
|
|
TrackBuffer::ContainsTime(int64_t aTime)
|
|
{
|
|
ReentrantMonitorAutoEnter mon(mParentDecoder->GetReentrantMonitor());
|
|
for (uint32_t i = 0; i < mInitializedDecoders.Length(); ++i) {
|
|
nsRefPtr<dom::TimeRanges> r = new dom::TimeRanges();
|
|
mInitializedDecoders[i]->GetBuffered(r);
|
|
if (r->Find(double(aTime) / USECS_PER_S) != dom::TimeRanges::NoIndex) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
TrackBuffer::BreakCycles()
|
|
{
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
|
|
for (uint32_t i = 0; i < mDecoders.Length(); ++i) {
|
|
mDecoders[i]->BreakCycles();
|
|
}
|
|
mDecoders.Clear();
|
|
|
|
// These are cleared in Shutdown()
|
|
MOZ_ASSERT(mInitializedDecoders.IsEmpty());
|
|
MOZ_ASSERT(!mParentDecoder);
|
|
}
|
|
|
|
void
|
|
TrackBuffer::ResetDecode()
|
|
{
|
|
for (uint32_t i = 0; i < mDecoders.Length(); ++i) {
|
|
mDecoders[i]->GetReader()->ResetDecode();
|
|
}
|
|
}
|
|
|
|
const nsTArray<nsRefPtr<SourceBufferDecoder>>&
|
|
TrackBuffer::Decoders()
|
|
{
|
|
// XXX assert OnDecodeThread
|
|
return mInitializedDecoders;
|
|
}
|
|
|
|
#ifdef MOZ_EME
|
|
nsresult
|
|
TrackBuffer::SetCDMProxy(CDMProxy* aProxy)
|
|
{
|
|
ReentrantMonitorAutoEnter mon(mParentDecoder->GetReentrantMonitor());
|
|
|
|
for (uint32_t i = 0; i < mDecoders.Length(); ++i) {
|
|
nsresult rv = mDecoders[i]->SetCDMProxy(aProxy);
|
|
NS_ENSURE_SUCCESS(rv, rv);
|
|
}
|
|
|
|
for (uint32_t i = 0; i < mWaitingDecoders.Length(); ++i) {
|
|
CDMCaps::AutoLock caps(aProxy->Capabilites());
|
|
caps.CallOnMainThreadWhenCapsAvailable(
|
|
NS_NewRunnableMethodWithArg<SourceBufferDecoder*>(this,
|
|
&TrackBuffer::QueueInitializeDecoder,
|
|
mWaitingDecoders[i]));
|
|
}
|
|
|
|
mWaitingDecoders.Clear();
|
|
|
|
return NS_OK;
|
|
}
|
|
#endif
|
|
|
|
#if defined(DEBUG)
|
|
void
|
|
TrackBuffer::Dump(const char* aPath)
|
|
{
|
|
char path[255];
|
|
PR_snprintf(path, sizeof(path), "%s/trackbuffer-%p", aPath, this);
|
|
PR_MkDir(path, 0700);
|
|
|
|
for (uint32_t i = 0; i < mDecoders.Length(); ++i) {
|
|
char buf[255];
|
|
PR_snprintf(buf, sizeof(buf), "%s/reader-%p", path, mDecoders[i]->GetReader());
|
|
PR_MkDir(buf, 0700);
|
|
|
|
mDecoders[i]->GetResource()->Dump(buf);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
class DelayedDispatchToMainThread : public nsRunnable {
|
|
public:
|
|
explicit DelayedDispatchToMainThread(SourceBufferDecoder* aDecoder)
|
|
: mDecoder(aDecoder)
|
|
{
|
|
}
|
|
|
|
NS_IMETHOD Run() MOZ_OVERRIDE MOZ_FINAL {
|
|
// Shutdown the reader, and remove its reference to the decoder
|
|
// so that it can't accidentally read it after the decoder
|
|
// is destroyed.
|
|
mDecoder->GetReader()->Shutdown();
|
|
mDecoder->GetReader()->ClearDecoder();
|
|
RefPtr<nsIRunnable> task = new ReleaseDecoderTask(mDecoder);
|
|
mDecoder = nullptr;
|
|
// task now holds the only ref to the decoder.
|
|
NS_DispatchToMainThread(task);
|
|
return NS_OK;
|
|
}
|
|
|
|
private:
|
|
RefPtr<SourceBufferDecoder> mDecoder;
|
|
};
|
|
|
|
void
|
|
TrackBuffer::RemoveDecoder(SourceBufferDecoder* aDecoder)
|
|
{
|
|
RefPtr<nsIRunnable> task;
|
|
nsRefPtr<MediaTaskQueue> taskQueue;
|
|
{
|
|
ReentrantMonitorAutoEnter mon(mParentDecoder->GetReentrantMonitor());
|
|
if (mInitializedDecoders.RemoveElement(aDecoder)) {
|
|
taskQueue = aDecoder->GetReader()->GetTaskQueue();
|
|
task = new DelayedDispatchToMainThread(aDecoder);
|
|
} else {
|
|
task = new ReleaseDecoderTask(aDecoder);
|
|
}
|
|
mDecoders.RemoveElement(aDecoder);
|
|
|
|
if (mCurrentDecoder == aDecoder) {
|
|
DiscardDecoder();
|
|
}
|
|
}
|
|
// At this point, task should be holding the only reference to aDecoder.
|
|
if (taskQueue) {
|
|
// If we were initialized, post the task via the reader's
|
|
// task queue to ensure that the reader isn't in the middle
|
|
// of an existing task.
|
|
taskQueue->Dispatch(task);
|
|
} else {
|
|
NS_DispatchToMainThread(task);
|
|
}
|
|
}
|
|
|
|
} // namespace mozilla
|