diff --git a/GoogleCast.ConsoleApp/GoogleCast.ConsoleApp.csproj b/GoogleCast.ConsoleApp/GoogleCast.ConsoleApp.csproj new file mode 100644 index 0000000..1c56fd1 --- /dev/null +++ b/GoogleCast.ConsoleApp/GoogleCast.ConsoleApp.csproj @@ -0,0 +1,18 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + diff --git a/GoogleCast.ConsoleApp/Program.cs b/GoogleCast.ConsoleApp/Program.cs new file mode 100644 index 0000000..4725aa9 --- /dev/null +++ b/GoogleCast.ConsoleApp/Program.cs @@ -0,0 +1,71 @@ + +using System.Net; +using CommandLine; +using GoogleCast; +using GoogleCast.Channels; +using GoogleCast.Models.Cast; + + +class ConsoleApp +{ + public class Options + { + public Options() { Host = string.Empty; } + + [Option('h', "host", Required = true, HelpText = "Host")] + public string Host { get; set; } + [Option('p', "port", Required = false, Default = 8009, HelpText = "Port")] + public int Port { get; set; } + [Option('t', "timeout", Required = false, Default = 5000, HelpText = "Timeout")] + public int Timeout { get; set; } + } + + static void Main(string[] args) + { + Parser.Default.ParseArguments(args) + .WithParsed(o => + { + Task.Run(async () => + { + var sender = new Sender(); + Console.Write("Connection..."); + + var connected = await sender.ConnectAsync(IPAddress.Parse(o.Host), o.Port, o.Timeout); + if (!connected) + { + Console.WriteLine("Unable to connect to " + o.Host); + return; + } + + Console.WriteLine("Done."); + + var alive = false; + var heartBeatChannel = sender.GetChannel(); + heartBeatChannel.PingReceived += (object? sender, GoogleCast.Models.HeartBeat.PingEvent e) => + { + alive = true; + }; + + Console.Write("Waiting for ChromeCast Feedback"); + while (!alive) + { + await Task.Delay(1000); + Console.Write("."); + + } + Console.WriteLine(" Done."); + + // Launch the default media receiver application + Console.Write("Initializing Cast Channel..."); + var castChannel = sender.GetChannel(); + await sender.LaunchAsync(castChannel); + Console.WriteLine(" Done."); + + // Load an example website + Console.Write("Load URL http://www.example.com..."); + await castChannel.LoadUrl(new CastInformation { Url = "https://www.example.com" }); + Console.WriteLine(" Done."); + }).GetAwaiter().GetResult(); + }); + } +} diff --git a/GoogleCast.SampleApp/MainViewModel.cs b/GoogleCast.SampleApp/MainViewModel.cs index ddf1b85..3f26bc5 100644 --- a/GoogleCast.SampleApp/MainViewModel.cs +++ b/GoogleCast.SampleApp/MainViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Drawing; using System.Linq; +using System.Net; using System.Threading.Tasks; using GoogleCast.Channels; using GoogleCast.Models.Media; @@ -31,6 +32,8 @@ public MainViewModel(IDeviceLocator deviceLocator, ISender sender) PauseCommand = new RelayCommand(async () => await TryAsync(PauseAsync), () => AreButtonsEnabled); StopCommand = new RelayCommand(async () => await TryAsync(StopAsync), () => AreButtonsEnabled); RefreshCommand = new RelayCommand(async () => await TryAsync(RefreshAsync), () => IsLoaded); + ConnectCommand = new RelayCommand(async () => await TryAsync(ConnectAsync), () => IsLoaded); + CastCommand = new RelayCommand(async () => await TryAsync(CastAsync), () => IsCastButtonEnabled); } private IDeviceLocator DeviceLocator { get; } @@ -68,6 +71,28 @@ public IReceiver? SelectedReceiver } } + private string? _editedReceiver; + /// + /// Gets or sets the selected receiver + /// + public string? EditedReceiver + { + get => _editedReceiver; + set + { + if (_editedReceiver != null && !_editedReceiver.Equals(value) || + _editedReceiver == null && value != null) + { + _editedReceiver = value; + IsInitialized = false; + OnPropertyChanged(nameof(EditedReceiver)); + NotifyButtonsCommandsCanExecuteChanged(); + } + } + } + + + private bool _isLoaded; /// /// Gets a value indicating whether the list of the GoogleCast devices is loaded or not @@ -82,6 +107,8 @@ private set _isLoaded = value; OnPropertyChanged(nameof(IsLoaded)); RefreshCommand.NotifyCanExecuteChanged(); + ConnectCommand.NotifyCanExecuteChanged(); + CastCommand.NotifyCanExecuteChanged(); NotifyButtonsCommandsCanExecuteChanged(); } } @@ -90,7 +117,22 @@ private set /// /// Gets a value indicating whether the Play, Pause and Stop buttons must be enabled or not /// - public bool AreButtonsEnabled => IsLoaded && SelectedReceiver != null && !string.IsNullOrWhiteSpace(Link); + public bool AreButtonsEnabled => IsLoaded && + ( + (SelectedReceiver != null && !string.IsNullOrWhiteSpace(Link)) + || + (EditedReceiver != null && !string.IsNullOrWhiteSpace(Link)) + ); + + /// + /// Gets a value indicating whether the Cast buttons must be enabled or not + /// + public bool IsCastButtonEnabled => IsLoaded && + ( + (SelectedReceiver != null && !string.IsNullOrWhiteSpace(CastUrl)) + || + (EditedReceiver != null && !string.IsNullOrWhiteSpace(CastUrl)) + ); private string? _playerState; /// @@ -121,6 +163,25 @@ public string Link } } + private string _castUrl = "https://example.com"; + /// + /// Gets or sets the url to cast + /// + public string CastUrl + { + get => _castUrl; + set + { + if (_castUrl != value) + { + _link = value; + IsInitialized = false; + OnPropertyChanged(nameof(CastUrl)); + NotifyButtonsCommandsCanExecuteChanged(); + } + } + } + private string _subtitle = "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/tracks/DesigningForGoogleCast-en.vtt"; /// /// Gets or sets the subtitle file @@ -196,6 +257,14 @@ private bool IsStopped /// Gets the refresh command /// public RelayCommand RefreshCommand { get; } + /// + /// Gets the connect command + /// + public RelayCommand ConnectCommand { get; } + /// + /// Gets the connect command + /// + public RelayCommand CastCommand { get; } private void NotifyButtonsCommandsCanExecuteChanged() { @@ -203,6 +272,7 @@ private void NotifyButtonsCommandsCanExecuteChanged() PlayCommand.NotifyCanExecuteChanged(); PauseCommand.NotifyCanExecuteChanged(); StopCommand.NotifyCanExecuteChanged(); + CastCommand.NotifyCanExecuteChanged(); } private async Task TryAsync(Func action) @@ -240,9 +310,39 @@ private async Task ConnectAsync() await Sender.ConnectAsync(selectedReceiver); return true; } + else + { + if(!string.IsNullOrEmpty(EditedReceiver)) + { + await Sender.ConnectAsync( + new Receiver + { + IPEndPoint = new System.Net.IPEndPoint( + IPAddress.Parse(EditedReceiver), + 8009 + ) + }); + return true; + } + } return false; } + private async Task CastAsync() + { + await SendChannelCommandAsync(true, + async channel => + { + var sender = Sender; + var castChannel = sender.GetChannel(); + await sender.LaunchAsync(castChannel); + await channel.LoadUrl(new Models.Cast.CastInformation { Url = CastUrl }); + }, + c=> Task.CompletedTask); + + return true; + } + private async Task PlayAsync() { await SendChannelCommandAsync(!IsInitialized || IsStopped, diff --git a/GoogleCast.SampleApp/MainWindow.xaml b/GoogleCast.SampleApp/MainWindow.xaml index 6b00c0c..75453da 100644 --- a/GoogleCast.SampleApp/MainWindow.xaml +++ b/GoogleCast.SampleApp/MainWindow.xaml @@ -4,25 +4,29 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" - Title="GoogleCast" Height="200" Width="784" + Title="GoogleCast" Height="229" Width="784" ResizeMode="CanResizeWithGrip" Loaded="WindowLoadedAsync" - DataContext="{Binding Main, Source={StaticResource ViewModelLocator}}"> + DataContext="{Binding Main, Source={StaticResource ViewModelLocator}}" WindowStartupLocation="CenterScreen"> + - + + + - - + + + @@ -37,6 +41,14 @@ + + + + + + + + @@ -44,7 +56,7 @@ - + @@ -56,6 +68,6 @@ Mute - + diff --git a/GoogleCast.sln b/GoogleCast.sln index 37a3a45..ab9bb9b 100644 --- a/GoogleCast.sln +++ b/GoogleCast.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31205.134 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32811.315 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GoogleCast", "GoogleCast\GoogleCast.csproj", "{0B336E66-C5C4-47B7-BD45-85847CCEF991}" EndProject @@ -13,6 +13,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .github\workflows\dotnet.yml = .github\workflows\dotnet.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoogleCast.ConsoleApp", "GoogleCast.ConsoleApp\GoogleCast.ConsoleApp.csproj", "{CA4470AE-1BD2-4BA3-86A8-45235252E0D1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {347B2A0B-04B1-40D7-A63E-D0077A651306}.Debug|Any CPU.Build.0 = Debug|Any CPU {347B2A0B-04B1-40D7-A63E-D0077A651306}.Release|Any CPU.ActiveCfg = Release|Any CPU {347B2A0B-04B1-40D7-A63E-D0077A651306}.Release|Any CPU.Build.0 = Release|Any CPU + {CA4470AE-1BD2-4BA3-86A8-45235252E0D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA4470AE-1BD2-4BA3-86A8-45235252E0D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA4470AE-1BD2-4BA3-86A8-45235252E0D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA4470AE-1BD2-4BA3-86A8-45235252E0D1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/GoogleCast/CastMessage.cs b/GoogleCast/CastMessage.cs index 02119ac..4ec424f 100644 --- a/GoogleCast/CastMessage.cs +++ b/GoogleCast/CastMessage.cs @@ -1,4 +1,5 @@ -using ProtoBuf; +using System.Runtime.Serialization; +using ProtoBuf; namespace GoogleCast { @@ -6,48 +7,56 @@ namespace GoogleCast /// Cast message /// [ProtoContract] + [DataContract] class CastMessage { /// /// Gets or sets the protocol version /// [ProtoMember(1, IsRequired = true, Name = "protocol_version")] + [DataMember] public ProtocolVersion ProtocolVersion { get; set; } /// /// Gets or sets the source identifier /// [ProtoMember(2, IsRequired = true, Name = "source_id")] + [DataMember] public string SourceId { get; set; } = "sender-0"; /// /// Gets or sets the destination identifier /// [ProtoMember(3, IsRequired = true, Name = "destination_id")] + [DataMember] public string DestinationId { get; set; } = "receiver-0"; /// /// Gets or sets the namespace /// [ProtoMember(4, IsRequired = true, Name = "namespace")] + [DataMember] public string Namespace { get; set; } = default!; /// /// Gets or sets the payload type /// [ProtoMember(5, IsRequired = true, Name = "payload_type")] + [DataMember] public PayloadType PayloadType { get; set; } = default!; /// /// Gets or sets the UTF-8 payload /// [ProtoMember(6, IsRequired = false, Name = "payload_utf8")] + [DataMember] public string? PayloadUtf8 { get; set; } /// /// Gets or sets the binary payload /// [ProtoMember(7, IsRequired = false, Name = "payload_binary")] + [DataMember] public byte[]? PayloadBinary { get; set; } } } diff --git a/GoogleCast/Channels/CastChannel.cs b/GoogleCast/Channels/CastChannel.cs new file mode 100644 index 0000000..9f13db2 --- /dev/null +++ b/GoogleCast/Channels/CastChannel.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using GoogleCast.Messages; +using GoogleCast.Messages.Cast; +using GoogleCast.Messages.Media; +using GoogleCast.Models.Cast; +using GoogleCast.Models.Media; +using GoogleCast.Models.Receiver; + +namespace GoogleCast.Channels +{ + /// + /// Cast channel + /// + class CastChannel : StatusChannel, CastStatusMessage>, ICastChannel + { + /// + /// Initializes a new instance of class + /// + public CastChannel() : base("urn:x-cast:com.madmod.dashcast") + { + } + + /// + public string ApplicationId { get; } = "84912283"; + + private Task GetApplicationAsync() + { + return Sender!.GetChannel().EnsureConnectionAsync(Namespace); + } + + public async Task LoadUrl(CastInformation information) + { + var message = new CastLoadMessage + { + Url = information.Url, + Force = false, + Reload = true, + ReloadTime = 0, + SessionId = (await GetApplicationAsync()).SessionId, + }; + + await SendAsync(message, (await GetApplicationAsync()).TransportId); + } + } +} diff --git a/GoogleCast/Channels/Channel.cs b/GoogleCast/Channels/Channel.cs index 0df3fe2..f6ea71c 100644 --- a/GoogleCast/Channels/Channel.cs +++ b/GoogleCast/Channels/Channel.cs @@ -23,7 +23,10 @@ protected Channel() /// namespace protected Channel(string ns) { - Namespace = $"{BASE_NAMESPACE}.{ns}"; + if(!ns.StartsWith("urn:")) + Namespace = $"{BASE_NAMESPACE}.{ns}"; + else + Namespace = ns; } /// diff --git a/GoogleCast/Channels/HeartbeatChannel.cs b/GoogleCast/Channels/HeartbeatChannel.cs index 6a8cee3..259f891 100644 --- a/GoogleCast/Channels/HeartbeatChannel.cs +++ b/GoogleCast/Channels/HeartbeatChannel.cs @@ -1,5 +1,7 @@ using GoogleCast.Messages; using GoogleCast.Messages.Heartbeat; +using GoogleCast.Models.HeartBeat; +using System; using System.Threading.Tasks; namespace GoogleCast.Channels @@ -16,12 +18,15 @@ public HeartbeatChannel() : base("tp.heartbeat") { } + public event EventHandler? PingReceived; + /// public override async Task OnMessageReceivedAsync(IMessage message) { switch (message) { case PingMessage: + PingReceived?.Invoke(this, new PingEvent { Date = DateTime.Now }); await SendAsync(new PongMessage()); break; } diff --git a/GoogleCast/Channels/ICastChannel.cs b/GoogleCast/Channels/ICastChannel.cs new file mode 100644 index 0000000..bb53c0b --- /dev/null +++ b/GoogleCast/Channels/ICastChannel.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using GoogleCast.Models.Cast; +using GoogleCast.Models.Media; + +namespace GoogleCast.Channels +{ + /// + /// Interface for the media channel + /// + public interface ICastChannel : IStatusChannel>, IApplicationChannel + { + /// + /// Retrieves the media status + /// + /// the media status + Task LoadUrl(CastInformation information); + } +} diff --git a/GoogleCast/Channels/IHeartbeatChannel.cs b/GoogleCast/Channels/IHeartbeatChannel.cs index 8fd4e66..f45299e 100644 --- a/GoogleCast/Channels/IHeartbeatChannel.cs +++ b/GoogleCast/Channels/IHeartbeatChannel.cs @@ -1,9 +1,16 @@ -namespace GoogleCast.Channels +using System; +using GoogleCast.Models.HeartBeat; + +namespace GoogleCast.Channels { /// /// Interface for the heartbeat channel /// - interface IHeartbeatChannel : IChannel + public interface IHeartbeatChannel : IChannel { + /// + /// Event when a ping is received + /// + public event EventHandler PingReceived; } -} \ No newline at end of file +} diff --git a/GoogleCast/ISender.cs b/GoogleCast/ISender.cs index 22b5fe6..5436ada 100644 --- a/GoogleCast/ISender.cs +++ b/GoogleCast/ISender.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Net; using System.Threading.Tasks; using GoogleCast.Channels; using GoogleCast.Messages; @@ -30,6 +31,24 @@ public interface ISender /// receiver Task ConnectAsync(IReceiver receiver); + + /// + /// Connects to a receiver + /// + /// receiver + /// timeout + /// Status of connection + Task ConnectAsync(IReceiver receiver, int timeout = 5000); + + /// + /// Connects to a receiver + /// + /// IP Address of the receiver + /// port + /// timeout + /// + Task ConnectAsync(IPAddress address, int port, int timeout); + /// /// Disconnects /// diff --git a/GoogleCast/Messages/Cast/CastLoadMessage.cs b/GoogleCast/Messages/Cast/CastLoadMessage.cs new file mode 100644 index 0000000..fcef770 --- /dev/null +++ b/GoogleCast/Messages/Cast/CastLoadMessage.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using System.Text; +using GoogleCast.Models.Cast; + +namespace GoogleCast.Messages.Cast +{ + /// + /// Message to load specific URL + /// + [DataContract] + [ReceptionMessage] + class CastLoadMessage : SessionMessage + { + /// + /// Gets or sets the url to load + /// + [DataMember(Name = "url")] + public string Url { get; set; } = default!; + + /// + /// Gets or sets force information + /// + [DataMember(Name = "force")] + public bool Force { get; set; } = false; + + /// + /// Gets or sets reload information + /// + [DataMember(Name = "reload")] + public bool Reload { get; set; } = false; + + /// + /// Gets or sets reload time + /// + [DataMember(Name = "reload_time")] + public int ReloadTime { get; set; } = 0; + } +} diff --git a/GoogleCast/Messages/Cast/CastStatusMessage.cs b/GoogleCast/Messages/Cast/CastStatusMessage.cs new file mode 100644 index 0000000..44da986 --- /dev/null +++ b/GoogleCast/Messages/Cast/CastStatusMessage.cs @@ -0,0 +1,15 @@ +using GoogleCast.Models.Cast; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace GoogleCast.Messages.Cast +{ + /// + /// Message to retrieve the cast status + /// + [DataContract] + [ReceptionMessage] + class CastStatusMessage : StatusMessage> + { + } +} diff --git a/GoogleCast/Models/Cast/CastInformation.cs b/GoogleCast/Models/Cast/CastInformation.cs new file mode 100644 index 0000000..86b388e --- /dev/null +++ b/GoogleCast/Models/Cast/CastInformation.cs @@ -0,0 +1,17 @@ +using System.Runtime.Serialization; + +namespace GoogleCast.Models.Cast +{ + /// + /// Cast Information + /// + [DataContract] + public class CastInformation + { + /// + /// Gets or sets the URL to display + /// + [DataMember(Name = "url")] + public string Url { get; set; } + } +} diff --git a/GoogleCast/Models/Cast/CastStatus.cs b/GoogleCast/Models/Cast/CastStatus.cs new file mode 100644 index 0000000..3648e9f --- /dev/null +++ b/GoogleCast/Models/Cast/CastStatus.cs @@ -0,0 +1,17 @@ +using System.Runtime.Serialization; + +namespace GoogleCast.Models.Cast +{ + /// + /// Media status + /// + [DataContract] + public class CastStatus + { + /// + /// Gets or sets the playback rate + /// + [DataMember(Name = "url")] + public string? Url { get; set; } + } +} diff --git a/GoogleCast/Models/HeartBeat/PingEvent.cs b/GoogleCast/Models/HeartBeat/PingEvent.cs new file mode 100644 index 0000000..4896d4c --- /dev/null +++ b/GoogleCast/Models/HeartBeat/PingEvent.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace GoogleCast.Models.HeartBeat +{ + public class PingEvent + { + public DateTime Date { get; set; } + } +} diff --git a/GoogleCast/Sender.cs b/GoogleCast/Sender.cs index 08bdb50..2debb0a 100644 --- a/GoogleCast/Sender.cs +++ b/GoogleCast/Sender.cs @@ -4,12 +4,14 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Net; using System.Net.Security; using System.Net.Sockets; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Timers; using GoogleCast.Channels; using GoogleCast.Messages; using GoogleCast.Models.Receiver; @@ -119,21 +121,41 @@ public TChannel GetChannel() where TChannel : IChannel /// public async Task ConnectAsync(IReceiver receiver) + { + if(!await ConnectAsync(receiver, 10000)) + throw new TimeoutException("Connect Timeout"); + } + + /// + public async Task ConnectAsync(IPAddress address, int port, int timeout = 10000) + { + var receiver = new Receiver + { + IPEndPoint = new System.Net.IPEndPoint(address, port) + }; + return await ConnectAsync(receiver, timeout); + } + + /// + public async Task ConnectAsync(IReceiver receiver, int timeout = 10000) { Dispose(); Receiver = receiver; - var tcpClient = new TcpClient(); - TcpClient = tcpClient; - var ipEndPoint = receiver.IPEndPoint; - var host = ipEndPoint.Address.ToString(); - await tcpClient.ConnectAsync(host, ipEndPoint.Port); - var secureStream = new SslStream(tcpClient.GetStream(), true, (sender, certificate, chain, sslPolicyErrors) => true); - await secureStream.AuthenticateAsClientAsync(host); + TcpClient = new TcpClient(); + var result = TcpClient.BeginConnect(receiver.IPEndPoint.Address.ToString(), receiver.IPEndPoint.Port, null, null); + var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(timeout)); + if (!success) + { + return false; + } + var secureStream = new SslStream(TcpClient.GetStream(), true, (sender, certificate, chain, sslPolicyErrors) => true); + await secureStream.AuthenticateAsClientAsync(receiver.IPEndPoint.Address.ToString()); NetworkStream = secureStream; Receive(); await GetChannel().ConnectAsync(); + return true; } private void Receive() @@ -147,39 +169,44 @@ private void Receive() { var channels = Channels; var messageTypes = ServiceProvider.GetRequiredService(); - while (true) + while (!cancellationToken.IsCancellationRequested) { var buffer = await ReadAsync(4, cancellationToken); - if (BitConverter.IsLittleEndian) - { - Array.Reverse(buffer); - } - var length = BitConverter.ToInt32(buffer, 0); - CastMessage castMessage; - using (var ms = new MemoryStream()) + if (!cancellationToken.IsCancellationRequested) { - await ms.WriteAsync(await ReadAsync(length, cancellationToken), 0, length, cancellationToken); - ms.Position = 0; - castMessage = Serializer.Deserialize(ms); - } - var payload = (castMessage.PayloadType == PayloadType.Binary ? - Encoding.UTF8.GetString(castMessage.PayloadBinary) : castMessage.PayloadUtf8)!; - Debug.WriteLine($"RECEIVED: {castMessage.Namespace} : {payload}"); - var channel = channels.FirstOrDefault(c => c.Namespace == castMessage.Namespace); - if (channel != null) - { - var message = JsonSerializer.Deserialize(payload)!; - if (messageTypes.TryGetValue(message.Type, out var type)) + if (BitConverter.IsLittleEndian) { - try - { - var response = (IMessage)JsonSerializer.Deserialize(type, payload)!; - await channel.OnMessageReceivedAsync(response); - TaskCompletionSourceInvoke(message, "SetResult", response); - } - catch (Exception ex) + Array.Reverse(buffer); + } + var length = BitConverter.ToInt32(buffer, 0); + CastMessage castMessage; + using (var ms = new MemoryStream()) + { + await ms.WriteAsync(await ReadAsync(length, cancellationToken), 0, length, cancellationToken); + ms.Position = 0; + castMessage = Serializer.Deserialize(ms); + } + var payload = (castMessage.PayloadType == PayloadType.Binary ? + Encoding.UTF8.GetString(castMessage.PayloadBinary) : castMessage.PayloadUtf8)!; + + Debug.WriteLine($"RECEIVED { Encoding.Default.GetString(JsonSerializer.Serialize( castMessage)) }"); + + var channel = channels.FirstOrDefault(c => c.Namespace == castMessage.Namespace); + if (channel != null) + { + var message = JsonSerializer.Deserialize(payload)!; + if (messageTypes.TryGetValue(message.Type, out var type)) { - TaskCompletionSourceInvoke(message, "SetException", ex, new Type[] { typeof(Exception) }); + try + { + var response = (IMessage)JsonSerializer.Deserialize(type, payload)!; + await channel.OnMessageReceivedAsync(response); + TaskCompletionSourceInvoke(message, "SetResult", response); + } + catch (Exception ex) + { + TaskCompletionSourceInvoke(message, "SetException", ex, new Type[] { typeof(Exception) }); + } } } } @@ -204,18 +231,26 @@ private void TaskCompletionSourceInvoke(MessageWithId message, string method, ob private async Task ReadAsync(int bufferLength, CancellationToken cancellationToken) { - var buffer = new byte[bufferLength]; - int nb, length = 0; - while (length < bufferLength) + try { - nb = await NetworkStream!.ReadAsync(buffer, length, bufferLength - length, cancellationToken); - if (nb == 0) + var buffer = new byte[bufferLength]; + int nb, length = 0; + while (length < bufferLength) { - throw new InvalidOperationException(); + nb = await NetworkStream!.ReadAsync(buffer, length, bufferLength - length, cancellationToken); + if (nb == 0) + { + throw new InvalidOperationException(); + } + length += nb; } - length += nb; + return buffer; + } + catch(OperationCanceledException ex) + { + // Swallow Exception since Cancelation was requested by the Dispose() + return new byte[0]; } - return buffer; } private async Task EnsureConnectionAsync() @@ -244,7 +279,7 @@ private async Task SendAsync(CastMessage castMessage) await SendSemaphoreSlim.WaitAsync(); try { - Debug.WriteLine($"SENT : {castMessage.DestinationId}: {castMessage.PayloadUtf8}"); + Debug.WriteLine($"SENT: { Encoding.Default.GetString(JsonSerializer.Serialize( castMessage)) }"); byte[] message; using (var ms = new MemoryStream()) diff --git a/README.md b/README.md index 6dd8a7f..9ea4906 100644 --- a/README.md +++ b/README.md @@ -21,5 +21,27 @@ var mediaStatus = await mediaChannel.LoadAsync( new MediaInformation() { ContentId = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" }); ``` +For Website casting purpose, with a static address + +```cs +// Connect to the Chromecast +await sender.ConnectAsync( + new Receiver + { + IPEndPoint = new System.Net.IPEndPoint( + IPAddress.Parse("10.0.0.2"), + 8009 + ) + }); + +// Launch the default media receiver application +var castChannel = sender.GetChannel(); +await sender.LaunchAsync(castChannel); + +// Load an example website +await castChannel.LoadUrl(new CastInformation { Url = "https://www.example.com" }); + +``` + ## Download [![NuGet](https://img.shields.io/nuget/v/GoogleCast.svg)](https://www.nuget.org/packages/GoogleCast)