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