diff --git a/OpenTok/Caption.cs b/OpenTok/Caption.cs
new file mode 100644
index 0000000..5438581
--- /dev/null
+++ b/OpenTok/Caption.cs
@@ -0,0 +1,15 @@
+using System;
+
+namespace OpenTokSDK
+{
+ ///
+ /// Represents a success caption.
+ ///
+ public struct Caption
+ {
+ ///
+ /// The unique ID for the audio captioning session.
+ ///
+ public Guid CaptionsId { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/OpenTok/CaptionOptions.cs b/OpenTok/CaptionOptions.cs
new file mode 100644
index 0000000..22b1e59
--- /dev/null
+++ b/OpenTok/CaptionOptions.cs
@@ -0,0 +1,248 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using EnumsNET;
+using OpenTokSDK.Exception;
+
+namespace OpenTokSDK
+{
+ ///
+ /// Represents options to start live captions.
+ ///
+ public class CaptionOptions
+ {
+ private CaptionOptions(string sessionId, string token, LanguageCodeValue languageCode = LanguageCodeValue.EnUs,
+ Uri statusCallbackUrl = null, TimeSpan? maxDuration = null, bool partialCaptions = true)
+ {
+ this.LanguageCode = languageCode;
+ this.PartialCaptions = partialCaptions;
+ this.SessionId = sessionId;
+ this.StatusCallbackUrl = statusCallbackUrl;
+ this.Token = token;
+ this.MaxDuration = maxDuration ?? new TimeSpan(4, 0, 0);
+ }
+
+ ///
+ /// The BCP-47 code for a spoken language used on this call. The default value is "en-US".
+ ///
+ public LanguageCodeValue LanguageCode { get; }
+
+ ///
+ /// The maximum duration for the audio captioning, in seconds. The default value is 14,400 seconds (4 hours).
+ ///
+ public TimeSpan MaxDuration { get; }
+
+ ///
+ /// Whether to enable this to faster captioning at the cost of some degree of inaccuracies. The default value is true.
+ ///
+ public bool PartialCaptions { get; }
+
+ ///
+ /// The session ID of the OpenTok session. The audio from Publishers publishing into this session will be used to
+ /// generate the captions.
+ ///
+ public string SessionId { get; }
+
+ ///
+ /// A publicly reachable URL controlled by the customer and capable of generating the content to be rendered without
+ /// user intervention. The minimum length of the URL is 15 characters and the maximum length is 2048 characters. For
+ /// more information, see
+ ///
+ /// Live Caption status
+ /// updates
+ ///
+ /// .
+ ///
+ public Uri StatusCallbackUrl { get; }
+
+ ///
+ /// A valid OpenTok token with role set to Moderator.
+ ///
+ public string Token { get; }
+
+ ///
+ /// Initializes a caption options with mandatory values.
+ ///
+ ///
+ /// The session ID of the OpenTok session. The audio from Publishers publishing into this session
+ /// will be used to generate the captions.
+ ///
+ /// A valid OpenTok token with role set to Moderator.
+ /// The options.
+ public static CaptionOptions Build(string sessionId, string token)
+ {
+ ValidateSessionId(sessionId);
+ ValidateToken(token);
+ return new CaptionOptions(sessionId, token);
+ }
+
+ ///
+ /// Disables partial captions.
+ ///
+ /// The options.
+ public CaptionOptions DisablePartialCaptions() => new CaptionOptions(this.SessionId, this.Token,
+ this.LanguageCode, this.StatusCallbackUrl, this.MaxDuration, false);
+
+ ///
+ /// Specifies callback Url.
+ ///
+ ///
+ /// A publicly reachable URL controlled by the customer and capable of generating the content to be
+ /// rendered without user intervention. The minimum length of the URL is 15 characters and the maximum length is 2048
+ /// characters. For more information, see <see
+ /// href="https://tokbox.com/developer/guides/live-captions/#live-caption-status-updates">Live Caption status
+ /// updates</see>.
+ ///
+ /// The options.
+ public CaptionOptions WithCallbackUrl(Uri uri) => new CaptionOptions(this.SessionId, this.Token,
+ this.LanguageCode, uri, this.MaxDuration, this.PartialCaptions);
+
+ ///
+ /// Specifies language code.
+ ///
+ /// The BCP-47 code for a spoken language used on this call. The default value is "en-US".
+ /// The options.
+ public CaptionOptions WithLanguageCode(LanguageCodeValue code) => new CaptionOptions(this.SessionId, this.Token,
+ code, this.StatusCallbackUrl, this.MaxDuration, this.PartialCaptions);
+
+ ///
+ /// Specifies the maximum duration.
+ ///
+ ///
+ /// The maximum duration for the audio captioning, in seconds. The default value is 14,400
+ /// seconds (4 hours).
+ ///
+ /// The options.
+ public CaptionOptions WithMaxDuration(TimeSpan maxDuration)
+ {
+ ValidateMaxDuration(maxDuration);
+ return new CaptionOptions(this.SessionId, this.Token, this.LanguageCode, this.StatusCallbackUrl,
+ maxDuration, this.PartialCaptions);
+ }
+
+ private static void ValidateMaxDuration(TimeSpan timeSpan)
+ {
+ if (timeSpan > new TimeSpan(4, 0, 0))
+ {
+ throw new OpenTokException("Max duration cannot exceed 4 hours.");
+ }
+ }
+
+ private static void ValidateSessionId(string sessionId)
+ {
+ if (string.IsNullOrWhiteSpace(sessionId))
+ {
+ throw new OpenTokException("SessionId cannot be null or empty.");
+ }
+ }
+
+ private static void ValidateToken(string token)
+ {
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ throw new OpenTokException("Token cannot be null or empty.");
+ }
+ }
+
+ ///
+ /// Represents the BCP-47 code for a spoken language used on this call.
+ ///
+ public enum LanguageCodeValue
+ {
+ ///
+ /// en-AU (English, Australia)
+ ///
+ [Description("en-AU")]
+ EnAu,
+
+ ///
+ /// en-US (English, US)
+ ///
+ [Description("en-US")]
+ EnUs,
+
+ ///
+ /// en-GB (English, UK)
+ ///
+ [Description("en-GB")]
+ EnGb,
+
+ ///
+ /// zh-CN (Chinese, Simplified)
+ ///
+ [Description("zh-CN")]
+ ZhCn,
+
+ ///
+ /// fr-FR (French)
+ ///
+ [Description("fr-FR")]
+ FrFr,
+
+ ///
+ /// fr-CA (French, Canadian)
+ ///
+ [Description("fr-CA")]
+ FrCa,
+
+ ///
+ /// de-DE (German)
+ ///
+ [Description("de-DE")]
+ DeDe,
+
+ ///
+ /// hi-IN (Hindi, Indian)
+ ///
+ [Description("hi-IN")]
+ HiIn,
+
+ ///
+ /// it-IT (Italian)
+ ///
+ [Description("it-IT")]
+ ItIt,
+
+ ///
+ /// ja-JP (Japanese)
+ ///
+ [Description("ja-JP")]
+ JaJp,
+
+ ///
+ /// ko-KR (Korean)
+ ///
+ [Description("ko-KR")]
+ KoKr,
+
+ ///
+ /// pt-BR (Portuguese, Brazilian)
+ ///
+ [Description("pt-BR")]
+ PrBr,
+
+ ///
+ /// th-TH (Thai)
+ ///
+ [Description("th-TH")]
+ ThTh,
+ }
+
+ internal Dictionary ToDataDictionary()
+ {
+ var dictionary = new Dictionary
+ {
+ {"sessionId", this.SessionId},
+ {"token", this.Token},
+ {"languageCode", this.LanguageCode.AsString(EnumFormat.Description)},
+ {"maxDuration", this.MaxDuration.TotalMinutes},
+ {"partialCaptions", this.PartialCaptions},
+ };
+ if (this.StatusCallbackUrl != null)
+ {
+ dictionary.Add("statusCallbackUrl", this.StatusCallbackUrl.AbsoluteUri);
+ }
+ return dictionary;
+ }
+ }
+}
\ No newline at end of file
diff --git a/OpenTok/OpenTok.Captions.cs b/OpenTok/OpenTok.Captions.cs
new file mode 100644
index 0000000..45c53a4
--- /dev/null
+++ b/OpenTok/OpenTok.Captions.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+
+namespace OpenTokSDK
+{
+ public partial class OpenTok
+ {
+ private const string CaptionsEndpoint = "/captions";
+
+ ///
+ ///
+ ///
+ ///
+ public async Task StartLiveCaptionsAsync(CaptionOptions options)
+ {
+ var response = await this.Client.PostAsync(
+ this.BuildUrl(CaptionsEndpoint),
+ GetHeaderDictionary("application/json"),
+ options.ToDataDictionary());
+ return JsonConvert.DeserializeObject(response);
+ }
+
+ ///
+ ///
+ ///
+ public Task StopLiveCaptionsAsync(Guid captionId) =>
+ this.Client.PostAsync(
+ $"{this.BuildUrlWithRouteParameter(CaptionsEndpoint, captionId.ToString())}/stop",
+ GetHeaderDictionary("application/json"), new Dictionary());
+ }
+}
\ No newline at end of file
diff --git a/OpenTok/OpenTok.Render.cs b/OpenTok/OpenTok.Render.cs
index e934b19..9d02cab 100644
--- a/OpenTok/OpenTok.Render.cs
+++ b/OpenTok/OpenTok.Render.cs
@@ -5,69 +5,69 @@
namespace OpenTokSDK
{
- public partial class OpenTok
- {
- private const string RenderEndpoint = "/render";
+ public partial class OpenTok
+ {
+ private const string RenderEndpoint = "/render";
///
- /// Starts a new Experience Composer renderer for an OpenTok session.
- ///
- ///
- /// For more information, see the .
- /// Experience Composer developer guide.
- ///
- /// The rendering request.
- /// The generated rendering.
- public async Task StartRenderAsync(StartRenderRequest request)
- {
- var response = await this.Client.PostAsync(
- this.BuildUrl(RenderEndpoint),
- GetHeaderDictionary("application/json"),
- request.ToDataDictionary());
- return JsonConvert.DeserializeObject(response);
- }
-
- ///
- /// Stops an Experience Composer renderer.
+ /// Retrieves an Experience Composer renderer.
///
/// The Id of the rendering.
- public async Task StopRenderAsync(string renderId) =>
- await this.Client.DeleteAsync(
- this.BuildUrlWithRouteParameter(RenderEndpoint, renderId),
- new Dictionary());
+ public async Task GetRenderAsync(string renderId)
+ {
+ var url = this.BuildUrlWithRouteParameter(RenderEndpoint, renderId);
+ var response = await this.Client.GetAsync(url);
+ return JsonConvert.DeserializeObject(response);
+ }
///
- /// Retrieves all Experience Composer renderers matching the provided request.
+ /// Retrieves all Experience Composer renderers matching the provided request.
///
/// The request containing filtering options.
/// The list of rendering.
public async Task ListRendersAsync(ListRendersRequest request)
- {
- var url = this.BuildUrlWithQueryParameter(RenderEndpoint, request.ToQueryParameters());
- var response = await this.Client.GetAsync(url);
- return JsonConvert.DeserializeObject(response);
- }
+ {
+ var url = this.BuildUrlWithQueryParameter(RenderEndpoint, request.ToQueryParameters());
+ var response = await this.Client.GetAsync(url);
+ return JsonConvert.DeserializeObject(response);
+ }
///
- /// Retrieves an Experience Composer renderer.
+ /// Starts a new Experience Composer renderer for an OpenTok session.
+ ///
+ ///
+ /// For more information, see the .
+ /// Experience Composer developer guide.
+ ///
+ /// The rendering request.
+ /// The generated rendering.
+ public async Task StartRenderAsync(StartRenderRequest request)
+ {
+ var response = await this.Client.PostAsync(
+ this.BuildUrl(RenderEndpoint),
+ GetHeaderDictionary("application/json"),
+ request.ToDataDictionary());
+ return JsonConvert.DeserializeObject(response);
+ }
+
+ ///
+ /// Stops an Experience Composer renderer.
///
/// The Id of the rendering.
- public async Task GetRenderAsync(string renderId)
- {
- var url = this.BuildUrlWithRouteParameter(RenderEndpoint, renderId);
- var response = await this.Client.GetAsync(url);
- return JsonConvert.DeserializeObject(response);
- }
+ public async Task StopRenderAsync(string renderId) =>
+ await this.Client.DeleteAsync(
+ this.BuildUrlWithRouteParameter(RenderEndpoint, renderId),
+ new Dictionary());
- private string BuildUrl(string endpoint) => $"v2/project/{this.ApiKey}{endpoint}";
+ private string BuildUrl(string endpoint) => $"v2/project/{this.ApiKey}{endpoint}";
- private string BuildUrlWithRouteParameter(string endpoint, string routeParameter) =>
- $"{this.BuildUrl(endpoint)}/{routeParameter}";
+ private string BuildUrlWithQueryParameter(string endpoint, string queryParameter) =>
+ $"{this.BuildUrl(endpoint)}?{queryParameter}";
- private string BuildUrlWithQueryParameter(string endpoint, string queryParameter) =>
- $"{this.BuildUrl(endpoint)}?{queryParameter}";
+ private string BuildUrlWithRouteParameter(string endpoint, string routeParameter) =>
+ $"{this.BuildUrl(endpoint)}/{routeParameter}";
- private static Dictionary GetHeaderDictionary(string contentType) =>
- new Dictionary {{"Content-Type", contentType}};
- }
+ private static Dictionary GetHeaderDictionary(string contentType) =>
+ new Dictionary {{"Content-Type", contentType}};
+ }
}
\ No newline at end of file
diff --git a/OpenTokTest/CaptionOptionsTest.cs b/OpenTokTest/CaptionOptionsTest.cs
new file mode 100644
index 0000000..900eb52
--- /dev/null
+++ b/OpenTokTest/CaptionOptionsTest.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using OpenTokSDK;
+using OpenTokSDK.Exception;
+using Xunit;
+
+namespace OpenTokSDKTest
+{
+ public class CaptionOptionsTest
+ {
+ [Fact]
+ public void Build_ShouldDisablePartialCaptionsByDefault() =>
+ Assert.True(CaptionOptions.Build("sessionId", "token").PartialCaptions);
+
+ [Fact]
+ public void Build_ShouldHaveNoCallbackUri() =>
+ Assert.Null(CaptionOptions.Build("sessionId", "token").StatusCallbackUrl);
+
+ [Fact]
+ public void Build_ShouldSetDefaultLanguageCode() => Assert.Equal(CaptionOptions.LanguageCodeValue.EnUs,
+ CaptionOptions.Build("sessionId", "token").LanguageCode);
+
+ [Fact]
+ public void Build_ShouldSetDefaultMaxDuration() => Assert.Equal(new TimeSpan(4, 0, 0),
+ CaptionOptions.Build("sessionId", "token").MaxDuration);
+
+ [Fact]
+ public void Build_ShouldSetSessionId() =>
+ Assert.Equal("sessionId", CaptionOptions.Build("sessionId", "token").SessionId);
+
+ [Fact]
+ public void Build_ShouldSetToken() => Assert.Equal("token",
+ CaptionOptions.Build("sessionId", "token").Token);
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData(null)]
+ public void Build_ShouldThrowException_GivenSessionIdIsNullOrEmpty(string invalidSessionId)
+ {
+ void Act() => CaptionOptions.Build(invalidSessionId, "token");
+ var exception = Assert.Throws(Act);
+ Assert.Equal("SessionId cannot be null or empty.", exception.Message);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData(null)]
+ public void Build_ShouldThrowException_GivenTokenIdIsNullOrEmpty(string invalidToken)
+ {
+ void Act() => CaptionOptions.Build("sessionId", invalidToken);
+ var exception = Assert.Throws(Act);
+ Assert.Equal("Token cannot be null or empty.", exception.Message);
+ }
+
+ [Fact]
+ public void DisablePartialCaptions_ShouldDisablePartialCaptions() => Assert.False(
+ CaptionOptions.Build("sessionId", "token").DisablePartialCaptions().PartialCaptions);
+
+ [Fact]
+ public void WithLanguageCode_ShouldSetLanguageCode() => Assert.Equal(CaptionOptions.LanguageCodeValue.FrFr,
+ CaptionOptions.Build("sessionId", "token").WithLanguageCode(CaptionOptions.LanguageCodeValue.FrFr)
+ .LanguageCode);
+
+ [Fact]
+ public void WithMaxDuration_ShouldSetMaxDuration() => Assert.Equal(new TimeSpan(1, 0, 0),
+ CaptionOptions.Build("sessionId", "token").WithMaxDuration(new TimeSpan(1, 0, 0))
+ .MaxDuration);
+
+ [Fact]
+ public void WithMaxDuration_ShouldThrowException_GivenValueIsExceededMaximum()
+ {
+ void Act() => CaptionOptions.Build("sessionId", "token")
+ .WithMaxDuration(new TimeSpan(4, 0, 1));
+
+ var exception = Assert.Throws(Act);
+ Assert.Equal("Max duration cannot exceed 4 hours.", exception.Message);
+ }
+
+ [Fact]
+ public void WithCallbackUrl_ShouldSetCallbackUrl() => Assert.Equal(new Uri("http://example.com"),
+ CaptionOptions.Build("sessionId", "token").WithCallbackUrl(new Uri("http://example.com"))
+ .StatusCallbackUrl);
+
+ [Fact]
+ public void ToDataDictionary_ShouldReturnDataDictionary()
+ {
+ var expectedData = new Dictionary
+ {
+ {"sessionId", "sessionId"},
+ {"token", "token"},
+ {"languageCode", "en-AU"},
+ {"maxDuration", (double)60},
+ {"partialCaptions", false},
+ {"statusCallbackUrl", "http://example.com/"},
+ };
+ var data = CaptionOptions.Build("sessionId", "token")
+ .WithLanguageCode(CaptionOptions.LanguageCodeValue.EnAu)
+ .WithMaxDuration(new TimeSpan(1, 0, 0))
+ .WithCallbackUrl(new Uri("http://example.com"))
+ .DisablePartialCaptions()
+ .ToDataDictionary();
+ Assert.Equal(data, expectedData);
+ }
+
+ [Fact]
+ public void ToDataDictionary_ShouldExcludeStatusCallbackUrl_GivenUrlIsNull()
+ {
+ var expectedData = new Dictionary
+ {
+ {"sessionId", "sessionId"},
+ {"token", "token"},
+ {"languageCode", "en-AU"},
+ {"maxDuration", (double)60},
+ {"partialCaptions", false},
+ };
+ var data = CaptionOptions.Build("sessionId", "token")
+ .WithLanguageCode(CaptionOptions.LanguageCodeValue.EnAu)
+ .WithMaxDuration(new TimeSpan(1, 0, 0))
+ .DisablePartialCaptions()
+ .ToDataDictionary();
+ Assert.Equal(data, expectedData);
+ }
+ }
+}
\ No newline at end of file
diff --git a/OpenTokTest/OpenTok.CaptionsTests.cs b/OpenTokTest/OpenTok.CaptionsTests.cs
new file mode 100644
index 0000000..92defab
--- /dev/null
+++ b/OpenTokTest/OpenTok.CaptionsTests.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using AutoFixture;
+using Moq;
+using Newtonsoft.Json;
+using OpenTokSDK;
+using OpenTokSDK.Util;
+using Xunit;
+
+namespace OpenTokSDKTest
+{
+ public class OpenTokCaptionsTests
+ {
+ private readonly int apiKey;
+ private readonly Mock mockClient;
+ private readonly OpenTok sut;
+
+ public OpenTokCaptionsTests()
+ {
+ var fixture = new Fixture();
+ this.apiKey = fixture.Create();
+ this.mockClient = new Mock();
+ this.sut = new OpenTok(this.apiKey, fixture.Create())
+ {
+ Client = this.mockClient.Object,
+ };
+ }
+
+ [Fact]
+ public async Task StartLiveCaptionsAsync_ShouldReturnResponse()
+ {
+ const string contentTypeKey = "Content-Type";
+ const string contentType = "application/json";
+ var expectedUrl = $"v2/project/{this.apiKey}/captions";
+ var expectedResponse = new Caption {CaptionsId = Guid.NewGuid()};
+ var serializedResponse = JsonConvert.SerializeObject(expectedResponse);
+ var request = CaptionOptions.Build("sessionId", "token");
+ this.mockClient.Setup(httpClient => httpClient.PostAsync(
+ expectedUrl,
+ It.Is>(dictionary =>
+ dictionary.ContainsKey(contentTypeKey) && dictionary[contentTypeKey] == contentType),
+ It.Is>(dictionary =>
+ dictionary.SequenceEqual(request.ToDataDictionary()))))
+ .ReturnsAsync(serializedResponse);
+ var response = await this.sut.StartLiveCaptionsAsync(request);
+ Assert.Equal(expectedResponse, response);
+ }
+
+ [Fact]
+ public async Task StopLiveCaptionsAsync_ShouldReturnResponse()
+ {
+ const string contentTypeKey = "Content-Type";
+ const string contentType = "application/json";
+ var captionId = Guid.NewGuid();
+ var expectedUrl = $"v2/project/{this.apiKey}/captions/{captionId}/stop";
+ await this.sut.StopLiveCaptionsAsync(captionId);
+ this.mockClient.Verify(httpClient => httpClient.PostAsync(
+ expectedUrl,
+ It.Is>(dictionary =>
+ dictionary.ContainsKey(contentTypeKey) && dictionary[contentTypeKey] == contentType),
+ It.Is>(dictionary => !dictionary.Any())), Times.Once);
+ }
+ }
+}
\ No newline at end of file