Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save guest271314/04a539c00926e15905b86d05138c113c to your computer and use it in GitHub Desktop.
Save guest271314/04a539c00926e15905b86d05138c113c to your computer and use it in GitHub Desktop.
Capture monitor device at Nightly stream to Chromium

Chromium does not support capture of monitor devices (system audio output to headphones and speakers; "What-U-Hear") when navigator.mediaDevices.getUserMedia() is called and does not list monitor devices when navigator.mediaDevices.enumerateDevices() is called at Linux, see

Firefox does support capture of monitor devices at Linux.

Using https://github.com/fippo/paste as a template capture monitor device at Nightly stream the captured monitor device to Chromium using RTCPeerConnection.

Using clipboard for signaling is not ideal.

TODO: Improve the means of signaling to establish WebRTC peer connection between different applications.

At Firefox set the following preferences to true

  • dom.events.testing.asyncClipboard
  • media.navigator.permission.disabled

Nightly

index.html

<!DOCTYPE html>

<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <script>
      (async _ => {
        const webrtc = new RTCPeerConnection({ sdpSemantics: 'unified-plan' });
        [
          'onsignalingstatechange',
          'oniceconnectionstatechange',
          'onicegatheringstatechange',
        ].forEach(event => webrtc.addEventListener(event, console.log));
        let sdp;
        webrtc.onicecandidate = async event => {
          console.log('candidate', event.candidate);
          if (!event.candidate) {
            sdp = webrtc.localDescription.sdp;
            if (sdp.indexOf('a=end-of-candidates') === -1) {
              sdp += 'a=end-of-candidates\r\n';
            }
            try {
              await navigator.clipboard.writeText(sdp);

              async function* readClipboard() {
                while (true) {
                  try {
                    await new Promise(resolve => setTimeout(resolve, 1000));
                    // dom.events.testing.asyncClipboard
                    // optionally dom.events.asyncClipboard.dataTransfer
                    const text = await navigator.clipboard.readText();
                    if (
                      text.replace(/[\n\s]+/g, '') !==
                      sdp.replace(/[\n\s]+/g, '')
                    ) {
                      sdp = text;
                      console.log({ sdp, text });
                      break;
                    }
                    yield text;
                  } catch (e) {
                    console.error(e);
                    throw e;
                  }
                }
              }
              for await (const text of readClipboard()) {
                console.log(text);
              }

              await webrtc.setRemoteDescription({ type: 'answer', sdp: sdp });
            } catch (e) {
              throw e;
            }
          }
        };
        try {
          // media.navigator.permission.disabled
          let stream = await navigator.mediaDevices.getUserMedia({
            audio: true,
          });
          const label = 'Monitor of Built-in Audio Analog Stereo';
          let [track] = stream.getAudioTracks();
          if (track.label !== label) {
            const device = (
              await navigator.mediaDevices.enumerateDevices()
            ).find(({ label: _ }) => label === _);
            const { deviceId } = device;
            console.log(device);
            track.stop();
            stream = await navigator.mediaDevices.getUserMedia({
              audio: { deviceId: { exact: deviceId } },
            });
            [track] = stream.getAudioTracks();
          }

          const sender = webrtc.addTransceiver(stream.getAudioTracks()[0], {
            streams: [stream],
            direction: 'sendonly',
          });
          const offer = await webrtc.createOffer();
          webrtc.setLocalDescription(offer);
        } catch (e) {
          throw e;
        }
      })().catch(console.error);
    </script>
  </body>
</html>

Chromium

answer.html

<!DOCTYPE html>

<html>
  <head>
    <meta charset="utf-8" />
    <style>
      body *:not(script) {
        display: block;
      }
    </style>
  </head>
  <body>
    <button id="capture">Capture system audio</button>
    <audio id="audio" autoplay controls muted></audio>

    <script>
      const audio = document.getElementById('audio');
      const capture = document.getElementById('capture');
      ['loadedmetadata', 'play', 'playing'].forEach(event =>
        audio.addEventListener(event, console.log)
      );
      const webrtc = new RTCPeerConnection({ sdpSemantics: 'unified-plan' });
      [
        'onsignalingstatechange',
        'oniceconnectionstatechange',
        'onicegatheringstatechange',
      ].forEach(event => webrtc.addEventListener(event, console.log));

      webrtc.onicecandidate = async event => {
        if (!event.candidate) {
          let sdp = webrtc.localDescription.sdp;
          if (sdp.indexOf('a=end-of-candidates') === -1) {
            sdp += 'a=end-of-candidates\r\n';
          }
          try {
            await navigator.clipboard.writeText(sdp);
          } catch (e) {
            console.error(e);
          }
        }
      };
      webrtc.ontrack = ({ transceiver, streams: [stream] }) => {
        console.log(transceiver);
        const {
          receiver: { track },
        } = transceiver;
        track.onmute = track.onunmute = e => console.log(e);
        audio.srcObject = stream;
      };
      onfocus = async _ => {
        onfocus = null;
        try {
          const sdp = await navigator.clipboard.readText();
          console.log(sdp);
          await webrtc.setRemoteDescription({ type: 'offer', sdp });
          const answer = await webrtc.createAnswer();
          webrtc.setLocalDescription(answer);
          await navigator.clipboard.writeText();
        } catch (e) {
          console.error(e);
        }
      };
    </script>
  </body>
