// 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 }