0

MediaDevices.getUserMedia在将 Chromium Audio 工作组与媒体流(麦克风)一起使用时,我注意到频繁且可重复的丢失。这不是 100% 可重现的,但是当它们确实发生时,它们确实倾向于遵循模式:

(每次尝试的时间范围略有不同)

  1. 0:00 -> 0:00.2:没有收到样本。(能够重现 100% 的时间,但这感觉像是一个单独的问题,我现在不一定要追踪)
  2. 0:00.2 -> 0:00.5 : 收到样本
  3. 0:00.5 -> 0:00.6 :发生丢失,没有收到样本(能够重现约 20% 的时间)。
  4. 0:00.6 -> 0:30.0 : 收到样品
  5. 从现在开始每隔 30 秒,偶尔会发生辍学。往往在前 30 岁时最常发生。(前 30 年代标记我也可以复制大约 20% 的时间)。

这是一个说明行为的代码笔: https ://codepen.io/GJStevenson/pen/GRErPbm

const startRecordingButton = document.getElementById('startRecordingButton');
let mediaStreamSourceNode;
let isRecording = false;
let timer;

const workletString = `
const formatTimeString = s => {
   const m = (s / 60).toFixed(2);
   const h = (m / 60).toFixed(2);
   const ms = Math.trunc(s * 1000) % 1000;
   const ss = Math.trunc(s) % 60;
   const mm = Math.trunc(m) % 60;
   const hh = Math.trunc(h);
   return hh + ":" + mm + ":" + ss + "." + ms;
};

class RecorderWorklet extends AudioWorkletProcessor {
    constructor(options) {
        super(options);
        this.sampleRate = 0;
        this.sampleCount = 0;

        this.port.onmessage = event => {
            if (event.data.message === 'init') {
                this.sampleRate = event.data.sampleRate;
            }
        }
    }

    process(inputs) {
        if (inputs.length > 0 && inputs[0].length > 0) {
            this.sampleCount += inputs[0][0].length; 
            //console.debug(formatTimeString(this.sampleCount/this.sampleRate), ' : ', inputs[0][0]);

            if (inputs[0][0].includes(0)) {
                console.log('Dropped Samples at: ', formatTimeString(this.sampleCount/this.sampleRate), ' : ', ...inputs[0][0])
            }
        }
        return true;
    }
}

registerProcessor('recorder-worklet', RecorderWorklet);
`;

async function listAudioInputs() {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.filter((device) => device.kind === 'audioinput');
}

async function getDefaultInput(fallbackToFirstInput = true) {
    const audioInputs = await listAudioInputs();
    const defaultDevice = audioInputs.find((device) => device.deviceId === 'default');
    if (defaultDevice) {
        return defaultDevice;
    }
    return fallbackToFirstInput && audioInputs.length > 0 ? audioInputs[0] : undefined;
}

async function getAudioStream(device) {
    const constraints = {
        audio: {
            deviceId: device.deviceId,
        },
    };
    return navigator.mediaDevices.getUserMedia(constraints);
}

async function createRecordingPipeline(device) {
    const stream = await getAudioStream(device);
    const audioTracks = stream.getAudioTracks();

    const sampleRate = audioTracks[0].getSettings().sampleRate;
    console.log('Sample Rate: ', sampleRate);
    const context = new AudioContext({ sampleRate, latencyHint: 'interactive' });

    const blob = new Blob([workletString], { type: 'text/javascript' });
    const workletUrl = URL.createObjectURL(blob);

    await context.audioWorklet.addModule(workletUrl);
    const workletNode = new AudioWorkletNode(context, 'recorder-worklet');

    workletNode.port.postMessage({
        message: 'init',
        sampleRate: sampleRate
    });

    mediaStreamSourceNode = context.createMediaStreamSource(stream);
    mediaStreamSourceNode.connect(workletNode)
                         .connect(context.destination);
}

function formatTimeString(s) {
   const m = (s / 60).toFixed(2);
   const h = (m / 60).toFixed(2);
   const ms = Math.trunc(s * 1000) % 1000;
   const ss = Math.trunc(s) % 60;
   const mm = Math.trunc(m) % 60;
   const hh = Math.trunc(h);
   return hh + ":" + mm + ":" + ss + "." + ms;
};

async function startRecording() {
    const device = await getDefaultInput();
    await createRecordingPipeline(device);

    let timeElapsed = 0;
    timer = setInterval(() => {
        timeElapsed++;
        console.log('Time: ', formatTimeString(timeElapsed));
    }, 1000);
  
    startRecordingButton.innerText = "Stop Recording";
}

async function stopRecording() {
    if (mediaStreamSourceNode) {
        mediaStreamSourceNode.mediaStream.getAudioTracks().forEach(track => {
           track.stop();
        });
        mediaStreamSourceNode.disconnect();
    }
    mediaStreamSourceNode = null;
    clearInterval(timer);
    
    startRecordingButton.innerText = "Start Recording";
}

async function toggleRecording() {
    if (!isRecording) {
        await startRecording();
    } else {
        await stopRecording();
    }
    isRecording = !isRecording;
}
<button onclick="toggleRecording()" id="startRecordingButton">Start Recording</button>

丢弃的样本在控制台中将如下所示:

在控制台中丢弃样本

关于问题可能是什么的任何想法?

编辑:运行 chrome://tracing 以捕获丢弃样本的跟踪。https://www.dropbox.com/s/veg1vgsg9nn03ty/trace_dropped-sample-trace.json.gz?dl=0。丢弃的样本发生在 ~.53s -> .61s

从 0.53 秒到 0.61 秒的下降痕迹图片

4

1 回答 1

0

在打开 Chromium 问题后得到了一些答案。总结来自https://bugs.chromium.org/p/chromium/issues/detail?id=1248169的一些回复:

0:00 -> 0:00.2:没有收到样本。(能够重现 100% 的时间,但这感觉像是一个单独的问题,我现在不一定要追踪)

这些前几个零样本是来自底层缓冲区的初始初始值,并且是预期的。

0:00.5 -> 0:00.6 :发生丢失,没有收到样本(能够重现约 20% 的时间)。

如果 AudioWorklet 线程以 RT 优先级运行,则不应发生这种情况。这很难复制,所以暂时搁置它。

从现在开始每隔 30 秒,偶尔会发生辍学。往往在前 30 岁时最常发生。(前 30 年代标记我也可以复制大约 20% 的时间)。

此丢失是由于已配置目标源但未使用它所致。静默 30 秒后,AudioWorklet 线程切换到低优先级线程,导致退出。

如此变化

mediaStreamSourceNode.connect(workletNode)
                     .connect(context.destination);

mediaStreamSourceNode.connect(workletNode);

解决了这个问题。

于 2021-10-06T18:05:13.267 回答