</html>

Launch with

$ $HOME/firefox/firefox-bin -new-instance -devtools -P "webrtc" & $HOME/chrome-linux/chrome-wrapper
@guest271314
Copy link
Author

Screenshot_2020-09-07_16-30-22

@guest271314
Copy link
Author

Nightly. Just capturing default device. Paste this in Browser Console. Run the script

(async (_) => {
  const webrtc = new RTCPeerConnection({ sdpSemantics: "unified-plan" });
  [
    "onsignalingstatechange",
    "oniceconnectionstatechange",
    "onicegatheringstatechange",
  ].forEach((event) => webrtc.addEventListener(event, console.log));
  let sdp;
  webrtc.onicecandidate = async (event) => {
    console.log("candidate", event.candidate);
    if (!event.candidate) {
      sdp = webrtc.localDescription.sdp;
      try {
        await navigator.clipboard.writeText(sdp);

        async function* readClipboard() {
          while (true) {
            try {
              await new Promise((resolve) => setTimeout(resolve, 1000));
              // dom.events.testing.asyncClipboard
              // optionally dom.events.asyncClipboard.dataTransfer
              const text = await navigator.clipboard.readText();
              if (
                text.replace(/[\n\s]+/g, "") !==
                  sdp.replace(/[\n\s]+/g, "")
              ) {
                sdp = text;
                console.log({ sdp, text });
                break;
              }
              yield text;
            } catch (e) {
              console.error(e);
              throw e;
            }
          }
        }
        for await (const text of readClipboard()) {
          console.log(text);
        }

        await webrtc.setRemoteDescription({ type: "answer", sdp: sdp });
      } catch (e) {
        throw e;
      }
    }
  };
  try {
    // media.navigator.permission.disabled
    let stream = await navigator.mediaDevices.getUserMedia({
      audio: true,
    });
    const sender = webrtc.addTransceiver(stream.getAudioTracks()[0], {
      streams: [stream],
      direction: "sendonly",
    });
    const offer = await webrtc.createOffer();
    webrtc.setLocalDescription(offer);
  } catch (e) {
    throw e;
  }
})().catch(console.error);

Chromium-based browser. Paste this in Snippets in DevTools. Give the script a name such as "nightly-chromium-clipboard-webrtc.js". Run the script after running the above script in Firefox Nightly

document.body.insertAdjacentHTML(
  "beforeend",
  `<button id="capture">Capture system audio</button>
    <audio id="audio" autoplay controls></audio>
`,
);

var audio = document.getElementById("audio");
var capture = document.getElementById("capture");
["loadedmetadata", "play", "playing"].forEach((event) =>
  audio.addEventListener(event, console.log)
);
const webrtc = new RTCPeerConnection({ sdpSemantics: "unified-plan" });
[
  "onsignalingstatechange",
  "oniceconnectionstatechange",
  "onicegatheringstatechange",
].forEach((event) => webrtc.addEventListener(event, console.log));

webrtc.onicecandidate = async (event) => {
  if (!event.candidate) {
    let sdp = webrtc.localDescription.sdp;
    try {
      await navigator.clipboard.writeText(sdp);
    } catch (e) {
      console.error(e);
    }
  }
};
webrtc.ontrack = ({ transceiver, streams: [stream] }) => {
  console.log(transceiver);
  const {
    receiver: { track },
  } = transceiver;
  track.onmute = track.onunmute = (e) => console.log(e);
  audio.srcObject = stream;
};
capture.onclick = async (_) => {
  console.log(_);
  capture.onclick = null;
  try {
    const sdp = await navigator.clipboard.readText();
    console.log(sdp);
    await webrtc.setRemoteDescription({ type: "offer", sdp });
    const answer = await webrtc.createAnswer();
    webrtc.setLocalDescription(answer);
    await navigator.clipboard.writeText("");
  } catch (e) {
    console.error(e);
  }
};

If everything worked as expected the timeline on the <audio> element in Chromium should advance after the connection is established and the audio element srcObject is set to the MediaStream from Nightly.

@guest271314
Copy link
Author

Click the appended button that reads "Capture system audio" in Chromium to make read the clipboard containing SDP from Firefox Nightly - before copying anything else to the clipboard. Add muted attribute to the <audio> element to avoid feedback from microphone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment