gotgcall

package module
v0.8.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 11, 2026 License: GPL-3.0 Imports: 13 Imported by: 0

README

gotgcall

The first drop-in replacement for Telegram Group Calls — with audio and video — in pure Go.
A drop-in alternative to ntgcalls / pytgcalls — built for Go music bots, livestream bots, and broadcast tooling. No libwebrtc. No cgo. No native build chain.

Go Reference Go Report Card DeepSource CGO-free pion v4 First pure-Go Telegram group call library

client, _ := gotgcall.New()
defer client.Close()

localParams, _ := client.CreateCall(chatID)
remoteParams   := joinViaYourMTProto(localParams)  // gogram / your MTProto stack
client.Connect(chatID, remoteParams)
client.SetStreamSources(chatID, gotgcall.FromFile("song.mp3", gotgcall.EncodeOptions{}))

That's a working voice-chat playback bot. Everything else in this README is options on top.

Highlights

  • Single static binary. CGO_ENABLED=0 go buildscp → run. No libwebrtc, no glibc, no C++ toolchain — ffmpeg is the only runtime dependency.
  • Fast connect. Reaches the SFU in tens of milliseconds. Built on pion v4 under the hood.
  • Blob-only signalling. The library never imports gogram or any MTProto code. Use any MTProto Go client you like.
  • ntgcalls-shaped API. CreateCall / Connect / SetStreamSources / Pause / Resume / Mute / SeekBy / Stop — existing bot code translates line-for-line.
  • Three source modes. FromFile, FromURL, FromShell — anything ffmpeg can decode is fair game (HLS, RTSP, RTMP, MJPEG, screen capture, …).
  • WebRTC + RTMP push. Group voice/video chats and "go live" RTMP broadcasts via one client.
  • Scales to tens of thousands of calls per process with WithSharedUDPMux + raised FD limits.

At a glance

Language Pure Go (CGO_ENABLED=0)
Min Go version 1.26
Codecs Opus (audio) · VP8 (video)
Signalling Blob JSON — bring your own MTProto layer
Runtime dep ffmpeg on PATH (or WithFFmpegPath)
Modes WebRTC group call · RTMP livestream push
License MIT

Status — Stable. Built for my own bots; the API is intentionally close to ntgcalls so existing code translates with minimal change. Breaking changes are tagged in releases.

Table of contents

Install

go get github.com/annihilatorrrr/gotgcall

ffmpeg must be on PATH at runtime (or set gotgcall.WithFFmpegPath("/path/to/ffmpeg")). New() fails fast if the binary isn't found, so the error surfaces at startup rather than on the first stream.

Requires Go 1.26+ (uses errors.AsType[T] and a few stdlib features added in 1.26).

Architecture at a glance

   ┌────────────┐    blob JSON     ┌─────────────────────┐
   │   Client   │ ◀──────────────▶ │   Your MTProto      │
   │ (gotgcall) │                  │   layer (gogram, …) │
   └────────────┘                  └─────────────────────┘
         │
         ├──▶  GroupCall   (WebRTC: audio + video)
         └──▶  RTMPCall    (RTMP push: "go live")
                  │
                  ▼
            Telegram SFU

Blob-only signalling. CreateCall(chatID) returns a JSON string; you hand it to phone.JoinGroupCall via your own MTProto stack, then feed the response back via Connect(chatID, respJSON). The library never imports gogram or any MTProto code, so it stays MTProto-version-independent.

Send-only audio + video. Outgoing Opus + VP8. The library doesn't receive incoming media — group calls are one-way from the bot's perspective.

ffmpeg is the encoder. ffmpeg is invoked as a subprocess for decoding and encoding; nothing is linked into the Go binary. That's how CGO_ENABLED=0 is possible.

Quick start

client, err := gotgcall.New()
if err != nil { log.Fatal(err) }
defer client.Close()

client.OnStreamEnd(func(chat int64, t gotgcall.StreamType, d gotgcall.Device, err error) {
    log.Printf("stream end: %v", err)
})
client.OnConnectionChange(func(chat int64, info gotgcall.NetworkInfo) {
    log.Printf("conn state: %s", info.State)
})
client.OnUpgrade(func(chat int64, state gotgcall.MediaState) {
    // Fires on Mute / Unmute / Pause / Resume and on spontaneous
    // transitions (video leg dying mid-stream, ICE Failed/Closed
    // while video was active). SetStreamSources and Stop stay silent
    // — the caller already knows the new state.
    //
    // state fields mirror Telegram's MTProto participant flags
    // (Paused maps to video_paused — "media not flowing"):
    //   Muted              — explicit mute toggle
    //   Paused             — Muted || the call was paused
    //   VideoStopped       — true for Play (audio-only), false for VPlay
    //   PresentationPaused — same lifecycle as Paused (no presentation
    //                        source in this library)
})

// 1. Local-side JSON.
localParams, _ := client.CreateCall(chatID)

// 2. Drive Telegram via your MTProto layer (gogram, etc.).
//    Pass localParams to phone.JoinGroupCall; read the response.
remoteParams := joinViaYourMTProto(localParams)

// 3. Finish the WebRTC handshake.
client.Connect(chatID, remoteParams)

// 4. Stream.
client.SetStreamSources(chatID, gotgcall.FromFile("song.mp3", gotgcall.EncodeOptions{}))

// 5. Pause / resume / mute / change source any time.
client.Pause(chatID)
client.Resume(chatID)
client.SetStreamSources(chatID, gotgcall.FromURL("https://stream.example.com/radio.m3u8", gotgcall.EncodeOptions{}))

// 6. Stop tears down the call.
client.Stop(chatID)

See examples/bot/ for a runnable skeleton against gogram (own go.mod so the example doesn't taint the library's dependency tree).

Sources

All sources target Opus-in-OGG (audio) and/or VP8-in-IVF (video) on ffmpeg's stdout. The library will not accept raw PCM/YUV — the frame readers can't parse them.

FromFile / FromURL

gotgcall.FromFile("song.mp3", gotgcall.EncodeOptions{})
gotgcall.FromURL("https://stream.example.com/...", gotgcall.EncodeOptions{})

Anything ffmpeg can decode is fair game — mp3, m4a, flac, ogg, opus, wav, webm, mp4, mkv, mov, m3u8 (HLS), live RTMP/RTSP, etc.

Defaults to audio only, regardless of what the container holds. Opt in to video extraction:

client.SetStreamSources(chatID, gotgcall.FromFile("movie.mp4", gotgcall.EncodeOptions{
    Tracks: gotgcall.TrackAudio | gotgcall.TrackVideo,
    // Or just TrackVideo — TrackVideo implies TrackAudio (a video file is a
    // video file with audio).
}))

