Compare commits
2
Commits
v1.3.1
...
e16cf50f2c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e16cf50f2c | ||
|
|
8aa1186d29 |
No files matched your search
@@ -49,8 +49,6 @@ Future<void> main(List<String> args) async {
|
|||||||
await AppSettings.instance.ensureLoaded();
|
await AppSettings.instance.ensureLoaded();
|
||||||
// The composition root: every system, made once.
|
// The composition root: every system, made once.
|
||||||
final graph = AppGraph.build();
|
final graph = AppGraph.build();
|
||||||
// Sounds stay off after an audio-device crash (see SoundEngine.crashMark).
|
|
||||||
graph.sfx.engine.crashMark = '${dirs.support}${Platform.pathSeparator}audio-init.pending';
|
|
||||||
final ports = graph.ports;
|
final ports = graph.ports;
|
||||||
// Close button / Alt+F4: the orderly close ([shutDownApp]), so others
|
// Close button / Alt+F4: the orderly close ([shutDownApp]), so others
|
||||||
// see "left", your server shuts down and no player outlives the engine.
|
// see "left", your server shuts down and no player outlives the engine.
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter_soloud/flutter_soloud.dart';
|
import 'package:flutter_soloud/flutter_soloud.dart';
|
||||||
@@ -51,13 +50,6 @@ class SoundEngine {
|
|||||||
|
|
||||||
Future<bool>? _ready;
|
Future<bool>? _ready;
|
||||||
var _disposed = false;
|
var _disposed = false;
|
||||||
|
|
||||||
/// A file that exists only while the audio device is opening (set by the
|
|
||||||
/// app). Found at the next start, the last open took the app down (a
|
|
||||||
/// native crash in the driver or miniaudio, seen on Windows): sounds stay
|
|
||||||
/// off rather than crash every start, until [retry] (Settings' preview).
|
|
||||||
String? crashMark;
|
|
||||||
var _skipped = false;
|
|
||||||
/// Per slot: the loaded key and its sound (null: failed to load).
|
/// Per slot: the loaded key and its sound (null: failed to load).
|
||||||
final Map<String, (String, Future<Object?>)> _slots = {};
|
final Map<String, (String, Future<Object?>)> _slots = {};
|
||||||
/// Load names are unique: SoLoud hands back the old sound for a name it
|
/// Load names are unique: SoLoud hands back the old sound for a name it
|
||||||
@@ -69,43 +61,15 @@ class SoundEngine {
|
|||||||
|
|
||||||
Future<bool> _init() async {
|
Future<bool> _init() async {
|
||||||
if (_disposed) return false;
|
if (_disposed) return false;
|
||||||
final mark = crashMark == null ? null : File(crashMark!);
|
|
||||||
try {
|
|
||||||
if (mark != null && mark.existsSync()) {
|
|
||||||
_skipped = true;
|
|
||||||
_log.warning('no sounds: opening the audio device crashed the app last time '
|
|
||||||
'(${mark.path}); a sound preview in Settings tries again');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
mark?.writeAsStringSync('opening the audio device');
|
|
||||||
} catch (_) {}
|
|
||||||
_log.info('opening the audio device');
|
|
||||||
try {
|
try {
|
||||||
await _backend.init();
|
await _backend.init();
|
||||||
_log.info('audio device open');
|
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_log.warning('no sounds: audio init failed: $e');
|
_log.warning('no sounds: audio init failed: $e');
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
if (mark != null && mark.existsSync()) mark.deleteSync();
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens the device again after a crash mark kept sounds off (the user
|
|
||||||
/// asked for a sound). A real crash leaves the mark again.
|
|
||||||
void retry() {
|
|
||||||
if (!_skipped) return;
|
|
||||||
_skipped = false;
|
|
||||||
try {
|
|
||||||
final m = crashMark;
|
|
||||||
if (m != null) File(m).deleteSync();
|
|
||||||
} catch (_) {}
|
|
||||||
_ready = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Plays the sound of [slot] at [volume] (linear gain). [key] names what
|
/// Plays the sound of [slot] at [volume] (linear gain). [key] names what
|
||||||
/// [wav] renders: the same key reuses the loaded sound; another key, or
|
/// [wav] renders: the same key reuses the loaded sound; another key, or
|
||||||
/// [fresh], renders and loads it again.
|
/// [fresh], renders and loads it again.
|
||||||
|
|||||||
@@ -12,9 +12,14 @@ import 'ports/player_host.dart';
|
|||||||
// in a session player with a video output through the media pipeline,
|
// in a session player with a video output through the media pipeline,
|
||||||
// play a second and close the normal way ([shutDownApp]). Progress goes to
|
// play a second and close the normal way ([shutDownApp]). Progress goes to
|
||||||
// `STARSYNC_SMOKE_OUT` (one line per step: `start`, `opened <ms> render=<mode>`,
|
// `STARSYNC_SMOKE_OUT` (one line per step: `start`, `opened <ms> render=<mode>`,
|
||||||
// `closing`, or `skip <why>` / `fail <why>`), so the script can tell a
|
// `closing`, `quitting`, or `skip <why>` / `fail <why>`), so the script can
|
||||||
// runner that cannot show a window (skip) from a close that hangs or
|
// tell a runner that cannot show a window (skip) from a close that hangs or
|
||||||
// leaves a process behind (fail).
|
// leaves a process behind, or a process that ends on its own (fail). It
|
||||||
|
// stays up [smokeUptime] first: a start that dies by itself (the 1.3.0
|
||||||
|
// Windows exit timer) ends before `closing`.
|
||||||
|
|
||||||
|
/// How long the smoke run keeps the app up before closing it.
|
||||||
|
const smokeUptime = Duration(seconds: 10);
|
||||||
|
|
||||||
/// The smoke run's media, or null for a normal start.
|
/// The smoke run's media, or null for a normal start.
|
||||||
String? get smokeMedia {
|
String? get smokeMedia {
|
||||||
@@ -65,6 +70,9 @@ Future<void> runSmoke(AppGraph graph, String media) async {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
step('fail $e');
|
step('fail $e');
|
||||||
}
|
}
|
||||||
|
final wait = smokeUptime - sw.elapsed;
|
||||||
|
if (wait > Duration.zero) await Future.delayed(wait);
|
||||||
step('closing');
|
step('closing');
|
||||||
await shutDownApp();
|
// Reached only through the app's own close (not a kill on the way).
|
||||||
|
await shutDownApp(beforeQuit: () async => step('quitting'));
|
||||||
}
|
}
|
||||||
@@ -37,11 +37,8 @@ class SynthSfx implements SfxService {
|
|||||||
|
|
||||||
/// Plays [kind] even when it is switched off (Settings' preview), always
|
/// Plays [kind] even when it is switched off (Settings' preview), always
|
||||||
/// freshly rendered: an edited spec plays after a hot reload.
|
/// freshly rendered: an edited spec plays after a hot reload.
|
||||||
Future<void> preview(SfxKind kind, {int readyIndex = 1, SoundPreset? preset}) {
|
Future<void> preview(SfxKind kind, {int readyIndex = 1, SoundPreset? preset}) =>
|
||||||
// Asked for: try the device again even after a crash kept it closed.
|
_play(kind, readyIndex, preset ?? presetFor(kind), fresh: true);
|
||||||
engine.retry();
|
|
||||||
return _play(kind, readyIndex, preset ?? presetFor(kind), fresh: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// One slot per sound and ready step: a new preset replaces it.
|
/// One slot per sound and ready step: a new preset replaces it.
|
||||||
Future<void> _play(SfxKind kind, int readyIndex, SoundPreset preset, {bool fresh = false}) {
|
Future<void> _play(SfxKind kind, int readyIndex, SoundPreset preset, {bool fresh = false}) {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@@ -148,46 +147,4 @@ void main() {
|
|||||||
expect(b.deinits, 1);
|
expect(b.deinits, 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('crash mark', () {
|
|
||||||
late Directory dir;
|
|
||||||
setUp(() => dir = Directory.systemTemp.createTempSync('sound_mark'));
|
|
||||||
tearDown(() => dir.deleteSync(recursive: true));
|
|
||||||
|
|
||||||
test('exists only while the device opens', () async {
|
|
||||||
final mark = '${dir.path}/audio-init.pending';
|
|
||||||
var seen = false;
|
|
||||||
final b = _MarkBackend(() => seen = File(mark).existsSync());
|
|
||||||
final e = SoundEngine(backend: b)..crashMark = mark;
|
|
||||||
expect(await e.warmUp(), isTrue);
|
|
||||||
expect(seen, isTrue, reason: 'there while opening');
|
|
||||||
expect(File(mark).existsSync(), isFalse, reason: 'gone once open');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('left by a crash: sounds stay off, a preview tries again', () async {
|
|
||||||
final mark = '${dir.path}/audio-init.pending';
|
|
||||||
File(mark).writeAsStringSync('opening the audio device');
|
|
||||||
final b = _FakeBackend();
|
|
||||||
final e = SoundEngine(backend: b)..crashMark = mark;
|
|
||||||
await e.play('chat', 'k', _bytes, volume: 1);
|
|
||||||
expect(b.inits, 0, reason: 'never opened after a crash');
|
|
||||||
expect(b.plays, isEmpty);
|
|
||||||
e.retry();
|
|
||||||
await e.play('chat', 'k', _bytes, volume: 1);
|
|
||||||
expect(b.inits, 1);
|
|
||||||
expect(b.plays, hasLength(1));
|
|
||||||
expect(File(mark).existsSync(), isFalse);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MarkBackend extends _FakeBackend {
|
|
||||||
final void Function() onInit;
|
|
||||||
_MarkBackend(this.onInit);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> init() async {
|
|
||||||
onInit();
|
|
||||||
await super.init();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,11 @@ bool FlutterWindow::OnCreate() {
|
|||||||
|
|
||||||
void FlutterWindow::OnDestroy() {
|
void FlutterWindow::OnDestroy() {
|
||||||
// The engine and the plugins go now; the app released its players first.
|
// The engine and the plugins go now; the app released its players first.
|
||||||
|
// Win32Window::Create runs this too, before there is an engine: only a
|
||||||
|
// real teardown may end the process (else every start dies at 3 s).
|
||||||
|
if (flutter_controller_) {
|
||||||
ExitAfter(3000);
|
ExitAfter(3000);
|
||||||
|
}
|
||||||
window_channel_ = nullptr;
|
window_channel_ = nullptr;
|
||||||
if (flutter_controller_) {
|
if (flutter_controller_) {
|
||||||
flutter_controller_ = nullptr;
|
flutter_controller_ = nullptr;
|
||||||
|
|||||||
@@ -1946,12 +1946,7 @@ panel) and chat messages (Copy message, and a link in it).
|
|||||||
switched off in `soundsOff`. The device opens 2 s after startup
|
switched off in `soundsOff`. The device opens 2 s after startup
|
||||||
(`warmUp`), so neither startup nor the first sound waits on it; if it
|
(`warmUp`), so neither startup nor the first sound waits on it; if it
|
||||||
can't open, the engine logs `no sounds: audio init failed` once and drops
|
can't open, the engine logs `no sounds: audio init failed` once and drops
|
||||||
every play. Opening it is logged (`opening the audio device`,
|
every play. App close disposes the engine (deinit, 500 ms bound)
|
||||||
`audio device open`) and leaves `<support>/audio-init.pending` until it
|
|
||||||
returns: a native crash there (a driver, miniaudio) leaves the file, and
|
|
||||||
the next start skips sounds (`no sounds: opening the audio device
|
|
||||||
crashed the app last time`) instead of crashing again; a sound preview
|
|
||||||
in Settings deletes it and tries once more. App close disposes the engine (deinit, 500 ms bound)
|
|
||||||
before leaving the server. Settings → Notifications & sounds has the volume, the
|
before leaving the server. Settings → Notifications & sounds has the volume, the
|
||||||
overall sound set (`soundPreset`), and per sound a preview, its own set
|
overall sound set (`soundPreset`), and per sound a preview, its own set
|
||||||
(`soundPresetFor`, else the overall one) and a switch; a preview always
|
(`soundPresetFor`, else the overall one) and a switch; a preview always
|
||||||
@@ -2298,7 +2293,9 @@ app kills itself (`Process.killPid`, no exit handlers; it only gets there
|
|||||||
if the close did not go through); the runners end the process 15 s after
|
if the close did not go through); the runners end the process 15 s after
|
||||||
the first close request (Linux: a `delete-event` handler connected before
|
the first close request (Linux: a `delete-event` handler connected before
|
||||||
window_manager's; Windows: the first `WM_CLOSE`), and Windows also 3 s
|
window_manager's; Windows: the first `WM_CLOSE`), and Windows also 3 s
|
||||||
after the window is destroyed, while the engine and plugins go.
|
after the window is destroyed, while the engine and plugins go (only
|
||||||
|
with an engine: `Win32Window::Create` runs `OnDestroy` before the window
|
||||||
|
exists, and a timer armed there ended every start at 3 s).
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -210,10 +210,12 @@ updater tests, the release build and the close smoke run on that build
|
|||||||
`STARSYNC_SMOKE=<clip>` (a 5 s clip from ffmpeg, else a WAV the script
|
`STARSYNC_SMOKE=<clip>` (a 5 s clip from ffmpeg, else a WAV the script
|
||||||
writes) under its own profile (`STARSYNC_PROFILE=smoke`), opens the clip
|
writes) under its own profile (`STARSYNC_PROFILE=smoke`), opens the clip
|
||||||
in a player with a video output through the media pipeline
|
in a player with a video output through the media pipeline
|
||||||
(`platform/smoke.dart`), plays a second and closes the normal way; the
|
(`platform/smoke.dart`), plays it, stays up 10 s from its start and
|
||||||
steps go to `STARSYNC_SMOKE_OUT` (`start`, `opened <ms> render=<mode>`,
|
closes the normal way; the steps go to `STARSYNC_SMOKE_OUT` (`start`,
|
||||||
`closing`, or `skip` / `fail` with why). It fails when the app does not
|
`opened <ms> render=<mode>`, `closing`, `quitting` just before the
|
||||||
exit within 45 s or leaves a `starsync` process, skips with a warning
|
window closes, or `skip` / `fail` with why). It fails when the app ends
|
||||||
|
by itself before `closing` or never reaches `quitting`, does not exit
|
||||||
|
within 45 s or leaves a `starsync` process, skips with a warning
|
||||||
when the runner cannot bring the app or its video output up, and prints
|
when the runner cannot bring the app or its video output up, and prints
|
||||||
the `smoke:`, `close:` and `mpv: render` lines of the app log.
|
the `smoke:`, `close:` and `mpv: render` lines of the app log.
|
||||||
`tool/smoke_linux.sh` is the same on Linux (needs a display; after
|
`tool/smoke_linux.sh` is the same on Linux (needs a display; after
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ grep -qx start "$OUT" 2>/dev/null || { [[ $exited == 1 ]] && { echo "skipped: th
|
|||||||
grep -q '^skip ' "$OUT" && { echo "skipped: $(grep '^skip ' "$OUT")"; exit 0; }
|
grep -q '^skip ' "$OUT" && { echo "skipped: $(grep '^skip ' "$OUT")"; exit 0; }
|
||||||
grep -q '^fail ' "$OUT" && { echo "smoke failed: $(grep '^fail ' "$OUT")" >&2; exit 1; }
|
grep -q '^fail ' "$OUT" && { echo "smoke failed: $(grep '^fail ' "$OUT")" >&2; exit 1; }
|
||||||
grep -q '^opened ' "$OUT" || { echo "the media never opened" >&2; exit 1; }
|
grep -q '^opened ' "$OUT" || { echo "the media never opened" >&2; exit 1; }
|
||||||
|
grep -qx closing "$OUT" || { echo "the app ended on its own before closing" >&2; exit 1; }
|
||||||
|
grep -qx quitting "$OUT" || { echo "the close did not reach its end (killed on the way?)" >&2; exit 1; }
|
||||||
[[ $exited == 1 ]] || { echo "the app did not close within $LIMIT s (P32)" >&2; exit 1; }
|
[[ $exited == 1 ]] || { echo "the app did not close within $LIMIT s (P32)" >&2; exit 1; }
|
||||||
[[ -z "$left" ]] || { echo "a starsync process was left behind (P32)" >&2; exit 1; }
|
[[ -z "$left" ]] || { echo "a starsync process was left behind (P32)" >&2; exit 1; }
|
||||||
echo "smoke ok: opened and closed in $secs s, no process left"
|
echo "smoke ok: opened and closed in $secs s, no process left"
|
||||||
@@ -64,6 +64,8 @@ if (-not ($lines -contains 'start')) {
|
|||||||
if ($lines | Where-Object { $_ -like 'skip *' }) { Write-Warning "skipped: $($lines -like 'skip *')"; exit 0 }
|
if ($lines | Where-Object { $_ -like 'skip *' }) { Write-Warning "skipped: $($lines -like 'skip *')"; exit 0 }
|
||||||
if ($lines | Where-Object { $_ -like 'fail *' }) { Write-Error "smoke failed: $($lines -like 'fail *')"; exit 1 }
|
if ($lines | Where-Object { $_ -like 'fail *' }) { Write-Error "smoke failed: $($lines -like 'fail *')"; exit 1 }
|
||||||
if (-not ($lines | Where-Object { $_ -like 'opened *' })) { Write-Error 'the media never opened'; exit 1 }
|
if (-not ($lines | Where-Object { $_ -like 'opened *' })) { Write-Error 'the media never opened'; exit 1 }
|
||||||
|
if (-not ($lines -contains 'closing')) { Write-Error "the app ended on its own before closing (exit $($p.ExitCode))"; exit 1 }
|
||||||
|
if (-not ($lines -contains 'quitting')) { Write-Error 'the close did not reach its end (killed on the way?)'; exit 1 }
|
||||||
if (-not $exited) { Write-Error "the app did not close within $Limit s (P32)"; exit 1 }
|
if (-not $exited) { Write-Error "the app did not close within $Limit s (P32)"; exit 1 }
|
||||||
if ($left) { Write-Error "a starsync process was left behind (P32)"; exit 1 }
|
if ($left) { Write-Error "a starsync process was left behind (P32)"; exit 1 }
|
||||||
Write-Host "smoke ok: opened and closed in $secs s, no process left"
|
Write-Host "smoke ok: opened and closed in $secs s, no process left"
|
||||||
|
|||||||
Reference in new issue
Block a user