Documentation
¶
Overview ¶
Package call pkg/skychat/call/audio_ring.go c4-app-chat
sampleRing is the bounded int16 FIFO shared by all audio backends: the pure-Go PulseAudio backend on Linux, the cgo malgo backend on Windows/macOS, the browser WebAudio backend on js/wasm, and the host-app Bridge (bridge.go). It bridges a push-model capture callback and a pull-model playback callback to the voice package's pull-model Source / push-model Sink.
Built on every platform. It carries no build constraint of its own because the Bridge has none either — "the audio device belongs to another process" is not a property of the OS this compiles for.
Package call pkg/skychat/call/bridge.go c4-app-chat
Bridge is a voice audio device that lives OUTSIDE the visor process.
Every other backend here opens a device the visor can reach for itself: PulseAudio on Linux, miniaudio on Windows/macOS, WebAudio in the browser. **Android has none of them.** GOOS=android satisfies the `linux` build tag, so the visor there compiles the PulseAudio backend, finds no server to talk to, and degrades to SilentSource + NullSink — a call that connects and is mute in both directions. The device that does exist belongs to the host app (AudioRecord and AudioTrack, in the Android process that started the visor), so the visor borrows it: the app pushes captured PCM in and pulls playback PCM out over the visor's local API, and this is the seam in the middle.
Nothing here is Android-specific — it is "the audio device is someone else's problem", which is also what a headless host with a remote operator wants.
**Why a fan-out and not one buffer each way.** Source and Sink are built PER CALL SESSION, and there is exactly one device. Capture is therefore COPIED to every session — each gets its own ring, so one session's read cannot swallow a frame another session was going to get — and playback is SUMMED across sessions with clipping, which is what a real mixer does when two calls share one speaker. With the single call a phone actually places, both collapse to the obvious thing.
Package call pkg/skychat/call/codec_opus.go c4-app-chat
Opus codec — ~24 kbit/s versus the PCM passthrough's ~1.5 Mbit/s for the same 48 kHz mono voice. This uses github.com/thesyncim/gopus, a hand-written PURE-GO Opus implementation (no cgo, no libopus, no WASM/wazero, no embedded blob) with optional amd64/arm64 SIMD and a pure-Go fallback — so it cross-compiles to every platform and stays dependency-light, matching skywire's vendored, bloat-averse setup. Measured ~300 µs to encode+decode one 20 ms frame (~1.5% of the real-time budget). Each session gets its own codec (opus keeps encoder/decoder state — see Manager.NewCodec). Alternative if reference-exact libopus is ever needed: github.com/godeps/opus (real libopus via wazero, but adds a WASM blob).
Package call pkg/skychat/call/manager.go c4-app-chat
Package call pkg/skychat/call/progress.go c4-app-chat
Package call pkg/skychat/call/pulse_linux.go c4-app-chat
PulseAudio/PipeWire capture + playback for skychat voice, via github.com/jfreymuth/pulse — a PURE-GO PulseAudio client (no cgo, no system libs to link; it just needs a running PulseAudio/PipeWire at runtime). This is the Linux audio backend; a cgo malgo backend for Windows/macOS is a follow-up.
NewMicSource(monitor=false) captures the default microphone. NewMicSource( monitor=true) captures the default sink's MONITOR — i.e. whatever audio the system is playing — so a call (or the spectrogram demo) can carry internal audio with no mic plugged in, the way `pactl`/PulseAudio monitor sources work.
Package call pkg/skychat/call/resume.go c4-app-chat
Package call pkg/skychat/call/ringback.go c4-app-chat
Package call pkg/skychat/call/ringwatch.go c4-app-chat
Package call pkg/skychat/call/session.go c4-app-chat
Package call pkg/skychat/call/signal.go c4-app-chat
Package call pkg/skychat/call/tap.go c4-app-chat
Per-call audio taps for visualization. When a Manager has Visualize set, each session's Source (sent audio) and Sink (received audio) are wrapped so a copy of every frame lands in a small bounded ring. CallAudio(callID) returns the most recent ~1s of each direction, which the CLI polls to draw a two-panel sent/received spectrogram during a call. The audio itself is untouched — the tee just observes.
Package call pkg/skychat/call/voice.go c4-app-chat
Real-time 1:1 voice for skychat, per docs/skychat-voice-rfc.md. All media rides an ENCRYPTED skywire transport (dmsg stream or skynet route) — never a raw-internet/ICE path. This package is the skywire-specific control + media plane:
- signaling (signal.go): invite / accept / decline / hangup + media-session negotiation, listened on ONE port over BOTH dmsg and skynet (the callee accepts on either; the caller reaches whichever is up). See Signaler.
- media session (session.go): RTP framing over a skywire net.Conn, with a small reorder/jitter buffer; a send loop (capture→encode→RTP→conn) and a recv loop (conn→RTP→decode→playback).
- codec + audio (this file): small consumer-side interfaces (KG7). The codec and mic/speaker are pluggable seams; the real Opus codec + audio-device I/O are a follow-up that vendors libopus + an audio lib behind cgo build tags (the RFC's §4/§5). Until then a PCM passthrough codec + synthetic/loopback audio make the whole control+transport plane real and testable.
The manager (manager.go) owns active calls and drives the signaler.
Index ¶
- Constants
- Variables
- func NormalizeToneMime(mime string) string
- type Bridge
- type Codec
- type Config
- type Dial
- type DialFunc
- type DialState
- type InviteHandler
- type Manager
- func (m *Manager) Active() []string
- func (m *Manager) AddListener(ctx context.Context, lis net.Listener)
- func (m *Manager) Answer(callID string) error
- func (m *Manager) Call(ctx context.Context, peer cipher.PubKey) (*Session, error)
- func (m *Manager) CallAudio(callID string) (sent, recv []int16, err error)
- func (m *Manager) Decline(callID string) error
- func (m *Manager) Dial(peer cipher.PubKey, budget time.Duration) string
- func (m *Manager) DialRingback(callID string) (data []byte, mime string, ok bool)
- func (m *Manager) Dialing() []Dial
- func (m *Manager) Hangup(callID string) error
- func (m *Manager) Incoming() []Sig
- func (m *Manager) Ringback() (data []byte, mime string)
- func (m *Manager) Serve(ctx context.Context, listeners ...net.Listener)
- func (m *Manager) SetBlocked(pks []cipher.PubKey)
- func (m *Manager) SetMute(callID string, mic, speaker bool) error
- func (m *Manager) SetRingback(data []byte, mime string) error
- type NullSink
- type ResumeFunc
- type ResumeHandler
- type Session
- func (s *Session) AdoptConn(next net.Conn) bool
- func (s *Session) Close()
- func (s *Session) Closed() bool
- func (s *Session) EndReason() string
- func (s *Session) MicMuted() bool
- func (s *Session) Peer() cipher.PubKey
- func (s *Session) Run(ctx context.Context)
- func (s *Session) SetAwaitResume()
- func (s *Session) SetMicMuted(m bool)
- func (s *Session) SetPeer(pk cipher.PubKey)
- func (s *Session) SetResume(r ResumeFunc)
- func (s *Session) SetSpeakerMuted(m bool)
- func (s *Session) SpeakerMuted() bool
- type Sig
- type SigType
- type Signaler
- func (s *Signaler) AddListener(ctx context.Context, lis net.Listener)
- func (s *Signaler) FetchTone(ctx context.Context, peer cipher.PubKey, callID, hash string, size int) ([]byte, error)
- func (s *Signaler) Invite(ctx context.Context, peer cipher.PubKey, callID, codec string, ...) (net.Conn, Sig, error)
- func (s *Signaler) Resume(ctx context.Context, peer cipher.PubKey, callID string) (net.Conn, error)
- func (s *Signaler) Serve(ctx context.Context, listeners ...net.Listener)
- func (s *Signaler) SetInviteHandler(h InviteHandler)
- func (s *Signaler) SetResumeHandler(h ResumeHandler)
- func (s *Signaler) SetToneHandler(h ToneHandler)
- type SilentSource
- type Sink
- type Source
- type ToneHandler
Constants ¶
const ( SampleRate = sampleRate FrameSamples = frameSamples )
The format every byte crossing the bridge is in: mono int16 at SampleRate, in frames of FrameSamples. Exported because the host app has to configure its capture and playback to match, and a mismatch is silent corruption (wrong pitch, not an error).
const DialOutcomeLinger = 8 * time.Second
DialOutcomeLinger is how long an outbound call that ended unanswered stays in Dialing with its outcome.
The outcome is the point of the feature, and it is exactly what vanished: the call left the list the moment it ended, so a UI polling every couple of seconds found an empty list and could only guess why. Long enough for two polls to see it; short enough that a stale "offline" is not what greets the next call. A UI that has shown it can drop it early with Hangup.
const MaxRingbackSize = 1 << 20
MaxRingbackSize bounds a ringback tone: about a minute of 128 kbit/s audio, enough for any tone, and small enough to fetch within the first seconds of a ring.
const ResumeBudget = 30 * time.Second
ResumeBudget is how long a call goes on trying to rebuild its media conn before it gives up and hangs up.
A call's media is one stream on a SHARED dmsg session, so it does not survive that session on its own: a server evicting the client, a ping reaper, a phone moving from Wi-Fi to cellular — each takes the session down and every stream with it, and the call ended there. It ends here instead, only if the transport has not come back within this.
Thirty seconds because both ends of the recovery are much faster: a dmsg client re-establishes a session within about fifteen (measured: 1.5s on a healthy link), and a Wi-Fi/cellular handover completes in five to fifteen. Longer would leave a dead call on screen; much shorter would give up during an ordinary handover, which is the case this exists for.
const RingTimeout = ringTimeout
RingTimeout is how long an unanswered inbound call rings, and therefore the longest a caller can be kept waiting for an answer. Exported because the caller's own budget has to be derived from it rather than guessed: the two were independent 30s and 45s constants, so a caller gave up fifteen seconds before the callee stopped ringing — the phone went on ringing for a call nobody was on the other end of any more.
Variables ¶
var ErrResumeRefused = errors.New("voice: resume refused")
ErrResumeRefused is what Signaler.Resume returns when the peer answered and said no. It is final: the peer has dropped the call, or predates resumption and cannot take one, and asking again gets the same answer for the rest of the budget — which is what a call that a pre-resumption peer had hung up on used to do, for thirty seconds. Only the transport-level failures — no route yet, the dial timing out — are worth retrying, because those are the local dmsg client still reconnecting.
var ErrToneRefused = errors.New("voice: ringback tone refused")
ErrToneRefused is what a tone request answered with a refusal returns.
var ErrUnreachable = errors.New("peer unreachable")
ErrUnreachable marks a call that never reached the peer: the signaling dial failed. From a caller's seat that is "offline" — the peer is not on the network, or is on it and cannot be reached from here — and it is a different thing to say than "rang and nobody answered".
Functions ¶
func NormalizeToneMime ¶ added in v1.3.98
NormalizeToneMime returns the type a ringback tone of the given type is stored and served as, or "" when it is not an audio format a tone may be.
Types ¶
type Bridge ¶
type Bridge struct {
// contains filtered or unexported fields
}
Bridge is the host app's microphone and speaker, presented to the call manager as ordinary Source/Sink factories. The zero value is not usable — call NewBridge.
func NewBridge ¶
func NewBridge() *Bridge
NewBridge returns an idle bridge. It is safe to use before the host app has ever connected: capture then reads silence and playback is discarded, which is exactly how a machine with no audio hardware behaves.
func (*Bridge) Live ¶
Live reports whether any call currently holds the device. The host app uses it to decide whether to run capture and playback at all — an idle bridge should not be holding the microphone open.
func (*Bridge) Pull ¶
Pull fills dst with what the calls want played, mixed, padding with silence. Always fills dst completely and returns len(dst) — the host app's playback wants a frame on every tick, silence included, or its own buffer underruns and clicks.
func (*Bridge) Push ¶
Push hands captured PCM to every live call. Never blocks: a call whose ring is full drops its oldest audio rather than stalling the capture thread.
type Codec ¶
type Codec interface {
// Name is the codec label carried in signaling ("pcm", "opus").
Name() string
// Encode turns one PCM frame (frameSamples int16 samples) into a payload.
Encode(pcm []int16) ([]byte, error)
// Decode turns a payload back into a PCM frame.
Decode(payload []byte) ([]int16, error)
}
Codec encodes/decodes one audio frame. Real impls: Opus (follow-up, cgo). pcmCodec below is the identity passthrough used until Opus is wired.
func NewOpusCodec ¶
NewOpusCodec builds an Opus codec at the package framing (48 kHz mono, 20 ms frames), tuned for VoIP.
type Config ¶
type Config struct {
LocalPK cipher.PubKey
// Dial opens a stream to peer:port over an available skywire network.
Dial DialFunc
// SignalPort / MediaPort default to the skyenv voice ports when zero.
SignalPort uint16
// Codec is the template codec, used for the codec NAME in signaling. Defaults
// to the PCM passthrough.
Codec Codec
// NewCodec builds a fresh codec per session. A stateful codec (Opus keeps
// encoder/decoder state) MUST NOT be shared across concurrent calls, so each
// session gets its own. Defaults to returning the (stateless) Codec.
NewCodec func() Codec
// NewSource / NewSink build the mic / speaker for a call. Default to
// SilentSource / NullSink (headless) so the control+media plane runs without
// audio hardware; a real audio backend swaps these in.
NewSource func() Source
NewSink func() Sink
// OnIncoming decides whether to accept an inbound invite IMMEDIATELY (used in
// auto-answer mode). nil => decline all (voice off). A headless/test peer
// returns true to auto-answer. Ignored when ManualAnswer is set.
OnIncoming func(inv Sig) bool
// ManualAnswer, when true, makes an inbound invite RING (park, pending) until
// Answer(callID) or Decline(callID) — never auto-accepting. This is the mode
// used once real mic capture is enabled, so a visor never streams its
// microphone to a caller without an explicit answer. Ring (optional) is
// notified when a call starts ringing.
ManualAnswer bool
Ring func(inv Sig)
// Visualize, when true, taps each call's sent/received PCM into a small ring
// so CallAudio can serve it for a live spectrogram. The audio is unaffected.
Visualize bool
Logger *logging.Logger
}
Config wires a Manager. Dial + the listeners are supplied by the runtime (native visor or wasm) so this package stays transport-agnostic (KG7): both a dmsg client and a skynet networker satisfy them, and voice signals on the SAME port over both.
type Dial ¶
type Dial struct {
CallID string
Peer cipher.PubKey
State DialState
// Reason is the detail behind an outcome, for a UI that wants more than
// the state ("dmsg error 100 - entry is not found in discovery").
Reason string
// Ringback is true once the peer's own ringback tone is here to play
// (DialRingback). False means play an ordinary ring.
Ringback bool
}
Dial is one call this visor is placing, for a UI to show how it is going: still connecting, ringing, or — briefly, once over — why it did not connect.
type DialFunc ¶
DialFunc opens a signaling/media stream to peer:port over a skywire network (a dmsg stream or a skynet route). The manager supplies one that tries the available networks; both use the SAME port (skyenv.SkychatVoiceSignalPort / SkychatVoiceMediaPort).
type DialState ¶ added in v1.3.98
type DialState string
DialState is where an outbound call has got to, as the caller sees it.
A caller used to see one state for the whole of a ring: "calling…". A peer that was not on the network, a peer whose phone was ringing, and a peer who had just pressed decline all looked the same — a banner that sat there and then went away — so nobody placing a call could tell whether to wait, try again later, or give up.
const ( // DialConnecting: the invite has not reached the peer yet — the network // is still looking for them. DialConnecting DialState = "connecting" // DialCalling: the peer has the invite but has said nothing since. What // a peer that predates SigRinging looks like for the whole ring. DialCalling DialState = "calling" // DialRinging: the peer says it is ringing. DialRinging DialState = "ringing" // DialOffline: the peer could not be reached at all. DialOffline DialState = "offline" // DialDeclined: the peer turned the call down. DialDeclined DialState = "declined" // DialBusy: the peer is on another call and said so. DialBusy DialState = "busy" // DialNoAnswer: it rang out. DialNoAnswer DialState = "no_answer" // DialFailed: it ended some other way; Dial.Reason says how. DialFailed DialState = "failed" )
type InviteHandler ¶
InviteHandler is called when an inbound invite arrives. It receives the invite and the live signaling conn; the implementation decides to accept (reply SigAccept and set up media) or decline. It must not block long.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager owns the local voice endpoint: it accepts inbound calls via the Signaler and places outbound calls, holding one Session per active call.
func NewManager ¶
NewManager constructs a Manager. Call Serve to start accepting.
func (*Manager) AddListener ¶
AddListener joins an additional signaling listener after Serve has started (used for the skynet listener, which comes up after the dmsg one).
func (*Manager) Call ¶
Call places an outbound call to peer and BLOCKS until it is answered, declined, or the ring budget elapses. For a caller that waits — the CLI. Anything answering an HTTP request wants Dial.
func (*Manager) CallAudio ¶
CallAudio returns the most recent buffered sent + received PCM for a call (Visualize mode). Used to drive a live spectrogram.
func (*Manager) Dial ¶
Dial places an outbound call and returns its id straight away, leaving the invite to complete in the background.
This is what a UI needs. A call's id is the only handle on it — hanging up takes one — and until the callee answers, that id exists nowhere else: the call is in no active list and no ringing list. A caller that only learns the id when the call CONNECTS therefore cannot call off a call that is still ringing, which is the one moment anyone wants to.
It also has to not block. The two HTTP surfaces that place calls both cap a request well below the ring — skychat's own server at a 10s write timeout, the hypervisor's /api at 30s — so a handler that waited out a 55s ring could never deliver its answer to anyone. The caller saw a failed request for a call that was ringing perfectly well.
It takes a budget rather than a context because it owns the call's whole lifetime: there is no request to tie it to, and tying it to one would end the call when the request did.
func (*Manager) DialRingback ¶ added in v1.3.98
DialRingback returns the ringback tone the peer of an outbound call plays, once its bytes are here (Dial.Ringback). ok is false until then, and for a peer that plays none — the caller then plays an ordinary ring of its own.
func (*Manager) Dialing ¶
Dialing returns the calls this visor is placing and that have not been answered yet, with how far each has got — and, for DialOutcomeLinger after one ended unanswered, how it ended (State.Ended()).
func (*Manager) Hangup ¶
Hangup ends an active call by id: a farewell on its media conn and then the close, so the peer ends the call at once rather than trying to reconnect (see byeFrame).
func (*Manager) Incoming ¶
Incoming returns the invites of calls currently ringing (awaiting answer).
func (*Manager) Ringback ¶ added in v1.3.98
Ringback returns this endpoint's own ringback tone; nil data when none is set.
func (*Manager) Serve ¶
Serve accepts inbound call signaling on the given listeners — pass BOTH the dmsg listener and the skynet listener bound to SignalPort so callers reach us over whichever network is up. Blocks until ctx is done.
func (*Manager) SetBlocked ¶
SetBlocked replaces the callers whose calls never ring here. The owner of the block list (skychat) hands it over, since this side has none of its own.
type ResumeFunc ¶
ResumeFunc re-establishes a call's media conn after the transport under it died. It blocks until it has one or gives up, and owns its own budget — the session has no deadline to lend it.
type ResumeHandler ¶
ResumeHandler is called when a peer re-dials to continue a call whose media transport failed. It receives the resume frame and the fresh conn; the implementation answers SigAccept and hands the conn to the live session, or declines if it knows of no such call awaiting one.
type Session ¶
type Session struct {
CallID string
// contains filtered or unexported fields
}
Session is one established 1:1 call's MEDIA plane: RTP frames over a skywire net.Conn (a dmsg stream or skynet route conn — already Noise-encrypted, so no SRTP needed for the transport hop). It runs a send loop (capture → encode → RTP → conn) and a recv loop (conn → RTP → decode → playback) until ctx is canceled, or the conn fails and cannot be rebuilt.
The conn is not fixed for the life of the call. It is one stream on a SHARED dmsg session, so it dies whenever that session does — a server evicting the client, a reaper, a phone moving from Wi-Fi to cellular — none of which is a reason to hang up on anybody. Given a ResumeFunc the loops rebuild it and go on; see tryResume.
Framing: because a stream conn has no message boundaries, each RTP packet is length-prefixed (2-byte big-endian). Over a datagram route (a follow-up) each datagram is one RTP packet and the prefix is dropped — the send/recv split below is the seam for that swap.
func NewSession ¶
func NewSession(callID string, conn net.Conn, codec Codec, source Source, sink Sink, ssrc uint32, log *logging.Logger) *Session
NewSession builds a media session over conn with the given codec + audio.
func (*Session) AdoptConn ¶
AdoptConn installs a replacement media conn the PEER re-dialed, and reports whether it was taken.
This is what makes resumption work without the two sides having to notice the break at the same moment — and they do not. A dead TCP connection accepts writes until its buffer fills, so the side that is mostly writing can take the better part of a minute to find out, while the side that noticed has already re-dialed and given up. Requiring both to be in a "reconnecting" state at once meant their windows had to overlap, and in the one real failure this was tested against they missed each other by twenty-four seconds.
So adoption does not ask whether this side has noticed. It swaps the conn and closes the old one, which is itself what wakes any loop still blocked on it: the loop errors, sees the generation has moved, and carries on.
func (*Session) Close ¶
func (s *Session) Close()
Close tears down the session's conn (idempotent), telling the peer first. The Run loops observe the conn error and exit. It also closes the source and sink if they are Closers: a blocking Source.Read (e.g. an audio ring with no frames flowing) would otherwise wedge sendLoop, so Run's wg.Wait would never return and the call would never be dropped from the manager.
func (*Session) Closed ¶
Closed reports whether the call has been hung up here. For a resume that arrives after the fact: it must be declined, not accepted onto a conn the close still unwinding is about to take away.
func (*Session) EndReason ¶
EndReason says why the call ended, for whoever logs that it did. Empty while it is still running.
func (*Session) Run ¶
Run drives both media loops until ctx is done or the conn fails, then closes the conn. Blocks; call in a goroutine.
func (*Session) SetAwaitResume ¶
func (s *Session) SetAwaitResume()
SetAwaitResume marks the side that waits to be re-dialed rather than dialing. Only one side may dial, or one break becomes two conns.
func (*Session) SetMicMuted ¶
SetMicMuted toggles whether our captured audio is sent to the peer.
func (*Session) SetPeer ¶
SetPeer records who the call is with, so a resume arriving later can be checked against it.
func (*Session) SetResume ¶
func (s *Session) SetResume(r ResumeFunc)
SetResume gives the session a way to rebuild its media conn when the transport under it fails, for the side that PLACED the call. Without either this or SetAwaitResume a broken conn ends the call, which is what every session did before resumption existed.
func (*Session) SetSpeakerMuted ¶
SetSpeakerMuted toggles whether received audio is played locally.
func (*Session) SpeakerMuted ¶
type Sig ¶
type Sig struct {
Type SigType `json:"type"`
CallID string `json:"call_id"`
FromPK cipher.PubKey `json:"from_pk"`
Codec string `json:"codec,omitempty"`
MediaPort uint16 `json:"media_port,omitempty"`
Reason string `json:"reason,omitempty"`
// Resume, on an Invite or Accept, says the sender can continue this call
// over a rebuilt transport (SigResume). Resumption is only armed when
// BOTH sides said so: a peer that predates it never sends the field, and
// never re-dials or takes a re-dial either, so arming it against one only
// turns that peer's hangup into a thirty-to-sixty-second wait for a
// reconnect that cannot come. Absent, a broken conn ends the call the way
// it did before resumption existed.
Resume bool `json:"resume,omitempty"`
// Progress, on an Invite, asks the callee to say when it starts ringing
// (SigRinging). It is what lets a caller tell "reached, and ringing" from
// "still trying to reach them" — before it, both were the same silence.
Progress bool `json:"progress,omitempty"`
// Tone names a ringback tone by the hex SHA-256 of its bytes: on
// SigRinging, the tone the callee plays to its callers ("" for none); on
// SigTone, the tone asked for or being sent. ToneMime and ToneSize
// describe it, so a caller can bound the fetch before it starts.
Tone string `json:"tone,omitempty"`
ToneMime string `json:"tone_mime,omitempty"`
ToneSize int `json:"tone_size,omitempty"`
// Data is one piece of a ringback tone, on a SigTone reply.
Data []byte `json:"data,omitempty"`
}
Sig is one signaling message. It carries the call id, the sender, and — on Invite/Accept — the negotiated codec and the media port both sides will use for the RTP stream (identical over dmsg and skynet, per skyenv).
type SigType ¶
type SigType uint8
SigType is a voice-signaling message kind.
const ( // SigInvite offers a call (caller → callee). SigInvite SigType = iota + 1 // SigAccept accepts an invite (callee → caller). SigAccept // SigDecline rejects an invite (callee → caller); Reason is human text. SigDecline // SigHangup ends an established call (either side). SigHangup // SigBusy signals the callee is already in a call. SigBusy // SigResume re-attaches a media conn to a call already in progress whose // transport died under it. Appended last on purpose: a peer that predates // it reads an unknown type, answers SigDecline("expected invite"), and the // call ends the way it used to instead of misbehaving. SigResume // SigRinging tells a caller that the callee has started ringing (callee → // caller, on the invite's own conn, ahead of the answer). Sent only when // the invite asked for it (Sig.Progress): a caller that predates it reads // every frame but an Accept as the end of the call, so an unrequested one // would hang up on it. SigRinging // SigTone asks for, and carries, the callee's ringback tone. The caller // opens a SEPARATE conn for it, so the tone never queues ahead of the // answer on the call's own. See ringback.go. SigTone )
type Signaler ¶
type Signaler struct {
// contains filtered or unexported fields
}
Signaler runs the voice control channel. It ACCEPTS invites on one port over every skywire network it's given a listener for (dmsg + skynet — same port), and it DIALS invites out via DialFunc. Signaling frames are the length- prefixed JSON above.
func NewSignaler ¶
NewSignaler builds a Signaler bound to the given local PK / port / dialer.
func (*Signaler) AddListener ¶
AddListener starts accepting on an ADDITIONAL listener after Serve has already begun — e.g. the skynet networker registers later than the dmsg client, so its listener joins once it's up. Runs an accept loop until ctx is done.
func (*Signaler) FetchTone ¶ added in v1.3.98
func (s *Signaler) FetchTone(ctx context.Context, peer cipher.PubKey, callID, hash string, size int) ([]byte, error)
FetchTone asks the peer, on a conn of its own, for the ringback tone it announced for callID, and returns its bytes once all size of them have arrived and hash to what was announced.
func (*Signaler) Invite ¶
func (s *Signaler) Invite(ctx context.Context, peer cipher.PubKey, callID, codec string, mediaPort uint16, hooks inviteHooks) (net.Conn, Sig, error)
Invite dials the peer's signaling port (over whichever network DialFunc resolves), sends an Invite, and returns the live conn + the peer's reply (SigAccept or SigDecline/SigBusy). On a non-accept reply it closes the conn and returns the reply with a nil conn. A dial that fails wraps ErrUnreachable.
func (*Signaler) Resume ¶
Resume re-dials the peer's signaling port to continue an EXISTING call whose media transport died, and returns the fresh media conn.
It carries the original call id, which is how the peer tells this apart from a new call: a resume must attach to the session already running rather than ring anybody. Same shape as Invite otherwise — request, bounded wait, and on SigAccept the conn becomes the media conn.
func (*Signaler) Serve ¶
Serve accepts signaling connections on ALL provided listeners concurrently — pass the dmsg listener AND the skynet listener (both bound to s.port) so a caller reaches us over whichever network is up. Returns when ctx is done or every listener has failed.
func (*Signaler) SetInviteHandler ¶
func (s *Signaler) SetInviteHandler(h InviteHandler)
SetInviteHandler registers the inbound-invite callback.
func (*Signaler) SetResumeHandler ¶
func (s *Signaler) SetResumeHandler(h ResumeHandler)
SetResumeHandler registers the callback for a peer re-dialing an existing call. Leaving it unset makes this endpoint answer every resume with a decline, which is what a build without call resumption should do.
func (*Signaler) SetToneHandler ¶ added in v1.3.98
func (s *Signaler) SetToneHandler(h ToneHandler)
SetToneHandler registers the callback for a ringback tone request. Left unset, every request is declined.
type SilentSource ¶
type SilentSource struct{}
SilentSource yields silence frames forever (a placeholder mic). Real capture is the cgo follow-up.
type Sink ¶
Sink is a speaker (or a discard/loopback). Write plays one PCM frame.
func NewSpeakerSink ¶
NewSpeakerSink opens a playback Sink at the package sample rate (mono). The returned Sink also implements io.Closer.
type Source ¶
Source is a microphone (or a synthetic generator). Read fills pcm with the next frame; it blocks until a frame is available, mirroring an audio device.
type ToneHandler ¶ added in v1.3.98
ToneHandler is called when a caller asks for this endpoint's ringback tone. It receives the request and the conn it came on, answers it, and closes the conn.