Fast-start probing (-analyzeduration 0 -probesize 64k) is on by default for every source — cuts ~1-2 s off ffmpeg's startup latency vs the stock defaults (5 s + 5 MB). HLS sources additionally get -user_agent, -protocol_whitelist file,http,https,tcp,tls, -rw_timeout 10s, -http_persistent 1; HTTP/HTTPS sources get -reconnect 1 -reconnect_at_eof 1 -reconnect_streamed 1 -reconnect_delay_max 5 -timeout 10s so transient network blips don't kill the stream.

Both FromFile and FromURL return seekable sources. Pause records the elapsed offset and Resume re-spawns ffmpeg with -ss <offset> injected before the input.

FromShell — single custom ffmpeg leg

gotgcall.FromShell(`ffmpeg -i "song.mp3"`, gotgcall.TrackAudio)

FromShell parses the cmdline as a shell-like argv (handles double-quoted args, plus \" and \\ escape sequences for filenames containing literal " or \ — e.g. a Telegram audio titled (From "Foo") that would otherwise slice the path mid-string when the embedded quote toggled the quote state) and spawns it directly via exec, NOT via /bin/sh. Shell metacharacters in filenames can't inject commands; use %q for filenames.

Auto-injected if missing (so the minimal command above just works):

Position Flags
Before -i -analyzeduration 0 -probesize 64k -err_detect ignore_err
Audio out -c:a libopus -application audio -frame_duration 20 -page_duration 20000 -mapping_family 0 -ar 48000 -ac 2 -f ogg
Video out -c:v libvpx -deadline realtime -f ivf
Last token pipe:1

Not auto-injected (specify yourself if you need them): -b:a / -b:v, -vn / -an, -map, -re, HLS reconnect flags (-user_agent, -protocol_whitelist, -reconnect *), HTTP -headers, -stream_loop, hardware accel. The auto-fill is conservative — anything you pass is left alone.

A single FromShell produces one output (audio OR video). Raw PCM/YUV output codecs (-c:a pcm_*, -f rawvideo, …) are rejected up front with a pointer at the correct flags.

Audio recipes

All examples below are FromShell(<cmd>, gotgcall.TrackAudio). The <cmd> is shown as a Go raw string literal.

Tempo change (atempo) — pitch-preserving speed-up/slow-down. Stack multiple atempo filters for ratios outside [0.5, 2.0]:

`ffmpeg -i "song.mp3" -af "atempo=1.25"`
`ffmpeg -i "song.mp3" -af "atempo=2.0,atempo=1.25"`   // = 2.5x

Loudness normalization (EBU R128) — broadcast-grade levelling. Two-pass is more accurate; one-pass is fine for live streams:

`ffmpeg -i "song.mp3" -af "loudnorm=I=-16:LRA=11:TP=-1.5"`

Volume / gain — linear or dB:

`ffmpeg -i "song.mp3" -af "volume=1.5"`        // +50 %
`ffmpeg -i "song.mp3" -af "volume=-6dB"`       // -6 dB

Bass / treble shelf — simple two-band EQ:

`ffmpeg -i "song.mp3" -af "bass=g=6,treble=g=2"`

Pitch shift (semitones) — resample + atempo trick; 1.06 ≈ +1 semitone, 0.944 ≈ -1:

`ffmpeg -i "song.mp3" -af "asetrate=48000*1.06,aresample=48000,atempo=1/1.06"`

Fade in / out:

`ffmpeg -i "song.mp3" -af "afade=t=in:d=2"`
`ffmpeg -i "song.mp3" -af "afade=t=out:st=180:d=5"`

Mix two sources (amix) — overlay background ambience under music:

`ffmpeg -i "music.mp3" -i "ambient.wav" -filter_complex "amix=inputs=2:duration=longest:weights=1 0.3"`

Seek to start position — initial play offset; note that Pause/Resume's -ss injection replaces this on resume (you control the first play position only):

`ffmpeg -ss 90 -i "song.mp3"`

Infinite loop — replay forever:

`ffmpeg -stream_loop -1 -i "jingle.mp3"`

Concat playlist (concat protocol) — gapless join of identically-encoded files:

`ffmpeg -i "concat:track01.mp3|track02.mp3|track03.mp3"`

For mixed-format playlists use the concat demuxer with a list file:

`ffmpeg -f concat -safe 0 -i "playlist.txt"`

HLS / live radio with reconnect + custom UAFromShell does NOT inject the HLS-specific flags that FromURL does; add them yourself if your source needs them:

`ffmpeg -user_agent "Mozilla/5.0" -reconnect 1 -reconnect_at_eof 1 ` +
`-reconnect_streamed 1 -reconnect_delay_max 5 -rw_timeout 10000000 ` +
`-protocol_whitelist "file,http,https,tcp,tls" ` +
`-i "https://stream.example.com/radio.m3u8"`

HTTP with custom headers / cookies — inject Referer / Cookie / Authorization on the input:

`ffmpeg -headers "Referer: https://example.com\r\nCookie: session=abc\r\n" ` +
`-i "https://example.com/protected.mp3"`

