Initial flame_lua_runtime package
This commit is contained in:
90
lib/runtime/audio/runtime_audio_cache.dart
Normal file
90
lib/runtime/audio/runtime_audio_cache.dart
Normal file
@@ -0,0 +1,90 @@
|
||||
part of 'runtime_audio_manager.dart';
|
||||
|
||||
extension _RuntimeAudioManagerCache on RuntimeAudioManager {
|
||||
void _releaseCachedAudio() {
|
||||
_loadLimiter.clearPending();
|
||||
for (final path in _audios.keys.toList(growable: false)) {
|
||||
_removeAudioRecord(path);
|
||||
}
|
||||
|
||||
_channels.clear();
|
||||
final players = _players.toList(growable: false);
|
||||
_players.clear();
|
||||
final pooledPlayers = _sfxPool.toList(growable: false);
|
||||
_sfxPool.clear();
|
||||
for (final player in [...players, ...pooledPlayers]) {
|
||||
player.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
RuntimeAudioPlayer _takeSfxPlayer() {
|
||||
if (_sfxPool.isNotEmpty) {
|
||||
return _sfxPool.removeLast();
|
||||
}
|
||||
return _playerFactory();
|
||||
}
|
||||
|
||||
Future<void> _releaseSfxPlayer(RuntimeAudioPlayer player) async {
|
||||
if (_disposed || _sfxPool.length >= _maxSfxPoolSize) {
|
||||
await player.dispose();
|
||||
return;
|
||||
}
|
||||
_sfxPool.add(player);
|
||||
}
|
||||
|
||||
void _touch(_AudioResourceRecord record) {
|
||||
record.lastUsed = ++_accessCounter;
|
||||
}
|
||||
|
||||
void _enforceAudioBudget() {
|
||||
while (_isOverBudget()) {
|
||||
final victim = _leastRecentlyUsedAudio();
|
||||
if (victim == null || !_removeAudioRecord(victim)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool _isOverBudget() {
|
||||
final maxBytes = _maxCacheBytes;
|
||||
final maxEntries = _maxCacheEntries;
|
||||
return (maxBytes != null && _cacheBytes > maxBytes) ||
|
||||
(maxEntries != null && _readyAudioCount > maxEntries);
|
||||
}
|
||||
|
||||
int get _readyAudioCount => _audios.values
|
||||
.where((record) => record.state == GameResourceState.ready)
|
||||
.length;
|
||||
|
||||
String? _leastRecentlyUsedAudio() {
|
||||
String? victimPath;
|
||||
_AudioResourceRecord? victim;
|
||||
for (final entry in _audios.entries) {
|
||||
final record = entry.value;
|
||||
if (record.state != GameResourceState.ready) {
|
||||
continue;
|
||||
}
|
||||
if (victim == null || record.lastUsed < victim.lastUsed) {
|
||||
victim = record;
|
||||
victimPath = entry.key;
|
||||
}
|
||||
}
|
||||
return victimPath;
|
||||
}
|
||||
|
||||
bool _removeAudioRecord(String path) {
|
||||
final record = _audios.remove(path);
|
||||
if (record == null) {
|
||||
return false;
|
||||
}
|
||||
_cacheBytes -= record.bytes?.length ?? 0;
|
||||
if (_cacheBytes < 0) {
|
||||
_cacheBytes = 0;
|
||||
}
|
||||
record
|
||||
..state = GameResourceState.disposed
|
||||
..bytes = null
|
||||
..inflight = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
27
lib/runtime/audio/runtime_audio_debug.dart
Normal file
27
lib/runtime/audio/runtime_audio_debug.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
part of 'runtime_audio_manager.dart';
|
||||
|
||||
extension _RuntimeAudioManagerDebug on RuntimeAudioManager {
|
||||
Map<String, Object?> _audioRecordDebugJson({
|
||||
required String? key,
|
||||
required String path,
|
||||
required String? preload,
|
||||
required bool declared,
|
||||
}) {
|
||||
final record = _audios[path];
|
||||
return {
|
||||
if (key != null) 'key': key,
|
||||
'path': path,
|
||||
'type': GameResourceType.audio,
|
||||
'declared': declared,
|
||||
if (preload != null) 'preload': preload,
|
||||
if (key != null && _package?.manifest.resources[key]?.group != null)
|
||||
'group': _package?.manifest.resources[key]?.group,
|
||||
'state': (record?.state ?? GameResourceState.idle).name,
|
||||
if (record != null) 'generation': record.generation,
|
||||
'loading': record?.inflight != null,
|
||||
'ready': record?.bytes != null,
|
||||
if (record?.bytes != null) 'bytes': record!.bytes!.length,
|
||||
if (record?.lastError != null) 'error': record!.lastError.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
124
lib/runtime/audio/runtime_audio_loading.dart
Normal file
124
lib/runtime/audio/runtime_audio_loading.dart
Normal file
@@ -0,0 +1,124 @@
|
||||
part of 'runtime_audio_manager.dart';
|
||||
|
||||
extension _RuntimeAudioManagerLoading on RuntimeAudioManager {
|
||||
Future<Uint8List?> _loadAudio(
|
||||
String? keyOrPath, {
|
||||
required bool failOnError,
|
||||
}) {
|
||||
if (keyOrPath == null || keyOrPath.isEmpty) {
|
||||
return Future.value(null);
|
||||
}
|
||||
|
||||
final requestToken = _asyncGate.token;
|
||||
final requestGeneration = requestToken.generation;
|
||||
final path = _tryResolve(keyOrPath);
|
||||
if (path == null) {
|
||||
return Future.value(null);
|
||||
}
|
||||
|
||||
final existing = _audios[path];
|
||||
if (existing != null) {
|
||||
final bytes = existing.bytes;
|
||||
if (existing.generation == requestGeneration &&
|
||||
existing.state == GameResourceState.ready &&
|
||||
bytes != null) {
|
||||
_touch(existing);
|
||||
return Future.value(bytes);
|
||||
}
|
||||
final inflight = existing.inflight;
|
||||
if (existing.generation == requestGeneration && inflight != null) {
|
||||
return failOnError
|
||||
? _throwIfNull(inflight, keyOrPath)
|
||||
: inflight.catchError((_) => null);
|
||||
}
|
||||
}
|
||||
|
||||
final record = _AudioResourceRecord(generation: requestGeneration)
|
||||
..state = GameResourceState.loading;
|
||||
_audios[path] = record;
|
||||
|
||||
final future = _readAudio(path, record, requestToken);
|
||||
record.inflight = future;
|
||||
return failOnError ? _throwIfNull(future, keyOrPath) : future;
|
||||
}
|
||||
|
||||
Future<Uint8List?> _throwIfNull(
|
||||
Future<Uint8List?> future,
|
||||
String keyOrPath,
|
||||
) async {
|
||||
final bytes = await future;
|
||||
if (bytes == null) {
|
||||
throw ResourceLoadException('Required audio resource failed: $keyOrPath');
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
Future<Uint8List?> _readAudio(
|
||||
String path,
|
||||
_AudioResourceRecord record,
|
||||
RuntimeAsyncToken requestToken,
|
||||
) async {
|
||||
try {
|
||||
final activePackage = _package;
|
||||
if (activePackage == null) {
|
||||
throw StateError('RuntimeAudioManager has no active package');
|
||||
}
|
||||
|
||||
final ownedBytes = await _loadLimiter.run(() async {
|
||||
final data = await activePackage.readBytes(path);
|
||||
final bytes = data.buffer.asUint8List(
|
||||
data.offsetInBytes,
|
||||
data.lengthInBytes,
|
||||
);
|
||||
return Uint8List.fromList(bytes);
|
||||
});
|
||||
record.inflight = null;
|
||||
|
||||
if (_disposed ||
|
||||
!_asyncGate.accepts(requestToken) ||
|
||||
_audios[path] != record) {
|
||||
record.state = GameResourceState.disposed;
|
||||
return null;
|
||||
}
|
||||
|
||||
record
|
||||
..bytes = ownedBytes
|
||||
..state = GameResourceState.ready
|
||||
..lastError = null;
|
||||
_cacheBytes += ownedBytes.length;
|
||||
_touch(record);
|
||||
_enforceAudioBudget();
|
||||
return ownedBytes;
|
||||
} catch (error) {
|
||||
record.inflight = null;
|
||||
if (_disposed ||
|
||||
!_asyncGate.accepts(requestToken) ||
|
||||
_audios[path] != record) {
|
||||
record.state = GameResourceState.disposed;
|
||||
return null;
|
||||
}
|
||||
record
|
||||
..state = GameResourceState.failed
|
||||
..lastError = error;
|
||||
_diagnostics?.record(
|
||||
type: RuntimeDiagnosticType.resourceLoadError,
|
||||
message: 'Audio resource failed to load',
|
||||
error: error,
|
||||
context: {'path': path, 'generation': requestToken.generation},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
String? _tryResolve(String keyOrPath) {
|
||||
try {
|
||||
final activePackage = _package;
|
||||
if (activePackage == null) {
|
||||
return null;
|
||||
}
|
||||
return activePackage.resolveResourcePath(keyOrPath);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
352
lib/runtime/audio/runtime_audio_manager.dart
Normal file
352
lib/runtime/audio/runtime_audio_manager.dart
Normal file
@@ -0,0 +1,352 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../diagnostics/runtime_diagnostics.dart';
|
||||
import '../lifecycle/runtime_async_gate.dart';
|
||||
import '../packages/game_package.dart';
|
||||
import '../packages/game_package_manifest.dart';
|
||||
import '../resources/game_resource_manager.dart';
|
||||
import '../resources/resource_load_limiter.dart';
|
||||
import 'runtime_audio_player.dart';
|
||||
|
||||
// These part files only group RuntimeAudioManager private helpers. The public
|
||||
// facade stays in RuntimeAudioManager so callers do not depend on extensions.
|
||||
part 'runtime_audio_loading.dart';
|
||||
part 'runtime_audio_debug.dart';
|
||||
part 'runtime_audio_cache.dart';
|
||||
|
||||
class RuntimeAudioManager {
|
||||
RuntimeAudioManager({
|
||||
RuntimeDiagnostics? diagnostics,
|
||||
RuntimeAudioPlayer Function()? playerFactory,
|
||||
int maxSfxPoolSize = 8,
|
||||
int? maxCacheBytes,
|
||||
int? maxCacheEntries,
|
||||
int maxConcurrentLoads = 4,
|
||||
}) : _diagnostics = diagnostics,
|
||||
_playerFactory = playerFactory ?? AudioplayersRuntimeAudioPlayer.new,
|
||||
_maxSfxPoolSize = maxSfxPoolSize,
|
||||
_maxCacheBytes = maxCacheBytes,
|
||||
_maxCacheEntries = maxCacheEntries,
|
||||
_loadLimiter = ResourceLoadLimiter(maxConcurrentLoads);
|
||||
|
||||
final RuntimeDiagnostics? _diagnostics;
|
||||
final RuntimeAudioPlayer Function() _playerFactory;
|
||||
final int _maxSfxPoolSize;
|
||||
final int? _maxCacheBytes;
|
||||
final int? _maxCacheEntries;
|
||||
final ResourceLoadLimiter _loadLimiter;
|
||||
final RuntimeAsyncGate _asyncGate = RuntimeAsyncGate(initiallyClosed: true);
|
||||
final Map<String, _AudioResourceRecord> _audios = {};
|
||||
final Set<RuntimeAudioPlayer> _players = {};
|
||||
final Map<String, RuntimeAudioPlayback> _channels = {};
|
||||
final List<RuntimeAudioPlayer> _sfxPool = [];
|
||||
GamePackage? _package;
|
||||
int _cacheBytes = 0;
|
||||
int _accessCounter = 0;
|
||||
bool _disposed = false;
|
||||
|
||||
int get generation => _asyncGate.generation;
|
||||
|
||||
bool get hasPackage => _package != null;
|
||||
|
||||
Future<void> mount(GamePackage package) async {
|
||||
_releaseCachedAudio();
|
||||
_asyncGate.activate();
|
||||
_disposed = false;
|
||||
_package = package;
|
||||
await preloadDeclaredAudio(package.manifest);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
_asyncGate.close();
|
||||
_releaseCachedAudio();
|
||||
_package = null;
|
||||
}
|
||||
|
||||
GameResourceState audioState(String keyOrPath) {
|
||||
final path = _tryResolve(keyOrPath);
|
||||
if (path == null) {
|
||||
return GameResourceState.failed;
|
||||
}
|
||||
return _audios[path]?.state ?? GameResourceState.idle;
|
||||
}
|
||||
|
||||
Object? audioError(String keyOrPath) {
|
||||
final path = _tryResolve(keyOrPath);
|
||||
if (path == null) {
|
||||
return StateError('RuntimeAudioManager has no active package');
|
||||
}
|
||||
return _audios[path]?.lastError;
|
||||
}
|
||||
|
||||
Map<String, Object?> audioDebugJson() {
|
||||
final activePackage = _package;
|
||||
final declaredPaths = <String>{};
|
||||
final resources = <Map<String, Object?>>[];
|
||||
|
||||
if (activePackage != null) {
|
||||
for (final entry in activePackage.manifest.resources.entries) {
|
||||
final resource = entry.value;
|
||||
if (resource.type != GameResourceType.audio) {
|
||||
continue;
|
||||
}
|
||||
final path = activePackage.resolveResourcePath(entry.key);
|
||||
declaredPaths.add(path);
|
||||
resources.add(
|
||||
_audioRecordDebugJson(
|
||||
key: entry.key,
|
||||
path: path,
|
||||
preload: resource.preload,
|
||||
declared: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (final path in _audios.keys) {
|
||||
if (declaredPaths.contains(path)) {
|
||||
continue;
|
||||
}
|
||||
resources.add(
|
||||
_audioRecordDebugJson(
|
||||
key: null,
|
||||
path: path,
|
||||
preload: null,
|
||||
declared: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
'generation': generation,
|
||||
'hasPackage': activePackage != null,
|
||||
'count': resources.length,
|
||||
'activeLoads': _loadLimiter.activeCount,
|
||||
'pendingLoads': _loadLimiter.pendingCount,
|
||||
'activePlayers': _players.length,
|
||||
'pooledPlayers': _sfxPool.length,
|
||||
'channels': _channels.keys.toList(growable: false)..sort(),
|
||||
'resources': resources,
|
||||
};
|
||||
}
|
||||
|
||||
bool evictAudio(String keyOrPath) {
|
||||
final path = _tryResolve(keyOrPath);
|
||||
if (path == null) {
|
||||
return false;
|
||||
}
|
||||
return _removeAudioRecord(path);
|
||||
}
|
||||
|
||||
Future<bool> retryAudio(String keyOrPath) async {
|
||||
evictAudio(keyOrPath);
|
||||
final bytes = await _loadAudio(keyOrPath, failOnError: false);
|
||||
return bytes != null;
|
||||
}
|
||||
|
||||
Future<void> preloadDeclaredAudio(GamePackageManifest manifest) async {
|
||||
final futures = <Future<void>>[];
|
||||
for (final entry in manifest.resources.entries) {
|
||||
final resource = entry.value;
|
||||
if (resource.type != GameResourceType.audio ||
|
||||
resource.preload == GameResourcePreload.lazy) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final failOnError = resource.preload == GameResourcePreload.required;
|
||||
futures.add(_loadAudio(entry.key, failOnError: failOnError).then((_) {}));
|
||||
}
|
||||
await Future.wait(futures);
|
||||
}
|
||||
|
||||
Future<RuntimeAudioPlayback?> play(
|
||||
String? keyOrPath, {
|
||||
double volume = 1,
|
||||
}) async {
|
||||
if (_disposed) {
|
||||
return null;
|
||||
}
|
||||
final bytes = await _loadAudio(keyOrPath, failOnError: false);
|
||||
if (_disposed || bytes == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final player = _takeSfxPlayer();
|
||||
_players.add(player);
|
||||
try {
|
||||
await player.start(bytes, volume: volume);
|
||||
} catch (error) {
|
||||
_players.remove(player);
|
||||
await player.dispose();
|
||||
_diagnostics?.record(
|
||||
type: RuntimeDiagnosticType.resourceLoadError,
|
||||
message: 'Audio resource failed to play',
|
||||
error: error,
|
||||
context: {'resource': keyOrPath, 'generation': generation},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final playback = RuntimeAudioPlayback._(player, player.done);
|
||||
playback.done.whenComplete(() async {
|
||||
_players.remove(player);
|
||||
if (playback.isCancelled) {
|
||||
await player.dispose();
|
||||
return;
|
||||
}
|
||||
await _releaseSfxPlayer(player);
|
||||
});
|
||||
return playback;
|
||||
}
|
||||
|
||||
Future<RuntimeAudioPlayback?> playBgm(
|
||||
String? keyOrPath, {
|
||||
String channel = RuntimeAudioChannel.defaultBgm,
|
||||
double volume = 1,
|
||||
bool loop = true,
|
||||
}) async {
|
||||
if (_disposed) {
|
||||
return null;
|
||||
}
|
||||
final bytes = await _loadAudio(keyOrPath, failOnError: false);
|
||||
if (_disposed || bytes == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await stopBgm(channel: channel);
|
||||
if (_disposed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final player = _playerFactory();
|
||||
_players.add(player);
|
||||
try {
|
||||
await player.start(bytes, volume: volume, loop: loop);
|
||||
} catch (error) {
|
||||
_players.remove(player);
|
||||
await player.dispose();
|
||||
_diagnostics?.record(
|
||||
type: RuntimeDiagnosticType.resourceLoadError,
|
||||
message: 'BGM resource failed to play',
|
||||
error: error,
|
||||
context: {
|
||||
'resource': keyOrPath,
|
||||
'channel': channel,
|
||||
'generation': generation,
|
||||
},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final playback = RuntimeAudioPlayback._(player, player.done);
|
||||
_channels[channel] = playback;
|
||||
playback.done.whenComplete(() async {
|
||||
if (_channels[channel] == playback) {
|
||||
_channels.remove(channel);
|
||||
}
|
||||
_players.remove(player);
|
||||
await player.dispose();
|
||||
});
|
||||
return playback;
|
||||
}
|
||||
|
||||
Future<void> pauseBgm({String channel = RuntimeAudioChannel.defaultBgm}) {
|
||||
return _channels[channel]?.pause() ?? Future.value();
|
||||
}
|
||||
|
||||
Future<void> resumeBgm({String channel = RuntimeAudioChannel.defaultBgm}) {
|
||||
return _channels[channel]?.resume() ?? Future.value();
|
||||
}
|
||||
|
||||
Future<void> stopBgm({
|
||||
String channel = RuntimeAudioChannel.defaultBgm,
|
||||
}) async {
|
||||
final playback = _channels.remove(channel);
|
||||
await playback?.stop();
|
||||
}
|
||||
|
||||
bool hasBgm({String channel = RuntimeAudioChannel.defaultBgm}) {
|
||||
return _channels.containsKey(channel);
|
||||
}
|
||||
|
||||
Future<void> preloadGroup(String group, {bool failOnError = false}) async {
|
||||
final activePackage = _package;
|
||||
if (activePackage == null) {
|
||||
throw StateError('RuntimeAudioManager has no active package');
|
||||
}
|
||||
final futures = <Future<void>>[];
|
||||
for (final entry in activePackage.manifest.resources.entries) {
|
||||
final resource = entry.value;
|
||||
if (resource.type == GameResourceType.audio && resource.group == group) {
|
||||
futures.add(
|
||||
_loadAudio(entry.key, failOnError: failOnError).then((_) {}),
|
||||
);
|
||||
}
|
||||
}
|
||||
await Future.wait(futures);
|
||||
}
|
||||
|
||||
int evictGroup(String group) {
|
||||
final activePackage = _package;
|
||||
if (activePackage == null) {
|
||||
return 0;
|
||||
}
|
||||
var count = 0;
|
||||
for (final entry in activePackage.manifest.resources.entries) {
|
||||
final resource = entry.value;
|
||||
if (resource.type != GameResourceType.audio || resource.group != group) {
|
||||
continue;
|
||||
}
|
||||
final path = activePackage.resolveResourcePath(entry.key);
|
||||
if (_removeAudioRecord(path)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
abstract final class RuntimeAudioChannel {
|
||||
static const defaultBgm = 'bgm';
|
||||
}
|
||||
|
||||
class RuntimeAudioPlayback {
|
||||
RuntimeAudioPlayback._(this._player, this.done);
|
||||
|
||||
final RuntimeAudioPlayer _player;
|
||||
bool _cancelled = false;
|
||||
|
||||
final Future<void> done;
|
||||
|
||||
bool get isCancelled => _cancelled;
|
||||
|
||||
Future<void> pause() {
|
||||
return _player.pause();
|
||||
}
|
||||
|
||||
Future<void> resume() {
|
||||
return _player.resume();
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
_cancelled = true;
|
||||
await _player.stop();
|
||||
}
|
||||
|
||||
Future<void> cancel() async {
|
||||
_cancelled = true;
|
||||
await _player.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _AudioResourceRecord {
|
||||
_AudioResourceRecord({required this.generation});
|
||||
|
||||
final int generation;
|
||||
GameResourceState state = GameResourceState.idle;
|
||||
Future<Uint8List?>? inflight;
|
||||
Uint8List? bytes;
|
||||
Object? lastError;
|
||||
int lastUsed = 0;
|
||||
}
|
||||
99
lib/runtime/audio/runtime_audio_player.dart
Normal file
99
lib/runtime/audio/runtime_audio_player.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'dart:async' as async;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
|
||||
abstract class RuntimeAudioPlayer {
|
||||
Future<void> start(
|
||||
Uint8List bytes, {
|
||||
required double volume,
|
||||
bool loop = false,
|
||||
});
|
||||
|
||||
Future<void> pause();
|
||||
|
||||
Future<void> resume();
|
||||
|
||||
Future<void> stop();
|
||||
|
||||
Future<void> get done;
|
||||
|
||||
Future<void> dispose();
|
||||
}
|
||||
|
||||
class AudioplayersRuntimeAudioPlayer implements RuntimeAudioPlayer {
|
||||
AudioplayersRuntimeAudioPlayer({AudioPlayer? player})
|
||||
: _player = player ?? AudioPlayer();
|
||||
|
||||
final AudioPlayer _player;
|
||||
async.Completer<void> _done = async.Completer<void>();
|
||||
async.StreamSubscription<void>? _completionSubscription;
|
||||
bool _disposed = false;
|
||||
|
||||
@override
|
||||
Future<void> get done => _done.future;
|
||||
|
||||
@override
|
||||
Future<void> start(
|
||||
Uint8List bytes, {
|
||||
required double volume,
|
||||
bool loop = false,
|
||||
}) async {
|
||||
if (_disposed) {
|
||||
return;
|
||||
}
|
||||
await _completionSubscription?.cancel();
|
||||
_completionSubscription = null;
|
||||
if (_done.isCompleted) {
|
||||
_done = async.Completer<void>();
|
||||
}
|
||||
_completionSubscription = _player.onPlayerComplete.listen((_) {
|
||||
_completeDone();
|
||||
});
|
||||
await _player.setReleaseMode(loop ? ReleaseMode.loop : ReleaseMode.release);
|
||||
await _player.play(BytesSource(bytes), volume: volume);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() async {
|
||||
if (_disposed) {
|
||||
return;
|
||||
}
|
||||
await _player.pause();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> resume() async {
|
||||
if (_disposed) {
|
||||
return;
|
||||
}
|
||||
await _player.resume();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
if (_disposed) {
|
||||
return;
|
||||
}
|
||||
await _player.stop();
|
||||
_completeDone();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
if (_disposed) {
|
||||
return;
|
||||
}
|
||||
_disposed = true;
|
||||
await _completionSubscription?.cancel();
|
||||
_completionSubscription = null;
|
||||
await _player.dispose();
|
||||
_completeDone();
|
||||
}
|
||||
|
||||
void _completeDone() {
|
||||
if (!_done.isCompleted) {
|
||||
_done.complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user