Bug 1156472 - Part 10 - Test AudioCaptureStream. r=pehrsons

This commit is contained in:
Paul Adenot 2015-07-24 14:28:17 +02:00
parent 0e0e2b315e
commit 6e3d84088b
6 changed files with 228 additions and 39 deletions

View File

@ -20,6 +20,114 @@ try {
FAKE_ENABLED = true;
}
/**
* This class provides helpers around analysing the audio content in a stream
* using WebAudio AnalyserNodes.
*
* @constructor
* @param {object} stream
* A MediaStream object whose audio track we shall analyse.
*/
function AudioStreamAnalyser(ac, stream) {
if (stream.getAudioTracks().length === 0) {
throw new Error("No audio track in stream");
}
this.audioContext = ac;
this.stream = stream;
this.sourceNode = this.audioContext.createMediaStreamSource(this.stream);
this.analyser = this.audioContext.createAnalyser();
this.sourceNode.connect(this.analyser);
this.data = new Uint8Array(this.analyser.frequencyBinCount);
}
AudioStreamAnalyser.prototype = {
/**
* Get an array of frequency domain data for our stream's audio track.
*
* @returns {array} A Uint8Array containing the frequency domain data.
*/
getByteFrequencyData: function() {
this.analyser.getByteFrequencyData(this.data);
return this.data;
},
/**
* Append a canvas to the DOM where the frequency data are drawn.
* Useful to debug tests.
*/
enableDebugCanvas: function() {
var cvs = document.createElement("canvas");
document.getElementById("content").appendChild(cvs);
// Easy: 1px per bin
cvs.width = this.analyser.frequencyBinCount;
cvs.height = 256;
cvs.style.border = "1px solid red";
var c = cvs.getContext('2d');
var self = this;
function render() {
c.clearRect(0, 0, cvs.width, cvs.height);
var array = self.getByteFrequencyData();
for (var i = 0; i < array.length; i++) {
c.fillRect(i, (256 - (array[i])), 1, 256);
}
requestAnimationFrame(render);
}
requestAnimationFrame(render);
},
/**
* Return a Promise, that will be resolved when the function passed as
* argument, when called, returns true (meaning the analysis was a
* success).
*
* @param {function} analysisFunction
* A fonction that performs an analysis, and returns true if the
* analysis was a success (i.e. it found what it was looking for)
*/
waitForAnalysisSuccess: function(analysisFunction) {
var self = this;
return new Promise((resolve, reject) => {
function analysisLoop() {
var success = analysisFunction(self.getByteFrequencyData());
if (success) {
resolve();
return;
}
// else, we need more time
requestAnimationFrame(analysisLoop);
}
analysisLoop();
});
},
/**
* Return the FFT bin index for a given frequency.
*
* @param {double} frequency
* The frequency for whicht to return the bin number.
* @returns {integer} the index of the bin in the FFT array.
*/
binIndexForFrequency: function(frequency) {
return 1 + Math.round(frequency *
this.analyser.fftSize /
this.audioContext.sampleRate);
},
/**
* Reverse operation, get the frequency for a bin index.
*
* @param {integer} index an index in an FFT array
* @returns {double} the frequency for this bin
*/
frequencyForBinIndex: function(index) {
return (index - 1) *
this.audioContext.sampleRate /
this.analyser.fftSize;
}
};
/**
* Create the necessary HTML elements for head and body as used by Mochitests
@ -136,7 +244,10 @@ function setupEnvironment() {
['media.navigator.permission.disabled', true],
['media.navigator.streams.fake', FAKE_ENABLED],
['media.getusermedia.screensharing.enabled', true],
['media.getusermedia.screensharing.allowed_domains', "mochi.test"]
['media.getusermedia.screensharing.allowed_domains', "mochi.test"],
['media.getusermedia.audiocapture.enabled', true],
['media.useAudioChannelService', true],
['media.recorder.audio_node.enabled', true]
]
}, setTestOptions);

View File

@ -30,6 +30,7 @@ skip-if = toolkit == 'gonk' || buildapp == 'mulet' # b2g emulator seems to be to
[test_dataChannel_noOffer.html]
[test_enumerateDevices.html]
skip-if = buildapp == 'mulet'
[test_getUserMedia_audioCapture.html]
[test_getUserMedia_basicAudio.html]
skip-if = (toolkit == 'gonk' || buildapp == 'mulet' && debug) # debug-only failure
[test_getUserMedia_basicVideo.html]

View File

@ -642,39 +642,6 @@ DataChannelWrapper.prototype = {
};
/**
* This class provides helpers around analysing the audio content in a stream
* using WebAudio AnalyserNodes.
*
* @constructor
* @param {object} stream
* A MediaStream object whose audio track we shall analyse.
*/
function AudioStreamAnalyser(stream) {
if (stream.getAudioTracks().length === 0) {
throw new Error("No audio track in stream");
}
this.stream = stream;
this.audioContext = new AudioContext();
this.sourceNode = this.audioContext.createMediaStreamSource(this.stream);
this.analyser = this.audioContext.createAnalyser();
this.sourceNode.connect(this.analyser);
this.data = new Uint8Array(this.analyser.frequencyBinCount);
}
AudioStreamAnalyser.prototype = {
/**
* Get an array of frequency domain data for our stream's audio track.
*
* @returns {array} A Uint8Array containing the frequency domain data.
*/
getByteFrequencyData: function() {
this.analyser.getByteFrequencyData(this.data);
return this.data;
}
};
/**
* This class acts as a wrapper around a PeerConnection instance.
*
@ -1559,20 +1526,20 @@ PeerConnectionWrapper.prototype = {
* @returns {Promise}
* A promise that resolves when we're receiving the tone from |from|.
*/
checkReceivingToneFrom : function(from) {
checkReceivingToneFrom : function(audiocontext, from) {
var inputElem = from.localMediaElements[0];
// As input we use the stream of |from|'s first available audio sender.
var inputSenderTracks = from._pc.getSenders().map(sn => sn.track);
var inputAudioStream = from._pc.getLocalStreams()
.find(s => s.getAudioTracks().some(t => inputSenderTracks.some(t2 => t == t2)));
var inputAnalyser = new AudioStreamAnalyser(inputAudioStream);
var inputAnalyser = new AudioStreamAnalyser(audiocontext, inputAudioStream);
// It would have been nice to have a working getReceivers() here, but until
// we do, let's use what remote streams we have.
var outputAudioStream = this._pc.getRemoteStreams()
.find(s => s.getAudioTracks().length > 0);
var outputAnalyser = new AudioStreamAnalyser(outputAudioStream);
var outputAnalyser = new AudioStreamAnalyser(audiocontext, outputAudioStream);
var maxWithIndex = (a, b, i) => (b >= a.value) ? { value: b, index: i } : a;
var initial = { value: -1, index: -1 };

View File

@ -0,0 +1,110 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test AudioCapture </title>
<script type="application/javascript" src="mediaStreamPlayback.js"></script>
</head>
<body>
<pre id="test">
<script>
createHTML({
bug: "1156472",
title: "Test AudioCapture with regular HTMLMediaElement, AudioContext, and HTMLMediaElement playing a MediaStream",
visible: true
});
scriptsReady
.then(() => FAKE_ENABLED = false)
.then(() => {
runTestWhenReady(function() {
// Get an opus file containing a sine wave at maximum amplitude, of duration
// `lengthSeconds`, and of frequency `frequency`.
function getSineWaveFile(frequency, lengthSeconds, callback) {
var chunks = [];
var off = new OfflineAudioContext(1, lengthSeconds * 48000, 48000);
var osc = off.createOscillator();
var rec = new MediaRecorder(osc);
rec.ondataavailable = function(e) {
chunks.push(e.data);
};
rec.onstop = function(e) {
var blob = new Blob(chunks, { 'type' : 'audio/ogg; codecs=opus' });
callback(blob);
}
osc.frequency.value = frequency;
osc.start();
rec.start();
off.startRendering().then(function(buffer) {
rec.stop();
});
}
/**
* Get two HTMLMediaElements:
* - One playing a sine tone from a blob (of an opus file created on the fly)
* - One being the output for an AudioContext's OscillatorNode, connected to
* a MediaSourceDestinationNode.
*
* Also, use the AudioContext playing through its AudioDestinationNode another
* tone, using another OscillatorNode.
*
* Capture the output of the document, feed that back into the AudioContext,
* with an AnalyserNode, and check the frequency content to make sure we
* have recorded the three sources.
*
* The three sine tones have frequencies far apart from each other, so that we
* can check that the spectrum of the capture stream contains three
* components with a high magnitude.
*/
var wavtone = createMediaElement("audio", "WaveTone");
var acTone = createMediaElement("audio", "audioContextTone");
var ac = new AudioContext();
var oscThroughMediaElement = ac.createOscillator();
oscThroughMediaElement.frequency.value = 1000;
var oscThroughAudioDestinationNode = ac.createOscillator();
oscThroughAudioDestinationNode.frequency.value = 5000;
var msDest = ac.createMediaStreamDestination();
oscThroughMediaElement.connect(msDest);
oscThroughAudioDestinationNode.connect(ac.destination);
acTone.mozSrcObject = msDest.stream;
getSineWaveFile(10000, 10, function(blob) {
wavtone.src = URL.createObjectURL(blob);
oscThroughMediaElement.start();
oscThroughAudioDestinationNode.start();
wavtone.loop = true;
wavtone.play();
acTone.play();
});
var constraints = {audio: {mediaSource: "audioCapture"}};
return getUserMedia(constraints).then((stream) => {
checkMediaStreamTracks(constraints, stream);
window.grip = stream;
var analyser = new AudioStreamAnalyser(ac, stream);
analyser.enableDebugCanvas();
return analyser.waitForAnalysisSuccess(function(array) {
// We want to find three frequency components here, around 1000, 5000
// and 10000Hz. Frequency are logarithmic. Also make sure we have low
// energy in between, not just a flat white noise.
return (array[analyser.binIndexForFrequency(50)] < 50 &&
array[analyser.binIndexForFrequency(1000)] > 200 &&
array[analyser.binIndexForFrequency(2500)] < 50 &&
array[analyser.binIndexForFrequency(5000)] > 200 &&
array[analyser.binIndexForFrequency(7500)] < 50 &&
array[analyser.binIndexForFrequency(10000)] > 200);
}).then(finish);
}).catch(finish);
});
});
</script>
</pre>
</body>
</html>

View File

@ -136,7 +136,7 @@
]);
test.chain.append([
function PC_LOCAL_CHECK_WEBAUDIO_FLOW_PRESENT(test) {
return test.pcRemote.checkReceivingToneFrom(test.pcLocal);
return test.pcRemote.checkReceivingToneFrom(test.audioCtx, test.pcLocal);
}
]);
test.chain.append([

View File

@ -32,7 +32,7 @@ runNetworkTest(function() {
]);
test.chain.append([
function CHECK_AUDIO_FLOW(test) {
return test.pcRemote.checkReceivingToneFrom(test.pcLocal);
return test.pcRemote.checkReceivingToneFrom(test.audioContext, test.pcLocal);
}
]);
test.run();