init: 自 zomaintain/backend/rdplib 平移独立成库; module path 从上游 github.com/nakagami/grdp 改为 git.zeroonesoft.cn/golib/rdplib
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user