package playback import ( "context" "errors" "fmt" ) var ( ErrAudioFactoryRequired = errors.New("audio reader factory is required") ErrAudioSinkRequired = errors.New("audio sink is required") ErrAudioRetryDeciderRequired = errors.New("audio decider is required") ErrAudioFeedInactive = errors.New("audio feed is not active") ) type AudioWorker struct { factory AudioReaderFactory sink AudioSink retry RetryPolicy shouldRetry retryDecider observer StatusObserver wait waitFunc } func NewAudioWorker( factory AudioReaderFactory, sink AudioSink, retry RetryPolicy, shouldRetry func(error) bool, observer StatusObserver, ) (*AudioWorker, error) { if factory == nil { return nil, ErrAudioFactoryRequired } if sink == nil { return nil, ErrAudioSinkRequired } if shouldRetry == nil { return nil, ErrAudioRetryDeciderRequired } if err := retry.Validate(); err != nil { return nil, fmt.Errorf("validate audio retry policy: %w", err) } return &AudioWorker{ factory: factory, sink: sink, retry: retry, shouldRetry: shouldRetry, observer: observer, wait: waitForRetry, }, nil } type stabilityAudioSink struct { sink AudioSink onStable func() stable bool } func (s *stabilityAudioSink) ConsumeAudio( ctx context.Context, frame AudioFrame, ) error { err := s.sink.ConsumeAudio(ctx, frame) if err == nil && !s.stable { s.stable = true if s.onStable != nil { s.onStable() } } return err } func (w *AudioWorker) emit(ctx context.Context, status Status) { status.Generation = generationFromContext(ctx) if w.observer != nil { w.observer(status) } } func (w *AudioWorker) Run( ctx context.Context, config FeedConfig, ) error { if err := config.Validate(); err != nil { return fmt.Errorf("validate audio config: %w", err) } if !config.Active { return ErrAudioFeedInactive } attemptNumber := 0 var latestRetry retryEvent attempt := func(ctx context.Context) (bool, error) { attemptNumber++ state := StateConnecting if attemptNumber > 1 { state = StateReconnecting } w.emit(ctx, Status{ Unit: UnitAudio, State: state, Attempt: attemptNumber, }) attemptSink := &stabilityAudioSink{ sink: w.sink, onStable: func() { w.emit(ctx, Status{ Unit: UnitAudio, State: StatePlaying, Attempt: attemptNumber, }) }, } err := runAudioAttempt(ctx, w.factory, attemptSink, config) return attemptSink.stable, err } decide := func(err error) bool { var sinkErr *audioSinkError if errors.As(err, &sinkErr) { return false } return w.shouldRetry(err) } observeRetry := func(event retryEvent) { latestRetry = event if !event.WillRetry { return } w.emit(ctx, Status{ Unit: UnitAudio, State: StateReconnecting, Attempt: attemptNumber + 1, FailedAttempts: event.FailedAttempts, RetryIn: event.RetryIn, Err: event.Err, }) } err := runWithRetry( ctx, w.retry, attempt, decide, w.wait, observeRetry, ) if ctx.Err() != nil { w.emit(ctx, Status{ Unit: UnitAudio, State: StateStopping, }) w.emit(ctx, Status{ Unit: UnitAudio, State: StateIdle, }) return ctx.Err() } if err != nil { w.emit(ctx, Status{ Unit: UnitAudio, State: StateFailed, Attempt: attemptNumber, FailedAttempts: latestRetry.FailedAttempts, Err: err, }) return err } w.emit(ctx, Status{ Unit: UnitAudio, State: StateIdle, }) return nil }