Understanding Janus need help client side

Hi everyone, I am having trouble understanding Janus and wondering if the following is possible:

Have janus running in a raspberry pi, this pi hosts its own website which is reachable via https.
From the client side on the site I want to get a video stream without exposing the janus api to the client, for example have a middleman api that uses janus-gateway-js to create the session and attach the plugin. I am able to do this but I am stuck in the part where the client side gets the stream from the middleman api. It seems that the client side HAS to create a connection to the janus api, for example running janus-gateway-js directly in the browser.

I’m kind of lost on this and I’d appreciate any help.

Use something like Janode to create an application server that will talk to Janus on behalf of the user. Then expose whatever API you want to end users from that application server.

Understood, are there any resources that explain the flow in detail? I am able to attach the plugin but once that is done I dont know how to proceed. How does the client establish a connection to Janode?

Janode doesn’t support the SIP plugin yet. You can refer to the documentation of the SIP plugin for more info on how the plugin works, and to figure out how to modify the SIP demo to fit your needs (or write a new frontend).

Thank you for your reply. I have been tinkering with it and I managed to get to the point where the client and server exchange sdp and even manage to get iceconnection connected on client, all through api calls from client to my middleman api, but janus api just hangs on this:

Creating new session: 7089357887750896; 0x7f24000d70
Creating new handle in session 7089357887750896: 7584922528700476; 0x7f24000d70 0x7f70002210
[7584922528700476] Creating ICE agent (ICE Full mode, controlling)
[WARN] [7584922528700476] Waiting for candidates-done callback... (slow gathering, are you using STUN or TURN for Janus too, instead of just for users? Consider enabling full-trickle instead)
[WARN] [7584922528700476] Still waiting for candidates-done callback... (slow gathering, are you using STUN or TURN for Janus too, instead of just for users? Consider enabling full-trickle instead)
[WARN] [7584922528700476] Still waiting for candidates-done callback... (slow gathering, are you using STUN or TURN for Janus too, instead of just for users? Consider enabling full-trickle instead)
[WARN] [7584922528700476] Still waiting for candidates-done callback... (slow gathering, are you using STUN or TURN for Janus too, instead of just for users? Consider enabling full-trickle instead)
[WARN] [7584922528700476] Waited 5s for candidates, that's way too much... going on with what we have (WebRTC setup might fail)

here is my api code, any idea why my .on never triggers? I was following the janode example but it doesnt work in my code, rather I never see the logs:

import express from 'express';
import http from 'http';
import Janode from 'janode';
import StreamingPlugin from 'janode/plugins/streaming';

import { fileURLToPath } from 'url';
import { dirname, basename } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const { Logger } = Janode;
const LOG_NS = `[${basename(__filename)}]`;

import cors from 'cors';

const connection = await Janode.connect({
  is_admin: false,
  address: {
    url: 'ws://localhost:8188/',
    apisecret: 'secret'
  }
});

// Map to store active sessions for each user
const userSessions = new Map();

let session;
let streamingHandle;

const createSessionAndAttachPlugin = async (userId) => {
  if (!userSessions.has(userId)) {
    session = await connection.create();
    
    // Attach to a plugin using the plugin descriptor
    streamingHandle = await session.attach(StreamingPlugin);
    
    userSessions.set(userId, { session, streamingHandle });
    return streamingHandle;
  } else {
    return userSessions.get(userId).streamingHandle;
  }
};

const createOffer = async (userId) => {
  if (userSessions.has(userId)) {
    const { streamingHandle } = userSessions.get(userId);
    const w = await streamingHandle.watch({ id: 99 });
    return w.jsep;
  } else {
    throw new Error('Streaming handle not available for this user.');
  }
};

const startStream = async (userId, answer) => {
  if (userSessions.has(userId)) {
    const { streamingHandle } = userSessions.get(userId);
    streamingHandle.on(Janode.EVENT.HANDLE_TRICKLE, evtdata => Logger.info(`${LOG_NS} ${streamingHandle.name} trickle event ${JSON.stringify(evtdata)}`));
    const start = await streamingHandle.start(99, answer);
    return 'Stream started successfully';
  } else {
    throw new Error('Streaming handle not available for this user.');
  }
};

const doTrickle = async (userId, candidate) => {
  if (userSessions.has(userId)) {
    const { streamingHandle } = userSessions.get(userId);
    const trickle = await streamingHandle.trickle(candidate);
    return 'Trickle started successfully';
  } else {
    throw new Error('Streaming handle not available for this user.');
  }
};

const doTrickleComplete = async (userId) => {
  if (userSessions.has(userId)) {
    const { streamingHandle } = userSessions.get(userId);
    const tricklecomplete = await streamingHandle.trickleComplete();
    return 'Trickle completed successfully';
  } else {
    throw new Error('Streaming handle not available for this user.');
  }
};

const app = express();

// Use CORS middleware
app.use(cors());

// Middleware to parse JSON requests
app.use(express.json());

// Handle incoming client requests
app.post('/advanced/watchstream', async (req, res) => {
  try {
    const userId = req.body.userId;
    const streamingHandle = await createSessionAndAttachPlugin(userId);
    const offer = await createOffer(userId);
    res.status(200).json(offer);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/advanced/startstream', async (req, res) => {
  try {
    const userId = req.body.userId;
    const answer = req.body.answer;
    const result = await startStream(userId, answer);
    const response = {"hello":"world"};
    res.status(200).json(response);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/advanced/sendcandidate', async (req, res) => {
  try {
    const candidate = req.body.candidate;
    const userId = req.body.userId;
    const response = await doTrickle(userId, candidate);
    res.status(200).json("test");
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.post('/advanced/rtctricklecomplete', async (req, res) => {
  try {
    const userId = req.body.userId;
    const response = await doTrickleComplete(userId);
    res.status(200).json("test");
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});


const server = http.createServer(app);
const PORT = process.env.PORT || 8080;
server.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

Thank you again for your time.

You’re not relaying candidates via trickle messages or in the SDPs.

I think I am? Unless I’m doing it wrong, here is the output of what I’m sending here:


app.post('/advanced/sendcandidate', async (req, res) => {
  try {
    const candidate = req.body.candidate;
    const userId = req.body.userId;
    const response = await doTrickle(userId, candidate);
    res.status(200).json("test");
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

const doTrickle = async (userId, candidate) => {
  if (userSessions.has(userId)) {
    const { streamingHandle } = userSessions.get(userId);
    console.log("CANDIDATE: ", candidate);
    const trickle = await streamingHandle.trickle(candidate);
    return 'Trickle started successfully';
  } else {
    throw new Error('Streaming handle not available for this user.');
  }
};
CANDIDATE:  {
  candidate: 'candidate:2578279508 1 udp 2113937151 realaddresswasherebutremovedit.local 51371 typ host generation 0 ufrag drJ3 network-cost 999',
  sdpMid: 'v',
  sdpMLineIndex: 0
}

I make the api call that sends the candidate from the client side triggered by pc.onicecandidate

Yes but is that candidate getting to Janus via a trickle request on the right handle?

I do that here, in doTrickle I find the corresponding handle and do .trickle but I notice that this is called after the 5s candidate timeout, which starts the moment I call createOffer and do streamingHandle.watch