Quick Start
This page takes you from nothing to audio playing in a group call, then adds video. Make sure you've read the prerequisites — you need an MTProto library logged in as a user account and ffmpeg installed.
The call always follows the same four steps:
- Create a call context and get a WebRTC offer.
- Join the group call through your MTProto lib, passing that offer, and get Telegram's answer.
- Connect with the answer to finish the handshake.
- Stream media into the call.
Audio
1. Create the context
import { NtgCalls } from '@arnabxd/ntgcalls-napi';
const ntg = new NtgCalls();
// Register listeners before the call starts.
ntg.on('connection-change', (chatId, kind, state) => {
console.log(`call ${chatId}: kind=${kind} state=${state}`);
});
const offer = await ntg.create(chatId); // WebRTC offer SDP
2. Join the call with your MTProto lib
You hand the offer to Telegram and get an answer back. Do this with whatever MTProto client you use:
const answer = await client.joinVideoChat(videoChatId, offer, {
isAudioEnabled: true,
isVideoEnabled: false,
});
// phone.joinGroupCall — offer goes in `params`, result holds the answer
const result = await client.invoke(
new Api.phone.JoinGroupCall({
call: inputGroupCall,
params: new Api.DataJSON({ data: offer }),
muted: false,
joinAs: 'me',
}),
);
const answer = /* the DataJSON answer from `result` */;
3. Connect
await ntg.connect(chatId, answer, false); // `false` = not a presentation/screen share
4. Play audio
set_audio_source is the shortcut for the mic-only case. It runs your ffmpeg command in SHELL
mode and reads raw s16le 48 kHz mono audio from its stdout:
await ntg.set_audio_source(
chatId,
`ffmpeg -i "${url}" -vn -f s16le -ar 48000 -ac 1 -`,
);
That's a full audio call. To stop:
await ntg.stop(chatId);
Video
Video isn't a separate method — set_audio_source is just a shortcut. For video you use
set_stream_sources with both a microphone and a camera description.
Important
Enable video at join time. If you join audio-only you can't hot-swap video in later — you have to stop and re-join with video enabled. So set
isVideoEnabled: truein step 2.
Step 2, now with video enabled:
const answer = await client.joinVideoChat(videoChatId, offer, {
isAudioEnabled: true,
isVideoEnabled: true,
});
// JoinGroupCall with video params enabled, offer in `params`
const result = await client.invoke(
new Api.phone.JoinGroupCall({
call: inputGroupCall,
params: new Api.DataJSON({ data: offer }),
muted: false,
videoStopped: false,
joinAs: 'me',
}),
);
const answer = /* the DataJSON answer from `result` */;
Then, instead of set_audio_source, push two tracks — one ffmpeg command each:
await ntg.set_stream_sources(chatId, 0 /* capture */, {
microphone: {
mediaSource: 2, // SHELL
input: `ffmpeg -i "${url}" -vn -f s16le -ar 48000 -ac 1 -`,
sampleRate: 48000,
channelCount: 1,
keepOpen: false,
},
camera: {
mediaSource: 2, // SHELL
input: `ffmpeg -i "${url}" -an -f rawvideo -pix_fmt yuv420p -vf "scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2,fps=30" -`,
width: 1280,
height: 720,
fps: 30,
keepOpen: false,
},
});
Warning
The raw frames ffmpeg produces must match the
width×heightyou declare exactly. That's what thescale=…,pad=…filter is for: it fits any source into a fixed 1280×720 canvas. Get this wrong and you'll see garbled or rejected video.
mediaSource: 2 is SHELL mode — ntgcalls spawns the command and reads the track from its stdout.
Audio is s16le 48 kHz; video is rawvideo yuv420p. The streamMode argument is 0 for capture
(what you send) and 1 for playback.
Knowing when a track ends
set_audio_source / set_stream_sources return as soon as streaming starts — they don't wait for
the media to finish. Listen for stream-end to queue the next track (e.g. a music bot playlist):
ntg.on('stream-end', (chatId, streamType, streamDevice) => {
if (streamType === 0) {
// 0 = audio track finished — play the next one
playNext(chatId);
}
});
A working reference
For a complete, real-world bot built on this exact flow, see
TGVCBot (branch feature/bun-rewrite) — look at
src/tgcalls.ts and src/ntgcalls/client.ts.
Next: the full API Reference, or Best Practices for the gotchas worth knowing up front.