feat: implement M1 teleport protocol sender
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.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
// 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:])
|
||||
}
|
||||
Reference in New Issue
Block a user