From 46ec72ca92cc21decd980bcdb6cf7ff544deef5b Mon Sep 17 00:00:00 2001 From: Teemu Tapanila Date: Sun, 21 Jul 2024 20:40:06 +0300 Subject: [PATCH 1/7] Initial test with dotnet 8 and AOT --- SharpCaster.Console/Program.cs | 19 +++++++ .../SharpCaster.Console.csproj | 16 ++++++ SharpCaster.sln | 6 +++ Sharpcaster.Test/MediaChannelTester.cs | 6 +-- Sharpcaster.Test/helper/TestHelper.cs | 8 +-- Sharpcaster/Channels/MediaChannel.cs | 6 +-- Sharpcaster/ChromeCastClient.cs | 51 ++++++++++++------- Sharpcaster/ChromecastLocator.cs | 2 +- Sharpcaster/Sharpcaster.csproj | 3 +- 9 files changed, 87 insertions(+), 30 deletions(-) create mode 100644 SharpCaster.Console/Program.cs create mode 100644 SharpCaster.Console/SharpCaster.Console.csproj diff --git a/SharpCaster.Console/Program.cs b/SharpCaster.Console/Program.cs new file mode 100644 index 0000000..365ff1f --- /dev/null +++ b/SharpCaster.Console/Program.cs @@ -0,0 +1,19 @@ +// See https://aka.ms/new-console-template for more information +using Sharpcaster.Interfaces; +using Sharpcaster; +using Sharpcaster.Models.Media; + +MdnsChromecastLocator locator = new(); +var source = new CancellationTokenSource(TimeSpan.FromMilliseconds(1500)); +var chromecasts = await locator.FindReceiversAsync(source.Token); + +var chromecast = chromecasts.First(); +var client = new ChromecastClient(); +await client.ConnectChromecast(chromecast); +_ = await client.LaunchApplicationAsync("B3419EF5"); + +var media = new Media +{ + ContentUrl = "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/mp4/DesigningForGoogleCast.mp4" +}; +_ = await client.MediaChannel.LoadAsync(media); diff --git a/SharpCaster.Console/SharpCaster.Console.csproj b/SharpCaster.Console/SharpCaster.Console.csproj new file mode 100644 index 0000000..316d17d --- /dev/null +++ b/SharpCaster.Console/SharpCaster.Console.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + true + true + + + + + + + diff --git a/SharpCaster.sln b/SharpCaster.sln index 5014bf6..bfcb414 100644 --- a/SharpCaster.sln +++ b/SharpCaster.sln @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sharpcaster", "Sharpcaster\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sharpcaster.Test", "Sharpcaster.Test\Sharpcaster.Test.csproj", "{C8C0A3A8-C6AC-4E1B-8D3B-E6764C45C35B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCaster.Console", "SharpCaster.Console\SharpCaster.Console.csproj", "{D31168F2-F70A-45A9-84B7-81A1B4956A2E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {C8C0A3A8-C6AC-4E1B-8D3B-E6764C45C35B}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8C0A3A8-C6AC-4E1B-8D3B-E6764C45C35B}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8C0A3A8-C6AC-4E1B-8D3B-E6764C45C35B}.Release|Any CPU.Build.0 = Release|Any CPU + {D31168F2-F70A-45A9-84B7-81A1B4956A2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D31168F2-F70A-45A9-84B7-81A1B4956A2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D31168F2-F70A-45A9-84B7-81A1B4956A2E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D31168F2-F70A-45A9-84B7-81A1B4956A2E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Sharpcaster.Test/MediaChannelTester.cs b/Sharpcaster.Test/MediaChannelTester.cs index 0dd72be..6ce3a27 100644 --- a/Sharpcaster.Test/MediaChannelTester.cs +++ b/Sharpcaster.Test/MediaChannelTester.cs @@ -90,7 +90,7 @@ public async Task TestLoadingMediaQueueAndNavigateNextPrev(ChromecastReceiver re AutoResetEvent _autoResetEvent = new AutoResetEvent(false); IMediaChannel mediaChannel = client.MediaChannel; - QueueItem[] MyCd = TestHelper.CreateTestCd(); + QueueItem[] MyCd = Test.TestHelper.CreateTestCd(); int testSequenceCount = 0; var mediaStatusChanged = 0; @@ -158,7 +158,7 @@ public async Task TestLoadMediaQueueAndCheckContent(ChromecastReceiver receiver) var TestHelper = new TestHelper(); ChromecastClient client = await TestHelper.CreateConnectAndLoadAppClient(output, receiver); - QueueItem[] MyCd = TestHelper.CreateTestCd(); + QueueItem[] MyCd = Test.TestHelper.CreateTestCd(); MediaStatus status = await client.MediaChannel.QueueLoadAsync(MyCd); @@ -190,7 +190,7 @@ public async Task TestLoadingMediaQueue(ChromecastReceiver receiver) { var TestHelper = new TestHelper(); ChromecastClient client = await TestHelper.CreateConnectAndLoadAppClient(output, receiver); - QueueItem[] MyCd = TestHelper.CreateTestCd(); + QueueItem[] MyCd = Test.TestHelper.CreateTestCd(); MediaStatus status = await client.MediaChannel.QueueLoadAsync(MyCd); diff --git a/Sharpcaster.Test/helper/TestHelper.cs b/Sharpcaster.Test/helper/TestHelper.cs index 155e4aa..05a9a65 100644 --- a/Sharpcaster.Test/helper/TestHelper.cs +++ b/Sharpcaster.Test/helper/TestHelper.cs @@ -170,7 +170,7 @@ public async Task CreateConnectAndLoadAppClient(string appId = { TestOutput = null; var chromecast = FindChromecast(); - ChromecastClient cc = new ChromecastClient(); + ChromecastClient cc = new(); await cc.ConnectChromecast(chromecast); await cc.LaunchApplicationAsync(appId, false); return cc; @@ -205,7 +205,7 @@ private Mock> CreateILoggerMock() var formatter = invocation.Arguments[4]; var invokeMethod = formatter.GetType().GetMethod("Invoke"); - var logMessage = (string)invokeMethod?.Invoke(formatter, new[] { state, exception }); + var logMessage = (string)invokeMethod?.Invoke(formatter, [state, exception]); var testingName = typeof(T).GetGenericArguments().FirstOrDefault()?.Name; @@ -241,7 +241,7 @@ public ILoggerFactory CreateMockedLoggerFactory(List assertableLog = nul var formatter = invocation.Arguments[4]; var invokeMethod = formatter.GetType().GetMethod("Invoke"); - var logMessage = (string)invokeMethod?.Invoke(formatter, new[] { state, exception }); + var logMessage = (string)invokeMethod?.Invoke(formatter, [state, exception]); try { @@ -297,7 +297,7 @@ public ILoggerFactory CreateMockedLoggerFactory(List assertableLog = nul } - public QueueItem[] CreateTestCd() + public static QueueItem[] CreateTestCd() { QueueItem[] MyCd = [ diff --git a/Sharpcaster/Channels/MediaChannel.cs b/Sharpcaster/Channels/MediaChannel.cs index 7daa8fc..b185de7 100644 --- a/Sharpcaster/Channels/MediaChannel.cs +++ b/Sharpcaster/Channels/MediaChannel.cs @@ -43,16 +43,16 @@ private async Task SendAsync(IMessageWithId message, ChromecastAppl } catch (Exception ex) { - _logger?.LogError($"Error sending message: {ex.Message}"); + _logger?.LogError("Error sending message: {Message}", ex.Message); Status = null; - throw ex; + throw; } } private async Task SendAsync(MediaSessionMessage message) { var chromecastStatus = Client.GetChromecastStatus(); - message.MediaSessionId = Status?.First().MediaSessionId ?? throw new ArgumentNullException("MediaSessionId"); + message.MediaSessionId = Status?.First().MediaSessionId ?? throw new ArgumentNullException(nameof(message), "MediaSessionID"); return await SendAsync(message, chromecastStatus.Applications[0]); } diff --git a/Sharpcaster/ChromeCastClient.cs b/Sharpcaster/ChromeCastClient.cs index 35af497..dd203f3 100644 --- a/Sharpcaster/ChromeCastClient.cs +++ b/Sharpcaster/ChromeCastClient.cs @@ -6,6 +6,12 @@ using Sharpcaster.Extensions; using Sharpcaster.Interfaces; using Sharpcaster.Messages; +using Sharpcaster.Messages.Connection; +using Sharpcaster.Messages.Heartbeat; +using Sharpcaster.Messages.Media; +using Sharpcaster.Messages.Multizone; +using Sharpcaster.Messages.Queue; +using Sharpcaster.Messages.Receiver; using Sharpcaster.Models; using Sharpcaster.Models.ChromecastStatus; using Sharpcaster.Models.Media; @@ -13,6 +19,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net.Security; @@ -49,7 +56,7 @@ public class ChromecastClient : IChromecastClient private TaskCompletionSource ReceiveTcs { get; set; } private SemaphoreSlim SendSemaphoreSlim { get; } = new SemaphoreSlim(1, 1); - private IDictionary MessageTypes { get; set; } + private Dictionary MessageTypes { get; set; } private IEnumerable Channels { get; set; } private ConcurrentDictionary WaitingTasks { get; } = new ConcurrentDictionary(); @@ -68,12 +75,20 @@ public ChromecastClient(ILoggerFactory loggerFactory = null) serviceCollection.AddTransient(); serviceCollection.AddTransient(); var messageInterfaceType = typeof(IMessage); - foreach (var type in (from t in typeof(IConnectionChannel).GetTypeInfo().Assembly.GetTypes() - where t.GetTypeInfo().IsClass && !t.GetTypeInfo().IsAbstract && messageInterfaceType.IsAssignableFrom(t) && t.GetTypeInfo().GetCustomAttribute() != null - select t)) - { - serviceCollection.AddTransient(messageInterfaceType, type); - } + serviceCollection.AddTransient(messageInterfaceType, typeof(CloseMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(PingMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(ReceiverStatusMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(LoadFailedMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(LoadCancelledMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(MediaStatusMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(MultizoneStatusMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(DeviceUpdatedMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(QueueChangeMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(QueueItemsMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(QueueItemIdsMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(LaunchErrorMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(InvalidRequestMessage)); + Init(serviceCollection); } @@ -96,8 +111,8 @@ private void Init(IServiceCollection serviceCollection) Channels = channels; _logger = serviceProvider.GetService>(); - _logger?.LogDebug(MessageTypes.Keys.ToString(",")); - _logger?.LogDebug(Channels.ToString(",")); + _logger?.LogDebug("MessageTypes: {MessageTypes}", MessageTypes.Keys.ToString(",")); + _logger?.LogDebug("Channels: {Channels}", Channels.ToString(",")); foreach (var channel in Channels) { @@ -159,7 +174,7 @@ private void Receive() { HeartbeatChannel.StopTimeoutTimer(); } - channel?._logger?.LogTrace($"RECEIVED: {payload}"); + channel?._logger?.LogTrace("RECEIVED: {Payload}", payload); var message = JsonConvert.DeserializeObject(payload); if (MessageTypes.TryGetValue(message.Type, out Type type)) @@ -173,8 +188,8 @@ private void Receive() } catch (Exception ex) { - _logger?.LogError($"Exception processing the Response: {ex.Message}"); - TaskCompletionSourceInvoke(ref tcs, message, "SetException", ex, new Type[] { typeof(Exception) }); + _logger?.LogError("Exception processing the Response: {Message}", ex.Message); + TaskCompletionSourceInvoke(ref tcs, message, "SetException", ex, [typeof(Exception)]); } } else @@ -185,13 +200,13 @@ private void Receive() } } else { - _logger?.LogError($"Couldn't parse the channel from: {castMessage.Namespace} : {payload}"); + _logger?.LogError("Couldn't parse the channel from: {NameSpace} : {Payload}", castMessage.Namespace, payload); } } } catch (Exception exception) { - _logger?.LogError($"Error in receive loop: {exception.Message}"); + _logger?.LogError("Error in receive loop: {Message}", exception.Message); //await Dispose(false); ReceiveTcs.SetResult(true); } @@ -207,7 +222,7 @@ private void TaskCompletionSourceInvoke(ref object tcs, MessageWithId message, s } if (tcs != null) { var tcsType = tcs.GetType(); - (types == null ? tcsType.GetMethod(method) : tcsType.GetMethod(method, types)).Invoke(tcs, new object[] { parameter }); + (types == null ? tcsType.GetMethod(method) : tcsType.GetMethod(method, types)).Invoke(tcs, [parameter]); } } @@ -223,10 +238,10 @@ private async Task SendAsync(ILogger channelLogger, CastMessage castMessage) await SendSemaphoreSlim.WaitAsync(); try { - ((channelLogger!=null)?channelLogger:_logger)?.LogTrace($"SENT : {castMessage.DestinationId}: {castMessage.PayloadUtf8}"); + (channelLogger ?? _logger)?.LogTrace("SENT: {Destination}: {PayLoad}", castMessage.DestinationId, castMessage.PayloadUtf8); byte[] message = castMessage.ToProto(); var networkStream = _stream; - await networkStream.WriteAsync(message, 0, message.Length); + await networkStream.WriteAsync(message); await networkStream.FlushAsync(); } finally @@ -295,7 +310,7 @@ private void Dispose(IDisposable disposable, Action action) } catch (Exception ex) { - _logger?.LogError($"Error on disposing. {ex.Message}"); + _logger?.LogError("Error on disposing. {Message}", ex.Message); } finally { diff --git a/Sharpcaster/ChromecastLocator.cs b/Sharpcaster/ChromecastLocator.cs index 7329b38..41b2c0c 100644 --- a/Sharpcaster/ChromecastLocator.cs +++ b/Sharpcaster/ChromecastLocator.cs @@ -43,7 +43,7 @@ private void OnServiceAdded(object sender, ServiceAnnouncementEventArgs e) { return i.Split('='); } - return new string[] { "", "" }; + return ["", ""]; }) .ToDictionary(y => y[0], y => y[1]); if (!txtValues.ContainsKey("fn")) return; diff --git a/Sharpcaster/Sharpcaster.csproj b/Sharpcaster/Sharpcaster.csproj index c8bee9c..00be718 100644 --- a/Sharpcaster/Sharpcaster.csproj +++ b/Sharpcaster/Sharpcaster.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + net8.0 Sharpcaster git https://github.com/Tapanila/SharpCaster/ @@ -15,6 +15,7 @@ MIT sharpcaster-logo-64x64.png README.md + true From 3920aecdf743647ea6bdd272d76f1dbf5350159e Mon Sep 17 00:00:00 2001 From: Teemu Tapanila Date: Mon, 2 Dec 2024 22:33:34 +0100 Subject: [PATCH 2/7] updated to dotnet 9 --- SharpCaster.Console/SharpCaster.Console.csproj | 2 +- Sharpcaster.Test/Sharpcaster.Test.csproj | 2 +- Sharpcaster/Sharpcaster.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/SharpCaster.Console/SharpCaster.Console.csproj b/SharpCaster.Console/SharpCaster.Console.csproj index 316d17d..fb553c8 100644 --- a/SharpCaster.Console/SharpCaster.Console.csproj +++ b/SharpCaster.Console/SharpCaster.Console.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable enable true diff --git a/Sharpcaster.Test/Sharpcaster.Test.csproj b/Sharpcaster.Test/Sharpcaster.Test.csproj index 28a9a24..6c8f6e5 100644 --- a/Sharpcaster.Test/Sharpcaster.Test.csproj +++ b/Sharpcaster.Test/Sharpcaster.Test.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 diff --git a/Sharpcaster/Sharpcaster.csproj b/Sharpcaster/Sharpcaster.csproj index 056b49f..e8e4283 100644 --- a/Sharpcaster/Sharpcaster.csproj +++ b/Sharpcaster/Sharpcaster.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 Sharpcaster git https://github.com/Tapanila/SharpCaster/ From f69f135c0bf6395fa777d568d61c0f5f3a463b1a Mon Sep 17 00:00:00 2001 From: Teemu Tapanila Date: Mon, 2 Dec 2024 23:13:34 +0100 Subject: [PATCH 3/7] fixing aot warnings --- Sharpcaster/Channels/StatusChannel.cs | 5 +++++ Sharpcaster/ChromeCastClient.cs | 15 +++++++-------- Sharpcaster/ChromecastLocator.cs | 2 +- Sharpcaster/Interfaces/IStatusChannel.cs | 5 +++++ Sharpcaster/Models/CastMessage.cs | 5 +++-- Sharpcaster/Sharpcaster.csproj | 4 ++-- 6 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Sharpcaster/Channels/StatusChannel.cs b/Sharpcaster/Channels/StatusChannel.cs index aae7d4a..1989a47 100644 --- a/Sharpcaster/Channels/StatusChannel.cs +++ b/Sharpcaster/Channels/StatusChannel.cs @@ -54,5 +54,10 @@ protected virtual void OnStatusChanged() { StatusChanged?.Invoke(this, EventArgs.Empty); } + + public void ClearStatus() + { + Status = default; + } } } diff --git a/Sharpcaster/ChromeCastClient.cs b/Sharpcaster/ChromeCastClient.cs index 82139da..c1838bb 100644 --- a/Sharpcaster/ChromeCastClient.cs +++ b/Sharpcaster/ChromeCastClient.cs @@ -190,7 +190,7 @@ private void Receive() catch (Exception ex) { _logger?.LogError("Exception processing the Response: {Message}", ex.Message); - TaskCompletionSourceInvoke(ref tcs, message, "SetException", ex, [typeof(Exception)]); + TaskCompletionSourceInvoke(ref tcs, message, "SetException", ex, new Type[] { typeof(Exception) }); } } else @@ -227,7 +227,7 @@ private void TaskCompletionSourceInvoke(ref object tcs, MessageWithId message, s if (tcs != null) { var tcsType = tcs.GetType(); - (types == null ? tcsType.GetMethod(method) : tcsType.GetMethod(method, types)).Invoke(tcs, [parameter]); + (types == null ? tcsType.GetMethod(method) : tcsType.GetMethod(method, types)).Invoke(tcs, new object[] { parameter }); } } @@ -246,7 +246,7 @@ private async Task SendAsync(ILogger channelLogger, CastMessage castMessage) (channelLogger ?? _logger)?.LogTrace($"SENT : {castMessage.DestinationId}: {castMessage.PayloadUtf8}"); byte[] message = castMessage.ToProto(); var networkStream = _stream; - await networkStream.WriteAsync(message); + await networkStream.WriteAsync(message, 0, message.Length); await networkStream.FlushAsync(); } finally @@ -277,7 +277,7 @@ public async Task DisconnectAsync() { foreach (var channel in GetStatusChannels()) { - channel.GetType().GetProperty("Status").SetValue(channel, null); + channel.ClearStatus(); } HeartbeatChannel.StopTimeoutTimer(); HeartbeatChannel.StatusChanged -= HeartBeatTimedOut; @@ -364,10 +364,9 @@ public async Task LaunchApplicationAsync(string applicationId, return await ReceiverChannel.GetChromecastStatusAsync(); } - private IEnumerable GetStatusChannels() + private IEnumerable> GetStatusChannels() { - var statusChannelType = typeof(IStatusChannel<>); - return Channels.Where(c => c.GetType().GetInterfaces().Any(i => i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == statusChannelType)); + return Channels.OfType>(); } /// @@ -376,7 +375,7 @@ private IEnumerable GetStatusChannels() /// a dictionnary of namespace/status public IDictionary GetStatuses() { - return GetStatusChannels().ToDictionary(c => c.Namespace, c => c.GetType().GetProperty("Status").GetValue(c)); + return GetStatusChannels().ToDictionary(c => c.Namespace, c => c.Status); } public ChromecastStatus GetChromecastStatus() diff --git a/Sharpcaster/ChromecastLocator.cs b/Sharpcaster/ChromecastLocator.cs index 3d7e4a1..53c77b1 100644 --- a/Sharpcaster/ChromecastLocator.cs +++ b/Sharpcaster/ChromecastLocator.cs @@ -43,7 +43,7 @@ private void OnServiceAdded(object sender, ServiceAnnouncementEventArgs e) { return i.Split('='); } - return ["", ""]; + return new string[] { "", "" }; }) .ToDictionary(y => y[0], y => y[1]); if (!txtValues.ContainsKey("fn")) return; diff --git a/Sharpcaster/Interfaces/IStatusChannel.cs b/Sharpcaster/Interfaces/IStatusChannel.cs index e2dd09b..22b5ca0 100644 --- a/Sharpcaster/Interfaces/IStatusChannel.cs +++ b/Sharpcaster/Interfaces/IStatusChannel.cs @@ -17,5 +17,10 @@ public interface IStatusChannel : IChromecastChannel /// Gets the status /// TStatus Status { get; } + + /// + /// Clears the status + /// + void ClearStatus(); } } diff --git a/Sharpcaster/Models/CastMessage.cs b/Sharpcaster/Models/CastMessage.cs index ff0d605..7a0ec1e 100644 --- a/Sharpcaster/Models/CastMessage.cs +++ b/Sharpcaster/Models/CastMessage.cs @@ -1,5 +1,6 @@ using Google.Protobuf; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Sharpcaster; using System; using System.Linq; @@ -43,9 +44,9 @@ public string GetJsonType() return string.Empty; } - dynamic stuff = JsonConvert.DeserializeObject(PayloadUtf8); + var stuff = JObject.Parse(PayloadUtf8); - return stuff.type; + return stuff["type"]?.ToString() ?? string.Empty; } } } diff --git a/Sharpcaster/Sharpcaster.csproj b/Sharpcaster/Sharpcaster.csproj index e8e4283..c52bde0 100644 --- a/Sharpcaster/Sharpcaster.csproj +++ b/Sharpcaster/Sharpcaster.csproj @@ -1,7 +1,7 @@  - net9.0 + net9.0;netstandard2.0 Sharpcaster git https://github.com/Tapanila/SharpCaster/ @@ -15,7 +15,7 @@ MIT sharpcaster-logo-64x64.png README.md - true + true From 0df09fc919a9a7db4d54fd6eb4a3839b60588483 Mon Sep 17 00:00:00 2001 From: Teemu Tapanila Date: Tue, 3 Dec 2024 09:56:51 +0100 Subject: [PATCH 4/7] rewrote logic to avoid reflection usage --- Sharpcaster/Channels/MediaChannel.cs | 2 +- Sharpcaster/Channels/ReceiverChannel.cs | 2 +- Sharpcaster/ChromeCastClient.cs | 54 +++++++++---------- Sharpcaster/ChromecastLocator.cs | 13 +++-- .../Models/SharpCasterTaskCompletionSource.cs | 37 +++++++++++++ 5 files changed, 73 insertions(+), 35 deletions(-) create mode 100644 Sharpcaster/Models/SharpCasterTaskCompletionSource.cs diff --git a/Sharpcaster/Channels/MediaChannel.cs b/Sharpcaster/Channels/MediaChannel.cs index c625b31..f9c4d2c 100644 --- a/Sharpcaster/Channels/MediaChannel.cs +++ b/Sharpcaster/Channels/MediaChannel.cs @@ -47,7 +47,7 @@ private async Task SendAsync(IMessageWithId message, ChromecastAppl } catch (Exception ex) { - Logger?.LogError($"Error sending message: {ex.Message}"); + Logger?.LogError("Error sending message: {exceptionMessage}", ex.Message); Status = null; throw; } diff --git a/Sharpcaster/Channels/ReceiverChannel.cs b/Sharpcaster/Channels/ReceiverChannel.cs index 8b9d34e..1644ce0 100644 --- a/Sharpcaster/Channels/ReceiverChannel.cs +++ b/Sharpcaster/Channels/ReceiverChannel.cs @@ -35,7 +35,7 @@ public async Task SetVolume(double level) { if (level < 0 || level > 1.0) { - Logger?.LogError($"level must be between 0.0 and 1.0 - is {level}"); + Logger?.LogError("level must be between 0.0 and 1.0 - is {level}", level); throw new ArgumentException("level must be between 0.0 and 1.0", nameof(level)); } return (await SendAsync(new SetVolumeMessage() { Volume = new Models.Volume() { Level = level } })).Status; diff --git a/Sharpcaster/ChromeCastClient.cs b/Sharpcaster/ChromeCastClient.cs index c1838bb..2794b3e 100644 --- a/Sharpcaster/ChromeCastClient.cs +++ b/Sharpcaster/ChromeCastClient.cs @@ -51,13 +51,13 @@ public class ChromecastClient : IChromecastClient private ILogger _logger = null; private TcpClient _client; - private Stream _stream; + private SslStream _stream; private TaskCompletionSource ReceiveTcs { get; set; } private SemaphoreSlim SendSemaphoreSlim { get; } = new SemaphoreSlim(1, 1); private Dictionary MessageTypes { get; set; } private IEnumerable Channels { get; set; } - private ConcurrentDictionary WaitingTasks { get; } = new ConcurrentDictionary(); + private ConcurrentDictionary WaitingTasks { get; } = new ConcurrentDictionary(); public ChromecastClient(ILoggerFactory loggerFactory = null) { @@ -175,22 +175,29 @@ private void Receive() { HeartbeatChannel.StopTimeoutTimer(); } - channel?.Logger?.LogTrace($"RECEIVED: {payload}"); + channel?.Logger?.LogTrace("RECEIVED: {payload}", payload); var message = JsonConvert.DeserializeObject(payload); if (MessageTypes.TryGetValue(message.Type, out Type type)) { - object tcs = null; try { var response = (IMessage)JsonConvert.DeserializeObject(payload, type); await channel.OnMessageReceivedAsync(response); - TaskCompletionSourceInvoke(ref tcs, message, "SetResult", response); + if (message.HasRequestId) + { + WaitingTasks.TryRemove(message.RequestId, out SharpCasterTaskCompletionSource tcs); + tcs.SetResult(response as IMessageWithId); + } } catch (Exception ex) { _logger?.LogError("Exception processing the Response: {Message}", ex.Message); - TaskCompletionSourceInvoke(ref tcs, message, "SetException", ex, new Type[] { typeof(Exception) }); + if (message.HasRequestId) + { + WaitingTasks.TryRemove(message.RequestId, out SharpCasterTaskCompletionSource tcs); + tcs.SetException(ex); + } } } else @@ -215,22 +222,6 @@ private void Receive() }); } - private void TaskCompletionSourceInvoke(ref object tcs, MessageWithId message, string method, object parameter, Type[] types = null) - { - if (tcs == null) - { - if (message.HasRequestId && WaitingTasks.TryRemove(message.RequestId, out object newtcs)) - { - tcs = newtcs; - } - } - if (tcs != null) - { - var tcsType = tcs.GetType(); - (types == null ? tcsType.GetMethod(method) : tcsType.GetMethod(method, types)).Invoke(tcs, new object[] { parameter }); - } - } - public async Task SendAsync(ILogger channelLogger, string ns, IMessage message, string destinationId) { var castMessage = CreateCastMessage(ns, destinationId); @@ -243,11 +234,18 @@ private async Task SendAsync(ILogger channelLogger, CastMessage castMessage) await SendSemaphoreSlim.WaitAsync(); try { - (channelLogger ?? _logger)?.LogTrace($"SENT : {castMessage.DestinationId}: {castMessage.PayloadUtf8}"); + (channelLogger ?? _logger)?.LogTrace("SENT: {DestinationId}: {PayloadUtf8}", castMessage.DestinationId, castMessage.PayloadUtf8); +#if NETSTANDARD2_0 byte[] message = castMessage.ToProto(); - var networkStream = _stream; - await networkStream.WriteAsync(message, 0, message.Length); - await networkStream.FlushAsync(); +#else + ReadOnlyMemory message = castMessage.ToProto(); +#endif +#if NETSTANDARD2_0 + await _stream.WriteAsync(message, 0, message.Length); +#else + await _stream.WriteAsync(message); +#endif + await _stream.FlushAsync(); } finally { @@ -267,10 +265,10 @@ private CastMessage CreateCastMessage(string ns, string destinationId) public async Task SendAsync(ILogger channelLogger, string ns, IMessageWithId message, string destinationId) where TResponse : IMessageWithId { - var taskCompletionSource = new TaskCompletionSource(); + var taskCompletionSource = new SharpCasterTaskCompletionSource(); WaitingTasks[message.RequestId] = taskCompletionSource; await SendAsync(channelLogger, ns, message, destinationId); - return await taskCompletionSource.Task.TimeoutAfter(RECEIVE_TIMEOUT); + return (TResponse)await taskCompletionSource.Task.TimeoutAfter(RECEIVE_TIMEOUT); } public async Task DisconnectAsync() diff --git a/Sharpcaster/ChromecastLocator.cs b/Sharpcaster/ChromecastLocator.cs index 53c77b1..f31059d 100644 --- a/Sharpcaster/ChromecastLocator.cs +++ b/Sharpcaster/ChromecastLocator.cs @@ -15,9 +15,12 @@ namespace Sharpcaster public class MdnsChromecastLocator : IChromecastLocator { public event EventHandler ChromecastReceivedFound; - private IList DiscoveredDevices { get; } + private List DiscoveredDevices { get; } private readonly ServiceBrowser _serviceBrowser; private SemaphoreSlim ServiceAddedSemaphoreSlim { get; } = new SemaphoreSlim(1, 1); + + private static readonly string[] stringArray = new string[] { "", "" }; + public MdnsChromecastLocator() { DiscoveredDevices = new List(); @@ -43,16 +46,16 @@ private void OnServiceAdded(object sender, ServiceAnnouncementEventArgs e) { return i.Split('='); } - return new string[] { "", "" }; + return stringArray; }) .ToDictionary(y => y[0], y => y[1]); - if (!txtValues.ContainsKey("fn")) return; + if (!txtValues.TryGetValue("fn", out string value)) return; var ip = e.Announcement.Addresses[0]; Uri.TryCreate("https://" + ip, UriKind.Absolute, out Uri myUri); var chromecast = new ChromecastReceiver { DeviceUri = myUri, - Name = txtValues["fn"], + Name = value, Model = txtValues["md"], Version = txtValues["ve"], ExtraInformation = txtValues, @@ -90,7 +93,7 @@ public async Task> FindReceiversAsync(Cancellati _serviceBrowser.StartBrowse("_googlecast._tcp"); while (!cancellationToken.IsCancellationRequested) { - await Task.Delay(100); + await Task.Delay(100, cancellationToken); } _serviceBrowser.StopBrowse(); return DiscoveredDevices; diff --git a/Sharpcaster/Models/SharpCasterTaskCompletionSource.cs b/Sharpcaster/Models/SharpCasterTaskCompletionSource.cs new file mode 100644 index 0000000..e623b4e --- /dev/null +++ b/Sharpcaster/Models/SharpCasterTaskCompletionSource.cs @@ -0,0 +1,37 @@ +using Sharpcaster.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Sharpcaster.Models +{ + public class SharpCasterTaskCompletionSource + { + private readonly TaskCompletionSource _tcs; + + public Task Task { get => _tcs.Task; } + + public SharpCasterTaskCompletionSource() + { + _tcs = new TaskCompletionSource(); + } + + public void SetResult(IMessageWithId parameter) + { + _tcs.SetResult(parameter); + } + + public void SetException(Exception exception) + { + _tcs.SetException(exception); + } + } + + public enum TaskCompletionMethod + { + SetResult, + SetException + } +} From 271f04e2fbb5ec0a9424bad5cb7df2a84e16b3e5 Mon Sep 17 00:00:00 2001 From: Teemu Tapanila Date: Tue, 3 Dec 2024 09:58:25 +0100 Subject: [PATCH 5/7] updated pipelines for dotnet 9 --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/dotnet.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index fffc9ae..9150e73 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -48,7 +48,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: Install dependencies run: dotnet restore - name: Build diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 88de9f4..a66de15 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -19,7 +19,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: 9.0.x - name: Restore dependencies run: dotnet restore SharpCaster.sln - name: tag From a7bd9cc42c81094f76030f9d8cb226cd37487438 Mon Sep 17 00:00:00 2001 From: Teemu Tapanila Date: Tue, 3 Dec 2024 19:07:37 +0100 Subject: [PATCH 6/7] IPV6 support, added new Launch Status to receiver channel, improved error handling --- .../ChromecastApplicationTester.cs | 4 +- Sharpcaster.Test/LoggingTester.cs | 2 +- Sharpcaster.Test/MediaChannelTester.cs | 18 +++---- Sharpcaster.Test/MemoryAllocationTester.cs | 47 +++++++++++++++++++ Sharpcaster.Test/MultipleClientTester.cs | 6 +-- Sharpcaster.Test/ReceiverChannelTester.cs | 26 ++++++++++ Sharpcaster.Test/SpotifyTester.cs | 4 +- Sharpcaster/Channels/ReceiverChannel.cs | 14 ++++++ Sharpcaster/ChromeCastClient.cs | 34 +++++++++----- Sharpcaster/ChromecastLocator.cs | 9 +++- Sharpcaster/Extensions/SslStreamExtensions.cs | 7 ++- Sharpcaster/Interfaces/IReceiverChannel.cs | 7 ++- .../Messages/Receiver/LaunchStatusMessage.cs | 19 ++++++++ 13 files changed, 163 insertions(+), 34 deletions(-) create mode 100644 Sharpcaster.Test/MemoryAllocationTester.cs create mode 100644 Sharpcaster/Messages/Receiver/LaunchStatusMessage.cs diff --git a/Sharpcaster.Test/ChromecastApplicationTester.cs b/Sharpcaster.Test/ChromecastApplicationTester.cs index 5c98529..98548b5 100644 --- a/Sharpcaster.Test/ChromecastApplicationTester.cs +++ b/Sharpcaster.Test/ChromecastApplicationTester.cs @@ -66,7 +66,7 @@ public async Task ConnectToChromecastAndLaunchApplicationTwiceWithoutJoining1(Ch } [Theory] - [MemberData(nameof(ChromecastReceiversFilter.GetDefaultDevice), MemberType = typeof(ChromecastReceiversFilter))] + [MemberData(nameof(ChromecastReceiversFilter.GetAny), MemberType = typeof(ChromecastReceiversFilter))] public async Task ConnectToChromecastAndLaunchApplicationTwiceWithoutJoining2(ChromecastReceiver receiver) { var TestHelper = new TestHelper(); @@ -116,7 +116,7 @@ public async Task ConnectToChromecastAndLaunchApplicationOnceAndJoinIt(Chromecas } [Theory] - [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + [MemberData(nameof(ChromecastReceiversFilter.GetAny), MemberType = typeof(ChromecastReceiversFilter))] public async Task ConnectToChromecastAndLaunchWebPage(ChromecastReceiver receiver) { var TestHelper = new TestHelper(); diff --git a/Sharpcaster.Test/LoggingTester.cs b/Sharpcaster.Test/LoggingTester.cs index 67dbfe6..f94e919 100644 --- a/Sharpcaster.Test/LoggingTester.cs +++ b/Sharpcaster.Test/LoggingTester.cs @@ -20,7 +20,7 @@ public void TestLogging() List logLines = []; _ = TestHelper.GetClientWithTestOutput(output, assertableLog: logLines); - Assert.Equal("[addUserResponse,getInfoResponse,LAUNCH_ERROR,RECEIVER_STATUS,QUEUE_CHANGE,QUEUE_ITEM_IDS,QUEUE_ITEMS,DEVICE_UPDATED,MULTIZONE_STATUS,ERROR,INVALID_REQUEST,LOAD_CANCELLED,LOAD_FAILED,MEDIA_STATUS,PING,CLOSE]", logLines[0]); + Assert.Equal("MessageTypes: [addUserResponse,getInfoResponse,LAUNCH_ERROR,RECEIVER_STATUS,QUEUE_CHANGE,QUEUE_ITEM_IDS,QUEUE_ITEMS,DEVICE_UPDATED,MULTIZONE_STATUS,ERROR,INVALID_REQUEST,LOAD_CANCELLED,LOAD_FAILED,MEDIA_STATUS,PING,CLOSE]", logLines[0]); } [Theory] diff --git a/Sharpcaster.Test/MediaChannelTester.cs b/Sharpcaster.Test/MediaChannelTester.cs index 6ed645e..50e9447 100644 --- a/Sharpcaster.Test/MediaChannelTester.cs +++ b/Sharpcaster.Test/MediaChannelTester.cs @@ -92,7 +92,7 @@ public async Task TestWaitForDeviceStopDuringPlayback(ChromecastReceiver receive } [Theory] - [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + [MemberData(nameof(ChromecastReceiversFilter.GetAny), MemberType = typeof(ChromecastReceiversFilter))] public async Task TestLoadingMediaQueueAndNavigateNextPrev(ChromecastReceiver receiver) { var TestHelper = new TestHelper(); @@ -258,7 +258,7 @@ public async Task StartApplicationAThenStartBAndLoadMedia(ChromecastReceiver rec } [Theory] - [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + [MemberData(nameof(ChromecastReceiversFilter.GetAny), MemberType = typeof(ChromecastReceiversFilter))] public async Task TestLoadingAndPausingMedia(ChromecastReceiver receiver) { var TestHelper = new TestHelper(); @@ -305,7 +305,7 @@ public async Task TestLoadingAndPausingMedia(ChromecastReceiver receiver) } [Theory] - [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + [MemberData(nameof(ChromecastReceiversFilter.GetAny), MemberType = typeof(ChromecastReceiversFilter))] public async Task TestLoadingAndStoppingMedia(ChromecastReceiver receiver) { var TestHelper = new TestHelper(); @@ -379,7 +379,7 @@ public async Task TestFailingLoadMedia(ChromecastReceiver receiver) } [Theory] - [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + [MemberData(nameof(ChromecastReceiversFilter.GetAny), MemberType = typeof(ChromecastReceiversFilter))] public async Task TestJoiningRunningMediaSessionAndPausingMedia(ChromecastReceiver receiver) { var TestHelper = new TestHelper(); @@ -402,7 +402,7 @@ public async Task TestJoiningRunningMediaSessionAndPausingMedia(ChromecastReceiv } [Theory] - [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + [MemberData(nameof(ChromecastReceiversFilter.GetAny), MemberType = typeof(ChromecastReceiversFilter))] public async Task TestRepeatingAllQueueMedia(ChromecastReceiver receiver) { var TestHelper = new TestHelper(); @@ -425,7 +425,7 @@ public async Task TestRepeatingAllQueueMedia(ChromecastReceiver receiver) } [Theory] - [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + [MemberData(nameof(ChromecastReceiversFilter.GetAny), MemberType = typeof(ChromecastReceiversFilter))] public async Task TestRepeatingOffQueueMedia(ChromecastReceiver receiver) { var TestHelper = new TestHelper(); @@ -448,7 +448,7 @@ public async Task TestRepeatingOffQueueMedia(ChromecastReceiver receiver) } [Theory] - [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + [MemberData(nameof(ChromecastReceiversFilter.GetAny), MemberType = typeof(ChromecastReceiversFilter))] public async Task TestRepeatingSingleQueueMedia(ChromecastReceiver receiver) { var TestHelper = new TestHelper(); @@ -471,7 +471,7 @@ public async Task TestRepeatingSingleQueueMedia(ChromecastReceiver receiver) } [Theory] - [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + [MemberData(nameof(ChromecastReceiversFilter.GetAny), MemberType = typeof(ChromecastReceiversFilter))] public async Task TestRepeatingAllAndShuffleQueueMedia(ChromecastReceiver receiver) { var TestHelper = new TestHelper(); @@ -494,7 +494,7 @@ public async Task TestRepeatingAllAndShuffleQueueMedia(ChromecastReceiver receiv } [Theory] - [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + [MemberData(nameof(ChromecastReceiversFilter.GetAny), MemberType = typeof(ChromecastReceiversFilter))] public async Task TestFailingQueue(ChromecastReceiver receiver) { var TestHelper = new TestHelper(); diff --git a/Sharpcaster.Test/MemoryAllocationTester.cs b/Sharpcaster.Test/MemoryAllocationTester.cs new file mode 100644 index 0000000..940aaee --- /dev/null +++ b/Sharpcaster.Test/MemoryAllocationTester.cs @@ -0,0 +1,47 @@ +using Microsoft.VisualStudio.TestPlatform.Utilities; +using Sharpcaster.Test.helper; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Sharpcaster.Test +{ + [Collection("SingleCollection")] + public class MemoryAllocationTester + { + [Fact] + public async Task ConnectToChromecastAndLaunchApplication() + { + long memoryBefore = GC.GetTotalMemory(true); + MdnsChromecastLocator locator = new(); + var receivers = await locator.FindReceiversAsync(); + var TestHelper = new TestHelper(); + var client = new ChromecastClient(); + Assert.NotEmpty(receivers); + await client.ConnectChromecast(receivers.First()); + //var status = await client.LaunchApplicationAsync("B3419EF5"); + + //Assert.Equal("B3419EF5", status.Application.AppId); + + await client.DisconnectAsync(); + + long memoryAfter = GC.GetTotalMemory(true); + long memoryUsed = memoryAfter - memoryBefore; + double memoryUsedMB = memoryUsed / (1024.0 * 1024.0); + Console.WriteLine($"Memory used: {memoryUsedMB:F2} MB"); + + var memoryInfo = GC.GetGCMemoryInfo(); + var generations = memoryInfo.GenerationInfo; + + for (int i = 0; i < generations.Length; i++) + { + Console.WriteLine($"Generation {i}:"); + Console.WriteLine($" Size Before GC: {generations[i].SizeBeforeBytes / (1024.0 * 1024.0):F2} MB"); + Console.WriteLine($" Size After GC: {generations[i].SizeAfterBytes / (1024.0 * 1024.0):F2} MB"); + } + } + } +} diff --git a/Sharpcaster.Test/MultipleClientTester.cs b/Sharpcaster.Test/MultipleClientTester.cs index 3c7d2bf..8a2e858 100644 --- a/Sharpcaster.Test/MultipleClientTester.cs +++ b/Sharpcaster.Test/MultipleClientTester.cs @@ -20,7 +20,7 @@ public MultipleClientTester(ITestOutputHelper outputHelper, ChromecastDevicesFix } [Theory] - [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + [MemberData(nameof(ChromecastReceiversFilter.GetAny), MemberType = typeof(ChromecastReceiversFilter))] public async Task TestTwoClients(ChromecastReceiver receiver) { var TestHelper = new TestHelper(); @@ -46,7 +46,7 @@ public async Task TestTwoClients(ChromecastReceiver receiver) } [Theory] - [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + [MemberData(nameof(ChromecastReceiversFilter.GetAny), MemberType = typeof(ChromecastReceiversFilter))] public async Task TestThreeClients(ChromecastReceiver receiver) { var TestHelper = new TestHelper(); @@ -81,7 +81,7 @@ public async Task TestThreeClients(ChromecastReceiver receiver) } [Theory] - [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + [MemberData(nameof(ChromecastReceiversFilter.GetAny), MemberType = typeof(ChromecastReceiversFilter))] public async Task TestCommandsFromMultipleDifferentClients(ChromecastReceiver receiver) { var TestHelper = new TestHelper(); diff --git a/Sharpcaster.Test/ReceiverChannelTester.cs b/Sharpcaster.Test/ReceiverChannelTester.cs index 0e24293..6cf5be6 100644 --- a/Sharpcaster.Test/ReceiverChannelTester.cs +++ b/Sharpcaster.Test/ReceiverChannelTester.cs @@ -70,5 +70,31 @@ public async Task TestStoppingApplication(ChromecastReceiver receiver) var status = await client.ReceiverChannel.StopApplication(); Assert.Null(status.Applications); } + + [Theory] + [MemberData(nameof(ChromecastReceiversFilter.GetAny), MemberType = typeof(ChromecastReceiversFilter))] + public async Task TestApplicationLaunchStatusMessage(ChromecastReceiver receiver) + { + var TestHelper = new TestHelper(); + var client = await TestHelper.CreateAndConnectClient(output, receiver); + + string launchStatus = ""; + + client.ReceiverChannel.LaunchStatusChanged += (sender, e) => + { + launchStatus = e.Status; + }; + + await client.LaunchApplicationAsync("B3419EF5"); + + var status = await client.ReceiverChannel.StopApplication(); + Assert.Null(status.Applications); + Assert.Equal("USER_ALLOWED", launchStatus); + } + + private void ReceiverChannel_LaunchStatusChanged(object sender, Messages.Receiver.LaunchStatusMessage e) + { + throw new System.NotImplementedException(); + } } } diff --git a/Sharpcaster.Test/SpotifyTester.cs b/Sharpcaster.Test/SpotifyTester.cs index 5e2a645..5483397 100644 --- a/Sharpcaster.Test/SpotifyTester.cs +++ b/Sharpcaster.Test/SpotifyTester.cs @@ -31,9 +31,7 @@ public SpotifyTester(ITestOutputHelper outputHelper, ChromecastDevicesFixture fi } [Theory] - //[MemberData(nameof(ChromecastReceiversFilter.GetGoogleCastGroup), MemberType = typeof(ChromecastReceiversFilter))] - //[MemberData(nameof(CCDevices.GetJblSpeaker), MemberType = typeof(CCDevices))] - [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + [MemberData(nameof(ChromecastReceiversFilter.GetAny), MemberType = typeof(ChromecastReceiversFilter))] public async Task TestChromecastGetInfo(ChromecastReceiver receiver) { var TestHelper = new TestHelper(); diff --git a/Sharpcaster/Channels/ReceiverChannel.cs b/Sharpcaster/Channels/ReceiverChannel.cs index 1644ce0..13d6327 100644 --- a/Sharpcaster/Channels/ReceiverChannel.cs +++ b/Sharpcaster/Channels/ReceiverChannel.cs @@ -16,6 +16,8 @@ public ReceiverChannel(ILogger logger = null) : base("receiver" { } + public event EventHandler LaunchStatusChanged; + public async Task GetChromecastStatusAsync() { return (await SendAsync(new GetStatusMessage())).Status; @@ -45,5 +47,17 @@ public async Task StopApplication() { return (await SendAsync(new StopMessage() { SessionId = Status.Application.SessionId })).Status; } + + public override Task OnMessageReceivedAsync(IMessage message) + { + switch (message) + { + case LaunchStatusMessage launchStatusMessage: + LaunchStatusChanged?.Invoke(this, launchStatusMessage); + break; + + } + return base.OnMessageReceivedAsync(message); + } } } \ No newline at end of file diff --git a/Sharpcaster/ChromeCastClient.cs b/Sharpcaster/ChromeCastClient.cs index 2794b3e..3eefb11 100644 --- a/Sharpcaster/ChromeCastClient.cs +++ b/Sharpcaster/ChromeCastClient.cs @@ -12,6 +12,7 @@ using Sharpcaster.Messages.Multizone; using Sharpcaster.Messages.Queue; using Sharpcaster.Messages.Receiver; +using Sharpcaster.Messages.Spotify; using Sharpcaster.Models; using Sharpcaster.Models.ChromecastStatus; using Sharpcaster.Models.Media; @@ -76,19 +77,24 @@ public ChromecastClient(ILoggerFactory loggerFactory = null) serviceCollection.AddTransient(); serviceCollection.AddTransient(); var messageInterfaceType = typeof(IMessage); - serviceCollection.AddTransient(messageInterfaceType, typeof(CloseMessage)); - serviceCollection.AddTransient(messageInterfaceType, typeof(PingMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(AddUserResponseMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(GetInfoResponseMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(LaunchErrorMessage)); serviceCollection.AddTransient(messageInterfaceType, typeof(ReceiverStatusMessage)); - serviceCollection.AddTransient(messageInterfaceType, typeof(LoadFailedMessage)); - serviceCollection.AddTransient(messageInterfaceType, typeof(LoadCancelledMessage)); - serviceCollection.AddTransient(messageInterfaceType, typeof(MediaStatusMessage)); - serviceCollection.AddTransient(messageInterfaceType, typeof(MultizoneStatusMessage)); - serviceCollection.AddTransient(messageInterfaceType, typeof(DeviceUpdatedMessage)); serviceCollection.AddTransient(messageInterfaceType, typeof(QueueChangeMessage)); - serviceCollection.AddTransient(messageInterfaceType, typeof(QueueItemsMessage)); serviceCollection.AddTransient(messageInterfaceType, typeof(QueueItemIdsMessage)); - serviceCollection.AddTransient(messageInterfaceType, typeof(LaunchErrorMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(QueueItemsMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(DeviceUpdatedMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(MultizoneStatusMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(ErrorMessage)); serviceCollection.AddTransient(messageInterfaceType, typeof(InvalidRequestMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(LoadCancelledMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(LoadFailedMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(MediaStatusMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(PingMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(CloseMessage)); + serviceCollection.AddTransient(messageInterfaceType, typeof(LaunchStatusMessage)); + Init(serviceCollection); } @@ -123,6 +129,10 @@ private void Init(IServiceCollection serviceCollection) public async Task ConnectChromecast(ChromecastReceiver chromecastReceiver) { + if (chromecastReceiver.DeviceUri == null) + { + throw new ArgumentNullException(nameof(chromecastReceiver.DeviceUri)); + } await Dispose(); FriendlyName = chromecastReceiver.Name; @@ -187,7 +197,9 @@ private void Receive() if (message.HasRequestId) { WaitingTasks.TryRemove(message.RequestId, out SharpCasterTaskCompletionSource tcs); - tcs.SetResult(response as IMessageWithId); + tcs?.SetResult(response as IMessageWithId); + if (tcs == null) + _logger.LogTrace("No TaskCompletionSource found for RequestId: {RequestId}", message.RequestId); } } catch (Exception ex) @@ -196,7 +208,7 @@ private void Receive() if (message.HasRequestId) { WaitingTasks.TryRemove(message.RequestId, out SharpCasterTaskCompletionSource tcs); - tcs.SetException(ex); + tcs?.SetException(ex); } } } diff --git a/Sharpcaster/ChromecastLocator.cs b/Sharpcaster/ChromecastLocator.cs index f31059d..0e11696 100644 --- a/Sharpcaster/ChromecastLocator.cs +++ b/Sharpcaster/ChromecastLocator.cs @@ -2,6 +2,7 @@ using Sharpcaster.Models; using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -51,7 +52,11 @@ private void OnServiceAdded(object sender, ServiceAnnouncementEventArgs e) .ToDictionary(y => y[0], y => y[1]); if (!txtValues.TryGetValue("fn", out string value)) return; var ip = e.Announcement.Addresses[0]; - Uri.TryCreate("https://" + ip, UriKind.Absolute, out Uri myUri); + var uriBuilder = new UriBuilder("https", ip.ToString()); + Uri myUri = uriBuilder.Uri; + + if (myUri == null) + Debugger.Break(); var chromecast = new ChromecastReceiver { DeviceUri = myUri, @@ -93,7 +98,7 @@ public async Task> FindReceiversAsync(Cancellati _serviceBrowser.StartBrowse("_googlecast._tcp"); while (!cancellationToken.IsCancellationRequested) { - await Task.Delay(100, cancellationToken); + await Task.Delay(100, CancellationToken.None); } _serviceBrowser.StopBrowse(); return DiscoveredDevices; diff --git a/Sharpcaster/Extensions/SslStreamExtensions.cs b/Sharpcaster/Extensions/SslStreamExtensions.cs index f0bb06a..f544f70 100644 --- a/Sharpcaster/Extensions/SslStreamExtensions.cs +++ b/Sharpcaster/Extensions/SslStreamExtensions.cs @@ -1,17 +1,22 @@ using System; using System.IO; +using System.Net.Security; using System.Threading.Tasks; namespace Sharpcaster.Extensions { public static class StreamExtensions { - public static async Task ReadAsync(this Stream stream, int bufferLength) + public static async Task ReadAsync(this SslStream stream, int bufferLength) { var buffer = new byte[bufferLength]; int nb, length = 0; while (length < bufferLength) { + if (stream == null) + { + throw new InvalidOperationException(); + } nb = await stream.ReadAsync(buffer, length, bufferLength - length); if (nb == 0) { diff --git a/Sharpcaster/Interfaces/IReceiverChannel.cs b/Sharpcaster/Interfaces/IReceiverChannel.cs index df51e37..da95dc1 100644 --- a/Sharpcaster/Interfaces/IReceiverChannel.cs +++ b/Sharpcaster/Interfaces/IReceiverChannel.cs @@ -1,4 +1,6 @@ -using Sharpcaster.Models.ChromecastStatus; +using Sharpcaster.Messages.Receiver; +using Sharpcaster.Models.ChromecastStatus; +using System; using System.Threading.Tasks; namespace Sharpcaster.Interfaces @@ -14,10 +16,11 @@ public interface IReceiverChannel : IStatusChannel, IChromecas /// application identifier /// receiver status Task LaunchApplicationAsync(string applicationId); - Task GetChromecastStatusAsync(); Task StopApplication(); Task SetVolume(double level); Task SetMute(bool muted); + + event EventHandler LaunchStatusChanged; } } diff --git a/Sharpcaster/Messages/Receiver/LaunchStatusMessage.cs b/Sharpcaster/Messages/Receiver/LaunchStatusMessage.cs new file mode 100644 index 0000000..7134372 --- /dev/null +++ b/Sharpcaster/Messages/Receiver/LaunchStatusMessage.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace Sharpcaster.Messages.Receiver +{ + [DataContract] + public class LaunchStatusMessage : MessageWithId + { + [DataMember(Name = "launchRequestId")] + public string LaunchRequestId { get; set; } + + [DataMember(Name = "status")] + public string Status { get; set; } + } +} From 2a5a4a0eea015ebf4430e2f3e792b2b9228b19e8 Mon Sep 17 00:00:00 2001 From: Teemu Tapanila Date: Tue, 3 Dec 2024 20:55:56 +0100 Subject: [PATCH 7/7] Improved reliability and added status into statuschanged events --- .../ChromecastConnectionTester.cs | 3 ++- Sharpcaster.Test/MediaChannelTester.cs | 26 +++++++------------ Sharpcaster/Channels/StatusChannel.cs | 8 +++--- Sharpcaster/ChromeCastClient.cs | 15 ++++++----- .../Converters/PlayerStateEnumConverter.cs | 2 +- Sharpcaster/Extensions/SslStreamExtensions.cs | 25 +++++++++++------- Sharpcaster/Interfaces/IStatusChannel.cs | 2 +- 7 files changed, 43 insertions(+), 38 deletions(-) diff --git a/Sharpcaster.Test/ChromecastConnectionTester.cs b/Sharpcaster.Test/ChromecastConnectionTester.cs index 76cea1c..559c27d 100644 --- a/Sharpcaster.Test/ChromecastConnectionTester.cs +++ b/Sharpcaster.Test/ChromecastConnectionTester.cs @@ -7,6 +7,7 @@ using Sharpcaster.Models.Media; using System; using System.Linq; +using System.Collections.Generic; namespace Sharpcaster.Test { @@ -76,7 +77,7 @@ public async Task TestingHeartBeat(ChromecastReceiver receiver) int commandsToRun = 10; //We are setting up an event to listen to status change. Because we don't know when the video has started to play - client.MediaChannel.StatusChanged += (object sender, EventArgs e) => + client.MediaChannel.StatusChanged += (object sender, IEnumerable e) => { _autoResetEvent.Set(); }; diff --git a/Sharpcaster.Test/MediaChannelTester.cs b/Sharpcaster.Test/MediaChannelTester.cs index 50e9447..12f1034 100644 --- a/Sharpcaster.Test/MediaChannelTester.cs +++ b/Sharpcaster.Test/MediaChannelTester.cs @@ -5,6 +5,7 @@ using Sharpcaster.Models.Queue; using Sharpcaster.Test.helper; using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -50,16 +51,9 @@ public async Task TestWaitForDeviceStopDuringPlayback(ChromecastReceiver receive AutoResetEvent _disconnectReceived = new(false); IMediaChannel mediaChannel = client.MediaChannel; - mediaChannel.StatusChanged += (object sender, EventArgs e) => + mediaChannel.StatusChanged += (object sender, IEnumerable e) => { - try - { - MediaStatus status = mediaChannel.MediaStatus; - output.WriteLine(status?.PlayerState.ToString()); - } - catch (Exception) - { - } + output.WriteLine(e.FirstOrDefault()?.PlayerState.ToString()); }; client.Disconnected += (object sender, EventArgs e) => @@ -106,12 +100,12 @@ public async Task TestLoadingMediaQueueAndNavigateNextPrev(ChromecastReceiver re var mediaStatusChanged = 0; //We are setting up an event to listen to status change. Because we don't know when the audio has started to play - mediaChannel.StatusChanged += async (object sender, EventArgs e) => + mediaChannel.StatusChanged += async (object sender, IEnumerable e) => { try { mediaStatusChanged += 1; - MediaStatus status = mediaChannel.MediaStatus; + MediaStatus status = e.FirstOrDefault(); int currentItemId = status?.CurrentItemId ?? -1; if (currentItemId != -1 && status.PlayerState == PlayerStateType.Playing) @@ -158,7 +152,7 @@ public async Task TestLoadingMediaQueueAndNavigateNextPrev(ChromecastReceiver re MediaStatus status = await client.MediaChannel.QueueLoadAsync(MyCd); Assert.Equal(PlayerStateType.Playing, status.PlayerState); - Assert.Equal(2, status.Items.Count()); // The status message only contains the next (and if available Prev) Track/QueueItem! + Assert.Equal(2, status.Items.Length); // The status message only contains the next (and if available Prev) Track/QueueItem! Assert.Equal(status.CurrentItemId, status.Items[0].ItemId); //This keeps the test running untill all eventhandler sequence steps are finished. If something goes wrong we get a very slow timeout here. @@ -275,10 +269,10 @@ public async Task TestLoadingAndPausingMedia(ChromecastReceiver receiver) bool firstPlay = true; //We are setting up an event to listen to status change. Because we don't know when the video has started to play - client.MediaChannel.StatusChanged += async (object sender, EventArgs e) => + client.MediaChannel.StatusChanged += async (object sender, IEnumerable e) => { //runSequence += "."; - if (client.MediaChannel.Status.FirstOrDefault()?.PlayerState == PlayerStateType.Playing) + if (e.FirstOrDefault().PlayerState == PlayerStateType.Playing) { if (firstPlay) { @@ -321,11 +315,11 @@ public async Task TestLoadingAndStoppingMedia(ChromecastReceiver receiver) bool firstPlay = true; //We are setting up an event to listen to status change. Because we don't know when the video has started to play - client.MediaChannel.StatusChanged += async (object sender, EventArgs e) => + client.MediaChannel.StatusChanged += async (object sender, IEnumerable e) => { try { - if (client.MediaChannel.MediaStatus?.PlayerState == PlayerStateType.Playing) + if (e.FirstOrDefault().PlayerState == PlayerStateType.Playing) { if (firstPlay) { diff --git a/Sharpcaster/Channels/StatusChannel.cs b/Sharpcaster/Channels/StatusChannel.cs index 1989a47..3eedcbd 100644 --- a/Sharpcaster/Channels/StatusChannel.cs +++ b/Sharpcaster/Channels/StatusChannel.cs @@ -15,7 +15,7 @@ public abstract class StatusChannel : ChromecastChannel /// /// Raised when the status has changed /// - public event EventHandler StatusChanged; + public event EventHandler StatusChanged; /// /// Initialization @@ -40,7 +40,7 @@ public override Task OnMessageReceivedAsync(IMessage message) { case TStatusMessage statusMessage: Status = statusMessage.Status; - OnStatusChanged(); + OnStatusChanged(statusMessage.Status); break; } @@ -50,9 +50,9 @@ public override Task OnMessageReceivedAsync(IMessage message) /// /// Raises the StatusChanged event /// - protected virtual void OnStatusChanged() + protected virtual void OnStatusChanged(TStatus status) { - StatusChanged?.Invoke(this, EventArgs.Empty); + StatusChanged?.Invoke(this, status); } public void ClearStatus() diff --git a/Sharpcaster/ChromeCastClient.cs b/Sharpcaster/ChromeCastClient.cs index 3eefb11..81bcc85 100644 --- a/Sharpcaster/ChromeCastClient.cs +++ b/Sharpcaster/ChromeCastClient.cs @@ -53,6 +53,7 @@ public class ChromecastClient : IChromecastClient private ILogger _logger = null; private TcpClient _client; private SslStream _stream; + private CancellationTokenSource _cancellationTokenSource; private TaskCompletionSource ReceiveTcs { get; set; } private SemaphoreSlim SendSemaphoreSlim { get; } = new SemaphoreSlim(1, 1); @@ -95,7 +96,6 @@ public ChromecastClient(ILoggerFactory loggerFactory = null) serviceCollection.AddTransient(messageInterfaceType, typeof(CloseMessage)); serviceCollection.AddTransient(messageInterfaceType, typeof(LaunchStatusMessage)); - Init(serviceCollection); } @@ -144,8 +144,9 @@ public async Task ConnectChromecast(ChromecastReceiver chromec await secureStream.AuthenticateAsClientAsync(chromecastReceiver.DeviceUri.Host); _stream = secureStream; + _cancellationTokenSource = new CancellationTokenSource(); ReceiveTcs = new TaskCompletionSource(); - Receive(); + Receive(_cancellationTokenSource.Token); HeartbeatChannel.StartTimeoutTimer(); HeartbeatChannel.StatusChanged += HeartBeatTimedOut; await ConnectionChannel.ConnectAsync(); @@ -158,7 +159,7 @@ private async void HeartBeatTimedOut(object sender, EventArgs e) await DisconnectAsync(); } - private void Receive() + private void Receive(CancellationToken cancellationToken) { Task.Run(async () => { @@ -167,13 +168,13 @@ private void Receive() while (true) { //First 4 bytes contains the length of the message - var buffer = await _stream.ReadAsync(4); + var buffer = await _stream.ReadAsync(4, cancellationToken); if (BitConverter.IsLittleEndian) { Array.Reverse(buffer); } var length = BitConverter.ToInt32(buffer, 0); - var castMessage = CastMessage.Parser.ParseFrom(await _stream.ReadAsync(length)); + var castMessage = CastMessage.Parser.ParseFrom(await _stream.ReadAsync(length, cancellationToken)); //Payload can either be Binary or UTF8 json var payload = (castMessage.PayloadType == PayloadType.Binary ? Encoding.UTF8.GetString(castMessage.PayloadBinary.ToByteArray()) : castMessage.PayloadUtf8); @@ -231,7 +232,7 @@ private void Receive() //await Dispose(false); ReceiveTcs.SetResult(true); } - }); + }, cancellationToken); } public async Task SendAsync(ILogger channelLogger, string ns, IMessage message, string destinationId) @@ -291,6 +292,8 @@ public async Task DisconnectAsync() } HeartbeatChannel.StopTimeoutTimer(); HeartbeatChannel.StatusChanged -= HeartBeatTimedOut; + _cancellationTokenSource.Cancel(true); + await Task.Delay(100); await Dispose(); } diff --git a/Sharpcaster/Converters/PlayerStateEnumConverter.cs b/Sharpcaster/Converters/PlayerStateEnumConverter.cs index 319d70d..85beafb 100644 --- a/Sharpcaster/Converters/PlayerStateEnumConverter.cs +++ b/Sharpcaster/Converters/PlayerStateEnumConverter.cs @@ -24,7 +24,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s writer.WriteValue("PLAYING"); break; default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(value)); } } diff --git a/Sharpcaster/Extensions/SslStreamExtensions.cs b/Sharpcaster/Extensions/SslStreamExtensions.cs index f544f70..77744ad 100644 --- a/Sharpcaster/Extensions/SslStreamExtensions.cs +++ b/Sharpcaster/Extensions/SslStreamExtensions.cs @@ -1,30 +1,37 @@ using System; using System.IO; using System.Net.Security; +using System.Threading; using System.Threading.Tasks; namespace Sharpcaster.Extensions { public static class StreamExtensions { - public static async Task ReadAsync(this SslStream stream, int bufferLength) + public static async Task ReadAsync(this SslStream stream, int bufferLength, CancellationToken cancellationToken = default) { var buffer = new byte[bufferLength]; - int nb, length = 0; - while (length < bufferLength) + + #if NETSTANDARD2_0 + int bytesRead, totalBytesRead = 0; + while (totalBytesRead < bufferLength) { if (stream == null) { - throw new InvalidOperationException(); + throw new ObjectDisposedException(nameof(stream)); } - nb = await stream.ReadAsync(buffer, length, bufferLength - length); - if (nb == 0) + bytesRead = await stream.ReadAsync(buffer, totalBytesRead, bufferLength - totalBytesRead, cancellationToken); + if (bytesRead == 0) { - throw new InvalidOperationException(); + throw new EndOfStreamException(); } - length += nb; + totalBytesRead += bytesRead; } + #else + ObjectDisposedException.ThrowIf(stream == null, stream); + await stream.ReadExactlyAsync(buffer.AsMemory(0, bufferLength), cancellationToken); + #endif return buffer; } } -} +} \ No newline at end of file diff --git a/Sharpcaster/Interfaces/IStatusChannel.cs b/Sharpcaster/Interfaces/IStatusChannel.cs index 22b5ca0..202f87e 100644 --- a/Sharpcaster/Interfaces/IStatusChannel.cs +++ b/Sharpcaster/Interfaces/IStatusChannel.cs @@ -11,7 +11,7 @@ public interface IStatusChannel : IChromecastChannel /// /// Raised when the status has changed /// - event EventHandler StatusChanged; + event EventHandler StatusChanged; /// /// Gets the status