Stream a synthetic test pattern and silent PCM audio over the OBS Teleport protocol: - protocol: wire format (Header/ImageHeader/WaveHeader), BT.709 full range colour matrix, JPEG encode via turbojpeg cgo, WAVE packet builder - output: TCP sender with per-connection buffered channels and drop-on-overflow - discovery: multicast announce via peerdiscovery - cmd: teleportfling CLI with flags, test-pattern frame generator Verified end-to-end: OBS discovers and renders the stream with correct colours and motion.
81 lines
2.0 KiB
Go
81 lines
2.0 KiB
Go
// Package protocol implements the obs-teleport wire format.
|
|
//
|
|
// This file builds the audio ("WAVE") side of the protocol: Header +
|
|
// WaveHeader + raw interleaved PCM, byte-for-byte compatible with the
|
|
// reference implementation (GPL-2.0).
|
|
package protocol
|
|
|
|
// BuildWavePacket assembles a complete ""WAVE"" wire packet:
|
|
// Header + WaveHeader + interleaved PCM payload.
|
|
//
|
|
// pcm is already interleaved (L,R,L,R,… for stereo). format must be one of
|
|
// the AudioFormat* constants. frames is the PCM frame count (samples per
|
|
// channel). The returned slice is a freshly allocated buffer safe to hand to
|
|
// the network.
|
|
func BuildWavePacket(timestamp uint64, format int32, sampleRate, speakers, frames int32, pcm []byte) ([]byte, error) {
|
|
expected := int64(speakers) * int64(frames) * int64(bytesPerSample(format))
|
|
if int64(len(pcm)) != expected {
|
|
return nil, errBadPCMLength{got: len(pcm), want: int(expected)}
|
|
}
|
|
|
|
h := Header{
|
|
Type: AudioType,
|
|
Timestamp: timestamp,
|
|
Size: int32(len(pcm)),
|
|
}
|
|
wave := WaveHeader{
|
|
Format: format,
|
|
SampleRate: sampleRate,
|
|
Speakers: speakers,
|
|
Frames: frames,
|
|
}
|
|
return WritePacket(h, nil, &wave, pcm)
|
|
}
|
|
|
|
// bytesPerSample maps an AudioFormat* value to its size in bytes.
|
|
func bytesPerSample(format int32) int {
|
|
switch format {
|
|
case AudioFormatU8:
|
|
return 1
|
|
case AudioFormatS16:
|
|
return 2
|
|
case AudioFormatS32, AudioFormatF32:
|
|
return 4
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// errBadPCMLength is returned when pcm length does not match the declared
|
|
// format/speakers/frames.
|
|
type errBadPCMLength struct{ got, want int }
|
|
|
|
func (e errBadPCMLength) Error() string {
|
|
return "protocol: pcm length mismatch" +
|
|
": got " + itoa(e.got) + " bytes, want " + itoa(e.want)
|
|
}
|
|
|
|
// itoa is a tiny local int→string helper to avoid importing strconv in the
|
|
// hot path callers.
|
|
func itoa(v int) string {
|
|
if v == 0 {
|
|
return "0"
|
|
}
|
|
neg := v < 0
|
|
if neg {
|
|
v = -v
|
|
}
|
|
var buf [20]byte
|
|
i := len(buf)
|
|
for v > 0 {
|
|
i--
|
|
buf[i] = byte('0' + v%10)
|
|
v /= 10
|
|
}
|
|
if neg {
|
|
i--
|
|
buf[i] = '-'
|
|
}
|
|
return string(buf[i:])
|
|
}
|