diff --git a/Sharpcaster.Test/MediaChannelTester.cs b/Sharpcaster.Test/MediaChannelTester.cs index 86650d47..17db3d1d 100644 --- a/Sharpcaster.Test/MediaChannelTester.cs +++ b/Sharpcaster.Test/MediaChannelTester.cs @@ -1,11 +1,9 @@ -using Sharpcaster.Channels; -using Sharpcaster.Interfaces; +using Sharpcaster.Interfaces; using Sharpcaster.Models; using Sharpcaster.Models.Media; using Sharpcaster.Models.Queue; using Sharpcaster.Test.helper; using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -382,6 +380,106 @@ public async Task TestJoiningRunningMediaSessionAndPausingMedia(ChromecastReceiv await client.GetChannel().PauseAsync(); } + [Theory] + [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + public async Task TestRepeatingAllQueueMedia(ChromecastReceiver receiver) + { + var TestHelper = new TestHelper(); + ChromecastClient client = await TestHelper.CreateConnectAndLoadAppClient(output, receiver); + + var media = new Media + { + ContentUrl = "https://incompetech.com/music/royalty-free/mp3-royaltyfree/Loping%20Sting.mp3" + }; + + var queueItem = new QueueItem + { + Media = media, + + }; + + + await client.GetChannel().QueueLoadAsync([queueItem], null, RepeatModeType.ALL); + var test = await client.GetChannel().PlayAsync(); + + Assert.Equal(RepeatModeType.ALL, test.RepeatMode); + } + + [Theory] + [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + public async Task TestRepeatingOffQueueMedia(ChromecastReceiver receiver) + { + var TestHelper = new TestHelper(); + ChromecastClient client = await TestHelper.CreateConnectAndLoadAppClient(output, receiver); + + var media = new Media + { + ContentUrl = "https://incompetech.com/music/royalty-free/mp3-royaltyfree/Loping%20Sting.mp3" + }; + + var queueItem = new QueueItem + { + Media = media, + + }; + + + await client.GetChannel().QueueLoadAsync([queueItem], null, RepeatModeType.OFF); + var test = await client.GetChannel().PlayAsync(); + + Assert.Equal(RepeatModeType.OFF, test.RepeatMode); + } + + [Theory] + [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + public async Task TestRepeatingSingleQueueMedia(ChromecastReceiver receiver) + { + var TestHelper = new TestHelper(); + ChromecastClient client = await TestHelper.CreateConnectAndLoadAppClient(output, receiver); + + var media = new Media + { + ContentUrl = "https://incompetech.com/music/royalty-free/mp3-royaltyfree/Loping%20Sting.mp3" + }; + + var queueItem = new QueueItem + { + Media = media, + + }; + + + await client.GetChannel().QueueLoadAsync([queueItem], null, RepeatModeType.SINGLE); + var test = await client.GetChannel().PlayAsync(); + + Assert.Equal(RepeatModeType.SINGLE, test.RepeatMode); + } + + [Theory] + [MemberData(nameof(ChromecastReceiversFilter.GetChromecastUltra), MemberType = typeof(ChromecastReceiversFilter))] + public async Task TestRepeatingAllAndShuffleQueueMedia(ChromecastReceiver receiver) + { + var TestHelper = new TestHelper(); + ChromecastClient client = await TestHelper.CreateConnectAndLoadAppClient(output, receiver); + + var media = new Media + { + ContentUrl = "https://incompetech.com/music/royalty-free/mp3-royaltyfree/Loping%20Sting.mp3" + }; + + var queueItem = new QueueItem + { + Media = media, + + }; + + + await client.GetChannel().QueueLoadAsync([queueItem], null, RepeatModeType.ALL_AND_SHUFFLE); + var test = await client.GetChannel().PlayAsync(); + + Assert.Equal(RepeatModeType.ALL_AND_SHUFFLE, test.RepeatMode); + } + } } diff --git a/Sharpcaster/Channels/MediaChannel.cs b/Sharpcaster/Channels/MediaChannel.cs index a6b62c0a..a33eac4c 100644 --- a/Sharpcaster/Channels/MediaChannel.cs +++ b/Sharpcaster/Channels/MediaChannel.cs @@ -105,10 +105,10 @@ public async Task SeekAsync(double seconds) return await SendAsync(new SeekMessage() { CurrentTime = seconds }); } - public async Task QueueLoadAsync(QueueItem[] items) + public async Task QueueLoadAsync(QueueItem[] items, int? currentTime = null, RepeatModeType repeatMode = RepeatModeType.OFF, int? startIndex = null) { var chromecastStatus = Client.GetChromecastStatus(); - return (await SendAsync(new QueueLoadMessage() { SessionId = chromecastStatus.Applications[0].SessionId, Items = items }, chromecastStatus.Applications[0].TransportId)).Status?.FirstOrDefault(); + return (await SendAsync(new QueueLoadMessage() { SessionId = chromecastStatus.Applications[0].SessionId, Items = items, CurrentTime = currentTime, RepeatMode = repeatMode, StartIndex = startIndex }, chromecastStatus.Applications[0].TransportId)).Status?.FirstOrDefault(); } public async Task QueueNextAsync(long mediaSessionId) diff --git a/Sharpcaster/Converters/RepeatModeEnumConverter.cs b/Sharpcaster/Converters/RepeatModeEnumConverter.cs new file mode 100644 index 00000000..63d02b5c --- /dev/null +++ b/Sharpcaster/Converters/RepeatModeEnumConverter.cs @@ -0,0 +1,61 @@ +using Newtonsoft.Json; +using Sharpcaster.Models.Media; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Sharpcaster.Converters +{ + public class RepeatModeEnumConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var repeatModeType = (RepeatModeType)value; + switch (repeatModeType) + { + case RepeatModeType.OFF: + writer.WriteValue("REPEAT_OFF"); + break; + case RepeatModeType.ALL: + writer.WriteValue("REPEAT_ALL"); + break; + case RepeatModeType.SINGLE: + writer.WriteValue("REPEAT_SINGLE"); + break; + case RepeatModeType.ALL_AND_SHUFFLE: + writer.WriteValue("REPEAT_ALL_AND_SHUFFLE"); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var enumString = (string)reader.Value; + RepeatModeType? repeatModeType = null; + + switch (enumString) + { + case "REPEAT_OFF": + repeatModeType = RepeatModeType.OFF; + break; + case "REPEAT_ALL": + repeatModeType = RepeatModeType.ALL; + break; + case "REPEAT_SINGLE": + repeatModeType = RepeatModeType.SINGLE; + break; + case "REPEAT_ALL_AND_SHUFFLE": + repeatModeType = RepeatModeType.ALL_AND_SHUFFLE; + break; + } + return repeatModeType; + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(string); + } + } +} diff --git a/Sharpcaster/Interfaces/IMediaChannel.cs b/Sharpcaster/Interfaces/IMediaChannel.cs index 2d8568b1..2169a032 100644 --- a/Sharpcaster/Interfaces/IMediaChannel.cs +++ b/Sharpcaster/Interfaces/IMediaChannel.cs @@ -40,7 +40,7 @@ public interface IMediaChannel : IStatusChannel>, IChro /// time in seconds /// media status Task SeekAsync(double seconds); - Task QueueLoadAsync(QueueItem[] items); + Task QueueLoadAsync(QueueItem[] items, int? currentTime = null, RepeatModeType repeatMode = RepeatModeType.OFF, int? startIndex = null); Task QueueNextAsync(long mediaSessionId); Task QueuePrevAsync(long mediaSessionId); Task QueueGetItemsAsync(long mediaSessionId, int[] ids = null); diff --git a/Sharpcaster/Messages/Queue/QueueLoadMessage.cs b/Sharpcaster/Messages/Queue/QueueLoadMessage.cs index 65d2c721..1b9c3f69 100644 --- a/Sharpcaster/Messages/Queue/QueueLoadMessage.cs +++ b/Sharpcaster/Messages/Queue/QueueLoadMessage.cs @@ -1,4 +1,7 @@ -using Sharpcaster.Models.Queue; +using Newtonsoft.Json; +using Sharpcaster.Converters; +using Sharpcaster.Models.Media; +using Sharpcaster.Models.Queue; using System.Runtime.Serialization; namespace Sharpcaster.Messages.Queue @@ -8,5 +11,34 @@ public class QueueLoadMessage : MessageWithSession { [DataMember(Name = "items")] public QueueItem[] Items { get; set; } + + /// + /// The index of the item in the items array that must be the first currentItem (the item that will be played first). + /// Note this is the index of the array (starts at 0) and not the itemId (as it is not known until the queue is created). + /// If repeatMode is REPEAT_OFF playback will end when the last item in the array is played (elements before the startIndex will not be played). + /// This may be useful for continuation scenarios where the user was already using the sender app and in the middle decides to cast. + /// In this way the sender app does not need to map between the local and remote queue positions or saves one extra QUEUE_UPDATE request. + /// + [DataMember(Name = "startIndex")] + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public int? StartIndex { get; set; } + /// + /// Behavior of the queue when all items have been played. + /// + + [DataMember(Name = "repeatMode")] + [JsonConverter(typeof(RepeatModeEnumConverter))] + public RepeatModeType RepeatMode { get; set; } + + /// + /// Seconds (since the beginning of content) to start playback of the first item to be played. + /// If provided, this value will take precedence over the startTime value provided at the QueueItem level but only the first time the item is played. + /// This is to cover the common case where the user casts the item that was playing locally so the currentTime does not apply to the item permanently like the QueueItem startTime does. + /// It avoids having to reset the startTime dynamically (that may not be possible if the phone has gone to sleep). + /// + + [DataMember(Name = "currentTime")] + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public int? CurrentTime { get; set; } } } diff --git a/Sharpcaster/Models/Media/MediaStatus.cs b/Sharpcaster/Models/Media/MediaStatus.cs index 1e845d70..74e067a1 100644 --- a/Sharpcaster/Models/Media/MediaStatus.cs +++ b/Sharpcaster/Models/Media/MediaStatus.cs @@ -76,7 +76,8 @@ public class MediaStatus /// Gets or sets the repeat mode /// [DataMember(Name = "repeatMode")] - public string RepeatMode { get; set; } + [JsonConverter(typeof(RepeatModeEnumConverter))] + public RepeatModeType RepeatMode { get; set; } [DataMember(Name = "queueData")] public QueueData QueueData { get; set; } diff --git a/Sharpcaster/Models/Media/RepeatModeType.cs b/Sharpcaster/Models/Media/RepeatModeType.cs new file mode 100644 index 00000000..67339298 --- /dev/null +++ b/Sharpcaster/Models/Media/RepeatModeType.cs @@ -0,0 +1,10 @@ +namespace Sharpcaster.Models.Media +{ + public enum RepeatModeType + { + OFF, + ALL, + SINGLE, + ALL_AND_SHUFFLE + } +} diff --git a/Sharpcaster/Models/Queue/QueueData.cs b/Sharpcaster/Models/Queue/QueueData.cs index bee5e99a..7529808f 100644 --- a/Sharpcaster/Models/Queue/QueueData.cs +++ b/Sharpcaster/Models/Queue/QueueData.cs @@ -1,4 +1,6 @@ -using Sharpcaster.Models.Media; +using Newtonsoft.Json; +using Sharpcaster.Converters; +using Sharpcaster.Models.Media; using System.Runtime.Serialization; namespace Sharpcaster.Models.Queue @@ -40,7 +42,8 @@ public class QueueData /// The continuous playback behavior of the queue. /// [DataMember(Name = "repeatMode")] - public string RepeatMode { get; set; } + [JsonConverter(typeof(RepeatModeEnumConverter))] + public RepeatModeType RepeatMode { get; set; } /// /// True indicates that the queue is shuffled. ///