Skip to content

Instantly share code, notes, and snippets.

@saghul
Last active August 24, 2024 04:29
Show Gist options
  • Save saghul/179feba3df9f12ddf316decd0181b03e to your computer and use it in GitHub Desktop.
Save saghul/179feba3df9f12ddf316decd0181b03e to your computer and use it in GitHub Desktop.
Streaming a webcam to a Jitsi Meet room
const puppeteer = require('puppeteer');
// Streams the first webcam in the system to the specified Jitsi Meet room. Audio is currently
// not sent, but it can be easily enabled by disabling the corresponding setting in `meetArgs`.
//
// TODO
// - Detect if we are kicked from the room
// - Support authenticated deployments
//
// NOTE: only tested on GNU/Linux.
async function main(room, baseUrl='https://meet.jit.si') {
const chromeArgs = [
// Disable sandboxing, gives an error on Linux
'--no-sandbox',
'--disable-setuid-sandbox',
// Automatically give permission to use media devices
'--use-fake-ui-for-media-stream',
// Silence all output, just in case
'--alsa-output-device=plug:null'
];
const meetArgs = [
// Disable receiving of video
'config.channelLastN=0',
// Mute our audio
'config.startWithAudioMuted=true',
// Don't use simulcast to save resources on the sender (our) side
'config.disableSimulcast=true',
// No need to process audio levels
'config.disableAudioLevels=true',
// Disable P2P mode due to a bug in Jitsi Meet
'config.p2p.enabled=false'
];
const url = `${baseUrl}/${room}#${meetArgs.join('&')}`;
console.log(`Loading ${url}`);
const browser = await puppeteer.launch({ args: chromeArgs, handleSIGINT: false });
const page = await browser.newPage();
// Manual handling on SIGINT to gracefully hangup and exit
process.on('SIGINT', async () => {
console.log('Exiting...');
await page.evaluate('APP.conference.hangup();');
await page.close();
browser.close();
console.log('Done!');
process.exit();
});
await page.goto(url);
// Set some friendly display name
await page.evaluate('APP.conference._room.setDisplayName("Streamer");');
console.log('Running...');
}
main(process.argv[2] || 'test123');
@MartijnHols
Copy link

MartijnHols commented May 4, 2021

Thanks! I used this to make a version that can be used to test Jitsi call integrations with Cypress. It starts a webserver to listen for /start and /stop calls. This was necessary in CI, since the connection uses a significant amount of CPU which can be troublesome in CI (e.g. in GitHub Actions). It isn't very pretty but it works and it's for test so 🤷‍♂️

// Based on https://gist.github.com/saghul/179feba3df9f12ddf316decd0181b03e
const http = require('http')
const path = require('path')
const puppeteer = require('puppeteer')

async function main(room, baseUrl) {
  const chromeArgs = [
    // Disable sandboxing, gives an error on Linux and supposedly breaks fake audio capture
    '--no-sandbox',
    '--disable-setuid-sandbox',
    // Automatically give permission to use media devices
    '--use-fake-ui-for-media-stream',
    // feeds a test pattern to getUserMedia() instead of live camera input
    '--use-fake-device-for-media-stream',
    // To make your own video see https://testrtc.com/y4m-video-chrome/ or download one from https://media.xiph.org/video/derf/
    `--use-file-for-fake-video-capture=${path.resolve(
      __dirname,
      'akiyo_cif.y4m',
    )}`,
    // To make your own just use a recorded and convert to wav, e.g. `ffmpeg -i in.mp3 out.wav`
    `--use-file-for-fake-audio-capture=${path.resolve(__dirname, 'test.wav')}`,
    // Silence all output, just in case
    '--alsa-output-device=plug:null',
    // Performance from https://stackoverflow.com/a/58589026/684353
    '--disable-dev-shm-usage',
    '--disable-accelerated-2d-canvas',
    '--no-first-run',
    '--no-zygote',
    '--single-process',
    '--disable-gpu',
  ]

  const browser = await puppeteer.launch({
    args: chromeArgs,
    handleSIGINT: false,
  })

  const page = await browser.newPage()
  page.on('console', (msg) => console.log('CONSOLE:', msg.text()))

  const meetArgs = [
    // Disable receiving of video
    'config.channelLastN=0',
    // Mute our audio
    // 'config.startWithAudioMuted=true',
    // Don't use simulcast to save resources on the sender (our) side
    'config.disableSimulcast=true',
    // No need to process audio levels
    'config.disableAudioLevels=true',
    // Disable P2P mode due to a bug in Jitsi Meet
    'config.p2p.enabled=false',
  ]
  const url = `${baseUrl}/${room}#${meetArgs.join('&')}`
  console.log(`Loading ${url}`)

  await page.goto(url)

  // Set some friendly display name
  await page.evaluate('APP.conference.changeLocalDisplayName("Fake Client");')

  console.log('Running...')

  return async () => {
    await page.evaluate('APP.conference.hangup();')
    await page.close()
    await browser.close()
    console.log('Stopped.')
  }
}

