diff --git a/SharpCaster.sln b/SharpCaster.sln index 5014bf6..e912ef5 100644 --- a/SharpCaster.sln +++ b/SharpCaster.sln @@ -13,8 +13,8 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7B2AED87-CDE7-4B71-A5B6-19512202FF60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7B2AED87-CDE7-4B71-A5B6-19512202FF60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B2AED87-CDE7-4B71-A5B6-19512202FF60}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {7B2AED87-CDE7-4B71-A5B6-19512202FF60}.Debug|Any CPU.Build.0 = Release|Any CPU {7B2AED87-CDE7-4B71-A5B6-19512202FF60}.Release|Any CPU.ActiveCfg = Release|Any CPU {7B2AED87-CDE7-4B71-A5B6-19512202FF60}.Release|Any CPU.Build.0 = Release|Any CPU {C8C0A3A8-C6AC-4E1B-8D3B-E6764C45C35B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU diff --git a/Sharpcaster.Test/ChromecastApplicationTester.cs b/Sharpcaster.Test/ChromecastApplicationTester.cs index 777b491..65e7180 100644 --- a/Sharpcaster.Test/ChromecastApplicationTester.cs +++ b/Sharpcaster.Test/ChromecastApplicationTester.cs @@ -1,11 +1,21 @@ -using Xunit; +using Sharpcaster.Test.customChannel; +using System; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; namespace Sharpcaster.Test { + [Collection("SingleCollection")] public class ChromecastApplicationTester { + private ITestOutputHelper output; + public ChromecastApplicationTester(ITestOutputHelper outputHelper) { + output = outputHelper; + } + [Fact] - public async void ConnectToChromecastAndLaunchApplication() + public async Task ConnectToChromecastAndLaunchApplication() { var chromecast = await TestHelper.FindChromecast(); var client = new ChromecastClient(); @@ -17,7 +27,7 @@ public async void ConnectToChromecastAndLaunchApplication() } [Fact] - public async void ConnectToChromecastAndLaunchApplicationTwice() + public async Task ConnectToChromecastAndLaunchApplicationTwice() { var chromecast = await TestHelper.FindChromecast(); var client = new ChromecastClient(); @@ -35,24 +45,31 @@ public async void ConnectToChromecastAndLaunchApplicationTwice() [Fact] - public async void ConnectToChromecastAndLaunchApplicationTwiceWithoutJoining() + public async Task ConnectToChromecastAndLaunchApplicationTwiceWithoutJoining() { - var chromecast = await TestHelper.FindChromecast(); - var client = new ChromecastClient(); - var status = await client.ConnectChromecast(chromecast); - status = await client.LaunchApplicationAsync("B3419EF5"); + var client = await TestHelper.CreateAndConnectClient(output); + + var status = await client.LaunchApplicationAsync("B3419EF5"); var firstLaunchTransportId = status.Applications[0].TransportId; await client.DisconnectAsync(); - status = await client.ConnectChromecast(chromecast); + status = await client.ConnectChromecast(TestHelper.CurrentReceiver); status = await client.LaunchApplicationAsync("B3419EF5", false); - Assert.Equal(firstLaunchTransportId, status.Applications[0].TransportId); + // ?????? + // My JBL Device (almost every time - but not always ) makes a new ID here!!!! (The other device - ChromecastAudio DOES NOT!?) + if (TestHelper.CurrentReceiver.Model.Contains("JBL")) { + Assert.NotEqual(firstLaunchTransportId, status.Applications[0].TransportId); + } else { + Assert.Equal(firstLaunchTransportId, status.Applications[0].TransportId); + } + + } [Fact] - public async void ConnectToChromecastAndLaunchApplicationAThenLaunchApplicationB() + public async Task ConnectToChromecastAndLaunchApplicationAThenLaunchApplicationB() { var chromecast = await TestHelper.FindChromecast(); var client = new ChromecastClient(); @@ -70,18 +87,34 @@ public async void ConnectToChromecastAndLaunchApplicationAThenLaunchApplicationB } [Fact] - public async void ConnectToChromecastAndLaunchApplicationOnceAndJoinIt() + public async Task ConnectToChromecastAndLaunchApplicationOnceAndJoinIt() { - var chromecast = await TestHelper.FindChromecast(); - var client = new ChromecastClient(); - var status = await client.ConnectChromecast(chromecast); - status = await client.LaunchApplicationAsync("B3419EF5"); + var client = await TestHelper.CreateAndConnectClient(output); + var status = await client.LaunchApplicationAsync("B3419EF5"); var firstLaunchTransportId = status.Applications[0].TransportId; status = await client.LaunchApplicationAsync("B3419EF5"); + Assert.Equal(firstLaunchTransportId, status.Applications[0].TransportId); + + } + + //Seems like this isn't really working anymore and just loading a white screen + [Fact] + public async Task ConnectToChromecastAndLaunchWebPage() + { + var client = await TestHelper.CreateConnectAndLoadAppClient(output, "5CB45E5A"); + + var req = new WebMessage + { + Type = "loc", + Url = "https://www.google.com/" + }; + + + await client.SendAsync("urn:x-cast:com.url.cast", req, "receiver-0"); } } } diff --git a/Sharpcaster.Test/ChromecastConnectionTester.cs b/Sharpcaster.Test/ChromecastConnectionTester.cs index ce222ba..05f39d2 100644 --- a/Sharpcaster.Test/ChromecastConnectionTester.cs +++ b/Sharpcaster.Test/ChromecastConnectionTester.cs @@ -1,11 +1,13 @@ -using Xunit; +using System.Threading.Tasks; +using Xunit; namespace Sharpcaster.Test { + [Collection("SingleCollection")] public class ChromecastConnectionTester { [Fact] - public async void SearchChromecastsAndConnectToIt() + public async Task SearchChromecastsAndConnectToIt() { var chromecast = await TestHelper.FindChromecast(); var client = new ChromecastClient(); diff --git a/Sharpcaster.Test/LoggingTester.cs b/Sharpcaster.Test/LoggingTester.cs index 188bcf5..b847eee 100644 --- a/Sharpcaster.Test/LoggingTester.cs +++ b/Sharpcaster.Test/LoggingTester.cs @@ -5,16 +5,13 @@ using Sharpcaster.Interfaces; using Sharpcaster.Messages; using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; using Xunit; namespace Sharpcaster.Test { + [Collection("SingleCollection")] public class LoggingTester { @@ -62,7 +59,7 @@ where t.GetTypeInfo().IsClass && !t.GetTypeInfo().IsAbstract && messageInterface } var client = new ChromecastClient(serviceCollection); - Assert.True(logMessageFirst == "[RECEIVER_STATUS,INVALID_REQUEST,LOAD_CANCELLED,LOAD_FAILED,MEDIA_STATUS,PING,CLOSE]"); + Assert.Equal("[RECEIVER_STATUS,QUEUE_CHANGE,QUEUE_ITEM_IDS,QUEUE_ITEMS,INVALID_REQUEST,LOAD_CANCELLED,LOAD_FAILED,MEDIA_STATUS,PING,CLOSE]", logMessageFirst); } } } diff --git a/Sharpcaster.Test/MdnsChromecastLocatorTester.cs b/Sharpcaster.Test/MdnsChromecastLocatorTester.cs index fa19829..befd5b3 100644 --- a/Sharpcaster.Test/MdnsChromecastLocatorTester.cs +++ b/Sharpcaster.Test/MdnsChromecastLocatorTester.cs @@ -2,14 +2,16 @@ using Sharpcaster.Models; using System; using System.Threading; +using System.Threading.Tasks; using Xunit; namespace Sharpcaster.Test { + [Collection("SingleCollection")] public class MdnsChromecastLocatorTester { [Fact] - public async void SearchChromecasts() + public async Task SearchChromecasts() { IChromecastLocator locator = new MdnsChromecastLocator(); var chromecasts = await locator.FindReceiversAsync(); @@ -17,7 +19,7 @@ public async void SearchChromecasts() } [Fact] - public async void SearchChromecastsTrickerEvent() + public async Task SearchChromecastsTrickerEvent() { int counter = 0; IChromecastLocator locator = new MdnsChromecastLocator(); @@ -31,7 +33,7 @@ public async void SearchChromecastsTrickerEvent() } [Fact] - public async void SearchChromecastsWithTooShortTimeout() + public async Task SearchChromecastsWithTooShortTimeout() { IChromecastLocator locator = new MdnsChromecastLocator(); CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); @@ -42,7 +44,7 @@ public async void SearchChromecastsWithTooShortTimeout() [Fact] - public async void SearchChromecastsCancellationToken() + public async Task SearchChromecastsCancellationToken() { IChromecastLocator locator = new MdnsChromecastLocator(); var source = new CancellationTokenSource(TimeSpan.FromMilliseconds(1500)); diff --git a/Sharpcaster.Test/MediaChannelTester.cs b/Sharpcaster.Test/MediaChannelTester.cs index a10308b..4b570ef 100644 --- a/Sharpcaster.Test/MediaChannelTester.cs +++ b/Sharpcaster.Test/MediaChannelTester.cs @@ -1,40 +1,218 @@ -using Sharpcaster.Interfaces; +using Sharpcaster.Channels; +using Sharpcaster.Interfaces; using Sharpcaster.Models.Media; +using Sharpcaster.Models.Queue; using System; +using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Xunit; +using Xunit.Abstractions; namespace Sharpcaster.Test { + + [Collection("SingleCollection")] public class MediaChannelTester { + private ITestOutputHelper output; + public MediaChannelTester(ITestOutputHelper outputHelper) { + output = outputHelper; + } + + [Fact] + public async Task TestWaitForDeviceStopDuringPlayback() { + + // To get this test Passing, you have to manually operate the used Chromecast device! + // I use it with a JBL speaker device. This device has 5 buttons. (ON/OFF, Vol-, Vol+, Play/Pause, (and WLAN-Connect)) + // Vol+/- and Play/Pause do operate and trigger 'unasked' MediaStatusChanged events which work as designed. + // + // Pressing the ON/OFF key during Playback causes the device to send: + // 1. on media channel MediaStatus -> changed to 'Paused' + // 2. on receiver channel a ReceiverStatus Message -> the applications array is omitted (set to NULL) here. + // 3. on connection channel a close message. + // + // after the test media starts playing you have 20 seconds to press the device stop button. Then this should pass as green! + // + ChromecastClient client = await TestHelper.CreateConnectAndLoadAppClient(output); + if (TestHelper.CurrentReceiver.Model == "JBL Playlist") { + + var media = new Media { + ContentUrl = "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/mp4/DesigningForGoogleCast.mp4" + }; + + AutoResetEvent _disconnectReceived = new AutoResetEvent(false); + IMediaChannel mediaChannel = client.GetChannel(); + + mediaChannel.StatusChanged += (object sender, EventArgs e) => { + try { + MediaStatus status = mediaChannel.Status.FirstOrDefault(); + output.WriteLine(status?.PlayerState.ToString()); + } catch (Exception) { + } + }; + + client.Disconnected += (object sender, EventArgs e) => { + try { + _disconnectReceived.Set(); + output.WriteLine("Disconnect received."); + } catch (Exception) { + } + }; + + MediaStatus status = await client.GetChannel().LoadAsync(media); + + //This keeps the test running for 20 seconds or until the device initates the wanted stop-disconnect. + Assert.True(_disconnectReceived.WaitOne(20000), "Have you manually stopped the device while playback? If you did so, this is a real Error :-) !"); + + // To reuse the device now you have to create a new connection and reload the app ... + client = await TestHelper.CreateConnectAndLoadAppClient(output); + status = await client.GetChannel().LoadAsync(media); + Assert.Equal(PlayerStateType.Playing, status.PlayerState); + } else { + Assert.Fail("This test only runs with a 'JBL Playlist' device and also needs manual operations!"); + } + } + + + [Fact] + public async Task TestLoadingMediaQueueAndNavigateNextPrev() { + ChromecastClient client = await TestHelper.CreateConnectAndLoadAppClient(output); + + AutoResetEvent _autoResetEvent = new AutoResetEvent(false); + IMediaChannel mediaChannel = client.GetChannel(); + QueueItem[] MyCd = TestHelper.CreateTestCd(); + + int testSequenceCount = 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) => { + try { + MediaStatus status = mediaChannel.Status.FirstOrDefault(); + int currentItemId = status?.CurrentItemId ?? -1; + + if (currentItemId != -1 && status.PlayerState == PlayerStateType.Playing) { + + if (status?.Items?.ToList()?.Where(i => i.ItemId == currentItemId).FirstOrDefault()?.Media?.ContentUrl?.Equals(MyCd[0].Media.ContentUrl) ?? false) { + if (testSequenceCount == 0) { + testSequenceCount++; + output.WriteLine("First Test Track started playin. listen for some seconds...."); + await Task.Delay(6000); + output.WriteLine("Lets goto next item"); + status = await mediaChannel.QueueNextAsync(status.MediaSessionId); + // Asserts + // ... + } else { + testSequenceCount++; + output.WriteLine("First Test Track started for the 2nd time. Stop and end the test"); + await Task.Delay(1000); + status = await mediaChannel.StopAsync(); + output.WriteLine("test Sequence finished"); + _autoResetEvent.Set(); + } + + } else if (status?.Items?.ToList()?.Where(i => i.ItemId == currentItemId).FirstOrDefault()?.Media?.ContentUrl?.Equals(MyCd[1].Media.ContentUrl) ?? false) { + output.WriteLine("2nd Test Track started playin. listen for some seconds...."); + testSequenceCount++; + await Task.Delay(6000); + output.WriteLine("Lets goto back to first one"); + status = await mediaChannel.QueuePrevAsync(status.MediaSessionId); + } + + } + } catch (Exception ex) { + output?.WriteLine(ex.ToString()); + Assert.Fail(ex.ToString()); + } + }; + + + + MediaStatus status = await client.GetChannel().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(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. + Assert.True(_autoResetEvent.WaitOne(20000)); + + } + + [Fact] + public async Task TestLoadMediaQueueAndCheckContent() { + ChromecastClient client = await TestHelper.CreateConnectAndLoadAppClient(output); + + QueueItem[] MyCd = TestHelper.CreateTestCd(); + + MediaStatus status = await client.GetChannel().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(status.CurrentItemId, status.Items[0].ItemId); + + await Task.Delay(2000); + + int[] ids = await client.GetChannel().QueueGetItemIdsAsync(status.MediaSessionId); + + Assert.Equal(4, ids.Length); + + foreach (int id in ids) { + QueueItem[] items = await client.GetChannel().QueueGetItemsAsync(status.MediaSessionId, new int[] {id}); + Assert.Single(items); + } + + QueueItem[] items2 = await client.GetChannel().QueueGetItemsAsync(status.MediaSessionId, ids); + Assert.Equal(4, items2.Length); + + } + + + [Fact] - public async void TestLoadingMedia() + public async Task TestLoadingMediaQueue() { + ChromecastClient client = await TestHelper.CreateConnectAndLoadAppClient(output); + + QueueItem[] MyCd = TestHelper.CreateTestCd(); + + MediaStatus status = await client.GetChannel().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(status.CurrentItemId, status.Items[0].ItemId); + + } + + [Fact] + public async Task TestLoadingMedia() { - var chromecast = await TestHelper.FindChromecast(); - var client = new ChromecastClient(); - await client.ConnectChromecast(chromecast); - _ = await client.LaunchApplicationAsync("B3419EF5"); + ChromecastClient client = await TestHelper.CreateConnectAndLoadAppClient(output); var media = new Media { ContentUrl = "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/mp4/DesigningForGoogleCast.mp4" }; - _ = await client.GetChannel().LoadAsync(media); + + MediaStatus status = await client.GetChannel().LoadAsync(media); + + Assert.Equal(PlayerStateType.Playing, status.PlayerState); + Assert.Single(status.Items); + Assert.Equal(status.CurrentItemId, status.Items[0].ItemId); + } [Fact] - public async void StartApplicationAThenStartBAndLoadMedia() + public async Task StartApplicationAThenStartBAndLoadMedia() { var chromecast = await TestHelper.FindChromecast(); - var client = new ChromecastClient(); + var client = TestHelper.GetClientWithTestOutput(output); await client.ConnectChromecast(chromecast); - _ = await client.LaunchApplicationAsync("A9BCCB7C"); + _ = await client.LaunchApplicationAsync("A9BCCB7C", false); await client.DisconnectAsync(); await client.ConnectChromecast(chromecast); - _ = await client.LaunchApplicationAsync("B3419EF5"); + _ = await client.LaunchApplicationAsync("B3419EF5", false); var media = new Media { ContentUrl = "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/mp4/DesigningForGoogleCast.mp4" @@ -43,62 +221,76 @@ public async void StartApplicationAThenStartBAndLoadMedia() } [Fact] - public async void TestLoadingAndPausingMedia() + public async Task TestLoadingAndPausingMedia() { + ChromecastClient client = await TestHelper.CreateConnectAndLoadAppClient(output); AutoResetEvent _autoResetEvent = new AutoResetEvent(false); - var chromecast = await TestHelper.FindChromecast(); - var client = new ChromecastClient(); - await client.ConnectChromecast(chromecast); - - var status = await client.LaunchApplicationAsync("B3419EF5"); - var media = new Media { ContentUrl = "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/mp4/DesigningForGoogleCast.mp4" }; MediaStatus mediaStatus; + String runSequence = "R"; + 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.GetChannel().StatusChanged += async (object sender, EventArgs e) => { - if (client.GetChannel().Status.First().PlayerState == PlayerStateType.Playing) + //runSequence += "."; + if (client.GetChannel().Status.FirstOrDefault()?.PlayerState == PlayerStateType.Playing) { - mediaStatus = await client.GetChannel().PauseAsync(); - _autoResetEvent.Set(); - } + if (firstPlay) { + firstPlay = false; + runSequence += "p"; + mediaStatus = await client.GetChannel().PauseAsync(); + Assert.Equal(PlayerStateType.Paused, mediaStatus.PlayerState); + runSequence += "P"; + _autoResetEvent.Set(); + } + } }; + runSequence += "1"; mediaStatus = await client.GetChannel().LoadAsync(media); + runSequence += "2"; //This checks that within 5000 ms we have loaded video and were able to pause it Assert.True(_autoResetEvent.WaitOne(5000)); + runSequence += "3"; + + Assert.Equal("R1p2P3", runSequence); } [Fact] - public async void TestLoadingAndStoppingMedia() + public async Task TestLoadingAndStoppingMedia() { + ChromecastClient client = await TestHelper.CreateConnectAndLoadAppClient(output); AutoResetEvent _autoResetEvent = new AutoResetEvent(false); - var chromecast = await TestHelper.FindChromecast(); - var client = new ChromecastClient(); - await client.ConnectChromecast(chromecast); - - var status = await client.LaunchApplicationAsync("B3419EF5"); - var media = new Media { ContentUrl = "https://commondatastorage.googleapis.com/gtv-videos-bucket/CastVideos/mp4/DesigningForGoogleCast.mp4" }; MediaStatus mediaStatus; + 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.GetChannel().StatusChanged += async (object sender, EventArgs e) => { - if (client.GetChannel().Status.First().PlayerState == PlayerStateType.Playing) - { - mediaStatus = await client.GetChannel().StopAsync(); - _autoResetEvent.Set(); + try { + if (client.GetChannel().Status.FirstOrDefault()?.PlayerState == PlayerStateType.Playing) { + if (firstPlay) { + firstPlay = false; + await Task.Delay(2000); // Listen for some time + mediaStatus = await client.GetChannel().StopAsync(); + _autoResetEvent.Set(); + } + } + } catch (Exception ex) { + output.WriteLine("Exception in Event Handler: " + ex.ToString()); } }; @@ -106,6 +298,9 @@ public async void TestLoadingAndStoppingMedia() //This checks that within 5000 ms we have loaded video and were able to pause it Assert.True(_autoResetEvent.WaitOne(5000)); + } + } + } diff --git a/Sharpcaster.Test/ReceiverChannelTester.cs b/Sharpcaster.Test/ReceiverChannelTester.cs index 4d8865d..0217957 100644 --- a/Sharpcaster.Test/ReceiverChannelTester.cs +++ b/Sharpcaster.Test/ReceiverChannelTester.cs @@ -1,12 +1,14 @@ using Sharpcaster.Channels; +using System.Threading.Tasks; using Xunit; namespace Sharpcaster.Test { + [Collection("SingleCollection")] public class ReceiverChannelTester { [Fact] - public async void TestMute() + public async Task TestMute() { var chromecast = await TestHelper.FindChromecast(); @@ -18,7 +20,7 @@ public async void TestMute() } [Fact] - public async void TestUnMute() + public async Task TestUnMute() { var chromecast = await TestHelper.FindChromecast(); @@ -30,7 +32,7 @@ public async void TestUnMute() } [Fact] - public async void TestVolume() + public async Task TestVolume() { var chromecast = await TestHelper.FindChromecast(); @@ -40,12 +42,12 @@ public async void TestVolume() var status = await client.GetChannel().SetVolume(0.1); Assert.Equal(0.1, status.Volume.Level.Value, precision: 1); - status = await client.GetChannel().SetVolume(1.0); - Assert.Equal(1.0, status.Volume.Level.Value, precision: 1); + status = await client.GetChannel().SetVolume(0.3); + Assert.Equal(0.3, status.Volume.Level.Value, precision: 1); } [Fact] - public async void TestStoppingApplication() + public async Task TestStoppingApplication() { var chromecast = await TestHelper.FindChromecast(); diff --git a/Sharpcaster.Test/Sharpcaster.Test.csproj b/Sharpcaster.Test/Sharpcaster.Test.csproj index f2ec8b9..db2037f 100644 --- a/Sharpcaster.Test/Sharpcaster.Test.csproj +++ b/Sharpcaster.Test/Sharpcaster.Test.csproj @@ -1,15 +1,15 @@ - + net6.0 - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Sharpcaster.Test/TestHelper.cs b/Sharpcaster.Test/TestHelper.cs index fee7dfd..eed35e8 100644 --- a/Sharpcaster.Test/TestHelper.cs +++ b/Sharpcaster.Test/TestHelper.cs @@ -1,28 +1,107 @@ -using Sharpcaster.Interfaces; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Sharpcaster.Channels; +using Sharpcaster.Interfaces; +using Sharpcaster.Messages; using Sharpcaster.Models; +using Sharpcaster.Models.Media; +using Sharpcaster.Models.Queue; using System; using System.Linq; -using System.Threading; +using System.Reflection; using System.Threading.Tasks; +using Xunit.Abstractions; namespace Sharpcaster.Test { public static class TestHelper { + private static ITestOutputHelper TestOutput = null; + public static ChromecastReceiver CurrentReceiver { get; private set; } + + public async static Task FindChromecast() { IChromecastLocator locator = new MdnsChromecastLocator(); var chromecasts = await locator.FindReceiversAsync(); - return chromecasts.First(); + CurrentReceiver = chromecasts.First(); + try { + TestOutput?.WriteLine("Using Receiver '" + (CurrentReceiver?.Model ?? "") + "' at " + CurrentReceiver?.DeviceUri); + } catch { + // If a test does not create a new ITestOutputHelper the old one gets used here and throws + // "InvalidOperationException : There is no currently active test." + } + return CurrentReceiver; } - public async static Task FindChromecast(string name, double timeoutSeconds) - { - IChromecastLocator locator = new MdnsChromecastLocator(); - var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds)); - var chromecasts = await locator.FindReceiversAsync(cts.Token); - return chromecasts.First(x => x.Name == name); + public async static Task CreateAndConnectClient(ITestOutputHelper output) { + TestOutput = output; + var chromecast = await TestHelper.FindChromecast(); + ChromecastClient cc = GetClientWithTestOutput(output); + await cc.ConnectChromecast(chromecast); + return cc; + } + + public async static Task CreateConnectAndLoadAppClient(ITestOutputHelper output, string appId = "B3419EF5") { + TestOutput = output; + ChromecastClient cc = await CreateAndConnectClient(output); + await cc.LaunchApplicationAsync(appId, false); + return cc; + } + + + public static ChromecastClient GetClientWithTestOutput(ITestOutputHelper output) { + + TestOutput = output; + var logger = new Mock>(); + + logger.Setup(x => x.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + (Func)It.IsAny())) + .Callback(new InvocationAction(invocation => { + var logLevel = (LogLevel)invocation.Arguments[0]; // The first two will always be whatever is specified in the setup above + var eventId = (EventId)invocation.Arguments[1]; // so I'm not sure you would ever want to actually use them + var state = invocation.Arguments[2]; + var exception = (Exception)invocation.Arguments[3]; + var formatter = invocation.Arguments[4]; + + var invokeMethod = formatter.GetType().GetMethod("Invoke"); + var logMessage = (string)invokeMethod?.Invoke(formatter, new[] { state, exception }); + + TestOutput.WriteLine(DateTime.Now.ToLongTimeString() + " " + logMessage); + })); + + return new ChromecastClient(logger: logger.Object); + } + + public static QueueItem[] CreateTestCd() { + QueueItem[] MyCd = new QueueItem[4]; + MyCd[0] = new QueueItem() { + Media = new Media { + ContentUrl = "http://www.openmusicarchive.org/audio/Frankie%20by%20Mississippi%20John%20Hurt.mp3" + } + }; + MyCd[1] = new QueueItem() { + Media = new Media { + ContentUrl = "http://www.openmusicarchive.org/audio/Mississippi%20Boweavil%20Blues%20by%20The%20Masked%20Marvel.mp3" + } + }; + MyCd[2] = new QueueItem() { + Media = new Media { + ContentUrl = "http://www.openmusicarchive.org/audio/The%20Wild%20Wagoner%20by%20Jilson%20Setters.mp3" + } + }; + MyCd[3] = new QueueItem() { + Media = new Media { + ContentUrl = "http://www.openmusicarchive.org/audio/Drunkards%20Special%20by%20Coley%20Jones.mp3" + } + }; + return MyCd; } + } } diff --git a/Sharpcaster.Test/customMessage/webMessage.cs b/Sharpcaster.Test/customMessage/webMessage.cs new file mode 100644 index 0000000..be9e75f --- /dev/null +++ b/Sharpcaster.Test/customMessage/webMessage.cs @@ -0,0 +1,13 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Sharpcaster.Messages; + +namespace Sharpcaster.Test.customChannel +{ + [DataContract] + public class WebMessage : MessageWithId + { + [DataMember(Name = "url")] + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Sharpcaster.Test/xunit.runner.json b/Sharpcaster.Test/xunit.runner.json index e45d614..56a8af0 100644 --- a/Sharpcaster.Test/xunit.runner.json +++ b/Sharpcaster.Test/xunit.runner.json @@ -1,3 +1,4 @@ { - "methodDisplay": "method" + "methodDisplay": "method", + "parallelAlgorithm": "aggressive" } diff --git a/Sharpcaster/Channels/ConnectionChannel.cs b/Sharpcaster/Channels/ConnectionChannel.cs index 8ea67a8..b2eb63c 100644 --- a/Sharpcaster/Channels/ConnectionChannel.cs +++ b/Sharpcaster/Channels/ConnectionChannel.cs @@ -38,9 +38,11 @@ public async Task ConnectAsync(string transportId) /// message to process public async override Task OnMessageReceivedAsync(IMessage message) { - if (message is CloseMessage) - { - await Client.DisconnectAsync(); + if (message is CloseMessage) { + // In order to avoid usage deadlocks we need to spawn a new Task here!? + _ = Task.Run(async () => { + await Client.DisconnectAsync(); + }); } await base.OnMessageReceivedAsync(message); } diff --git a/Sharpcaster/Channels/MediaChannel.cs b/Sharpcaster/Channels/MediaChannel.cs index d41b820..752a748 100644 --- a/Sharpcaster/Channels/MediaChannel.cs +++ b/Sharpcaster/Channels/MediaChannel.cs @@ -1,7 +1,9 @@ using Sharpcaster.Interfaces; using Sharpcaster.Messages.Media; +using Sharpcaster.Messages.Queue; using Sharpcaster.Models.ChromecastStatus; using Sharpcaster.Models.Media; +using Sharpcaster.Models.Queue; using System; using System.Collections.Generic; using System.Linq; @@ -91,5 +93,37 @@ public async Task SeekAsync(double seconds) { return await SendAsync(new SeekMessage() { CurrentTime = seconds }); } + + public async Task QueueLoadAsync(QueueItem[] items) + { + var chromecastStatus = Client.GetChromecastStatus(); + return (await SendAsync(new QueueLoadMessage() { SessionId = chromecastStatus.Applications[0].SessionId, Items = items }, chromecastStatus.Applications[0].TransportId)).Status?.FirstOrDefault(); + } + + public async Task QueueNextAsync(long mediaSessionId) + { + var chromecastStatus = Client.GetChromecastStatus(); + return (await SendAsync(new QueueNextMessage() { MediaSessionId = mediaSessionId }, chromecastStatus.Applications[0].TransportId)).Status?.FirstOrDefault(); + } + + public async Task QueuePrevAsync(long mediaSessionId) + { + var chromecastStatus = Client.GetChromecastStatus(); + return (await SendAsync(new QueuePrevMessage() { MediaSessionId = mediaSessionId }, chromecastStatus.Applications[0].TransportId)).Status?.FirstOrDefault(); + } + + + public async Task QueueGetItemsAsync(long mediaSessionId, int[] ids = null) + { + var chromecastStatus = Client.GetChromecastStatus(); + return (await SendAsync(new QueueGetItemsMessage() { MediaSessionId = mediaSessionId, Ids = ids }, chromecastStatus.Applications[0].TransportId)).Items; + } + + public async Task QueueGetItemIdsAsync(long mediaSessionId) + { + var chromecastStatus = Client.GetChromecastStatus(); + return (await SendAsync(new QueueGetItemIdsMessage() { MediaSessionId = mediaSessionId }, chromecastStatus.Applications[0].TransportId)).Ids; + } + } } diff --git a/Sharpcaster/ChromeCastClient.cs b/Sharpcaster/ChromeCastClient.cs index 8fdb1d6..80f623d 100644 --- a/Sharpcaster/ChromeCastClient.cs +++ b/Sharpcaster/ChromeCastClient.cs @@ -6,7 +6,6 @@ using Sharpcaster.Extensions; using Sharpcaster.Interfaces; using Sharpcaster.Messages; -using Sharpcaster.Messages.Media; using Sharpcaster.Models; using Sharpcaster.Models.ChromecastStatus; using Sharpcaster.Models.Media; @@ -26,8 +25,10 @@ namespace Sharpcaster { + public class ChromecastClient : IChromecastClient { + private const int RECEIVE_TIMEOUT = 30000; /// @@ -45,9 +46,16 @@ public class ChromecastClient : IChromecastClient private IEnumerable Channels { get; set; } private ConcurrentDictionary WaitingTasks { get; } = new ConcurrentDictionary(); - public ChromecastClient() + public ChromecastClient(ILogger logger = null, ILoggerFactory loggerFactory = null) { var serviceCollection = new ServiceCollection(); + if (logger != null) { + serviceCollection.AddSingleton(logger); + } + if (loggerFactory != null) { + serviceCollection.AddSingleton(loggerFactory); + } + serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); @@ -157,6 +165,8 @@ private void Receive() } else { + _logger?.LogError("The received Message of Type '{ty}' can not be converted to its response Type." + + " An implementing IMessage class is missing!", message.Type); Debugger.Break(); } } @@ -170,22 +180,13 @@ private void Receive() }); } - private async void TaskCompletionSourceInvoke(MessageWithId message, string method, object parameter, Type[] types = null) + private void TaskCompletionSourceInvoke(MessageWithId message, string method, object parameter, Type[] types = null) { if (message.HasRequestId && WaitingTasks.TryRemove(message.RequestId, out object tcs)) { var tcsType = tcs.GetType(); (types == null ? tcsType.GetMethod(method) : tcsType.GetMethod(method, types)).Invoke(tcs, new object[] { parameter }); } - else - { - //This is just to handle media status messages. Where we want to update the status of media but we are not expecting an update - if (message.Type == "MEDIA_STATUS") - { - var statusMessage = parameter as MediaStatusMessage; - await GetChannel().OnMessageReceivedAsync(statusMessage); - } - } } public async Task SendAsync(string ns, IMessage message, string destinationId) @@ -201,7 +202,6 @@ private async Task SendAsync(CastMessage castMessage) try { _logger?.LogTrace($"SENT : {castMessage.DestinationId}: {castMessage.PayloadUtf8}"); - byte[] message = castMessage.ToProto(); var networkStream = _stream; await networkStream.WriteAsync(message, 0, message.Length); diff --git a/Sharpcaster/Interfaces/IConsoleWrapper.cs b/Sharpcaster/Interfaces/IConsoleWrapper.cs new file mode 100644 index 0000000..2c0c1f5 --- /dev/null +++ b/Sharpcaster/Interfaces/IConsoleWrapper.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Sharpcaster.Interfaces { + public interface IConsoleWrapper { + void WriteLine(string line); + void WriteLine(string line, Exception ex, object p); + } +} diff --git a/Sharpcaster/Interfaces/IMediaChannel.cs b/Sharpcaster/Interfaces/IMediaChannel.cs index 52a6a1a..e2be11d 100644 --- a/Sharpcaster/Interfaces/IMediaChannel.cs +++ b/Sharpcaster/Interfaces/IMediaChannel.cs @@ -1,14 +1,13 @@ using Sharpcaster.Models.Media; +using Sharpcaster.Models.Queue; using System.Collections.Generic; using System.Threading.Tasks; -namespace Sharpcaster.Interfaces -{ +namespace Sharpcaster.Interfaces { /// /// Interface for the media channel /// - public interface IMediaChannel : IStatusChannel>, IChromecastChannel - { + public interface IMediaChannel : IStatusChannel>, IChromecastChannel { /// /// Loads a media /// @@ -41,5 +40,10 @@ public interface IMediaChannel : IStatusChannel>, IChro /// time in seconds /// media status Task SeekAsync(double seconds); + Task QueueLoadAsync(QueueItem[] items); + Task QueueNextAsync(long mediaSessionId); + Task QueuePrevAsync(long mediaSessionId); + Task QueueGetItemsAsync(long mediaSessionId, int[] ids = null); + Task QueueGetItemIdsAsync(long mediaSessionId); } } diff --git a/Sharpcaster/Messages/Queue/QueueChangeMessage.cs b/Sharpcaster/Messages/Queue/QueueChangeMessage.cs new file mode 100644 index 0000000..335764a --- /dev/null +++ b/Sharpcaster/Messages/Queue/QueueChangeMessage.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; + +namespace Sharpcaster.Messages.Queue +{ + + //TODO: not tested yet. Either implement Queue manipulation Requests or test with 2nd external client!? + + [DataContract] + [ReceptionMessage] + public class QueueChangeMessage : MessageWithSession + { + [DataMember(Name = "changeType")] + public string ChangeType { get; set; } + + [DataMember(Name = "itemIds")] + public int[] ChangedIds { get; set; } + } + + +} diff --git a/Sharpcaster/Messages/Queue/QueueGetItemIdsMessage.cs b/Sharpcaster/Messages/Queue/QueueGetItemIdsMessage.cs new file mode 100644 index 0000000..d535ae2 --- /dev/null +++ b/Sharpcaster/Messages/Queue/QueueGetItemIdsMessage.cs @@ -0,0 +1,13 @@ +using Sharpcaster.Messages.Media; +using System.Runtime.Serialization; + + +namespace Sharpcaster.Messages.Queue +{ + + [DataContract] + public class QueueGetItemIdsMessage : MediaSessionMessage { + + } + +} diff --git a/Sharpcaster/Messages/Queue/QueueGetItemsMessage.cs b/Sharpcaster/Messages/Queue/QueueGetItemsMessage.cs new file mode 100644 index 0000000..9f551d0 --- /dev/null +++ b/Sharpcaster/Messages/Queue/QueueGetItemsMessage.cs @@ -0,0 +1,15 @@ +using Sharpcaster.Messages.Media; +using System.Runtime.Serialization; + +namespace Sharpcaster.Messages.Queue +{ + + [DataContract] + public class QueueGetItemsMessage : MediaSessionMessage + { + [DataMember(Name = "itemIds")] + public int[] Ids { get; set; } + } + + +} \ No newline at end of file diff --git a/Sharpcaster/Messages/Queue/QueueItemIdsMessage.cs b/Sharpcaster/Messages/Queue/QueueItemIdsMessage.cs new file mode 100644 index 0000000..da8e62b --- /dev/null +++ b/Sharpcaster/Messages/Queue/QueueItemIdsMessage.cs @@ -0,0 +1,14 @@ +using Sharpcaster.Messages.Media; +using System.Runtime.Serialization; + +namespace Sharpcaster.Messages.Queue +{ + [DataContract] + [ReceptionMessage] + public class QueueItemIdsMessage : MediaSessionMessage + { + + [DataMember(Name = "itemIds")] + public int[] Ids { get; set; } + } +} diff --git a/Sharpcaster/Messages/Queue/QueueItemsMessage.cs b/Sharpcaster/Messages/Queue/QueueItemsMessage.cs new file mode 100644 index 0000000..06db7d6 --- /dev/null +++ b/Sharpcaster/Messages/Queue/QueueItemsMessage.cs @@ -0,0 +1,16 @@ +using Sharpcaster.Messages.Media; +using Sharpcaster.Models.Queue; +using System.Runtime.Serialization; + +namespace Sharpcaster.Messages.Queue +{ + + [DataContract] + [ReceptionMessage] + public class QueueItemsMessage : MediaSessionMessage + { + + [DataMember(Name = "items")] + public QueueItem[] Items { get; set; } + } +} diff --git a/Sharpcaster/Messages/Queue/QueueLoadMessage.cs b/Sharpcaster/Messages/Queue/QueueLoadMessage.cs new file mode 100644 index 0000000..65d2c72 --- /dev/null +++ b/Sharpcaster/Messages/Queue/QueueLoadMessage.cs @@ -0,0 +1,12 @@ +using Sharpcaster.Models.Queue; +using System.Runtime.Serialization; + +namespace Sharpcaster.Messages.Queue +{ + [DataContract] + public class QueueLoadMessage : MessageWithSession + { + [DataMember(Name = "items")] + public QueueItem[] Items { get; set; } + } +} diff --git a/Sharpcaster/Messages/Queue/QueueNextMessage.cs b/Sharpcaster/Messages/Queue/QueueNextMessage.cs new file mode 100644 index 0000000..7c9a969 --- /dev/null +++ b/Sharpcaster/Messages/Queue/QueueNextMessage.cs @@ -0,0 +1,11 @@ +using System.Runtime.Serialization; +using Sharpcaster.Messages.Media; + +namespace Sharpcaster.Messages.Queue +{ + [DataContract] + public class QueueNextMessage : MediaSessionMessage + { + + } +} diff --git a/Sharpcaster/Messages/Queue/QueuePrevMessage .cs b/Sharpcaster/Messages/Queue/QueuePrevMessage .cs new file mode 100644 index 0000000..892f190 --- /dev/null +++ b/Sharpcaster/Messages/Queue/QueuePrevMessage .cs @@ -0,0 +1,11 @@ +using System.Runtime.Serialization; +using Sharpcaster.Messages.Media; + +namespace Sharpcaster.Messages.Queue +{ + [DataContract] + public class QueuePrevMessage : MediaSessionMessage + { + + } +} diff --git a/Sharpcaster/Models/Media/Item.cs b/Sharpcaster/Models/Media/Item.cs deleted file mode 100644 index 39a6980..0000000 --- a/Sharpcaster/Models/Media/Item.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Runtime.Serialization; - -namespace Sharpcaster.Models.Media -{ - /// - /// Item - /// - [DataContract] - public class Item - { - /// - /// Gets or sets the item identifier - /// - [DataMember(Name = "itemId")] - public int ItemId { get; set; } - - /// - /// Gets or sets the media - /// - [DataMember(Name = "media")] - public Media Media { get; set; } - - /// - /// Gets or sets a value indicating whether autoplay is enabled or not - /// - [DataMember(Name = "autoplay")] - public bool Autoplay { get; set; } - } -} diff --git a/Sharpcaster/Models/Media/MediaStatus.cs b/Sharpcaster/Models/Media/MediaStatus.cs index af112a5..1e845d7 100644 --- a/Sharpcaster/Models/Media/MediaStatus.cs +++ b/Sharpcaster/Models/Media/MediaStatus.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using Sharpcaster.Converters; +using Sharpcaster.Models.Queue; using System.Runtime.Serialization; namespace Sharpcaster.Models.Media @@ -63,7 +64,7 @@ public class MediaStatus /// Gets or sets the current item identifier /// [DataMember(Name = "currentItemId")] - public int CurrentItemId { get; set; } + public int CurrentItemId { get; set; } = -1; /// /// Gets or sets the extended status @@ -76,5 +77,13 @@ public class MediaStatus /// [DataMember(Name = "repeatMode")] public string RepeatMode { get; set; } + + [DataMember(Name = "queueData")] + public QueueData QueueData { get; set; } + + + [DataMember(Name = "items")] + public QueueItem[] Items { get; set; } + } } diff --git a/Sharpcaster/Models/Queue/QueueData.cs b/Sharpcaster/Models/Queue/QueueData.cs new file mode 100644 index 0000000..bee5e99 --- /dev/null +++ b/Sharpcaster/Models/Queue/QueueData.cs @@ -0,0 +1,60 @@ +using Sharpcaster.Models.Media; +using System.Runtime.Serialization; + +namespace Sharpcaster.Models.Queue +{ + [DataContract] + public class QueueData + { + /// + /// The description of the queue. + /// + [DataMember(Name = "description")] + public string Description { get; set; } + /// + /// An optional Queue entity ID, providing a Google Assistant deep link. + /// + [DataMember(Name = "entity")] + public string Entity { get; set; } + /// + /// The ID of the queue. + /// + [DataMember(Name ="id")] + public string Id { get; set; } + /// + /// An Array of queue items, sorted in playback order. + /// + [DataMember(Name ="items")] + public QueueItem[] Items { get; set; } + /// + /// The name of the queue. + /// + [DataMember(Name ="name")] + public string Name { get; set; } + /// + /// A queue type, such as album, playlist, radio station, or tv series. + /// + [DataMember(Name= "queueType")] + public string QueueType { get; set; } + /// + /// The continuous playback behavior of the queue. + /// + [DataMember(Name = "repeatMode")] + public string RepeatMode { get; set; } + /// + /// True indicates that the queue is shuffled. + /// + [DataMember(Name = "shuffle")] + public bool IsShuffle { get; set; } + /// + /// The index of the item in the queue that should be used to start playback first. + /// + [DataMember(Name = "startIndex")] + public long? StartIndex { get; set; } + /// + /// When to start playback of the first item, expressed as the number of seconds since the beginning of the media. + /// + [DataMember(Name = "startTime")] + public long? StartTime { get; set; } + } +} \ No newline at end of file diff --git a/Sharpcaster/Models/Queue/QueueItem.cs b/Sharpcaster/Models/Queue/QueueItem.cs new file mode 100644 index 0000000..75e5059 --- /dev/null +++ b/Sharpcaster/Models/Queue/QueueItem.cs @@ -0,0 +1,36 @@ +using System.Runtime.Serialization; + +namespace Sharpcaster.Models.Queue +{ + [DataContract] + public class QueueItem + { + /// + /// Gets or sets the item identifier + /// + [DataMember(Name = "itemId", EmitDefaultValue = false)] + public long? ItemId { get; set; } + /// + /// Gets or sets the media + /// + [DataMember(Name = "media")] + public Media.Media Media { get; set; } + /// + /// Gets or sets a value indicating whether autoplay is enabled or not + /// + [DataMember(Name = "autoPlay")] + public bool? IsAutoPlay { get; set; } + + [DataMember(Name = "orderId", EmitDefaultValue = false)] + public long? OrderId { get; set; } + + [DataMember(Name = "startTime", EmitDefaultValue = false)] + public long? StartTime { get; set; } + + [DataMember(Name = "preloadTime", EmitDefaultValue = false)] + public long? PreloadTime { get; set; } + + [DataMember(Name = "activeTrackIds", EmitDefaultValue = false)] + public long[] ActiveTrackIds { get; set; } + } +} diff --git a/Sharpcaster/Sharpcaster.csproj b/Sharpcaster/Sharpcaster.csproj index 4729f11..afa0e1c 100644 --- a/Sharpcaster/Sharpcaster.csproj +++ b/Sharpcaster/Sharpcaster.csproj @@ -1,4 +1,4 @@ - + netstandard2.0 @@ -8,10 +8,10 @@ - + - - + +