using Microsoft.JSInterop; namespace DeepDrftWeb.Client.Services; public class AudioInteropService : IAsyncDisposable { private readonly IJSRuntime _jsRuntime; private readonly Dictionary _callbacks = new(); public AudioInteropService(IJSRuntime jsRuntime) { _jsRuntime = jsRuntime; } public async Task CreatePlayerAsync(string playerId) { try { var result = await _jsRuntime.InvokeAsync("DeepDrftAudio.createPlayer", playerId); return result; } catch (Exception ex) { return new AudioOperationResult { Success = false, Error = ex.Message }; } } public async Task InitializeBufferedPlayerAsync(string playerId) { return await InvokeJsAsync("DeepDrftAudio.initializeBufferedPlayer", playerId); } public async Task AppendAudioBlockAsync(string playerId, byte[] audioBlock) { return await InvokeJsAsync("DeepDrftAudio.appendAudioBlock", playerId, audioBlock); } public async Task FinalizeAudioBufferAsync(string playerId) { return await InvokeJsAsync("DeepDrftAudio.finalizeAudioBuffer", playerId); } // Streaming methods public async Task InitializeStreaming(string playerId, long totalStreamLength) { return await InvokeJsAsync("DeepDrftAudio.initializeStreaming", playerId, totalStreamLength); } public async Task ProcessStreamingChunk(string playerId, byte[] audioChunk) { return await InvokeJsAsync("DeepDrftAudio.processStreamingChunk", playerId, audioChunk); } public async Task StartStreamingPlayback(string playerId) { return await InvokeJsAsync("DeepDrftAudio.startStreamingPlayback", playerId); } public async Task EnsureAudioContextReady(string playerId) { return await InvokeJsAsync("DeepDrftAudio.ensureAudioContextReady", playerId); } public async Task PlayAsync(string playerId) { return await InvokeJsAsync("DeepDrftAudio.play", playerId); } public async Task PauseAsync(string playerId) { return await InvokeJsAsync("DeepDrftAudio.pause", playerId); } public async Task StopAsync(string playerId) { return await InvokeJsAsync("DeepDrftAudio.stop", playerId); } public async Task UnloadAsync(string playerId) { return await InvokeJsAsync("DeepDrftAudio.unload", playerId); } public async Task SeekAsync(string playerId, double position) { return await InvokeJsAsync("DeepDrftAudio.seek", playerId, position); } // New methods for seek-beyond-buffer support public async Task GetBufferedDuration(string playerId) { try { return await _jsRuntime.InvokeAsync("DeepDrftAudio.getBufferedDuration", playerId); } catch { return 0; } } public async Task CalculateByteOffset(string playerId, double positionSeconds) { try { return (long)await _jsRuntime.InvokeAsync("DeepDrftAudio.calculateByteOffset", playerId, positionSeconds); } catch { return 0; } } public async Task ReinitializeFromOffset(string playerId, long totalStreamLength, double seekPosition) { return await InvokeJsAsync("DeepDrftAudio.reinitializeFromOffset", playerId, totalStreamLength, seekPosition); } public async Task SetVolumeAsync(string playerId, double volume) { return await InvokeJsAsync("DeepDrftAudio.setVolume", playerId, volume); } public async Task GetCurrentTimeAsync(string playerId) { try { return await _jsRuntime.InvokeAsync("DeepDrftAudio.getCurrentTime", playerId); } catch (Exception) { return 0; } } public async Task GetStateAsync(string playerId) { try { return await _jsRuntime.InvokeAsync("DeepDrftAudio.getState", playerId); } catch (Exception) { return null; } } public async Task SetOnProgressCallbackAsync(string playerId, Func callback) { return await SetCallbackAsync(playerId, "_progress", "setOnProgressCallback", "OnProgressCallback", wrapper => wrapper.OnProgress = callback); } public async Task SetOnEndCallbackAsync(string playerId, Func callback) { return await SetCallbackAsync(playerId, "_end", "setOnEndCallback", "OnEndCallback", wrapper => wrapper.OnEnd = callback); } // Spectrum analyzer methods public async Task GetSpectrumDataAsync(string playerId) { try { return await _jsRuntime.InvokeAsync("DeepDrftAudio.getSpectrumData", playerId); } catch { return null; } } public async Task SetSpectrumHighPassAsync(string playerId, double freq) { return await InvokeJsAsync("DeepDrftAudio.setSpectrumHighPass", playerId, freq); } public async Task SetSpectrumLowPassAsync(string playerId, double freq) { return await InvokeJsAsync("DeepDrftAudio.setSpectrumLowPass", playerId, freq); } public async Task SetSpectrumSlopeAsync(string playerId, double dbPerDecade) { return await InvokeJsAsync("DeepDrftAudio.setSpectrumSlope", playerId, dbPerDecade); } public async Task StartSpectrumAnimationAsync(string playerId, string callbackId, Func callback) { try { var callbackWrapper = new SpectrumCallback { OnData = callback }; var dotNetObjectRef = DotNetObjectReference.Create(callbackWrapper); _callbacks[playerId + "_spectrum_" + callbackId] = dotNetObjectRef; return await _jsRuntime.InvokeAsync( "DeepDrftAudio.startSpectrumAnimation", playerId, callbackId, dotNetObjectRef, "OnSpectrumDataCallback"); } catch (Exception ex) { return new AudioOperationResult { Success = false, Error = ex.Message }; } } public async Task StopSpectrumAnimationAsync(string playerId, string callbackId) { var key = playerId + "_spectrum_" + callbackId; if (_callbacks.TryGetValue(key, out var callback)) { callback?.Dispose(); _callbacks.Remove(key); } return await InvokeJsAsync("DeepDrftAudio.stopSpectrumAnimation", playerId, callbackId); } public async Task DisposePlayerAsync(string playerId) { CleanupPlayerCallbacks(playerId); return await InvokeJsAsync("DeepDrftAudio.disposePlayer", playerId); } private async Task InvokeJsAsync(string identifier, params object[] args) { try { return await _jsRuntime.InvokeAsync(identifier, args); } catch (Exception ex) { if (typeof(T) == typeof(AudioOperationResult)) return (T)(object)new AudioOperationResult { Success = false, Error = ex.Message }; if (typeof(T) == typeof(AudioLoadResult)) return (T)(object)new AudioLoadResult { Success = false, Error = ex.Message }; if (typeof(T) == typeof(StreamingResult)) return (T)(object)new StreamingResult { Success = false, Error = ex.Message }; if (typeof(T) == typeof(SeekResult)) return (T)(object)new SeekResult { Success = false, Error = ex.Message }; throw; } } private async Task SetCallbackAsync(string playerId, string suffix, string jsMethod, string callbackMethod, Action configureCallback) { try { var callbackWrapper = new AudioPlayerCallback(); configureCallback(callbackWrapper); var dotNetObjectRef = DotNetObjectReference.Create(callbackWrapper); _callbacks[playerId + suffix] = dotNetObjectRef; return await _jsRuntime.InvokeAsync($"DeepDrftAudio.{jsMethod}", playerId, dotNetObjectRef, callbackMethod); } catch (Exception ex) { return new AudioOperationResult { Success = false, Error = ex.Message }; } } private void CleanupPlayerCallbacks(string playerId) { var keysToRemove = _callbacks.Keys.Where(k => k.StartsWith(playerId + "_")).ToList(); foreach (var key in keysToRemove) { _callbacks[key]?.Dispose(); _callbacks.Remove(key); } } public async ValueTask DisposeAsync() { foreach (var callback in _callbacks.Values) { callback?.Dispose(); } _callbacks.Clear(); } } public class AudioPlayerCallback { public Func? OnProgress { get; set; } public Func? OnEnd { get; set; } [JSInvokable] public async Task OnProgressCallback(double currentTime) { if (OnProgress != null) await OnProgress(currentTime); } [JSInvokable] public async Task OnEndCallback() { if (OnEnd != null) await OnEnd(); } } public class SpectrumCallback { public Func? OnData { get; set; } [JSInvokable] public async Task OnSpectrumDataCallback(double[] data) { if (OnData != null) await OnData(data); } } public class AudioOperationResult { public bool Success { get; set; } public string? Error { get; set; } } public class SeekResult : AudioOperationResult { public bool SeekBeyondBuffer { get; set; } public long ByteOffset { get; set; } } public class AudioLoadResult : AudioOperationResult { public double Duration { get; set; } public int SampleRate { get; set; } public int NumberOfChannels { get; set; } public double LoadProgress { get; set; } } public class StreamingResult : AudioOperationResult { public bool CanStartStreaming { get; set; } public bool HeaderParsed { get; set; } public int BufferCount { get; set; } public double? Duration { get; set; } // Duration in seconds calculated from WAV header } public class AudioPlayerState { public bool IsPlaying { get; set; } public bool IsPaused { get; set; } public double CurrentTime { get; set; } public double Duration { get; set; } public double Volume { get; set; } public double LoadProgress { get; set; } }