(\r\n here is literal four characters in the Go raw string — ffmpeg's -headers parses them as CRLF separators between header lines.)

RTSP / RTMP / SRT inputFromShell is the right escape hatch when you need transport flags:

`ffmpeg -rtsp_transport tcp -i "rtsp://camera.local/live"`
`ffmpeg -i "srt://ingest.example.com:9000?mode=caller"`
Video recipes

All examples below are FromShell(<cmd>, gotgcall.TrackVideo). Telegram requires VP8 — libvpx is the only video encoder that works end-to-end, so most recipes here are filter-side, not codec-side.

Scale + framerate + bitrate:

`ffmpeg -i "movie.mp4" -vf "scale=1280:720" -r 30 -b:v 1500k`

Letterbox a vertical / odd-aspect source to 720p:

`ffmpeg -i "vertical.mp4" -vf "scale=1280:-2:force_original_aspect_ratio=decrease,` +
`pad=1280:720:(ow-iw)/2:(oh-ih)/2:black"`

Watermark / logo overlay:

`ffmpeg -i "movie.mp4" -i "logo.png" -filter_complex "overlay=W-w-20:20"`

Burned-in timestamp (drawtext) — useful for security-camera feeds:

`ffmpeg -i "movie.mp4" -vf "drawtext=text='%{localtime}':fontcolor=white:fontsize=24:` +
`box=1:boxcolor=black@0.5:boxborderw=5:x=10:y=10"`

RTSP IP camera — TCP transport survives lossy Wi-Fi better than the UDP default:

`ffmpeg -rtsp_transport tcp -i "rtsp://user:pass@192.168.1.10/Streaming/Channels/101"`

Live screen capture:

// Linux (X11):
`ffmpeg -f x11grab -framerate 30 -video_size 1920x1080 -i ":0.0"`

// Windows:
`ffmpeg -f gdigrab -framerate 30 -i "desktop"`

// macOS (avfoundation index from -f avfoundation -list_devices true -i ""):
`ffmpeg -f avfoundation -framerate 30 -i "1:none"`

FromShells — dual ffmpeg legs

For ntgcalls-style "microphone + camera" patterns where you want full control over both legs:

gotgcall.FromShells(
    `ffmpeg -i "movie.mp4"`,                                // audio leg
    `ffmpeg -i "movie.mp4" -vf "scale=1280:720" -b:v 1500k`, // video leg
)

Each cmd goes through the same auto-flag injection as FromShell. Either string may be empty to skip that track.

For the convenience path use FromFile/FromURL with Tracks: TrackVideo and let the library construct both ffmpeg commands for you.

FromShells returns *MultiShellSource, which satisfies both Source and SeekableSourceclient.SeekBy(chatID, deltaMs) works for dual-leg sources, killing both ffmpegs and re-spawning with -ss <offset> injected into each leg.

Sequential vs parallel spawn. By default both legs spawn sequentially (audio then video). When both legs read the same URL, this avoids tripping CDN per-IP concurrency throttles. Opt into concurrent spawn when the legs read independent inputs (separate files, separate camera/mic devices):

gotgcall.FromShells(audioCmd, videoCmd).WithParallelSpawn()

Single-leg sources ignore the flag — there's nothing to parallelize.

Dual-leg recipes

Audio file over a static cover image — "music with art":

gotgcall.FromShells(
    `ffmpeg -i "song.mp3"`,
    `ffmpeg -loop 1 -framerate 1 -i "cover.jpg" -vf "scale=1280:720" -r 1 -b:v 200k`,
)

Different sources per leg — radio audio + live webcam:

gotgcall.FromShells(
    `ffmpeg -i "https://stream.example.com/radio.mp3"`,
    `ffmpeg -f v4l2 -framerate 30 -video_size 1280x720 -i "/dev/video0"`,
)

A/V sync under time-distortion — when speeding up audio with atempo, scale video PTS by the same factor or the legs drift apart:

gotgcall.FromShells(
    `ffmpeg -i "movie.mp4" -af "atempo=1.25"`,
    `ffmpeg -i "movie.mp4" -vf "setpts=PTS/1.25,scale=1280:720" -r 30 -b:v 1500k`,
)
Shell-source gotchas
  • No shell features. The argv is exec'd directly, so $VAR, ${VAR}, *.mp3, $(cmd), cmd1 | cmd2, cmd1 && cmd2, > redirects, and ~ expansion are all literal characters. Substitute env vars in Go before composing the string.
  • No /dev/stdin source. FromShell has no way to pipe bytes in from your Go process; ffmpeg -i pipe:0 would just block. Spawn external producers (yt-dlp, etc.) yourself and write the file to disk first, or have them stream to a URL you can then -i.
  • Quoting. Use double quotes for arguments with spaces; \" for a literal " inside; \\ for a literal \. Single quotes are not quote characters — they're literal apostrophes (filenames like Don't Stop.mp3 work as-is, no quoting needed unless there's a space).
  • HLS/HTTP convenience flags don't apply. FromFile/FromURL inject -user_agent, -reconnect *, -protocol_whitelist, -rw_timeout automatically; FromShell does not. Add them yourself when streaming m3u8 / unreliable HTTP.
  • Hardware encoders rarely help. Telegram only accepts VP8, and very few platforms have a VP8 hardware encoder (some Intel iGPUs have vp8_vaapi; most NVENC/QSV builds don't). Stick with libvpx.
  • -c:a copy / -c:v copy is brittle. Even if the source is already Opus or VP8, pacing depends on per-frame metadata the OGG/IVF muxers add — copy paths often miss the page/keyframe cadence the streamer expects. Re-encode is the safe default.
  • Auto-fill is per-flag, not all-or-nothing. Each flag is checked independently — -c:a libopus -b:a 192k keeps your bitrate and still fills in -application, -frame_duration, -page_duration, -mapping_family, -ar, -ac, -f. The only setting that gets rejected is a raw PCM/YUV output codec, with an error pointing at the right replacement.
  • Inspecting the realized argv. gotgcall doesn't currently log the post-injection argv. Turn on WithFFmpegStderrLog() and you'll see ffmpeg's own "Input #0 …" / "Stream mapping" output, which confirms what it parsed and which streams it picked.

EncodeOptions

type EncodeOptions struct {
    VideoBitrateKbps int   // default 800
    VideoWidth       int   // default 1280
    VideoHeight      int   // default 720
    VideoFPS         int   // default 30
    AudioBitrateKbps int   // default 128 (music-grade; bump to 192+ for transparent quality, Telegram fmtp accepts up to 510)
    AudioChannels    int   // default 2
    Tracks           Track // default TrackAudio; TrackVideo implies +TrackAudio
}

Set on the constructor (FromFile/FromURL); rides with the Source. FromShell / FromShells ignore EncodeOptions because you control ffmpeg directly.

Client options

gotgcall.New(
    gotgcall.WithFFmpegPath("/opt/ffmpeg/bin/ffmpeg"),  // override binary lookup
    gotgcall.WithLogger(slog.Default()),                // structured logger
    gotgcall.WithDebugLogs(),                           // shortcut: text handler @ Debug level to stderr
    gotgcall.WithFFmpegStderrLog(),                     // tee ffmpeg stderr → debug log
    gotgcall.WithSharedUDPMux(),                        // one UDP socket for all calls
    gotgcall.WithDTLSCertPool(16),                      // pre-generate N DTLS certs
    gotgcall.WithDispatchBuffer(512),                   // event-dispatcher queue size
    gotgcall.WithNetworkTypes(                          // enable IPv6/TCP for restrictive nets
        gotgcall.NetworkTypeUDP4,
        gotgcall.NetworkTypeUDP6,
        gotgcall.NetworkTypeTCP4,
    ),
)
Option Default Notes
WithFFmpegPath "ffmpeg" New() fails fast if the binary is missing.
WithLogger discard (no logs at all) Pass a *slog.Logger to receive gotgcall events plus ffmpeg stderr/exit. Without this, every log call — Info, Warn, Error — is silently dropped.
WithDebugLogs off Convenience shortcut for debug-level slog to stderr. Use when reporting bugs.
WithFFmpegStderrLog off Tees ffmpeg stderr line-by-line into the logger. Helpful for "stream runs but I hear nothing" diagnostics.
WithSharedUDPMux off Multiplex every call through one UDP socket. See UDP mux scaling.
WithDTLSCertPool 8 Pre-generate N DTLS certs so CreateCall doesn't stall during bursts. 0 = disabled.
WithDispatchBuffer 256 Callback queue size. Raise to absorb bursts of state changes.
WithNetworkTypes UDP4+UDP6 Override the candidate network-type whitelist. Add TCP for environments where UDP is blocked.
WithConnectTimeout 10 s How long SetSource / Resume wait for the call to be ready.
WithVerboseConnectionLogs off Debug slog + per-candidate logs. Use when reporting a stuck-in-Connecting bug.

Enabling debug logs

gotgcall.New() with no logger option produces no logs at all — not Info, not Warn, not Error. Logging is opt-in so the library never spams your stdout/stderr unexpectedly. Pass WithLogger, WithDebugLogs, or WithVerboseConnectionLogs to turn it on.

For maximum verbosity when reporting a bug:

client, err := gotgcall.New(
    gotgcall.WithVerboseConnectionLogs(), // ICE + DTLS + per-candidate trace
    gotgcall.WithFFmpegStderrLog(),       // ffmpeg stderr line-by-line
)

UDP mux & scaling

The README said "use WithSharedUDPMux at 100+ calls". That was a conservative guess — the real picture:

Default (one socket per call):

  • 1 UDP socket = 1 file descriptor + 1 ephemeral port per call.
  • Linux defaults: ulimit -n 1024 (raise to 65535), ephemeral port range 32768–60999 (~28000 usable).
  • Practical ceiling without any tuning: ~900 calls (bounded by FDs, leaving room for other FDs).
  • After ulimit -n 65535 and net.ipv4.ip_local_port_range="1024 65000": tens of thousands of calls on a beefy server.
  • Benefit: kernel-level UDP receive-queue per call, parallelism scales with CPU cores naturally.

WithSharedUDPMux (one socket total):

  • 1 UDP socket, 1 FD, 1 port for the entire process — FD/port limits stop mattering.
  • All traffic funnels through one socket → kernel UDP buffer might become contended at extreme rates.
  • Per-socket UDP throughput on modern Linux: easily 1–10 Gbps. At ~50 kbps per voice call, that's 20 000–200 000 concurrent voice calls through one socket before throughput becomes the bottleneck.
  • Best for huge call counts where FD/port pressure is the limiting factor, or where firewall rules need to pin a single port.

Rule of thumb:

  • < 1000 calls: per-call sockets is fine, simpler, and gives you natural per-call kernel-queue isolation.
  • 1000–10000 calls: either works; WithSharedUDPMux simplifies sysctl tuning.
  • 10000+ calls: WithSharedUDPMux is the easier path; tune the kernel UDP receive buffer (net.core.rmem_max, net.core.rmem_default).

Note: client.Stop(chatID) closes only that call's WebRTC stack (and the per-call socket if not using the shared mux). The shared mux survives every Stop and is only closed when you call client.Close() on the parent client. So you can spin calls up and down freely without leaking or thrashing the shared socket.

Lifecycle

WebRTC mode

The default. Use for normal group voice/video.

localParams, err := client.CreateCall(chatID)
// → send localParams to phone.JoinGroupCall; read remoteParams from response.
err = client.Connect(chatID, remoteParams)
err = client.SetStreamSources(chatID, gotgcall.FromFile("song.mp3", gotgcall.EncodeOptions{}))
// …
err = client.Stop(chatID)
  • CreateCall returns ErrConnectionExists only if a live call for that chat exists. Failed/Closed calls are reaped automatically — retries on a dead chat just work.
  • Connect before CreateCall returns ErrConnectionNotFound. Re-calling Connect updates the remote params.
  • After Stop you can re-use the same chatID cleanly.
  • client.AudioSSRC(chatID) returns the audio SSRC for phone.LeaveGroupCall's Source field. RTMP calls return ErrWrongMode.

RTMP mode

For "go live" / host-style broadcasts. Obtain the URL via phone.GetGroupCallStreamRtmpUrl:

err := client.StartRTMP(chatID, rtmpURL)
err  = client.SetStreamSources(chatID, gotgcall.FromFile("movie.mp4", gotgcall.EncodeOptions{}))
// Pause/Resume/Stop work identically. Mute/Unmute are best-effort (RTMP push has
// no per-track control); the lib tracks state but doesn't drop frames.

RTMP transcodes to H.264 + AAC. Pause/Resume in RTMP mode incurs a brief silence (~100–300 ms) on resume because Telegram's RTMP ingest closes silent streams; WebRTC mode pauses silently.

Pause / Resume / Mute

ok, err := client.Pause(chatID)   // false if already paused
ok, err  = client.Resume(chatID)
ok, err  = client.Mute(chatID)    // mute audio track; video keeps going
ok, err  = client.Unmute(chatID)
  • WebRTC Pause/Resume: silent — no audible gap on resume.
  • RTMP Pause/Resume: a brief ~100–300 ms gap on resume (Telegram's RTMP ingest closes silent streams).
  • Mute silences the audio track; video keeps going.
  • SetStreamSources can be called any time. While paused, the new source is recorded and starts at offset 0 on Resume.

Seek

err := client.SeekBy(chatID, +30_000) // forward 30s
err  = client.SeekBy(chatID, -10_000) // back 10s

SeekBy(chatID, deltaMs) is relative to the current position. Positive jumps forward, negative jumps backward. Internally it kills ffmpeg and respawns at the new offset via SeekableSource.OpenAt — same machinery Resume uses, just with a user-chosen target.

  • Out-of-range → EOF. Underflow below 0 fires OnStreamEnd instead of seeking. Forward overshoots past the source duration are detected naturally — ffmpeg yields zero frames after -ss and the streamer EOFs on its own. Both paths land your "play next track" logic on the same callback.
  • Errors. ErrNoSource when nothing is playing, ErrSeekUnsupported when the active source doesn't implement SeekableSource (today every built-in source does — FromFile / FromURL / FromShell all inject -ss).
  • No OnUpgrade fire. SeekBy is user-initiated; the caller already knows they moved.
  • Works while paused. Position updates immediately; Resume picks up at the new offset.
  • For absolute seeks: client.SeekBy(chat, targetMs - int64(client.Time(chat))) — the lib intentionally doesn't expose a SeekTo (one line at the caller side).

Callbacks

client.OnStreamEnd(func(chat int64, t StreamType, d Device, err error) {
    // Fires on natural EOF (err == nil) or ffmpeg crash (err != nil).
    // Manual Stop / SetSource don't fire — the caller already knows.
    // For video+audio sources fires twice: first Video, then Audio.
})

client.OnConnectionChange(func(chat int64, info NetworkInfo) {
    // info.State: Connecting | Connected | Disconnected | Failed | Closed | Timeout
})

client.OnUpgrade(func(chat int64, state MediaState) {
    // Mirror of ntgcalls' onUpgrade(MediaState). Fires on Mute /
    // Unmute / Pause / Resume and on spontaneous transitions (a video
    // leg ending mid-stream via EOF or ffmpeg crash, or the WebRTC
    // PC reaching Failed/Closed while video was active).
    //
    // SetStreamSources and Stop stay silent: the caller chose the new
    // source / brought the call down and can mirror MTProto in the
    // same code path. No-op toggles (e.g. Mute when already muted)
    // are also silent.
    //
    // MediaState fields (Paused maps to Telegram's video_paused —
    // i.e. "media not flowing"):
    //   Muted              — explicit mute toggle
    //   Paused             — Muted || internally-paused
    //   VideoStopped       — true for Play (audio-only), false for VPlay
    //   PresentationPaused — same as Paused (no presentation source
    //                        in this library)
})

All callbacks fire on a single dispatcher goroutine, so you can safely re-enter the API from inside (e.g. call client.Stop(chat) from inside OnStreamEnd). If your callback panics it is recovered and logged; the dispatcher keeps running.

If the dispatch queue fills up (slow consumer), the dispatcher drops the oldest queued event and logs a warning. Tune with WithDispatchBuffer.

Server-side media-state changes (admin mute, video off)

The library is blob-only and never sees MTProto updates. When Telegram tells you the bot was admin-muted (via your UpdateGroupCallParticipants handler), react directly:

tg.AddRawHandler(&telegram.UpdateGroupCallParticipants{}, func(u telegram.Update, _ *telegram.Client) error {
    upd := u.(*telegram.UpdateGroupCallParticipants)
    for _, p := range upd.Participants {
        // compare p.Peer to your own user id, then:
        if p.Muted {
            client.Pause(chatID)
        } else if p.CanSelfUnmute {
            client.Resume(chatID)
        }
    }
    return nil
})

The OnUpgrade(MediaState) callback fires for outgoing state changes — Mute / Unmute / Pause / Resume plus spontaneous video-leg EOF or ICE Failed/Closed. Server-side mute / video-stop from Telegram is delivered only via your MTProto UpdateGroupCallParticipants handler — gotgcall stays out of MTProto by design.

Errors

All errors are sentinels — branch with errors.Is:

Error Returned when
ErrConnectionExists CreateCall / StartRTMP for a chatID that already has a live call. Failed/Closed calls are auto-reaped, so retries on a dead chat just work.
ErrConnectionNotFound Any method called with an unknown chatID, or after Stop.
ErrConnectionTimeout Reserved for future use. ICE-failure currently surfaces via OnConnectionChange(Failed).
ErrConnectionFailed Reserved for branching; ICE-failure currently surfaces via OnConnectionChange(Failed).
ErrInvalidParams Malformed remote JSON in Connect, or FromShell with empty/invalid command.
ErrFFmpegSpawn ffmpeg couldn't start (binary missing / permission denied / OS resource exhaustion).
ErrFFmpegCrashed ffmpeg exited non-zero. Wrapped error carries exit=<code> and the last 512 bytes of stderr.
ErrFile Source contained no playable audio or video stream.
ErrClosed Any method called after Client.Close().
ErrNotConnected SetSource timed out waiting for the call to reach Connected (10 s default; override with WithConnectTimeout).
ErrInternal Wrapping for internal errors that shouldn't normally occur.
ErrWrongMode WebRTC-only method called on an RTMP call (or vice versa).

Concurrency model

  • One *Client per process multiplexes any number of group calls.
  • All public methods are safe for concurrent use.
  • Concurrent CreateCall / StartRTMP for the same chat are deduped — the first wins, others get ErrConnectionExists without doing any allocation.
  • After Stop, the same chatID can be re-used cleanly.
  • Callbacks fire on a single dispatcher goroutine, so you can safely re-enter the API from inside (client.Stop(chat) from OnStreamEnd is fine).

Goroutine budget

Deliberately frugal:

  • 3 shared per process — keepalive ticker, callback dispatcher, DTLS cert pool refill.
  • 3 per live call — audio streamer, video streamer, and one inbound drainer.
  • 1 per ffmpeg subprocess — waits for the process to exit and surfaces the error.
  • pion adds ~5–8 of its own per call (ICE/DTLS/SRTP internals) — upstream territory.

Scales linearly with live calls; nothing is allocated per-source-switch or per-frame.

Networking

  • Transport: UDP4 + UDP6 by default. Override with WithNetworkTypes(...) to restrict or add TCP.
  • STUN / TURN: not exposed — host candidates only, matching ntgcalls. Telegram's edge learns our post-NAT source peer-reflexively as ICE-CONTROLLED, so STUN adds nothing for this flow.
  • Interface filter: virtual / VPN interfaces (Docker bridges, WSL, VMware, Tailscale, ZeroTier, OpenVPN, etc.) are skipped automatically. Override is not exposed; report a bug if your interface name is being filtered incorrectly.
  • UDP mux: default = one socket per call. Pass WithSharedUDPMux() to multiplex all calls through one udp4:0 socket (recommended once you're above ~1 000 concurrent calls — see UDP mux & scaling).
  • Connect gate: SetSource waits up to 10 s for the call to reach Connected before returning ErrNotConnected. Override with WithConnectTimeout(...).
  • ICE timeouts: internal — 10 s disconnect grace, 30 s failed, 2 s keepalive. Not user-tunable; matches ntgcalls.

Performance tuning

  • Cert pool (WithDTLSCertPool): default 8; raise for very bursty workloads so CreateCall doesn't block on keygen.
  • Dispatch buffer (WithDispatchBuffer): default 256. Raise if you see drop warnings under bursty callback fan-out.
  • Shared UDP mux (WithSharedUDPMux): cuts FD use once you're above ~1 000 concurrent calls.
  • Fast cold-start: FromFile / FromURL already inject -analyzeduration 0 -probesize 64k to cut ~1–2 s from ffmpeg startup. Add the same flags in your FromShell commands if cold-start matters.

Memory usage

Measured per-process on Linux/amd64, Go 1.26, GOGC=100. RSS includes ffmpeg subprocesses. Round figures — your workload will move them ±30 %.

State Go heap ffmpeg RSS (per call) Total per call
Idle (no calls) ~6–8 MB
One audio-only call +~1–2 MB ~6–10 MB ~7–12 MB
One audio+video call (720p30) +~2–3 MB ~25–40 MB (1 ffmpeg/leg) ~50–80 MB
One RTMP push +~1 MB ~20–35 MB ~20–35 MB

Audio-only is the cheap path. The 25–40 MB number for video is ffmpeg's encoder state, not gotgcall.

Concurrency / scaling ballparks

Concurrent calls Recommended tuning
1–100 Defaults. Don't touch anything.
100–1 000 WithSharedUDPMux(). Raise FD limit (ulimit -n 65535).
1 000–10 000 Above + WithDTLSCertPool(64), WithDispatchBuffer(4096). Pin GOMAXPROCS. Watch ffmpeg total RSS — this is the bottleneck.
10 000+ Above + shard across processes; ffmpeg memory dominates at this scale.

A/V sync

  • Audio and video legs share a wall-clock baseline within microseconds and pace by per-frame duration; drift does not accumulate.
  • Don't apply different time-distortion filters to the two legs — e.g. atempo=1.25 on audio without setpts=PTS/1.25 on video — they will desync linearly.
  • In RTMP mode, sync is ffmpeg's responsibility (single muxed push).

Pitfalls

  • Requesting video on an audio-only source. Don't pass Tracks: TrackVideo unless the container actually has video; you'll get ErrFile.
  • Raw PCM/YUV codecs. FromShell rejects raw output up front with ErrInvalidParams.
  • SetSource blocks until the call is ready (10 s default). On failure: ErrNotConnected.
  • Pause in RTMP mode causes a brief silence on resume — see RTMP mode.

Performance vs ntgcalls

Both use the same codecs at the same bitrates against the same SFU, so wire bandwidth is identical. The differences are operational.

Apples-to-apples note. Both stacks run ffmpeg as a subprocess — the difference is where the encoder lives. ntgcalls pipes raw pcm_s16le / YUV into libwebrtc and encodes Opus / VP8 in-process; gotgcall has ffmpeg emit pre-encoded Opus (OGG) / VP8 (IVF) and the library just packetises + SRTPs. Total encoding work is the same — gotgcall just moves it out of your bot process where you can pin it with -threads 1.

CPU per call (audio-only, steady state)

Component ntgcalls gotgcall
Library itself ~1.5–2.5 % (Opus encode + RTP + SRTP + jitter) under 1 % (RTP packetise + SRTP only)
ffmpeg subprocess ~0.5–1 % (decode + resample to PCM, no encoder) ~1–2 % (decode + resample + Opus encode)
Total ~2–3.5 % ~1.5–3 %

CPU per call (audio + 720p30 video)

Component ntgcalls gotgcall
Library itself ~6–12 % (VP8 + Opus encode + pacer + SRTP) under 1 % (RTP packetise + SRTP only)
ffmpeg subprocess ~3–5 % (decode + YUV output, no encoder) ~5–10 % (decode + VP8 + Opus encode)
Total ~9–17 % ~6–11 %

Memory per call

Component ntgcalls gotgcall
Library itself ~15–25 MB (libwebrtc state) ~1–3 MB Go heap
ffmpeg subprocess ~5–8 MB (audio) · ~20–30 MB (+video) ~6–10 MB (audio) · ~25–40 MB (audio+video)
Total ~20–33 MB · ~35–55 MB (+video) ~7–13 MB · ~26–43 MB (+video)

Everything else

Dimension ntgcalls (libwebrtc, C++) gotgcall (pure Go)
Cold-start to first packet ~50–150 ms ~80–300 ms
Cross-compile / deploy libwebrtc + glibc + C++ toolchain + cgo CGO_ENABLED=0 go build → single static binary → scp → run
Binary size ~20–30 MB ~12–18 MB
Pause/resume Sub-ms WebRTC: sub-ms · RTMP: ~100–300 ms gap
Concurrent calls per process ~hundreds without tuning Tens of thousands with WithSharedUDPMux + raised FDs
Hot-reload of encoder logic Recompile + redeploy Swap an ffmpeg flag string at runtime

The library itself is leaner in gotgcall — well under a percent of CPU and a few MB of heap per call. The full-pipeline number is higher because ffmpeg is counted; that subprocess cost is bounded (-threads 1), inspectable (ps, top), and isolated (an ffmpeg crash doesn't take the bot down).

Trade-offs:

  • ntgcalls is leaner per call (no subprocess overhead).
  • gotgcall is dramatically easier to deploy and customise (static binary, ffmpeg-flag flexibility).
  • For typical music bots (10–500 concurrent calls), the per-call difference is invisible.
  • For 10 000+ concurrent calls, ntgcalls' lower memory footprint matters; WithSharedUDPMux closes part of that gap.

Numbers are order-of-magnitude estimates — benchmark your workload.

Why pure Go

gotgcall is — at the time of writing — the first pure-Go library that joins Telegram group calls end-to-end with audio and video. Every other option in the Go ecosystem until now required wrapping libwebrtc through ntgcalls + cgo + a C++ toolchain.

ntgcalls works fine but pulls in libwebrtc + glibc + a C++ build chain and has a lot of surprises like panic: segment fault issues with CGo. Cross-compiling music bots becomes a maintenance burden. gotgcall builds with CGO_ENABLED=0 to a single static binary on every supported platform.

FAQ

Is this a port of ntgcalls / pytgcalls to Go?

No — it's an independent implementation with a deliberately ntgcalls-shaped API so existing bot code translates almost line-for-line. ntgcalls wraps libwebrtc (C++); gotgcall uses pion, the pure-Go WebRTC stack.

Does it work with gogram, MTProto-Go, or other MTProto libraries?

Yes — any of them. The library is blob-only: it produces and consumes JSON strings; you handle the MTProto layer (phone.JoinGroupCall / phone.LeaveGroupCall) in your bot using whichever MTProto Go library you prefer. The examples/bot/ directory has a runnable skeleton against gogram.

Can I use this for a Telegram music bot?

That's the primary use case. See examples/bot/ and the FromShell audio recipes for atempo, loudness normalisation, equalizer, fade, mix, and live-radio HLS pipelines. FromShell cannot pipe bytes in from another Go process (no stdin source) — fetch with yt-dlp / similar tools to a file or URL first, then point FromFile / FromURL / FromShell at it.

Does it support video chats / livestreams / RTMP push?

Yes — three modes:

  1. WebRTC group video. Send-only audio + video into a normal voice/video chat.
  2. RTMP push. "Go live" broadcasts to a channel via Telegram's RTMP ingest URL — see RTMP mode.
  3. Custom ffmpeg. FromShell / FromShells lets you point at any decodable container or live source — HLS, RTSP, MJPEG, screen capture, IP camera, etc.
Does it support TGCalls / MTProto E2E voice calls?

No — only group calls and channel RTMP livestreams. 1-on-1 MTProto voice/video calls (TGCalls) require a different signalling path that this library does not currently target.

What Go version is required?

Go 1.26 or newer (uses errors.AsType[T] and a few stdlib refinements added in 1.26).

Does it run on Windows?

Yes. Pure-Go means no Make/gcc/clang. Pause/Resume in WebRTC mode uses a channel gate (works on every OS); RTMP mode uses kill+restart-with--ss (also OS-agnostic — SIGSTOP would be killed by Telegram's RTMP ingest timeout anyway).

How many concurrent calls can one process handle?

The library has no hardcoded limit. The practical ceiling is ffmpeg subprocess count + ICE socket count. Use WithSharedUDPMux() to collapse all calls onto one UDP socket once you're above ~100 concurrent calls. See UDP mux & scaling.

Where do I report bugs?

Open an issue with logs from WithVerboseConnectionLogs() + WithFFmpegStderrLog() — that combination covers streamer state, ffmpeg exit, ICE transitions, DTLS, and per-candidate trace.

See also

License

MIT — see LICENSE.

Documentation

Overview

Package gotgcall is a pure-Go library for streaming audio and video into Telegram group calls. The public API mirrors ntgcalls method names so bot code translates one-to-one.

The library is blob-only: signaling JSON is exchanged through your own MTProto client (typically gogram). Two calls are required:

params, _ := client.CreateCall(chatID)
resp, _   := tg.PhoneJoinGroupCall(... Params: &DataJson{Data: params})
client.Connect(chatID, resp.Updates[...].Call.Params.Data)
client.SetStreamSources(chatID, gotgcall.FromFile("song.mp3", gotgcall.EncodeOptions{}))

See README.md for the full pattern.

Index

Constants

View Source
const (
	NetworkTypeUDP4 = wrtc.NetworkTypeUDP4
	NetworkTypeUDP6 = wrtc.NetworkTypeUDP6
	NetworkTypeTCP4 = wrtc.NetworkTypeTCP4
	NetworkTypeTCP6 = wrtc.NetworkTypeTCP6
)
View Source
const (
	TrackAudio = media.TrackAudio
	TrackVideo = media.TrackVideo

	Audio      = models.Audio
	Video      = models.Video
	Microphone = models.Microphone
	Camera     = models.Camera

	Connecting   = models.Connecting
	Connected    = models.Connected
	Disconnected = models.Disconnected
	Failed       = models.Failed
	Closed       = models.Closed
)

Variables

View Source
var (
	FromFile   = media.FromFile
	FromURL    = media.FromURL
	FromShell  = media.FromShell
	FromShells = media.FromShells
)
View Source
var (
	ErrConnectionExists    = models.ErrConnectionExists
	ErrConnectionNotFound  = models.ErrConnectionNotFound
	ErrConnectionFailed    = models.ErrConnectionFailed
	ErrInvalidParams       = models.ErrInvalidParams
	ErrUnsupportedCallMode = models.ErrUnsupportedCallMode
	ErrFFmpegSpawn         = models.ErrFFmpegSpawn
	ErrFFmpegCrashed       = models.ErrFFmpegCrashed
	ErrFile                = models.ErrFile
	ErrClosed              = models.ErrClosed
	ErrInternal            = models.ErrInternal
	ErrNotConnected        = models.ErrNotConnected
	ErrWrongMode           = models.ErrWrongMode
)

Functions

This section is empty.

Types

type CallInfo

type CallInfo = models.CallInfo

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client multiplexes many concurrent group calls behind a single process-wide handle. Safe for concurrent use.

func New

func New(opts ...Option) (*Client, error)

New constructs a Client with the given options. Fails fast if the ffmpeg binary isn't on PATH (or wherever WithFFmpegPath points) so callers see the error at startup rather than on first stream.

func (*Client) AudioSSRC

func (c *Client) AudioSSRC(chatID int64) (uint32, error)

AudioSSRC returns the audio SSRC of a WebRTC call. Pass as Source to phone.LeaveGroupCall. Returns ErrWrongMode for RTMP calls.

func (*Client) Calls

func (c *Client) Calls() map[int64]CallInfo

Calls returns a snapshot of all active calls.

func (*Client) Close

func (c *Client) Close() error

Close stops every call and releases resources. Idempotent.

func (*Client) Connect

func (c *Client) Connect(chatID int64, telegramParams string) error

Connect finishes the WebRTC handshake using Telegram's response JSON. On error the call is auto-reaped so the caller can immediately retry CreateCall without coordinating a separate Stop.

func (*Client) CreateCall

func (c *Client) CreateCall(chatID int64) (string, error)

CreateCall starts a new WebRTC group-call instance for chatID and returns the JSON params the caller must pass to phone.JoinGroupCall.

Concurrent CreateCall / StartRTMP calls for the same chat are serialized; the first one wins, others get ErrConnectionExists without allocating a pion PeerConnection.

func (*Client) GetState

func (c *Client) GetState(chatID int64) (MediaState, error)

GetState returns the current media-state (mute/pause flags).

func (*Client) Mute

func (c *Client) Mute(chatID int64) (bool, error)

func (*Client) OnConnectionChange

func (c *Client) OnConnectionChange(fn func(chatID int64, info NetworkInfo))

OnConnectionChange registers a callback for ICE/DTLS state transitions.

func (*Client) OnStreamEnd

func (c *Client) OnStreamEnd(fn func(chatID int64, t StreamType, d Device, err error))

OnStreamEnd registers a callback fired when a track ends from EOF or ffmpeg crash. Manual Stop / SetSource don't fire — the caller already knows they initiated those.

For video+audio sources (vplay) the callback fires twice in fixed order: first StreamType=Video, then StreamType=Audio. Audio-only and video-only sources fire once. Called on the dispatcher goroutine so it is safe to re-enter the Client API from within.

func (*Client) OnUpgrade

func (c *Client) OnUpgrade(fn func(chatID int64, state MediaState))

OnUpgrade registers a callback fired whenever the outgoing MediaState flips a bit. Mirror of ntgcalls' onUpgrade(MediaState).

Fires on:

  • Mute / Unmute — flips Muted (and Paused / PresentationPaused follow, since the mic is no longer producing audio).
  • Pause / Resume — flips Paused and PresentationPaused while Muted stays put.
  • A video leg ending mid-stream (EOF / ffmpeg crash) — VideoStopped flips false→true. Audio-only sources had VideoStopped=true already, so they don't fire here.
  • The WebRTC PC transitioning to Failed/Closed while video was active — same VideoStopped flip as the EOF case.

Does NOT fire on:

  • SetStreamSources — the caller chose the new source and already knows the resulting VideoStopped (true for Play / audio-only, false for VPlay / audio+video). A same-shape re-source (e.g. Play → Play) would have prev == cur anyway.
  • Stop — the call is gone, there is nothing left to mirror.
  • No-op toggles (Mute when already muted, etc.) — the helper skips dispatch when prev == cur.

Fires on the dispatcher goroutine, so it is safe to re-enter the Client API from inside the callback.

func (*Client) Pause

func (c *Client) Pause(chatID int64) (bool, error)

func (*Client) Resume

func (c *Client) Resume(chatID int64) (bool, error)

func (*Client) SeekBy added in v0.8.2

func (c *Client) SeekBy(chatID int64, deltaMs int64) error

SeekBy shifts playback by deltaMs (signed; positive forward, negative backward) relative to the current position. Underflow below 0 fires OnStreamEnd instead of seeking — caller's auto-skip-to-next logic can drive off the same callback as natural end-of-stream. Forward overshoots past source duration are detected by ffmpeg yielding zero frames after the seek (also natural OnStreamEnd path).

Returns ErrSeekUnsupported if the active source is not seekable (live FromShell commands that ignore -ss fall here), ErrNoSource if no source is currently playing, and ErrConnectionNotFound if there's no call for chatID.

func (*Client) SetStreamSources

func (c *Client) SetStreamSources(chatID int64, src Source) error

SetStreamSources installs or replaces the streaming source for chatID. Encode options (FPS, tracks, bitrates) ride along with the Source — set them on the constructor (FromFile/FromURL).

On error the call is auto-reaped (closed and removed from the per-client registry) so the caller can immediately retry CreateCall without first invoking Stop. This covers the failure modes the user would otherwise need to clean up by hand: ICE/DTLS gate timeout, "connection closed during setup", and ffmpeg / source-open errors.

func (*Client) StartRTMP

func (c *Client) StartRTMP(chatID int64, rtmpURL string) error

StartRTMP creates an RTMP-push call for chatID. The caller obtains rtmpURL via phone.GetGroupCallStreamRtmpUrl gogram-side. Serialised with CreateCall via the same per-chat creation mutex.

func (*Client) Stop

func (c *Client) Stop(chatID int64) error

Stop tears down the call and clears the per-chat call entry. The per-chat create-mutex is intentionally kept (see reap) so a concurrent CreateCall parked on mu.Lock() doesn't end up racing a later one on a fresh mutex. The mutex memory is negligible (sizeof sync.Mutex per chatID ever used).

func (*Client) Time

func (c *Client) Time(chatID int64) (uint64, error)

Time returns elapsed ms of media pushed.

func (*Client) Unmute

func (c *Client) Unmute(chatID int64) (bool, error)

type ConnState

type ConnState = models.ConnState

type Device

type Device = models.Device

type EncodeOptions

type EncodeOptions = media.EncodeOptions

type MediaState

type MediaState = models.MediaState

type MultiShellSource added in v0.8.3

type MultiShellSource = media.MultiShellSource

type NetworkInfo

type NetworkInfo = models.NetworkInfo

type NetworkType added in v0.6.0

type NetworkType = wrtc.NetworkType

NetworkType is re-exported for WithNetworkTypes.

type Option

type Option func(*config)

func WithConnectTimeout added in v0.6.17

func WithConnectTimeout(d time.Duration) Option

WithConnectTimeout overrides how long SetSource/Resume wait for the WebRTC connection to reach Connected before giving up. Default 10s — matches ntgcalls' own internal connection timeout. With pion running as ICE-CONTROLLED (since v0.6.26) and Telegram's edges responding within 50-300ms in healthy networks, 10s is generous. Set higher on unstable networks where ICE re-pairing on cross-DC moves takes longer.

func WithDTLSCertPool

func WithDTLSCertPool(n int) Option

WithDTLSCertPool sets the size of the pre-generated DTLS certificate pool. Larger pools absorb bigger call-creation bursts without keygen latency. 0 disables pre-generation.

func WithDebugLogs added in v0.6.0

func WithDebugLogs() Option

WithDebugLogs is a convenience that installs a Debug-level text handler writing to os.Stderr. Equivalent to:

WithLogger(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})))

Use this when reporting bugs — debug-level output covers ICE/DTLS state, ffmpeg exit codes, streamer pacing, and pion-internal events bridged through the new pion→slog adapter.

func WithDispatchBuffer

func WithDispatchBuffer(n int) Option

WithDispatchBuffer sizes the event dispatcher's channel. Default 256.

func WithFFmpegPath

func WithFFmpegPath(p string) Option

WithFFmpegPath overrides the ffmpeg binary path (default "ffmpeg").

func WithFFmpegStderrLog added in v0.6.0

func WithFFmpegStderrLog() Option

WithFFmpegStderrLog tees ffmpeg's stderr output to the library logger at Debug level while the process is running. Without this, ffmpeg stderr is only surfaced in the final error message (last 512 bytes) when the subprocess crashes — useful for crash diagnosis but useless for "ffmpeg is running, but I see no audio" symptoms. Enable for verbose diagnosis.

func WithICECandidateLogs added in v0.6.3

func WithICECandidateLogs() Option

WithICECandidateLogs logs every locally-gathered ICE candidate (host / srflx / relay, address, port, foundation) at Debug level via the PeerConnection's OnICECandidate hook. Pairs well with WithPionTraceLogs for "why is ICE failing" diagnosis: this option shows what we offered, pion-trace shows which pairs were tried, and the remote answer's candidate list (parsed in jsonparams) shows what Telegram returned.

func WithLogger

func WithLogger(l *slog.Logger) Option

WithLogger sets a structured logger for internal events.

func WithNetworkTypes added in v0.6.0

func WithNetworkTypes(types ...NetworkType) Option

WithNetworkTypes overrides the ICE candidate network-type whitelist. Default is UDP4+UDP6 (matching ntgcalls' PORTALLOCATOR_ENABLE_IPV6). Telegram's SFU accepts IPv6 candidates and dual-stack hosts get more candidate pairs. Add TCP for restrictive environments where UDP is blocked.

gotgcall.WithNetworkTypes(
    gotgcall.NetworkTypeUDP4,
    gotgcall.NetworkTypeUDP6,
    gotgcall.NetworkTypeTCP4,
)

func WithPionTraceLogs added in v0.6.3

func WithPionTraceLogs() Option

WithPionTraceLogs remaps pion's Trace-level output (per-ICE-check, per- candidate-pair, per-binding-request) to slog.LevelDebug instead of the default sub-debug level. Use this when ICE is stuck in "Checking" and you need to see exactly which candidate pairs are being tried, which fail, and which (if any) get a response from the remote.

gotgcall.New(gotgcall.WithDebugLogs(), gotgcall.WithPionTraceLogs())

Volume warning: ICE Trace at scale is several hundred lines per call. Use for diagnosis, not steady-state production.

func WithSharedUDPMux

func WithSharedUDPMux() Option

WithSharedUDPMux makes all calls share one UDP socket for ICE traffic. Useful for high-concurrency setups (100+ simultaneous calls).

func WithVerboseConnectionLogs added in v0.6.25

func WithVerboseConnectionLogs() Option

WithVerboseConnectionLogs is a one-flag bundle for diagnosing "ICE/DTLS did not reach Connected within Ns" failures. It enables:

  • Debug-level slog handler to stderr (WithDebugLogs)
  • Per-candidate gather logging (WithICECandidateLogs)
  • Pion's per-binding-request / per-pair-check trace at Debug (WithPionTraceLogs)

Use when reporting a stuck-in-Connecting bug — the resulting log contains every signal the library can surface about the ICE state machine. Library still also emits an Info-level checking-phase snapshot every ~5 seconds without this flag, so most issues can be triaged from a non-Debug run; flip this when those Info lines aren't enough.

type SeekableSource

type SeekableSource = media.SeekableSource

type Source

type Source = media.Source

type StreamType

type StreamType = models.StreamType

type Track

type Track = media.Track

Directories

Path Synopsis
Package instances holds the per-chat call state.
Package instances holds the per-chat call state.
jsonparams
Package jsonparams encodes and decodes the SDP-like JSON envelope that Telegram's group-call signaling uses in place of standard SDP O/A.
Package jsonparams encodes and decodes the SDP-like JSON envelope that Telegram's group-call signaling uses in place of standard SDP O/A.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL