init: 自 zomaintain/backend/rdplib 平移独立成库; module path 从上游 github.com/nakagami/grdp 改为 git.zeroonesoft.cn/golib/rdplib

This commit is contained in:
w11
2026-09-19 18:55:43 +08:00
commit 1096fbce9d
94 changed files with 36790 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
// Package aac provides AAC-to-PCM decoding for RDPSND audio streams.
package aac
import "git.zeroonesoft.cn/golib/rdplib/plugin/rdpsnd"
// Decoder decodes MPEG-4 AAC packets into signed 16-bit little-endian PCM.
type Decoder interface {
// Decode decodes one raw AAC packet. Returns nil, nil for empty input.
Decode(data []byte) ([]byte, error)
// Close releases all resources held by the decoder.
Close()
}
// New creates a Decoder for the given RDPSND AudioFormat.
// Returns an error on platforms where AAC decoding is not supported.
func New(format rdpsnd.AudioFormat) (Decoder, error) {
return newDecoder(format)
}
+210
View File
@@ -0,0 +1,210 @@
//go:build darwin && cgo
package aac
/*
#cgo LDFLAGS: -framework AudioToolbox
#cgo nocallback grdp_aac_converter_new
#cgo nocallback grdp_aac_decode
#cgo nocallback AudioConverterDispose
#cgo noescape grdp_aac_converter_new
#cgo noescape grdp_aac_decode
#include <AudioToolbox/AudioToolbox.h>
#include <stdlib.h>
typedef struct {
const uint8_t *data;
uint32_t size;
int consumed;
} grdp_aac_input_t;
// AudioConverter input callback — called by AudioToolbox to pull one AAC
// packet per invocation. Returns noErr on the first call, then -1 (no data)
// on subsequent calls to signal end-of-input for the current decode round.
static OSStatus grdp_aac_input_proc(
AudioConverterRef inConverter,
UInt32 *ioNumberDataPackets,
AudioBufferList *ioData,
AudioStreamPacketDescription **outDataPacketDescription,
void *inUserData)
{
grdp_aac_input_t *state = (grdp_aac_input_t *)inUserData;
if (state->consumed || state->size == 0) {
*ioNumberDataPackets = 0;
ioData->mBuffers[0].mData = NULL;
ioData->mBuffers[0].mDataByteSize = 0;
return -1;
}
*ioNumberDataPackets = 1;
ioData->mBuffers[0].mData = (void *)state->data;
ioData->mBuffers[0].mDataByteSize = state->size;
if (outDataPacketDescription != NULL) {
static AudioStreamPacketDescription desc;
desc.mStartOffset = 0;
desc.mDataByteSize = state->size;
desc.mVariableFramesInPacket = 0;
*outDataPacketDescription = &desc;
}
state->consumed = 1;
return noErr;
}
// grdp_aac_converter_new creates an AudioConverter that decodes MPEG-4 AAC at
// the given sample rate / channel count. asc/ascLen is the AudioSpecificConfig
// stored in the WAVEFORMATEX ExtraData field; it may be NULL/0 for ADTS input.
static AudioConverterRef grdp_aac_converter_new(
double sampleRate,
uint32_t channels,
const uint8_t *asc,
uint32_t ascLen,
OSStatus *outErr)
{
AudioStreamBasicDescription inFmt = {
.mSampleRate = sampleRate,
.mFormatID = kAudioFormatMPEG4AAC,
.mChannelsPerFrame = channels,
};
AudioStreamBasicDescription outFmt = {
.mSampleRate = sampleRate,
.mFormatID = kAudioFormatLinearPCM,
.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger |
kLinearPCMFormatFlagIsPacked,
.mBitsPerChannel = 16,
.mChannelsPerFrame = channels,
.mBytesPerFrame = (uint32_t)(2 * channels),
.mFramesPerPacket = 1,
.mBytesPerPacket = (uint32_t)(2 * channels),
};
AudioConverterRef conv = NULL;
OSStatus err = AudioConverterNew(&inFmt, &outFmt, &conv);
if (err != noErr) { *outErr = err; return NULL; }
if (ascLen > 0 && asc != NULL) {
err = AudioConverterSetProperty(
conv,
kAudioConverterDecompressionMagicCookie,
ascLen, asc);
if (err != noErr) {
AudioConverterDispose(conv);
*outErr = err;
return NULL;
}
}
*outErr = noErr;
return conv;
}
// grdp_aac_decode decodes one AAC packet (inData/inSize) into signed 16-bit
// little-endian PCM stored in outBuf. *outBytesWritten is set to the number
// of bytes actually written. Returns noErr on success.
static OSStatus grdp_aac_decode(
AudioConverterRef conv,
const uint8_t *inData,
uint32_t inSize,
uint8_t *outBuf,
uint32_t outBufSize,
uint32_t *outBytesWritten)
{
grdp_aac_input_t state = { inData, inSize, 0 };
AudioBufferList outList = {
.mNumberBuffers = 1,
.mBuffers[0] = {
.mNumberChannels = 0,
.mDataByteSize = outBufSize,
.mData = outBuf,
},
};
// AAC-LC produces 1024 samples/channel per frame; AAC-HE produces 2048.
// 4096 is large enough for both at up to 2 channels.
UInt32 numFrames = 4096;
OSStatus err = AudioConverterFillComplexBuffer(
conv, grdp_aac_input_proc, &state, &numFrames, &outList, NULL);
// -1 from the input callback signals "no more data"; that is not an error.
if (err != noErr && err != -1) {
*outBytesWritten = 0;
return err;
}
*outBytesWritten = outList.mBuffers[0].mDataByteSize;
return noErr;
}
*/
import "C"
import (
"fmt"
"log/slog"
"runtime"
"unsafe"
"git.zeroonesoft.cn/golib/rdplib/plugin/rdpsnd"
)
// darwinDecoder decodes MPEG-4 AAC audio into signed 16-bit PCM using
// macOS AudioToolbox (hardware-accelerated on Apple Silicon / Intel iGPU).
type darwinDecoder struct {
conv C.AudioConverterRef
channels int
outBuf []byte // reusable output scratch buffer
}
func newDecoder(format rdpsnd.AudioFormat) (Decoder, error) {
var oscErr C.OSStatus
var ascPtr *C.uint8_t
ascLen := C.uint32_t(0)
if len(format.ExtraData) > 0 {
ascPtr = (*C.uint8_t)(unsafe.Pointer(&format.ExtraData[0]))
ascLen = C.uint32_t(len(format.ExtraData))
}
conv := C.grdp_aac_converter_new(
C.double(format.SamplesPerSec),
C.uint32_t(format.Channels),
ascPtr, ascLen,
&oscErr)
if len(format.ExtraData) > 0 {
runtime.KeepAlive(format.ExtraData)
}
if conv == nil {
return nil, fmt.Errorf("AudioToolbox: AudioConverterNew failed (err=%d)", int32(oscErr))
}
slog.Info("AAC decoder created",
"rate", format.SamplesPerSec,
"channels", format.Channels,
"ascLen", len(format.ExtraData))
// 4096 samples × channels × 2 bytes/sample — large enough for AAC-HE at 2 ch.
outBufSize := 4096 * int(format.Channels) * 2
return &darwinDecoder{conv: conv, channels: int(format.Channels), outBuf: make([]byte, outBufSize)}, nil
}
// Decode decodes one raw AAC packet into signed 16-bit little-endian PCM.
// Returns nil, nil when data is empty.
func (d *darwinDecoder) Decode(data []byte) ([]byte, error) {
if len(data) == 0 {
return nil, nil
}
var written C.uint32_t
err := C.grdp_aac_decode(
d.conv,
(*C.uint8_t)(unsafe.Pointer(&data[0])),
C.uint32_t(len(data)),
(*C.uint8_t)(unsafe.Pointer(&d.outBuf[0])),
C.uint32_t(len(d.outBuf)),
&written)
runtime.KeepAlive(data)
runtime.KeepAlive(d.outBuf)
if err != 0 {
return nil, fmt.Errorf("AudioToolbox: decode error %d", int32(err))
}
if written == 0 {
return nil, nil
}
out := make([]byte, int(written))
copy(out, d.outBuf[:written])
return out, nil
}
// Close releases the AudioConverter.
func (d *darwinDecoder) Close() {
if d.conv != nil {
C.AudioConverterDispose(d.conv)
d.conv = nil
}
}
+22
View File
@@ -0,0 +1,22 @@
//go:build !darwin || !cgo
package aac
import (
"errors"
"git.zeroonesoft.cn/golib/rdplib/plugin/rdpsnd"
)
// stubDecoder is used on platforms without AudioToolbox support.
type stubDecoder struct{}
func newDecoder(_ rdpsnd.AudioFormat) (Decoder, error) {
return nil, errors.New("AAC decoding not supported on this platform")
}
func (d *stubDecoder) Decode(_ []byte) ([]byte, error) {
return nil, errors.New("AAC decoding not supported on this platform")
}
func (d *stubDecoder) Close() {}
+524
View File
@@ -0,0 +1,524 @@
// Package rdpsnd implements the RDPSND (Audio Output Virtual Channel Extension)
// protocol (MS-RDPEA) for server-to-client audio redirection.
//
// It can operate over either a static virtual channel ("rdpsnd") or
// a dynamic virtual channel (AUDIO_PLAYBACK_DVC / AUDIO_PLAYBACK_LOSSY_DVC).
package rdpsnd
import (
"bytes"
"encoding/binary"
"fmt"
"log/slog"
"git.zeroonesoft.cn/golib/rdplib/core"
"git.zeroonesoft.cn/golib/rdplib/plugin"
)
const (
ChannelName = plugin.RDPSND_SVC_CHANNEL_NAME
ChannelOption = plugin.CHANNEL_OPTION_INITIALIZED |
plugin.CHANNEL_OPTION_ENCRYPT_RDP
)
// RDPSND PDU types (MS-RDPEA 2.2)
const (
SNDC_CLOSE = 0x01
SNDC_WAVE = 0x02
SNDC_SETVOLUME = 0x03
SNDC_SETPITCH = 0x04
SNDC_WAVECONFIRM = 0x05
SNDC_TRAINING = 0x06
SNDC_FORMATS = 0x07
SNDC_CRYPTKEY = 0x08
SNDC_WAVEENCRYPT = 0x09
SNDC_UDPWAVE = 0x0A
SNDC_UDPWAVELAST = 0x0B
SNDC_QUALITYMODE = 0x0C
SNDC_WAVE2 = 0x0D
)
// RDPSND capabilities flags
const (
TSSNDCAPS_ALIVE = 0x00000001
TSSNDCAPS_VOLUME = 0x00000002
TSSNDCAPS_PITCH = 0x00000004
)
// Quality mode values (MS-RDPEA 2.2.2.9)
const (
DYNAMIC_QUALITY = 0x0000
MEDIUM_QUALITY = 0x0002
HIGH_QUALITY = 0x0001
)
// Audio format tags
const (
WAVE_FORMAT_PCM = 0x0001
WAVE_FORMAT_ADPCM = 0x0002
WAVE_FORMAT_ALAW = 0x0006
WAVE_FORMAT_MULAW = 0x0007
WAVE_FORMAT_AAC = 0x00FF // MPEG-4 AAC (AudioSpecificConfig in ExtraData)
)
// RDPSND version
// gnome-remote-desktop (grd-rdp-dvc-audio-playback.c) requires
// clientVersion >= 8 (CHANNEL_VERSION_WIN_8). FreeRDP WIN_7=6, WIN_8=8.
const (
RDPSND_VERSION_MAJOR = 0x08
)
// AudioFormat represents a WAVEFORMATEX structure.
type AudioFormat struct {
Tag uint16
Channels uint16
SamplesPerSec uint32
AvgBytesPerSec uint32
BlockAlign uint16
BitsPerSample uint16
ExtraData []byte
}
func (f AudioFormat) String() string {
var name string
switch f.Tag {
case WAVE_FORMAT_PCM:
name = "PCM"
case WAVE_FORMAT_ADPCM:
name = "ADPCM"
case WAVE_FORMAT_ALAW:
name = "A-Law"
case WAVE_FORMAT_MULAW:
name = "μ-Law"
case WAVE_FORMAT_AAC:
name = "AAC"
default:
name = fmt.Sprintf("0x%04x", f.Tag)
}
return fmt.Sprintf("%s %dHz %dch %dbit", name, f.SamplesPerSec, f.Channels, f.BitsPerSample)
}
func (f AudioFormat) IsPCM() bool {
return f.Tag == WAVE_FORMAT_PCM
}
// IsAAC reports whether the format uses MPEG-4 AAC encoding.
func (f AudioFormat) IsAAC() bool {
return f.Tag == WAVE_FORMAT_AAC
}
func (f AudioFormat) pack() []byte {
b := make([]byte, 18+len(f.ExtraData))
binary.LittleEndian.PutUint16(b[0:], f.Tag)
binary.LittleEndian.PutUint16(b[2:], f.Channels)
binary.LittleEndian.PutUint32(b[4:], f.SamplesPerSec)
binary.LittleEndian.PutUint32(b[8:], f.AvgBytesPerSec)
binary.LittleEndian.PutUint16(b[12:], f.BlockAlign)
binary.LittleEndian.PutUint16(b[14:], f.BitsPerSample)
binary.LittleEndian.PutUint16(b[16:], uint16(len(f.ExtraData)))
copy(b[18:], f.ExtraData)
return b
}
func unpackAudioFormat(data []byte, offset int) (AudioFormat, int) {
if len(data)-offset < 18 {
return AudioFormat{}, offset
}
f := AudioFormat{
Tag: binary.LittleEndian.Uint16(data[offset:]),
Channels: binary.LittleEndian.Uint16(data[offset+2:]),
SamplesPerSec: binary.LittleEndian.Uint32(data[offset+4:]),
AvgBytesPerSec: binary.LittleEndian.Uint32(data[offset+8:]),
BlockAlign: binary.LittleEndian.Uint16(data[offset+12:]),
BitsPerSample: binary.LittleEndian.Uint16(data[offset+14:]),
}
cbSize := int(binary.LittleEndian.Uint16(data[offset+16:]))
if offset+18+cbSize <= len(data) {
f.ExtraData = make([]byte, cbSize)
copy(f.ExtraData, data[offset+18:offset+18+cbSize])
}
return f, offset + 18 + cbSize
}
// Handler implements the RDPSND protocol over a static virtual channel.
// It also serves as the DVC audio handler via ProcessData.
type Handler struct {
channelSender core.ChannelSender
serverFormats []AudioFormat
clientFormatIndices []int
activeFormatIndex int
// Wave state
waveTimestamp uint16
waveBlockNo uint8
pendingWaveHdr [4]byte // backing array for pendingWave initial bytes (avoids alloc)
pendingWave []byte
expectingWave bool
// DVC send callback for the current message's channel
dvcSendFunc func([]byte)
// viaDvc tracks whether the current message arrived via DVC
viaDvc bool
// Application callback: called with the active AudioFormat and PCM data
onAudio func(AudioFormat, []byte)
// onAudioReset is called when the server closes the audio channel
// (SNDC_CLOSE). The application should flush its audio playback buffer
// so that stale audio from before a seek does not keep playing.
onAudioReset func()
// muted 为 true 时丢弃 wave 数据不回调 onAudio,但协议握手与
// wave 确认照常——服务器认为音频已被重定向而保持静音
//(对应 mstsc「不播放」模式)。
muted bool
}
// NewHandler creates a new RDPSND handler.
// onAudio is called with the active AudioFormat and PCM audio data for each wave.
func NewHandler(onAudio func(AudioFormat, []byte)) *Handler {
return &Handler{
activeFormatIndex: -1,
onAudio: onAudio,
}
}
// SetAudioResetCallback sets a function that is called when the server
// closes the audio channel (e.g. on media seek). The application should
// flush its audio playback buffer in this callback.
func (h *Handler) SetAudioResetCallback(f func()) {
h.onAudioReset = f
}
// SetMuted controls wave playback: muted=true 丢弃音频数据(不回调
// onAudio),协议层格式协商与 wave 确认照常进行。
func (h *Handler) SetMuted(m bool) {
h.muted = m
}
// --- plugin.ChannelTransport interface ---
func (h *Handler) GetType() (string, uint32) {
return ChannelName, ChannelOption
}
func (h *Handler) Sender(s core.ChannelSender) {
h.channelSender = s
}
// Process handles data from the static virtual channel (already reassembled).
func (h *Handler) Process(s []byte) {
defer func() {
if r := recover(); r != nil {
slog.Error("rdpsnd: panic in Process", "err", r)
}
}()
h.viaDvc = false
h.ProcessData(s)
}
// ProcessData processes a reassembled RDPSND PDU payload.
// This is used by both the static VChannel path and the DVC path.
func (h *Handler) ProcessData(data []byte) {
if h.expectingWave {
h.processWaveBody(data)
return
}
if len(data) < 4 {
return
}
msgType := data[0]
// data[1] is bPad
bodySize := int(binary.LittleEndian.Uint16(data[2:4]))
body := data[4:]
if bodySize < len(body) {
body = body[:bodySize]
}
switch msgType {
case SNDC_FORMATS:
h.processServerFormats(body)
case SNDC_TRAINING:
h.processTraining(body)
case SNDC_WAVE:
h.processWaveInfo(body)
case SNDC_WAVE2:
h.processWave2(body)
case SNDC_CLOSE:
slog.Debug("rdpsnd: server closed audio channel")
if h.onAudioReset != nil {
h.onAudioReset()
}
case SNDC_SETVOLUME, SNDC_QUALITYMODE:
// ignored
default:
slog.Debug("rdpsnd: unknown msgType", "type", fmt.Sprintf("0x%02x", msgType))
}
}
// --- Server Audio Formats and Version (MS-RDPEA 2.2.2.1) ---
func (h *Handler) processServerFormats(body []byte) {
if len(body) < 20 {
slog.Warn("rdpsnd: Server Formats PDU too short")
return
}
dwFlags := binary.LittleEndian.Uint32(body[0:])
_ = dwFlags
wNumberOfFormats := binary.LittleEndian.Uint16(body[14:])
wVersion := binary.LittleEndian.Uint16(body[17:])
slog.Debug("rdpsnd: Server Formats", "version", wVersion, "numFormats", wNumberOfFormats)
offset := 20
h.serverFormats = nil
for i := 0; i < int(wNumberOfFormats); i++ {
fmt, newOffset := unpackAudioFormat(body, offset)
if newOffset == offset {
break
}
h.serverFormats = append(h.serverFormats, fmt)
slog.Debug("rdpsnd: server format", "idx", i, "fmt", fmt)
offset = newOffset
}
// Prefer AAC formats first (hardware-decoded on macOS), then fall back to PCM.
h.clientFormatIndices = nil
for i, f := range h.serverFormats {
if f.IsAAC() {
h.clientFormatIndices = append(h.clientFormatIndices, i)
}
}
for i, f := range h.serverFormats {
if f.IsPCM() && (f.BitsPerSample == 8 || f.BitsPerSample == 16) && (f.Channels == 1 || f.Channels == 2) {
h.clientFormatIndices = append(h.clientFormatIndices, i)
}
}
if len(h.clientFormatIndices) == 0 {
slog.Warn("rdpsnd: no supported PCM format found")
}
h.sendClientFormats(wVersion)
}
func (h *Handler) sendClientFormats(serverVersion uint16) {
version := min(serverVersion, RDPSND_VERSION_MAJOR)
formatData := &bytes.Buffer{}
for _, idx := range h.clientFormatIndices {
formatData.Write(h.serverFormats[idx].pack())
}
// Header: dwFlags(4) + dwVolume(4) + dwPitch(4) + wDGramPort(2)
// + wNumberOfFormats(2) + cLastBlockConfirmed(1) + wVersion(2) + bPad(1)
hdr := &bytes.Buffer{}
binary.Write(hdr, binary.LittleEndian, uint32(TSSNDCAPS_ALIVE)) // dwFlags
binary.Write(hdr, binary.LittleEndian, uint32(0)) // dwVolume
binary.Write(hdr, binary.LittleEndian, uint32(0)) // dwPitch
binary.Write(hdr, binary.LittleEndian, uint16(0)) // wDGramPort
binary.Write(hdr, binary.LittleEndian, uint16(len(h.clientFormatIndices)))
hdr.WriteByte(0) // cLastBlockConfirmed
binary.Write(hdr, binary.LittleEndian, version) // wVersion
hdr.WriteByte(0) // bPad
body := append(hdr.Bytes(), formatData.Bytes()...)
pdu := &bytes.Buffer{}
pdu.WriteByte(SNDC_FORMATS) // msgType
pdu.WriteByte(0) // bPad
binary.Write(pdu, binary.LittleEndian, uint16(len(body)))
pdu.Write(body)
h.send(pdu.Bytes())
slog.Debug("rdpsnd: sent Client Formats", "version", version, "numFormats", len(h.clientFormatIndices))
// FreeRDP sends a Quality Mode PDU immediately after Client Formats.
// Without it, Windows waits (up to ~10 seconds) before sending Training.
h.sendQualityMode()
}
// --- Quality Mode (MS-RDPEA 2.2.2.9) ---
func (h *Handler) sendQualityMode() {
pdu := [8]byte{
SNDC_QUALITYMODE, 0,
4, 0, // bodySize = 4 (little-endian uint16)
}
binary.LittleEndian.PutUint16(pdu[4:], HIGH_QUALITY)
// pdu[6:8] = Reserved, already zero
h.send(pdu[:])
slog.Debug("rdpsnd: sent QualityMode")
}
// --- Training (MS-RDPEA 2.2.2.3) ---
func (h *Handler) processTraining(body []byte) {
if len(body) < 4 {
return
}
wTimeStamp := binary.LittleEndian.Uint16(body[0:])
wPackSize := binary.LittleEndian.Uint16(body[2:])
slog.Debug("rdpsnd: Training", "timestamp", wTimeStamp, "packSize", wPackSize)
pdu := [8]byte{SNDC_TRAINING, 0, 4, 0} // msgType, bPad, bodySize=4 (LE)
binary.LittleEndian.PutUint16(pdu[4:], wTimeStamp)
binary.LittleEndian.PutUint16(pdu[6:], wPackSize)
h.send(pdu[:])
slog.Debug("rdpsnd: sent Training Confirm")
}
// --- Wave Info / Wave Data (MS-RDPEA 2.2.2.5 / 2.2.2.6) ---
func (h *Handler) processWaveInfo(body []byte) {
if len(body) < 12 {
slog.Warn("rdpsnd: WaveInfo body too short")
return
}
wTimeStamp := binary.LittleEndian.Uint16(body[0:])
wFormatNo := binary.LittleEndian.Uint16(body[2:])
cBlockNo := body[4]
copy(h.pendingWaveHdr[:], body[8:12])
h.waveTimestamp = wTimeStamp
h.waveBlockNo = cBlockNo
h.pendingWave = h.pendingWaveHdr[:]
if int(wFormatNo) < len(h.clientFormatIndices) {
serverIdx := h.clientFormatIndices[wFormatNo]
h.activeFormatIndex = serverIdx
} else {
slog.Warn("rdpsnd: WaveInfo format index out of range", "idx", wFormatNo, "max", len(h.clientFormatIndices))
}
h.expectingWave = true
slog.Debug("rdpsnd: WaveInfo", "ts", wTimeStamp, "fmt", wFormatNo, "block", cBlockNo)
}
func (h *Handler) processWaveBody(data []byte) {
h.expectingWave = false
// First 4 bytes are padding (duplicate of WaveInfo header)
var audioData []byte
if len(data) > 4 {
audioData = append(h.pendingWave, data[4:]...)
} else {
audioData = h.pendingWave
}
h.pendingWave = nil
slog.Debug("rdpsnd: Wave data", "len", len(audioData))
h.deliverAudio(audioData)
var confirmFmt AudioFormat
if h.activeFormatIndex >= 0 && h.activeFormatIndex < len(h.serverFormats) {
confirmFmt = h.serverFormats[h.activeFormatIndex]
}
h.sendWaveConfirm(waveConfirmTimestamp(h.waveTimestamp, audioData, confirmFmt), h.waveBlockNo)
}
// --- Wave2 (MS-RDPEA 2.2.2.7) ---
func (h *Handler) processWave2(body []byte) {
if len(body) < 12 {
slog.Warn("rdpsnd: Wave2 body too short")
return
}
wTimeStamp := binary.LittleEndian.Uint16(body[0:])
wFormatNo := binary.LittleEndian.Uint16(body[2:])
cBlockNo := body[4]
audioData := body[12:]
if int(wFormatNo) < len(h.clientFormatIndices) {
serverIdx := h.clientFormatIndices[wFormatNo]
h.activeFormatIndex = serverIdx
} else {
slog.Warn("rdpsnd: Wave2 format index out of range", "idx", wFormatNo, "max", len(h.clientFormatIndices))
}
slog.Debug("rdpsnd: Wave2", "ts", wTimeStamp, "fmt", wFormatNo, "block", cBlockNo, "dataLen", len(audioData))
h.deliverAudio(audioData)
var confirmFmt AudioFormat
if h.activeFormatIndex >= 0 && h.activeFormatIndex < len(h.serverFormats) {
confirmFmt = h.serverFormats[h.activeFormatIndex]
}
h.sendWaveConfirm(waveConfirmTimestamp(wTimeStamp, audioData, confirmFmt), cBlockNo)
}
// --- Wave Confirm (MS-RDPEA 2.2.2.8) ---
// waveConfirmTimestamp computes the wTimeStamp for WAVE_CONFIRM_PDU.
// MS-RDPEA §2.2.2.8: the confirmed timestamp MUST be the server's timestamp
// PLUS the estimated playback duration of the audio data in milliseconds.
// This allows the server to pace audio delivery accurately.
func waveConfirmTimestamp(serverTs uint16, audioData []byte, fmt AudioFormat) uint16 {
if fmt.AvgBytesPerSec == 0 {
return serverTs
}
playMs := uint32(len(audioData)) * 1000 / fmt.AvgBytesPerSec
return serverTs + uint16(playMs)
}
func (h *Handler) sendWaveConfirm(timestamp uint16, blockNo uint8) {
var pdu [8]byte
pdu[0] = SNDC_WAVECONFIRM
// pdu[1] = bPad (zero)
pdu[2] = 4 // bodySize = 4 (little-endian uint16, high byte stays 0)
binary.LittleEndian.PutUint16(pdu[4:], timestamp)
pdu[6] = blockNo
// pdu[7] = bPad (zero)
h.send(pdu[:])
slog.Debug("rdpsnd: sent WaveConfirm", "ts", timestamp, "block", blockNo)
}
// --- Audio delivery ---
func (h *Handler) deliverAudio(data []byte) {
if h.muted || h.onAudio == nil || h.activeFormatIndex < 0 || h.activeFormatIndex >= len(h.serverFormats) {
return
}
h.onAudio(h.serverFormats[h.activeFormatIndex], data)
}
// --- Send helpers ---
// send sends a response on the same path that the current message arrived on.
// Static channel messages get static channel responses; DVC messages get DVC responses.
func (h *Handler) send(data []byte) {
if h.viaDvc && h.dvcSendFunc != nil {
h.dvcSendFunc(data)
} else if h.channelSender != nil {
h.channelSender.SendToChannel(ChannelName, data)
}
}
// --- DVC adapter ---
// DvcAdapter wraps an rdpsnd Handler to work as a DVC channel handler.
// Each DVC channel gets its own adapter so responses go to the correct channel.
type DvcAdapter struct {
handler *Handler
sendFunc func([]byte)
}
// NewDvcAdapter creates a DVC adapter that routes audio data to the given Handler.
func NewDvcAdapter(handler *Handler) *DvcAdapter {
return &DvcAdapter{handler: handler}
}
// Process implements drdynvc.DvcChannelHandler.
func (a *DvcAdapter) Process(data []byte) {
a.handler.viaDvc = true
a.handler.dvcSendFunc = a.sendFunc
a.handler.ProcessData(data)
}
// SetSendFunc is called by the DVC client to provide the send function.
func (a *DvcAdapter) SetSendFunc(fn func([]byte)) {
a.sendFunc = fn
}