using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Diagnostics; namespace Evergreen.Health.Application.Health.Integrations.MiAirX; /// Demo 音乐任务由服务端持续驱动,页面关闭后仍按体质曲目循环渐弱播放。 public sealed class TcmMusicTransitionService : IHostedService { private readonly MiAirXClient _miAirX; private readonly TcmMusicOptions _options; private readonly ILogger _logger; private readonly SemaphoreSlim _operationLock = new(1, 1); private readonly object _statusLock = new(); private TcmMusicTransitionOutput _status = new(); private CancellationTokenSource? _activeCancellation; private Task? _activeTask; public TcmMusicTransitionService( IHttpClientFactory httpClientFactory, IOptions miAirXOptions, IOptions musicOptions, ILogger logger) { _miAirX = new MiAirXClient(httpClientFactory.CreateClient(nameof(TcmMusicTransitionService)), miAirXOptions); _options = musicOptions.Value; _logger = logger; } public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; public TcmMusicTransitionOutput GetStatus() { lock (_statusLock) return new TcmMusicTransitionOutput { Running = _status.Running, HasVolume = _status.HasVolume, VolumePercent = _status.VolumePercent, Message = _status.Message }; } public async Task StopAsync(CancellationToken cancellationToken) { try { await StopMusicAsync(cancellationToken); } catch (Exception exception) when (exception is not OperationCanceledException) { _logger.LogWarning(exception, "应用退出时未能停止助眠音乐"); } } public bool HasPlayableTracks(string constitutionCode) => _miAirX.IsConfigured && _options.Tracks.TryGetValue(constitutionCode, out var tracks) && tracks.Count > 0 && tracks.All(track => IsValidTrack(track)); public async Task StartForConstitutionAsync(string constitutionCode, CancellationToken cancellationToken) { if (!HasPlayableTracks(constitutionCode)) throw new InvalidOperationException($"{constitutionCode} 的体质音乐 OSS URL 尚未配置完整"); if (_options.StartVolume is < 1 or > 100 || _options.FadeStepSeconds is < 1 or > 60) throw new InvalidOperationException("体质音乐渐弱参数无效"); var tracks = _options.Tracks[constitutionCode].ToArray(); await _operationLock.WaitAsync(cancellationToken); try { await CancelCurrentLoopAsync(); await _miAirX.SetVolumeAsync(_options.StartVolume, cancellationToken); await _miAirX.PlayUrlAsync(tracks[0].Url, cancellationToken); UpdateStatus(true, _options.StartVolume, "体质音乐正在自动渐弱"); _activeCancellation = new CancellationTokenSource(); _activeTask = RunFadeAsync(tracks, constitutionCode, _activeCancellation.Token); } finally { _operationLock.Release(); } } public async Task PlayTestMusicAsync(CancellationToken cancellationToken) { await _operationLock.WaitAsync(cancellationToken); try { await CancelCurrentLoopAsync(); await _miAirX.PlayAsync(cancellationToken); UpdateStatus(false, null, "正在播放测试音乐"); } finally { _operationLock.Release(); } } public async Task SetManualVolumeAsync(int volume, CancellationToken cancellationToken) { await _operationLock.WaitAsync(cancellationToken); try { // 手动调音量接管当前播放,避免下一次自动渐弱立刻覆盖用户选择。 await CancelCurrentLoopAsync(); await _miAirX.SetVolumeAsync(volume, cancellationToken); UpdateStatus(false, volume, "音量已手动设置"); } finally { _operationLock.Release(); } } public async Task StopMusicAsync(CancellationToken cancellationToken) { await _operationLock.WaitAsync(cancellationToken); try { await CancelCurrentLoopAsync(); await _miAirX.StopAsync(cancellationToken); UpdateStatus(false, null, "音乐已停止"); } finally { _operationLock.Release(); } } private async Task RunFadeAsync( IReadOnlyList tracks, string constitutionCode, CancellationToken cancellationToken) { var clock = Stopwatch.StartNew(); var currentVolume = _options.StartVolume; var nextFadeSecond = (double)_options.FadeStepSeconds; var trackStartSecond = 0d; var trackIndex = 0; try { while (true) { await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); var elapsed = clock.Elapsed.TotalSeconds; while (elapsed >= nextFadeSecond && currentVolume > 0) { currentVolume--; await _miAirX.SetVolumeAsync(currentVolume, cancellationToken); UpdateStatus(true, currentVolume, "体质音乐正在自动渐弱"); nextFadeSecond += _options.FadeStepSeconds; } if (currentVolume == 0) { await _miAirX.StopAsync(cancellationToken); UpdateStatus(false, 0, "音量已降至 0,音乐已停止"); _logger.LogInformation("体质 {ConstitutionCode} 的助眠音乐已渐弱至 0 并停止", constitutionCode); return; } if (elapsed - trackStartSecond < tracks[trackIndex].DurationSeconds) continue; trackIndex = (trackIndex + 1) % tracks.Count; await _miAirX.PlayUrlAsync(tracks[trackIndex].Url, cancellationToken); trackStartSecond = clock.Elapsed.TotalSeconds; _logger.LogInformation("体质 {ConstitutionCode} 循环播放 {Track},继承音量 {Volume}", constitutionCode, tracks[trackIndex].Title, currentVolume); } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { } catch (Exception exception) { _logger.LogWarning(exception, "体质音乐渐弱或续播中断"); try { await _miAirX.StopAsync(CancellationToken.None); } catch (Exception stopException) { _logger.LogWarning(stopException, "体质音乐中断后停止音箱失败"); } UpdateStatus(false, null, "自动渐弱中断,请查看后端日志"); } } private async Task CancelCurrentLoopAsync() { var cancellation = _activeCancellation; var task = _activeTask; _activeCancellation = null; _activeTask = null; if (cancellation is null) return; cancellation.Cancel(); try { if (task is not null) await task; } finally { cancellation.Dispose(); } UpdateStatus(false, null, "自动渐弱已停止"); } private void UpdateStatus(bool running, int? volume, string message) { lock (_statusLock) { _status.Running = running; if (volume.HasValue) { _status.HasVolume = true; _status.VolumePercent = volume.Value; } _status.Message = message; } } private static bool IsValidTrack(TcmMusicTrackOptions track) => track.DurationSeconds > 0 && Uri.TryCreate(track.Url, UriKind.Absolute, out var uri) && uri.Scheme is "http" or "https"; } public sealed class TcmMusicTransitionOutput { public bool Running { get; set; } public bool HasVolume { get; set; } public int VolumePercent { get; set; } public string Message { get; set; } = string.Empty; }