Skip to main content

Programmatic client (with audio)

Read availability and transport limits first. This complete main.go targets the current source API through a trusted local endpoint. Its default WebRTC leg is silent; real microphone/speaker audio requires a custom factory.

With the matching SDK and protocol modules available in your Go module, run go run . and use dial <number>, answer <id>, hangup <id>, or quit. Set BC_ADDR to a trusted host:port and supply a short-lived agent token as BC_TOKEN through your local environment.

package main

import (
"bufio"
"context"
"fmt"
"log"
"os"
"strings"

bcv1 "github.com/babelforce/babelconnect-proto/gen/go/babelconnect/v1"
bcclient "github.com/babelforce/babelconnect-sdk-go"
)

func main() {
addr := os.Getenv("BC_ADDR")
if addr == "" {
addr = "localhost:7091"
}
cli, err := bcclient.Dial(context.Background(), bcclient.Options{
Addr: addr, Token: os.Getenv("BC_TOKEN"), AutoAnswer: true,
OnError: func(e *bcv1.Error) { log.Printf("%s (%s): %s", e.Code, e.CallId, e.Message) },
OnGap: func() { log.Print("State is stale; quit and reconnect for a fresh snapshot") },
})
if err != nil {
log.Print(err)
return
}
defer cli.Close()
cli.Subscribe(func(v *bcv1.AgentView) {
for _, c := range v.GetActiveCalls() {
fmt.Printf("%s: %s %s → %s\n", c.Id, c.State, c.From, c.To)
}
})
if err := cli.Register("webrtc"); err != nil {
log.Print(err)
return
}
scanner := bufio.NewScanner(os.Stdin)
fmt.Println("Wait for registration, then: dial <number>, answer <id>, hangup <id>, quit")
for scanner.Scan() {
parts := strings.Fields(scanner.Text())
if len(parts) == 1 && parts[0] == "quit" {
return
}
if len(parts) != 2 {
fmt.Println("Expected command and number/call id")
continue
}
switch parts[0] {
case "dial":
v := cli.View()
if !v.GetConfig().GetCalls().GetEnabled() || v.GetAgent().GetDisplayAs() == "" {
fmt.Println("Wait for call config and select an allowed caller ID first")
continue
}
err = cli.PlaceCall(parts[1], "", "", false)
case "answer":
var ringing bool
for _, c := range cli.View().GetActiveCalls() {
if c.Id == parts[1] && c.State == bcv1.CallLifecycle_CALL_LIFECYCLE_RINGING && c.WebrtcOffer != "" {
ringing = true
}
}
if !ringing {
fmt.Println("No ringing WebRTC offer for that id")
continue
}
err = cli.Answer(parts[1])
case "hangup":
err = cli.Hangup(parts[1])
default:
fmt.Println("Unknown command")
continue
}
if err != nil {
log.Print(err)
}
}
if err := scanner.Err(); err != nil {
log.Print(err)
}
}

What's happening

Register loads reference data and requests WebRTC reachability. The current server ignores its capability strings, so Register() also requests reachability. AutoAnswer defaults to false; the example enables it for your own outbound leg. It never auto-answers inbound calls or callbacks. PlaceCall takes positional to, displayAsTo, displayAsFrom, record; empty display overrides use the server's defaults. The command can still be rejected asynchronously.

Handling inbound calls

The subscriber prints state only. Type answer <id> to accept a ringing offer or hangup <id> to reject it. Go enums include the full prefix, such as bcv1.CallLifecycle_CALL_LIFECYCLE_RINGING. View() returns a clone (possibly empty); ActiveCall() returns the first call or nil. Prefer explicit IDs when more than one call exists.

Concurrency

Sends are serialized. Subscription callbacks run synchronously for their immediate cache delivery and on the receive loop for later updates; keep them short. A newly attached callback can overlap an update, so synchronize shared UI state. Views are cloned from the cache but should be treated as read-only. Subscribe has no unsubscribe function: attach once, then close the client.

Bring your own media

Use Options.Media for real audio. The default lets you verify RTP flow, not what a person hears.

Next steps

The example reports gaps but leaves recovery to the operator. Add the policy in Errors & reconnects before relying on a long-lived session. Authentication covers PKCE and logout; Intents lists the remaining commands.