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 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 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); _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); } finally { _operationLock.Release(); } } public async Task SetManualVolumeAsync(int volume, CancellationToken cancellationToken) { await _operationLock.WaitAsync(cancellationToken); try { // 手动调音量接管当前播放,避免下一次自动渐弱立刻覆盖用户选择。 await CancelCurrentLoopAsync(); await _miAirX.SetVolumeAsync(volume, cancellationToken); } finally { _operationLock.Release(); } } public async Task StopMusicAsync(CancellationToken cancellationToken) { await _operationLock.WaitAsync(cancellationToken); try { await CancelCurrentLoopAsync(); await _miAirX.StopAsync(cancellationToken); } 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); nextFadeSecond += _options.FadeStepSeconds; } if (currentVolume == 0) { await _miAirX.StopAsync(cancellationToken); _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, "体质音乐中断后停止音箱失败"); } } } 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(); } } private static bool IsValidTrack(TcmMusicTrackOptions track) => track.DurationSeconds > 0 && Uri.TryCreate(track.Url, UriKind.Absolute, out var uri) && uri.Scheme is "http" or "https"; }