From 6117fcc81fd8f64d84e786a8408e90bf49b81983 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Mon, 18 Dec 2023 19:52:17 +0100 Subject: [PATCH] Add WAV streaming support --- HyperbolicDownloader/InputHandler.cs | 5 + .../Commands/DownloadCommands.cs | 2 +- .../Commands/StreamingCommands.cs | 240 ++++++++++++++++++ .../HyperbolicDownloaderApi.csproj | 1 + .../Networking/NetworkClient.cs | 69 ++++- 5 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 HyperbolicDownloaderApi/Commands/StreamingCommands.cs diff --git a/HyperbolicDownloader/InputHandler.cs b/HyperbolicDownloader/InputHandler.cs index c3bd8d1..0a3aab2 100644 --- a/HyperbolicDownloader/InputHandler.cs +++ b/HyperbolicDownloader/InputHandler.cs @@ -19,6 +19,8 @@ internal class InputHandler FileCommands fileCommands = new FileCommands(hostsManager, filesManager); DirectoryCommands directoryCommands = new DirectoryCommands(directoryWatcher); DownloadCommands downloadCommands = new DownloadCommands(hostsManager, filesManager); + StreamingCommands streamingCommands = new StreamingCommands(hostsManager); + ClientCommands clientCommands = new ClientCommands(); LogCommands logCommands = new LogCommands(); @@ -35,6 +37,9 @@ internal class InputHandler Command getCommand = commander.Register(downloadCommands.GetFile, (HelpText)"Attempts to retrieve a file from another host using a hash.", "get"); _ = getCommand.Register(downloadCommands.GetFileFrom, (HelpText)"Attempts to retrieve a file from another host using a .hyper file.", "from"); + Command streamCommand = commander.Register(streamingCommands.StreamWav, (HelpText)"Attempts to stream a .wav file from another host using a hash.", "stream"); + _ = streamCommand.Register(streamingCommands.GetWavStreamFrom, (HelpText)"Attempts to stream a .wav file from another host using a .hyper file.", "from"); + Command generateCommad = commander.Register(fileCommands.GenerateFileFull, (HelpText)"Generates a .hyper file from a file hash.", "generate", "gen"); _ = generateCommad.Register(fileCommands.GenerateFileSingle, (HelpText)"Generates a .hyper file from a file hash without checking the known hosts. This adds only the local host to the file.", "noscan"); diff --git a/HyperbolicDownloaderApi/Commands/DownloadCommands.cs b/HyperbolicDownloaderApi/Commands/DownloadCommands.cs index 7597a74..48d778f 100644 --- a/HyperbolicDownloaderApi/Commands/DownloadCommands.cs +++ b/HyperbolicDownloaderApi/Commands/DownloadCommands.cs @@ -141,7 +141,6 @@ public class DownloadCommands(HostsManager hostsManager, FilesManager filesManag ApiManager.SendNotificationMessageNewLine($"File name: {fileName}"); ApiManager.SendNotificationMessageNewLine($"Starting download..."); - int totalBytesRead = 0; if (!Directory.Exists(directoryPath)) { @@ -150,6 +149,7 @@ public class DownloadCommands(HostsManager hostsManager, FilesManager filesManag using FileStream? fileStream = new FileStream(filePath, FileMode.Create); + int totalBytesRead = 0; int bytesPerSecond = 0; int transferRate = 0; TimeSpan timeRemaining = TimeSpan.Zero; diff --git a/HyperbolicDownloaderApi/Commands/StreamingCommands.cs b/HyperbolicDownloaderApi/Commands/StreamingCommands.cs new file mode 100644 index 0000000..e11acd9 --- /dev/null +++ b/HyperbolicDownloaderApi/Commands/StreamingCommands.cs @@ -0,0 +1,240 @@ +using HyperbolicDownloaderApi.FileProcessing; +using HyperbolicDownloaderApi.Managment; +using HyperbolicDownloaderApi.Networking; + +using NAudio.Utils; +using NAudio.Wave; + +using Stone_Red_Utilities.StringExtentions; + +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Text.Json; + +namespace HyperbolicDownloaderApi.Commands; + +public class StreamingCommands(HostsManager hostsManager) +{ + public void GetWavStreamFrom(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + ApiManager.SendNotificationMessageNewLine("Path is empty!", NotificationMessageType.Error); + return; + } + + string fullPath = Path.GetFullPath(path); + + if (!File.Exists(fullPath)) + { + ApiManager.SendNotificationMessageNewLine("Invalid file path!", NotificationMessageType.Error); + return; + } + + string json = File.ReadAllText(fullPath); + + PublicHyperFileInfo? publicHyperFileInfo = JsonSerializer.Deserialize(json); + if (publicHyperFileInfo == null) + { + ApiManager.SendNotificationMessageNewLine("Parsing file failed!", NotificationMessageType.Error); + return; + } + + _ = hostsManager.AddRange(publicHyperFileInfo.Hosts); + StreamWav(publicHyperFileInfo.Hash); + } + + public void StreamWav(string hash) + { + if (string.IsNullOrEmpty(hash)) + { + ApiManager.SendNotificationMessageNewLine("No hash value specified!", NotificationMessageType.Error); + return; + } + + hash = hash.Trim().ToLower(); + + foreach (NetworkSocket host in hostsManager.ToList()) + { + bool validIpAdress = IPAddress.TryParse(host.IPAddress, out IPAddress? ipAddress); + + if (!validIpAdress) + { + hostsManager.Remove(host, true); + continue; + } + + ApiManager.SendNotificationMessage($"{host.IPAddress}:{host.Port} > ???", NotificationMessageType.Warning); + + Console.CursorLeft = 0; + + Task sendTask = NetworkClient.SendAsync(ipAddress!, host.Port, "HasFile", hash); + + _ = sendTask.Wait(1000); + + if (!sendTask.IsCompletedSuccessfully) + { + Console.CursorLeft = 0; + ApiManager.SendNotificationMessageNewLine($"{host.IPAddress}:{host.Port} > Inactive", NotificationMessageType.Error); + + hostsManager.Remove(host); + continue; + } + else if (!sendTask.Result) + { + host.LastActive = DateTime.Now; + ApiManager.SendNotificationMessageNewLine($"{host.IPAddress}:{host.Port} > Does not have the requested file", NotificationMessageType.Error); + continue; + } + + host.LastActive = DateTime.Now; + + ApiManager.SendNotificationMessageNewLine($"{host.IPAddress}:{host.Port} > Has the requested file", NotificationMessageType.Success); + ApiManager.SendNotificationMessageNewLine("Requesting stream..."); + + using TcpClient tcpClient = new TcpClient(); + tcpClient.Connect(ipAddress!, host.Port); + tcpClient.ReceiveBufferSize = 6400; + + NetworkStream nwStream = tcpClient.GetStream(); + byte[] buffer = new byte[tcpClient.ReceiveBufferSize]; + byte[] reciveBuffer = new byte[6400]; + + byte[] bytesToSend = Encoding.ASCII.GetBytes($"StreamWav {hash}"); + nwStream.Write(bytesToSend); + nwStream.ReadTimeout = 5000; + + int bytesRead; + try + { + bytesRead = nwStream.Read(buffer, 0, 1000); + } + catch (IOException) + { + ApiManager.SendNotificationMessageNewLine(string.Empty); + ApiManager.SendNotificationMessageNewLine("Lost connection to other host!", NotificationMessageType.Error); + continue; + } + + string dataReceived = Encoding.ASCII.GetString(buffer, 0, bytesRead); + + string[] parts = dataReceived.Split('/'); + + if (parts.Length != 5) //If received data does not contain 5 parts -> error + { + ApiManager.SendNotificationMessageNewLine(dataReceived, NotificationMessageType.Error); + continue; + } + + if (!int.TryParse(parts[0], out int dataLength) || dataLength <= 0) + { + ApiManager.SendNotificationMessageNewLine("Invalid data length!", NotificationMessageType.Error); + continue; + } + + if (!int.TryParse(parts[2], out int sampleRate) || sampleRate <= 0) + { + ApiManager.SendNotificationMessageNewLine("Invalid sample rate!", NotificationMessageType.Error); + continue; + } + + if (!int.TryParse(parts[3], out int bitsPerSample) || bitsPerSample <= 0) + { + ApiManager.SendNotificationMessageNewLine("Invalid bits per sample!", NotificationMessageType.Error); + continue; + } + + if (!int.TryParse(parts[4], out int channels) || channels <= 0) + { + ApiManager.SendNotificationMessageNewLine("Invalid channels!", NotificationMessageType.Error); + continue; + } + + string fileName = parts[1].ToFileName(); + + ApiManager.SendNotificationMessageNewLine($"File name: {fileName}"); + ApiManager.SendNotificationMessageNewLine($"Starting stream..."); + + BufferedWaveProvider bufferedWaveProvider = new BufferedWaveProvider(new WaveFormat(sampleRate, bitsPerSample, channels)); + using WaveOutEvent player = new WaveOutEvent(); + player.Init(bufferedWaveProvider); + player.Play(); + + int totalBytesRead = 0; + TimeSpan totalTime = TimeSpan.FromSeconds(dataLength / (double)sampleRate / channels / (bitsPerSample / 8)); + + Task task = Task.Run(async () => + { + while (player.GetPosition() < dataLength && player.PlaybackState != PlaybackState.Stopped) + { + Console.Write($"\r[{player.PlaybackState,-7}] {player.GetPositionTimeSpan():hh\\:mm\\:ss}/{totalTime:hh\\:mm\\:ss}"); + + if (IsBufferNearlyFull(bufferedWaveProvider)) + { + await Task.Delay(100); + continue; + } + + try + { + bytesRead = nwStream.Read(reciveBuffer, 0, reciveBuffer.Length); + } + catch (IOException ex) + { + Debug.WriteLine(ex); + ApiManager.SendNotificationMessageNewLine(string.Empty); + ApiManager.SendNotificationMessageNewLine("Lost connection to other host!", NotificationMessageType.Error); + break; + } + + bytesRead = Math.Min(bytesRead, dataLength - totalBytesRead); + + bufferedWaveProvider.AddSamples(reciveBuffer, 0, bytesRead); + totalBytesRead += bytesRead; + } + + ApiManager.SendNotificationMessageNewLine(string.Empty); + ApiManager.SendNotificationMessageNewLine("Stream ended!", NotificationMessageType.Warning); + }); + + while (!task.IsCompleted) + { + ApiManager.SendNotificationMessage($"\r[{player.PlaybackState,-7}]"); + + char c = Console.ReadKey(true).KeyChar; + + if (c == 'p') + { + if (player.PlaybackState == PlaybackState.Playing) + { + player.Pause(); + } + else + { + player.Play(); + } + } + else if (c == 's') + { + player.Stop(); + ApiManager.SendNotificationMessage($"\r[{player.PlaybackState,-7}]"); + break; + } + } + + hostsManager.SaveHosts(); + return; + } + ApiManager.SendNotificationMessageNewLine("None of the available hosts have the requested file!", NotificationMessageType.Error); + hostsManager.SaveHosts(); + } + + private bool IsBufferNearlyFull(BufferedWaveProvider bufferedWaveProvider) + { + return bufferedWaveProvider != null && + bufferedWaveProvider.BufferLength - bufferedWaveProvider.BufferedBytes + < bufferedWaveProvider.WaveFormat.AverageBytesPerSecond / 4; + } +} \ No newline at end of file diff --git a/HyperbolicDownloaderApi/HyperbolicDownloaderApi.csproj b/HyperbolicDownloaderApi/HyperbolicDownloaderApi.csproj index 2367984..ac847e9 100644 --- a/HyperbolicDownloaderApi/HyperbolicDownloaderApi.csproj +++ b/HyperbolicDownloaderApi/HyperbolicDownloaderApi.csproj @@ -7,6 +7,7 @@ + diff --git a/HyperbolicDownloaderApi/Networking/NetworkClient.cs b/HyperbolicDownloaderApi/Networking/NetworkClient.cs index 0812239..3853c5e 100644 --- a/HyperbolicDownloaderApi/Networking/NetworkClient.cs +++ b/HyperbolicDownloaderApi/Networking/NetworkClient.cs @@ -1,6 +1,8 @@ using HyperbolicDownloaderApi.FileProcessing; using HyperbolicDownloaderApi.Managment; +using NAudio.Wave; + using System.Diagnostics; using System.Net; using System.Net.Sockets; @@ -143,6 +145,14 @@ internal class NetworkClient(FilesManager filesManager) _ = Upload(client, dataReceived[8..]); return; } + + if (dataReceived.StartsWith("StreamWav")) + { + ApiManager.SendNotificationMessageNewLine($"{(client.Client.RemoteEndPoint as IPEndPoint)?.Address} > Download request", NotificationMessageType.Debug); + _ = StreamWav(client, dataReceived[9..]); + return; + } + if (string.IsNullOrWhiteSpace(dataReceived)) { ApiManager.SendNotificationMessageNewLine($"{(client.Client.RemoteEndPoint as IPEndPoint)?.Address} > Disconnected", NotificationMessageType.Debug); @@ -181,7 +191,7 @@ internal class NetworkClient(FilesManager filesManager) client.Close(); - ApiManager.SendNotificationMessageNewLine($"{(client.Client.RemoteEndPoint as IPEndPoint)?.Address} > Connection closed", NotificationMessageType.Debug); + ApiManager.SendNotificationMessageNewLine($"{(client.Client?.RemoteEndPoint as IPEndPoint)?.Address} > Connection closed", NotificationMessageType.Debug); } private async Task Upload(TcpClient client, string hash) @@ -201,8 +211,6 @@ internal class NetworkClient(FilesManager filesManager) bytesToSend = Encoding.ASCII.GetBytes($"{fileInfo.Length}/{Path.GetFileName(hyperFileInfo.FilePath)}"); - Array.Resize(ref bytesToSend, 1000); - await nwStream.WriteAsync(bytesToSend); ApiManager.SendNotificationMessageNewLine($"{(client.Client.RemoteEndPoint as IPEndPoint)?.Address} > Starting file download of file [{Path.GetFileName(hyperFileInfo.FilePath)}] [{hash}]", NotificationMessageType.Log); @@ -232,7 +240,60 @@ internal class NetworkClient(FilesManager filesManager) client.Close(); - ApiManager.SendNotificationMessageNewLine($"{(client.Client.RemoteEndPoint as IPEndPoint)?.Address} > Connection closed", NotificationMessageType.Debug); + ApiManager.SendNotificationMessageNewLine($"{(client.Client?.RemoteEndPoint as IPEndPoint)?.Address} > Connection closed", NotificationMessageType.Debug); + } + } + + private async Task StreamWav(TcpClient client, string hash) + { + try + { + ApiManager.SendNotificationMessageNewLine($"{(client.Client.RemoteEndPoint as IPEndPoint)?.Address} > Requesting file stream [{hash}]", NotificationMessageType.Log); + byte[] bytesToSend; + hash = hash.Trim(); + NetworkStream nwStream = client.GetStream(); + client.SendBufferSize = 6400; + if (filesManager.TryGet(hash, out PrivateHyperFileInfo? hyperFileInfo) && File.Exists(hyperFileInfo?.FilePath)) + { + ApiManager.SendNotificationMessageNewLine($"{(client.Client.RemoteEndPoint as IPEndPoint)?.Address} > Accepting file stream [{Path.GetFileName(hyperFileInfo.FilePath)}] [{hash}]", NotificationMessageType.Log); + + using WaveFileReader reader = new WaveFileReader(hyperFileInfo.FilePath); + + bytesToSend = Encoding.ASCII.GetBytes($"{reader.Length}/{Path.GetFileName(hyperFileInfo.FilePath)}/{reader.WaveFormat.SampleRate}/{reader.WaveFormat.BitsPerSample}/{reader.WaveFormat.Channels}"); + + await nwStream.WriteAsync(bytesToSend); + + ApiManager.SendNotificationMessageNewLine($"{(client.Client.RemoteEndPoint as IPEndPoint)?.Address} > Starting file stream of file [{Path.GetFileName(hyperFileInfo.FilePath)}] [{hash}]", NotificationMessageType.Log); + + byte[]? buffer = new byte[64000]; + + while (reader.Position < reader.Length) + { + int bytesRead = reader.Read(buffer, 0, 6400); + await nwStream.WriteAsync(buffer.AsMemory(0, bytesRead)); + } + + ApiManager.SendNotificationMessageNewLine($"{(client.Client.RemoteEndPoint as IPEndPoint)?.Address} > Completed stream of file [{Path.GetFileName(hyperFileInfo.FilePath)}] [{hash}]", NotificationMessageType.Log); + } + else + { + bytesToSend = Encoding.ASCII.GetBytes("File not found!"); + ApiManager.SendNotificationMessageNewLine($"{(client.Client.RemoteEndPoint as IPEndPoint)?.Address} > File not found [{hash}]", NotificationMessageType.Log); + await nwStream.WriteAsync(bytesToSend); + } + } + catch (Exception ex) + { + ApiManager.SendNotificationMessageNewLine($"{(client.Client.RemoteEndPoint as IPEndPoint)?.Address} > Error streaming file {ex.Message} [{hash}]", NotificationMessageType.Log); + Debug.WriteLine(ex); + } + finally + { + ApiManager.SendNotificationMessageNewLine($"{(client.Client.RemoteEndPoint as IPEndPoint)?.Address} > Closing connection", NotificationMessageType.Debug); + + client.Close(); + + ApiManager.SendNotificationMessageNewLine($"{(client.Client?.RemoteEndPoint as IPEndPoint)?.Address} > Connection closed", NotificationMessageType.Debug); } } } \ No newline at end of file