Skip to content

How to call the play method only once for streaming audio uint8List data? #1946

Description

@release-kang

Since my audio file data is returned by the interface as a uint8List (addStreamChunk method), I need to splice it, but the requirement is to play it immediately, so I set up a buffer to play it while receiving. However, now when there is a new audio clip, it can only be played by re-calling _audioPlayer.resume() in the _safePlay method. Otherwise, only adding _audioPlayer.setSourceBytes(audioData) will not play the newly added voice clip, which will cause playback to be stuck. So how can I modify it to ensure that I don't have to call resume every time a new clip is added?

`
import 'dart:async';
import 'dart:typed_data';
import 'package:audioplayers/audioplayers.dart' as audio;
import 'package:synchronized/synchronized.dart';

class AudioManager {
static AudioManager? _instance;
static final Lock _instanceLock = Lock();

factory AudioManager() {
return _instance ??= AudioManager._internal();
}

static Future get instance async {
return await _instanceLock.synchronized(() async {
_instance ??= AudioManager._internal();
return _instance!;
});
}

AudioManager._internal() {
_init();
}

final audio.AudioPlayer _audioPlayer = audio.AudioPlayer();
final Lock _playLock = Lock();
final List _audioBuffer = [];
bool _isStreamMode = false;
bool _isPlaying = false;
bool _isPaused = false;
Completer? _streamCompleter;
Timer? _streamCompletionTimer;
bool _streamFinished = false; // Flag to mark if stream has ended
bool _hasStartedPlaying = false; // Flag to mark if playback has started for quick start

static const int _minBufferSize = 16384; // 16KB - Reduced initial buffer to minimize stuttering
static const int _maxBufferSize = 32768; // 32KB - Reduced max buffer for better responsiveness
static const Duration _streamCompletionDelay = Duration(milliseconds: 200); // Further optimized delay
static const Duration _playerResetDelay = Duration(milliseconds: 30); // Further reduced reset delay
static const Duration _stateStabilizationDelay = Duration(milliseconds: 20); // Reduced state stabilization delay

// ========== Public Properties ==========
bool get isStreamMode => _isStreamMode;
bool get isPlaying => _isPlaying;
bool get isPaused => _isPaused;
int get bufferSize => _audioBuffer.length;
audio.PlayerState get playerState => _audioPlayer.state;
int get minBufferSize => _minBufferSize;
int get maxBufferSize => _maxBufferSize;

// ========== Initialization ==========
Future _init() async {
await _audioPlayer.setPlayerMode(audio.PlayerMode.mediaPlayer); // Use mediaPlayer mode for byte source
await _audioPlayer.setReleaseMode(audio.ReleaseMode.stop);
await _audioPlayer.setVolume(0.8); // Set appropriate volume to eliminate static noise
print('🎵 AudioManager initialized - MediaPlayer mode, Volume 0.8');
}

// ========== Public Methods ==========

/// Add audio stream chunk
Future addStreamChunk(Uint8List chunk) async {
await _playLock.synchronized(() async {
if (!_isStreamMode) {
await _startStreamPlaybackInternal();
}

  _audioBuffer.addAll(chunk);
  print(
      '📦 Added audio chunk: ${chunk.length} bytes, Total buffer: ${_audioBuffer.length} bytes');

  _startStreamCompletionTimer();

  // Quick start mechanism: Start playing immediately on first data with sufficient audio
  if (!_hasStartedPlaying && !_isPlaying && _audioBuffer.length >= 8192) { // 8KB quick start
    print('🚀 Quick start playback: ${_audioBuffer.length} bytes');
    _hasStartedPlaying = true;
    await _playBufferedAudio();
  }
  // Priority check: if max buffer is reached
  else if (!_isPlaying && _audioBuffer.length >= _maxBufferSize) {
    print('📊 Buffer reached maximum ($_maxBufferSize bytes), starting playback');
    await _playBufferedAudio();
  }
  // Secondary check: if min buffer is reached
  else if (!_isPlaying && _audioBuffer.length >= _minBufferSize) {
    print('📊 Buffer reached minimum ($_minBufferSize bytes), starting playback');
    await _playBufferedAudio();
  }
});

}

/// Mark stream playback as finished (no more data will be received)
Future finishStreamPlayback() async {
await _playLock.synchronized(() async {
if (!_isStreamMode || _streamFinished) return;

  print('🏁 Marking stream playback as finished, waiting for remaining audio to play...');
  _streamFinished = true;
  _streamCompletionTimer?.cancel();
  _streamCompletionTimer = null;

  // If not playing and buffer is empty, stop immediately
  if (!_isPlaying && _audioBuffer.isEmpty) {
    await _stopStreamPlaybackInternal();
    print('✅ Stream playback completed');
  }
  // If there is data but not playing, play the remaining data
  else if (!_isPlaying && _audioBuffer.isNotEmpty) {
    await _playBufferedAudio();
  }
  // If currently playing, wait for completion callback to handle remaining data
});

}

/// Pause playback
Future pause() async {
await _playLock.synchronized(() async {
if (_isPlaying && !_isPaused) {
await _audioPlayer.pause();
_isPaused = true;
print('⏸️ Audio paused');
}
});
}

/// Resume playback
Future resume() async {
await _playLock.synchronized(() async {
if (_isPlaying && _isPaused) {
await _audioPlayer.resume();
_isPaused = false;
print('▶️ Audio resumed');
}
});
}

/// Immediately stop current stream playback
Future forceStopStream() async {
await _playLock.synchronized(() async {
await _forceStopInternal();
});
}

/// Release resources
Future dispose() async {
await _playLock.synchronized(() async {
await _forceStopInternal();
await _audioPlayer.dispose();
print('🗑️ AudioManager resources released');
});
}

// ========== Internal Methods ==========

Future _startStreamPlaybackInternal() async {
if (_isStreamMode) {
print('⚠️ Detected ongoing stream playback, stopping old stream first');
await _forceStopInternal();
await Future.delayed(_playerResetDelay);
// State stabilization delay ensures old stream is fully stopped
await Future.delayed(_stateStabilizationDelay);
}

print('🎵 Starting new stream playback session');
_isStreamMode = true;
_isPlaying = false;
_isPaused = false;
_streamFinished = false;
_hasStartedPlaying = false; // Reset quick start flag
_audioBuffer.clear();
_streamCompleter = Completer<void>();

await _resetPlayer();
_startStreamCompletionTimer();

}

void _startStreamCompletionTimer() {
_streamCompletionTimer?.cancel();
_streamCompletionTimer = Timer(_streamCompletionDelay, () async {
await _playLock.synchronized(() async {
if (_isStreamMode && _audioBuffer.isNotEmpty && !_isPlaying) {
print('⏰ Stream completion timer triggered, playing remaining data (${_audioBuffer.length} bytes)');
await _playBufferedAudio();
}
});
});
}

Future _playBufferedAudio() async {
if (_isPlaying || _audioBuffer.isEmpty) return;

_isPlaying = true;
_isPaused = false;

try {
  final bytesToPlay = _audioBuffer.length > _maxBufferSize
      ? _audioBuffer.sublist(0, _maxBufferSize)
      : _audioBuffer;

  final audioData = Uint8List.fromList(bytesToPlay);

  if (_audioBuffer.length > _maxBufferSize) {
    _audioBuffer.removeRange(0, _maxBufferSize);
  } else {
    _audioBuffer.clear();
  }

  print(
      '▶️ Playing ${audioData.length} bytes, Remaining buffer: ${_audioBuffer.length} bytes');
  await _safePlay(audioData);
  _setupPlaybackCompletionListener();
} catch (e) {
  print('❌ Error playing audio: $e');
  await _handlePlaybackError();
}

}

void _setupPlaybackCompletionListener() {
audioPlayer.onPlayerComplete.first.then(() async {
await _playLock.synchronized(() async {
// State stabilization delay ensures state consistency
await Future.delayed(_stateStabilizationDelay);

    _isPlaying = false;
    _isPaused = false;

    if (_audioBuffer.isNotEmpty) {
      print('🔄 Playback completed, checking for new data: ${_audioBuffer.length} bytes pending');
      // Play next segment immediately to maintain smoothness
      await _playBufferedAudio();
    } else if (_streamFinished) {
      // All data played, finish stream playback
      await _stopStreamPlaybackInternal();
      print('✅ All audio data playback completed');
    }
  });
}).catchError((e) async {
  print('❌ Playback completion listener error: $e');
  await _playLock.synchronized(() async {
    _isPlaying = false;
    _isPaused = false;
    // State stabilization delay needed on error as well
    await Future.delayed(_stateStabilizationDelay);
  });
});

}

Future _safePlay(Uint8List audioData) async {
try {
// Ensure player state consistency
await _audioPlayer.stop();
await Future.delayed(_playerResetDelay);

  // State stabilization delay to eliminate static noise
  await Future.delayed(_stateStabilizationDelay);

  await _audioPlayer.setSourceBytes(audioData);

  // Ensure state stability again
  await Future.delayed(_stateStabilizationDelay);

  await _audioPlayer.resume();
  _isPaused = false;
} catch (e) {
  print('❌ Safe play failed: $e');
  await _handlePlaybackError();
  rethrow;
}

}

Future _stopStreamPlaybackInternal() async {
if (!_isStreamMode) return;

print('⏹️ Stopping stream playback');
_isStreamMode = false;
_isPlaying = false;
_isPaused = false;
_streamFinished = false;
_hasStartedPlaying = false; // Reset quick start flag
_audioBuffer.clear();

_streamCompletionTimer?.cancel();
_streamCompletionTimer = null;

try {
  await _audioPlayer.stop();
  // State stabilization delay ensures player is fully stopped
  await Future.delayed(_stateStabilizationDelay);
} catch (e) {
  print('Error stopping player: $e');
}

if (_streamCompleter != null && !_streamCompleter!.isCompleted) {
  _streamCompleter!.complete();
}
_streamCompleter = null;

}

Future _forceStopInternal() async {
print('🛑 Forcibly stopping current audio stream');
_isStreamMode = false;
_isPlaying = false;
_isPaused = false;
_streamFinished = false;
_hasStartedPlaying = false; // Reset quick start flag
_audioBuffer.clear();

_streamCompletionTimer?.cancel();
_streamCompletionTimer = null;

try {
  await _audioPlayer.stop();
  // State stabilization delay ensures force stop is fully effective
  await Future.delayed(_stateStabilizationDelay);
} catch (e) {
  print('Error force stopping player: $e');
}

if (_streamCompleter != null && !_streamCompleter!.isCompleted) {
  _streamCompleter!.complete();
}
_streamCompleter = null;

}

Future _resetPlayer() async {
try {
await _audioPlayer.stop();
await Future.delayed(_playerResetDelay);
// State stabilization delay ensures player is fully reset
await Future.delayed(_stateStabilizationDelay);
} catch (e) {
print('Error resetting player: $e');
}
}

Future _handlePlaybackError() async {
_isPlaying = false;
_isPaused = false;
try {
await _resetPlayer();
// State stabilization delay after error handling
await Future.delayed(_stateStabilizationDelay);
} catch (e) {
print('Failed to reset player: $e');
}
}

Future getAudioDuration(Uint8List audioData) async {
try {
await _audioPlayer.setSourceBytes(audioData);
final duration = await _audioPlayer.getDuration();
return duration != null ? duration.inMilliseconds / 1000.0 : 0;
} catch (e) {
print('Failed to get audio duration: $e');
return 0;
} finally {
await _audioPlayer.stop();
}
}
}
`

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions