mirror of https://github.com/jetkvm/kvm.git
[Milestone] Improvement: In-flight audio output quality update
This commit is contained in:
parent
2bc7e50391
commit
3c6184d0e8
|
@ -87,10 +87,10 @@ static volatile int playback_initialized = 0;
|
||||||
// Function to dynamically update Opus encoder parameters
|
// Function to dynamically update Opus encoder parameters
|
||||||
int update_opus_encoder_params(int bitrate, int complexity, int vbr, int vbr_constraint,
|
int update_opus_encoder_params(int bitrate, int complexity, int vbr, int vbr_constraint,
|
||||||
int signal_type, int bandwidth, int dtx) {
|
int signal_type, int bandwidth, int dtx) {
|
||||||
// This function is specifically for audio OUTPUT encoder parameters
|
// This function works for both audio input and output encoder parameters
|
||||||
// Only require playback initialization for audio output quality changes
|
// Require either capture (output) or playback (input) initialization
|
||||||
if (!encoder || !playback_initialized) {
|
if (!encoder || (!capture_initialized && !playback_initialized)) {
|
||||||
return -1; // Audio output encoder not initialized
|
return -1; // Audio encoder not initialized
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the static variables
|
// Update the static variables
|
||||||
|
|
|
@ -15,16 +15,18 @@ import (
|
||||||
|
|
||||||
// Legacy aliases for backward compatibility
|
// Legacy aliases for backward compatibility
|
||||||
type OutputIPCConfig = UnifiedIPCConfig
|
type OutputIPCConfig = UnifiedIPCConfig
|
||||||
|
type OutputIPCOpusConfig = UnifiedIPCOpusConfig
|
||||||
type OutputMessageType = UnifiedMessageType
|
type OutputMessageType = UnifiedMessageType
|
||||||
type OutputIPCMessage = UnifiedIPCMessage
|
type OutputIPCMessage = UnifiedIPCMessage
|
||||||
|
|
||||||
// Legacy constants for backward compatibility
|
// Legacy constants for backward compatibility
|
||||||
const (
|
const (
|
||||||
OutputMessageTypeOpusFrame = MessageTypeOpusFrame
|
OutputMessageTypeOpusFrame = MessageTypeOpusFrame
|
||||||
OutputMessageTypeConfig = MessageTypeConfig
|
OutputMessageTypeConfig = MessageTypeConfig
|
||||||
OutputMessageTypeStop = MessageTypeStop
|
OutputMessageTypeOpusConfig = MessageTypeOpusConfig
|
||||||
OutputMessageTypeHeartbeat = MessageTypeHeartbeat
|
OutputMessageTypeStop = MessageTypeStop
|
||||||
OutputMessageTypeAck = MessageTypeAck
|
OutputMessageTypeHeartbeat = MessageTypeHeartbeat
|
||||||
|
OutputMessageTypeAck = MessageTypeAck
|
||||||
)
|
)
|
||||||
|
|
||||||
// Methods are now inherited from UnifiedIPCMessage
|
// Methods are now inherited from UnifiedIPCMessage
|
||||||
|
@ -142,14 +144,134 @@ func (s *AudioOutputServer) acceptConnections() {
|
||||||
s.mtx.Unlock()
|
s.mtx.Unlock()
|
||||||
|
|
||||||
s.logger.Info().Msg("Client connected to audio output server")
|
s.logger.Info().Msg("Client connected to audio output server")
|
||||||
// Only handle one connection at a time for simplicity
|
// Start message processing for this connection
|
||||||
for s.running && s.conn != nil {
|
s.wg.Add(1)
|
||||||
// Keep connection alive until stopped or disconnected
|
go s.handleConnection(conn)
|
||||||
time.Sleep(100 * time.Millisecond)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleConnection processes messages from a client connection
|
||||||
|
func (s *AudioOutputServer) handleConnection(conn net.Conn) {
|
||||||
|
defer s.wg.Done()
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
for s.running {
|
||||||
|
msg, err := s.readMessage(conn)
|
||||||
|
if err != nil {
|
||||||
|
if s.running {
|
||||||
|
s.logger.Error().Err(err).Msg("Failed to read message from client")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.processMessage(msg); err != nil {
|
||||||
|
s.logger.Error().Err(err).Msg("Failed to process message")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readMessage reads a message from the connection
|
||||||
|
func (s *AudioOutputServer) readMessage(conn net.Conn) (*OutputIPCMessage, error) {
|
||||||
|
header := make([]byte, 17)
|
||||||
|
if _, err := io.ReadFull(conn, header); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
magic := binary.LittleEndian.Uint32(header[0:4])
|
||||||
|
if magic != s.magicNumber {
|
||||||
|
return nil, fmt.Errorf("invalid magic number: expected %d, got %d", s.magicNumber, magic)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgType := OutputMessageType(header[4])
|
||||||
|
length := binary.LittleEndian.Uint32(header[5:9])
|
||||||
|
timestamp := int64(binary.LittleEndian.Uint64(header[9:17]))
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
if length > 0 {
|
||||||
|
data = make([]byte, length)
|
||||||
|
if _, err := io.ReadFull(conn, data); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read data: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OutputIPCMessage{
|
||||||
|
Magic: magic,
|
||||||
|
Type: msgType,
|
||||||
|
Length: length,
|
||||||
|
Timestamp: timestamp,
|
||||||
|
Data: data,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// processMessage processes a received message
|
||||||
|
func (s *AudioOutputServer) processMessage(msg *OutputIPCMessage) error {
|
||||||
|
switch msg.Type {
|
||||||
|
case OutputMessageTypeOpusConfig:
|
||||||
|
return s.processOpusConfig(msg.Data)
|
||||||
|
case OutputMessageTypeStop:
|
||||||
|
s.logger.Info().Msg("Received stop message")
|
||||||
|
return nil
|
||||||
|
case OutputMessageTypeHeartbeat:
|
||||||
|
s.logger.Debug().Msg("Received heartbeat")
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
s.logger.Warn().Int("type", int(msg.Type)).Msg("Unknown message type")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processOpusConfig processes Opus configuration updates
|
||||||
|
func (s *AudioOutputServer) processOpusConfig(data []byte) error {
|
||||||
|
// Validate configuration data size (9 * int32 = 36 bytes)
|
||||||
|
if len(data) != 36 {
|
||||||
|
return fmt.Errorf("invalid Opus configuration data size: expected 36 bytes, got %d", len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode Opus configuration
|
||||||
|
config := OutputIPCOpusConfig{
|
||||||
|
SampleRate: int(binary.LittleEndian.Uint32(data[0:4])),
|
||||||
|
Channels: int(binary.LittleEndian.Uint32(data[4:8])),
|
||||||
|
FrameSize: int(binary.LittleEndian.Uint32(data[8:12])),
|
||||||
|
Bitrate: int(binary.LittleEndian.Uint32(data[12:16])),
|
||||||
|
Complexity: int(binary.LittleEndian.Uint32(data[16:20])),
|
||||||
|
VBR: int(binary.LittleEndian.Uint32(data[20:24])),
|
||||||
|
SignalType: int(binary.LittleEndian.Uint32(data[24:28])),
|
||||||
|
Bandwidth: int(binary.LittleEndian.Uint32(data[28:32])),
|
||||||
|
DTX: int(binary.LittleEndian.Uint32(data[32:36])),
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info().Interface("config", config).Msg("Received Opus configuration update")
|
||||||
|
|
||||||
|
// Ensure we're running in the audio server subprocess
|
||||||
|
if !isAudioServerProcess() {
|
||||||
|
s.logger.Warn().Msg("Opus configuration update ignored - not running in audio server subprocess")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if audio output streaming is currently active
|
||||||
|
if atomic.LoadInt32(&outputStreamingRunning) == 0 {
|
||||||
|
s.logger.Info().Msg("Audio output streaming not active, configuration will be applied when streaming starts")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure capture is initialized before updating encoder parameters
|
||||||
|
// The C function requires both encoder and capture_initialized to be true
|
||||||
|
if err := cgoAudioInit(); err != nil {
|
||||||
|
s.logger.Debug().Err(err).Msg("Audio capture already initialized or initialization failed")
|
||||||
|
// Continue anyway - capture may already be initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply configuration using CGO function (only if audio system is running)
|
||||||
|
vbrConstraint := Config.CGOOpusVBRConstraint
|
||||||
|
if err := updateOpusEncoderParams(config.Bitrate, config.Complexity, config.VBR, vbrConstraint, config.SignalType, config.Bandwidth, config.DTX); err != nil {
|
||||||
|
s.logger.Error().Err(err).Msg("Failed to update Opus encoder parameters - encoder may not be initialized")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info().Msg("Opus encoder parameters updated successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// SendFrame sends an audio frame to the client
|
// SendFrame sends an audio frame to the client
|
||||||
func (s *AudioOutputServer) SendFrame(frame []byte) error {
|
func (s *AudioOutputServer) SendFrame(frame []byte) error {
|
||||||
s.mtx.Lock()
|
s.mtx.Lock()
|
||||||
|
@ -320,6 +442,53 @@ func (c *AudioOutputClient) ReceiveFrame() ([]byte, error) {
|
||||||
return frame, nil
|
return frame, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SendOpusConfig sends Opus configuration to the audio output server
|
||||||
|
func (c *AudioOutputClient) SendOpusConfig(config OutputIPCOpusConfig) error {
|
||||||
|
c.mtx.Lock()
|
||||||
|
defer c.mtx.Unlock()
|
||||||
|
|
||||||
|
if !c.running || c.conn == nil {
|
||||||
|
return fmt.Errorf("not connected to audio output server")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate configuration parameters
|
||||||
|
if config.SampleRate <= 0 || config.Channels <= 0 || config.FrameSize <= 0 || config.Bitrate <= 0 {
|
||||||
|
return fmt.Errorf("invalid Opus configuration: SampleRate=%d, Channels=%d, FrameSize=%d, Bitrate=%d",
|
||||||
|
config.SampleRate, config.Channels, config.FrameSize, config.Bitrate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize Opus configuration using common function
|
||||||
|
data := EncodeOpusConfig(config.SampleRate, config.Channels, config.FrameSize, config.Bitrate, config.Complexity, config.VBR, config.SignalType, config.Bandwidth, config.DTX)
|
||||||
|
|
||||||
|
msg := &OutputIPCMessage{
|
||||||
|
Magic: c.magicNumber,
|
||||||
|
Type: OutputMessageTypeOpusConfig,
|
||||||
|
Length: uint32(len(data)),
|
||||||
|
Timestamp: time.Now().UnixNano(),
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.writeMessage(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeMessage writes a message to the connection
|
||||||
|
func (c *AudioOutputClient) writeMessage(msg *OutputIPCMessage) error {
|
||||||
|
header := EncodeMessageHeader(msg.Magic, uint8(msg.Type), msg.Length, msg.Timestamp)
|
||||||
|
|
||||||
|
if _, err := c.conn.Write(header); err != nil {
|
||||||
|
return fmt.Errorf("failed to write header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Length > 0 && msg.Data != nil {
|
||||||
|
if _, err := c.conn.Write(msg.Data); err != nil {
|
||||||
|
return fmt.Errorf("failed to write data: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
atomic.AddInt64(&c.totalFrames, 1)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetClientStats returns client performance statistics
|
// GetClientStats returns client performance statistics
|
||||||
func (c *AudioOutputClient) GetClientStats() (total, dropped int64) {
|
func (c *AudioOutputClient) GetClientStats() (total, dropped int64) {
|
||||||
stats := GetFrameStats(&c.totalFrames, &c.droppedFrames)
|
stats := GetFrameStats(&c.totalFrames, &c.droppedFrames)
|
||||||
|
|
|
@ -125,6 +125,12 @@ func (s *AudioOutputSupervisor) Start() error {
|
||||||
// Start the supervision loop
|
// Start the supervision loop
|
||||||
go s.supervisionLoop()
|
go s.supervisionLoop()
|
||||||
|
|
||||||
|
// Establish IPC connection to subprocess after a brief delay
|
||||||
|
go func() {
|
||||||
|
time.Sleep(500 * time.Millisecond) // Wait for subprocess to start
|
||||||
|
s.connectClient()
|
||||||
|
}()
|
||||||
|
|
||||||
s.logger.Info().Str("component", AudioOutputSupervisorComponent).Msg("component started successfully")
|
s.logger.Info().Str("component", AudioOutputSupervisorComponent).Msg("component started successfully")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -274,3 +280,43 @@ func (s *AudioOutputSupervisor) calculateRestartDelay() time.Duration {
|
||||||
|
|
||||||
return delay
|
return delay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// client holds the IPC client for communicating with the subprocess
|
||||||
|
var outputClient *AudioOutputClient
|
||||||
|
|
||||||
|
// IsConnected returns whether the supervisor has an active connection to the subprocess
|
||||||
|
func (s *AudioOutputSupervisor) IsConnected() bool {
|
||||||
|
return outputClient != nil && outputClient.IsConnected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClient returns the IPC client for the subprocess
|
||||||
|
func (s *AudioOutputSupervisor) GetClient() *AudioOutputClient {
|
||||||
|
return outputClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectClient establishes connection to the audio output subprocess
|
||||||
|
func (s *AudioOutputSupervisor) connectClient() {
|
||||||
|
if outputClient == nil {
|
||||||
|
outputClient = NewAudioOutputClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to connect to the subprocess
|
||||||
|
if err := outputClient.Connect(); err != nil {
|
||||||
|
s.logger.Warn().Err(err).Msg("Failed to connect to audio output subprocess")
|
||||||
|
} else {
|
||||||
|
s.logger.Info().Msg("Connected to audio output subprocess")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendOpusConfig sends Opus configuration to the audio output subprocess
|
||||||
|
func (s *AudioOutputSupervisor) SendOpusConfig(config OutputIPCOpusConfig) error {
|
||||||
|
if outputClient == nil {
|
||||||
|
return fmt.Errorf("client not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !outputClient.IsConnected() {
|
||||||
|
return fmt.Errorf("client not connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputClient.SendOpusConfig(config)
|
||||||
|
}
|
||||||
|
|
|
@ -211,33 +211,46 @@ func SetAudioQuality(quality AudioQuality) {
|
||||||
// Set new OPUS configuration for future restarts
|
// Set new OPUS configuration for future restarts
|
||||||
if supervisor := GetAudioOutputSupervisor(); supervisor != nil {
|
if supervisor := GetAudioOutputSupervisor(); supervisor != nil {
|
||||||
supervisor.SetOpusConfig(config.Bitrate*1000, complexity, vbr, signalType, bandwidth, dtx)
|
supervisor.SetOpusConfig(config.Bitrate*1000, complexity, vbr, signalType, bandwidth, dtx)
|
||||||
}
|
|
||||||
|
|
||||||
// Send dynamic configuration update to running audio output
|
// Send dynamic configuration update to running subprocess via IPC
|
||||||
vbrConstraint := Config.CGOOpusVBRConstraint
|
if supervisor.IsConnected() {
|
||||||
if err := updateOpusEncoderParams(config.Bitrate*1000, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx); err != nil {
|
// Convert AudioConfig to OutputIPCOpusConfig with complete Opus parameters
|
||||||
logger.Warn().Err(err).Msg("failed to update OPUS encoder parameters dynamically")
|
opusConfig := OutputIPCOpusConfig{
|
||||||
// Fallback to subprocess restart if dynamic update fails
|
SampleRate: config.SampleRate,
|
||||||
if supervisor := GetAudioOutputSupervisor(); supervisor != nil {
|
Channels: config.Channels,
|
||||||
logger.Info().Msg("falling back to subprocess restart")
|
FrameSize: int(config.FrameSize.Milliseconds() * int64(config.SampleRate) / 1000), // Convert ms to samples
|
||||||
supervisor.Stop()
|
Bitrate: config.Bitrate * 1000, // Convert kbps to bps
|
||||||
if err := supervisor.Start(); err != nil {
|
Complexity: complexity,
|
||||||
logger.Error().Err(err).Msg("failed to restart audio output subprocess after dynamic update failure")
|
VBR: vbr,
|
||||||
|
SignalType: signalType,
|
||||||
|
Bandwidth: bandwidth,
|
||||||
|
DTX: dtx,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.Info().Msg("audio output quality updated dynamically")
|
|
||||||
|
|
||||||
// Reset audio output stats after config update
|
logger.Info().Interface("opusConfig", opusConfig).Msg("sending Opus configuration to audio output subprocess")
|
||||||
// Allow adaptive buffer manager to naturally adjust buffer sizes
|
if err := supervisor.SendOpusConfig(opusConfig); err != nil {
|
||||||
go func() {
|
logger.Warn().Err(err).Msg("failed to send dynamic Opus config update via IPC, falling back to subprocess restart")
|
||||||
time.Sleep(Config.QualityChangeSettleDelay) // Wait for quality change to settle
|
// Fallback to subprocess restart if IPC update fails
|
||||||
// Reset audio input server stats to clear persistent warnings
|
supervisor.Stop()
|
||||||
ResetGlobalAudioInputServerStats()
|
if err := supervisor.Start(); err != nil {
|
||||||
// Attempt recovery if there are still issues
|
logger.Error().Err(err).Msg("failed to restart audio output subprocess after IPC update failure")
|
||||||
time.Sleep(1 * time.Second)
|
}
|
||||||
RecoverGlobalAudioInputServer()
|
} else {
|
||||||
}()
|
logger.Info().Msg("audio output quality updated dynamically via IPC")
|
||||||
|
|
||||||
|
// Reset audio output stats after config update
|
||||||
|
go func() {
|
||||||
|
time.Sleep(Config.QualityChangeSettleDelay) // Wait for quality change to settle
|
||||||
|
// Reset audio input server stats to clear persistent warnings
|
||||||
|
ResetGlobalAudioInputServerStats()
|
||||||
|
// Attempt recovery if there are still issues
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
RecoverGlobalAudioInputServer()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.Info().Bool("supervisor_running", supervisor.IsRunning()).Msg("audio output subprocess not connected, configuration will apply on next start")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue