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
[](https://www.nuget.org/packages/GoogleCast)