Table of Contents |
---|
Пример стримера c доступом к медиа-устройствам
Данный стример может использоваться для публикации следующих типов потоков с и проигрывания WebRTC потоков на Web Call Server
- WebRTC
- RTMFP
- RTMP
и позволяет выбрать медиа-устройства и параметры для публикуемого видео
...
На скриншоте ниже представлен пример во время публикации потока.
На странице вопроизводятся отображаются два видео элемента:
- 'Local' - видео с камеры
- 'PreviewPlayer' - видео, которое приходит с сервера
Код примера
Код данного примера находится на WCS-сервере по следующему пути:
...
Здесь host - адрес WCS-сервера.
Работа с кодом примера
Для разбора кода возьмем версию файла manager.js с хэшем 66cc393 ecbadc3, которая находится здесь и доступна для скачивания в соответствующей сборке 2.0.5.28.2753.133.212.
1. Инициализация API.
Flashphoner.init() code
Code Block | ||||
---|---|---|---|---|
| ||||
Flashphoner.init({ screenSharingExtensionId: extensionId, flashMediaProviderSwfLocation: '../../../../media-provider.swf',mediaProvidersReadyCallback: function (mediaProviders) { mediaProvidersReadyCallback: function (mediaProviders if (Flashphoner.isUsingTemasys()) { //hide remote video if current media provider is Flash if (mediaProviders[0] == "Flash") { $("#audioInputForm").hide(); $("#fecForm#videoInputForm").hide(); $("#stereoForm").hide();} } $("#sendAudioBitrateForm").hide(); }) |
2. Получение списка доступных медиа-устройств ввода
Flashphoner.getMediaDevices() code
При получении списка медиа-устройств заполняются выпадающие списки микрофонов и камер на странице клиента.
Code Block | ||||
---|---|---|---|---|
| ||||
Flashphoner.getMediaDevices(null, true).then(function (list) { $("#cpuOveruseDetectionForm").hide();list.audio.forEach(function (device) { }... }); iflist.video.forEach(function (Flashphoner.isUsingTemasys(device)) { ... $("#audioInputForm").hide(}); ... }).catch(function (error) { $("#videoInputForm#notifyFlash").hide(); text("Failed to get media devices"); } } }) |
...
}); |
3. Получение списка доступных медиа-устройств вводавывода звука
Flashphoner.getMediaDevices() code
При получении списка медиа-устройств заполняются выпадающие списки микрофонов и камер выпадающий список устройств вывода звука на странице клиента.
Code Block | ||||
---|---|---|---|---|
| ||||
Flashphoner.getMediaDevices(null, true, MEDIA_DEVICE_KIND.OUTPUT).then(function (list) { list.audio.forEach(function (device) { ... }); list.video.forEach... }).catch(function (deviceerror) { $("#notifyFlash").text("Failed to get ...media devices"); }); |
4. Получение граничных параметров для публикации аудио и видео со страницы клиента
getConstraints() code
Источники публикации:
- камера (sendVideo)
- микрофон (sendAudio)
Code Block | ||||
---|---|---|---|---|
| ||||
}); constraints = { ... }audio: $("#sendAudio").catch(function (error) { is(':checked'), video: $("#notifyFlash#sendVideo").text("Failed to get media devices");is(':checked'), }); |
3. Получение списка доступных медиа-устройств вывода звука
Flashphoner.getMediaDevices() code
...
;
|
Параметры аудио:
- выбор микрофона (deviceId)
- коррекция ошибок для кодека Opus (fec)
- режим стерео (stereo)
- битрейт аудио (bitrate)
Code Block | ||||
---|---|---|---|---|
| ||||
if Flashphoner.getMediaDevices(null, true, MEDIA_DEVICE_KIND.OUTPUT).then(function (list(constraints.audio) { listconstraints.audio.forEach(function (device) = { ...deviceId: $('#audioInput').val() }); ...if ($("#fec").is(':checked')) }).catch(function (error) { constraints.audio.fec = $("#notifyFlash#fec").text("Failed to get media devices"); is(':checked'); }); |
4. Получение граничных параметров для публикации аудио и видео со страницы клиента
getConstraints() code
Источники публикации:
- камера (sendVideo)
- микрофон (sendAudio)
Code Block | ||||
---|---|---|---|---|
| ||||
if ($("#sendStereoAudio").is(':checked')) constraints = { constraints.audio.stereo = audio: $("#sendAudio#sendStereoAudio").is(':checked'),; video:if (parseInt($("#sendVideo").is(':checked'),('#sendAudioBitrate').val()) > 0) constraints.audio.bitrate = parseInt($('#sendAudioBitrate').val()); }; |
Параметры аудиовидео:
- выбор микрофона камеры (deviceId)
- коррекция ошибок для кодека Opus (fec)
- режим стерео (stereo)
- битрейт аудио (bitrateразмеры при публикации (width, height)
- минимальный и максимальный битрейт видео (minBitrate, maxBitrate)
- FPS (frameRate)
Code Block | ||||
---|---|---|---|---|
| ||||
if (constraints.audio) { constraints.audiovideo = { deviceId: {exact: $('#audioInput#videoInput').val()}, }; ifwidth: parseInt($("#fec"'#sendWidth').isval(':checked')), constraints.audio.fec = $("#fec").is(':checked'); height: parseInt($('#sendHeight').val()) if ($("#sendStereoAudio").is(':checked')) }; constraints.audio.stereo = $("#sendStereoAudio").is(':checked'); if (parseInt($('#sendAudioBitrate').val()) > 0)if (Browser.isSafariWebRTC() && Browser.isiOS() && Flashphoner.getMediaProviders()[0] === "WebRTC") { constraints.audiovideo.bitratedeviceId = parseInt({exact: $('#sendAudioBitrate#videoInput').val()); } |
Параметры видео:
- выбор камеры (deviceId)
- размеры при публикации (width, height)
- минимальный и максимальный битрейт видео (minBitrate, maxBitrate)
- FPS (frameRate)
Code Block | ||||
---|---|---|---|---|
| ||||
}; constraints.video = {} deviceId: {exact: if (parseInt($('#videoInput#sendVideoMinBitrate').val()},) > 0) width: constraints.video.minBitrate = parseInt($('#sendWidth#sendVideoMinBitrate').val()),; height: if (parseInt($('#sendHeight#sendVideoMaxBitrate').val()) > 0) } constraints.video.maxBitrate = parseInt($('#sendVideoMaxBitrate').val()); if (Browser.isSafariWebRTCparseInt($('#fps').val()) && Browser.isiOS() && Flashphoner.getMediaProviders()[0] === "WebRTC") {> 0) constraints.video.deviceIdframeRate = {exact: parseInt($('#videoInput#fps').val()}; } if (parseInt($('#sendVideoMinBitrate').val()) > 0) ); |
5. Получение доступа к медиаустройствам для локального тестирования
Flashphoner.getMediaAccess() code
В метод передаются граничные параметры для аудио и видео (constrains), а также localVideo - div-элемент, в котором будет отображаться видео с выбранной камеры.
Code Block | ||||
---|---|---|---|---|
| ||||
Flashphoner.getMediaAccess(getConstraints(), localVideo).then(function (disp) {
$("#testBtn").text("Release").off('click').click(function () {
$(this).prop('disabled', true);
stopTest();
}).prop('disabled', false);
...
testStarted = true;
}).catch(function (error) {
$("#testBtn").prop('disabled', false);
testStarted = false;
}); |
6. Подключение к серверу.
Flashphoner.createSession() code
Code Block | ||||
---|---|---|---|---|
| ||||
Flashphoner.createSession({urlServer: url, timeout: tm}).on(SESSION_STATUS.ESTABLISHED, function (session) {
...
}).on(SESSION_STATUS.DISCONNECTED, function () {
...
}).on(SESSION_STATUS.FAILED, function () {
...
}); |
7. Получение от сервера события, подтверждающего успешное соединение.
ConnectionStatusEvent ESTABLISHED code
Code Block | ||||
---|---|---|---|---|
| ||||
Flashphoner.createSession({urlServer: url, timeout: tm}).on(SESSION_STATUS.ESTABLISHED, function (session) {
setStatus("#connectStatus", session.status());
onConnected(session);
...
}); |
8. Публикация видеопотока
session.createStream(), publishStream.publish() code
Code Block | ||||
---|---|---|---|---|
| ||||
publishStream = session.createStream({
name: streamName,
display: localVideo,
cacheLocalResources: true,
constraints: constraints,
mediaConnectionConstraints: mediaConnectionConstraints,
sdpHook: rewriteSdp,
transport: transportInput,
cvoExtension: cvo,
stripCodecs: strippedCodecs,
videoContentHint: contentHint
...
});
publishStream.publish(); |
9. Получение от сервера события, подтверждающего успешную публикацию потока
StreamStatusEvent PUBLISHING code
Code Block | ||||
---|---|---|---|---|
| ||||
publishStream = session.createStream({ ... }).on(STREAM_STATUS.PUBLISHING, function (stream) { $("#testBtn").prop('disabled', true); var video = document.getElementById(stream.id()); //resize local if resolution is available if (video.videoWidth > 0 && video.videoHeight > 0) { resizeLocalVideo({target: video}); } enablePublishToggles(true); if ($("#muteVideoToggle").is(":checked")) { muteVideo(); } if ($("#muteAudioToggle").is(":checked")) { muteAudio(); } //remove resize listener in case this video was cached earlier video.removeEventListener('resize', resizeLocalVideo); video.addEventListener('resize', resizeLocalVideo); publishStream.setMicrophoneGain(currentGainValue); setStatus("#publishStatus", STREAM_STATUS.PUBLISHING); onPublishing(stream); }).on(STREAM_STATUS.UNPUBLISHED, function () { ... }).on(STREAM_STATUS.FAILED, function () { constraints..video.minBitrate = parseInt($('#sendVideoMinBitrate').val()); . }); publishStream.publish(); |
10. Воспроизведение потока
session.createStream(), previewStream.play() code
Code Block | ||||
---|---|---|---|---|
| ||||
previewStream = session.createStream({ if (parseInt($('#sendVideoMaxBitrate').val()) > 0) name: streamName, display: remoteVideo, constraints.video.maxBitrate = parseInt($('#sendVideoMaxBitrate').val()); : constraints, if (parseInt($('#fps').val()) > 0) transport: transportOutput, stripCodecs: strippedCodecs constraints ..video.frameRate = parseInt($('#fps').val()); |
...
Flashphoner.getMediaAccess() code
...
});
previewStream.play(); |
11. Получение от сервера события, подтверждающего успешное воспроизведение потока
StreamStatusEvent PLAYING code
Code Block | ||||
---|---|---|---|---|
| ||||
Flashphoner.getMediaAccess(getConstraints(), localVideo).then(function (disppreviewStream = session.createStream({ ... }).on(STREAM_STATUS.PLAYING, function (stream) { $("#testBtn").text("Release").off('click').click(function () {playConnectionQualityStat.connectionQualityUpdateTimestamp = new Date().valueOf(); $(this).prop('disabled', truesetStatus("#playStatus", stream.status()); stopTestonPlaying(stream); }document.getElementById(stream.id()).propaddEventListener('disabledresize', false); function (event) { $("#playResolution").text(event.. target.videoWidth + "x" + event.target.videoHeight); testStarted = true; }).catch(function (error) {resizeVideo(event.target); $("#testBtn").prop('disabled', false)}); testStarted = false;//wait for incoming stream }); |
6. Подключение к серверу.
Flashphoner.createSession() code
Code Block | ||||
---|---|---|---|---|
| ||||
Flashphoner.createSession({urlServer: url, timeout: tm}).on(SESSION_STATUS.ESTABLISHED, function (session) { if (Flashphoner.getMediaProviders()[0] == "WebRTC") { setTimeout(function () { setStatus("#connectStatus", session.status if(Browser.isChrome()); { onConnected(session); }).on(SESSION_STATUS.DISCONNECTED, function () { setStatus("#connectStatus", SESSION_STATUS.DISCONNECTEDdetectSpeechChrome(stream); onDisconnected(); }).on(SESSION_STATUS.FAILED, function () { else { setStatus("#connectStatus", SESSION_STATUS.FAILED); onDisconnecteddetectSpeech(stream); }); |
7. Получение от сервера события, подтверждающего успешное соединение.
ConnectionStatusEvent ESTABLISHED code
Code Block | ||||
---|---|---|---|---|
| ||||
Flashphoner.createSession({urlServer: url, timeout: tm}).on(SESSION_STATUS.ESTABLISHED, function (session) { } setStatus("#connectStatus"}, session.status(3000)); onConnected(session);} ... }); |
8. Публикация видеопотока
...
previewStream.play(); |
12. Остановка воспроизведения потока.
stream.stop() code
Code Block | ||||
---|---|---|---|---|
| ||||
publishStream = session.createStream({ name: streamName,$("#playBtn").text("Stop").off('click').click(function () { display: localVideo, cacheLocalResources: true,$(this).prop('disabled', true); constraints: constraints, stream.stop(); }).prop('disabled', false); |
13. Получение от сервера события, подтверждающего остановку воспроизведения
StreamStatusEvent STOPPED code
Code Block | ||||
---|---|---|---|---|
| ||||
previewStream mediaConnectionConstraints: mediaConnectionConstraints,= session.createStream({ sdpHook: rewriteSdp,... transport: transportInput,}).on(STREAM_STATUS.STOPPED, function () { cvoExtension: cvo,setStatus("#playStatus", STREAM_STATUS.STOPPED); stripCodecs: strippedCodecsonStopped(); ... }); publishStreampreviewStream.publishplay(); |
9. Получение от сервера события, подтверждающего успешную публикацию потока
StreamStatusEvent PUBLISHING 14. Остановка публикации видеопотока
stream.stop() code
Code Block | ||||
---|---|---|---|---|
| ||||
publishStream = session.createStream({ ... }$("#publishBtn").text("Stop").on(STREAM_STATUS.PUBLISHING, off('click').click(function (stream) { $("#testBtn"this).prop('disabled', true); var video = document.getElementById(stream.idstop()); }).prop('disabled', false); |
15. Получение от сервера события, подтверждающего успешную остановку публикации
StreamStatusEvent UNPUBLISHED code
Code Block | ||||
---|---|---|---|---|
| ||||
//resize localpublishStream if resolution is available= session.createStream({ if (video.videoWidth > 0 && video.videoHeight > 0... }).on(STREAM_STATUS.UNPUBLISHED, function () { resizeLocalVideo({target: video}setStatus("#publishStatus", STREAM_STATUS.UNPUBLISHED); }onUnpublished(); enablePublishToggles(true);... }); if ($("#muteVideoToggle").is(":checked")) { publishStream.publish(); |
16. Отключение микрофона
stream.muteAudio() code:
Code Block | ||||
---|---|---|---|---|
| ||||
function muteAudio() { if (publishStream) { muteVideopublishStream.muteAudio(); } } } |
17. Отключение камеры
stream.muteVideo() code:
Code Block | ||||
---|---|---|---|---|
| ||||
function muteVideo() { if ($("#muteAudioToggle").is(":checked"))publishStream) { muteAudiopublishStream.muteVideo(); } } } |
18. Отображение статистики при публикации потока
stream.getStats() code:
Code Block | ||||
---|---|---|---|---|
| ||||
//remove resize listener in case this video was cached earlier publishStream.getStats(function (stats) { if video.removeEventListener('resize', resizeLocalVideo);(stats && stats.outboundStream) { video.addEventListener('resize', resizeLocalVideo); if publishStream.setMicrophoneGain(currentGainValue);(stats.outboundStream.video) { setStatus("#publishStatus", STREAM_STATUS.PUBLISHING); onPublishing(stream); }).on(STREAM_STATUS.UNPUBLISHED, function () {showStat(stats.outboundStream.video, "outVideoStat"); ... }).on(STREAM_STATUS.FAILED, function () { let vBitrate = (stats.outboundStream.video. bytesSent - videoBytesSent) * })8; publishStream.publish(); |
10. Воспроизведение потока
session.createStream(), previewStream.play() code
Code Block | ||||
---|---|---|---|---|
| ||||
previewStream = session.createStream( if ($('#outVideoStatBitrate').length == 0) { name: streamName, display: remoteVideo, let html = constraints"<div>Bitrate: constraints, transport: transportOutput, " + "<span id='outVideoStatBitrate' style='font-weight: normal'>" + vBitrate + "</span>" + "</div>"; stripCodecs: strippedCodecs ... }); previewStream.play(); |
11. Получение от сервера события, подтверждающего успешное воспроизведение потока
StreamStatusEvent PLAYING code
Code Block | ||||
---|---|---|---|---|
| ||||
previewStream = session.createStream({ $("#outVideoStat").append(html); ... }).on(STREAM_STATUS.PLAYING, function (stream) { } else { playConnectionQualityStat.connectionQualityUpdateTimestamp = new Date().valueOf(); setStatus("#playStatus", stream.status()); onPlaying(stream$('#outVideoStatBitrate').text(vBitrate); document.getElementById(stream.id()).addEventListener('resize', function (event) { $("#playResolution").text(event.target.videoWidth + "x" + event.target.videoHeight); } videoBytesSent resizeVideo(event.target)= stats.outboundStream.video.bytesSent; }); //wait for incoming stream... if (Flashphoner.getMediaProviders()[0] == "WebRTC") { } setTimeout(function () { if (stats.outboundStream.audio) { detectSpeech(stream); }showStat(stats.outboundStream.audio, 3000"outAudioStat"); } ... }); let aBitrate = previewStream.play(); |
12. Остановка воспроизведения потока.
stream.stop() code
Code Block | ||||
---|---|---|---|---|
| ||||
$("#playBtn").text("Stop").off('click').click(function () {(stats.outboundStream.audio.bytesSent - audioBytesSent) * 8; $(this).prop('disabled', true); stream.stop(); }).propif ($('disabled#outAudioStatBitrate', false); |
13. Получение от сервера события, подтверждающего остановку воспроизведения
StreamStatusEvent STOPPED code
Code Block | ||||
---|---|---|---|---|
| ||||
.length == 0) { previewStream = session.createStream({ ... }).on(STREAM_STATUS.STOPPED, function () { let html = "<div>Bitrate: setStatus("#playStatus + ", STREAM_STATUS.STOPPED); onStopped()<span id='outAudioStatBitrate' style='font-weight: normal'>" + aBitrate + "</span>" + "</div>"; ... }); previewStream.play(); |
14. Остановка публикации видеопотока
stream.stop() code
Code Block | ||||
---|---|---|---|---|
| ||||
$("#publishBtn").text("Stop").off('click').click(function () { $(this"#outAudioStat").prop('disabled', trueappend(html); stream.stop(); }).prop('disabled', false); |
15. Получение от сервера события, подтверждающего успешную остановку публикации
StreamStatusEvent UNPUBLISHED code
Code Block | ||||
---|---|---|---|---|
| ||||
publishStream} =else session.createStream({ ... }).on(STREAM_STATUS.UNPUBLISHED, function () { setStatus("#publishStatus", STREAM_STATUS.UNPUBLISHED$('#outAudioStatBitrate').text(aBitrate); onUnpublished(); ... }); publishStream.publish(); |
16. Отключение микрофона
code:
Code Block | ||||
---|---|---|---|---|
| ||||
audioBytesSent if ($("#muteAudioToggle").is(":checked")) {= stats.outboundStream.audio.bytesSent; muteAudio(); } } |
17. Отключение камеры
code:
Code Block | ||||
---|---|---|---|---|
| ||||
if ($("#muteVideoToggle").is(":checked")) { } muteVideo();... }); |
1819. Отображение статистики при публикации потокавоспроизведении потока
stream.getStats() code:
Code Block | ||||
---|---|---|---|---|
| ||||
publishStreampreviewStream.getStats(function (stats) { if (stats && stats.outboundStreaminboundStream) { if (stats.outboundStreaminboundStream.video) { showStat(stats.outboundStreaminboundStream.video, "outVideoStatinVideoStat"); let vBitrate = (stats.outboundStreaminboundStream.video.bytesSentbytesReceived - videoBytesSentvideoBytesReceived) * 8; if ($('#outVideoStatBitrate#inVideoStatBitrate').length == 0) { let html = "<div>Bitrate: " + "<span id='outVideoStatBitrateinVideoStatBitrate' style='font-weight: normal'>" + vBitrate + "</span>" + "</div>"; $("#outVideoStat#inVideoStat").append(html); } else { $('#outVideoStatBitrate#inVideoStatBitrate').text(vBitrate); } videoBytesSentvideoBytesReceived = stats.outboundStreaminboundStream.video.bytesSentbytesReceived; ... } if (stats.outboundStreaminboundStream.audio) { showStat(stats.outboundStreaminboundStream.audio, "outAudioStatinAudioStat"); let aBitrate = (stats.outboundStreaminboundStream.audio.bytesSentbytesReceived - audioBytesSentaudioBytesReceived) * 8; if ($('#outAudioStatBitrate#inAudioStatBitrate').length == 0) { let html = "<div>Bitrate<div style='font-weight: bold'>Bitrate: " + "<span id='outAudioStatBitrateinAudioStatBitrate' style='font-weight: normal'>" + aBitrate + "</span>" + "</div>"; $("#outAudioStat#inAudioStat").append(html); } else { $('#outAudioStatBitrate#inAudioStatBitrate').text(aBitrate); ; } audioBytesReceived = stats.inboundStream.audio.bytesReceived; } } audioBytesSent = stats.outboundStream.audio.bytesSent; } } }); |
20. Определение речи при помощи интерфейса ScriptProcessor (любой браузер, кроме Chrome)
audioContext.createMediaStreamSource(), audioContext.createScriptProcessor() code
Code Block | ||||
---|---|---|---|---|
| ||||
function detectSpeech(stream, level, latency) { var mediaStream = document.getElementById(stream.id()).srcObject; } var source = audioContext.createMediaStreamSource(mediaStream); var processor = ...audioContext.createScriptProcessor(512); processor.onaudioprocess = handleAudio; }); |
19. Отображение статистики при воспроизведении потока
stream.getStats() code:
Code Block | ||||
---|---|---|---|---|
| ||||
processor.connect(audioContext.destination); processor.clipping = false; previewStream.getStats(function (stats) { processor.lastClip = 0; // threshold processor.threshold = if (stats && stats.inboundStream) { level || 0.10; processor.latency = latency || 750; processor.isSpeech = iffunction (stats.inboundStream.video) { ) { if (!this.clipping) return false; if showStat((stats.inboundStream.video, "inVideoStat")this.lastClip + this.latency) < window.performance.now()) this.clipping = false; return this.clipping; }; let vBitrate = (stats.inboundStream.video.bytesReceived - videoBytesReceived) * 8; source.connect(processor); // Check speech every 500 ms speechIntervalID = setInterval(function () { if ($('#inVideoStatBitrate').length == 0processor.isSpeech()) { $("#talking").css('background-color', 'green'); } let html = "<div>Bitrate: " + "<span id='inVideoStatBitrate' style='font-weight: normal'>" + vBitrate + "</span>" + "</div>"else { $("#talking").css('background-color', 'red'); } }, 500); } |
Обработка аудиоданных code
Code Block | ||||
---|---|---|---|---|
| ||||
function handleAudio(event) { var buf $("#inVideoStat").append(html= event.inputBuffer.getChannelData(0); var bufLength = buf.length; var x; for (var i }= else0; { i < bufLength; i++) { x = buf[i]; $('#inVideoStatBitrate').text(vBitrate);if (Math.abs(x) >= this.threshold) { this.clipping = true; } this.lastClip = window.performance.now(); } videoBytesReceived = stats.inboundStream.video.bytesReceived; } } |
21. Определение речи по WebRTC статистике входящего аудио потока в браузере Chrome
stream.getStats() code
Code Block | ||||
---|---|---|---|---|
| ||||
function detectSpeechChrome(stream, level, latency) { statSpeechDetector.threshold = level || 0.010; ... statSpeechDetector.latency = latency || 750; statSpeechDetector.clipping = false; statSpeechDetector.lastClip = }0; speechIntervalID = setInterval(function() { if (stats.inboundStream.audiostream.getStats(function(stat) { let audioStats showStat(stats= stat.inboundStream.audio, "inAudioStat"); let aBitrate = (stats.inboundStream.audio.bytesReceived - audioBytesReceived) * 8; if(!audioStats) { return; if ($('#inAudioStatBitrate').length == 0) {} // Using audioLevel WebRTC stats parameter let html = "<div style='font-weight: bold'>Bitrate: " + "<span id='inAudioStatBitrate' style='font-weight: normal'>" + aBitrate + "</span>" + "</div>"; if (audioStats.audioLevel >= statSpeechDetector.threshold) { statSpeechDetector.clipping = true; statSpeechDetector.lastClip $("#inAudioStat").append(html= window.performance.now(); } } else { if ((statSpeechDetector.lastClip + statSpeechDetector.latency) < window.performance.now()) { $('#inAudioStatBitrate').text(aBitrate)statSpeechDetector.clipping = false; } } if (statSpeechDetector.clipping) { audioBytesReceived = stats.inboundStream.audio.bytesReceived $("#talking").css('background-color', 'green'); } else }{ ...$("#talking").css('background-color', 'red'); } }); },500); } |