Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
/* 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,
#include "MediaTrackGraphImpl.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/Unused.h"
#include "AudioSegment.h"
#include "CrossGraphPort.h"
#include "VideoSegment.h"
#include "nsContentUtils.h"
#include "nsGlobalWindowInner.h"
#include "nsPrintfCString.h"
#include "nsServiceManagerUtils.h"
#include "prerror.h"
#include "mozilla/Logging.h"
#include "mozilla/Attributes.h"
#include "ForwardedInputTrack.h"
#include "ImageContainer.h"
#include "AudioCaptureTrack.h"
#include "AudioDeviceInfo.h"
#include "AudioNodeTrack.h"
#include "AudioNodeExternalInputTrack.h"
#if defined(MOZ_WEBRTC)
# include "MediaEngineWebRTCAudio.h"
#endif // MOZ_WEBRTC
#include "MediaTrackListener.h"
#include "mozilla/dom/BaseAudioContextBinding.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/WorkletThread.h"
#include "mozilla/media/MediaUtils.h"
#include <algorithm>
#include "GeckoProfiler.h"
#include "VideoFrameContainer.h"
#include "mozilla/AbstractThread.h"
#include "mozilla/StaticPrefs_dom.h"
#include "mozilla/StaticPrefs_media.h"
#include "transport/runnable_utils.h"
#include "VideoUtils.h"
#include "GraphRunner.h"
#include "Tracing.h"
#include "UnderrunHandler.h"
#include "mozilla/CycleCollectedJSRuntime.h"
#include "mozilla/Preferences.h"
#include "webaudio/blink/DenormalDisabler.h"
#include "webaudio/blink/HRTFDatabaseLoader.h"
using namespace mozilla::layers;
using namespace mozilla::dom;
using namespace mozilla::gfx;
using namespace mozilla::media;
namespace mozilla {
using AudioDeviceID = CubebUtils::AudioDeviceID;
using IsInShutdown = MediaTrack::IsInShutdown;
LazyLogModule gMediaTrackGraphLog("MediaTrackGraph");
#ifdef LOG
# undef LOG
#endif // LOG
#define LOG(type, msg) MOZ_LOG(gMediaTrackGraphLog, type, msg)
NativeInputTrack* DeviceInputTrackManager::GetNativeInputTrack() {
return mNativeInputTrack.get();
}
DeviceInputTrack* DeviceInputTrackManager::GetDeviceInputTrack(
CubebUtils::AudioDeviceID aID) {
if (mNativeInputTrack && mNativeInputTrack->mDeviceId == aID) {
return mNativeInputTrack.get();
}
for (const RefPtr<NonNativeInputTrack>& t : mNonNativeInputTracks) {
if (t->mDeviceId == aID) {
return t.get();
}
}
return nullptr;
}
NonNativeInputTrack* DeviceInputTrackManager::GetFirstNonNativeInputTrack() {
if (mNonNativeInputTracks.IsEmpty()) {
return nullptr;
}
return mNonNativeInputTracks[0].get();
}
void DeviceInputTrackManager::Add(DeviceInputTrack* aTrack) {
if (NativeInputTrack* native = aTrack->AsNativeInputTrack()) {
MOZ_ASSERT(!mNativeInputTrack);
mNativeInputTrack = native;
} else {
NonNativeInputTrack* nonNative = aTrack->AsNonNativeInputTrack();
MOZ_ASSERT(nonNative);
struct DeviceTrackComparator {
public:
bool Equals(const RefPtr<NonNativeInputTrack>& aTrack,
CubebUtils::AudioDeviceID aDeviceId) const {
return aTrack->mDeviceId == aDeviceId;
}
};
MOZ_ASSERT(!mNonNativeInputTracks.Contains(aTrack->mDeviceId,
DeviceTrackComparator()));
mNonNativeInputTracks.AppendElement(nonNative);
}
}
void DeviceInputTrackManager::Remove(DeviceInputTrack* aTrack) {
if (aTrack->AsNativeInputTrack()) {
MOZ_ASSERT(mNativeInputTrack);
MOZ_ASSERT(mNativeInputTrack.get() == aTrack->AsNativeInputTrack());
mNativeInputTrack = nullptr;
} else {
NonNativeInputTrack* nonNative = aTrack->AsNonNativeInputTrack();
MOZ_ASSERT(nonNative);
DebugOnly<bool> removed = mNonNativeInputTracks.RemoveElement(nonNative);
MOZ_ASSERT(removed);
}
}
/**
* A hash table containing the graph instances, one per Window ID,
* sample rate, and device ID combination.
*/
struct MediaTrackGraphImpl::Lookup final {
HashNumber Hash() const {
return HashGeneric(mWindowID, mSampleRate, mOutputDeviceID);
}
const uint64_t mWindowID;
const TrackRate mSampleRate;
const CubebUtils::AudioDeviceID mOutputDeviceID;
};
// Implicit to support GraphHashSet.lookup(*graph).
MOZ_IMPLICIT MediaTrackGraphImpl::operator MediaTrackGraphImpl::Lookup() const {
return {mWindowID, mSampleRate, PrimaryOutputDeviceID()};
}
namespace {
struct GraphHasher { // for HashSet
using Lookup = const MediaTrackGraphImpl::Lookup;
static HashNumber hash(const Lookup& aLookup) { return aLookup.Hash(); }
static bool match(const MediaTrackGraphImpl* aGraph, const Lookup& aLookup) {
return aGraph->mWindowID == aLookup.mWindowID &&
aGraph->GraphRate() == aLookup.mSampleRate &&
aGraph->PrimaryOutputDeviceID() == aLookup.mOutputDeviceID;
}
};
// The weak reference to the graph is removed when its last track is removed.
using GraphHashSet =
HashSet<MediaTrackGraphImpl*, GraphHasher, InfallibleAllocPolicy>;
GraphHashSet* Graphs() {
MOZ_ASSERT(NS_IsMainThread());
static GraphHashSet sGraphs(4); // 4 is minimum HashSet capacity
return &sGraphs;
}
} // anonymous namespace
static void ApplyTrackDisabling(DisabledTrackMode aDisabledMode,
MediaSegment* aSegment,
MediaSegment* aRawSegment) {
if (aDisabledMode == DisabledTrackMode::ENABLED) {
return;
}
if (aDisabledMode == DisabledTrackMode::SILENCE_BLACK) {
aSegment->ReplaceWithDisabled();
if (aRawSegment) {
aRawSegment->ReplaceWithDisabled();
}
} else if (aDisabledMode == DisabledTrackMode::SILENCE_FREEZE) {
aSegment->ReplaceWithNull();
if (aRawSegment) {
aRawSegment->ReplaceWithNull();
}
} else {
MOZ_CRASH("Unsupported mode");
}
}
MediaTrackGraphImpl::~MediaTrackGraphImpl() {
MOZ_ASSERT(mTracks.IsEmpty() && mSuspendedTracks.IsEmpty(),
"All tracks should have been destroyed by messages from the main "
"thread");
LOG(LogLevel::Debug, ("MediaTrackGraph %p destroyed", this));
LOG(LogLevel::Debug, ("MediaTrackGraphImpl::~MediaTrackGraphImpl"));
}
void MediaTrackGraphImpl::AddTrackGraphThread(MediaTrack* aTrack) {
MOZ_ASSERT(OnGraphThreadOrNotRunning());
aTrack->mStartTime = mProcessedTime;
if (aTrack->IsSuspended()) {
mSuspendedTracks.AppendElement(aTrack);
LOG(LogLevel::Debug,
("%p: Adding media track %p, in the suspended track array", this,
aTrack));
} else {
mTracks.AppendElement(aTrack);
LOG(LogLevel::Debug, ("%p: Adding media track %p, count %zu", this, aTrack,
mTracks.Length()));
}
SetTrackOrderDirty();
}
void MediaTrackGraphImpl::RemoveTrackGraphThread(MediaTrack* aTrack) {
MOZ_ASSERT(OnGraphThreadOrNotRunning());
// Remove references in mTrackUpdates before we allow aTrack to die.
// Pending updates are not needed (since the main thread has already given
// up the track) so we will just drop them.
{
MonitorAutoLock lock(mMonitor);
for (uint32_t i = 0; i < mTrackUpdates.Length(); ++i) {
if (mTrackUpdates[i].mTrack == aTrack) {
mTrackUpdates[i].mTrack = nullptr;
}
}
}
// Ensure that mFirstCycleBreaker is updated when necessary.
SetTrackOrderDirty();
UnregisterAllAudioOutputs(aTrack);
if (aTrack->IsSuspended()) {
mSuspendedTracks.RemoveElement(aTrack);
} else {
mTracks.RemoveElement(aTrack);
}
LOG(LogLevel::Debug, ("%p: Removed media track %p, count %zu", this, aTrack,
mTracks.Length()));
NS_RELEASE(aTrack); // probably destroying it
}
TrackTime MediaTrackGraphImpl::GraphTimeToTrackTimeWithBlocking(
const MediaTrack* aTrack, GraphTime aTime) const {
MOZ_ASSERT(
aTime <= mStateComputedTime,
"Don't ask about times where we haven't made blocking decisions yet");
return std::max<TrackTime>(
0, std::min(aTime, aTrack->mStartBlocking) - aTrack->mStartTime);
}
void MediaTrackGraphImpl::UpdateCurrentTimeForTracks(
GraphTime aPrevCurrentTime) {
MOZ_ASSERT(OnGraphThread());
for (MediaTrack* track : AllTracks()) {
// Shouldn't have already notified of ended *and* have output!
MOZ_ASSERT_IF(track->mStartBlocking > aPrevCurrentTime,
!track->mNotifiedEnded);
// Calculate blocked time and fire Blocked/Unblocked events
GraphTime blockedTime = mStateComputedTime - track->mStartBlocking;
NS_ASSERTION(blockedTime >= 0, "Error in blocking time");
track->AdvanceTimeVaryingValuesToCurrentTime(mStateComputedTime,
blockedTime);
LOG(LogLevel::Verbose,
("%p: MediaTrack %p bufferStartTime=%f blockedTime=%f", this, track,
MediaTimeToSeconds(track->mStartTime),
MediaTimeToSeconds(blockedTime)));
track->mStartBlocking = mStateComputedTime;
TrackTime trackCurrentTime =
track->GraphTimeToTrackTime(mStateComputedTime);
if (track->mEnded) {
MOZ_ASSERT(track->GetEnd() <= trackCurrentTime);
if (!track->mNotifiedEnded) {
// Playout of this track ended and listeners have not been notified.
track->mNotifiedEnded = true;
SetTrackOrderDirty();
for (const auto& listener : track->mTrackListeners) {
listener->NotifyOutput(this, track->GetEnd());
listener->NotifyEnded(this);
}
}
} else {
for (const auto& listener : track->mTrackListeners) {
listener->NotifyOutput(this, trackCurrentTime);
}
}
}
}
template <typename C, typename Chunk>
void MediaTrackGraphImpl::ProcessChunkMetadataForInterval(MediaTrack* aTrack,
C& aSegment,
TrackTime aStart,
TrackTime aEnd) {
MOZ_ASSERT(OnGraphThreadOrNotRunning());
MOZ_ASSERT(aTrack);
TrackTime offset = 0;
for (typename C::ConstChunkIterator chunk(aSegment); !chunk.IsEnded();
chunk.Next()) {
if (offset >= aEnd) {
break;
}
offset += chunk->GetDuration();
if (chunk->IsNull() || offset < aStart) {
continue;
}
const PrincipalHandle& principalHandle = chunk->GetPrincipalHandle();
if (principalHandle != aSegment.GetLastPrincipalHandle()) {
aSegment.SetLastPrincipalHandle(principalHandle);
LOG(LogLevel::Debug,
("%p: MediaTrack %p, principalHandle "
"changed in %sChunk with duration %lld",
this, aTrack,
aSegment.GetType() == MediaSegment::AUDIO ? "Audio" : "Video",
(long long)chunk->GetDuration()));
for (const auto& listener : aTrack->mTrackListeners) {
listener->NotifyPrincipalHandleChanged(this, principalHandle);
}
}
}
}
void MediaTrackGraphImpl::ProcessChunkMetadata(GraphTime aPrevCurrentTime) {
MOZ_ASSERT(OnGraphThreadOrNotRunning());
for (MediaTrack* track : AllTracks()) {
TrackTime iterationStart = track->GraphTimeToTrackTime(aPrevCurrentTime);
TrackTime iterationEnd = track->GraphTimeToTrackTime(mProcessedTime);
if (!track->mSegment) {
continue;
}
if (track->mType == MediaSegment::AUDIO) {
ProcessChunkMetadataForInterval<AudioSegment, AudioChunk>(
track, *track->GetData<AudioSegment>(), iterationStart, iterationEnd);
} else if (track->mType == MediaSegment::VIDEO) {
ProcessChunkMetadataForInterval<VideoSegment, VideoChunk>(
track, *track->GetData<VideoSegment>(), iterationStart, iterationEnd);
} else {
MOZ_CRASH("Unknown track type");
}
}
}
GraphTime MediaTrackGraphImpl::WillUnderrun(MediaTrack* aTrack,
GraphTime aEndBlockingDecisions) {
// Ended tracks can't underrun. ProcessedMediaTracks also can't cause
// underrun currently, since we'll always be able to produce data for them
// unless they block on some other track.
if (aTrack->mEnded || aTrack->AsProcessedTrack()) {
return aEndBlockingDecisions;
}
// This track isn't ended or suspended. We don't need to call
// TrackTimeToGraphTime since an underrun is the only thing that can block
// it.
GraphTime bufferEnd = aTrack->GetEnd() + aTrack->mStartTime;
#ifdef DEBUG
if (bufferEnd < mProcessedTime) {
LOG(LogLevel::Error, ("%p: MediaTrack %p underrun, "
"bufferEnd %f < mProcessedTime %f (%" PRId64
" < %" PRId64 "), TrackTime %" PRId64,
this, aTrack, MediaTimeToSeconds(bufferEnd),
MediaTimeToSeconds(mProcessedTime), bufferEnd,
mProcessedTime, aTrack->GetEnd()));
NS_ASSERTION(bufferEnd >= mProcessedTime, "Buffer underran");
}
#endif
return std::min(bufferEnd, aEndBlockingDecisions);
}
namespace {
// Value of mCycleMarker for unvisited tracks in cycle detection.
const uint32_t NOT_VISITED = UINT32_MAX;
// Value of mCycleMarker for ordered tracks in muted cycles.
const uint32_t IN_MUTED_CYCLE = 1;
} // namespace
bool MediaTrackGraphImpl::AudioTrackPresent() {
MOZ_ASSERT(OnGraphThreadOrNotRunning());
bool audioTrackPresent = false;
for (MediaTrack* track : mTracks) {
if (track->AsAudioNodeTrack()) {
audioTrackPresent = true;
break;
}
if (track->mType == MediaSegment::AUDIO && !track->mNotifiedEnded) {
audioTrackPresent = true;
break;
}
}
// We may not have audio input device when we only have AudioNodeTracks. But
// if audioTrackPresent is false, we must have no input device.
MOZ_DIAGNOSTIC_ASSERT_IF(
!audioTrackPresent,
!mDeviceInputTrackManagerGraphThread.GetNativeInputTrack());
return audioTrackPresent;
}
void MediaTrackGraphImpl::CheckDriver() {
MOZ_ASSERT(OnGraphThread());
// An offline graph has only one driver.
// Otherwise, if a switch is already pending, let that happen.
if (!mRealtime || Switching()) {
return;
}
AudioCallbackDriver* audioCallbackDriver =
CurrentDriver()->AsAudioCallbackDriver();
if (audioCallbackDriver && !audioCallbackDriver->OnFallback()) {
for (PendingResumeOperation& op : mPendingResumeOperations) {
op.Apply(this);
}
mPendingResumeOperations.Clear();
}
// Note that this looks for any audio tracks, input or output, and switches
// to a SystemClockDriver if there are none active or no resume operations
// to make any active.
bool needAudioCallbackDriver =
!mPendingResumeOperations.IsEmpty() || AudioTrackPresent();
if (!needAudioCallbackDriver) {
if (audioCallbackDriver && audioCallbackDriver->IsStarted()) {
SwitchAtNextIteration(
new SystemClockDriver(this, CurrentDriver(), mSampleRate));
}
return;
}
NativeInputTrack* native =
mDeviceInputTrackManagerGraphThread.GetNativeInputTrack();
CubebUtils::AudioDeviceID inputDevice = native ? native->mDeviceId : nullptr;
uint32_t inputChannelCount = AudioInputChannelCount(inputDevice);
AudioInputType inputPreference = AudioInputDevicePreference(inputDevice);
Maybe<AudioInputProcessingParamsRequest> processingRequest =
ToMaybeRef(native).map([](auto& native) {
return native.UpdateRequestedProcessingParams();
});
uint32_t primaryOutputChannelCount = PrimaryOutputChannelCount();
if (!audioCallbackDriver) {
if (primaryOutputChannelCount > 0) {
AudioCallbackDriver* driver = new AudioCallbackDriver(
this, CurrentDriver(), mSampleRate, primaryOutputChannelCount,
inputChannelCount, PrimaryOutputDeviceID(), inputDevice,
inputPreference, processingRequest);
SwitchAtNextIteration(driver);
}
return;
}
bool needInputProcessingParamUpdate =
processingRequest &&
processingRequest->mGeneration !=
audioCallbackDriver->RequestedInputProcessingParams().mGeneration;
// Check if this graph should switch to a different number of output channels.
// Generally, a driver switch is explicitly made by an event (e.g., setting
// the AudioDestinationNode channelCount), but if an HTMLMediaElement is
// directly playing back via another HTMLMediaElement, the number of channels
// of the media determines how many channels to output, and it can change
// dynamically.
if (primaryOutputChannelCount != audioCallbackDriver->OutputChannelCount()) {
if (needInputProcessingParamUpdate) {
needInputProcessingParamUpdate = false;
}
AudioCallbackDriver* driver = new AudioCallbackDriver(
this, CurrentDriver(), mSampleRate, primaryOutputChannelCount,
inputChannelCount, PrimaryOutputDeviceID(), inputDevice,
inputPreference, processingRequest);
SwitchAtNextIteration(driver);
}
if (needInputProcessingParamUpdate) {
needInputProcessingParamUpdate = false;
LOG(LogLevel::Debug,
("%p: Setting on the fly requested processing params %s (Gen %d)", this,
CubebUtils::ProcessingParamsToString(processingRequest->mParams).get(),
processingRequest->mGeneration));
audioCallbackDriver->RequestInputProcessingParams(*processingRequest);
}
}
void MediaTrackGraphImpl::UpdateTrackOrder() {
if (!mTrackOrderDirty) {
return;
}
mTrackOrderDirty = false;
// The algorithm for finding cycles is based on Tim Leslie's iterative
// implementation [1][2] of Pearce's variant [3] of Tarjan's strongly
// connected components (SCC) algorithm. There are variations (a) to
// distinguish whether tracks in SCCs of size 1 are in a cycle and (b) to
// re-run the algorithm over SCCs with breaks at DelayNodes.
//
// [2]
//
// There are two stacks. One for the depth-first search (DFS),
mozilla::LinkedList<MediaTrack> dfsStack;
// and another for tracks popped from the DFS stack, but still being
// considered as part of SCCs involving tracks on the stack.
mozilla::LinkedList<MediaTrack> sccStack;
// An index into mTracks for the next track found with no unsatisfied
// upstream dependencies.
uint32_t orderedTrackCount = 0;
for (uint32_t i = 0; i < mTracks.Length(); ++i) {
MediaTrack* t = mTracks[i];
ProcessedMediaTrack* pt = t->AsProcessedTrack();
if (pt) {
// The dfsStack initially contains a list of all processed tracks in
// unchanged order.
dfsStack.insertBack(t);
pt->mCycleMarker = NOT_VISITED;
} else {
// SourceMediaTracks have no inputs and so can be ordered now.
mTracks[orderedTrackCount] = t;
++orderedTrackCount;
}
}
// mNextStackMarker corresponds to "index" in Tarjan's algorithm. It is a
// counter to label mCycleMarker on the next visited track in the DFS
// uniquely in the set of visited tracks that are still being considered.
//
// In this implementation, the counter descends so that the values are
// strictly greater than the values that mCycleMarker takes when the track
// has been ordered (0 or IN_MUTED_CYCLE).
//
// Each new track labelled, as the DFS searches upstream, receives a value
// less than those used for all other tracks being considered.
uint32_t nextStackMarker = NOT_VISITED - 1;
// Reset list of DelayNodes in cycles stored at the tail of mTracks.
mFirstCycleBreaker = mTracks.Length();
// Rearrange dfsStack order as required to DFS upstream and pop tracks
// in processing order to place in mTracks.
while (auto pt = static_cast<ProcessedMediaTrack*>(dfsStack.getFirst())) {
const auto& inputs = pt->mInputs;
MOZ_ASSERT(pt->AsProcessedTrack());
if (pt->mCycleMarker == NOT_VISITED) {
// Record the position on the visited stack, so that any searches
// finding this track again know how much of the stack is in the cycle.
pt->mCycleMarker = nextStackMarker;
--nextStackMarker;
// Not-visited input tracks should be processed first.
// SourceMediaTracks have already been ordered.
for (uint32_t i = inputs.Length(); i--;) {
if (inputs[i]->GetSource()->IsSuspended()) {
continue;
}
auto input = inputs[i]->GetSource()->AsProcessedTrack();
if (input && input->mCycleMarker == NOT_VISITED) {
// It can be that this track has an input which is from a suspended
// AudioContext.
if (input->isInList()) {
input->remove();
dfsStack.insertFront(input);
}
}
}
continue;
}
// Returning from DFS. Pop from dfsStack.
pt->remove();
// cycleStackMarker keeps track of the highest marker value on any
// upstream track, if any, found receiving input, directly or indirectly,
// from the visited stack (and so from |ps|, making a cycle). In a
// variation from Tarjan's SCC algorithm, this does not include |ps|
// unless it is part of the cycle.
uint32_t cycleStackMarker = 0;
for (uint32_t i = inputs.Length(); i--;) {
if (inputs[i]->GetSource()->IsSuspended()) {
continue;
}
auto input = inputs[i]->GetSource()->AsProcessedTrack();
if (input) {
cycleStackMarker = std::max(cycleStackMarker, input->mCycleMarker);
}
}
if (cycleStackMarker <= IN_MUTED_CYCLE) {
// All inputs have been ordered and their stack markers have been removed.
// This track is not part of a cycle. It can be processed next.
pt->mCycleMarker = 0;
mTracks[orderedTrackCount] = pt;
++orderedTrackCount;
continue;
}
// A cycle has been found. Record this track for ordering when all
// tracks in this SCC have been popped from the DFS stack.
sccStack.insertFront(pt);
if (cycleStackMarker > pt->mCycleMarker) {
// Cycles have been found that involve tracks that remain on the stack.
// Leave mCycleMarker indicating the most downstream (last) track on
// the stack known to be part of this SCC. In this way, any searches on
// other paths that find |ps| will know (without having to traverse from
// this track again) that they are part of this SCC (i.e. part of an
// intersecting cycle).
pt->mCycleMarker = cycleStackMarker;
continue;
}
// |pit| is the root of an SCC involving no other tracks on dfsStack, the
// complete SCC has been recorded, and tracks in this SCC are part of at
// least one cycle.
MOZ_ASSERT(cycleStackMarker == pt->mCycleMarker);
// If there are DelayNodes in this SCC, then they may break the cycles.
bool haveDelayNode = false;
auto next = sccStack.getFirst();
// Tracks in this SCC are identified by mCycleMarker <= cycleStackMarker.
// (There may be other tracks later in sccStack from other incompletely
// searched SCCs, involving tracks still on dfsStack.)
//
// DelayNodes in cycles must behave differently from those not in cycles,
// so all DelayNodes in the SCC must be identified.
while (next && static_cast<ProcessedMediaTrack*>(next)->mCycleMarker <=
cycleStackMarker) {
auto nt = next->AsAudioNodeTrack();
// Get next before perhaps removing from list below.
next = next->getNext();
if (nt && nt->Engine()->AsDelayNodeEngine()) {
haveDelayNode = true;
// DelayNodes break cycles by producing their output in a
// preprocessing phase; they do not need to be ordered before their
// consumers. Order them at the tail of mTracks so that they can be
// handled specially. Do so now, so that DFS ignores them.
nt->remove();
nt->mCycleMarker = 0;
--mFirstCycleBreaker;
mTracks[mFirstCycleBreaker] = nt;
}
}
auto after_scc = next;
while ((next = sccStack.getFirst()) != after_scc) {
next->remove();
auto removed = static_cast<ProcessedMediaTrack*>(next);
if (haveDelayNode) {
// Return tracks to the DFS stack again (to order and detect cycles
// without delayNodes). Any of these tracks that are still inputs
// for tracks on the visited stack must be returned to the front of
// the stack to be ordered before their dependents. We know that none
// of these tracks need input from tracks on the visited stack, so
// they can all be searched and ordered before the current stack head
// is popped.
removed->mCycleMarker = NOT_VISITED;
dfsStack.insertFront(removed);
} else {
// Tracks in cycles without any DelayNodes must be muted, and so do
// not need input and can be ordered now. They must be ordered before
// their consumers so that their muted output is available.
removed->mCycleMarker = IN_MUTED_CYCLE;
mTracks[orderedTrackCount] = removed;
++orderedTrackCount;
}
}
}
MOZ_ASSERT(orderedTrackCount == mFirstCycleBreaker);
}
TrackTime MediaTrackGraphImpl::PlayAudio(const TrackAndVolume& aOutput,
GraphTime aPlayedTime,
uint32_t aOutputChannelCount) {
MOZ_ASSERT(OnGraphThread());
MOZ_ASSERT(mRealtime, "Should only attempt to play audio in realtime mode");
TrackTime ticksWritten = 0;
ticksWritten = 0;
MediaTrack* track = aOutput.mTrack;
AudioSegment* audio = track->GetData<AudioSegment>();
AudioSegment output;
TrackTime offset = track->GraphTimeToTrackTime(aPlayedTime);
// We don't update Track->mTracksStartTime here to account for time spent
// blocked. Instead, we'll update it in UpdateCurrentTimeForTracks after
// the blocked period has completed. But we do need to make sure we play
// from the right offsets in the track buffer, even if we've already
// written silence for some amount of blocked time after the current time.
GraphTime t = aPlayedTime;
while (t < mStateComputedTime) {
bool blocked = t >= track->mStartBlocking;
GraphTime end = blocked ? mStateComputedTime : track->mStartBlocking;
NS_ASSERTION(end <= mStateComputedTime, "mStartBlocking is wrong!");
// Check how many ticks of sound we can provide if we are blocked some
// time in the middle of this cycle.
TrackTime toWrite = end - t;
if (blocked) {
output.InsertNullDataAtStart(toWrite);
ticksWritten += toWrite;
LOG(LogLevel::Verbose,
("%p: MediaTrack %p writing %" PRId64 " blocking-silence samples for "
"%f to %f (%" PRId64 " to %" PRId64 ")",
this, track, toWrite, MediaTimeToSeconds(t), MediaTimeToSeconds(end),
offset, offset + toWrite));
} else {
TrackTime endTicksNeeded = offset + toWrite;
TrackTime endTicksAvailable = audio->GetDuration();
if (endTicksNeeded <= endTicksAvailable) {
LOG(LogLevel::Verbose,
("%p: MediaTrack %p writing %" PRId64 " samples for %f to %f "
"(samples %" PRId64 " to %" PRId64 ")",
this, track, toWrite, MediaTimeToSeconds(t),
MediaTimeToSeconds(end), offset, endTicksNeeded));
output.AppendSlice(*audio, offset, endTicksNeeded);
ticksWritten += toWrite;
offset = endTicksNeeded;
} else {
// MOZ_ASSERT(track->IsEnded(), "Not enough data, and track not
// ended."); If we are at the end of the track, maybe write the
// remaining samples, and pad with/output silence.
if (endTicksNeeded > endTicksAvailable && offset < endTicksAvailable) {
output.AppendSlice(*audio, offset, endTicksAvailable);
LOG(LogLevel::Verbose,
("%p: MediaTrack %p writing %" PRId64 " samples for %f to %f "
"(samples %" PRId64 " to %" PRId64 ")",
this, track, toWrite, MediaTimeToSeconds(t),
MediaTimeToSeconds(end), offset, endTicksNeeded));
uint32_t available = endTicksAvailable - offset;
ticksWritten += available;
toWrite -= available;
offset = endTicksAvailable;
}
output.AppendNullData(toWrite);
LOG(LogLevel::Verbose,
("%p MediaTrack %p writing %" PRId64 " padding slsamples for %f to "
"%f (samples %" PRId64 " to %" PRId64 ")",
this, track, toWrite, MediaTimeToSeconds(t),
MediaTimeToSeconds(end), offset, endTicksNeeded));
ticksWritten += toWrite;
}
output.ApplyVolume(mGlobalVolume * aOutput.mVolume);
}
t = end;
output.Mix(mMixer, aOutputChannelCount, mSampleRate);
}
return ticksWritten;
}
DeviceInputTrack* MediaTrackGraph::GetDeviceInputTrackMainThread(
CubebUtils::AudioDeviceID aID) {
MOZ_ASSERT(NS_IsMainThread());
auto* impl = static_cast<MediaTrackGraphImpl*>(this);
return impl->mDeviceInputTrackManagerMainThread.GetDeviceInputTrack(aID);
}
NativeInputTrack* MediaTrackGraph::GetNativeInputTrackMainThread() {
MOZ_ASSERT(NS_IsMainThread());
auto* impl = static_cast<MediaTrackGraphImpl*>(this);
return impl->mDeviceInputTrackManagerMainThread.GetNativeInputTrack();
}
void MediaTrackGraphImpl::OpenAudioInputImpl(DeviceInputTrack* aTrack) {
MOZ_ASSERT(OnGraphThread());
LOG(LogLevel::Debug,
("%p OpenAudioInputImpl: device %p", this, aTrack->mDeviceId));
mDeviceInputTrackManagerGraphThread.Add(aTrack);
if (aTrack->AsNativeInputTrack()) {
// Switch Drivers since we're adding input (to input-only or full-duplex)
AudioCallbackDriver* driver = new AudioCallbackDriver(
this, CurrentDriver(), mSampleRate, PrimaryOutputChannelCount(),
AudioInputChannelCount(aTrack->mDeviceId), PrimaryOutputDeviceID(),
aTrack->mDeviceId, AudioInputDevicePreference(aTrack->mDeviceId),
Some(aTrack->UpdateRequestedProcessingParams()));
LOG(LogLevel::Debug,
("%p OpenAudioInputImpl: starting new AudioCallbackDriver(input) %p",
this, driver));
SwitchAtNextIteration(driver);
} else {
NonNativeInputTrack* nonNative = aTrack->AsNonNativeInputTrack();
MOZ_ASSERT(nonNative);
// Start non-native input right away.
nonNative->StartAudio(MakeRefPtr<AudioInputSource>(
MakeRefPtr<AudioInputSourceListener>(nonNative),
nonNative->GenerateSourceId(), nonNative->mDeviceId,
AudioInputChannelCount(nonNative->mDeviceId),
AudioInputDevicePreference(nonNative->mDeviceId) ==
AudioInputType::Voice,
nonNative->mPrincipalHandle, nonNative->mSampleRate, GraphRate()));
}
}
void MediaTrackGraphImpl::OpenAudioInput(DeviceInputTrack* aTrack) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aTrack);
LOG(LogLevel::Debug, ("%p OpenInput: DeviceInputTrack %p for device %p", this,
aTrack, aTrack->mDeviceId));
class Message : public ControlMessage {
public:
Message(MediaTrackGraphImpl* aGraph, DeviceInputTrack* aInputTrack)
: ControlMessage(nullptr), mGraph(aGraph), mInputTrack(aInputTrack) {}
void Run() override {
TRACE("MTG::OpenAudioInputImpl ControlMessage");
mGraph->OpenAudioInputImpl(mInputTrack);
}
MediaTrackGraphImpl* mGraph;
DeviceInputTrack* mInputTrack;
};
mDeviceInputTrackManagerMainThread.Add(aTrack);
this->AppendMessage(MakeUnique<Message>(this, aTrack));
}
void MediaTrackGraphImpl::CloseAudioInputImpl(DeviceInputTrack* aTrack) {
MOZ_ASSERT(OnGraphThread());
LOG(LogLevel::Debug,
("%p CloseAudioInputImpl: device %p", this, aTrack->mDeviceId));
if (NonNativeInputTrack* nonNative = aTrack->AsNonNativeInputTrack()) {
nonNative->StopAudio();
mDeviceInputTrackManagerGraphThread.Remove(aTrack);
return;
}
MOZ_ASSERT(aTrack->AsNativeInputTrack());
mDeviceInputTrackManagerGraphThread.Remove(aTrack);
// Switch Drivers since we're adding or removing an input (to nothing/system
// or output only)
bool audioTrackPresent = AudioTrackPresent();
GraphDriver* driver;
if (audioTrackPresent) {
// We still have audio output
LOG(LogLevel::Debug,
("%p: CloseInput: output present (AudioCallback)", this));
driver = new AudioCallbackDriver(
this, CurrentDriver(), mSampleRate, PrimaryOutputChannelCount(),
AudioInputChannelCount(aTrack->mDeviceId), PrimaryOutputDeviceID(),
nullptr, AudioInputDevicePreference(aTrack->mDeviceId),
Some(aTrack->UpdateRequestedProcessingParams()));
SwitchAtNextIteration(driver);
} else if (CurrentDriver()->AsAudioCallbackDriver()) {
LOG(LogLevel::Debug,
("%p: CloseInput: no output present (SystemClockCallback)", this));
driver = new SystemClockDriver(this, CurrentDriver(), mSampleRate);
SwitchAtNextIteration(driver);
} // else SystemClockDriver->SystemClockDriver, no switch
}
void MediaTrackGraphImpl::UnregisterAllAudioOutputs(MediaTrack* aTrack) {
MOZ_ASSERT(OnGraphThreadOrNotRunning());
mOutputDevices.RemoveElementsBy([&](OutputDeviceEntry& aDeviceRef) {
aDeviceRef.mTrackOutputs.RemoveElement(aTrack);
// mReceiver is null for the primary output device, which is retained for
// AudioCallbackDriver output even when no tracks have audio outputs.
return aDeviceRef.mTrackOutputs.IsEmpty() && aDeviceRef.mReceiver;
});
}
void MediaTrackGraphImpl::CloseAudioInput(DeviceInputTrack* aTrack) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aTrack);
LOG(LogLevel::Debug, ("%p CloseInput: DeviceInputTrack %p for device %p",
this, aTrack, aTrack->mDeviceId));
class Message : public ControlMessage {
public:
Message(MediaTrackGraphImpl* aGraph, DeviceInputTrack* aInputTrack)
: ControlMessage(nullptr), mGraph(aGraph), mInputTrack(aInputTrack) {}
void Run() override {
TRACE("MTG::CloseAudioInputImpl ControlMessage");
mGraph->CloseAudioInputImpl(mInputTrack);
}
MediaTrackGraphImpl* mGraph;
DeviceInputTrack* mInputTrack;
};
// DeviceInputTrack is still alive (in mTracks) even we remove it here, since
// aTrack->Destroy() is called after this. See DeviceInputTrack::CloseAudio
// for more details.
mDeviceInputTrackManagerMainThread.Remove(aTrack);
this->AppendMessage(MakeUnique<Message>(this, aTrack));
if (aTrack->AsNativeInputTrack()) {
LOG(LogLevel::Debug,
("%p Native input device %p is closed!", this, aTrack->mDeviceId));
SetNewNativeInput();
}
}
// All AudioInput listeners get the same speaker data (at least for now).
void MediaTrackGraphImpl::NotifyOutputData(const AudioChunk& aChunk) {
if (!mDeviceInputTrackManagerGraphThread.GetNativeInputTrack()) {
return;
}
#if defined(MOZ_WEBRTC)
for (const auto& track : mTracks) {
if (const auto& t = track->AsAudioProcessingTrack()) {
t->NotifyOutputData(this, aChunk);
}
}
#endif
}
void MediaTrackGraphImpl::NotifyInputStopped() {
NativeInputTrack* native =
mDeviceInputTrackManagerGraphThread.GetNativeInputTrack();
if (!native) {
return;
}
native->NotifyInputStopped(this);
}
void MediaTrackGraphImpl::NotifyInputData(const AudioDataValue* aBuffer,
size_t aFrames, TrackRate aRate,
uint32_t aChannels,
uint32_t aAlreadyBuffered) {
// Either we have an audio input device, or we just removed the audio input
// this iteration, and we're switching back to an output-only driver next
// iteration.
NativeInputTrack* native =
mDeviceInputTrackManagerGraphThread.GetNativeInputTrack();
MOZ_ASSERT(native || Switching());
if (!native) {
return;
}
native->NotifyInputData(this, aBuffer, aFrames, aRate, aChannels,
aAlreadyBuffered);
}
void MediaTrackGraphImpl::NotifySetRequestedInputProcessingParamsResult(
AudioCallbackDriver* aDriver, int aGeneration,
Result<cubeb_input_processing_params, int>&& aResult) {
MOZ_ASSERT(NS_IsMainThread());
NativeInputTrack* native =
mDeviceInputTrackManagerMainThread.GetNativeInputTrack();
if (!native) {
return;
}
QueueControlMessageWithNoShutdown([this, self = RefPtr(this),
driver = RefPtr(aDriver), aGeneration,
result = std::move(aResult)]() mutable {
NativeInputTrack* native =
mDeviceInputTrackManagerGraphThread.GetNativeInputTrack();
if (!native) {
return;
}
if (driver != mDriver) {
return;
}
native->NotifySetRequestedProcessingParamsResult(this, aGeneration, result);
});
}
void MediaTrackGraphImpl::DeviceChangedImpl() {
MOZ_ASSERT(OnGraphThread());
NativeInputTrack* native =
mDeviceInputTrackManagerGraphThread.GetNativeInputTrack();
if (!native) {
return;
}
native->DeviceChanged(this);
}
void MediaTrackGraphImpl::SetMaxOutputChannelCount(uint32_t aMaxChannelCount) {
MOZ_ASSERT(OnGraphThread());
mMaxOutputChannelCount = aMaxChannelCount;
}
void MediaTrackGraphImpl::DeviceChanged() {
// This is safe to be called from any thread: this message comes from an
// underlying platform API, and we don't have much guarantees. If it is not
// called from the main thread (and it probably will rarely be), it will post
// itself to the main thread, and the actual device change message will be ran
// and acted upon on the graph thread.
if (!NS_IsMainThread()) {
RefPtr<nsIRunnable> runnable = WrapRunnable(
RefPtr<MediaTrackGraphImpl>(this), &MediaTrackGraphImpl::DeviceChanged);
mMainThread->Dispatch(runnable.forget());
return;
}
class Message : public ControlMessage {
public:
explicit