mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-07-25 03:17:00 +00:00
Renamed clients directory to senders
This commit is contained in:
parent
7405cca08f
commit
0d6856872f
30 changed files with 0 additions and 0 deletions
|
@ -1,355 +0,0 @@
|
|||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Net.WebSockets;
|
||||
using FCastClient;
|
||||
using NestedArgs;
|
||||
|
||||
internal class Program
|
||||
{
|
||||
private static async Task Main(string[] args)
|
||||
{
|
||||
Command rootCommand = new CommandBuilder("fcast", "Control FCast Receiver through the terminal.")
|
||||
.Option(new Option()
|
||||
{
|
||||
LongName = "connection_type",
|
||||
ShortName = 'c',
|
||||
Description = "Type of connection: tcp or ws (websocket)",
|
||||
DefaultValue = "tcp",
|
||||
IsRequired = false
|
||||
})
|
||||
.Option(new Option()
|
||||
{
|
||||
LongName = "host",
|
||||
ShortName = 'h',
|
||||
Description = "The host address to send the command to",
|
||||
IsRequired = true
|
||||
})
|
||||
.Option(new Option()
|
||||
{
|
||||
LongName = "port",
|
||||
ShortName = 'p',
|
||||
Description = "The port to send the command to",
|
||||
IsRequired = false
|
||||
})
|
||||
.SubCommand(new CommandBuilder("play", "Play media")
|
||||
.Option(new Option()
|
||||
{
|
||||
LongName = "mime_type",
|
||||
ShortName = 'm',
|
||||
Description = "Mime type (e.g., video/mp4)",
|
||||
IsRequired = true
|
||||
})
|
||||
.Option(new Option()
|
||||
{
|
||||
LongName = "file",
|
||||
ShortName = 'f',
|
||||
Description = "File content to play",
|
||||
IsRequired = false
|
||||
})
|
||||
.Option(new Option()
|
||||
{
|
||||
LongName = "url",
|
||||
ShortName = 'u',
|
||||
Description = "URL to the content",
|
||||
IsRequired = false
|
||||
})
|
||||
.Option(new Option()
|
||||
{
|
||||
LongName = "content",
|
||||
ShortName = 'c',
|
||||
Description = "The actual content",
|
||||
IsRequired = false
|
||||
})
|
||||
.Option(new Option()
|
||||
{
|
||||
LongName = "timestamp",
|
||||
ShortName = 't',
|
||||
Description = "Timestamp to start playing",
|
||||
DefaultValue = "0",
|
||||
IsRequired = false
|
||||
})
|
||||
.Option(new Option()
|
||||
{
|
||||
LongName = "speed",
|
||||
ShortName = 's',
|
||||
Description = "Factor to multiply playback speed by",
|
||||
DefaultValue = "1",
|
||||
IsRequired = false
|
||||
})
|
||||
.Option(new Option()
|
||||
{
|
||||
LongName = "header",
|
||||
ShortName = 'H',
|
||||
Description = "HTTP header to add to request",
|
||||
IsRequired = false,
|
||||
AllowMultiple = true
|
||||
})
|
||||
.Build())
|
||||
.SubCommand(new CommandBuilder("seek", "Seek to a timestamp")
|
||||
.Option(new Option()
|
||||
{
|
||||
LongName = "timestamp",
|
||||
ShortName = 't',
|
||||
Description = "Timestamp to start playing",
|
||||
IsRequired = true
|
||||
})
|
||||
.Build())
|
||||
.SubCommand(new CommandBuilder("pause", "Pause media").Build())
|
||||
.SubCommand(new CommandBuilder("resume", "Resume media").Build())
|
||||
.SubCommand(new CommandBuilder("stop", "Stop media").Build())
|
||||
.SubCommand(new CommandBuilder("listen", "Listen to incoming events").Build())
|
||||
.SubCommand(new CommandBuilder("setvolume", "Set the volume")
|
||||
.Option(new Option()
|
||||
{
|
||||
LongName = "volume",
|
||||
ShortName = 'v',
|
||||
Description = "Volume level (0-1)",
|
||||
IsRequired = true
|
||||
})
|
||||
.Build())
|
||||
.SubCommand(new CommandBuilder("setspeed", "Set the playback speed")
|
||||
.Option(new Option()
|
||||
{
|
||||
LongName = "speed",
|
||||
ShortName = 's',
|
||||
Description = "Factor to multiply playback speed by",
|
||||
IsRequired = true
|
||||
})
|
||||
.Build())
|
||||
.Build();
|
||||
|
||||
CommandMatches matches = rootCommand.Parse(args);
|
||||
Console.WriteLine(matches.ToString());
|
||||
|
||||
var host = matches.Value("host")!;
|
||||
var connectionType = matches.Value("connection_type")!;
|
||||
|
||||
var port = matches.ValueAsInt32("port") ?? connectionType switch
|
||||
{
|
||||
"tcp" => 46899,
|
||||
"ws" => 46898,
|
||||
_ => throw new Exception($"{connectionType} is not a valid connection type.")
|
||||
};
|
||||
|
||||
var cancellationTokenSource = new CancellationTokenSource();
|
||||
var cancellationToken = cancellationTokenSource.Token;
|
||||
Console.CancelKeyPress += (_, _) =>
|
||||
{
|
||||
cancellationTokenSource.Cancel();
|
||||
};
|
||||
|
||||
using var session = await EstablishConnection(host, port, connectionType, cancellationToken);
|
||||
await session.SendMessageAsync(Opcode.Version, new VersionMessage() { Version = 1 }, cancellationToken);
|
||||
|
||||
switch (matches.SubCommand)
|
||||
{
|
||||
case "play":
|
||||
{
|
||||
var playMatches = matches.SubCommandMatch!;
|
||||
var mimeType = playMatches.Value("mime_type")!;
|
||||
var timestamp = playMatches.ValueAsDouble("timestamp")!;
|
||||
var speed = playMatches.ValueAsDouble("speed")!;
|
||||
var headers = playMatches.Values("header")?.Select(SplitHeader).ToDictionary(v => v.Key, v => v.Value);
|
||||
|
||||
if (playMatches.Has("file"))
|
||||
{
|
||||
IPAddress localAddress;
|
||||
{
|
||||
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
socket.Connect(host, port);
|
||||
localAddress = (socket.LocalEndPoint as IPEndPoint)!.Address;
|
||||
}
|
||||
|
||||
var path = playMatches.Value("file")!;
|
||||
var (url, task) = HostFileAndGetUrl(localAddress, path, mimeType, cancellationToken);
|
||||
await session.SendMessageAsync(Opcode.Play, new PlayMessage()
|
||||
{
|
||||
Container = mimeType,
|
||||
Speed = speed,
|
||||
Time = timestamp,
|
||||
Url = url,
|
||||
Headers = headers
|
||||
}, cancellationToken);
|
||||
|
||||
Console.WriteLine("Waiting for video to finish. Press CTRL+C to exit.");
|
||||
await task;
|
||||
}
|
||||
else
|
||||
{
|
||||
var url = playMatches.Value("url");
|
||||
var content = playMatches.Value("content");
|
||||
|
||||
await session.SendMessageAsync(Opcode.Play, new PlayMessage()
|
||||
{
|
||||
Container = mimeType,
|
||||
Content = content,
|
||||
Speed = speed,
|
||||
Time = timestamp,
|
||||
Url = url,
|
||||
Headers = headers
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "seek":
|
||||
{
|
||||
await session.SendMessageAsync(Opcode.Seek, new SeekMessage() { Time = matches.SubCommandMatch!.ValueAsDouble("timestamp")!.Value }, cancellationToken);
|
||||
break;
|
||||
}
|
||||
case "pause":
|
||||
{
|
||||
await session.SendMessageAsync(Opcode.Pause, cancellationToken);
|
||||
break;
|
||||
}
|
||||
case "resume":
|
||||
{
|
||||
await session.SendMessageAsync(Opcode.Resume, cancellationToken);
|
||||
break;
|
||||
}
|
||||
case "stop":
|
||||
{
|
||||
await session.SendMessageAsync(Opcode.Stop, cancellationToken);
|
||||
break;
|
||||
}
|
||||
case "listen":
|
||||
{
|
||||
Console.WriteLine("Listening. Press CTRL+C to exit.");
|
||||
await session.ReceiveLoopAsync(cancellationToken);
|
||||
break;
|
||||
}
|
||||
case "setvolume":
|
||||
{
|
||||
await session.SendMessageAsync(Opcode.SetVolume, new SetVolumeMessage() { Volume = matches.SubCommandMatch!.ValueAsDouble("volume")!.Value }, cancellationToken);
|
||||
break;
|
||||
}
|
||||
case "setspeed":
|
||||
{
|
||||
await session.SendMessageAsync(Opcode.SetSpeed, new SetSpeedMessage() { Speed = matches.SubCommandMatch!.ValueAsDouble("speed")!.Value }, cancellationToken);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
Console.WriteLine("Invalid command. Use --help for more information.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetAvailablePort()
|
||||
{
|
||||
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||||
socket.Bind(new IPEndPoint(IPAddress.Any, 0));
|
||||
return ((IPEndPoint)socket.LocalEndPoint!).Port;
|
||||
}
|
||||
|
||||
public static (string url, Task serverTask) HostFileAndGetUrl(IPAddress localAddress, string filePath, string mimeType, CancellationToken cancellationToken)
|
||||
{
|
||||
var listener = new HttpListener();
|
||||
listener.Prefixes.Add($"http://{localAddress}:{GetAvailablePort()}/");
|
||||
listener.Start();
|
||||
|
||||
var url = listener.Prefixes.First();
|
||||
Console.WriteLine($"Server started on {url}.");
|
||||
|
||||
var serverTask = Task.Run(async () =>
|
||||
{
|
||||
DateTime lastRequestTime = DateTime.Now;
|
||||
int activeConnections = 0;
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
if (activeConnections == 0 && (DateTime.Now - lastRequestTime).TotalSeconds > 300)
|
||||
{
|
||||
Console.WriteLine("No activity on server, closing...");
|
||||
break;
|
||||
}
|
||||
|
||||
if (listener.IsListening)
|
||||
{
|
||||
var contextTask = listener.GetContextAsync();
|
||||
await Task.WhenAny(contextTask, Task.Delay(Timeout.Infinite, cancellationToken));
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
var context = contextTask.Result;
|
||||
Console.WriteLine("Request received.");
|
||||
|
||||
try
|
||||
{
|
||||
Interlocked.Increment(ref activeConnections);
|
||||
lastRequestTime = DateTime.Now;
|
||||
|
||||
var response = context.Response;
|
||||
response.ContentType = mimeType;
|
||||
using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
|
||||
await fileStream.CopyToAsync(response.OutputStream);
|
||||
response.OutputStream.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error handling request: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Decrement(ref activeConnections);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(5000);
|
||||
}
|
||||
}
|
||||
|
||||
listener.Stop();
|
||||
}, cancellationToken);
|
||||
|
||||
return (url, serverTask);
|
||||
}
|
||||
|
||||
public static (string Key, string Value) SplitHeader(string input)
|
||||
{
|
||||
int colonIndex = input.IndexOf(':');
|
||||
if (colonIndex == -1)
|
||||
{
|
||||
throw new Exception("Header format invalid");
|
||||
}
|
||||
|
||||
string beforeColon = input.Substring(0, colonIndex);
|
||||
string afterColon = input.Substring(colonIndex + 1);
|
||||
|
||||
return (beforeColon, afterColon);
|
||||
}
|
||||
|
||||
private static async Task<FCastSession> EstablishConnection(string host, int port, string connectionType, CancellationToken cancellationToken)
|
||||
{
|
||||
switch (connectionType.ToLower())
|
||||
{
|
||||
case "tcp":
|
||||
return await EstablishTcpConnection(host, port, cancellationToken);
|
||||
case "ws":
|
||||
return await EstablishWebSocketConnection(host, port, cancellationToken);
|
||||
default:
|
||||
throw new ArgumentException("Invalid connection type: " + connectionType);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<FCastSession> EstablishTcpConnection(string host, int port, CancellationToken cancellationToken)
|
||||
{
|
||||
TcpClient client = new TcpClient();
|
||||
await client.ConnectAsync(host, port, cancellationToken);
|
||||
return new FCastSession(client.GetStream());
|
||||
}
|
||||
|
||||
private static async Task<FCastSession> EstablishWebSocketConnection(string host, int port, CancellationToken cancellationToken)
|
||||
{
|
||||
ClientWebSocket webSocket = new ClientWebSocket();
|
||||
string scheme = "ws";
|
||||
string uriString = $"{scheme}://{host}:{port}";
|
||||
Uri serverUri = new Uri(uriString);
|
||||
|
||||
await webSocket.ConnectAsync(serverUri, cancellationToken);
|
||||
return new FCastSession(new WebSocketStream(webSocket));
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue