From ea3ec8751f43e2d840aa46417543dc5d7994da8f Mon Sep 17 00:00:00 2001 From: tr00d Date: Fri, 21 Jun 2024 11:33:04 +0200 Subject: [PATCH] feat: implement GetSession for ExperienceComposer --- .../Data/ShouldDeserialize200-response.json | 14 ++++ .../ExperienceComposer/GetSession/E2ETest.cs | 32 ++++++++ .../GetSession/RequestTest.cs | 49 ++++++++++++ .../GetSession/SerializationTest.cs | 37 +++++++++ Vonage.Test/Vonage.Test.csproj | 3 + .../ExperienceComposerClient.cs | 29 +++++++ .../GetSession/GetSessionRequest.cs | 50 ++++++++++++ Vonage/Video/ExperienceComposer/Session.cs | 78 +++++++++++++++++++ Vonage/Video/IVideoClient.cs | 8 +- Vonage/Video/VideoClient.cs | 6 +- 10 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 Vonage.Test/Video/ExperienceComposer/GetSession/Data/ShouldDeserialize200-response.json create mode 100644 Vonage.Test/Video/ExperienceComposer/GetSession/E2ETest.cs create mode 100644 Vonage.Test/Video/ExperienceComposer/GetSession/RequestTest.cs create mode 100644 Vonage.Test/Video/ExperienceComposer/GetSession/SerializationTest.cs create mode 100644 Vonage/Video/ExperienceComposer/ExperienceComposerClient.cs create mode 100644 Vonage/Video/ExperienceComposer/GetSession/GetSessionRequest.cs create mode 100644 Vonage/Video/ExperienceComposer/Session.cs diff --git a/Vonage.Test/Video/ExperienceComposer/GetSession/Data/ShouldDeserialize200-response.json b/Vonage.Test/Video/ExperienceComposer/GetSession/Data/ShouldDeserialize200-response.json new file mode 100644 index 000000000..c0b7578e5 --- /dev/null +++ b/Vonage.Test/Video/ExperienceComposer/GetSession/Data/ShouldDeserialize200-response.json @@ -0,0 +1,14 @@ +{ + "id": "1248e7070b81464c9789f46ad10e7764", + "sessionId": "flR1ZSBPY3QgMjkgMTI6MTM6MjMgUERUIDIwMTN", + "applicationId": "93e36bb9-b72c-45b6-a9ea-5c37dbc49906", + "createdAt": 1437676551000, + "callbackUrl": "https://example.com/video/events", + "updatedAt": 1437676551000, + "name": "Composed stream for Live event #1", + "url": "https://example.com/", + "resolution": "720x1280", + "status": "failed", + "streamId": "e32445b743678c98230f238", + "reason": "Could not load URL" +} \ No newline at end of file diff --git a/Vonage.Test/Video/ExperienceComposer/GetSession/E2ETest.cs b/Vonage.Test/Video/ExperienceComposer/GetSession/E2ETest.cs new file mode 100644 index 000000000..75af6cd47 --- /dev/null +++ b/Vonage.Test/Video/ExperienceComposer/GetSession/E2ETest.cs @@ -0,0 +1,32 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Vonage.Test.Common.Extensions; +using Vonage.Video.ExperienceComposer.GetSession; +using WireMock.ResponseBuilders; +using Xunit; + +namespace Vonage.Test.Video.ExperienceComposer.GetSession; + +[Trait("Category", "E2E")] +public class E2ETest : E2EBase +{ + public E2ETest() : base(typeof(E2ETest).Namespace) + { + } + + [Fact] + public async Task GetSession() + { + this.Helper.Server.Given(WireMock.RequestBuilders.Request.Create() + .WithPath("/v2/project/e3e78a75-221d-41ec-8846-25ae3db1943a/render/EXP-123") + .WithHeader("Authorization", this.Helper.ExpectedAuthorizationHeaderValue) + .UsingGet()) + .RespondWith(Response.Create().WithStatusCode(HttpStatusCode.OK) + .WithBody(this.Serialization.GetResponseJson(nameof(SerializationTest.ShouldDeserialize200)))); + await this.Helper.VonageClient.VideoClient.ExperienceComposerClient + .GetSessionAsync(GetSessionRequest.Parse(new Guid("e3e78a75-221d-41ec-8846-25ae3db1943a"), "EXP-123")) + .Should() + .BeSuccessAsync(SerializationTest.BuildExpectedSession()); + } +} \ No newline at end of file diff --git a/Vonage.Test/Video/ExperienceComposer/GetSession/RequestTest.cs b/Vonage.Test/Video/ExperienceComposer/GetSession/RequestTest.cs new file mode 100644 index 000000000..96bbfcbfe --- /dev/null +++ b/Vonage.Test/Video/ExperienceComposer/GetSession/RequestTest.cs @@ -0,0 +1,49 @@ +using System; +using Vonage.Test.Common.Extensions; +using Vonage.Video.ExperienceComposer.GetSession; +using Xunit; + +namespace Vonage.Test.Video.ExperienceComposer.GetSession; + +[Trait("Category", "Request")] +public class RequestTest +{ + private const string ValidExperienceComposerId = "EXP-123"; + private readonly Guid validApplicationId = Guid.NewGuid(); + + [Fact] + public void GetEndpointPath_ShouldReturnApiEndpoint() => + GetSessionRequest.Parse(this.validApplicationId, ValidExperienceComposerId) + .Map(request => request.GetEndpointPath()) + .Should() + .BeSuccess($"/v2/project/{this.validApplicationId}/render/EXP-123"); + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void Parse_ShouldReturnFailure_GivenExperienceComposerIdIsEmpty(string invalidId) => + GetSessionRequest.Parse(Guid.NewGuid(), invalidId) + .Should() + .BeParsingFailure("ExperienceComposerId cannot be null or whitespace."); + + [Fact] + public void Parse_ShouldReturnFailure_GivenApplicationIdIsEmpty() => + GetSessionRequest.Parse(Guid.Empty, ValidExperienceComposerId) + .Should() + .BeParsingFailure("ApplicationId cannot be empty."); + + [Fact] + public void Parse_ShouldSetApplicationId() => + GetSessionRequest.Parse(this.validApplicationId, ValidExperienceComposerId) + .Map(request => request.ApplicationId) + .Should() + .BeSuccess(this.validApplicationId); + + [Fact] + public void Parse_ShouldSetExperienceComposerId() => + GetSessionRequest.Parse(this.validApplicationId, ValidExperienceComposerId) + .Map(request => request.ExperienceComposerId) + .Should() + .BeSuccess(ValidExperienceComposerId); +} \ No newline at end of file diff --git a/Vonage.Test/Video/ExperienceComposer/GetSession/SerializationTest.cs b/Vonage.Test/Video/ExperienceComposer/GetSession/SerializationTest.cs new file mode 100644 index 000000000..b5136229c --- /dev/null +++ b/Vonage.Test/Video/ExperienceComposer/GetSession/SerializationTest.cs @@ -0,0 +1,37 @@ +using System; +using Vonage.Serialization; +using Vonage.Server; +using Vonage.Test.Common; +using Vonage.Test.Common.Extensions; +using Vonage.Video.ExperienceComposer; +using Xunit; + +namespace Vonage.Test.Video.ExperienceComposer.GetSession; + +[Trait("Category", "Serialization")] +public class SerializationTest +{ + private readonly SerializationTestHelper helper = new SerializationTestHelper( + typeof(SerializationTest).Namespace, + JsonSerializerBuilder.BuildWithCamelCase()); + + [Fact] + public void ShouldDeserialize200() => this.helper.Serializer + .DeserializeObject(this.helper.GetResponseJson()) + .Should() + .BeSuccess(BuildExpectedSession()); + + internal static Session BuildExpectedSession() => + new Session("1248e7070b81464c9789f46ad10e7764", + "flR1ZSBPY3QgMjkgMTI6MTM6MjMgUERUIDIwMTN", + new Guid("93e36bb9-b72c-45b6-a9ea-5c37dbc49906"), + 1437676551000, + new Uri("https://example.com/video/events"), + 1437676551000, + "Composed stream for Live event #1", + new Uri("https://example.com/"), + RenderResolution.HighDefinitionPortrait, + SessionStatus.Failed, + "e32445b743678c98230f238", + "Could not load URL"); +} \ No newline at end of file diff --git a/Vonage.Test/Vonage.Test.csproj b/Vonage.Test/Vonage.Test.csproj index b11b5a15d..0607dac7c 100644 --- a/Vonage.Test/Vonage.Test.csproj +++ b/Vonage.Test/Vonage.Test.csproj @@ -1233,6 +1233,9 @@ Always + + PreserveNewest + diff --git a/Vonage/Video/ExperienceComposer/ExperienceComposerClient.cs b/Vonage/Video/ExperienceComposer/ExperienceComposerClient.cs new file mode 100644 index 000000000..2a0942eac --- /dev/null +++ b/Vonage/Video/ExperienceComposer/ExperienceComposerClient.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using Vonage.Common.Client; +using Vonage.Common.Monads; +using Vonage.Serialization; +using Vonage.Video.ExperienceComposer.GetSession; + +namespace Vonage.Video.ExperienceComposer; + +/// +/// Represents a client exposing Experience Composer features. +/// +public class ExperienceComposerClient +{ + private readonly VonageHttpClient vonageClient; + + internal ExperienceComposerClient(VonageHttpClientConfiguration configuration) => this.vonageClient = + new VonageHttpClient(configuration, JsonSerializerBuilder.BuildWithCamelCase()); + + /// + /// Retrieves details on an Experience Composer session. + /// + /// The request. + /// + /// A success state with the archive if the operation succeeded. A failure state with the error message if it + /// failed. + /// + public Task> GetSessionAsync(Result request) => + this.vonageClient.SendWithResponseAsync(request); +} \ No newline at end of file diff --git a/Vonage/Video/ExperienceComposer/GetSession/GetSessionRequest.cs b/Vonage/Video/ExperienceComposer/GetSession/GetSessionRequest.cs new file mode 100644 index 000000000..d337acdc9 --- /dev/null +++ b/Vonage/Video/ExperienceComposer/GetSession/GetSessionRequest.cs @@ -0,0 +1,50 @@ +using System; +using System.Net.Http; +using Vonage.Common.Client; +using Vonage.Common.Client.Builders; +using Vonage.Common.Monads; +using Vonage.Common.Validation; + +namespace Vonage.Video.ExperienceComposer.GetSession; + +/// +/// Represents a request to retrieve a session. +/// +public readonly struct GetSessionRequest : IVonageRequest, IHasApplicationId +{ + /// + public Guid ApplicationId { get; internal init; } + + /// + /// ID of the Experience Composer instance + /// + public string ExperienceComposerId { get; internal init; } + + /// + public HttpRequestMessage BuildRequestMessage() => + VonageRequestBuilder + .Initialize(HttpMethod.Get, this.GetEndpointPath()) + .Build(); + + /// + public string GetEndpointPath() => $"/v2/project/{this.ApplicationId}/render/{this.ExperienceComposerId}"; + + /// + /// Parses the input into a GetEventRequest. + /// + /// The application Id. + /// The experience composer Id. + /// A success state with the request if the parsing succeeded. A failure state with an error if it failed. + public static Result Parse(Guid application, string experienceComposerId) => + Result + .FromSuccess(new GetSessionRequest + {ApplicationId = application, ExperienceComposerId = experienceComposerId}) + .Map(InputEvaluation.Evaluate) + .Bind(evaluation => evaluation.WithRules(VerifyExperienceComposerId, VerifyApplicationId)); + + private static Result VerifyExperienceComposerId(GetSessionRequest request) => + InputValidation.VerifyNotEmpty(request, request.ExperienceComposerId, nameof(ExperienceComposerId)); + + private static Result VerifyApplicationId(GetSessionRequest request) => + InputValidation.VerifyNotEmpty(request, request.ApplicationId, nameof(ApplicationId)); +} \ No newline at end of file diff --git a/Vonage/Video/ExperienceComposer/Session.cs b/Vonage/Video/ExperienceComposer/Session.cs new file mode 100644 index 000000000..c302093f4 --- /dev/null +++ b/Vonage/Video/ExperienceComposer/Session.cs @@ -0,0 +1,78 @@ +using System; +using System.ComponentModel; +using System.Text.Json.Serialization; +using Vonage.Common.Serialization; +using Vonage.Server; + +namespace Vonage.Video.ExperienceComposer; + +/// +/// Represents an Experience Composer session. +/// +/// The unique ID for the Experience Composer. +/// The session ID of the Vonage Video session you are working with +/// A Vonage Application ID +/// +/// The time the Experience Composer started, expressed in milliseconds since the Unix epoch +/// (January 1, 1970, 00:00:00 UTC). +/// +/// The callback URL for Experience Composer events (if one was set). +/// +/// This is the UNIX timestamp when the Experience Composer status was last updated. For this start +/// method, this timestamp matches the createdAt timestamp. +/// +/// The name of the composed output stream which is published to the session. +/// +/// A publicly reachable URL controlled by the customer and capable of generating the content to be +/// rendered without user intervention. +/// +/// +/// The resolution of the archive, either "640x480" (SD landscape, the default), "1280x720" (HD +/// landscape), "1920x1080" (FHD landscape), "480x640" (SD portrait), "720x1280" (HD portrait), or "1080x1920" (FHD +/// portrait). You may want to use a portrait aspect ratio for archives that include video streams from mobile devices +/// (which often use the portrait aspect ratio). This property only applies to composed archives. If you set this +/// property and set the outputMode property to "individual", the call to the REST method results in an error. +/// +/// The session status. +/// The ID of the composed stream being published. +/// +/// The reason field is only available when the status is either "stopped" or "failed". If the status +/// is stopped, the reason field will contain either "Max Duration Exceeded" or "Stop Requested." If the status is +/// failed, the reason will contain a more specific error message. +/// +public record Session( + string Id, + string SessionId, + Guid ApplicationId, + long CreatedAt, + Uri CallbackUrl, + long UpdatedAt, + string Name, + Uri Url, + [property: JsonConverter(typeof(EnumDescriptionJsonConverter))] + RenderResolution Resolution, + [property: JsonConverter(typeof(EnumDescriptionJsonConverter))] + SessionStatus Status, + string StreamId, + string Reason); + +/// +/// +public enum SessionStatus +{ + /// + /// + [Description("starting")] Starting, + + /// + /// + [Description("started")] Started, + + /// + /// + [Description("stopped")] Stopped, + + /// + /// + [Description("failed")] Failed, +} \ No newline at end of file diff --git a/Vonage/Video/IVideoClient.cs b/Vonage/Video/IVideoClient.cs index 425542222..2b5e804ae 100644 --- a/Vonage/Video/IVideoClient.cs +++ b/Vonage/Video/IVideoClient.cs @@ -1,5 +1,6 @@ using Vonage.Video.Archives; using Vonage.Video.Broadcast; +using Vonage.Video.ExperienceComposer; using Vonage.Video.Moderation; using Vonage.Video.Sessions; using Vonage.Video.Signaling; @@ -38,7 +39,12 @@ public interface IVideoClient SignalingClient SignalingClient { get; } /// - /// Clients for managing SIP calls in a video session. + /// Client for managing SIP calls in a video session. /// SipClient SipClient { get; } + + /// + /// Client for managing experience composer. + /// + ExperienceComposerClient ExperienceComposerClient { get; } } \ No newline at end of file diff --git a/Vonage/Video/VideoClient.cs b/Vonage/Video/VideoClient.cs index f592f3eec..f323ae713 100644 --- a/Vonage/Video/VideoClient.cs +++ b/Vonage/Video/VideoClient.cs @@ -1,6 +1,7 @@ using Vonage.Common.Client; using Vonage.Video.Archives; using Vonage.Video.Broadcast; +using Vonage.Video.ExperienceComposer; using Vonage.Video.Moderation; using Vonage.Video.Sessions; using Vonage.Video.Signaling; @@ -13,6 +14,8 @@ public class VideoClient : IVideoClient { private readonly VonageHttpClientConfiguration configuration; + internal VideoClient(VonageHttpClientConfiguration configuration) => this.configuration = configuration; + /// public ArchiveClient ArchiveClient => new(this.configuration); @@ -31,5 +34,6 @@ public class VideoClient : IVideoClient /// public SipClient SipClient => new(this.configuration); - internal VideoClient(VonageHttpClientConfiguration configuration) => this.configuration = configuration; + /// + public ExperienceComposerClient ExperienceComposerClient => new ExperienceComposerClient(this.configuration); } \ No newline at end of file