mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 04:41:11 +00:00
Bug 1869043 allow a device to be specified with MediaTrack::AddAudioOutput() r=padenot
Output to secondary devices is supported through CrossGraphReceivers on other graphs created when required. Differential Revision: https://phabricator.services.mozilla.com/D198229
This commit is contained in:
parent
28fcad562f
commit
7b15ffca4e
@ -29,6 +29,7 @@ class AudioDeviceInfo final : public nsIAudioDeviceInfo {
|
||||
|
||||
AudioDeviceID DeviceID() const;
|
||||
const nsString& Name() const;
|
||||
uint32_t DefaultRate() const { return mDefaultRate; }
|
||||
uint32_t MaxChannels() const;
|
||||
uint32_t Type() const;
|
||||
uint32_t State() const;
|
||||
|
@ -22,7 +22,7 @@ RefPtr<GenericPromise> AudioStreamTrack::AddAudioOutput(
|
||||
UniquePtr<CrossGraphPort> manager;
|
||||
if (!aSink || !(manager = CrossGraphPort::Connect(this, aSink, mWindow))) {
|
||||
// We are setting the default output device.
|
||||
mTrack->AddAudioOutput(aKey);
|
||||
mTrack->AddAudioOutput(aKey, nullptr);
|
||||
return GenericPromise::CreateAndResolve(true, __func__);
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,8 @@ UniquePtr<CrossGraphPort> CrossGraphPort::Connect(
|
||||
}
|
||||
|
||||
void CrossGraphPort::AddAudioOutput(void* aKey) {
|
||||
mReceiver->AddAudioOutput(aKey);
|
||||
mReceiver->AddAudioOutput(aKey, mReceiver->Graph()->PrimaryOutputDeviceID(),
|
||||
0);
|
||||
}
|
||||
|
||||
void CrossGraphPort::RemoveAudioOutput(void* aKey) {
|
||||
|
@ -853,9 +853,11 @@ void MediaTrackGraphImpl::CloseAudioInputImpl(DeviceInputTrack* aTrack) {
|
||||
|
||||
void MediaTrackGraphImpl::UnregisterAllAudioOutputs(MediaTrack* aTrack) {
|
||||
MOZ_ASSERT(OnGraphThreadOrNotRunning());
|
||||
|
||||
mAudioOutputs.RemoveElementsBy([aTrack](const TrackAndVolume& aOutput) {
|
||||
return aOutput.mTrack == aTrack;
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
@ -1434,19 +1436,29 @@ void MediaTrackGraphImpl::Process(MixerCallbackReceiver* aMixerReceiver) {
|
||||
}
|
||||
mProcessedTime = mStateComputedTime;
|
||||
|
||||
if (aMixerReceiver) {
|
||||
MOZ_ASSERT(mRealtime, "If there's a mixer, this graph must be realtime");
|
||||
MOZ_ASSERT(CurrentDriver()->AsAudioCallbackDriver(),
|
||||
"Driver must be AudioCallbackDriver if aMixerReceiver");
|
||||
// Use the number of channel the driver expects: this is the number of
|
||||
// channel that can be output by the underlying system level audio stream.
|
||||
uint32_t outputChannelCount =
|
||||
CurrentDriver()->AsAudioCallbackDriver()->OutputChannelCount();
|
||||
for (const auto& outputDeviceEntry : mOutputDevices) {
|
||||
uint32_t outputChannelCount;
|
||||
if (!outputDeviceEntry.mReceiver) { // primary output
|
||||
if (!aMixerReceiver) {
|
||||
// Running off a system clock driver. No need to mix output.
|
||||
continue;
|
||||
}
|
||||
MOZ_ASSERT(CurrentDriver()->AsAudioCallbackDriver(),
|
||||
"Driver must be AudioCallbackDriver if aMixerReceiver");
|
||||
// Use the number of channel the driver expects: this is the number of
|
||||
// channel that can be output by the underlying system level audio stream.
|
||||
outputChannelCount =
|
||||
CurrentDriver()->AsAudioCallbackDriver()->OutputChannelCount();
|
||||
} else {
|
||||
outputChannelCount = AudioOutputChannelCount(outputDeviceEntry);
|
||||
}
|
||||
MOZ_ASSERT(mRealtime,
|
||||
"If there's an output device, this graph must be realtime");
|
||||
mMixer.StartMixing();
|
||||
// This is the number of frames that are written to the output buffer, for
|
||||
// this iteration.
|
||||
TrackTime ticksPlayed = 0;
|
||||
for (auto& t : mAudioOutputs) {
|
||||
for (const auto& t : outputDeviceEntry.mTrackOutputs) {
|
||||
TrackTime ticksPlayedForThisTrack =
|
||||
PlayAudio(t, oldProcessedTime, outputChannelCount);
|
||||
if (ticksPlayed == 0) {
|
||||
@ -1465,7 +1477,12 @@ void MediaTrackGraphImpl::Process(MixerCallbackReceiver* aMixerReceiver) {
|
||||
mMixer.Mix(nullptr, outputChannelCount,
|
||||
mStateComputedTime - oldProcessedTime, mSampleRate);
|
||||
}
|
||||
aMixerReceiver->MixerCallback(mMixer.MixedChunk(), mSampleRate);
|
||||
AudioChunk* outputChunk = mMixer.MixedChunk();
|
||||
if (!outputDeviceEntry.mReceiver) { // primary output
|
||||
aMixerReceiver->MixerCallback(outputChunk, mSampleRate);
|
||||
} else {
|
||||
outputDeviceEntry.mReceiver->EnqueueAudio(*outputChunk);
|
||||
}
|
||||
}
|
||||
|
||||
if (!allBlockedForever) {
|
||||
@ -2263,13 +2280,25 @@ TrackTime MediaTrack::GetEnd() const {
|
||||
return mSegment ? mSegment->GetDuration() : 0;
|
||||
}
|
||||
|
||||
void MediaTrack::AddAudioOutput(void* aKey) {
|
||||
void MediaTrack::AddAudioOutput(void* aKey, const AudioDeviceInfo* aSink) {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
AudioDeviceID deviceID = nullptr;
|
||||
TrackRate preferredSampleRate = 0;
|
||||
if (aSink) {
|
||||
deviceID = aSink->DeviceID();
|
||||
preferredSampleRate = static_cast<TrackRate>(aSink->DefaultRate());
|
||||
}
|
||||
AddAudioOutput(aKey, deviceID, preferredSampleRate);
|
||||
}
|
||||
|
||||
void MediaTrack::AddAudioOutput(void* aKey, CubebUtils::AudioDeviceID aDeviceID,
|
||||
TrackRate aPreferredSampleRate) {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
if (mMainThreadDestroyed) {
|
||||
return;
|
||||
}
|
||||
LOG(LogLevel::Info, ("MediaTrack %p adding AudioOutput", this));
|
||||
GraphImpl()->RegisterAudioOutput(this, aKey);
|
||||
GraphImpl()->RegisterAudioOutput(this, aKey, aDeviceID, aPreferredSampleRate);
|
||||
}
|
||||
|
||||
void MediaTrackGraphImpl::SetAudioOutputVolume(MediaTrack* aTrack, void* aKey,
|
||||
@ -2278,7 +2307,7 @@ void MediaTrackGraphImpl::SetAudioOutputVolume(MediaTrack* aTrack, void* aKey,
|
||||
for (auto& params : mAudioOutputParams) {
|
||||
if (params.mKey == aKey && aTrack == params.mTrack) {
|
||||
params.mVolume = aVolume;
|
||||
UpdateAudioOutput(aTrack);
|
||||
UpdateAudioOutput(aTrack, params.mDeviceID);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -2301,13 +2330,18 @@ void MediaTrack::RemoveAudioOutput(void* aKey) {
|
||||
GraphImpl()->UnregisterAudioOutput(this, aKey);
|
||||
}
|
||||
|
||||
void MediaTrackGraphImpl::RegisterAudioOutput(MediaTrack* aTrack, void* aKey) {
|
||||
void MediaTrackGraphImpl::RegisterAudioOutput(
|
||||
MediaTrack* aTrack, void* aKey, CubebUtils::AudioDeviceID aDeviceID,
|
||||
TrackRate aPreferredSampleRate) {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(!mAudioOutputParams.Contains(TrackAndKey{aTrack, aKey}));
|
||||
|
||||
mAudioOutputParams.EmplaceBack(TrackKeyAndVolume{aTrack, aKey, 1.f});
|
||||
IncrementOutputDeviceRefCnt(aDeviceID, aPreferredSampleRate);
|
||||
|
||||
UpdateAudioOutput(aTrack);
|
||||
mAudioOutputParams.EmplaceBack(
|
||||
TrackKeyDeviceAndVolume{aTrack, aKey, aDeviceID, 1.f});
|
||||
|
||||
UpdateAudioOutput(aTrack, aDeviceID);
|
||||
}
|
||||
|
||||
void MediaTrackGraphImpl::UnregisterAudioOutput(MediaTrack* aTrack,
|
||||
@ -2316,43 +2350,110 @@ void MediaTrackGraphImpl::UnregisterAudioOutput(MediaTrack* aTrack,
|
||||
|
||||
size_t index = mAudioOutputParams.IndexOf(TrackAndKey{aTrack, aKey});
|
||||
MOZ_ASSERT(index != mAudioOutputParams.NoIndex);
|
||||
AudioDeviceID deviceID = mAudioOutputParams[index].mDeviceID;
|
||||
mAudioOutputParams.UnorderedRemoveElementAt(index);
|
||||
|
||||
UpdateAudioOutput(aTrack);
|
||||
UpdateAudioOutput(aTrack, deviceID);
|
||||
|
||||
DecrementOutputDeviceRefCnt(deviceID);
|
||||
}
|
||||
|
||||
void MediaTrackGraphImpl::UpdateAudioOutput(MediaTrack* aTrack) {
|
||||
void MediaTrackGraphImpl::UpdateAudioOutput(MediaTrack* aTrack,
|
||||
AudioDeviceID aDeviceID) {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(!aTrack->IsDestroyed());
|
||||
|
||||
float volume = 0.f;
|
||||
bool found = false;
|
||||
for (const auto& params : mAudioOutputParams) {
|
||||
if (params.mTrack == aTrack) {
|
||||
if (params.mTrack == aTrack && params.mDeviceID == aDeviceID) {
|
||||
volume += params.mVolume;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
QueueControlMessageWithNoShutdown([track = RefPtr{aTrack}, volume, found] {
|
||||
TRACE("MediaTrack::UpdateAudioOutput ControlMessage");
|
||||
MediaTrackGraphImpl* graph = track->GraphImpl();
|
||||
auto& audioOutputsRef = graph->mAudioOutputs;
|
||||
if (found) {
|
||||
for (auto& outputRef : audioOutputsRef) {
|
||||
if (outputRef.mTrack == track) {
|
||||
outputRef.mVolume = volume;
|
||||
return;
|
||||
QueueControlMessageWithNoShutdown(
|
||||
// track has a strong reference to this.
|
||||
[track = RefPtr{aTrack}, aDeviceID, volume, found] {
|
||||
TRACE("MediaTrack::UpdateAudioOutput ControlMessage");
|
||||
MediaTrackGraphImpl* graph = track->GraphImpl();
|
||||
auto& outputDevicesRef = graph->mOutputDevices;
|
||||
size_t deviceIndex = outputDevicesRef.IndexOf(aDeviceID);
|
||||
MOZ_ASSERT(deviceIndex != outputDevicesRef.NoIndex);
|
||||
auto& deviceOutputsRef = outputDevicesRef[deviceIndex].mTrackOutputs;
|
||||
if (found) {
|
||||
for (auto& outputRef : deviceOutputsRef) {
|
||||
if (outputRef.mTrack == track) {
|
||||
outputRef.mVolume = volume;
|
||||
return;
|
||||
}
|
||||
}
|
||||
deviceOutputsRef.EmplaceBack(TrackAndVolume{track, volume});
|
||||
} else {
|
||||
DebugOnly<bool> removed = deviceOutputsRef.RemoveElement(track);
|
||||
MOZ_ASSERT(removed);
|
||||
// mOutputDevices[0] is retained for AudioCallbackDriver output even
|
||||
// when no tracks have audio outputs.
|
||||
if (deviceIndex != 0 && deviceOutputsRef.IsEmpty()) {
|
||||
// The device is no longer in use.
|
||||
outputDevicesRef.UnorderedRemoveElementAt(deviceIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
audioOutputsRef.EmplaceBack(TrackAndVolume{track, volume});
|
||||
} else {
|
||||
DebugOnly<bool> removed = audioOutputsRef.RemoveElement(track);
|
||||
MOZ_ASSERT(removed);
|
||||
});
|
||||
}
|
||||
|
||||
void MediaTrackGraphImpl::IncrementOutputDeviceRefCnt(
|
||||
AudioDeviceID aDeviceID, TrackRate aPreferredSampleRate) {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
for (auto& elementRef : mOutputDeviceRefCnts) {
|
||||
if (elementRef.mDeviceID == aDeviceID) {
|
||||
++elementRef.mRefCnt;
|
||||
return;
|
||||
}
|
||||
}
|
||||
MOZ_ASSERT(aDeviceID != mPrimaryOutputDeviceID,
|
||||
"mOutputDeviceRefCnts should always have the primary device");
|
||||
// Need to add an output device.
|
||||
// Output via another graph for this device.
|
||||
// This sample rate is not exposed to content.
|
||||
TrackRate sampleRate =
|
||||
aPreferredSampleRate != 0
|
||||
? aPreferredSampleRate
|
||||
: static_cast<TrackRate>(CubebUtils::PreferredSampleRate(
|
||||
/*aShouldResistFingerprinting*/ false));
|
||||
MediaTrackGraph* newGraph = MediaTrackGraphImpl::GetInstance(
|
||||
MediaTrackGraph::AUDIO_THREAD_DRIVER, mWindowID, sampleRate, aDeviceID,
|
||||
GetMainThreadSerialEventTarget());
|
||||
// CreateCrossGraphReceiver wants the sample rate of this graph.
|
||||
RefPtr receiver = newGraph->CreateCrossGraphReceiver(mSampleRate);
|
||||
receiver->AddAudioOutput(nullptr, aDeviceID, sampleRate);
|
||||
mOutputDeviceRefCnts.EmplaceBack(
|
||||
DeviceReceiverAndCount{aDeviceID, receiver, 1});
|
||||
|
||||
QueueControlMessageWithNoShutdown([self = RefPtr{this}, this, aDeviceID,
|
||||
receiver = std::move(receiver)]() mutable {
|
||||
TRACE("MediaTrackGraph add output device ControlMessage");
|
||||
MOZ_ASSERT(!mOutputDevices.Contains(aDeviceID));
|
||||
mOutputDevices.EmplaceBack(
|
||||
OutputDeviceEntry{aDeviceID, std::move(receiver)});
|
||||
});
|
||||
}
|
||||
|
||||
void MediaTrackGraphImpl::DecrementOutputDeviceRefCnt(AudioDeviceID aDeviceID) {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
size_t index = mOutputDeviceRefCnts.IndexOf(aDeviceID);
|
||||
MOZ_ASSERT(index != mOutputDeviceRefCnts.NoIndex);
|
||||
// mOutputDeviceRefCnts[0] is retained for consistency with
|
||||
// mOutputDevices[0], which is retained for AudioCallbackDriver output even
|
||||
// when no tracks have audio outputs.
|
||||
if (--mOutputDeviceRefCnts[index].mRefCnt == 0 && index != 0) {
|
||||
mOutputDeviceRefCnts[index].mReceiver->Destroy();
|
||||
mOutputDeviceRefCnts.UnorderedRemoveElementAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
void MediaTrack::Suspend() {
|
||||
// This can happen if this method has been called asynchronously, and the
|
||||
// track has been destroyed since then.
|
||||
@ -3262,6 +3363,13 @@ MediaTrackGraphImpl::MediaTrackGraphImpl(
|
||||
mMainThreadGraphTime(0, "MediaTrackGraphImpl::mMainThreadGraphTime"),
|
||||
mAudioOutputLatency(0.0),
|
||||
mMaxOutputChannelCount(std::min(8u, CubebUtils::MaxNumberOfChannels())) {
|
||||
// The primary output device always exists because an AudioCallbackDriver
|
||||
// may exist, and want to be fed data, even when no tracks have audio
|
||||
// outputs.
|
||||
mOutputDeviceRefCnts.EmplaceBack(
|
||||
DeviceReceiverAndCount{aPrimaryOutputDeviceID, nullptr, 0});
|
||||
mOutputDevices.EmplaceBack(OutputDeviceEntry{aPrimaryOutputDeviceID});
|
||||
|
||||
bool failedToGetShutdownBlocker = false;
|
||||
if (!IsNonRealtime()) {
|
||||
failedToGetShutdownBlocker = !AddShutdownBlocker();
|
||||
@ -3615,12 +3723,14 @@ void MediaTrackGraphImpl::RemoveTrack(MediaTrack* aTrack) {
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_DIAGNOSTIC_ASSERT(mMainThreadTrackCount > 0);
|
||||
|
||||
mAudioOutputParams.RemoveElementsBy([&](const TrackKeyAndVolume& aElement) {
|
||||
if (aElement.mTrack != aTrack) {
|
||||
return false;
|
||||
};
|
||||
return true;
|
||||
});
|
||||
mAudioOutputParams.RemoveElementsBy(
|
||||
[&](const TrackKeyDeviceAndVolume& aElement) {
|
||||
if (aElement.mTrack != aTrack) {
|
||||
return false;
|
||||
};
|
||||
DecrementOutputDeviceRefCnt(aElement.mDeviceID);
|
||||
return true;
|
||||
});
|
||||
|
||||
if (--mMainThreadTrackCount == 0) {
|
||||
LOG(LogLevel::Info, ("MediaTrackGraph %p, last track %p removed from "
|
||||
@ -3837,19 +3947,26 @@ auto MediaTrackGraph::ApplyAudioContextOperation(
|
||||
}
|
||||
|
||||
uint32_t MediaTrackGraphImpl::PrimaryOutputChannelCount() const {
|
||||
MOZ_ASSERT(!mOutputDevices[0].mReceiver);
|
||||
return AudioOutputChannelCount(mOutputDevices[0]);
|
||||
}
|
||||
|
||||
uint32_t MediaTrackGraphImpl::AudioOutputChannelCount(
|
||||
const OutputDeviceEntry& aDevice) const {
|
||||
MOZ_ASSERT(OnGraphThread());
|
||||
// The audio output channel count for a graph is the maximum of the output
|
||||
// channel count of all the tracks that are in mAudioOutputs, or the max audio
|
||||
// output channel count the machine can do, whichever is smaller.
|
||||
uint32_t channelCount = 0;
|
||||
for (const auto& output : mAudioOutputs) {
|
||||
for (const auto& output : aDevice.mTrackOutputs) {
|
||||
channelCount = std::max(channelCount, output.mTrack->NumberOfChannels());
|
||||
}
|
||||
channelCount = std::min(channelCount, mMaxOutputChannelCount);
|
||||
if (channelCount) {
|
||||
return channelCount;
|
||||
} else {
|
||||
if (CurrentDriver()->AsAudioCallbackDriver()) {
|
||||
// null aDevice.mReceiver indicates the primary graph output device.
|
||||
if (!aDevice.mReceiver && CurrentDriver()->AsAudioCallbackDriver()) {
|
||||
return CurrentDriver()->AsAudioCallbackDriver()->OutputChannelCount();
|
||||
}
|
||||
return 2;
|
||||
|
@ -269,7 +269,9 @@ class MediaTrack : public mozilla::LinkedListElement<MediaTrack> {
|
||||
void SetGraphImpl(MediaTrackGraph* aGraph);
|
||||
|
||||
// Control API.
|
||||
void AddAudioOutput(void* aKey);
|
||||
void AddAudioOutput(void* aKey, const AudioDeviceInfo* aSink);
|
||||
void AddAudioOutput(void* aKey, CubebUtils::AudioDeviceID aDeviceID,
|
||||
TrackRate aPreferredSampleRate);
|
||||
void SetAudioOutputVolume(void* aKey, float aVolume);
|
||||
void RemoveAudioOutput(void* aKey);
|
||||
// Explicitly suspend. Useful for example if a media element is pausing
|
||||
|
@ -203,14 +203,25 @@ class MediaTrackGraphImpl : public MediaTrackGraph,
|
||||
std::forward<Function>(aFunction))));
|
||||
}
|
||||
/* Add or remove an audio output for this track. At most one output may be
|
||||
* registered per key. */
|
||||
void RegisterAudioOutput(MediaTrack* aTrack, void* aKey);
|
||||
* registered per key. aPreferredSampleRate is the rate preferred by the
|
||||
* output device; it may be zero to indicate the preferred rate for the
|
||||
* default device; it is unused when aDeviceID is the graph's primary output.
|
||||
*/
|
||||
void RegisterAudioOutput(MediaTrack* aTrack, void* aKey,
|
||||
CubebUtils::AudioDeviceID aDeviceID,
|
||||
TrackRate aPreferredSampleRate);
|
||||
void UnregisterAudioOutput(MediaTrack* aTrack, void* aKey);
|
||||
|
||||
void SetAudioOutputVolume(MediaTrack* aTrack, void* aKey, float aVolume);
|
||||
/* Send a control message to update mAudioOutputs for main thread changes to
|
||||
/* Manage the creation and destruction of CrossGraphReceivers.
|
||||
* aPreferredSampleRate is the rate preferred by the output device. */
|
||||
void IncrementOutputDeviceRefCnt(CubebUtils::AudioDeviceID aDeviceID,
|
||||
TrackRate aPreferredSampleRate);
|
||||
void DecrementOutputDeviceRefCnt(CubebUtils::AudioDeviceID aDeviceID);
|
||||
/* Send a control message to update mOutputDevices for main thread changes to
|
||||
* mAudioOutputParams. */
|
||||
void UpdateAudioOutput(MediaTrack* aTrack);
|
||||
void UpdateAudioOutput(MediaTrack* aTrack,
|
||||
CubebUtils::AudioDeviceID aDeviceID);
|
||||
/**
|
||||
* Dispatches a runnable from any thread to the correct main thread for this
|
||||
* MediaTrackGraph.
|
||||
@ -515,9 +526,16 @@ class MediaTrackGraphImpl : public MediaTrackGraph,
|
||||
mTrackOrderDirty = true;
|
||||
}
|
||||
|
||||
private:
|
||||
// Get the current maximum channel count required for a device.
|
||||
// aDevice is an element of mOutputDevices. Graph thread only.
|
||||
struct OutputDeviceEntry;
|
||||
uint32_t AudioOutputChannelCount(const OutputDeviceEntry& aDevice) const;
|
||||
// Get the current maximum channel count for audio output through an
|
||||
// AudioCallbackDriver. Graph thread only.
|
||||
uint32_t PrimaryOutputChannelCount() const;
|
||||
|
||||
public:
|
||||
// Set a new maximum channel count. Graph thread only.
|
||||
void SetMaxOutputChannelCount(uint32_t aMaxChannelCount);
|
||||
|
||||
@ -982,28 +1000,52 @@ class MediaTrackGraphImpl : public MediaTrackGraph,
|
||||
|
||||
/**
|
||||
* Main thread unordered record of audio outputs, keyed by Track and output
|
||||
* key. Used to record the volumes corresponding to each key. An array is
|
||||
* used as a simple hash table, on the assumption that the number of outputs
|
||||
* is small.
|
||||
* key. Used to determine when an output device is no longer in use and to
|
||||
* record the volumes corresponding to each key. An array is used as a
|
||||
* simple hash table, on the assumption that the number of outputs is small.
|
||||
*/
|
||||
struct TrackAndKey {
|
||||
MOZ_UNSAFE_REF("struct exists only if track exists") MediaTrack* mTrack;
|
||||
void* mKey;
|
||||
};
|
||||
struct TrackKeyAndVolume {
|
||||
struct TrackKeyDeviceAndVolume {
|
||||
MOZ_UNSAFE_REF("struct exists only if track exists")
|
||||
MediaTrack* const mTrack;
|
||||
void* const mKey;
|
||||
const CubebUtils::AudioDeviceID mDeviceID;
|
||||
float mVolume;
|
||||
|
||||
bool operator==(const TrackAndKey& aTrackAndKey) const {
|
||||
return mTrack == aTrackAndKey.mTrack && mKey == aTrackAndKey.mKey;
|
||||
}
|
||||
};
|
||||
nsTArray<TrackKeyAndVolume> mAudioOutputParams;
|
||||
nsTArray<TrackKeyDeviceAndVolume> mAudioOutputParams;
|
||||
/**
|
||||
* Mapping from MediaTrack to volume for all tracks that have their audio
|
||||
* output mixed and written to an audio output device. Graph thread.
|
||||
* Main thread record of which audio output devices are active, keyed by
|
||||
* AudioDeviceID, and their CrossGraphReceivers if any.
|
||||
* mOutputDeviceRefCnts[0] always exists and corresponds to the primary
|
||||
* audio output device, which an AudioCallbackDriver will use if active.
|
||||
* mCount may be zero for the first entry only. */
|
||||
struct DeviceReceiverAndCount {
|
||||
const CubebUtils::AudioDeviceID mDeviceID;
|
||||
// For secondary devices, mReceiver receives audio output.
|
||||
// Null for the primary output device, fed by an AudioCallbackDriver.
|
||||
const RefPtr<CrossGraphReceiver> mReceiver;
|
||||
size_t mRefCnt; // number of mAudioOutputParams entries with this device
|
||||
|
||||
bool operator==(CubebUtils::AudioDeviceID aDeviceID) const {
|
||||
return mDeviceID == aDeviceID;
|
||||
}
|
||||
};
|
||||
nsTArray<DeviceReceiverAndCount> mOutputDeviceRefCnts;
|
||||
/**
|
||||
* Graph thread record of devices to which audio outputs are mixed, keyed by
|
||||
* AudioDeviceID. All tracks that have an audio output to each device are
|
||||
* grouped for mixing their outputs to a single stream.
|
||||
* mOutputDevices[0] always exists and corresponds to the primary audio
|
||||
* output device, which an AudioCallbackDriver will use if active.
|
||||
* An AudioCallbackDriver may be active when no audio outputs have audio
|
||||
* outputs.
|
||||
*/
|
||||
struct TrackAndVolume {
|
||||
MOZ_UNSAFE_REF("struct exists only if track exists")
|
||||
@ -1012,7 +1054,22 @@ class MediaTrackGraphImpl : public MediaTrackGraph,
|
||||
|
||||
bool operator==(const MediaTrack* aTrack) const { return mTrack == aTrack; }
|
||||
};
|
||||
nsTArray<TrackAndVolume> mAudioOutputs;
|
||||
struct OutputDeviceEntry {
|
||||
const CubebUtils::AudioDeviceID mDeviceID;
|
||||
// For secondary devices, mReceiver receives audio output.
|
||||
// Null for the primary output device, fed by an AudioCallbackDriver.
|
||||
const RefPtr<CrossGraphReceiver> mReceiver;
|
||||
/**
|
||||
* Mapping from MediaTrack to volume for all tracks that have their audio
|
||||
* output mixed and written to this output device.
|
||||
*/
|
||||
nsTArray<TrackAndVolume> mTrackOutputs;
|
||||
|
||||
bool operator==(CubebUtils::AudioDeviceID aDeviceID) const {
|
||||
return mDeviceID == aDeviceID;
|
||||
}
|
||||
};
|
||||
nsTArray<OutputDeviceEntry> mOutputDevices;
|
||||
|
||||
/**
|
||||
* Global volume scale. Used when running tests so that the output is not too
|
||||
|
@ -209,7 +209,9 @@ class MockCubebStream {
|
||||
MediaEventSource<nsCString>& NameSetEvent();
|
||||
MediaEventSource<cubeb_state>& StateEvent();
|
||||
MediaEventSource<uint32_t>& FramesProcessedEvent();
|
||||
// Notified when frames are processed after first non-silent output
|
||||
MediaEventSource<uint32_t>& FramesVerifiedEvent();
|
||||
// Notified when the stream is Stop()ed
|
||||
MediaEventSource<std::tuple<uint64_t, float, uint32_t>>&
|
||||
OutputVerificationEvent();
|
||||
MediaEventSource<void>& ErrorForcedEvent();
|
||||
|
@ -1189,7 +1189,7 @@ TEST(TestAudioTrackGraph, ErrorCallback)
|
||||
processingTrack->ConnectDeviceInput(deviceId, listener,
|
||||
PRINCIPAL_HANDLE_NONE);
|
||||
EXPECT_EQ(processingTrack->DeviceId().value(), deviceId);
|
||||
processingTrack->AddAudioOutput(reinterpret_cast<void*>(1));
|
||||
processingTrack->AddAudioOutput(reinterpret_cast<void*>(1), nullptr);
|
||||
return graph->NotifyWhenDeviceStarted(processingTrack);
|
||||
});
|
||||
|
||||
@ -1248,7 +1248,7 @@ TEST(TestAudioTrackGraph, AudioProcessingTrack)
|
||||
processingTrack = AudioProcessingTrack::Create(graph);
|
||||
outputTrack = graph->CreateForwardedInputTrack(MediaSegment::AUDIO);
|
||||
outputTrack->QueueSetAutoend(false);
|
||||
outputTrack->AddAudioOutput(reinterpret_cast<void*>(1));
|
||||
outputTrack->AddAudioOutput(reinterpret_cast<void*>(1), nullptr);
|
||||
port = outputTrack->AllocateInputPort(processingTrack);
|
||||
/* Primary graph: Open Audio Input through SourceMediaTrack */
|
||||
listener = new AudioInputProcessing(2);
|
||||
@ -1339,7 +1339,7 @@ TEST(TestAudioTrackGraph, ReConnectDeviceInput)
|
||||
processingTrack = AudioProcessingTrack::Create(graph);
|
||||
outputTrack = graph->CreateForwardedInputTrack(MediaSegment::AUDIO);
|
||||
outputTrack->QueueSetAutoend(false);
|
||||
outputTrack->AddAudioOutput(reinterpret_cast<void*>(1));
|
||||
outputTrack->AddAudioOutput(reinterpret_cast<void*>(1), nullptr);
|
||||
port = outputTrack->AllocateInputPort(processingTrack);
|
||||
listener = new AudioInputProcessing(2);
|
||||
processingTrack->SetInputProcessing(listener);
|
||||
@ -1495,7 +1495,7 @@ TEST(TestAudioTrackGraph, AudioProcessingTrackDisabling)
|
||||
processingTrack = AudioProcessingTrack::Create(graph);
|
||||
outputTrack = graph->CreateForwardedInputTrack(MediaSegment::AUDIO);
|
||||
outputTrack->QueueSetAutoend(false);
|
||||
outputTrack->AddAudioOutput(reinterpret_cast<void*>(1));
|
||||
outputTrack->AddAudioOutput(reinterpret_cast<void*>(1), nullptr);
|
||||
port = outputTrack->AllocateInputPort(processingTrack);
|
||||
/* Primary graph: Open Audio Input through SourceMediaTrack */
|
||||
listener = new AudioInputProcessing(2);
|
||||
@ -2410,7 +2410,7 @@ void TestCrossGraphPort(uint32_t aInputRate, uint32_t aOutputRate,
|
||||
/*OutputDeviceID*/ reinterpret_cast<cubeb_devid>(1),
|
||||
GetMainThreadSerialEventTarget());
|
||||
|
||||
const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
|
||||
const CubebUtils::AudioDeviceID inputDeviceId = (CubebUtils::AudioDeviceID)1;
|
||||
|
||||
RefPtr<AudioProcessingTrack> processingTrack;
|
||||
RefPtr<AudioInputProcessing> listener;
|
||||
@ -2424,7 +2424,7 @@ void TestCrossGraphPort(uint32_t aInputRate, uint32_t aOutputRate,
|
||||
processingTrack->SetInputProcessing(listener);
|
||||
processingTrack->GraphImpl()->AppendMessage(
|
||||
MakeUnique<StartInputProcessing>(processingTrack, listener));
|
||||
processingTrack->ConnectDeviceInput(deviceId, listener,
|
||||
processingTrack->ConnectDeviceInput(inputDeviceId, listener,
|
||||
PRINCIPAL_HANDLE_NONE);
|
||||
primaryFallbackListener = new OnFallbackListener(processingTrack);
|
||||
processingTrack->AddListener(primaryFallbackListener);
|
||||
@ -2455,7 +2455,7 @@ void TestCrossGraphPort(uint32_t aInputRate, uint32_t aOutputRate,
|
||||
/* How the input track connects to another ProcessedMediaTrack.
|
||||
* Check in MediaManager how it is connected to AudioStreamTrack. */
|
||||
port = transmitter->AllocateInputPort(processingTrack);
|
||||
receiver->AddAudioOutput((void*)1);
|
||||
receiver->AddAudioOutput((void*)1, partner->PrimaryOutputDeviceID(), 0);
|
||||
|
||||
partnerFallbackListener = new OnFallbackListener(receiver);
|
||||
receiver->AddListener(partnerFallbackListener);
|
||||
@ -2622,6 +2622,112 @@ TEST(TestAudioTrackGraph, CrossGraphPortUnderrun)
|
||||
TestCrossGraphPort(52110, 17781, 1.01, 30, 1);
|
||||
TestCrossGraphPort(52110, 17781, 1.03, 40, 3);
|
||||
}
|
||||
|
||||
TEST(TestAudioTrackGraph, SecondaryOutputDevice)
|
||||
{
|
||||
MockCubeb* cubeb = new MockCubeb();
|
||||
CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
|
||||
|
||||
const TrackRate primaryRate = 48000;
|
||||
const TrackRate secondaryRate = 44100; // for secondary output device
|
||||
|
||||
MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance(
|
||||
MediaTrackGraph::SYSTEM_THREAD_DRIVER,
|
||||
/*Window ID*/ 1, primaryRate, nullptr, GetMainThreadSerialEventTarget());
|
||||
|
||||
RefPtr<AudioProcessingTrack> processingTrack;
|
||||
RefPtr<AudioInputProcessing> listener;
|
||||
DispatchFunction([&] {
|
||||
/* Create an input track and connect it to a device */
|
||||
processingTrack = AudioProcessingTrack::Create(graph);
|
||||
listener = new AudioInputProcessing(2);
|
||||
processingTrack->GraphImpl()->AppendMessage(
|
||||
MakeUnique<SetPassThrough>(processingTrack, listener, true));
|
||||
processingTrack->SetInputProcessing(listener);
|
||||
processingTrack->GraphImpl()->AppendMessage(
|
||||
MakeUnique<StartInputProcessing>(processingTrack, listener));
|
||||
processingTrack->ConnectDeviceInput(nullptr, listener,
|
||||
PRINCIPAL_HANDLE_NONE);
|
||||
});
|
||||
RefPtr<SmartMockCubebStream> primaryStream =
|
||||
WaitFor(cubeb->StreamInitEvent());
|
||||
|
||||
const void* secondaryDeviceID = CubebUtils::AudioDeviceID(2);
|
||||
DispatchFunction([&] {
|
||||
processingTrack->AddAudioOutput(nullptr, secondaryDeviceID, secondaryRate);
|
||||
processingTrack->SetAudioOutputVolume(nullptr, 0.f);
|
||||
});
|
||||
RefPtr<SmartMockCubebStream> secondaryStream =
|
||||
WaitFor(cubeb->StreamInitEvent());
|
||||
EXPECT_EQ(secondaryStream->GetOutputDeviceID(), secondaryDeviceID);
|
||||
EXPECT_EQ(static_cast<TrackRate>(secondaryStream->SampleRate()),
|
||||
secondaryRate);
|
||||
|
||||
nsIThread* currentThread = NS_GetCurrentThread();
|
||||
uint32_t audioFrames = 0; // excludes pre-silence
|
||||
MediaEventListener audioListener =
|
||||
secondaryStream->FramesVerifiedEvent().Connect(
|
||||
currentThread, [&](uint32_t aFrames) { audioFrames += aFrames; });
|
||||
|
||||
// Wait for 100ms of pre-silence to verify that SetAudioOutputVolume() is
|
||||
// effective.
|
||||
uint32_t processedFrames = 0;
|
||||
WaitUntil(secondaryStream->FramesProcessedEvent(), [&](uint32_t aFrames) {
|
||||
processedFrames += aFrames;
|
||||
return processedFrames > static_cast<uint32_t>(secondaryRate / 10);
|
||||
});
|
||||
EXPECT_EQ(audioFrames, 0U) << "audio frames at zero volume";
|
||||
|
||||
secondaryStream->SetOutputRecordingEnabled(true);
|
||||
DispatchFunction(
|
||||
[&] { processingTrack->SetAudioOutputVolume(nullptr, 1.f); });
|
||||
|
||||
// Wait for enough audio after initial silence to check the frequency.
|
||||
SpinEventLoopUntil("200ms of audio"_ns, [&] {
|
||||
return audioFrames > static_cast<uint32_t>(secondaryRate / 5);
|
||||
});
|
||||
audioListener.Disconnect();
|
||||
|
||||
// Stop recording now so as not to record the discontinuity when the
|
||||
// CrossGraphReceiver is removed from the secondary graph before its
|
||||
// AudioCallbackDriver is stopped.
|
||||
secondaryStream->SetOutputRecordingEnabled(false);
|
||||
|
||||
DispatchFunction([&] { processingTrack->RemoveAudioOutput(nullptr); });
|
||||
WaitFor(secondaryStream->OutputVerificationEvent());
|
||||
// The frequency from OutputVerificationEvent() is estimated by
|
||||
// AudioVerifier from a zero-crossing count. When the discontinuity from
|
||||
// the volume change is resampled, the discontinuity presents as
|
||||
// oscillations, which increase the zero-crossing count and corrupt the
|
||||
// frequency estimate. Trim off sufficient leading from the output to
|
||||
// remove this discontinuity.
|
||||
uint32_t channelCount = secondaryStream->OutputChannels();
|
||||
nsTArray<AudioDataValue> output = secondaryStream->TakeRecordedOutput();
|
||||
size_t leadingIndex = 0;
|
||||
for (; leadingIndex < output.Length() && output[leadingIndex] == 0.f;
|
||||
leadingIndex += channelCount) {
|
||||
};
|
||||
leadingIndex += 10 * channelCount; // skip discontinuity oscillations
|
||||
EXPECT_LT(leadingIndex, output.Length());
|
||||
auto trimmed = Span(output).From(std::min(leadingIndex, output.Length()));
|
||||
size_t frameCount = trimmed.Length() / channelCount;
|
||||
uint32_t inputFrequency = primaryStream->InputFrequency();
|
||||
AudioVerifier<AudioDataValue> verifier(secondaryRate, inputFrequency);
|
||||
verifier.AppendDataInterleaved(trimmed.Elements(), frameCount, channelCount);
|
||||
EXPECT_EQ(verifier.EstimatedFreq(), inputFrequency);
|
||||
// AudioVerifier considers the previous value before the initial sample to
|
||||
// be zero and so considers any initial sample >> 0 to be a discontinuity.
|
||||
EXPECT_EQ(verifier.CountDiscontinuities(), 1U);
|
||||
|
||||
DispatchFunction([&] {
|
||||
// Clean up
|
||||
processingTrack->GraphImpl()->AppendMessage(
|
||||
MakeUnique<StopInputProcessing>(processingTrack, listener));
|
||||
processingTrack->DisconnectDeviceInput();
|
||||
processingTrack->Destroy();
|
||||
});
|
||||
WaitFor(primaryStream->OutputVerificationEvent());
|
||||
}
|
||||
#endif // MOZ_WEBRTC
|
||||
|
||||
#undef Invoke
|
||||
|
@ -315,7 +315,7 @@ AudioDestinationNode::AudioDestinationNode(AudioContext* aContext,
|
||||
mTrack = AudioNodeTrack::Create(aContext, engine, kTrackFlags, graph);
|
||||
mTrack->AddMainThreadListener(this);
|
||||
// null key is fine: only one output per mTrack
|
||||
mTrack->AddAudioOutput(nullptr);
|
||||
mTrack->AddAudioOutput(nullptr, nullptr);
|
||||
}
|
||||
|
||||
void AudioDestinationNode::Init() {
|
||||
|
Loading…
Reference in New Issue
Block a user