{"id":21293228,"url":"https://github.com/guest271314/capturesystemaudio","last_synced_at":"2025-07-11T16:32:33.652Z","repository":{"id":37471306,"uuid":"265761439","full_name":"guest271314/captureSystemAudio","owner":"guest271314","description":"Capture system audio (\"What-U-Hear\")","archived":false,"fork":false,"pushed_at":"2024-03-15T22:20:59.000Z","size":901,"stargazers_count":39,"open_issues_count":0,"forks_count":7,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-03-15T23:29:41.232Z","etag":null,"topics":["c","cpp","file-api","file-system-access","javascript","linux","native-messaging","python3","recorder","shell","system-audio-capture"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/guest271314.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2020-05-21T05:20:49.000Z","updated_at":"2024-03-15T13:50:37.000Z","dependencies_parsed_at":"2024-03-22T06:55:38.471Z","dependency_job_id":null,"html_url":"https://github.com/guest271314/captureSystemAudio","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guest271314%2FcaptureSystemAudio","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guest271314%2FcaptureSystemAudio/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guest271314%2FcaptureSystemAudio/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/guest271314%2FcaptureSystemAudio/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/guest271314","download_url":"https://codeload.github.com/guest271314/captureSystemAudio/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225741166,"owners_count":17516895,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["c","cpp","file-api","file-system-access","javascript","linux","native-messaging","python3","recorder","shell","system-audio-capture"],"created_at":"2024-11-21T13:53:49.311Z","updated_at":"2024-11-21T13:53:49.989Z","avatar_url":"https://github.com/guest271314.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# captureSystemAudio\nCapture system audio ([\"What-U-Hear\"](https://wiki.archlinux.org/index.php/PulseAudio/Examples#ALSA_monitor_source)) \n\n\u003e To be able to record from a monitor source (a.k.a. \"What-U-Hear\", \"Stereo Mix\"), use `pactl` list to find out the name of the source in PulseAudio (e.g. `alsa_output.pci-0000_00_1b.0.analog-stereo.monitor`). \n\n\u003ch5\u003eBackground\u003c/h5\u003e\n\nBased on the results of testing default implementation and experiments with different approaches to get access to the device within the scope of API's shipped with the browser it is not possible to select `Monitor of \u003cdevice\u003e` at Chromium at Linux, which is not exposed at `getUserMedia()` UI prompt or at `enumerateDevices()` after permission to capture audio is granted, without manually setting the device to `Monitor of \u003cdevice\u003e` _during_ recording a `MediaStream` from `getUserMedia({audio: true})` at PulseAudio sound settings GUI `Recording` tab. Once that user action is performed outside of the browser at the OS the setting becomes persistent where subsequent calls to `getUserMedia({audio: true})`. To capture microphone input after manually setting the `Monitor of \u003cdevice\u003e` at PulseAudio sound settings GUI the user must perform the procedure in reverse by recording a `MediaStream` and setting the device back to the default `Built-in \u003cdevice\u003e` _during_ capture of a `MediaStream` from `getUserMedia({audio: true})`.\n\nFirefox supports selection of `Monitor of \u003cdevice\u003e` at `getUserMedia()` at Linux at the UI prompt by selecting the device from `enumerateDevices()` after permission is granted for media capture at first `getUserMedia()` and  `getUserMedia()` is executed a second time with the `deviceId` of `Monitor of \u003cdevice\u003e` from `MediaDeviceInfo` object constraint set `{audio:{deviceId:{exact:device.deviceId}}}`. \n\nFirefox and Chromium do not support system or application capture of system audio at `getDisplayMedia({video: true, audio: true})` at Linux.\n\nChrome on Windows evidently does to support the user selecting audio capture at `getDisplayMedia({video: true, audio: true})` UI prompt.\n\n`getUserMedia()` and `getDisplayMedia()` specifications do not explicitly state the user agent \"MUST\" provide the user with the option to capture application or system audio. From Screen Capture https://w3c.github.io/mediacapture-screen-share/ \n\n\u003e In the case of audio, the user agent MAY present the end-user with audio sources to share. Which choices are available to choose from is up to the user agent, and the audio source(s) are not necessarily the same as the video source(s). An audio source may be a particular application, window, browser, the entire system audio or any combination thereof. Unlike mediadevices.getUserMedia() with regards to audio+video, the user agent is allowed not to return audio even if the audio constraint is present. If the user agent knows no audio will be shared for the lifetime of the stream it MUST NOT include an audio track in the resulting stream. The user agent MAY accept a request for audio and video by only returning a video track in the resulting stream, or it MAY accept the request by returning both an audio track and a video track in the resulting stream. The user agent MUST reject audio-only requests. \n\n_\"MAY\"_ being the key term in the language at _\"the user agent MAY\"_, indicating that implementation of capturing audio from _\"a particular application, window, browser, the entire system audio or any combination thereof\"_ is solely an individual choice of the _\"user agent\"_ to implement or not and thus can be considered null and void as to being a requirement for conformance with the specification if the _\"user agent\"_ decides to omit audio capture from the implementation of the specification. \n\nAudio capture is described in broad context as to potential applicable coverage in general in the Screen Capture specification where that same description of potential coverage can be narrowly interpreted by the term _\"MAY\"_ to mean not required to implement for conformance and thus not applicable solely at the _\"user agent\"_ discretion.\n\n\u003ch5\u003eMotivation\u003c/h5\u003e\n\nSpecify and implement web compatible system audio capture.\n\nThe origin of and primary requirement is to capture output of `window.speechSynthesis.speak()`.\n\nThe code can also be used to capture playback of media at native applications where the container and codec being played are not be supported at the browser by default, not supported as a video document when directly navigated to, or output from a native application supporting features not implemented at the browser, for example, `mpv output.amr sound.caf`, `ffplay blade_runner.mkv`, `paplay output.wav`, `espeak-ng -m '\u003cp\u003eA paragraph.\u003c/p\u003e\u003cbreak time=\"2s\"\u003e\u003cs\u003eA sentence\u003cs\u003e'`.\n\n\u003ch5\u003eSynopsis\u003c/h5\u003e\n\nOpen local files watched by `inotifywait` from [inotify-tools](https://github.com/inotify-tools/inotify-tools) to capture system audio monitor device at Linux, write output to a local file, stop system audio capture, get the resulting local file in the browser.\n\n\u003ch5\u003eDependencies\u003c/h5\u003e\n\n`pacat`, `inotify-tools`.\n\n\u003ch6\u003eOptional\u003c/h6\u003e\n\n`opus-tools`, `mkvtoolnix`, both used by default to convert WAV to Opus and write Opus to WebM container to decrease resulting file size and encoded and write track to Matroska, WebM, or other media container supported at the system. `opus-tools`, `mkvtoolnix` are included in the code by default to reduce resulting file size of captured stream by converting to Opus codec from audio from WAV. `ffmpeg` is used to write WebM file to local filesystem piped from `parec` and `opusenc` in \"real-time\", where `MediaSource` can be used to stream the captured audio in \"real-time\" (`ffmpeg` does not write WebM to local filesystem until 669 bytes are accumulated).\n\n\u003ch5\u003eUsage\u003c/h5\u003e\n\n\u003ch6\u003eCommand line, Chromium launcher\u003c/h6\u003e\n\nCreate a local folder in `/home/user/localscripts` containing the files in this repository, run the command\n\n`$HOME/notify.sh \u0026 chromium-browser --enable-experimental-web-platform-features \u0026\u0026 killall -q -9 inotifywait`\n\nto start `inotifywait` watching two `.txt` files in the directory for open events and launches Chromium.\n\nTo start system audio capture at the browser open the local file `captureSystemAudio.txt`, to stop capture by open the local file `stopSystemAudioCapture.txt`, where each file contains one space character, then get the captured audio from local filesystem using `\u003cinput type=\"file\"\u003e` or where implemented Native File System `showDirectoryPicker()`.\n\n\u003ch5\u003eCapture 50 minutes of audio to file\u003c/h5\u003e\n\n```\ncaptureSystemAudio()\n.then(async requestNativeScript =\u003e {\n  // system audio is being captured, wait 50 minutes\n  await requestNativeScript.get('wait')(60 * 1000 * 50);\n  // stop system audio capture\n  await requestNativeScript.get('stop').arrayBuffer(); \n  // avoid Native File System ERR_UPLOAD_FILE_CHANGED error\n  await requestNativeScript.get('wait')(100);\n  try {\n    const output = await requestNativeScript.get('dir').getFile('output.webm', {create:false});\n    // resulting File object\n    const file = await output.getFile(); \n    // do stuff with captured system audio as WAV, Opus, other codec and container the system supports\n    console.log(output, file, URL.createObjectURL(file));\n  } catch (e) {\n      throw e;\n  }\n})\n.catch(e =\u003e {\n  console.error(e);\n  console.trace();\n});\n```\n\n\u003ch5\u003eStream file being written at local filesystem to MediaSource, capture as MediaStream, record with MediaRecorder in \"real-time\"\u003c/h5\u003e\n\nAdjust shell script `captureSystemAudio.sh` to pipe `opusenc` to `ffmpeg` to write file while reading file at browser\n\n```\n#!/bin/bash\ncaptureSystemAudio() {\n  parec -v --raw -d alsa_output.pci-0000_00_1b.0.analog-stereo.monitor | opusenc --raw-rate 44100 - - \\\n    | ffmpeg -y -i - -c:a copy $HOME/localscripts/output.webm\n}\ncaptureSystemAudio\n```\n\nat JavaScript use `HTMLMediaElement`, `MediaSource` to capture `timeSlice` seconds, minutes, hours, or, given unlimited computational resources, an infinite stream of system audio output\n\n```\n  captureSystemAudio()\n  .then(async requestNativeScript =\u003e {\n    const audio = new Audio();\n    let mediaStream, mediaRecorder;\n    audio.controls = audio.autoplay = audio.muted = true;\n    audio.onloadedmetadata = _ =\u003e {\n      console.log(audio.duration, ms.duration);\n      mediaStream = audio.captureStream();\n      mediaRecorder = new MediaRecorder(mediaStream, {\n        mimeType: 'audio/webm;codecs=opus',\n        audioBitrateMode: 'cbr'\n      });\n      mediaRecorder.start();\n      mediaRecorder.ondataavailable = e =\u003e {\n        console.log(URL.createObjectURL(e.data));\n      };\n    };\n    audio.onended = _ =\u003e {\n      mediaRecorder.stop();\n    };\n    document.body.appendChild(audio);\n    let ms = new MediaSource();\n    let sourceBuffer;\n    let domExceptionsCaught = 0;\n    ms.onsourceopen = e =\u003e {\n      sourceBuffer = ms.addSourceBuffer('audio/webm;codecs=opus');\n    };\n    audio.src = URL.createObjectURL(ms);\n    async function* fileStream(timeSlice = 5) {\n      const { readable, writable } = new TransformStream();\n      // do stuff with readable: ReadableStream, e.g., transfer; export\n      const reader = readable.getReader();\n      let offset = 0;\n      let start = false;\n      let stop = false;\n      audio.ontimeupdate = _ =\u003e {\n        if (audio.currentTime \u003e= timeSlice) {\n          stop = true;\n          audio.ontimeupdate = null;\n        }\n        console.log(audio.currentTime);\n      };\n      function readFileStream() {\n        return reader\n          .read()\n          .then(async function processFileStream({ value, done }) {\n            if (done) {\n              console.log(done);\n              ms.endOfStream();\n              return reader.closed;\n            }\n            await new Promise(resolve =\u003e {\n              sourceBuffer.addEventListener('updateend', resolve, {\n                once: true,\n              });\n              console.log(value);\n              sourceBuffer.appendBuffer(value);\n            });\n            return reader\n              .read()\n              .then(processFileStream)\n              .catch(e =\u003e {\n                throw e;\n              });\n          });\n      }\n      while (true) {\n        try {\n          const output = await requestNativeScript\n            .get('dir')\n            .getFile('output.webm', { create: false });\n          const file = await output.getFile();\n          const slice = file.slice(offset, file.size);\n          // native application could already be writing file\n          // file can be 669 bytes before written to local filesystem by ffmpeg\n          // wait until File.size \u003e 0\n          if (slice.size \u003e 0) {\n            slice.stream().pipeTo(writable, { preventClose: stop === false });\n          }\n          offset = file.size;\n          if (!start) {\n            start = true;\n            // do stuff with fileBits\n            readFileStream().catch(e =\u003e {\n              throw e;\n            });\n          }\n          yield;\n        } catch (e) {\n          // handle DOMException:\n          // A requested file or directory could not be found at the time an operation was processed.\n          ++domExceptionsCaught;\n          console.error(e);\n          console.trace();\n        } finally {\n          if (stop === true) {\n            break;\n          }\n        }\n      }\n      try {\n        await requestNativeScript.get('stop').arrayBuffer();\n        await writable.close();\n      } catch (e) {\n        console.error(e);\n      }\n    }\n    // capture 2 minutes of system audio output\n    for await (const fileBits of fileStream(60 * 2));\n    await requestNativeScript.get('dir').removeEntry('output.webm');\n    console.log('done streaming file', { domExceptionsCaught });\n  })\n  .catch(e =\u003e {\n    console.error(e);\n    console.trace();\n  });\n  ```\n  \n\u003ch5\u003eLaunch pavucontrol to select audio device\u003c/h5\u003e\n\nWhere it is currently not possible to select `\"Monitor of Built-in Audio Analog Stereo\"` at Chromium implementation of media capture by default, launch [`pavucontrol`](https://gitlab.freedesktop.org/pulseaudio/pavucontrol) `Recording` tab using `pavucontrol -t 2` after `getUserMedia({audio: true})` for capability to change the audio device being captured dynamically, e.g., from default microphone `\"Built-in Audio Analog Stereo\"` to `\"Monitor of Built-in Audio Analog Stereo\"` (\"What-U-Hear\") \n\n\u003cimg src=\"./pavucontrol.png\" alt=\"pavucontrol audio device selection\"\u003e\n\n```\nasync function chromiumLinuxSetAudioCaptureDevice() {\n  try {\n    const requestNativeScript = new Map();\n    const mediaStream = await navigator.mediaDevices.getUserMedia({audio: true});\n    requestNativeScript.set(\n      'wait',\n      (ms = 50) =\u003e new Promise(resolve =\u003e setTimeout(resolve, ms))\n    );\n    requestNativeScript.set(\n      'dir',\n      await self.showDirectoryPicker()\n    );\n    requestNativeScript.set(\n      'status',\n      await requestNativeScript.get('dir').requestPermission({ writable: true })\n    );\n    requestNativeScript.set(\n      'start',\n      await (\n        await requestNativeScript\n          .get('dir')\n          .getFile('openpavucontrol.txt', { create: false })\n      ).getFile()\n    );\n    requestNativeScript.set(\n      'stop',\n      await (\n        await requestNativeScript\n          .get('dir')\n          .getFile('closepavucontrol.txt', { create: false })\n      ).getFile()\n    );\n    \n    const executeNativeScript = await requestNativeScript\n      .get('start')\n      .arrayBuffer();\n    return {requestNativeScript, mediaStream};\n  } catch (e) {\n    throw e;\n  }\n}\nchromiumLinuxSetAudioCaptureDevice()\n.then(({\n  requestNativeScript, mediaStream\n}) =\u003e {\n  // do stuff with MediaStream, MediaStreamTrack\n  // after selecting specific device at pavucontrol Recording tab\n  console.log(mediaStream, requestNativeScript);\n  const recorder = new MediaRecorder(mediaStream);\n  recorder.start();\n  recorder.ondataavailable = e =\u003e console.log(URL.createObjectURL(e.data));\n  setTimeout(_ =\u003e recorder.stop(), 30000);\n});\n```\n\n\u003ch5\u003eNative Messaging\u003c/h5\u003e\n\n\u003ch6\u003elaunch_pavucontrol\u003c/h6\u003e\n\nTo launch `pavucontrol` or `pavucontrol-qt` using Native Messaging open a terminal, `cd` to `native_messaging/host` folder, open `launch_pavucontrol.json` and substitute aboslute path to `launch_pavucontrol.sh` for `\"HOST_PATH\"`, then run the commands\n\n```\n$ cp launch_pavucontrol.json ~/.config/chromium/NativeMessagingHosts # Chromium, Chrome user configuration folder at Linux\n$ chmod u+x launch_pavucontrol.sh\n```\n\nnavigate to `chrome://extensions`, set `Developer mode` to on, click `Load unpacked` and select `app` folder.\n\nPin the app badge to the extension toolbar (it might be necessary to enable Extentions Toolbar Menu at `chrome://flags/#extensions-toolbar-menu`). When the  browser action of clicking the icon occurs `pavucontrol` (or, if installed and set in `launch_pavucontrol.sh`, `pavucontrol-qt`) will be launched. When no audio  device is being captured the `Recording` tab will be empty. When `navigator.getUserMedia({audio: true})` is executed a list populate the `Recording` tab where the user can check a device that will be dynamically set as the device being captured by  `getUserMedia({audio: true})`, using `pavucontrol-qt`\n\n\u003cimg src=\"./launch_pavucontrol_native_messaging.png\" alt=\"launch pavucontrol\"\u003e\n\n\u003cimg src=\"./launch_pavucontrol_native_messaging_1.png\" alt=\"pavucontrol before getUserMedia({audio: true})\"\u003e\n\n\u003cimg src=\"./launch_pavucontrol_native_messaging_2.png\" alt=\"pavucontrol after getUserMedia({audio: true}), dynamic audio device capture selection\"\u003e\n\n\u003ch6\u003efile_stream\u003c/h6\u003e\n\nSet permissions for `.js`, `.sh` files in `host` folder to executable. \n\nSet `\"HOST_PATH\"` in `host/native_messaging_file_stream.json` to absolute path to `host/native_messaging_file_stream.js`.\n\nCopy `native_messaging_file_stream.json` to `~/.config/chromium/NativeMessagingHosts`. \n\nClick `Load unpacked` at `chrome://extensions`, select `app` folder. \n\nTo set permission to communicate with Native Messaging on a web page run `app/set_externally_connectable.js` at `console`, select `app` directory to update `app/manifest.json`, then reload `background.js` at extensions tab GUI or using `chrom.runtime.reload()` at DevTools chrome-extension URL.\n\n\n\u003cb\u003eUsage\u003c/b\u003e\n\nSelect `app` directory at Native File System prompts for read and write access to local filesystem where raw PCM of system audio output is written to a file using  `parec` while reading the file during the write using Native File System, storing the data in shared memory, parsing input data in `AudioWorklet` connected to `MediaStreamTrack` outputting the captured system audio. \n\n```\nonclick = async _ =\u003e {\n  onclick = null;\n  // pass seconds, capture 9 minutes of system audio output\n  captureSystemAudio(60 * 9);\n  // do stuff with MediaStreamTrack of system audio capture\n    .then(async track =\u003e {\n      const stream = new MediaStream([track]);\n      const recorder = new MediaRecorder(stream);\n      recorder.start();\n      recorder.onstart = recorder.onstop = e =\u003e console.log(e);\n      stream.oninactive = stream.onactive = e =\u003e console.log(e);\n      track.onmute = track.onunmute = track.onended = e =\u003e console.log(e);\n      console.log(recorder, stream, track);\n      recorder.ondataavailable = async e =\u003e {\n        console.log(e.data);\n      };\n    })\n    .catch(console.error);\n};\n```\n\n\u003ch6\u003eWeb Accessible Resources, Transferable Streams, Media Capture Transform (\"Breakout Box\")\u003c/h6\u003e\n\nUtilize Chromium extension with `\"web_accessible_resources\"` set to an HTML file that we load as an `\u003ciframe\u003e` in Web pages listed in `\"matches\"`. Stream from Native Messaging host to `\u003ciframe\u003e`, enqueue data in a `ReadableStream` then transfer the stream to `parent` with `postMessage()`, read the stream in \"real-time\", write values to a `MediaStreamTrackGenerator`.\n\nDownload the directory [capture_system_audio](https://github.com/guest271314/captureSystemAudio/tree/master/native_messaging/capture_system_audio), set \"Developer mode\" to on at `chrome://extensions`, click \"Load unpacked\". Use `background_transferable.js`.\n\nNote the generated extension ID and substitute that value for `\u003cid\u003e` in `capture_system_audio.json`.\n  \nSet `.py`, or `.js` Native Messaging host file to executable `chmod u+x \u003chost\u003e`. Compile C and C++ versions. \n\nHosts should each produce the same result. Kindly file an issue if you find they do not.\n\n- C [capture_system_audio.c](https://github.com/guest271314/captureSystemAudio/blob/master/native_messaging/capture_system_audio/capture_system_audio.c)\n- C++ [capture_system_audio.cpp](https://github.com/guest271314/captureSystemAudio/blob/master/native_messaging/capture_system_audio/capture_system_audio.cpp)\n- Python3 [capture_system_audio.py](https://github.com/guest271314/captureSystemAudio/blob/master/native_messaging/capture_system_audio/capture_system_audio.py)\n- JavaScript (Node.js) [capture_system_audio_node.js](https://github.com/guest271314/captureSystemAudio/blob/master/native_messaging/capture_system_audio/capture_system_audio_node.js)\n- JavaScript (QuickJS) [capture_system_audio.js](https://github.com/guest271314/captureSystemAudio/blob/master/native_messaging/capture_system_audio/capture_system_audio.js)\n\nAdjust `\"path\"` in `capture_system_audio.json` to location of (compiled) executable.\n\nCopy Native Messaging manifest to Chromium \nor Chrome configuration folder\n\n`cp capture_system_audio.json ~/.config/chromium/NativeMessagingHosts`\n\nor\n\n`cp capture_system_audio.json ~/.config/google-chrome-unstable/NativeMessagingHosts`\n\nAt `console` or Sources -\u003e Snippets at origins set in `\"matches\"` in `manifest.json`.\n\n```\nvar audioStream = new AudioStream(\n  'parec -d @DEFAULT_MONITOR@', 'audio/webm;codecs=opus' // 'audio/mp3'\n);\n// audioStream.mediaStream: live MediaStream\naudioStream\n  .start()\n  .then((ab) =\u003e {\n    // ab: ArrayBuffer representation of WebM file from MediaRecorder\n    console.log(\n      URL.createObjectURL(\n        new Blob([ab], {\n          type: 'audio/webm;codecs=opus',\n        })\n      )\n    );\n  })\n  .catch(console.error);\n// stop capturing system audio output\naudioStream.stop();\n```\n\nAlternatively, click extension icon to start/stop system audio output capture.\n\n\u003ch6\u003eDynamically set and use \"externally_connectable\", Media Capture Transform (\"Breakout Box\")\u003c/h6\u003e\n\nSet `capture_system_audio.js` and `set_externally_connectable.js` executable. Follow same steps in [Web Accessible Resources, Transferable Streams, Media Capture Transform (\"Breakout Box\")](https://github.com/guest271314/captureSystemAudio#web-accessible-resources-transferable-streams-media-capture-transform-breakout-box) to set `\"path\"` in `capture_system_audio.json` and `set_externally_connectable.json` and copy the files to Chrome/Chromium configuration directory.\n\nOn click of action icon the current origin will be stored in a variable and the `manifest.json` will be overwritten with current origin pushed to `\"matches\"` array in `\"externally_connectable\"`.\n\nTo unset origins, pass empty array or array containing origins expected to be set in `manifest.json` in a copy of `manifest.json` to `chrome.runtime.sendNativeMessage('set_externally_connectable', manifest)`.\n\n\n\u003ch5\u003ePulseAudio module-remap-source\u003c/h5\u003e\n\nThis article [Virtual microphone using GStreamer and PulseAudio](https://aweirdimagination.net/2020/07/19/virtual-microphone-using-gstreamer-and-pulseaudio/) describes a workaround Chrome and Chromium browsers' refusal to list or capture monitor devices on Linux\n\n\u003e \u003cb\u003eRemap source\u003c/b\u003e\n\u003e\n\u003e While the null sink automatically includes a \"monitor\" source, many programs know to exclude monitors when listing microphones. To work around that, the [module-remap-source](https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/Modules/#module-remap-source) module lets us clone that source to another one not labeled as being a monitor:\n\u003e\n\u003e ```\n\u003e pactl load-module module-remap-source \\ \n\u003e     master=virtmic.monitor source_name=virtmic \\ \n\u003e     source_properties=device.description=Virtual_Microphone\n\u003e ```\n\nwe can run\n\n```\npactl load-module module-remap-source \\\n  master=@DEFAULT_MONITOR@ \\\n  source_name=virtmic source_properties=device.description=Virtual_Microphone\n```\n\nand then at Chromium and Chrome run\n\n```\nvar recorder;\nconst devices = await navigator.mediaDevices.enumerateDevices();\nconst device = devices.find(({label})=\u003elabel === 'Virtual_Microphone');\nconst stream = await navigator.mediaDevices.getUserMedia({\n          audio: {\n            deviceId: {\n              exact: device.deviceId\n            },\n            echoCancellation: false,\n            noiseSuppression: false,\n            autoGainControl: false,\n            channelCount: 2,\n          },\n        });\nconst [track] = stream.getAudioTracks();\nconsole.log(devices, track.label, track.getSettings(), await track.getConstraints());\n// do stuff with rempapped monitor device\nrecorder = new MediaRecorder(stream);\nrecorder.ondataavailable = e =\u003e console.log(URL.createObjectURL(e.data));\nrecorder.onstop = () =\u003e recorder.stream.getAudioTracks()[0].stop();\nrecorder.start();\nsetTimeout(()=\u003erecorder.stop(), 10000);\n```\n\nto first get permission to read labels of devices, find the device we want to capture, capture the virtual microphone device, in this case a monitor device, see https://bugs.chromium.org/p/chromium/issues/detail?id=931749#c6.\n\nWhen no microphone input devices are connected to the machine the remapped monitor device will be the default device `\"Virtual_Microphone\"` when `navigator.mediaDevices.getUserMedia({audio: true})` is executed the first time, negating the need to call `MediaStreamTrack.stop()` to stop capture of a microphone device just to get device access permission, then use `navigator.mediaDevices.enumerateDevices()` to get `deviceId` of monitor device, create a constraints object `{deviceId: {exact: device.deviceId}}` and call `navigator.mediaDevices.getUserMedia({audio: constraints})` a second time.\n\nTo set the default source programmatically to the virtual microphone `\"virtmic\"` `set-default-source` can be utilized\n```\npactl set-default-source virtmic\n```\nif running, closing then restarting Chrome, Chromium, or Firefox, the device selected by `navigator,mediaDevices.getUserMedia({audio: true})`, unless changed by selection or other setting, will be the remapped monitor device `\"Virtual_Microphone\"`.\n\nWhen `echoCancellation` is set to true and `channelCount` is not explicitly set to `2`, respectively, `channelCount` of audio `MediaStreamTrack` will always be `1`.\n\nRelated: When `channelCount` is set to `2` and `echoCancellation` is set to `true`, only silence is captured by `MediaRecorder`.\n\nExplicitly set `channelCount` to `2`, `echoCancellation` to `false` in audio constraints to capture 2 channels, when available.\n\n\u003ch5\u003eReferences\u003c/h5\u003e\n\n- https://lists.w3.org/Archives/Public/public-speech-api/2017Jun/0000.html\n- https://github.com/whatwg/html/issues/2823\n- https://github.com/whatwg/html/issues/3443\n- https://github.com/guest271314/SpeechSynthesisRecorder/issues/14\n- https://github.com/WICG/native-file-system/issues/72\n- https://github.com/WICG/native-file-system/issues/97\n- https://github.com/w3c/mediacapture-main/issues/629\n- https://github.com/WICG/speech-api/issues/69\n- https://github.com/web-platform-tests/wpt/issues/23084\n- https://github.com/w3c/mediacapture-main/issues/650\n- https://github.com/w3c/mediacapture-main/issues/654\n- https://gist.github.com/guest271314/59406ad47a622d19b26f8a8c1e1bdfd5\n- https://github.com/guest271314/requestNativeScripts\n- https://github.com/web-platform-tests/wpt/issues/23084\n- https://bugs.chromium.org/p/chromium/issues/detail?id=1013881\n- https://bugs.chromium.org/p/chromium/issues/detail?id=1032815\n- https://bugs.chromium.org/p/chromium/issues/detail?id=1074529\n- https://bugs.chromium.org/p/chromium/issues/detail?id=865799\n- https://github.com/w3c/mediacapture-screen-share/issues/140\n- https://stackoverflow.com/q/61502237\n- https://bugzilla.mozilla.org/show_bug.cgi?id=1422891\n- https://bugs.chromium.org/p/chromium/issues/detail?id=999580\n- https://bugs.chromium.org/p/chromium/issues/detail?id=795371\n- https://bugzilla.mozilla.org/show_bug.cgi?id=1425523\n- https://gitlab.freedesktop.org/pulseaudio/pavucontrol/-/issues/82\n- https://github.com/WebAudio/web-audio-api-v2/issues/97\n- https://gist.github.com/guest271314/04a539c00926e15905b86d05138c113c\n- https://github.com/guest271314/setUserMediaAudioSource\n- https://gist.github.com/guest271314/53e00c6765aa256362fb52c08e82d189#file-capture_monitor_devices_at_chromium_and_chrome_on_linux-md\n- https://www.freedesktop.org/wiki/Software/PulseAudio/FAQ/\n- https://bugs.chromium.org/p/chromium/issues/detail?id=453876\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fguest271314%2Fcapturesystemaudio","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fguest271314%2Fcapturesystemaudio","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fguest271314%2Fcapturesystemaudio/lists"}