const port = process.env.PORT || 8000
const jitsiDomain = process.env.JITSI_DOMAIN || 'https://meet.jit.si'

let close = undefined
const server = http
  .createServer(async (req, res) => {
    const url = new URL(req.url, `http://${req.headers.host}`)

    if (url.pathname === '/start') {
      const room = url.searchParams.get('room')
      if (!room) {
        res.statusCode = 400
        res.statusMessage = 'MISSING ROOM PARAM'
      } else if (close) {
        res.statusCode = 422
        res.statusMessage = 'ALREADY STARTED'
      } else {
        close = await main(room, jitsiDomain)
        res.write('OK')
      }
      res.end()
      return
    }

    if (url.pathname === '/stop') {
      if (close) {
        await close()
        close = undefined
        res.write('OK')
      } else {
        res.statusCode = 422
        res.statusMessage = 'NOT STARTED'
      }
      res.end()
      return
    }

    res.statusCode = 404
    res.statusMessage = 'NOT FOUND'
    res.end()
  })
  .listen(port)
console.log('Now listening to port', port)

// Manual handling on SIGINT to gracefully hangup and exit
process.on('SIGINT', async () => {
  if (close) {
    console.log('Exiting...')
    await close()
    console.log('Done!')
  }
  server.close()
  process.exit()
})

Just start this before Cypress with node and add this to the Cypress test suite that tests the Jitsi integration:

  before(() => {
    cy.request({
      url: 'http://localhost:8000/start?room=test',
      failOnStatusCode: false,
    }).then(({ status, statusText }) => {
      // Allow 422: already started. This can occur when the `after` wasn't called, e.g. in development
      if (status !== 200 && status !== 422) {
        throw new Error(statusText)
      }
    })
  })
  after(() => {
    cy.request('http://localhost:8000/stop')
  })

@saghul
Copy link
Author

saghul commented May 4, 2021

Nice!

@MartijnHols
Copy link

Current version needs the following as part of the meetArgs in order to actually connect:

    // Skip the prejoin page - join the conference immediately
    'config.prejoinPageEnabled=false',

@alexivaner
Copy link

alexivaner commented Sep 27, 2023

    async isOnlyParticipant() {
        try {
            const members = await this.page.evaluate('APP.conference.membersCount;');


            console.log('Number of participants: %o', members);

            // Check if there is only one member (yourself)
            return members.length === 1;
        } catch (error) {
            console.error('Error checking the number of participants:', error);
            throw error;
        }
    }

Hi everyone, do you know why I always get this error in my puppeteer when executing the above code? Thanks

Error checking the number of participants: 91 |         name = detail.name;
92 |         message = detail.message;
93 |     }
94 |     const messageHeight = message.split('\n').length;
95 |     const error = new Error(message);
96 |     error.name = name;
                     ^
TypeError: Cannot read properties of undefined (reading 'getParticipants')
      at /Users/ivanhutomo/Documents/GitHub/quickConference/node_modules/puppeteer-core/lib/esm/puppeteer/common/util.js:96:17

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