1
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
namespace Evergreen.Health.Application.Health.Integrations.MiAirX;
|
||||
|
||||
/// <summary>播放能力契约,未来音箱接入不要求是 MiAirX。</summary>
|
||||
public interface IAudioPlaybackGateway
|
||||
{
|
||||
bool IsConfigured { get; }
|
||||
Task PlayAsync(CancellationToken cancellationToken = default);
|
||||
Task PlayUrlAsync(string musicUrl, CancellationToken cancellationToken = default);
|
||||
Task StopAsync(CancellationToken cancellationToken = default);
|
||||
Task SetVolumeAsync(int volume, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface ITcmMusicRuntime
|
||||
{
|
||||
TcmMusicTransitionOutput GetStatus();
|
||||
bool HasPlayableTracks(string constitutionCode);
|
||||
Task StartForConstitutionAsync(string constitutionCode, CancellationToken cancellationToken);
|
||||
Task PlayTestMusicAsync(CancellationToken cancellationToken);
|
||||
Task SetManualVolumeAsync(int volume, CancellationToken cancellationToken);
|
||||
Task StopMusicAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using System.Text.Json;
|
||||
|
||||
namespace Evergreen.Health.Application.Health.Integrations.MiAirX;
|
||||
|
||||
public sealed class MiAirXClient
|
||||
public sealed class MiAirXClient : IAudioPlaybackGateway
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly MiAirXOptions _options;
|
||||
@@ -23,7 +23,6 @@ public sealed class MiAirXClient
|
||||
public bool IsConfigured => _options.Enabled &&
|
||||
!string.IsNullOrWhiteSpace(_options.BaseUrl) &&
|
||||
!string.IsNullOrWhiteSpace(_options.DeviceId) &&
|
||||
!string.IsNullOrWhiteSpace(_options.MusicUrl) &&
|
||||
(!_options.RequireWebPassword || !string.IsNullOrWhiteSpace(_options.WebPassword));
|
||||
|
||||
public Task PlayAsync(CancellationToken cancellationToken = default)
|
||||
@@ -48,6 +47,15 @@ public sealed class MiAirXClient
|
||||
}
|
||||
|
||||
private async Task SendCommandAsync(string path, object payload, CancellationToken cancellationToken)
|
||||
{
|
||||
try { await SendCommandCoreAsync(path, payload, cancellationToken); }
|
||||
catch (OperationCanceledException exception) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
throw new TimeoutException("MiAirX 请求超时,未确认音箱操作完成", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendCommandCoreAsync(string path, object payload, CancellationToken cancellationToken)
|
||||
{
|
||||
var baseUri = GetBaseUri();
|
||||
if (!string.IsNullOrWhiteSpace(_options.WebPassword))
|
||||
|
||||
+22
-20
@@ -6,11 +6,14 @@ using System.Diagnostics;
|
||||
namespace Evergreen.Health.Application.Health.Integrations.MiAirX;
|
||||
|
||||
/// <summary>Demo 音乐任务由服务端持续驱动,页面关闭后仍按体质曲目循环渐弱播放。</summary>
|
||||
public sealed class TcmMusicTransitionService : IHostedService
|
||||
public sealed class TcmMusicTransitionService : IHostedService, ITcmMusicRuntime
|
||||
{
|
||||
private readonly MiAirXClient _miAirX;
|
||||
private const int DemoStartVolume = 50;
|
||||
private const int DemoFadeStepSeconds = 5;
|
||||
private readonly IAudioPlaybackGateway _miAirX;
|
||||
private readonly TcmMusicOptions _options;
|
||||
private readonly ILogger<TcmMusicTransitionService> _logger;
|
||||
private readonly TimeProvider _time;
|
||||
private readonly SemaphoreSlim _operationLock = new(1, 1);
|
||||
private readonly object _statusLock = new();
|
||||
private TcmMusicTransitionOutput _status = new();
|
||||
@@ -18,14 +21,15 @@ public sealed class TcmMusicTransitionService : IHostedService
|
||||
private Task? _activeTask;
|
||||
|
||||
public TcmMusicTransitionService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IOptions<MiAirXOptions> miAirXOptions,
|
||||
IAudioPlaybackGateway audioGateway,
|
||||
IOptions<TcmMusicOptions> musicOptions,
|
||||
ILogger<TcmMusicTransitionService> logger)
|
||||
ILogger<TcmMusicTransitionService> logger,
|
||||
TimeProvider time)
|
||||
{
|
||||
_miAirX = new MiAirXClient(httpClientFactory.CreateClient(nameof(TcmMusicTransitionService)), miAirXOptions);
|
||||
_miAirX = audioGateway;
|
||||
_options = musicOptions.Value;
|
||||
_logger = logger;
|
||||
_time = time;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
@@ -53,24 +57,22 @@ public sealed class TcmMusicTransitionService : IHostedService
|
||||
|
||||
public bool HasPlayableTracks(string constitutionCode)
|
||||
=> _miAirX.IsConfigured &&
|
||||
_options.Tracks.TryGetValue(constitutionCode, out var tracks) && tracks.Count > 0 &&
|
||||
_options.Tracks is not null && _options.Tracks.TryGetValue(constitutionCode, out var tracks) &&
|
||||
tracks is not null && 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.SetVolumeAsync(DemoStartVolume, cancellationToken);
|
||||
await _miAirX.PlayUrlAsync(tracks[0].Url, cancellationToken);
|
||||
UpdateStatus(true, _options.StartVolume, "体质音乐正在自动渐弱");
|
||||
UpdateStatus(true, DemoStartVolume, "体质音乐正在自动渐弱");
|
||||
_activeCancellation = new CancellationTokenSource();
|
||||
_activeTask = RunFadeAsync(tracks, constitutionCode, _activeCancellation.Token);
|
||||
}
|
||||
@@ -119,23 +121,23 @@ public sealed class TcmMusicTransitionService : IHostedService
|
||||
string constitutionCode,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var clock = Stopwatch.StartNew();
|
||||
var currentVolume = _options.StartVolume;
|
||||
var nextFadeSecond = (double)_options.FadeStepSeconds;
|
||||
var start = _time.GetTimestamp();
|
||||
var currentVolume = DemoStartVolume;
|
||||
var nextFadeSecond = (double)DemoFadeStepSeconds;
|
||||
var trackStartSecond = 0d;
|
||||
var trackIndex = 0;
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
var elapsed = clock.Elapsed.TotalSeconds;
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), _time, cancellationToken);
|
||||
var elapsed = _time.GetElapsedTime(start).TotalSeconds;
|
||||
while (elapsed >= nextFadeSecond && currentVolume > 0)
|
||||
{
|
||||
currentVolume--;
|
||||
await _miAirX.SetVolumeAsync(currentVolume, cancellationToken);
|
||||
UpdateStatus(true, currentVolume, "体质音乐正在自动渐弱");
|
||||
nextFadeSecond += _options.FadeStepSeconds;
|
||||
nextFadeSecond += DemoFadeStepSeconds;
|
||||
}
|
||||
|
||||
if (currentVolume == 0)
|
||||
@@ -149,7 +151,7 @@ public sealed class TcmMusicTransitionService : IHostedService
|
||||
if (elapsed - trackStartSecond < tracks[trackIndex].DurationSeconds) continue;
|
||||
trackIndex = (trackIndex + 1) % tracks.Count;
|
||||
await _miAirX.PlayUrlAsync(tracks[trackIndex].Url, cancellationToken);
|
||||
trackStartSecond = clock.Elapsed.TotalSeconds;
|
||||
trackStartSecond = _time.GetElapsedTime(start).TotalSeconds;
|
||||
_logger.LogInformation("体质 {ConstitutionCode} 循环播放 {Track},继承音量 {Volume}",
|
||||
constitutionCode, tracks[trackIndex].Title, currentVolume);
|
||||
}
|
||||
@@ -192,7 +194,7 @@ public sealed class TcmMusicTransitionService : IHostedService
|
||||
}
|
||||
|
||||
private static bool IsValidTrack(TcmMusicTrackOptions track)
|
||||
=> track.DurationSeconds > 0 &&
|
||||
=> track is not null && double.IsFinite(track.DurationSeconds) && track.DurationSeconds > 0 &&
|
||||
Uri.TryCreate(track.Url, UriKind.Absolute, out var uri) &&
|
||||
uri.Scheme is "http" or "https";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user