From a7bff9d3bd2a048378133f80dcfd4e796627fd80 Mon Sep 17 00:00:00 2001 From: Jeff Swartz Date: Wed, 12 Feb 2020 15:33:36 -0800 Subject: [PATCH 1/8] Correction to docs for automatic archiving --- OpenTok/Session.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OpenTok/Session.cs b/OpenTok/Session.cs index 4bc74cfd..f8ce3475 100644 --- a/OpenTok/Session.cs +++ b/OpenTok/Session.cs @@ -40,7 +40,7 @@ public enum ArchiveMode */ MANUAL, /** - * The session is archived automatically (as soon as there are clients publishing streams + * The session is archived automatically (as soon as there are clients connected * to the session). */ ALWAYS From fd313c76883335eb3eaa8b8269edc01d4bce81cc Mon Sep 17 00:00:00 2001 From: Patrick Childers Date: Tue, 9 Nov 2021 09:31:48 -0500 Subject: [PATCH 2/8] Fixes NullReferenceException in CreateSession (#114) * Better handle WebExceptions that don't have a Response and send a customized exception on WebExceptionStatus.SendFailure errors (usually from TLS errors). Fixes #108. * Adding extra instructions regarding TLS to readme, adding new exception handling if TLS is incorrect * removing erroneously added whitespace * list archive by session id, better exception handling for validateSessionid * removing extra whitespace from tests * cleaning up other whitespace issues * passing inner exception to OpenTokException * Fixing comments, removing redundant fields * adding custom layout for archiving * fixing spacing issue, adding javadocs to StartArchive for layout * revving to 3.4 * adding extra Archive Test * Adding non-custom with stylesheet case * adding contract annotations to ArchiveLayout to prevent invalid serialization of the stylesheet for non-custom layouts * bringing balance to the force * Dropping core 3.0 tests * moving back to newer container * removing .net core 2.x runs * Silenced warnings in the test project. * empty Statement Redundancy Fix * Spell Fix * Spell Fix * Spell Fix | GetPartnerIdFromSessionId Spell Fix | GetPartnerIdFromSessionId method's Property * moving dotnet version * specifying test framework * Changed Javadoc comments to XML comments. * Replace Co-authored-by: slorello89 Co-authored-by: Dirk Lemstra Co-authored-by: onpoc Co-authored-by: Matt Hunt Co-authored-by: Jeff Swartz --- .github/workflows/build.yml | 30 + .github/workflows/dotnet-core.yml | 26 - .github/workflows/release.yml | 24 + .travis.yml | 10 - OpenTok.sln | 14 +- OpenTok/Archive.cs | 244 ++++---- OpenTok/ArchiveLayout.cs | 38 ++ OpenTok/ArchiveList.cs | 12 +- OpenTok/Broadcast.cs | 109 ++-- OpenTok/BroadcastLayout.cs | 89 ++- OpenTok/Constants/OpenTokVersion.cs | 15 +- OpenTok/Exception/OpenTokException.cs | 91 +-- OpenTok/LayoutType.cs | 29 + OpenTok/OpenTok.cs | 779 ++++++++++++++------------ OpenTok/OpenTok.csproj | 12 +- OpenTok/OpenTok.nuspec | 17 - OpenTok/Role.cs | 39 +- OpenTok/Rtmp.cs | 36 +- OpenTok/ScreenShareLayoutType.cs | 27 + OpenTok/Session.cs | 157 +++--- OpenTok/SignalProperties.cs | 6 +- OpenTok/Stream.cs | 28 +- OpenTok/StreamList.cs | 12 +- OpenTok/StreamProperties.cs | 21 +- OpenTok/Util/HttpClient.cs | 12 +- OpenTok/Util/OpenTokUtils.cs | 26 +- OpenTokTest/OpenTokTest.cs | 560 +++++++++++++++++- OpenTokTest/OpenTokTest.csproj | 2 +- README.md | 11 +- appveyor.yml | 16 - 30 files changed, 1632 insertions(+), 860 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/dotnet-core.yml create mode 100644 .github/workflows/release.yml delete mode 100644 .travis.yml create mode 100644 OpenTok/ArchiveLayout.cs create mode 100644 OpenTok/LayoutType.cs delete mode 100644 OpenTok/OpenTok.nuspec create mode 100644 OpenTok/ScreenShareLayoutType.cs delete mode 100644 appveyor.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..76e95492 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,30 @@ +name: "Build & Test" + +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + +env: + CONFIGURATION: "Release" + +jobs: + build: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '3.1.401' + - name: Clean + run: dotnet clean OpenTok.sln --configuration ${{ env.CONFIGURATION }} && dotnet nuget locals all --clear + - name: Install dependencies + run: dotnet restore + - name: Build + run: dotnet build OpenTok.sln --configuration ${{ env.CONFIGURATION }} --no-restore + - name: Test + run: dotnet test OpenTokTest/OpenTokTest.csproj --configuration ${{ env.CONFIGURATION }} --no-build -f netcoreapp3.1 \ No newline at end of file diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml deleted file mode 100644 index bc2a5994..00000000 --- a/.github/workflows/dotnet-core.yml +++ /dev/null @@ -1,26 +0,0 @@ - -name: .NET Core - -on: - push: - branches: [ master, dev ] - pull_request: - branches: [ master, dev ] - -jobs: - build: - - runs-on: windows-latest - - steps: - - uses: actions/checkout@v2 - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 2.2.103 - - name: Install dependencies - run: dotnet restore - - name: Build - run: dotnet build OpenTok/OpenTok.csproj --configuration Release --no-restore - - name: Test - run: dotnet test OpenTokTest/OpenTokTest.csproj --no-restore \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..4b0234f1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,24 @@ +name: Nuget Release +on: + release: + types: [published] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.release.target_commitish }} + - name: Release Nuget + uses: nexmo/github-actions/nuget-release@main + env: + PROJECT_FILE : OpenTok/OpenTok.csproj + BRANCH: main + ORGANIZATION: opentok + REPO: Opentok-.NET-SDK + TAG: ${{ github.event.release.tag_name }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + GITHUB_USER_NAME: NexmoDev + GITHUB_EMAIL: 44278943+NexmoDev@users.noreply.github.com + OUTPUT_PATH: OpenTok/bin/Release \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f8c80485..00000000 --- a/.travis.yml +++ /dev/null @@ -1,10 +0,0 @@ -language: csharp -solution: OpenTok.sln -dotnet: 1.0.1 -dist: trusty -sudo: required -script: - - dotnet restore - - dotnet build - - dotnet test ./OpenTokTest/OpenTokTest.csproj - \ No newline at end of file diff --git a/OpenTok.sln b/OpenTok.sln index 5b68c146..4105326f 100644 --- a/OpenTok.sln +++ b/OpenTok.sln @@ -1,24 +1,24 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30114.105 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.31903.286 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DC02B3EA-F1B3-4A39-A151-4FE61A39F209}" ProjectSection(SolutionItems) = preProject .gitattributes = .gitattributes .gitignore = .gitignore - .travis.yml = .travis.yml - appveyor.yml = appveyor.yml + .github\workflows\build.yml = .github\workflows\build.yml CONTRIBUTING.md = CONTRIBUTING.md DEVELOPING.md = DEVELOPING.md README.md = README.md + .github\workflows\release.yml = .github\workflows\release.yml EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloWorld", "Samples\HelloWorld\HelloWorld.csproj", "{F8A5A9AD-F680-46F4-806F-653E7E51A030}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HelloWorld", "Samples\HelloWorld\HelloWorld.csproj", "{F8A5A9AD-F680-46F4-806F-653E7E51A030}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Archiving", "Samples\Archiving\Archiving.csproj", "{B6EFF9BA-20CD-4BDE-B67B-C8E22F00E640}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Archiving", "Samples\Archiving\Archiving.csproj", "{B6EFF9BA-20CD-4BDE-B67B-C8E22F00E640}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Broadcasting", "Samples\Broadcasting\Broadcasting.csproj", "{D593D74C-46D3-4947-9F57-B40D978C40B7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Broadcasting", "Samples\Broadcasting\Broadcasting.csproj", "{D593D74C-46D3-4947-9F57-B40D978C40B7}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTok", "OpenTok\OpenTok.csproj", "{C770C266-B8E6-413A-B5AB-68EB218DC76C}" EndProject diff --git a/OpenTok/Archive.cs b/OpenTok/Archive.cs index 976f3179..bdf398b0 100644 --- a/OpenTok/Archive.cs +++ b/OpenTok/Archive.cs @@ -5,84 +5,85 @@ namespace OpenTokSDK { - /** - * Defines values returned by the Status property of an Archive object. See the ListArchives() - * method of the OpenTok class. - */ + /// + /// Defines values returned by the Status property of an Archive object. See the ListArchives() + /// method of the OpenTok class. + /// public enum ArchiveStatus { - /** - * The archive file is available for download from the OpenTok cloud. You can get the URL of - * the download file by getting the Url property of the Archive object. - */ + /// + /// The archive file is available for download from the OpenTok cloud. You can get the URL of + /// the download file by getting the Url property of the Archive object. + /// AVAILABLE, - /** - * The archive file has been deleted. - */ + /// + /// The archive file has been deleted. + /// DELETED, - /** - * The recording of the archive failed. - */ + /// + /// The recording of the archive failed. + /// FAILED, - /** - * The archive is in progress and no clients are publishing streams to the session. - * When an archive is in progress and any client publishes a stream, the status is STARTED. - * When an archive is PAUSED, nothing is recorded. When a client starts publishing a stream, - * the recording starts (or resumes). If all clients disconnect from a session that is being - * archived, the status changes to PAUSED, and after 60 seconds the archive recording stops - * (and the status changes to STOPPED). - */ + /// + /// The archive is in progress and no clients are publishing streams to the session. + /// When an archive is in progress and any client publishes a stream, the status is STARTED. + /// When an archive is PAUSED, nothing is recorded. When a client starts publishing a stream, + /// the recording starts (or resumes). If all clients disconnect from a session that is being + /// archived, the status changes to PAUSED, and after 60 seconds the archive recording stops + /// (and the status changes to STOPPED). + /// PAUSED, - /** - * The archive recording has started and is in progress. - */ + /// + /// The archive recording has started and is in progress. + /// STARTED, - /** - * The archive recording has stopped, but the file is not available. - */ + /// + /// The archive recording has stopped, but the file is not available. + /// STOPPED, - /** - * The archive is available for download from the the upload target - * Amazon S3 bucket or Windows Azure container you set up for your - * OpenTok project. - */ + /// + /// The archive is available for download from the the upload targetAmazon S3 bucket or Windows Azure container you set up for your + /// OpenTok project. + /// UPLOADED, - /** - * The archive file is no longer available for download from the OpenTok cloud. - */ + /// + /// The archive file is no longer available for download from the OpenTok cloud. + /// EXPIRED, - /** - * The status of the archive is unknown. - */ + /// + /// The status of the archive is unknown. + /// UNKOWN } - /** - * Defines values for the OutputMode property of an Archive object. - */ + /// + /// Defines values for the OutputMode property of an Archive object. + /// public enum OutputMode { - /** - * All streams in the archive are recorded to a single (composed) file. - */ + /// + /// All streams in the archive are recorded to a single (composed) file. + /// COMPOSED, - /** - * Each stream in the archive is recorded to its own individual file. - */ + /// + /// Each stream in the archive is recorded to its own individual file. + /// INDIVIDUAL } - /** - * Represents an archive of an OpenTok session. - */ + /// + /// Represents an archive of an OpenTok session. + /// public class Archive { private OpenTok opentok; + /// + /// Initializes a new instance of the class. + /// protected Archive() { - } internal Archive(OpenTok opentok) @@ -108,96 +109,98 @@ internal void CopyArchive(Archive archive) this.Resolution = archive.Resolution; } - /** - * The time at which the archive was created, in milliseconds since the Unix epoch. - */ + /// + /// The time at which the archive was created, in milliseconds since the Unix epoch. + /// public long CreatedAt { get; set; } - /** - * The duration of the archive, in milliseconds. - */ + /// + /// The duration of the archive, in seconds. + /// public long Duration { get; set; } - /** - * The archive ID. - */ + /// + /// The archive ID. + /// public Guid Id { get; set; } - /** - * The name of the archive. - */ + /// + /// The name of the archive. + /// public string Name { get; set; } - /** - * The OpenTok API key associated with the archive. - */ + /// + /// The OpenTok API key associated with the archive. + /// public int PartnerId { get; set; } - /** - * The session ID of the OpenTok session associated with this archive. - */ + /// + /// The session ID of the OpenTok session associated with this archive. + /// public String SessionId { get; set; } - /** - * For archives with the status ArchiveStatus.STOPPED or ArchiveStatus.FAILED, this string - * describes the reason the archive stopped (such as "maximum duration exceeded") or failed. - */ + /// + /// For archives with the status ArchiveStatus.STOPPED or ArchiveStatus.FAILED, this string + /// describes the reason the archive stopped (such as "maximum duration exceeded") or failed. + /// public String Reason { get; set; } - /** - * Whether the archive includes a video track (true) or not (false). - */ + /// + /// Whether the archive includes a video track (true) or not (false). + /// public bool HasVideo { get; set; } - /** - * Whether the archive includes an audio track (true) or not (false). - */ + /// + /// Whether the archive includes an audio track (true) or not (false). + /// public bool HasAudio { get; set; } - /** - * The resolution of the archive. - */ + /// + /// The resolution of the archive. + /// public string Resolution { get; set; } - /** - * Whether all streams in the archive are recorded to a single file - * (OutputMode.COMPOSED) or to individual files - * (OutputMode.INDIVIDUAL). To record streams to individual - * files, pass OutputMode.INDIVIDUAL as the outputMode - * parameter when calling the OpenTok.StartArchive() method. - */ + + /// + /// Whether all streams in the archive are recorded to a single file + /// () or to individual files + /// (). To record streams to individual + /// files, pass as the + /// parameter when calling the method. + /// public OutputMode OutputMode { get; set; } - /** - * The size of the MP4 file. For archives that have not been generated, this value is set - * to 0. We use long instead of int to support archives larger than 2GB. - */ + /// + /// The size of the MP4 file. For archives that have not been generated, this value is set + /// to 0. We use long instead of int to support archives larger than 2GB. + /// public long Size { get; set; } - /** - * The status of the archive, as defined by the ArchiveStatus enum. - */ + /// + /// The status of the archive, as defined by the ArchiveStatus enum. + /// public ArchiveStatus Status { get; set; } - /** - * The download URL of the available MP4 file. This is only set for an archive with the - * status set to ArchiveStatus.AVAILABLE; for other archives, (including archives with the - * status of ArchiveStatus.UPLOADED) this method returns null. The download URL is - * obfuscated, and the file is only available from the URL for 10 minutes. To generate a - * new URL, call the ListArchives() or GetArchive() method of the OpenTok object. - */ + /// + /// The download URL of the available MP4 file. This is only set for an archive with the + /// status set to ArchiveStatus.AVAILABLE; for other archives, (including archives with the + /// status of ArchiveStatus.UPLOADED) this method returns null. The download URL is + /// obfuscated, and the file is only available from the URL for 10 minutes. To generate a + /// new URL, call the ListArchives() or GetArchive() method of the OpenTok object. + /// public String Url { get; set; } - /** - * The encryption password of the archive. - */ + /// + /// The encryption password of the archive. + /// public String Password { get; set; } - /** - * Stops the OpenTok archive if it is being recorded. - *

- * Archives automatically stop recording after 120 minutes or when all clients have - * disconnected from the session being archived. - */ + ///

+ /// Stops the OpenTok archive if it is being recorded. + /// + /// Archives automatically stop recording after 120 minutes or when all clients have + /// disconnected from the session being archived. + /// + /// public void Stop() { if (opentok != null) @@ -207,13 +210,14 @@ public void Stop() } } - /** - * Deletes the OpenTok archive. - *

- * You can only delete an archive which has a status of "available" or "uploaded". Deleting - * an archive removes its record from the list of archives. For an "available" archive, it - * also removes the archive file, making it unavailable for download. - */ + ///

+ /// Deletes the OpenTok archive. + /// + /// You can only delete an archive which has a status of "available" or "uploaded". Deleting + /// an archive removes its record from the list of archives. For an "available" archive, it + /// also removes the archive file, making it unavailable for download. + /// + /// public void Delete() { if (opentok != null) diff --git a/OpenTok/ArchiveLayout.cs b/OpenTok/ArchiveLayout.cs new file mode 100644 index 00000000..40a9ba8a --- /dev/null +++ b/OpenTok/ArchiveLayout.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.ComponentModel; + +namespace OpenTokSDK +{ + /// + /// Layout the archive is going to use + /// + public class ArchiveLayout + { + /// + /// The type of layout you'd like to use + /// + [JsonProperty("type")] + [JsonConverter(typeof(StringEnumConverter))] + public LayoutType Type { get; set; } + + /// + /// The stylesheet to use for the layout. Must be set if using custom. + /// + [JsonProperty("stylesheet", DefaultValueHandling = DefaultValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore)] + [DefaultValue("")] + public string StyleSheet { get; set; } + + /// + /// The to use when there is a screen-sharing + /// stream in the session. Note that to use this property, + /// you must set the property to + /// and leave the property unset. + /// For more information, see Layout types for screen sharing. + /// NOTE: is not valid for this property + /// + [JsonConverter(typeof(StringEnumConverter), true)] + [JsonProperty("screensharetype", NullValueHandling = NullValueHandling.Ignore)] + public ScreenShareLayoutType? ScreenShareType { get; set; } + } +} diff --git a/OpenTok/ArchiveList.cs b/OpenTok/ArchiveList.cs index a8e9a7b1..2b7e393d 100644 --- a/OpenTok/ArchiveList.cs +++ b/OpenTok/ArchiveList.cs @@ -5,14 +5,14 @@ namespace OpenTokSDK { - /** - * A class for accessing an array of Archive objects. - */ + /// + /// A class for accessing an array of Archive objects. + /// public class ArchiveList : List { - /** - * The total number of archives (associated with your OpenTok API key). - */ + /// + /// The total number of archives (associated with your OpenTok API key). + /// public int TotalCount { get; private set; } internal ArchiveList(List items, int totalCount) diff --git a/OpenTok/Broadcast.cs b/OpenTok/Broadcast.cs index 73d370fd..d08ac5c7 100644 --- a/OpenTok/Broadcast.cs +++ b/OpenTok/Broadcast.cs @@ -5,28 +5,31 @@ namespace OpenTokSDK { - /** - * Represents a broadcast of an OpenTok session. - */ + /// + /// Represents a broadcast of an OpenTok session. + /// public class Broadcast { - /** - * Defines values returned by the Status property of a Broadcast object. - */ + /// + /// Defines values returned by the Status property of a Broadcast object. + /// public enum BroadcastStatus { - /** - * The broadcast is stopped - */ + /// + /// The broadcast is stopped. + /// STOPPED, - /** - * The broadcast is started - */ + /// + /// The broadcast is started. + /// STARTED } private OpenTok opentok; + /// + /// Initializes a new instance of the class. + /// protected Broadcast() { @@ -48,10 +51,11 @@ internal void CopyBroadcast(Broadcast broadcast) MaxDuration = broadcast.MaxDuration; Status = broadcast.Status; BroadcastUrls = broadcast.BroadcastUrls; - + if (BroadcastUrls != null) { - if (BroadcastUrls.ContainsKey("hls")) { + if (BroadcastUrls.ContainsKey("hls")) + { Hls = BroadcastUrls["hls"].ToString(); } @@ -68,76 +72,75 @@ internal void CopyBroadcast(Broadcast broadcast) } } } - } + } - /** - * The broadcast ID. - */ + /// + /// The broadcast ID. + /// [JsonProperty("id")] public string Id { get; set; } - /** - * The session ID of the OpenTok session associated with this broadcast. - */ + /// + /// The session ID of the OpenTok session associated with this broadcast. + /// [JsonProperty("sessionId")] public String SessionId { get; set; } - /** - * The OpenTok API key associated with the broadcast. - */ + /// + /// The OpenTok API key associated with the broadcast. + /// [JsonProperty("projectId")] public int ProjectId { get; set; } - /** - * The time the broadcast started, expressed in milliseconds since the Unix epoch (January 1, 1970, 00:00:00 UTC). - */ + /// + /// The time the broadcast started, expressed in milliseconds since the Unix epoch (January 1, 1970, 00:00:00 UTC). + /// [JsonProperty("createdAt")] public long CreatedAt { get; set; } - /** - * The time the broadcast was updated, in milliseconds since the Unix epoch (January 1, 1970, 00:00:00 UTC). - */ + /// + /// The time the broadcast was updated, in milliseconds since the Unix epoch (January 1, 1970, 00:00:00 UTC). + /// [JsonProperty("updatedAt")] public long UpdatedAt { get; set; } - /** - * The resolution of the broadcast: either "640x480" (SD, the default) or "1280x720" (HD). - */ + /// + /// The resolution of the broadcast: either "640x480" (SD, the default) or "1280x720" (HD). + /// [JsonProperty("resolution")] public string Resolution { get; set; } - /** - * The status of the broadcast: either "started" or "stopped". - */ + /// + /// The status of the broadcast: either "started" or "stopped". + /// [JsonProperty("status")] public BroadcastStatus Status { get; set; } - /** - * The maximun duration of the broadcast - */ + /// + /// The maximun duration of the broadcast. + /// [JsonProperty("maxDuration")] public int MaxDuration { get; set; } - - /** - * The RTMP List - */ + /// + /// The RTMP List. + /// public List RtmpList { get; set; } - - /** - * HLS Url - */ + + /// + /// HLS Url. + /// public String Hls { get; set; } - /** - * The broadcast HLS and RTMP URLs - */ + /// + /// The broadcast HLS and RTMP URLs. + /// [JsonProperty("broadcastUrls")] private Dictionary BroadcastUrls { get; set; } - /** - * Stops the live broadcasting if it is started. - */ + /// + /// Stops the live broadcasting if it is started. + /// public void Stop() { if (opentok != null) diff --git a/OpenTok/BroadcastLayout.cs b/OpenTok/BroadcastLayout.cs index 9468cf13..418ef2ef 100644 --- a/OpenTok/BroadcastLayout.cs +++ b/OpenTok/BroadcastLayout.cs @@ -9,61 +9,92 @@ namespace OpenTokSDK { - /** - * Represents a broadcast layout of an OpenTok session. - */ + /// + /// Represents a broadcast layout of an OpenTok session. + /// public class BroadcastLayout { - /** - * Defines values for the role parameter of the GenerateToken method of the OpenTok class. - */ + /// + /// Defines values for the layout parameter of the StartBroadcast method of the OpenTok class. + /// public enum LayoutType { - - /** - * Picture-in-Picture - */ + /// + /// Picture-in-Picture + /// Pip, - /** - * Best Fit - */ + /// + /// Best Fit + /// BestFit, - /** - * Vertical Presentation - */ + /// + /// Vertical Presentation + /// VerticalPresentation, - /** - * Horizontal Presentation - */ + /// + /// Horizontal Presentation + /// HorizontalPresentation, - /** - * Custom Layout - */ + /// + /// Custom Layout + /// Custom } + /// + /// Initalizes a Broadcast layout with the given , automatically + /// setting the Type to BestFit. + /// + /// + public BroadcastLayout(ScreenShareLayoutType type) + { + Type = LayoutType.BestFit; + ScreenShareType = type; + } + + /// + /// Initalizes a BroadcastLayout with the given + /// + /// public BroadcastLayout(LayoutType Type) { this.Type = Type; } + /// + /// Initalizes a BroadcastLayout with the given and stylesheet - note Type must be + /// + /// + /// A string de public BroadcastLayout(LayoutType Type, string Stylesheet) { this.Type = Type; this.Stylesheet = Stylesheet; } - /** - * The Layout type - */ + /// + /// The Layout type + /// [JsonConverter(typeof(StringEnumConverter), true)] [JsonProperty("type")] public LayoutType Type { get; set; } - /** - * The Stylesheet for the Custom Layout - */ + /// + /// The Stylesheet for the Custom Layout + /// [JsonProperty("stylesheet")] - public string Stylesheet { get; set; } + public string Stylesheet { get; set; } = null; + + /// + /// The to use when there is a screen-sharing + /// stream in the session. Note that to use this property, + /// you must set the property to + /// and leave the property unset. + /// For more information, see Layout types for screen sharing. + /// NOTE: is not valid for this property + /// + [JsonConverter(typeof(StringEnumConverter), true)] + [JsonProperty("screensharetype", NullValueHandling = NullValueHandling.Ignore)] + public ScreenShareLayoutType? ScreenShareType { get; set; } } } diff --git a/OpenTok/Constants/OpenTokVersion.cs b/OpenTok/Constants/OpenTokVersion.cs index 07957c05..5ae90bde 100644 --- a/OpenTok/Constants/OpenTokVersion.cs +++ b/OpenTok/Constants/OpenTokVersion.cs @@ -1,14 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace OpenTokSDK.Constants +namespace OpenTokSDK.Constants { - /** - * For internal use. - */ - class OpenTokVersion + /// + /// For internal use. + /// + internal class OpenTokVersion { private static string Version = "Opentok-DotNet-SDK/" + typeof(OpenTokVersion).Assembly.GetName().Version; diff --git a/OpenTok/Exception/OpenTokException.cs b/OpenTok/Exception/OpenTokException.cs index c7eff36c..8c302b37 100644 --- a/OpenTok/Exception/OpenTokException.cs +++ b/OpenTok/Exception/OpenTokException.cs @@ -5,82 +5,87 @@ namespace OpenTokSDK.Exception { - /** - * Defines exceptions in the OpenTok SDK. - */ + /// + /// Defines exceptions in the OpenTok SDK. + /// public class OpenTokException : System.Exception { - private System.Exception exception; - private string message; - - /** - * Constructor. Do not use. - */ + + /// + /// Construct opentok exception + /// public OpenTokException() { } - /** - * Constructor. Do not use. - */ + /// + /// Construct OpentTokException with a message + /// + /// public OpenTokException(string message) - : base(message) - { - this.message = message; - } - - /** - * Constructor. Do not use. - */ + : base(message){} + + /// + /// Construct OpenTokException with a message and an inner exception + /// + /// + /// public OpenTokException(string message, System.Exception exception) - : base(message) - { - this.message = message; - this.exception = exception; - } + : base(message, exception){} - //GGB override Message property + /// + /// Get's the message of the exception + /// + /// public string GetMessage() { - return message; + return Message; } + /// + /// Get's the inner exception + /// + /// public System.Exception GetException() { - return exception; + return InnerException; } } - /** - * Defines an exception object thrown when an invalid argument is passed into a method. - */ + /// + /// Defines an exception object thrown when an invalid argument is passed into a method. + /// public class OpenTokArgumentException : OpenTokException { - /** - * Constructor. Do not use. - */ + /// + /// Constructor. Do not use. + /// + /// public OpenTokArgumentException(string message) : base(message) { } } - /** - * Defines an exception object thrown when a REST API call results in an error response. - */ + /// + /// Defines an exception object thrown when a REST API call results in an error response. + /// public class OpenTokWebException : OpenTokException { - /** - * Constructor. Do not use. - */ + /// + /// Constructor. Do not use. + /// + /// + /// public OpenTokWebException(string message, System.Exception exception) : base(message, exception) { } - /** - * Constructor. Do not use. - */ + /// + /// Constructor. Do not use. + /// + /// public OpenTokWebException(string message) : base(message) { diff --git a/OpenTok/LayoutType.cs b/OpenTok/LayoutType.cs new file mode 100644 index 00000000..a79ca195 --- /dev/null +++ b/OpenTok/LayoutType.cs @@ -0,0 +1,29 @@ +namespace OpenTokSDK +{ + /// + /// The Layout Type for the an Archive. + /// + public enum LayoutType + { + /// + /// Best fit layout. + /// + bestFit, + /// + /// Use a Custom layout (stylesheet property must be set). + /// + custom, + /// + /// Horizontal presentation. + /// + horizontalPresentation, + /// + /// Picture-in-picture. + /// + pip, + /// + /// Vertical presentation. + /// + verticalPresentation + } +} diff --git a/OpenTok/OpenTok.cs b/OpenTok/OpenTok.cs index 254c97c3..6228fb95 100644 --- a/OpenTok/OpenTok.cs +++ b/OpenTok/OpenTok.cs @@ -8,47 +8,51 @@ namespace OpenTokSDK { - /** - * Contains methods for creating OpenTok sessions, generating tokens, and working with archives. - *

- * To create a new OpenTok object, call the OpenTok() constructor with your OpenTok API key - * and the API secret for your OpenTok project. - * Do not publicly share your API secret. You will use it with the OpenTok constructor - * (only on your web server) to create OpenTok sessions. - */ + ///

+ /// Contains methods for creating OpenTok sessions, generating tokens, and working with archives. + /// + /// To create a new OpenTok object, call the OpenTok() constructor with your OpenTok API key + /// and the API secret for your OpenTok project. + /// Do not publicly share your API secret. You will use it with the OpenTok constructor + /// (only on your web server) to create OpenTok sessions. + /// + /// public class OpenTok { - - /** The OpenTok API key passed into the OpenTok() constructor. */ + /// + /// The OpenTok API key passed into the OpenTok() constructor. + /// public int ApiKey { get; private set; } - /** The OpenTok API secret passed into the OpenTok() constructor. */ + /// + /// The OpenTok API secret passed into the OpenTok() constructor. + /// public string ApiSecret { get; private set; } private string OpenTokServer { get; set; } - /** For internal use. */ + /// + /// For internal use + /// public HttpClient Client { private get; set; } - /** - * Enables writing request/response details to console. - * Don't use in a production environment. - */ private bool _debug; - public bool Debug { - get { return _debug; } - set - { - _debug = value; - Client.debug = _debug; - } + /// + /// Enables writing request/response details to console. + /// Don't use in a production environment. + /// + public bool Debug + { + get { return _debug; } + set + { + _debug = value; + Client.debug = _debug; + } } - /** - * Creates an OpenTok object. - * - * @param apiKey Your OpenTok API key. (See the - * TokBox account page.) - * @param apiSecret Your OpenTok API secret. (See the - * TokBox account page.) - */ + /// + /// Creates an OpenTok object. + /// + /// Your OpenTok API key. (See the TokBox account page + /// Your OpenTok API secret. (See the TokBox account page public OpenTok(int apiKey, string apiSecret) { this.ApiKey = apiKey; @@ -58,9 +62,12 @@ public OpenTok(int apiKey, string apiSecret) this.Debug = false; } - /** - * For TokBox internal use. - */ + /// + /// For TokBox internal use. + /// + /// + /// + /// public OpenTok(int apiKey, string apiSecret, string apiUrl) { this.ApiKey = apiKey; @@ -70,69 +77,70 @@ public OpenTok(int apiKey, string apiSecret, string apiUrl) this.Debug = false; } - /** - * Creates a new OpenTok session. - *

- * OpenTok sessions do not expire. However, authentication tokens do expire (see the - * generateToken() method). Also note that sessions cannot explicitly be destroyed. - *

- * A session ID string can be up to 255 characters long. - *

- * Calling this method results in an OpenTokException in the event of an error. - * Check the error message for details. - * - * You can also create a session using the - * OpenTok - * REST API or by logging in to your - * TokBox account. - * - * @param location (String) An IP address that the OpenTok servers will use to - * situate the session in its global network. If you do not set a location hint, - * the OpenTok servers will be based on the first client connecting to the session. - * - * @param mediaMode Whether the session will transmit streams using the - * OpenTok Media Router (MediaMode.ROUTED) or not - * (MediaMode.RELAYED). By default, the setting is - * MediaMode.RELAYED. - *

- * With the mediaMode parameter set to MediaMode.RELAYED, the - * session will attempt to transmit streams directly between clients. If clients cannot - * connect due to firewall restrictions, the session uses the OpenTok TURN server to relay - * streams. - *

- * The OpenTok Media Router provides the following benefits: - * - *

    - *
  • The OpenTok Media Router can decrease bandwidth usage in multiparty sessions. - * (When the mediaMode parameter is set to - * MediaMode.ROUTED, each client must send a separate audio-video stream - * to each client subscribing to it.)
  • - *
  • The OpenTok Media Router can improve the quality of the user experience through - * audio fallback and video - * recovery. With these features, if a client's connectivity degrades to a degree - * that it does not support video for a stream it's subscribing to, the video is dropped - * on that client (without affecting other clients), and the client receives audio only. - * If the client's connectivity improves, the video returns.
  • - *
  • The OpenTok Media Router supports the - * archiving - * feature, which lets you record, save, and retrieve OpenTok sessions.
  • - *
- * - * @param archiveMode Whether the session is automatically archived - * (ArchiveMode.ALWAYS) or not (ArchiveMode.MANUAL). By default, - * the setting is ArchiveMode.MANUAL, and you must call the - * StartArchive() method of the OpenTok object to start archiving. To archive the session - * (either automatically or not), you must set the mediaMode parameter to - * MediaMode.ROUTED. - * - * @return A Session object representing the new session. The Id property of - * the Session is the session ID, which uniquely identifies the session. You will use - * this session ID in the client SDKs to identify the session. For example, when using the - * OpenTok.js library, use the session ID when calling the - * - * OT.initSession() method (to initialize an OpenTok session). - */ + /// + /// Creates a new OpenTok session. + /// + /// OpenTok sessions do not expire. However, authentication tokens do expire (see the + /// generateToken() method). Also note that sessions cannot explicitly be destroyed. + /// + /// + /// A session ID string can be up to 255 characters long. + /// + /// + /// Calling this method results in an OpenTokException in the event of an error. + /// Check the error message for details. + /// + /// You can also create a session using the + /// OpenTok + /// REST API or by logging in to your + /// TokBox account. + /// + /// + /// + /// An IP address that the OpenTok servers will use to situate the session in its + /// global network. If you do not set a location hint, the OpenTok servers will be + /// based on the first client connecting to the session. + /// + /// + /// Whether the session will transmit streams using the OpenTok Media Router + /// () or not (). + /// By default, the setting is . + /// + /// With the parameter set to , the session will + /// attempt to transmit streams directly between clients. If clients cannot connect + /// due to firewall restrictions, the session uses the OpenTok TURN server to relay streams. + /// + /// + /// The + /// OpenTok Media Router provides the following benefits: + /// - The OpenTok Media Router can decrease bandwidth usage in multiparty sessions. + /// (When the parameter is set to , + /// each client must send a separate audio-video stream to each client subscribing to it.) + /// - The OpenTok Media Router can improve the quality of the user experience through + /// audio fallback and video recovery + /// With these features, if a client's connectivity degrades to a degree that it does not + /// support video for a stream it's subscribing to, the video is dropped on that client + /// (without affecting other clients), and the client receives audio only. If the client's + /// connectivity improves, the video returns. + /// - The OpenTok Media Router supports the archiving + /// feature, which lets you record, save, and retrieve OpenTok sessions. + /// + /// + /// + /// Whether the session is automatically archived () or not + /// (). By default, the setting is + /// and you must call the method of the OpenTok object to start archiving. + /// To archive the session (either automatically or not), you must set the mediaMode parameter to + /// + /// + /// + /// A Session object representing the new session. The property of the + /// is the session ID, which uniquely identifies the session. You will use + /// this session ID in the client SDKs to identify the session. For example, when using the + /// OpenTok.js library, use the session ID when calling the + /// OT.initSession() + /// method (to initialize an OpenTok session). + /// public Session CreateSession(string location = "", MediaMode mediaMode = MediaMode.RELAYED, ArchiveMode archiveMode = ArchiveMode.MANUAL) { @@ -168,43 +176,41 @@ public Session CreateSession(string location = "", MediaMode mediaMode = MediaMo return new Session(sessionId, apiKey, ApiSecret, location, mediaMode, archiveMode); } - /** - * Creates a token for connecting to an OpenTok session. In order to authenticate a user - * connecting to an OpenTok session, the client passes a token when connecting to the - * session. - *

- * For testing, you can also generate test tokens by logging in to your - * TokBox account. - * - * @param sessionId The session ID corresponding to the session to which the user will - * connect. - * - * @param role The role for the token. Valid values are defined in the Role enum: - *

    - *
  • Role.SUBSCRIBER — A subscriber can only subscribe to - * streams.
  • - * - *
  • Role.PUBLISHER — A publisher can publish streams, subscribe to - * streams, and signal. (This is the default value if you do not specify a role.)
  • - * - *
  • Role.MODERATOR — In addition to the privileges granted to a - * publisher, in clients using the OpenTok.js library, a moderator can call the - * forceUnpublish() and forceDisconnect() method of the - * Session object.
  • - *
- * - * @param expireTime The expiration time of the token, in seconds since the UNIX epoch. - * Pass in 0 to use the default expiration time of 24 hours after the token creation time. - * The maximum expiration time is 30 days after the creation time. - * - * @param data A string containing connection metadata describing the end-user. For example, - * you can pass the user ID, name, or other data describing the end-user. The length of the - * string is limited to 1000 characters. This data cannot be updated once it is set. - * - * @param initialLayoutClassList A list of strings values containing the initial layout for the stream. - * - * @return The token string. - */ + /// + /// Creates a token for connecting to an OpenTok session. In order to authenticate a user + /// connecting to an OpenTok session, the client passes a token when connecting to the session. + /// + /// For testing, you can also generate test tokens by logging in to your + /// TokBox account. + /// + /// + /// + /// The session ID corresponding to the session to which the user will connect. + /// + /// + /// The role for the token. Valid values are defined in the Role enum: + /// - (A subscriber can only subscribe to streams) + /// - (A publisher can publish streams, subscribe to streams, and signal. + /// (This is the default value if you do not specify a role.)) + /// - (In addition to the privileges granted to a publisher, + /// a moderator can perform moderation functions, such as forcing clients + /// to disconnect, to stop publishing streams, or to mute audio in published streams. See the + /// Moderation developer guide. + /// + /// + /// The expiration time of the token, in seconds since the UNIX epoch. Pass in 0 to use the default + /// expiration time of 24 hours after the token creation time. The maximum expiration time is 30 days + /// after the creation time. + /// + /// + /// A string containing connection metadata describing the end-user. For example, you can pass the + /// user ID, name, or other data describing the end-user. The length of the string is limited to 1000 + /// characters. This data cannot be updated once it is set. + /// + /// + /// A list of strings values containing the initial layout for the stream. + /// + /// public string GenerateToken(string sessionId, Role role = Role.PUBLISHER, double expireTime = 0, string data = null, List initialLayoutClassList = null) { if (String.IsNullOrEmpty(sessionId)) @@ -221,47 +227,56 @@ public string GenerateToken(string sessionId, Role role = Role.PUBLISHER, double return session.GenerateToken(role, expireTime, data, initialLayoutClassList); } - /** - * Starts archiving an OpenTok session. - * - *

- * Clients must be actively connected to the OpenTok session for you to successfully start - * recording an archive. - *

- * You can only record one archive at a time for a given session. You can only record - * archives of sessions that uses the OpenTok Media Router (sessions with the media mode set - * to routed); you cannot archive sessions with the media mode set to relayed. - *

- * Note that you can have the session be automatically archived by setting the archiveMode - * parameter of the OpenTok.CreateSession() method to ArchiveMode.ALWAYS. - * - * @param sessionId The session ID of the OpenTok session to archive. - * - * @param name The name of the archive. You can use this name to identify the archive. It is - * a property of the Archive object, and it is a property of archive-related events in the - * OpenTok client libraries. - * - * @param hasVideo Whether the archive will record video (true) or not (false). The default - * value is true (video is recorded). If you set both hasAudio and - * hasVideo to false, the call to the StartArchive() method - * results in an error. - * - * @param hasAudio Whether the archive will record audio (true) or not (false). The default - * value is true (audio is recorded). If you set both hasAudio and - * hasVideo to false, the call to the StartArchive() method - * results in an error. - * - * @param outputMode Whether all streams in the archive are recorded to a single file - * (OutputMode.COMPOSED, the default) or to individual files - * (OutputMode.INDIVIDUAL). - * - * @param resolution The resolution for the archive. The default for OutputMode.COMPOSED - * is "640x480". You cannot specify the resolution for OutputMode.INDIVIDUAL. - * - * @return The Archive object. This object includes properties defining the archive, - * including the archive ID. - */ - public Archive StartArchive(string sessionId, string name = "", bool hasVideo = true, bool hasAudio = true, OutputMode outputMode = OutputMode.COMPOSED, string resolution = null) + ///

+ /// Starts archiving an OpenTok session. + /// + /// Clients must be actively connected to the OpenTok session for you to successfully start + /// recording an archive. + /// + /// + /// You can only record one archive at a time for a given session. You can only record + /// archives of sessions that uses the OpenTok Media Router (sessions with the media mode set + /// to routed); you cannot archive sessions with the media mode set to relayed. + /// + /// + /// Note that you can have the session be automatically archived by setting the archiveMode + /// parameter of the method to . + /// + /// + /// + /// The session ID of the OpenTok session to archive. + /// + /// + /// The name of the archive. You can use this name to identify the archive. It is a property + /// of the Archive object, and it is a property of archive-related events in the OpenTok client + /// libraries. + /// + /// + /// Whether the archive will record video (true) or not (false). The default value is true + /// (video is recorded). If you set both and + /// to false, the call to the method results in an error. + /// + /// + /// Whether the archive will record audio (true) or not (false). The default value is true + /// (audio is recorded). If you set both and + /// to false, the call to the method results in an error. + /// + /// + /// Whether all streams in the archive are recorded to a single file (, + /// the default) or to individual files (). + /// + /// + /// The resolution for the archive. The default for is "640x480". + /// You cannot specify the resolution for . + /// + /// + /// The layout that you want to use for your archive. If type is set to + /// you must provide a StyleSheet string to Vonage how to layout your archive. + /// + /// + /// The Archive object. This object includes properties defining the archive, including the archive ID. + /// + public Archive StartArchive(string sessionId, string name = "", bool hasVideo = true, bool hasAudio = true, OutputMode outputMode = OutputMode.COMPOSED, string resolution = null, ArchiveLayout layout = null) { if (String.IsNullOrEmpty(sessionId)) { @@ -274,23 +289,38 @@ public Archive StartArchive(string sessionId, string name = "", bool hasVideo = if (!String.IsNullOrEmpty(resolution) && outputMode.Equals(OutputMode.INDIVIDUAL)) { throw new OpenTokArgumentException("Resolution can't be specified for Individual Archives"); - } else if(!String.IsNullOrEmpty(resolution) && outputMode.Equals(OutputMode.COMPOSED)) + } + else if (!String.IsNullOrEmpty(resolution) && outputMode.Equals(OutputMode.COMPOSED)) { data.Add("resolution", resolution); } + if (layout != null) + { + if (layout?.Type == LayoutType.custom && string.IsNullOrEmpty(layout?.StyleSheet) || + layout?.Type != LayoutType.custom && !string.IsNullOrEmpty(layout?.StyleSheet)) + { + throw new OpenTokArgumentException("Could not set layout, stylesheet must be set if and only if type is custom"); + } + else if(layout.ScreenShareType != null && layout.Type != LayoutType.bestFit) + { + throw new OpenTokArgumentException($"Could not set screenShareLayout. When screenShareType is set, layout.Type must be bestFit, was {layout.Type}"); + } + data.Add("layout", layout); + } + string response = Client.Post(url, headers, data); return OpenTokUtils.GenerateArchive(response, ApiKey, ApiSecret, OpenTokServer); } - /** - * Stops an OpenTok archive that is being recorded. - *

- * Archives automatically stop recording after 120 minutes or when all clients have - * disconnected from the session being archived. - * - * @param archiveId The archive ID of the archive you want to stop recording. - * @return The Archive object corresponding to the archive being STOPPED. - */ + ///

+ /// Stops an OpenTok archive that is being recorded. + /// + /// Archives automatically stop recording after 120 minutes or when all clients have + /// disconnected from the session being archived. + /// + /// + /// The archive ID of the archive you want to stop recording. + /// The Archive object corresponding to the archive being STOPPED. public Archive StopArchive(string archiveId) { string url = string.Format("v2/project/{0}/archive/{1}/stop", this.ApiKey, archiveId); @@ -300,43 +330,40 @@ public Archive StopArchive(string archiveId) return JsonConvert.DeserializeObject(response); } - /** - * Returns a List of Archive objects, representing archives that are both - * both completed and in-progress, for your API key. This list is limited to 1000 archives - * starting with the first archive recorded. For a specific range of archives, call - * listArchives(int offset, int count). - * - * @return A List of Archive objects. - */ - public ArchiveList ListArchives() - { - return ListArchives(0, 0); - } - - /** - * Returns a List of Archive objects, representing archives that are both - * both completed and in-progress, for your API key. - * - * @param offset The index offset of the first archive. 0 is offset of the most recently - * started archive. 1 is the offset of the archive that started prior to the most recent - * archive. - * - * @param count The number of archives to be returned. The maximum number of archives - * returned is 1000. - * - * @return A List of Archive objects. - */ - public ArchiveList ListArchives(int offset, int count) + /// + /// Returns a List of objects, representing archives that are both + /// both completed and in-progress, for your API key. + /// + /// + /// The index offset of the first archive. 0 is offset of the most recently started archive. + /// 1 is the offset of the archive that started prior to the most recent archive. + /// + /// + /// The number of archives to be returned. The maximum number of archives returned is 1000. + /// + /// + /// The session ID. + /// + /// A List of objects. + public ArchiveList ListArchives(int offset = 0 , int count = 0, string sessionId = "") { if (count < 0) { - throw new OpenTokArgumentException("count cannot be smaller than 1"); + throw new OpenTokArgumentException("count cannot be smaller than 0"); } string url = string.Format("v2/project/{0}/archive?offset={1}", this.ApiKey, offset); if (count > 0) { url = string.Format("{0}&count={1}", url, count); } + if (!string.IsNullOrEmpty(sessionId)) + { + if (!OpenTokUtils.ValidateSession(sessionId)) + { + throw new OpenTokArgumentException("Session Id is not valid"); + } + url = $"{url}&sessionId={sessionId}"; + } string response = Client.Get(url); JObject archives = JObject.Parse(response); JArray archiveArray = (JArray)archives["items"]; @@ -344,29 +371,28 @@ public ArchiveList ListArchives(int offset, int count) return archiveList; } - /** - * Gets an Archive object for the given archive ID. - * - * @param archiveId The archive ID. - * @return The Archive object. - */ + /// + /// Gets an Archive object for the given archive ID. + /// + /// The archive ID. + /// The object. public Archive GetArchive(string archiveId) { string url = string.Format("v2/project/{0}/archive/{1}", this.ApiKey, archiveId); var headers = new Dictionary { { "Content-type", "application/json" } }; - string response = Client.Get(url); ; + string response = Client.Get(url); return JsonConvert.DeserializeObject(response); } - /** - * Deletes an OpenTok archive. - *

- * You can only delete an archive which has a status of "available" or "uploaded". Deleting - * an archive removes its record from the list of archives. For an "available" archive, it - * also removes the archive file, making it unavailable for download. - * - * @param archiveId The archive ID of the archive you want to delete. - */ + ///

+ /// Deletes an OpenTok archive. + /// + /// You can only delete an archive which has a status of "available" or "uploaded". Deleting + /// an archive removes its record from the list of archives. For an "available" archive, it + /// also removes the archive file, making it unavailable for download. + /// + /// + /// The archive ID of the archive you want to delete. public void DeleteArchive(string archiveId) { string url = string.Format("v2/project/{0}/archive/{1}", this.ApiKey, archiveId); @@ -374,15 +400,12 @@ public void DeleteArchive(string archiveId) Client.Delete(url, headers); } - /** - * Gets a Stream object for the given stream ID. - * - * @param sessionId The session ID of the OpenTok session. - * - * @param streamId The stream ID. - * - * @return The Stream object. - */ + /// + /// Gets a Stream object for the given stream ID. + /// + /// The session ID of the OpenTok session. + /// The stream ID. + /// The object. public Stream GetStream(string sessionId, string streamId) { if (String.IsNullOrEmpty(sessionId) || String.IsNullOrEmpty(streamId)) @@ -398,14 +421,12 @@ public Stream GetStream(string sessionId, string streamId) return streamCopy; } - /** - * Returns a List of Stream objects, representing streams that are in-progress, - * for the Session Id. - * - * @param sessionId The session ID corresponding to the session. - * - * @return A List of Stream objects. - */ + /// + /// Returns a List of objects, representing streams that are in-progress, + /// for the Session Id. + /// + /// The session ID corresponding to the session. + /// A List of objects. public StreamList ListStreams(string sessionId) { if (String.IsNullOrEmpty(sessionId)) @@ -420,20 +441,18 @@ public StreamList ListStreams(string sessionId) return streamList; } - /** - * Force disconnects a specific client connected to an OpenTok session. - * - * @param sessionId The session ID corresponding to the session. - * - * @param connectionId The connectionId of the connection in a session.. - */ + /// + /// Force disconnects a specific client connected to an OpenTok session. + /// + /// The session ID corresponding to the session. + /// The connectionId of the connection in a session. public void ForceDisconnect(string sessionId, string connectionId) { if (String.IsNullOrEmpty(sessionId) || String.IsNullOrEmpty(connectionId)) { throw new OpenTokArgumentException("The sessionId or connectionId cannot be null or empty"); } - + if (!OpenTokUtils.ValidateSession(sessionId)) { throw new OpenTokArgumentException("Invalid session Id"); @@ -443,28 +462,44 @@ public void ForceDisconnect(string sessionId, string connectionId) Client.Delete(url, headers); } - /** - * Use this method to start a live streaming for an OpenTok session. - * This broadcasts the session to an HLS (HTTP live streaming) or to RTMP streams. - *

- * To successfully start broadcasting a session, at least one client must be connected to the session. - *

- * You can only have one active live streaming broadcast at a time for a session - * (however, having more than one would not be useful). - * The live streaming broadcast can target one HLS endpoint and up to five RTMP servers simulteneously for a session. - * You can only start live streaming for sessions that use the OpenTok Media Router (with the media mode set to routed); - * you cannot use live streaming with sessions that have the media mode set to relayed OpenTok Media Router. See - * The OpenTok Media Router and media modes. - *

- * For more information on broadcasting, see the - * Broadcast developer guide. - * - * @param sessionId The session ID corresponding to the session. - * - * @param properties This BroadcastProperties object defines options for the broadcast. - * - * @return The Broadcast object. This object includes properties defining the archive, including the archive ID. - */ + ///

+ /// Use this method to start a live streaming for an OpenTok session. + /// This broadcasts the session to an HLS (HTTP live streaming) or to RTMP streams. + /// + /// To successfully start broadcasting a session, at least one client must be connected to the session. + /// + /// + /// You can only have one active live streaming broadcast at a time for a session + /// (however, having more than one would not be useful). + /// The live streaming broadcast can target one HLS endpoint and up to five RTMP servers simultaneously for a session. + /// You can only start live streaming for sessions that use the OpenTok Media Router (with the media mode set to routed); + /// you cannot use live streaming with sessions that have the media mode set to relayed OpenTok Media Router. See + /// The OpenTok Media Router and media modes. + /// + /// + /// For more information on broadcasting, see the + /// Broadcast developer guide. + /// + /// + /// The session ID corresponding to the session. + /// Whether to include an HLS broadcast. + /// + /// A list of objects, defining RTMP streams to be broadcast (up to five). + /// + /// + /// The resolution of the broadcast video. This can be set to either "640x480" or "1280x720". + /// + /// + /// The maximum duration for the broadcast, in seconds. The broadcast will automatically + /// stop when the maximum duration is reached. You can set the maximum duration to a value + /// from 60 (60 seconds) to 36000 (10 hours). The default maximum duration is 2 hours + /// (7,200 seconds). + /// + /// + /// Specify this BroadcastLayout object to assign the initial layout type for + /// the broadcast. + /// + /// The Broadcast object. This object includes properties defining the archive, including the archive ID. public Broadcast StartBroadcast(string sessionId, Boolean hls = true, List rtmpList = null, string resolution = null, int maxDuration = 7200, BroadcastLayout layout = null) { if (String.IsNullOrEmpty(sessionId)) @@ -472,7 +507,7 @@ public Broadcast StartBroadcast(string sessionId, Boolean hls = true, List throw new OpenTokArgumentException("Session not valid"); } - if(!String.IsNullOrEmpty(resolution) && resolution != "640x480" && resolution != "1280x720") + if (!String.IsNullOrEmpty(resolution) && resolution != "640x480" && resolution != "1280x720") { throw new OpenTokArgumentException("Resolution value must be either 640x480 (SD) or 1280x720 (HD)."); } @@ -491,13 +526,14 @@ public Broadcast StartBroadcast(string sessionId, Boolean hls = true, List var headers = new Dictionary { { "Content-type", "application/json" } }; var outputs = new Dictionary(); - if (hls) { + if (hls) + { outputs.Add("hls", new Object()); } - if(rtmpList != null) + if (rtmpList != null) { - outputs.Add("rtmp", rtmpList); + outputs.Add("rtmp", rtmpList); } var data = new Dictionary() { @@ -509,19 +545,28 @@ public Broadcast StartBroadcast(string sessionId, Boolean hls = true, List if (!String.IsNullOrEmpty(resolution)) { data.Add("resolution", resolution); - } + } if (layout != null) { if ((layout.Type.Equals(BroadcastLayout.LayoutType.Custom) && String.IsNullOrEmpty(layout.Stylesheet)) || - (!layout.Type.Equals(BroadcastLayout.LayoutType.Custom) && !String.IsNullOrEmpty(layout.Stylesheet))) { + (!layout.Type.Equals(BroadcastLayout.LayoutType.Custom) && !String.IsNullOrEmpty(layout.Stylesheet))) + { throw new OpenTokArgumentException("Could not set the layout. Either an invalid JSON or an invalid layout options."); - } else { + } + else if (layout.ScreenShareType != null && layout.Type != BroadcastLayout.LayoutType.BestFit) + { + throw new OpenTokArgumentException($"Could not set screenShareLayout. When screenShareType is set, layout.Type must be bestFit, was {layout.Type}"); + } + else + { if (layout.Type.Equals(BroadcastLayout.LayoutType.Custom)) { data.Add("layout", layout); - } else { - data.Add("layout", new { type = OpenTokUtils.convertToCamelCase(layout.Type.ToString()) }); + } + else + { + data.Add("layout", layout); } } } @@ -530,17 +575,18 @@ public Broadcast StartBroadcast(string sessionId, Boolean hls = true, List return OpenTokUtils.GenerateBroadcast(response, ApiKey, ApiSecret, OpenTokServer); } - /** - * Use this method to stop a live broadcast of an OpenTok session. - * Note that broadcasts automatically stop 120 minutes after they are started. - *

- * For more information on broadcasting, see the - * Broadcast developer guide. - * - * @param broadcastId The broadcast ID of the broadcasting session - * - * @return The Broadcast object. This object includes properties defining the broadcast, including the broadcast ID. - */ + ///

+ /// Use this method to stop a live broadcast of an OpenTok session. + /// Note that broadcasts automatically stop 120 minutes after they are started. + /// + /// For more information on broadcasting, see the Broadcast developer guide. + /// + /// + /// The broadcast ID of the broadcasting session + /// + /// The object. This object includes properties defining the broadcast, + /// including the broadcast ID. + /// public Broadcast StopBroadcast(string broadcastId) { string url = string.Format("v2/project/{0}/broadcast/{1}/stop", this.ApiKey, broadcastId); @@ -550,16 +596,17 @@ public Broadcast StopBroadcast(string broadcastId) return JsonConvert.DeserializeObject(response); } - /** - * Use this method to get a live streaming broadcast object of an OpenTok session. - *

- * For more information on broadcasting, see the - * Broadcast developer guide. - * - * @param broadcastId The broadcast ID of the broadcasting session - * - * @return The Broadcast object. This object includes properties defining the broadcast, including the broadcast ID. - */ + ///

+ /// Use this method to get a live streaming broadcast object of an OpenTok session. + /// + /// + /// For more information on broadcasting, see the Broadcast developer guide. + /// + /// The broadcast ID of the broadcasting session + /// + /// The object. This object includes properties defining the broadcast, + /// including the broadcast ID. + /// public Broadcast GetBroadcast(string broadcastId) { string url = string.Format("v2/project/{0}/broadcast/{1}", this.ApiKey, broadcastId); @@ -567,15 +614,12 @@ public Broadcast GetBroadcast(string broadcastId) return OpenTokUtils.GenerateBroadcast(response, ApiKey, ApiSecret, OpenTokServer); } - /** - * Sets the layout type for the broadcast. For a description of layout types, see - * Configuring - * the video layout for OpenTok live streaming broadcasts. - * @param broadcastId The broadcast ID of the broadcasting session - * - * @param layout The BroadcastLayout that defines layout options for the broadcast. - * - */ + /// + /// Sets the layout type for the broadcast. For a description of layout types, see + /// Configuring the video layout for OpenTok live streaming broadcasts. + /// + /// The broadcast ID of the broadcasting session. + /// The BroadcastLayout that defines layout options for the broadcast. public void SetBroadcastLayout(string broadcastId, BroadcastLayout layout) { string url = string.Format("v2/project/{0}/broadcast/{1}/layout", this.ApiKey, broadcastId); @@ -588,12 +632,20 @@ public void SetBroadcastLayout(string broadcastId, BroadcastLayout layout) { throw new OpenTokArgumentException("Could not set the layout. Either an invalid JSON or an invalid layout options."); } + else if (layout.ScreenShareType != null && layout.Type != BroadcastLayout.LayoutType.BestFit) + { + throw new OpenTokArgumentException($"Could not set screenShareLayout. When screenShareType is set, layout.Type must be bestFit, was {layout.Type}"); + } else { data.Add("type", OpenTokUtils.convertToCamelCase(layout.Type.ToString())); if (layout.Type.Equals(BroadcastLayout.LayoutType.Custom)) { data.Add("stylesheet", layout.Stylesheet); + } + if (layout.ScreenShareType != null) + { + data.Add("screenShareType", OpenTokUtils.convertToCamelCase(layout.ScreenShareType.ToString())); } } } @@ -601,24 +653,61 @@ public void SetBroadcastLayout(string broadcastId, BroadcastLayout layout) Client.Put(url, headers, data); } - /** - * Sets the layout class list for streams in a session. Layout classes are used in - * the layout for composed archives and live streaming broadcasts. For more information, see - * Customizing - * the video layout for composed archives and - * Configuring - * video layout for OpenTok live streaming broadcasts. - * - *

- * You can set the initial layout class list for streams published by a client when you generate - * used by the client. See the {@link #generateToken(String, TokenOptions)} method. - * - * @param sessionId The sessionId - * - * @param streams A list of StreamsProperties that defines class lists for one or more - * streams in the session. - * - */ + + ///

+ /// Allows you to Dynamically change the layout of a composed archive while it's being recorded + /// see Customizing the video layout for composed archives + /// for details regarding customizing a layout. + /// + /// + /// + /// + public bool SetArchiveLayout(string archiveId, ArchiveLayout layout) + { + string url = $"v2/project/{ApiKey}/archive/{archiveId}/layout"; + var headers = new Dictionary { { "Content-type", "application/json" } }; + var data = new Dictionary(); + if(layout != null) + { + if (layout.Type == LayoutType.custom && string.IsNullOrEmpty(layout.StyleSheet)) + { + throw new OpenTokArgumentException("Invalid layout, layout is custom but no stylesheet provided"); + } + else if(layout.Type != LayoutType.custom && !string.IsNullOrEmpty(layout.StyleSheet)) + { + throw new OpenTokArgumentException("Invalid layout, layout is not custom, but stylesheet is set"); + } + + data.Add("type", layout.Type.ToString()); + if (!string.IsNullOrEmpty(layout.StyleSheet)) + { + data.Add("stylesheet", layout.StyleSheet); + } + if (layout.ScreenShareType != null) + { + if(layout.Type != LayoutType.bestFit) + { + throw new OpenTokArgumentException("Invalid layout, when ScreenShareType is set, Type must be bestFit"); + } + data.Add("screenshareType", OpenTokUtils.convertToCamelCase(layout.ScreenShareType.ToString())); + } + } + Client.Put(url, headers, data); + return true; + } + + /// + /// Sets the layout class list for streams in a session. Layout classes are used in + /// the layout for composed archives and live streaming broadcasts. For more information, see + /// Customizing the video layout for composed archives and + /// Configuring video layout for OpenTok live streaming broadcasts. + /// + /// You can set the initial layout class list for streams published by a client when you generate + /// used by the client. See the method. + /// + /// + /// The sessionId + /// A list of StreamsProperties that defines class lists for one or more streams in the session. public void SetStreamClassLists(string sessionId, List streams) { string url = string.Format("v2/project/{0}/session/{1}/stream", this.ApiKey, sessionId); @@ -627,8 +716,9 @@ public void SetStreamClassLists(string sessionId, List streams Dictionary data = new Dictionary(); if (streams == null || streams.Count() == 0) { - throw new OpenTokArgumentException("The stream list must include at least one item."); - } else + throw new OpenTokArgumentException("The stream list must include at least one item."); + } + else { foreach (StreamProperties stream in streams) { @@ -645,17 +735,14 @@ public void SetStreamClassLists(string sessionId, List streams Client.Put(url, headers, data); } - /** - * Sends a signal to clients (or a specific client) connected to an OpenTok session. - * - * @param sessionId The OpenTok sessionId where the signal will be sent. - * - * @param signalProperties This signalProperties defines the payload for the signal. - * - * @param connectionId An optional parameter used to send the signal to a specific connection in a session. - * - */ - public void Signal(string sessionId, SignalProperties signalProperties, string connectionId=null) + + /// + /// Sends a signal to clients (or a specific client) connected to an OpenTok session. + /// + /// The OpenTok sessionId where the signal will be sent. + /// This signalProperties defines the payload for the signal. + /// An optional parameter used to send the signal to a specific connection in a session. + public void Signal(string sessionId, SignalProperties signalProperties, string connectionId = null) { if (String.IsNullOrEmpty(sessionId)) { diff --git a/OpenTok/OpenTok.csproj b/OpenTok/OpenTok.csproj index 47ccae6d..6c1679e3 100644 --- a/OpenTok/OpenTok.csproj +++ b/OpenTok/OpenTok.csproj @@ -1,18 +1,24 @@ - net452;net46;net461;netstandard2.0 - 3.3.0 + 3.6.0 OpenTok is an API from TokBox that enables websites to weave live group video communication into their online experience. https://github.com/opentok/Opentok-.NET-SDK https://github.com/opentok/Opentok-.NET-SDK - https://github.com/opentok/Opentok-.NET-SDK/releases/tag/v3.3.0 + git + true + https://github.com/opentok/Opentok-.NET-SDK/releases/tag/v3.6.0 true true {C770C266-B8E6-413A-B5AB-68EB218DC76C} 671ed443 MIT Copyright © Vonage 2020 + true + snupkg + true + true + true diff --git a/OpenTok/OpenTok.nuspec b/OpenTok/OpenTok.nuspec deleted file mode 100644 index ebf973df..00000000 --- a/OpenTok/OpenTok.nuspec +++ /dev/null @@ -1,17 +0,0 @@ - - - - $id$ - $version$ - $title$ - $author$ - $author$ - http://opensource.org/licenses/mit-license - https://github.com/opentok/Opentok-.NET-SDK - false - $description$ - - Copyright TokBox, Inc. 2014 - video streaming webrtc api rest archiving realtime rtc communications - - \ No newline at end of file diff --git a/OpenTok/Role.cs b/OpenTok/Role.cs index cee4bff0..4651d67c 100644 --- a/OpenTok/Role.cs +++ b/OpenTok/Role.cs @@ -5,32 +5,33 @@ namespace OpenTokSDK { - /** - * Defines values for the role parameter of the GenerateToken method of the OpenTok class. - */ + /// + /// Defines values for the role parameter of the GenerateToken method of the OpenTok class. + /// public enum Role { - /** - * A publisher can publish streams, subscribe to streams, and signal. (This is the default - * value if you do not set a role when calling GenerateToken method of the OpenTok class. - */ + /// + /// A publisher can publish streams, subscribe to streams, and signal. (This is the default + /// value if you do not set a role when calling GenerateToken method of the OpenTok class. + /// PUBLISHER, - /** - * A subscriber can only subscribe to streams. - */ + /// + /// A subscriber can only subscribe to streams. + /// SUBSCRIBER, - /** - * In addition to the privileges granted to a publisher, in clients using the OpenTok.js - * library, a moderator can call the forceUnpublish() and - * forceDisconnect() methods of the Session object. - */ + /// + /// In addition to the privileges granted to a publisher, a moderator can perform moderation + /// functions, such as forcing clients to disconnect, to stop publishing streams, or to + /// mute audio in published streams. See the + /// Moderation developer guide. + /// MODERATOR } - /** - * For internal use. - */ - static class RoleExtensions + /// + /// For internal use. + /// + internal static class RoleExtensions { public static string ToString(this Role role) { diff --git a/OpenTok/Rtmp.cs b/OpenTok/Rtmp.cs index 5d863ff0..fd910183 100644 --- a/OpenTok/Rtmp.cs +++ b/OpenTok/Rtmp.cs @@ -2,35 +2,43 @@ using Newtonsoft.Json; namespace OpenTokSDK -{ - /** - * Represents a string in an OpenTok session. - */ +{ + /// + /// Represents an RTMP stream in an OpenTok session. + /// public class Rtmp { - /** - * The stream id. - */ + /// + /// The stream ID. + /// [JsonProperty("id")] public string Id { get; set; } - /** - * The server URL. - */ + /// + /// The server URL. + /// [JsonProperty("serverUrl")] public string ServerUrl { get; set; } - /** - * The stream name. - */ + /// + /// The stream name. + /// [JsonProperty("streamName")] public string StreamName { get; set; } + /// + /// Initializes a new instance of the class. + /// public Rtmp() { - } + /// + /// Initializes a new instance of the class. + /// + /// The stream ID. + /// The server URL. + /// The stream name. public Rtmp(string id, string serverUrl, string streamName) { Id = id; diff --git a/OpenTok/ScreenShareLayoutType.cs b/OpenTok/ScreenShareLayoutType.cs new file mode 100644 index 00000000..1ac5bf19 --- /dev/null +++ b/OpenTok/ScreenShareLayoutType.cs @@ -0,0 +1,27 @@ +namespace OpenTokSDK +{ + + /// + /// For archive and broadcast layouts, the layout type to use with screen shares. + /// If this enum is used, the Type property should be set to BestFit. + /// + public enum ScreenShareLayoutType + { + /// + /// Picture-in-Picture + /// + Pip, + /// + /// Best Fit + /// + BestFit, + /// + /// Vertical Presentation + /// + VerticalPresentation, + /// + /// Horizontal Presentation + /// + HorizontalPresentation + } +} diff --git a/OpenTok/Session.cs b/OpenTok/Session.cs index f8ce3475..630519cd 100644 --- a/OpenTok/Session.cs +++ b/OpenTok/Session.cs @@ -10,79 +10,80 @@ namespace OpenTokSDK { - /** - * Defines values for the mediaMode parameter of the CreateSession() method of the - * OpenTok class. - */ + /// + /// Defines values for the mediaMode parameter of the method of the + /// class. + /// public enum MediaMode { - /** - * The session will transmit streams using the OpenTok Media Router. - */ + /// + /// The session will transmit streams using the OpenTok Media Router. + /// ROUTED, - /** - * The session will attempt to transmit streams directly between clients. If two clients - * cannot send and receive each others' streams, due to firewalls on the clients' networks, - * their streams will be relayed using the OpenTok TURN Server. - */ + /// + /// The session will attempt to transmit streams directly between clients. If two clients + /// cannot send and receive each others' streams, due to firewalls on the clients' networks, + /// their streams will be relayed using the OpenTok TURN Server. + /// RELAYED } - /** - * Defines values for the archiveMode property of the Session object. You also use these values - * for the archiveMode parameter of the OpenTok.CreateSession() method. - */ + /// + /// Defines values for the archiveMode property of the object. + /// You also use these values for the archiveMode parameter of the method. + /// public enum ArchiveMode { - /** - * The session is not archived automatically. To archive the session, you can call the - * OpenTok.StartArchive() method. - */ + /// + /// The session is not archived automatically. To archive the session, you can call the + /// method. + /// MANUAL, - /** - * The session is archived automatically (as soon as there are clients connected - * to the session). - */ + /// + /// The session is archived automatically (as soon as there are clients publishing streams + /// to the session). + /// ALWAYS } - /** - * Represents an OpenTok session. Use the CreateSession() method of the OpenTok class to create - * an OpenTok session. Use the Id property of the Session object to get the session ID. - */ + /// + /// Represents an OpenTok session. Use the method of the + /// class to create an OpenTok session. Use the Id property of the + /// object to get the session ID. + /// public class Session { - /** - * The session ID, which uniquely identifies the session. - */ + /// + /// The session ID, which uniquely identifies the session. + /// public string Id { get; set; } - /** - * Your OpenTok API key. - */ + /// + /// Your OpenTok API key. + /// public int ApiKey { get; private set; } - /** - * Your OpenTok API secret. - */ + /// + /// Your OpenTok API secret. + /// public string ApiSecret { get; private set; } - /** - * The location hint IP address. - */ + /// + /// The location hint IP address. + /// public string Location { get; set; } - /** - * Defines whether the session will transmit streams using the OpenTok Media Router - * (MediaMode.ROUTED) or attempt to transmit streams directly between clients - * (MediaMode.RELAYED). - */ + /// + /// Defines whether the session will transmit streams using the OpenTok Media Router + /// () or attempt to transmit streams directly between clients + /// (). + /// public MediaMode MediaMode { get; private set; } - /** - * Defines whether the session is automatically archived (ArchiveMode.ALWAYS) - * or not (ArchiveMode.MANUAL). - */ + /// + /// Defines whether the session is automatically archived () + /// or not (). + /// public ArchiveMode ArchiveMode { get; private set; } private const int MAX_CONNECTION_DATA_LENGTH = 1000; @@ -104,37 +105,33 @@ internal Session(string sessionId, int apiKey, string apiSecret, string location this.ArchiveMode = archiveMode; } - - /** - * Creates a token for connecting to an OpenTok session. In order to authenticate a user - * connecting to an OpenTok session that user must pass an authentication token along with - * the API key. - * - * @param role The role for the token. Valid values are defined in the Role enum: - *
    - *
  • Role.SUBSCRIBER — A subscriber can only subscribe to - * streams.
  • - * - *
  • Role.PUBLISHER — A publisher can publish streams, subscribe to - * streams, and signal. (This is the default value if you do not specify a role.)
  • - * - *
  • Role.MODERATOR — In addition to the privileges granted to a - * publisher, in clients using the OpenTok.js library, a moderator can call the - * forceUnpublish() and forceDisconnect() method of the - * Session object.
  • - *
- * - * @param expireTime The expiration time of the token, in seconds since the UNIX epoch. - * Pass in 0 to use the default expiration time of 24 hours after the token creation time. - * The maximum expiration time is 30 days after the creation time. - * - * @param data A string containing connection metadata describing the end-user. For example, - * you can pass the user ID, name, or other data describing the end-user. The length of the - * string is limited to 1000 characters. This data cannot be updated once it is set. - * - * @return The token string. - */ - public string GenerateToken(Role role = Role.PUBLISHER, double expireTime = 0, string data = null, List initialLayoutClassList = null) + /// + /// Creates a token for connecting to an OpenTok session. In order to authenticate a user + /// connecting to an OpenTok session that user must pass an authentication token along with + /// the API key. + /// + /// + /// The role for the token. Valid values are defined in the Role enum: + /// - A subscriber can only subscribe to streams. + /// - A publisher can publish streams, subscribe to + /// streams, and signal. (This is the default value if you do not specify a role.) + /// - In addition to the privileges granted to a + /// publisher, in clients using the OpenTok.js library, a moderator can call the + /// forceUnpublish() and forceDisconnect() method of the Session object. + /// + /// + /// The expiration time of the token, in seconds since the UNIX epoch. + /// Pass in 0 to use the default expiration time of 24 hours after the token creation time. + /// The maximum expiration time is 30 days after the creation time. + /// + /// + /// A string containing connection metadata describing the end-user. For example, + /// you can pass the user ID, name, or other data describing the end-user. The length of the + /// string is limited to 1000 characters. This data cannot be updated once it is set. + /// + /// + /// The token string. + public string GenerateToken(Role role = Role.PUBLISHER, double expireTime = 0, string data = null, List initialLayoutClassList = null) { double createTime = OpenTokUtils.GetCurrentUnixTimeStamp(); int nonce = OpenTokUtils.GetRandomNumber(); diff --git a/OpenTok/SignalProperties.cs b/OpenTok/SignalProperties.cs index b49a1498..e66d9231 100644 --- a/OpenTok/SignalProperties.cs +++ b/OpenTok/SignalProperties.cs @@ -4,9 +4,9 @@ namespace OpenTokSDK { - /** - * Defines signal payload for the Signal API. - */ + /// + /// Defines signal payload for the Signal API. + /// public class SignalProperties { internal SignalProperties() diff --git a/OpenTok/Stream.cs b/OpenTok/Stream.cs index 57136041..5f76bf88 100644 --- a/OpenTok/Stream.cs +++ b/OpenTok/Stream.cs @@ -8,6 +8,9 @@ namespace OpenTokSDK { + /// + /// Represents a stream in an OpenTok session. + /// public class Stream { @@ -22,27 +25,28 @@ internal void CopyStream(Stream stream) this.LayoutClassList = stream.LayoutClassList; this.VideoType = stream.VideoType; } - /** - * The layout class list as a list of strings. - */ + + /// + /// The layout class list as a list of strings. + /// [JsonProperty("layoutClassList")] public List LayoutClassList { get; set; } - /** - * The video type as a string. - */ + /// + /// The video type as a string. + /// [JsonProperty("videoType")] public string VideoType { get; set; } - /** - * The stream ID. - */ + /// + /// The stream ID. + /// [JsonProperty("id")] public string Id { get; set; } - /** - * The name of the stream. - */ + /// + /// The name of the stream. + /// [JsonProperty("name")] public string Name { get; set; } diff --git a/OpenTok/StreamList.cs b/OpenTok/StreamList.cs index 186527cf..f228c190 100644 --- a/OpenTok/StreamList.cs +++ b/OpenTok/StreamList.cs @@ -5,14 +5,14 @@ namespace OpenTokSDK { - /** - * A class for accessing an array of Stream objects. - */ + /// + /// A class for accessing an array of Stream objects. + /// public class StreamList : List { - /** - * The total number of streams (associated with the sessionId). - */ + /// + /// The total number of streams (associated with the sessionId). + /// public int TotalCount { get; private set; } internal StreamList(List items, int totalCount) diff --git a/OpenTok/StreamProperties.cs b/OpenTok/StreamProperties.cs index f405d47d..152b0163 100644 --- a/OpenTok/StreamProperties.cs +++ b/OpenTok/StreamProperties.cs @@ -8,6 +8,10 @@ namespace OpenTokSDK { + /// + /// Defines class lists for one or more streams in the session. Used by the + /// OpenTok.SetStreamClassLists() method. + /// public class StreamProperties { @@ -15,6 +19,11 @@ internal StreamProperties() { } + /// + /// Initializes a new instance of the class. + /// + /// The stream ID. + /// The layout class list as a list of strings. public StreamProperties(string Id = null, List LayoutClassList = null) { this.Id = Id; @@ -33,15 +42,15 @@ public void addLayoutClass(string layoutClass) LayoutClassList.Add(layoutClass); } - /** - * The stream ID. - */ + /// + /// The stream ID. + /// [JsonProperty("id")] public string Id { get; set; } - /** - * The layout class list as a list of strings. - */ + /// + /// The layout class list as a list of strings. + /// [JsonProperty("layoutClassList")] public List LayoutClassList { get; set; } diff --git a/OpenTok/Util/HttpClient.cs b/OpenTok/Util/HttpClient.cs index 05b434ca..f71260a5 100644 --- a/OpenTok/Util/HttpClient.cs +++ b/OpenTok/Util/HttpClient.cs @@ -19,9 +19,9 @@ namespace OpenTokSDK.Util { - /** - * For internal use. - */ + /// + /// For internal use. + /// public class HttpClient { private string userAgent; @@ -141,6 +141,12 @@ public string DoRequest(string url, Dictionary specificHeaders, } } } + else if (e.Status == WebExceptionStatus.SendFailure) + { + throw new OpenTokWebException("Error with request submission (TLS1.1 or other network/protocol issue)", e); + } + + OpenTokUtils.ValidateTlsVersion(e); throw new OpenTokWebException("Error with request submission", e); } diff --git a/OpenTok/Util/OpenTokUtils.cs b/OpenTok/Util/OpenTokUtils.cs index 4e29c7a1..ad9b4400 100644 --- a/OpenTok/Util/OpenTokUtils.cs +++ b/OpenTok/Util/OpenTokUtils.cs @@ -7,6 +7,7 @@ using System.Net; using Newtonsoft.Json; +using OpenTokSDK.Exception; namespace OpenTokSDK.Util { @@ -141,8 +142,8 @@ public static int GetPartnerIdFromSessionId(string sessionId) throw new FormatException("SessionId can not be empty"); } - string formatedSessionId = sessionId.Replace('-', '+'); - string[] splittedSessionId = OpenTokUtils.SplitString(formatedSessionId, '_', 2); + string formattedSessionId = sessionId.Replace('-', '+'); + string[] splittedSessionId = OpenTokUtils.SplitString(formattedSessionId, '_', 2); if (splittedSessionId == null) { throw new FormatException("Session id could not be decoded"); @@ -156,7 +157,28 @@ public static int GetPartnerIdFromSessionId(string sessionId) throw new FormatException("Session id could not be decoded"); } + if (sessionParameters.Count() < 2) + { + throw new FormatException("Session id could not be decoded"); + } + return Convert.ToInt32(sessionParameters[1]); } + + /// + /// Used if a WebException is caught to check that the TLS version makes sense + /// If the TLS version is less than 1.2 and not the System Default (0) this method + /// throws an exception + /// + /// + public static void ValidateTlsVersion(WebException e) + { + if (ServicePointManager.SecurityProtocol > 0 && ServicePointManager.SecurityProtocol < SecurityProtocolType.Tls12) + { + throw new OpenTokWebException("Error with request submission.\n" + + "This application appears to not support TLS1.2.\n" + + "Please enable TLS 1.2 and try again.", e); + } + } } } diff --git a/OpenTokTest/OpenTokTest.cs b/OpenTokTest/OpenTokTest.cs index eb4171d0..d7aa138c 100644 --- a/OpenTokTest/OpenTokTest.cs +++ b/OpenTokTest/OpenTokTest.cs @@ -10,6 +10,7 @@ using OpenTokSDK; using OpenTokSDK.Util; using OpenTokSDK.Exception; +using System.Net; namespace OpenTokSDKTest { @@ -22,7 +23,29 @@ public class OpenTokTest public void InitializationTest() { var opentok = new OpenTok(apiKey, apiSecret); - Assert.IsType(typeof(OpenTok), opentok); + Assert.IsType(opentok); + } + + [Theory] + [InlineData(SecurityProtocolType.Tls11)] + [InlineData(SecurityProtocolType.Tls12)] + [InlineData((SecurityProtocolType)0)] + public void CreateSessionFailedDueToTLS(SecurityProtocolType protocolType) + { + ServicePointManager.SecurityProtocol = protocolType; + var e = new WebException("Test Exception"); + try + { + OpenTokUtils.ValidateTlsVersion(e); + Assert.NotEqual(SecurityProtocolType.Tls11, protocolType); + } + catch(OpenTokWebException ex) + { + Assert.Equal("Error with request submission.\nThis application appears to not support TLS1.2.\nPlease enable TLS 1.2 and try again.", ex.Message); + Assert.Equal(SecurityProtocolType.Tls11, protocolType); + + } + } // TODO: all create session and archive tests should verify the HTTP request body @@ -48,8 +71,8 @@ public void CreateSimpleSessionTest() Assert.NotNull(session); Assert.Equal(this.apiKey, session.ApiKey); Assert.Equal(sessionId, session.Id); - Assert.Equal(session.MediaMode, MediaMode.RELAYED); - Assert.Equal(session.Location, ""); + Assert.Equal(MediaMode.RELAYED, session.MediaMode); + Assert.Equal("", session.Location); mockClient.Verify(httpClient => httpClient.Post(It.Is(url => url.Equals(expectedUrl)), It.IsAny>(), It.IsAny>()), Times.Once()); } @@ -75,8 +98,8 @@ public void CreateRoutedSessionTest() Assert.NotNull(session); Assert.Equal(this.apiKey, session.ApiKey); Assert.Equal(sessionId, session.Id); - Assert.Equal(session.MediaMode, MediaMode.ROUTED); - Assert.Equal(session.Location, ""); + Assert.Equal(MediaMode.ROUTED, session.MediaMode); + Assert.Equal("", session.Location); mockClient.Verify(httpClient => httpClient.Post(It.Is(url => url.Equals(expectedUrl)), It.IsAny>(), It.IsAny>()), Times.Once()); } @@ -102,8 +125,8 @@ public void CreateSessionWithLocationTest() Assert.NotNull(session); Assert.Equal(this.apiKey, session.ApiKey); Assert.Equal(sessionId, session.Id); - Assert.Equal(session.MediaMode, MediaMode.RELAYED); - Assert.Equal(session.Location, "0.0.0.0"); + Assert.Equal(MediaMode.RELAYED, session.MediaMode); + Assert.Equal("0.0.0.0", session.Location); mockClient.Verify(httpClient => httpClient.Post(It.Is(url => url.Equals(expectedUrl)), It.IsAny>(), It.IsAny>()), Times.Once()); @@ -130,8 +153,8 @@ public void CreateRoutedSessionWithLocationTest() Assert.NotNull(session); Assert.Equal(this.apiKey, session.ApiKey); Assert.Equal(sessionId, session.Id); - Assert.Equal(session.MediaMode, MediaMode.ROUTED); - Assert.Equal(session.Location, "0.0.0.0"); + Assert.Equal(MediaMode.ROUTED, session.MediaMode); + Assert.Equal("0.0.0.0", session.Location); mockClient.Verify(httpClient => httpClient.Post(It.Is(url => url.Equals(expectedUrl)), It.IsAny>(), It.IsAny>()), Times.Once()); } @@ -157,8 +180,8 @@ public void CreateAlwaysArchivedSessionTest() Assert.NotNull(session); Assert.Equal(this.apiKey, session.ApiKey); Assert.Equal(sessionId, session.Id); - Assert.Equal(session.MediaMode, MediaMode.ROUTED); - Assert.Equal(session.ArchiveMode, ArchiveMode.ALWAYS); + Assert.Equal(MediaMode.ROUTED, session.MediaMode); + Assert.Equal(ArchiveMode.ALWAYS, session.ArchiveMode); mockClient.Verify(httpClient => httpClient.Post(It.Is(url => url.Equals(expectedUrl)), It.IsAny>(), It.IsAny>()), Times.Once()); } @@ -296,7 +319,7 @@ public void GenerateTokenWithInitialLayoutClass() Assert.NotNull(data["create_time"]); Assert.NotNull(data["nonce"]); Assert.Equal(data["role"], Role.PUBLISHER.ToString()); - Assert.Equal(data["initial_layout_class_list"], "focus"); + Assert.Equal("focus", data["initial_layout_class_list"]); } [Fact] @@ -335,7 +358,7 @@ public void GenerateInvalidTokensTest() exceptions.Add(e); } - Assert.Equal(exceptions.Count, 3); + Assert.Equal(3, exceptions.Count); foreach(Exception exception in exceptions) { Assert.True(exception is OpenTokArgumentException); @@ -410,6 +433,7 @@ public void GetExpiredArchiveTest() mockClient.Verify(httpClient => httpClient.Get(It.Is(url => url.Equals("v2/project/" + this.apiKey + "/archive/" + archiveId))), Times.Once()); } + [Fact] public void GetArchiveWithUnknownPropertiesTest() { string archiveId = "936da01f-9abd-4d9d-80c7-02af85c822a8"; @@ -536,6 +560,137 @@ public void ListArchivesTest() mockClient.Verify(httpClient => httpClient.Get(It.Is(url => url.Equals("v2/project/"+apiKey + "/archive?offset=0"))), Times.Once()); } + [Fact] + public void ListArchivesTestWithValidSessionId() + { + var sessionId = "1_MX4xMjM0NTZ-flNhdCBNYXIgMTUgMTQ6NDI6MjMgUERUIDIwMTR-MC40OTAxMzAyNX4"; + string returnString = "{\n" + + " \"count\" : 6,\n" + + " \"items\" : [ {\n" + + " \"createdAt\" : 1395187930000,\n" + + " \"duration\" : 22,\n" + + " \"id\" : \"ef546c5a-4fd7-4e59-ab3d-f1cfb4148d1d\",\n" + + " \"name\" : \"\",\n" + + " \"partnerId\" : 123456,\n" + + " \"reason\" : \"\",\n" + + " \"sessionId\" : \"SESSIONID\",\n" + + " \"size\" : 2909274,\n" + + " \"status\" : \"available\",\n" + + " \"url\" : \"http://tokbox.com.archive2.s3.amazonaws.com/123456%2Fef546c5" + + "a-4fd7-4e59-ab3d-f1cfb4148d1d%2Farchive.mp4?Expires=1395188695&AWSAccessKeyId=AKIAI6" + + "LQCPIXYVWCQV6Q&Signature=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n" + + " }, {\n" + + " \"createdAt\" : 1395187910000,\n" + + " \"duration\" : 14,\n" + + " \"id\" : \"5350f06f-0166-402e-bc27-09ba54948512\",\n" + + " \"name\" : \"\",\n" + + " \"partnerId\" : 123456,\n" + + " \"reason\" : \"\",\n" + + " \"sessionId\" : \"SESSIONID\",\n" + + " \"size\" : 1952651,\n" + + " \"status\" : \"available\",\n" + + " \"url\" : \"http://tokbox.com.archive2.s3.amazonaws.com/123456%2F5350f06" + + "f-0166-402e-bc27-09ba54948512%2Farchive.mp4?Expires=1395188695&AWSAccessKeyId=AKIAI6" + + "LQCPIXYVWCQV6Q&Signature=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n" + + " }, {\n" + + " \"createdAt\" : 1395187836000,\n" + + " \"duration\" : 62,\n" + + " \"id\" : \"f6e7ee58-d6cf-4a59-896b-6d56b158ec71\",\n" + + " \"name\" : \"\",\n" + + " \"partnerId\" : 123456,\n" + + " \"reason\" : \"\",\n" + + " \"sessionId\" : \"SESSIONID\",\n" + + " \"size\" : 8347554,\n" + + " \"status\" : \"available\",\n" + + " \"url\" : \"http://tokbox.com.archive2.s3.amazonaws.com/123456%2Ff6e7ee5" + + "8-d6cf-4a59-896b-6d56b158ec71%2Farchive.mp4?Expires=1395188695&AWSAccessKeyId=AKIAI6" + + "LQCPIXYVWCQV6Q&Signature=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n" + + " }, {\n" + + " \"createdAt\" : 1395183243000,\n" + + " \"duration\" : 544,\n" + + " \"id\" : \"30b3ebf1-ba36-4f5b-8def-6f70d9986fe9\",\n" + + " \"name\" : \"\",\n" + + " \"partnerId\" : 123456,\n" + + " \"reason\" : \"\",\n" + + " \"sessionId\" : \"SESSIONID\",\n" + + " \"size\" : 78499758,\n" + + " \"status\" : \"available\",\n" + + " \"url\" : \"http://tokbox.com.archive2.s3.amazonaws.com/123456%2F30b3ebf" + + "1-ba36-4f5b-8def-6f70d9986fe9%2Farchive.mp4?Expires=1395188695&AWSAccessKeyId=AKIAI6" + + "LQCPIXYVWCQV6Q&Signature=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n" + + " }, {\n" + + " \"createdAt\" : 1394396753000,\n" + + " \"duration\" : 24,\n" + + " \"id\" : \"b8f64de1-e218-4091-9544-4cbf369fc238\",\n" + + " \"name\" : \"showtime again\",\n" + + " \"partnerId\" : 123456,\n" + + " \"reason\" : \"\",\n" + + " \"sessionId\" : \"SESSIONID\",\n" + + " \"size\" : 2227849,\n" + + " \"status\" : \"available\",\n" + + " \"url\" : \"http://tokbox.com.archive2.s3.amazonaws.com/123456%2Fb8f64de" + + "1-e218-4091-9544-4cbf369fc238%2Farchive.mp4?Expires=1395188695&AWSAccessKeyId=AKIAI6" + + "LQCPIXYVWCQV6Q&Signature=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n" + + " }, {\n" + + " \"createdAt\" : 1394321113000,\n" + + " \"duration\" : 1294,\n" + + " \"id\" : \"832641bf-5dbf-41a1-ad94-fea213e59a92\",\n" + + " \"name\" : \"showtime\",\n" + + " \"partnerId\" : 123456,\n" + + " \"reason\" : \"\",\n" + + " \"sessionId\" : \"SESSIONID\",\n" + + " \"size\" : 42165242,\n" + + " \"status\" : \"available\",\n" + + " \"url\" : \"http://tokbox.com.archive2.s3.amazonaws.com/123456%2F832641b" + + "f-5dbf-41a1-ad94-fea213e59a92%2Farchive.mp4?Expires=1395188695&AWSAccessKeyId=AKIAI6" + + "LQCPIXYVWCQV6Q&Signature=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n" + + " } ]\n" + + " }"; + var mockClient = new Mock(); + mockClient.Setup(httpClient => httpClient.Get(It.IsAny())).Returns(returnString); + + OpenTok opentok = new OpenTok(apiKey, apiSecret); + opentok.Client = mockClient.Object; + ArchiveList archives = opentok.ListArchives(sessionId:sessionId); + + Assert.NotNull(archives); + Assert.Equal(6, archives.Count); + + mockClient.Verify(httpClient => httpClient.Get(It.Is(url => url.Equals("v2/project/" + apiKey + $"/archive?offset=0&sessionId={sessionId}"))), Times.Once()); + } + + [Fact] + public void TestListArchivesBadCount() + { + OpenTok opentok = new OpenTok(apiKey, apiSecret); + try + { + opentok.ListArchives(count: -5); + Assert.True(false, "TestListArchivesBadCount should not have reached here as the count passed in was negative"); + } + catch (OpenTokArgumentException ex) + { + Assert.Equal("count cannot be smaller than 0", ex.Message); + } + + } + + [Fact] + public void TestListArchivesBadSessionId() + { + OpenTok opentok = new OpenTok(apiKey, apiSecret); + try + { + opentok.ListArchives(sessionId: "This-is-not-a-valid-session-id"); + Assert.True(false, "TestListArchivesBadCount should not have reached here as the count passed in was negative"); + } + catch (OpenTokArgumentException ex) + { + Assert.Equal("Session Id is not valid", ex.Message); + } + + } + [Fact] public void StartArchiveTest() { @@ -561,7 +716,7 @@ public void StartArchiveTest() Assert.NotNull(archive); Assert.Equal(sessionId, archive.SessionId); - Assert.NotNull(archive.Id); + Assert.NotEqual(Guid.Empty, archive.Id); mockClient.Verify(httpClient => httpClient.Post(It.Is(url => url.Equals("v2/project/"+ apiKey +"/archive")), It.IsAny>(), It.IsAny>()), Times.Once()); } @@ -628,6 +783,257 @@ public void StartArchiveWithSDResolutionTest() mockClient.Verify(httpClient => httpClient.Post(It.Is(url => url.Equals("v2/project/" + apiKey + "/archive")), It.IsAny>(), It.IsAny>()), Times.Once()); } + [Fact] + public void TestArchiveScreenShareLayout() + { + var expected = @"{""sessionId"":""abcd12345"",""name"":""an_archive_name"",""hasVideo"":true,""hasAudio"":true,""outputMode"":""composed"",""layout"":{""type"":""bestFit"",""screensharetype"":""bestFit""}}"; + var httpClient = new HttpClient(); + var data = new Dictionary() { { "sessionId", "abcd12345" }, { "name", "an_archive_name" }, { "hasVideo", true }, { "hasAudio", true }, { "outputMode", "composed" } }; + var layout = new ArchiveLayout { Type = LayoutType.bestFit, ScreenShareType=ScreenShareLayoutType.BestFit }; + data.Add("layout", layout); + var headers = new Dictionary(); + headers.Add("Content-type", "application/json"); + var clientType = typeof(HttpClient); + var layoutString = (string)clientType.GetMethod("GetRequestPostData", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).Invoke(httpClient, new object[] { data, headers }); + Assert.Equal(expected, layoutString); + } + + [Fact] + public void TestArchiveScreenShareInvalidType() + { + OpenTok opentok = new OpenTok(apiKey, apiSecret); + ArchiveLayout layout = new ArchiveLayout { Type = LayoutType.pip, ScreenShareType = ScreenShareLayoutType.BestFit }; + try + { + opentok.StartArchive("abcd", layout: layout); + Assert.True(false, "Should have seen an exception"); + } + catch (OpenTokArgumentException ex) + { + + Assert.Equal($"Could not set screenShareLayout. When screenShareType is set, layout.Type must be bestFit, was {layout.Type}", ex.Message); + } + } + + [Fact] + public void TestSetArchiveScreenShareType() + { + var opentok = new OpenTok(apiKey, apiSecret); + var layout = new ArchiveLayout { Type = LayoutType.bestFit, ScreenShareType = ScreenShareLayoutType.Pip }; + var headers = new Dictionary { { "Content-type", "application/json" } }; + var archiveId = "123456789"; + var expectedUrl = $"v2/project/{apiKey}/archive/{archiveId}/layout"; + var mockClient = new Mock(); + opentok.Client = mockClient.Object; + mockClient.Setup(c => c.Put(expectedUrl, headers, It.Is>(x => (string)x["type"] == "bestFit" && (string)x["screenshareType"] == "pip"))); + Assert.True(opentok.SetArchiveLayout(archiveId, layout)); + } + + [Fact] + public void TestSetArchiveScreenShareTypeInvalid() + { + var opentok = new OpenTok(apiKey, apiSecret); + var layout = new ArchiveLayout { Type = LayoutType.pip, ScreenShareType = ScreenShareLayoutType.Pip }; + try + { + opentok.SetArchiveLayout("12345", layout); + Assert.True(false, "Failing because we should have had an exception"); + } + catch (OpenTokArgumentException ex) + { + Assert.Equal("Invalid layout, when ScreenShareType is set, Type must be bestFit", ex.Message); + } + } + + [Fact] + public void TestArchiveCustomLayout() + { + var expected = @"{""sessionId"":""abcd12345"",""name"":""an_archive_name"",""hasVideo"":true,""hasAudio"":true,""outputMode"":""composed"",""layout"":{""type"":""custom"",""stylesheet"":""stream.instructor {position: absolute; width: 100%; height:50%;}""}}"; + var httpClient = new HttpClient(); + var data = new Dictionary() { { "sessionId", "abcd12345" }, { "name", "an_archive_name" }, { "hasVideo", true }, { "hasAudio", true }, { "outputMode", "composed" } }; + var layout = new ArchiveLayout { Type = LayoutType.custom, StyleSheet = "stream.instructor {position: absolute; width: 100%; height:50%;}" }; + data.Add("layout", layout); + var headers = new Dictionary(); + headers.Add("Content-type", "application/json"); + var clientType = typeof(HttpClient); + var layoutString = (string)clientType.GetMethod("GetRequestPostData", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).Invoke(httpClient, new object[] { data, headers }); + Assert.Equal(expected, layoutString); + } + + [Fact] + public void TestArchiveNonCustomLayout() + { + var expectedString = @"{""sessionId"":""abcd12345"",""name"":""an_archive_name"",""hasVideo"":true,""hasAudio"":true,""outputMode"":""composed"",""layout"":{""type"":""pip""}}"; + var httpClient = new HttpClient(); + var data = new Dictionary() { { "sessionId", "abcd12345" }, { "name", "an_archive_name" }, { "hasVideo", true }, { "hasAudio", true }, { "outputMode", "composed" } }; + var layout = new ArchiveLayout { Type = LayoutType.pip }; + data.Add("layout", layout); + var headers = new Dictionary(); + headers.Add("Content-type", "application/json"); + var clientType = typeof(HttpClient); + var layoutString = (string)clientType.GetMethod("GetRequestPostData", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).Invoke(httpClient, new object[] { data, headers }); + Assert.Equal(expectedString, layoutString); + } + + [Fact] + public void TestArchiveNonCustomLayoutEmptyString() + { + var expectedString = @"{""sessionId"":""abcd12345"",""name"":""an_archive_name"",""hasVideo"":true,""hasAudio"":true,""outputMode"":""composed"",""layout"":{""type"":""pip""}}"; + var httpClient = new HttpClient(); + var data = new Dictionary() { { "sessionId", "abcd12345" }, { "name", "an_archive_name" }, { "hasVideo", true }, { "hasAudio", true }, { "outputMode", "composed" } }; + var layout = new ArchiveLayout { Type = LayoutType.pip, StyleSheet=string.Empty }; + data.Add("layout", layout); + var headers = new Dictionary(); + headers.Add("Content-type", "application/json"); + var clientType = typeof(HttpClient); + var layoutString = (string)clientType.GetMethod("GetRequestPostData", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).Invoke(httpClient, new object[] { data, headers }); + Assert.Equal(expectedString, layoutString); + } + + [Fact] + public void StartArchiveCustomLayout() + { + string sessionId = "SESSIONID"; + string resolution = "1280x720"; + string returnString = "{\n" + + " \"createdAt\" : 1395183243556,\n" + + " \"duration\" : 0,\n" + + " \"id\" : \"30b3ebf1-ba36-4f5b-8def-6f70d9986fe9\",\n" + + " \"name\" : \"\",\n" + + " \"outputMode\" : \"composed\",\n" + + " \"resolution\" : \"1280x720\",\n" + + " \"partnerId\" : 123456,\n" + + " \"reason\" : \"\",\n" + + " \"sessionId\" : \"" + sessionId + "\",\n" + + " \"size\" : 0,\n" + + " \"status\" : \"started\",\n" + + " \"url\" : null\n" + + " }"; + var mockClient = new Mock(); + mockClient.Setup(httpClient => httpClient.Post(It.IsAny(), It.IsAny>(), It.IsAny>())).Returns(returnString); + + OpenTok opentok = new OpenTok(apiKey, apiSecret); + opentok.Client = mockClient.Object; + var layout = new ArchiveLayout { Type = LayoutType.custom, StyleSheet = "stream.instructor {position: absolute; width: 100%; height:50%;}" }; + Archive archive = opentok.StartArchive(sessionId, outputMode: OutputMode.COMPOSED, resolution: resolution, layout: layout); + + Assert.NotNull(archive); + Assert.Equal(OutputMode.COMPOSED, archive.OutputMode); + Assert.Equal(resolution, archive.Resolution); + + mockClient.Verify(httpClient => httpClient.Post(It.Is(url => url.Equals("v2/project/" + apiKey + "/archive")), It.IsAny>(), It.IsAny>()), Times.Once()); + } + + [Fact] + public void StartArchiveVerticalLayout() + { + string sessionId = "SESSIONID"; + string resolution = "1280x720"; + string returnString = "{\n" + + " \"createdAt\" : 1395183243556,\n" + + " \"duration\" : 0,\n" + + " \"id\" : \"30b3ebf1-ba36-4f5b-8def-6f70d9986fe9\",\n" + + " \"name\" : \"\",\n" + + " \"outputMode\" : \"composed\",\n" + + " \"resolution\" : \"1280x720\",\n" + + " \"partnerId\" : 123456,\n" + + " \"reason\" : \"\",\n" + + " \"sessionId\" : \"" + sessionId + "\",\n" + + " \"size\" : 0,\n" + + " \"status\" : \"started\",\n" + + " \"url\" : null\n" + + " }"; + var mockClient = new Mock(); + mockClient.Setup(httpClient => httpClient.Post(It.IsAny(), It.IsAny>(), It.IsAny>())).Returns(returnString); + OpenTok opentok = new OpenTok(apiKey, apiSecret); + opentok.Client = mockClient.Object; + var layout = new ArchiveLayout + { + Type = LayoutType.verticalPresentation, + StyleSheet = "" + }; + Archive archive = opentok.StartArchive(sessionId, outputMode: OutputMode.COMPOSED, resolution: resolution, layout: layout); + Assert.NotNull(archive); + Assert.Equal(OutputMode.COMPOSED, archive.OutputMode); + Assert.Equal(resolution, archive.Resolution); + mockClient.Verify(httpClient => httpClient.Post(It.Is(url => url.Equals("v2/project/" + apiKey + "/archive")), It.IsAny>(), It.IsAny>()), Times.Once()); + } + + [Fact] + public void StartArchiveVerticalLayoutWithStyleSheet() + { + string sessionId = "SESSIONID"; + string resolution = "1280x720"; + string returnString = "{\n" + + " \"createdAt\" : 1395183243556,\n" + + " \"duration\" : 0,\n" + + " \"id\" : \"30b3ebf1-ba36-4f5b-8def-6f70d9986fe9\",\n" + + " \"name\" : \"\",\n" + + " \"outputMode\" : \"composed\",\n" + + " \"resolution\" : \"1280x720\",\n" + + " \"partnerId\" : 123456,\n" + + " \"reason\" : \"\",\n" + + " \"sessionId\" : \"" + sessionId + "\",\n" + + " \"size\" : 0,\n" + + " \"status\" : \"started\",\n" + + " \"url\" : null\n" + + " }"; + var mockClient = new Mock(); + mockClient.Setup(httpClient => httpClient.Post(It.IsAny(), It.IsAny>(), It.IsAny>())).Returns(returnString); + OpenTok opentok = new OpenTok(apiKey, apiSecret); + opentok.Client = mockClient.Object; + var layout = new ArchiveLayout + { + Type = LayoutType.verticalPresentation, + StyleSheet = "blah" + }; + try + { + Archive archive = opentok.StartArchive(sessionId, outputMode: OutputMode.COMPOSED, resolution: resolution, layout: layout); + Assert.True(false, "StartArchive should have thrown an exception"); + } + catch (OpenTokArgumentException ex) + { + Assert.Equal("Could not set layout, stylesheet must be set if and only if type is custom", ex.Message); + } + } + + [Fact] + public void StartArchiveCustomLayoutMissingStylesheet() + { + string sessionId = "SESSIONID"; + string resolution = "1280x720"; + string returnString = "{\n" + + " \"createdAt\" : 1395183243556,\n" + + " \"duration\" : 0,\n" + + " \"id\" : \"30b3ebf1-ba36-4f5b-8def-6f70d9986fe9\",\n" + + " \"name\" : \"\",\n" + + " \"outputMode\" : \"composed\",\n" + + " \"resolution\" : \"1280x720\",\n" + + " \"partnerId\" : 123456,\n" + + " \"reason\" : \"\",\n" + + " \"sessionId\" : \"" + sessionId + "\",\n" + + " \"size\" : 0,\n" + + " \"status\" : \"started\",\n" + + " \"url\" : null\n" + + " }"; + var mockClient = new Mock(); + mockClient.Setup(httpClient => httpClient.Post(It.IsAny(), It.IsAny>(), It.IsAny>())).Returns(returnString); + + OpenTok opentok = new OpenTok(apiKey, apiSecret); + opentok.Client = mockClient.Object; + var layout = new ArchiveLayout { Type = LayoutType.custom }; + try + { + Archive archive = opentok.StartArchive(sessionId, outputMode: OutputMode.COMPOSED, resolution: resolution, layout: layout); + Assert.True(false, "StartArchive should have thrown an exception"); + } + catch(OpenTokArgumentException ex) + { + Assert.Equal("Could not set layout, stylesheet must be set if and only if type is custom", ex.Message); + } + } + [Fact] public void StartArchiveWithHDResolutionTest() { @@ -724,7 +1130,7 @@ public void StartArchiveNoResolutionTest() Assert.NotNull(archive); Assert.Equal(sessionId, archive.SessionId); - Assert.NotNull(archive.Id); + Assert.NotEqual(Guid.Empty, archive.Id); Assert.Equal(resolution, archive.Resolution); mockClient.Verify(httpClient => httpClient.Post(It.Is(url => url.Equals("v2/project/" + apiKey + "/archive")), It.IsAny>(), It.IsAny>()), Times.Once()); @@ -757,7 +1163,7 @@ public void StartArchiveVoiceOnlyTest() Assert.NotNull(archive); Assert.Equal(sessionId, archive.SessionId); - Assert.NotNull(archive.Id); + Assert.NotEqual(Guid.Empty, archive.Id); mockClient.Verify(httpClient => httpClient.Post(It.Is(url => url.Equals("v2/project/" + apiKey + "/archive")), It.IsAny>(), It.IsAny>()), Times.Once()); } @@ -888,7 +1294,7 @@ public void GetStreamTestThrowArgumentException() Stream stream = opentok.GetStream(null, null); } - catch (OpenTokArgumentException e) + catch (OpenTokArgumentException) { Assert.True(true); } @@ -956,7 +1362,7 @@ public void ListStreamTestThrowArgumentException() StreamList streamlist = opentok.ListStreams(null); } - catch (OpenTokArgumentException e) + catch (OpenTokArgumentException) { Assert.True(true); } @@ -978,7 +1384,7 @@ public void ForceDisconnectOpenTokArgumentTest() try { opentok.ForceDisconnect(sessionId, connectionId); - } catch (OpenTokArgumentException e) + } catch (OpenTokArgumentException) { Assert.True(true); } @@ -1018,7 +1424,7 @@ public void SignalOpenTokArgumentExceptionTest() { opentok.Signal(sessionId, signalProperties); } - catch (OpenTokArgumentException e) + catch (OpenTokArgumentException) { Assert.True(true); } @@ -1107,11 +1513,111 @@ public void StartBroadcastTest() Assert.NotNull(broadcast); Assert.Equal(sessionId, broadcast.SessionId); Assert.NotNull(broadcast.Id); - Assert.Equal(broadcast.Status, Broadcast.BroadcastStatus.STARTED); + Assert.Equal(Broadcast.BroadcastStatus.STARTED, broadcast.Status); mockClient.Verify(httpClient => httpClient.Post(It.Is(url => url.Equals("v2/project/" + apiKey + "/broadcast")), It.IsAny>(), It.IsAny>()), Times.Once()); } + [Fact] + public void TestStartBroadcastWithScreenShareType() + { + string sessionId = "SESSIONID"; + string returnString = "{\n" + + " \"id\" : \"30b3ebf1-ba36-4f5b-8def-6f70d9986fe9\",\n" + + " \"sessionId\" : \"SESSIONID\",\n" + + " \"projectId\" : 123456,\n" + + " \"createdAt\" : 1395183243556,\n" + + " \"updatedAt\" : 1395183243556,\n" + + " \"resolution\" : \"640x480\",\n" + + " \"status\" : \"started\",\n" + + " \"broadcastUrls\": { \n" + + " \"hls\": \"http://server/fakepath/playlist.m3u8\", \n" + + " } \n" + + " }"; + var mockClient = new Mock(); + var expectedUrl = $"v2/project/{apiKey}/broadcast"; + var outputs = new Dictionary() { { "hls", new Object() } }; + var data = new Dictionary() { + { "sessionId", sessionId }, + { "maxDuration", 7200 }, + { "outputs", outputs } + }; + var layout = new BroadcastLayout(ScreenShareLayoutType.BestFit); + data.Add("layout",layout); + mockClient.Setup(httpClient => httpClient.Post( + expectedUrl, + It.IsAny>(), + It.Is< Dictionary>(x=> + (string)x["sessionId"] == sessionId && x["layout"]==layout && (int)x["maxDuration"] == 7200 && ((Dictionary)x["outputs"]).ContainsKey("hls") + ))).Returns(returnString); + + OpenTok opentok = new OpenTok(apiKey, apiSecret); + opentok.Client = mockClient.Object; + + Broadcast broadcast = opentok.StartBroadcast(sessionId, layout: layout); + + Assert.NotNull(broadcast); + Assert.Equal(sessionId, broadcast.SessionId); + Assert.NotNull(broadcast.Id); + Assert.Equal(Broadcast.BroadcastStatus.STARTED, broadcast.Status); + } + + [Fact] + public void TestSetBroadcastLayoutScreenShareType() + { + var broadcastId = "12345"; + var mockClient = new Mock(); + var expectedUrl = $"v2/project/{apiKey}/broadcast/{broadcastId}/layout"; + var layout = new BroadcastLayout(ScreenShareLayoutType.BestFit); + var expectedContent = new Dictionary() + { + {"layout",layout } + }; + var expectedHeaders = new Dictionary { { "Content-type", "application/json" } }; + OpenTok opentok = new OpenTok(apiKey, apiSecret); + opentok.Client = mockClient.Object; + + opentok.SetBroadcastLayout(broadcastId, layout); + + mockClient.Verify(c => c.Put(expectedUrl, expectedHeaders, It.Is>( + x=>(string)x["type"] == "bestFit" + && (string)x["screenShareType"] == "bestFit")) + ); + } + + [Fact] + public void TestSEtBroadcastLayoutScreenShareTypeInvalid() + { + var layout = new BroadcastLayout(ScreenShareLayoutType.BestFit) { Type = BroadcastLayout.LayoutType.Pip }; + var opentok = new OpenTok(apiKey, apiSecret); + try + { + opentok.SetBroadcastLayout("12345", layout); + Assert.True(false, "Failed due to missing exception"); + } + catch (OpenTokArgumentException ex) + { + Assert.Equal($"Could not set screenShareLayout. When screenShareType is set, layout.Type must be bestFit, was {layout.Type}", ex.Message); + } + } + + [Fact] + public void TestStartBroadcastScreenShareInvalidType() + { + OpenTok opentok = new OpenTok(apiKey, apiSecret); + BroadcastLayout layout = new BroadcastLayout(BroadcastLayout.LayoutType.Pip) { ScreenShareType = ScreenShareLayoutType.BestFit }; + try + { + opentok.StartBroadcast("abcd", layout: layout); + Assert.True(false, "Should have seen an exception"); + } + catch (OpenTokArgumentException ex) + { + + Assert.Equal($"Could not set screenShareLayout. When screenShareType is set, layout.Type must be bestFit, was {layout.Type}", ex.Message); + } + } + [Fact] public void StartBroadcastWithHDResolutionTest() { @@ -1140,7 +1646,7 @@ public void StartBroadcastWithHDResolutionTest() Assert.Equal(sessionId, broadcast.SessionId); Assert.Equal(resolution, broadcast.Resolution); Assert.NotNull(broadcast.Id); - Assert.Equal(broadcast.Status, Broadcast.BroadcastStatus.STARTED); + Assert.Equal(Broadcast.BroadcastStatus.STARTED, broadcast.Status); mockClient.Verify(httpClient => httpClient.Post(It.Is(url => url.Equals("v2/project/" + apiKey + "/broadcast")), It.IsAny>(), It.IsAny>()), Times.Once()); } @@ -1173,7 +1679,7 @@ public void StartBroadcastWithSDResolutionTest() Assert.Equal(sessionId, broadcast.SessionId); Assert.Equal(resolution, broadcast.Resolution); Assert.NotNull(broadcast.Id); - Assert.Equal(broadcast.Status, Broadcast.BroadcastStatus.STARTED); + Assert.Equal(Broadcast.BroadcastStatus.STARTED, broadcast.Status); mockClient.Verify(httpClient => httpClient.Post(It.Is(url => url.Equals("v2/project/" + apiKey + "/broadcast")), It.IsAny>(), It.IsAny>()), Times.Once()); } @@ -1221,10 +1727,10 @@ public void StartBroadcastOnlyWithRTMPTest() Assert.NotNull(broadcast); Assert.Equal(sessionId, broadcast.SessionId); Assert.NotNull(broadcast.RtmpList); - Assert.Equal(broadcast.RtmpList.Count(), 2); + Assert.Equal(2, broadcast.RtmpList.Count()); Assert.Null(broadcast.Hls); Assert.NotNull(broadcast.Id); - Assert.Equal(broadcast.Status, Broadcast.BroadcastStatus.STARTED); + Assert.Equal(Broadcast.BroadcastStatus.STARTED, broadcast.Status); mockClient.Verify(httpClient => httpClient.Post(It.Is(url => url.Equals("v2/project/" + apiKey + "/broadcast")), It.IsAny>(), It.IsAny>()), Times.Once()); } @@ -1273,10 +1779,10 @@ public void StartBroadcastWithRTMPandHLSTest() Assert.NotNull(broadcast); Assert.Equal(sessionId, broadcast.SessionId); Assert.NotNull(broadcast.RtmpList); - Assert.Equal(broadcast.RtmpList.Count(), 2); + Assert.Equal(2, broadcast.RtmpList.Count()); Assert.NotNull(broadcast.Hls); Assert.NotNull(broadcast.Id); - Assert.Equal(broadcast.Status, Broadcast.BroadcastStatus.STARTED); + Assert.Equal(Broadcast.BroadcastStatus.STARTED, broadcast.Status); mockClient.Verify(httpClient => httpClient.Post(It.Is(url => url.Equals("v2/project/" + apiKey + "/broadcast")), It.IsAny>(), It.IsAny>()), Times.Once()); } diff --git a/OpenTokTest/OpenTokTest.csproj b/OpenTokTest/OpenTokTest.csproj index 0ebf87e7..cc09b27e 100644 --- a/OpenTokTest/OpenTokTest.csproj +++ b/OpenTokTest/OpenTokTest.csproj @@ -1,7 +1,7 @@  Vonage - net452;net46;net461;net462;netcoreapp2.0;netcoreapp2.1;netcoreapp2.2;net47;net471;net472;net48;netcoreapp3.0;netcoreapp3.1 + net452;net46;net461;net462;net47;net471;net472;net48;netcoreapp3.1 Vonage Vonage 2020 diff --git a/README.md b/README.md index 34bdee92..af7f12b9 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ paginate the Archives you receive using the offset and count parameters. This wi `OpenTokSDK.ArchiveList` object. ```csharp -// Get a list with the first 1000 archives created by the API Key +// Get a list with the first 50 archives created by the API Key var archives = OpenTok.ListArchives(); // Get a list of the first 50 archives created by the API Key @@ -169,6 +169,9 @@ var archives = OpenTok.ListArchives(0, 50); // Get a list of the next 50 archives var archives = OpenTok.ListArchives(50, 50); + +// Get a list of the first 50 archives created for the given sessionId +var archives = OpenTok.ListArchives(sessionId:sessionId); ``` Note that you can also create an automatically archived session, by passing in `ArchiveMode.ALWAYS` @@ -262,6 +265,12 @@ The OpenTok .NET SDK requires .NET Framework 4.5.2 or greater. ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; ``` +Alternatively, if your application is dependant on a different version of TLS for other APIs, you can alternatively add TLS to the list of supported methods with a bitwise OR: + +``` +ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; +``` + ## Release Notes See the [Releases](https://github.com/opentok/opentok-.net-sdk/releases) page for details diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 404a04b1..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: 1.0.{build} -image: -- Visual Studio 2019 -configuration: Debug -platform: Any CPU -before_build: -- ps: >- - nuget restore -build: - project: OpenTok.sln - verbosity: minimal -test_script: -- ps: >- - $ErrorActionPreference = "stop" - - dotnet test C:\projects\opentok-net-sdk\OpenTokTest\OpenTokTest.csproj From f1cc863a2bd3ffdefa8a0c221b367e4fdc1c2cf5 Mon Sep 17 00:00:00 2001 From: Matt Lethargic Date: Thu, 16 Dec 2021 09:09:46 +0000 Subject: [PATCH 3/8] Implementing Dial method for initiating a SIP call (#166) * Adding Dial method, test fails * Adding dial async and tests * Adding session validation * Removing key from appsettings * Adding valid session id to tests * Docs edits for Dial API ... and other minor docs corrections Co-authored-by: Jeff Swartz --- OpenTok/DialAuth.cs | 20 +++ OpenTok/DialOptions.cs | 54 ++++++++ OpenTok/OpenTok.cs | 125 +++++++++++++++-- OpenTok/OpenTok.csproj | 10 ++ OpenTok/Util/HttpClient.cs | 36 +++-- OpenTokTest/DialTests.cs | 245 +++++++++++++++++++++++++++++++++ OpenTokTest/OpenTokTest.csproj | 1 + OpenTokTest/TestBase.cs | 1 + README.md | 5 + Samples/Broadcasting/README.md | 2 +- 10 files changed, 476 insertions(+), 23 deletions(-) create mode 100644 OpenTok/DialAuth.cs create mode 100644 OpenTok/DialOptions.cs create mode 100644 OpenTokTest/DialTests.cs diff --git a/OpenTok/DialAuth.cs b/OpenTok/DialAuth.cs new file mode 100644 index 00000000..9d4759c1 --- /dev/null +++ b/OpenTok/DialAuth.cs @@ -0,0 +1,20 @@ +namespace OpenTokSDK +{ + /// + /// Used to set the username and password to be used in the method. + /// These are used in the SIP INVITE​ request for HTTP digest authentication, if it is required + /// by your SIP platform. See the class. + /// + public class DialAuth + { + /// + /// The username. + /// + public string Username { get; set; } + + /// + /// The password. + /// + public string Password { get; set; } + } +} diff --git a/OpenTok/DialOptions.cs b/OpenTok/DialOptions.cs new file mode 100644 index 00000000..923e8e2f --- /dev/null +++ b/OpenTok/DialOptions.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; + +namespace OpenTokSDK +{ + /// + /// Used to define options for the the method. These are + /// custom headers to be added to the SIP ​INVITE​ request initiated from OpenTok to + /// your SIP platform. + /// + public class DialOptions + { + /// + /// A dictionary of custom headers to be added to the SIP ​INVITE​ request initiated + /// from OpenTok to your SIP platform. + /// + public Dictionary Headers { get; set; } + + /// + /// Contains the username and password to be used in the the SIP INVITE​ request. + /// + public DialAuth Auth { get; set; } + + /// + /// Indicates whether the media must be transmitted encrypted (​true​) or not (​false​, the default). + /// + public bool? Secure { get; set; } + + /// + /// The number or string that will be sent to the final SIP number as the caller. + /// + /// + /// This must be a string in the form of "from@example.com", where from can be a string or a number. If it is set + /// to a number (for example, "14155550101@example.com"), it will show up as the incoming number on PSTN phones. + /// If it is undefined or set to a string (for example, "joe@example.com"), +00000000 will show up as the incoming + /// number on PSTN phones. + /// + public string From { get; set; } + + /// + /// Whether the SIP call will include video (​true​) or not (​false​, the default). + /// + /// With video included, the SIP client's video is included in the OpenTok stream + /// that is sent to the OpenTok session. The SIP client will receive a single composed video of the published streams + /// in the OpenTok session. + public bool? Video { get; set; } + + /// + /// Whether the SIP endpoint observes + /// force mute moderation + /// (true) or not (false, the default). + /// + public bool? ObserveForceMute { get; set; } + } +} diff --git a/OpenTok/OpenTok.cs b/OpenTok/OpenTok.cs index 45a84ade..3a31d9f2 100644 --- a/OpenTok/OpenTok.cs +++ b/OpenTok/OpenTok.cs @@ -32,7 +32,7 @@ public class OpenTok /// /// For internal use /// - public HttpClient Client { private get; set; } + public HttpClient Client { internal get; set; } private bool _debug; /// @@ -302,7 +302,7 @@ public Archive StartArchive(string sessionId, string name = "", bool hasVideo = { throw new OpenTokArgumentException("Could not set layout, stylesheet must be set if and only if type is custom"); } - else if(layout.ScreenShareType != null && layout.Type != LayoutType.bestFit) + else if (layout.ScreenShareType != null && layout.Type != LayoutType.bestFit) { throw new OpenTokArgumentException($"Could not set screenShareLayout. When screenShareType is set, layout.Type must be bestFit, was {layout.Type}"); } @@ -346,7 +346,7 @@ public Archive StopArchive(string archiveId) /// The session ID. /// /// A List of objects. - public ArchiveList ListArchives(int offset = 0 , int count = 0, string sessionId = "") + public ArchiveList ListArchives(int offset = 0, int count = 0, string sessionId = "") { if (count < 0) { @@ -424,7 +424,7 @@ public Stream GetStream(string sessionId, string streamId) /// /// Returns a List of objects, representing streams that are in-progress, - /// for the Session Id. + /// for the session ID. /// /// The session ID corresponding to the session. /// A List of objects. @@ -643,7 +643,7 @@ public void SetBroadcastLayout(string broadcastId, BroadcastLayout layout) if (layout.Type.Equals(BroadcastLayout.LayoutType.Custom)) { data.Add("stylesheet", layout.Stylesheet); - } + } if (layout.ScreenShareType != null) { data.Add("screenShareType", OpenTokUtils.convertToCamelCase(layout.ScreenShareType.ToString())); @@ -668,13 +668,13 @@ public bool SetArchiveLayout(string archiveId, ArchiveLayout layout) string url = $"v2/project/{ApiKey}/archive/{archiveId}/layout"; var headers = new Dictionary { { "Content-type", "application/json" } }; var data = new Dictionary(); - if(layout != null) + if (layout != null) { if (layout.Type == LayoutType.custom && string.IsNullOrEmpty(layout.StyleSheet)) { throw new OpenTokArgumentException("Invalid layout, layout is custom but no stylesheet provided"); } - else if(layout.Type != LayoutType.custom && !string.IsNullOrEmpty(layout.StyleSheet)) + else if (layout.Type != LayoutType.custom && !string.IsNullOrEmpty(layout.StyleSheet)) { throw new OpenTokArgumentException("Invalid layout, layout is not custom, but stylesheet is set"); } @@ -686,7 +686,7 @@ public bool SetArchiveLayout(string archiveId, ArchiveLayout layout) } if (layout.ScreenShareType != null) { - if(layout.Type != LayoutType.bestFit) + if (layout.Type != LayoutType.bestFit) { throw new OpenTokArgumentException("Invalid layout, when ScreenShareType is set, Type must be bestFit"); } @@ -813,5 +813,114 @@ public Task PlayDTMFAsync(string sessionId, string digits, string connectionId = var data = new Dictionary { { "digits", digits } }; return Client.PostAsync(url, headers, data); } + + /// + /// Connects a SIP platform to an OpenTok session. + /// + /// + /// For more information, including technical details and security considerations, see the + /// the OpenTok SIP interconnect developer guide. + /// + /// The session ID corresponding to the session to which the user will connect. + /// The token for the session ID with which the SIP user will use to connect. + /// The SIP URI to be used as destination of the SIP call initiated from + /// OpenTok to your SIP platform. If the SIP URI contains a ​transport=tls​ header, + /// the negotiation between OpenTok and the SIP endpoint will be done securely. Note that + /// this will only apply to the negotiation itself, and not to the transmission of audio. + /// If you also audio transmission to be encrypted, set the Secure property of the + /// of the DialOptions object passed into the options parameter to ​true​. + /// This is an example of setting sipUri for a secure call negotiation: + /// "sip:user@sip.partner.com;transport=tls". This is an example of insecure call negotiation: + /// "sip:user@sip.partner.com". + /// Optional parameters for SIP dialing. + public void Dial(string sessionId, string token, string sipUri, DialOptions options = null) + { + if (string.IsNullOrEmpty(sessionId)) + { + throw new OpenTokArgumentException("The sessionId cannot be empty."); + } + + if (!OpenTokUtils.ValidateSession(sessionId)) + { + throw new OpenTokArgumentException("Session Id is not valid"); + } + + string url = $"v2/project/{this.ApiKey}/dial"; + + var headers = new Dictionary { { "Content-Type", "application/json" } }; + var data = new Dictionary + { + { "sessionId", sessionId }, + { "token", token }, + { "spi", new { + uri = sipUri, + from = options?.From, + headers = options?.Headers, + auth = options?.Auth, + secure = options?.Secure, + video = options?.Video, + observeForceMute = options?.ObserveForceMute + } + } + }; + Client.Post(url, headers, data); + } + + /// + /// Connects a SIP platform to an OpenTok session. + /// + /// + ///

+ /// For more information, including technical details and security considerations, see the + /// the OpenTok SIP interconnect developer guide. + ///

+ ///

+ /// Also see OpenTok.Dial. + ///

+ ///
+ /// The session ID corresponding to the session to which the user will connect. + /// The token for the session ID with which the SIP user will use to connect. + /// The SIP URI to be used as destination of the SIP call initiated from + /// OpenTok to your SIP platform. If the SIP URI contains a ​transport=tls​ header, + /// the negotiation between OpenTok and the SIP endpoint will be done securely. Note that + /// this will only apply to the negotiation itself, and not to the transmission of audio. + /// If you also audio transmission to be encrypted, set the Secure property of the + /// of the DialOptions object passed into the options parameter to ​true​. + /// This is an example of setting sipUri for a secure call negotiation: + /// "sip:user@sip.partner.com;transport=tls". This is an example of insecure call negotiation: + /// "sip:user@sip.partner.com". + /// Optional parameters for SIP dialing. + public Task DialAsync(string sessionId, string token, string sipUri, DialOptions options = null) + { + if (string.IsNullOrEmpty(sessionId)) + { + throw new OpenTokArgumentException("The sessionId cannot be empty."); + } + + if (!OpenTokUtils.ValidateSession(sessionId)) + { + throw new OpenTokArgumentException("Session Id is not valid"); + } + + string url = $"v2/project/{this.ApiKey}/dial"; + + var headers = new Dictionary { { "Content-Type", "application/json" } }; + var data = new Dictionary + { + { "sessionId", sessionId }, + { "token", token }, + { "spi", new { + uri = sipUri, + from = options?.From, + headers = options?.Headers, + auth = options?.Auth, + secure = options?.Secure, + video = options?.Video, + observeForceMute = options?.ObserveForceMute + } + } + }; + return Client.PostAsync(url, headers, data); + } } } diff --git a/OpenTok/OpenTok.csproj b/OpenTok/OpenTok.csproj index fdcc6732..de1d00c1 100644 --- a/OpenTok/OpenTok.csproj +++ b/OpenTok/OpenTok.csproj @@ -51,4 +51,14 @@ + + + <_Parameter1>OpenTokTest + + + <_Parameter1>DynamicProxyGenAssembly2 + + + + diff --git a/OpenTok/Util/HttpClient.cs b/OpenTok/Util/HttpClient.cs index c741dfb3..35d3c4b0 100644 --- a/OpenTok/Util/HttpClient.cs +++ b/OpenTok/Util/HttpClient.cs @@ -20,10 +20,10 @@ namespace OpenTokSDK.Util ///
public class HttpClient { - private string userAgent; private int apiKey; private string apiSecret; - private string server; + private string apiUrl; + public bool debug = false; /// @@ -34,17 +34,22 @@ public class HttpClient 1970, 1, 1, 0, 0, 0, DateTimeKind.Utc ); - public HttpClient() + + internal string LastRequest { get; private set; } + + internal HttpClient() {} + + internal HttpClient(int apiKey, string apiSecret) { - // This is only for testing purposes + this.apiKey = apiKey; + this.apiSecret = apiSecret; } public HttpClient(int apiKey, string apiSecret, string apiUrl = "") { this.apiKey = apiKey; this.apiSecret = apiSecret; - this.server = apiUrl; - this.userAgent = OpenTokVersion.GetVersion(); + this.apiUrl = apiUrl; } public virtual string Get(string url) @@ -102,7 +107,7 @@ public string DoRequest(string url, Dictionary specificHeaders, DebugLog("Request Body: " + data); SendData(request, data); } - using (response = (HttpWebResponse) request.GetResponse()) + using (response = (HttpWebResponse)request.GetResponse()) { DebugLog("Response Status Code: " + response.StatusCode); DebugLog("Response Status Description: " + response.StatusDescription); @@ -232,8 +237,10 @@ public XmlDocument ReadXmlResponse(string xml) return xmlDoc; } - private void SendData(HttpWebRequest request, object data) + private void SendData(HttpWebRequest request, string data) { + LastRequest = data.ToString(); + using (StreamWriter stream = new StreamWriter(request.GetRequestStream())) { stream.Write(data); @@ -250,14 +257,14 @@ private async Task SendDataAsync(HttpWebRequest request, string data) private HttpWebRequest CreateRequest(string url, Dictionary headers, string data) { - Uri uri = new Uri(string.Format("{0}/{1}", server, url)); + Uri uri = new Uri(string.Format("{0}/{1}", apiUrl, url)); HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri); if (RequestTimeout != null) { request.Timeout = (int)RequestTimeout; } request.ContentLength = data.Length; - request.UserAgent = userAgent; + request.UserAgent = OpenTokVersion.GetVersion(); if (headers.ContainsKey("Content-type")) { @@ -278,6 +285,7 @@ private HttpWebRequest CreateRequest(string url, Dictionary head return request; } + private Dictionary GetRequestHeaders(Dictionary headers) { var requestHeaders = GetCommonHeaders(); @@ -292,7 +300,7 @@ private string GetRequestPostData(Dictionary data, Dictionary(() => opentok.Dial(sessionId, token, sipUri)); + } + + [Fact] + public async Task DialAsyncThrowsExceptionWithEmptySession() + { + string sessionId = ""; + string token = "1234567890"; + string sipUri = "SIPURI"; + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + + await Assert.ThrowsAsync(() => opentok.DialAsync(sessionId, token, sipUri)); + } + + [Fact] + public void DialCorrectUrl() + { + // arrange + string token = "1234567890"; + string sipUri = "SIPURI"; + + var expectedUrl = $"v2/project/{ApiKey}/dial"; + + var mockClient = new Mock(MockBehavior.Strict); + mockClient + .Setup(httpClient => httpClient.Post(expectedUrl, It.IsAny>(), It.IsAny>())) + .Returns(string.Empty) + .Verifiable(); + + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + + // act + opentok.Dial(SessionId, token, sipUri); + + // assert + mockClient.Verify(); + } + + [Fact] + public async Task DialAsyncCorrectUrl() + { + // arrange + string token = "1234567890"; + string sipUri = "SIPURI"; + + var expectedUrl = $"v2/project/{ApiKey}/dial"; + + var mockClient = new Mock(MockBehavior.Strict); + mockClient + .Setup(httpClient => httpClient.PostAsync(expectedUrl, It.IsAny>(), It.IsAny>())) + .ReturnsAsync(string.Empty) + .Verifiable(); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + + // act + await opentok.DialAsync(SessionId, token, sipUri); + + // assert + mockClient.Verify(); + } + + [Fact] + public void DialCorrectHeaders() + { + // arrange + string token = "1234567890"; + string sipUri = "SIPURI"; + + Dictionary headersSent = null; + Dictionary dataSent = null; + + var mockClient = new Mock(); + mockClient + .Setup(httpClient => httpClient.Post(It.IsAny(), It.IsAny>(), It.IsAny>())) + .Callback, Dictionary>((url, headers, data) => + { + headersSent = headers; + dataSent = data; + }); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + opentok.Dial(SessionId, token, sipUri); + + // assert + Assert.NotNull(headersSent); + Assert.Equal(new Dictionary { { "Content-Type", "application/json" } }, headersSent); + } + + [Fact] + public async Task DialAsyncCorrectHeaders() + { + // arrange + string token = "1234567890"; + string sipUri = "SIPURI"; + + Dictionary headersSent = null; + Dictionary dataSent = null; + + var mockClient = new Mock(); + mockClient + .Setup(httpClient => httpClient.PostAsync(It.IsAny(), It.IsAny>(), It.IsAny>())) + .Callback, Dictionary>((url, headers, data) => + { + headersSent = headers; + dataSent = data; + }); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + await opentok.DialAsync(SessionId, token, sipUri); + + // assert + Assert.NotNull(headersSent); + Assert.Equal(new Dictionary { { "Content-Type", "application/json" } }, headersSent); + } + + [Fact] + public void DialCorrectData() + { + // arrange + string token = "1234567890"; + string sipUri = "SIPURI"; + + DialOptions dialOptions = new DialOptions + { + From = "bob", + Headers = null, + Auth = new DialAuth { Username = "Tim", Password = "Bob" }, + Secure = true, + Video = true, + ObserveForceMute = true + }; + + Dictionary headersSent = null; + Dictionary dataSent = null; + + var mockClient = new Mock(); + mockClient + .Setup(httpClient => httpClient.Post(It.IsAny(), It.IsAny>(), It.IsAny>())) + .Callback, Dictionary>((url, headers, data) => + { + headersSent = headers; + dataSent = data; + }); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + opentok.Dial(SessionId, token, sipUri, dialOptions); + + // assert + Assert.NotNull(dataSent); + Assert.Equal(SessionId, dataSent["sessionId"]); + Assert.Equal(token, dataSent["token"]); + + Assert.True(dataSent.ContainsKey("spi")); + + dynamic spi = dataSent["spi"]; + + Assert.Equal(dialOptions.From, spi.from); + Assert.Equal(dialOptions.Headers, spi.headers); + Assert.Equal(dialOptions.Auth, spi.auth); + Assert.Equal(dialOptions.Secure, spi.secure); + Assert.Equal(dialOptions.Video, spi.video); + Assert.Equal(dialOptions.ObserveForceMute, spi.observeForceMute); + } + + [Fact] + public async Task DialAsyncCorrectData() + { + // arrange + string token = "1234567890"; + string sipUri = "SIPURI"; + + DialOptions dialOptions = new DialOptions + { + From = "bob", + Headers = null, + Auth = new DialAuth { Username = "Tim", Password = "Bob" }, + Secure = true, + Video = true, + ObserveForceMute = true + }; + + Dictionary headersSent = null; + Dictionary dataSent = null; + + var mockClient = new Mock(); + mockClient + .Setup(httpClient => httpClient.PostAsync(It.IsAny(), It.IsAny>(), It.IsAny>())) + .Callback, Dictionary>((url, headers, data) => + { + headersSent = headers; + dataSent = data; + }) + .ReturnsAsync(string.Empty); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + await opentok.DialAsync(SessionId, token, sipUri, dialOptions); + + // assert + Assert.NotNull(dataSent); + Assert.Equal(SessionId, dataSent["sessionId"]); + Assert.Equal(token, dataSent["token"]); + + Assert.True(dataSent.ContainsKey("spi")); + + dynamic spi = dataSent["spi"]; + + Assert.Equal(dialOptions.From, spi.from); + Assert.Equal(dialOptions.Headers, spi.headers); + Assert.Equal(dialOptions.Auth, spi.auth); + Assert.Equal(dialOptions.Secure, spi.secure); + Assert.Equal(dialOptions.Video, spi.video); + Assert.Equal(dialOptions.ObserveForceMute, spi.observeForceMute); + } + } +} diff --git a/OpenTokTest/OpenTokTest.csproj b/OpenTokTest/OpenTokTest.csproj index f1bd8a66..201d6a5e 100644 --- a/OpenTokTest/OpenTokTest.csproj +++ b/OpenTokTest/OpenTokTest.csproj @@ -17,6 +17,7 @@ + \ No newline at end of file diff --git a/OpenTokTest/TestBase.cs b/OpenTokTest/TestBase.cs index 9d2b7f3d..0f1fc857 100644 --- a/OpenTokTest/TestBase.cs +++ b/OpenTokTest/TestBase.cs @@ -4,5 +4,6 @@ public abstract class TestBase { protected string ApiSecret = "1234567890abcdef1234567890abcdef1234567890"; protected int ApiKey = 123456; + protected string SessionId = "1_MX4xMjM0NTZ-flNhdCBNYXIgMTUgMTQ6NDI6MjMgUERUIDIwMTR-MC40OTAxMzAyNX4"; } } diff --git a/README.md b/README.md index af7f12b9..20723042 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ The OpenTok .NET SDK lets you generate [tokens](https://www.tokbox.com/opentok/tutorials/create-token/) for [OpenTok](https://www.tokbox.com/) applications that run on the .NET platform. The SDK also includes support for working with [OpenTok archives](https://tokbox.com/opentok/tutorials/archiving). +working with [OpenTok live streaming broadcasts](https://tokbox.com/developer/guides/broadcast/live-streaming/), +[working with OpenTok SIP interconnect](https://tokbox.com/developer/guides/sip), +[sending signals to clients connected to a session](https://www.tokbox.com/developer/guides/signaling/), +and [disconnecting clients from sessions](https://tokbox.com/developer/guides/moderation/rest/). + ## Installation diff --git a/Samples/Broadcasting/README.md b/Samples/Broadcasting/README.md index 1615d27f..cdbab3f6 100644 --- a/Samples/Broadcasting/README.md +++ b/Samples/Broadcasting/README.md @@ -226,7 +226,7 @@ First, we're getting a list of the streams in a session using the OpenTok.ListSt For each one of these streams, we create a new StreamProperties object where we set the stream Id and the stream layout class, and we're adding it to a List of StreamProperties objects. In this case, we're adding the layout class `focus` only to the stream with the Id equals to `opentokService.focusStreamId` which causes it to be the large stream displayed in the broadcast. -Finally we pass the complete list of streams properties to the method OpenTok.SetStreamClassLists() that receives the session Id and the streamPropertiesList. +Finally we pass the complete list of streams properties to the method OpenTok.SetStreamClassLists() that receives the session ID and the streamPropertiesList. To see this effect, you should open the host and participant pages on different computers (using different cameras). Or, if you have multiple cameras connected to your machine, you can use one camera for publishing from the host, and use another for the participant. Or, if you are using a laptop with an external monitor, you can load the host page with the laptop closed (no camera) and open the participant page with the laptop open. From e357651f4881f4d286f8b28f0728b700588dabdc Mon Sep 17 00:00:00 2001 From: Jeff Swartz Date: Fri, 14 Jan 2022 15:30:15 -0500 Subject: [PATCH 4/8] Rev the version to 3.7.0 --- OpenTok/OpenTok.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OpenTok/OpenTok.csproj b/OpenTok/OpenTok.csproj index de1d00c1..0adfdf3f 100644 --- a/OpenTok/OpenTok.csproj +++ b/OpenTok/OpenTok.csproj @@ -1,13 +1,13 @@ net452;net46;net461;netstandard2.0 - 3.6.0 + 3.7.0 OpenTok is an API from TokBox that enables websites to weave live group video communication into their online experience. https://github.com/opentok/Opentok-.NET-SDK https://github.com/opentok/Opentok-.NET-SDK git true - https://github.com/opentok/Opentok-.NET-SDK/releases/tag/v3.6.0 + https://github.com/opentok/Opentok-.NET-SDK/releases/tag/v3.7.0 true true {C770C266-B8E6-413A-B5AB-68EB218DC76C} From bfaec99d096c9cfb0cb5f4fcfe01ad55a5ba5560 Mon Sep 17 00:00:00 2001 From: Matt Lethargic Date: Wed, 19 Jan 2022 10:08:44 +0000 Subject: [PATCH 5/8] Adding Force and Disable Mute (#167) * Adding Force and Disable Mute * Docs edits for the Force Mute API enhancements * Fixing PlayDTMF methods that had wrong URL Co-authored-by: Jeff Swartz Co-authored-by: matt-lethargic --- .github/workflows/build.yml | 11 +- OpenTok/Exception/OpenTokArgumentException.cs | 23 ++ OpenTok/Exception/OpenTokException.cs | 59 +---- OpenTok/Exception/OpenTokWebException.cs | 27 +++ OpenTok/OpenTok.cs | 184 +++++++++++++++- OpenTokTest/DisableForceMuteTests.cs | 168 ++++++++++++++ OpenTokTest/ForceMuteAllTests.cs | 207 ++++++++++++++++++ OpenTokTest/ForceMuteStreamTests.cs | 164 ++++++++++++++ OpenTokTest/PlayDtmfTests.cs | 4 +- 9 files changed, 786 insertions(+), 61 deletions(-) create mode 100644 OpenTok/Exception/OpenTokArgumentException.cs create mode 100644 OpenTok/Exception/OpenTokWebException.cs create mode 100644 OpenTokTest/DisableForceMuteTests.cs create mode 100644 OpenTokTest/ForceMuteAllTests.cs create mode 100644 OpenTokTest/ForceMuteStreamTests.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 76e95492..bc08e3b7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,8 @@ env: jobs: build: - + env: + DOTNET_NOLOGO: true runs-on: windows-latest steps: @@ -19,7 +20,13 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: '3.1.401' + dotnet-version: | + 2.0.x + 2.1.x + 2.2.x + 3.1.x + 5.0.x + 6.0.x - name: Clean run: dotnet clean OpenTok.sln --configuration ${{ env.CONFIGURATION }} && dotnet nuget locals all --clear - name: Install dependencies diff --git a/OpenTok/Exception/OpenTokArgumentException.cs b/OpenTok/Exception/OpenTokArgumentException.cs new file mode 100644 index 00000000..df15e101 --- /dev/null +++ b/OpenTok/Exception/OpenTokArgumentException.cs @@ -0,0 +1,23 @@ +using System; + +namespace OpenTokSDK.Exception +{ + /// + /// Defines an exception object thrown when an invalid argument is passed into a method. + /// + public class OpenTokArgumentException : ArgumentException + { + /// + public OpenTokArgumentException(string message) + :base(message) + { + + } + + /// + public OpenTokArgumentException(string message, string paramName) + : base(message, paramName) + { + } + } +} \ No newline at end of file diff --git a/OpenTok/Exception/OpenTokException.cs b/OpenTok/Exception/OpenTokException.cs index 8c302b37..2423c690 100644 --- a/OpenTok/Exception/OpenTokException.cs +++ b/OpenTok/Exception/OpenTokException.cs @@ -1,19 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace OpenTokSDK.Exception +namespace OpenTokSDK.Exception { /// /// Defines exceptions in the OpenTok SDK. /// public class OpenTokException : System.Exception { - - /// - /// Construct opentok exception - /// + + /// public OpenTokException() { } @@ -23,15 +16,15 @@ public OpenTokException() /// /// public OpenTokException(string message) - : base(message){} - + : base(message) { } + /// /// Construct OpenTokException with a message and an inner exception /// /// /// public OpenTokException(string message, System.Exception exception) - : base(message, exception){} + : base(message, exception) { } /// /// Get's the message of the exception @@ -51,44 +44,4 @@ public System.Exception GetException() return InnerException; } } - - /// - /// Defines an exception object thrown when an invalid argument is passed into a method. - /// - public class OpenTokArgumentException : OpenTokException - { - /// - /// Constructor. Do not use. - /// - /// - public OpenTokArgumentException(string message) - : base(message) - { - } - } - - /// - /// Defines an exception object thrown when a REST API call results in an error response. - /// - public class OpenTokWebException : OpenTokException - { - /// - /// Constructor. Do not use. - /// - /// - /// - public OpenTokWebException(string message, System.Exception exception) - : base(message, exception) - { - } - - /// - /// Constructor. Do not use. - /// - /// - public OpenTokWebException(string message) - : base(message) - { - } - } } diff --git a/OpenTok/Exception/OpenTokWebException.cs b/OpenTok/Exception/OpenTokWebException.cs new file mode 100644 index 00000000..259931da --- /dev/null +++ b/OpenTok/Exception/OpenTokWebException.cs @@ -0,0 +1,27 @@ +namespace OpenTokSDK.Exception +{ + /// + /// Defines an exception object thrown when a REST API call results in an error response. + /// + public class OpenTokWebException : OpenTokException + { + /// + /// Constructor. Do not use. + /// + /// + /// + public OpenTokWebException(string message, System.Exception exception) + : base(message, exception) + { + } + + /// + /// Constructor. Do not use. + /// + /// + public OpenTokWebException(string message) + : base(message) + { + } + } +} \ No newline at end of file diff --git a/OpenTok/OpenTok.cs b/OpenTok/OpenTok.cs index 3a31d9f2..11fb71a9 100644 --- a/OpenTok/OpenTok.cs +++ b/OpenTok/OpenTok.cs @@ -784,8 +784,8 @@ public void PlayDTMF(string sessionId, string digits, string connectionId = null } string url = string.IsNullOrEmpty(connectionId) - ? $"v2/project//session/{sessionId}/play-dtmf" - : $"v2/project//session/{sessionId}/connection/{connectionId}/play-dtmf"; + ? $"v2/project/{ApiKey}/session/{sessionId}/play-dtmf" + : $"v2/project/{ApiKey}/session/{sessionId}/connection/{connectionId}/play-dtmf"; var headers = new Dictionary { { "Content-Type", "application/json" } }; var data = new Dictionary { { "digits", digits } }; @@ -806,8 +806,8 @@ public Task PlayDTMFAsync(string sessionId, string digits, string connectionId = } string url = string.IsNullOrEmpty(connectionId) - ? $"v2/project//session/{sessionId}/play-dtmf" - : $"v2/project//session/{sessionId}/connection/{connectionId}/play-dtmf"; + ? $"v2/project/{ApiKey}/session/{sessionId}/play-dtmf" + : $"v2/project/{ApiKey}/session/{sessionId}/connection/{connectionId}/play-dtmf"; var headers = new Dictionary { { "Content-Type", "application/json" } }; var data = new Dictionary { { "digits", digits } }; @@ -922,5 +922,181 @@ public Task DialAsync(string sessionId, string token, string sipUri, DialOptions }; return Client.PostAsync(url, headers, data); } + + /// + /// Force the publisher of a specific stream to mute its published audio. + /// + /// + /// Also see the and methods. + /// + /// The session ID of the session that includes the stream. + /// The stream ID. + /// Thrown when session or stream ID is invalid. + /// Thrown when an HTTP error has occurred. + public void ForceMuteStream(string sessionId, string streamId) + { + if (string.IsNullOrEmpty(sessionId)) + { + throw new OpenTokArgumentException("The sessionId cannot be empty.", nameof(sessionId)); + } + + if (string.IsNullOrEmpty(streamId)) + { + throw new OpenTokArgumentException("The streamId cannot be empty.", nameof(streamId)); + } + + string url = $"v2/project/{this.ApiKey}/session/{sessionId}/stream/{streamId}/mute"; + + var headers = new Dictionary { { "Content-Type", "application/json" } }; + Client.Post(url, headers, null); + } + + /// + /// Force the publisher of a specific stream to mute its published audio. + /// + /// + /// Also see the and methods. + /// + /// The session ID of the session that includes the stream. + /// The stream ID. + /// Thrown when session or stream ID is invalid. + /// Thrown when an HTTP error has occurred. + public async Task ForceMuteStreamAsync(string sessionId, string streamId) + { + if (string.IsNullOrEmpty(sessionId)) + { + throw new OpenTokArgumentException("The sessionId cannot be empty.", nameof(sessionId)); + } + + if (string.IsNullOrEmpty(streamId)) + { + throw new OpenTokArgumentException("The streamId cannot be empty.", nameof(streamId)); + } + + string url = $"v2/project/{this.ApiKey}/session/{sessionId}/stream/{streamId}/mute"; + + var headers = new Dictionary { { "Content-Type", "application/json" } }; + await Client.PostAsync(url, headers, null); + } + + /// + /// Forces all streams (except for an optional list of streams) in a session to mute + /// published audio. + /// + /// + /// In addition to existing streams, any streams that are published after the call to + /// this method are published with audio muted. You can remove the mute state of a session + /// by calling the method. + /// + /// + /// Also see the and methods. + /// + /// The ID of session. + /// The stream IDs of streams that will not be muted. + /// Thrown when the session ID is invalid. + /// Thrown when an HTTP error has occurred. + public void ForceMuteAll(string sessionId, string[] excludedStreamIds) + { + if (string.IsNullOrEmpty(sessionId)) + { + throw new OpenTokArgumentException("The sessionId cannot be empty.", nameof(sessionId)); + } + + string url = $"v2/project/{ApiKey}/session/{sessionId}/mute"; + + var headers = new Dictionary { { "Content-Type", "application/json" } }; + var data = new Dictionary { { "active", true }, { "excludedStreamIds", excludedStreamIds } }; + Client.Post(url, headers, data); + } + + /// + /// Forces all streams (except for an optional list of streams) in a session to mute + /// published audio. + /// + /// + /// In addition to existing streams, any streams that are published after the call to + /// this method are published with audio muted. You can remove the mute state of a session + /// by calling the method. + /// + /// + /// Also see the and methods. + /// + /// The ID of session. + /// The stream IDs of streams that will not be muted. + /// Thrown when the session ID is invalid. + /// Thrown when an HTTP error has occurred. + public async Task ForceMuteAllAsync(string sessionId, string[] excludedStreamIds) + { + if (string.IsNullOrEmpty(sessionId)) + { + throw new OpenTokArgumentException("The sessionId cannot be empty.", nameof(sessionId)); + } + + string url = $"v2/project/{this.ApiKey}/session/{sessionId}/mute"; + + var headers = new Dictionary { { "Content-Type", "application/json" } }; + var data = new Dictionary { { "active", true }, { "excludedStreamIds", excludedStreamIds } }; + await Client.PostAsync(url, headers, data); + } + + /// + /// Disables the active mute state of the session. After you call this method, new streams + /// published to the session will no longer have audio muted. + /// + /// + /// After you call the method, any streams published after + /// the call are published with audio muted. Call the DisableForceMute() method + // to remove the mute state of a session, so that new published streams are not + /// automatically muted. + /// + /// + /// Also see the method. + /// + /// The session ID. + /// Thrown when the session ID is invalid. + /// Thrown when an HTTP error has occurred. + public void DisableForceMute(string sessionId) + { + if (string.IsNullOrEmpty(sessionId)) + { + throw new OpenTokArgumentException("The sessionId cannot be empty.", nameof(sessionId)); + } + + string url = $"v2/project/{ApiKey}/session/{sessionId}/mute"; + + var headers = new Dictionary { { "Content-Type", "application/json" } }; + var data = new Dictionary { { "active", false } }; + Client.Post(url, headers, data); + } + + /// + /// Disables the active mute state of the session. After you call this method, new streams + /// published to the session will no longer have audio muted. + /// + /// + /// After you call the method, any streams published after + /// the call are published with audio muted. Call the DisableForceMuteAsync() method + // to remove the mute state of a session, so that new published streams are not + /// automatically muted. + /// + /// + /// Also see the method. + /// + /// The session ID. + /// Thrown when the session ID is invalid. + /// Thrown when an HTTP error has occurred. + public async Task DisableForceMuteAsync(string sessionId) + { + if (string.IsNullOrEmpty(sessionId)) + { + throw new OpenTokArgumentException("The sessionId cannot be empty.", nameof(sessionId)); + } + + string url = $"v2/project/{ApiKey}/session/{sessionId}/mute"; + + var headers = new Dictionary { { "Content-Type", "application/json" } }; + var data = new Dictionary { { "active", false } }; + await Client.PostAsync(url, headers, data); + } } } diff --git a/OpenTokTest/DisableForceMuteTests.cs b/OpenTokTest/DisableForceMuteTests.cs new file mode 100644 index 00000000..f3dc6d82 --- /dev/null +++ b/OpenTokTest/DisableForceMuteTests.cs @@ -0,0 +1,168 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Moq; +using OpenTokSDK; +using OpenTokSDK.Exception; +using OpenTokSDK.Util; +using Xunit; + +namespace OpenTokSDKTest +{ + public class DisableForceMuteTests : TestBase + { + [Fact] + public void DisableForceMuteExceptionWithEmptySessionId() + { + string sessionId = ""; + + OpenTok openTok = new OpenTok(ApiKey, ApiSecret); + + var exception = Assert.Throws(() => openTok.DisableForceMute(sessionId)); + Assert.Contains("The sessionId cannot be empty.", exception.Message); + Assert.Equal("sessionId", exception.ParamName); + } + + [Fact] + public void DisableForceMuteCorrectUrl() + { + string sessionId = "SESSIONID"; + + string expectedUrl = $"v2/project/{ApiKey}/session/{sessionId}/mute"; + + var mockClient = new Mock(MockBehavior.Strict); + mockClient + .Setup(httpClient => httpClient.Post(expectedUrl, It.IsAny>(), + It.IsAny>())) + .Returns("") + .Verifiable(); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + opentok.DisableForceMute(sessionId); + + mockClient.Verify(); + } + + [Fact] + public void DisableForceMuteHeaderAndDataCorrect() + { + string sessionId = "SESSIONID"; + + Dictionary headersSent = null; + Dictionary dataSent = null; + + var mockClient = new Mock(); + mockClient + .Setup(httpClient => httpClient.Post(It.IsAny(), It.IsAny>(), + It.IsAny>())) + .Callback, Dictionary>((url, headers, data) => + { + headersSent = headers; + dataSent = data; + }); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + opentok.DisableForceMute(sessionId); + + Assert.NotNull(headersSent); + Assert.Equal(new Dictionary { { "Content-Type", "application/json" } }, headersSent); + + Assert.NotNull(dataSent); + Assert.Equal(new Dictionary { { "active", false } }, + dataSent); + } + + [Fact] + public async Task DisableForceMuteAsyncExceptionWithEmptySessionId() + { + string sessionId = ""; + + OpenTok openTok = new OpenTok(ApiKey, ApiSecret); + + var exception = await Assert.ThrowsAsync(async () => await openTok.DisableForceMuteAsync(sessionId)); + Assert.Contains("The sessionId cannot be empty.", exception.Message); + Assert.Equal("sessionId", exception.ParamName); + } + + [Fact] + public async Task DisableForceMuteAsyncCorrectUrl() + { + string sessionId = "SESSIONID"; + + string expectedUrl = $"v2/project/{ApiKey}/session/{sessionId}/mute"; + + var mockClient = new Mock(MockBehavior.Strict); + mockClient + .Setup(httpClient => httpClient.PostAsync(expectedUrl, It.IsAny>(), + It.IsAny>())) + .ReturnsAsync("") + .Verifiable(); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + await opentok.DisableForceMuteAsync(sessionId); + + mockClient.Verify(); + } + + [Fact] + public async Task DisableForceMuteAsyncWithNullExcludedStreamHeaderAndDataCorrect() + { + string sessionId = "SESSIONID"; + + Dictionary headersSent = null; + Dictionary dataSent = null; + + var mockClient = new Mock(); + mockClient + .Setup(httpClient => httpClient.PostAsync(It.IsAny(), It.IsAny>(), + It.IsAny>())) + .Callback, Dictionary>((url, headers, data) => + { + headersSent = headers; + dataSent = data; + }); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + await opentok.DisableForceMuteAsync(sessionId); + + Assert.NotNull(headersSent); + Assert.Equal(new Dictionary { { "Content-Type", "application/json" } }, headersSent); + + Assert.NotNull(dataSent); + Assert.Equal(new Dictionary { { "active", false } }, dataSent); + } + + [Fact] + public async Task DisableForceMuteAsyncWithExcludedStreamHeaderAndDataCorrect() + { + string sessionId = "SESSIONID"; + + Dictionary headersSent = null; + Dictionary dataSent = null; + + var mockClient = new Mock(); + mockClient + .Setup(httpClient => httpClient.PostAsync(It.IsAny(), It.IsAny>(), + It.IsAny>())) + .Callback, Dictionary>((url, headers, data) => + { + headersSent = headers; + dataSent = data; + }); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + await opentok.DisableForceMuteAsync(sessionId); + + Assert.NotNull(headersSent); + Assert.Equal(new Dictionary { { "Content-Type", "application/json" } }, headersSent); + + Assert.NotNull(dataSent); + Assert.Equal(new Dictionary { { "active", false } }, + dataSent); + } + } +} \ No newline at end of file diff --git a/OpenTokTest/ForceMuteAllTests.cs b/OpenTokTest/ForceMuteAllTests.cs new file mode 100644 index 00000000..d4e5bb80 --- /dev/null +++ b/OpenTokTest/ForceMuteAllTests.cs @@ -0,0 +1,207 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Moq; +using OpenTokSDK; +using OpenTokSDK.Exception; +using OpenTokSDK.Util; +using Xunit; + +namespace OpenTokSDKTest +{ + public class ForceMuteAllTests : TestBase + { + [Fact] + public void ForceMuteAllExceptionWithEmptySessionId() + { + string sessionId = ""; + + OpenTok openTok = new OpenTok(ApiKey, ApiSecret); + + var exception = Assert.Throws(() => openTok.ForceMuteAll(sessionId, null)); + Assert.Contains("The sessionId cannot be empty.", exception.Message); + Assert.Equal("sessionId", exception.ParamName); + } + + [Fact] + public void ForceMuteAllCorrectUrl() + { + string sessionId = "SESSIONID"; + + string expectedUrl = $"v2/project/{ApiKey}/session/{sessionId}/mute"; + + var mockClient = new Mock(MockBehavior.Strict); + mockClient + .Setup(httpClient => httpClient.Post(expectedUrl, It.IsAny>(), + It.IsAny>())) + .Returns("") + .Verifiable(); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + opentok.ForceMuteAll(sessionId, null); + + mockClient.Verify(); + } + + [Fact] + public void ForceMuteAllWithNullExcludedStreamHeaderAndDataCorrect() + { + string sessionId = "SESSIONID"; + + Dictionary headersSent = null; + Dictionary dataSent = null; + + var mockClient = new Mock(); + mockClient + .Setup(httpClient => httpClient.Post(It.IsAny(), It.IsAny>(), + It.IsAny>())) + .Callback, Dictionary>((url, headers, data) => + { + headersSent = headers; + dataSent = data; + }); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + opentok.ForceMuteAll(sessionId, null); + + Assert.NotNull(headersSent); + Assert.Equal(new Dictionary {{"Content-Type", "application/json"}}, headersSent); + + Assert.NotNull(dataSent); + Assert.Equal(new Dictionary {{"active", true}, {"excludedStreamIds", null}}, dataSent); + } + + [Fact] + public void ForceMuteAllWithExcludedStreamHeaderAndDataCorrect() + { + string sessionId = "SESSIONID"; + string[] excludedStreamIds = + { + "excludedStreamId1", + "excludedStreamId2" + }; + + Dictionary headersSent = null; + Dictionary dataSent = null; + + var mockClient = new Mock(); + mockClient + .Setup(httpClient => httpClient.Post(It.IsAny(), It.IsAny>(), + It.IsAny>())) + .Callback, Dictionary>((url, headers, data) => + { + headersSent = headers; + dataSent = data; + }); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + opentok.ForceMuteAll(sessionId, excludedStreamIds); + + Assert.NotNull(headersSent); + Assert.Equal(new Dictionary {{"Content-Type", "application/json"}}, headersSent); + + Assert.NotNull(dataSent); + Assert.Equal(new Dictionary {{"active", true}, {"excludedStreamIds", excludedStreamIds}}, + dataSent); + } + + [Fact] + public async Task ForceMuteAllAsyncExceptionWithEmptySessionId() + { + string sessionId = ""; + + OpenTok openTok = new OpenTok(ApiKey, ApiSecret); + + var exception = await Assert.ThrowsAsync(async () => await openTok.ForceMuteAllAsync(sessionId, null)); + Assert.Contains("The sessionId cannot be empty.", exception.Message); + Assert.Equal("sessionId", exception.ParamName); + } + + [Fact] + public async Task ForceMuteAllAsyncCorrectUrl() + { + string sessionId = "SESSIONID"; + + string expectedUrl = $"v2/project/{ApiKey}/session/{sessionId}/mute"; + + var mockClient = new Mock(MockBehavior.Strict); + mockClient + .Setup(httpClient => httpClient.PostAsync(expectedUrl, It.IsAny>(), + It.IsAny>())) + .ReturnsAsync("") + .Verifiable(); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + await opentok.ForceMuteAllAsync(sessionId, null); + + mockClient.Verify(); + } + + [Fact] + public async Task ForceMuteAllAsyncWithNullExcludedStreamHeaderAndDataCorrect() + { + string sessionId = "SESSIONID"; + + Dictionary headersSent = null; + Dictionary dataSent = null; + + var mockClient = new Mock(); + mockClient + .Setup(httpClient => httpClient.PostAsync(It.IsAny(), It.IsAny>(), + It.IsAny>())) + .Callback, Dictionary>((url, headers, data) => + { + headersSent = headers; + dataSent = data; + }); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + await opentok.ForceMuteAllAsync(sessionId, null); + + Assert.NotNull(headersSent); + Assert.Equal(new Dictionary {{"Content-Type", "application/json"}}, headersSent); + + Assert.NotNull(dataSent); + Assert.Equal(new Dictionary {{"active", true}, {"excludedStreamIds", null}}, dataSent); + } + + [Fact] + public async Task ForceMuteAllAsyncWithExcludedStreamHeaderAndDataCorrect() + { + string sessionId = "SESSIONID"; + string[] excludedStreamIds = + { + "excludedStreamId1", + "excludedStreamId2" + }; + + Dictionary headersSent = null; + Dictionary dataSent = null; + + var mockClient = new Mock(); + mockClient + .Setup(httpClient => httpClient.PostAsync(It.IsAny(), It.IsAny>(), + It.IsAny>())) + .Callback, Dictionary>((url, headers, data) => + { + headersSent = headers; + dataSent = data; + }); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + await opentok.ForceMuteAllAsync(sessionId, excludedStreamIds); + + Assert.NotNull(headersSent); + Assert.Equal(new Dictionary {{"Content-Type", "application/json"}}, headersSent); + + Assert.NotNull(dataSent); + Assert.Equal(new Dictionary {{"active", true}, {"excludedStreamIds", excludedStreamIds}}, + dataSent); + } + } +} \ No newline at end of file diff --git a/OpenTokTest/ForceMuteStreamTests.cs b/OpenTokTest/ForceMuteStreamTests.cs new file mode 100644 index 00000000..310dd26d --- /dev/null +++ b/OpenTokTest/ForceMuteStreamTests.cs @@ -0,0 +1,164 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Moq; +using OpenTokSDK; +using OpenTokSDK.Exception; +using OpenTokSDK.Util; +using Xunit; + +namespace OpenTokSDKTest +{ + public class ForceMuteStreamTests : TestBase + { + + [Fact] + public void ForceMuteStreamExceptionWithEmptySessionId() + { + string sessionId = ""; + string streamId = "1234567890"; + + OpenTok openTok = new OpenTok(ApiKey, ApiSecret); + + var exception = Assert.Throws(() => openTok.ForceMuteStream(sessionId, streamId)); + Assert.Contains("The sessionId cannot be empty.", exception.Message); + Assert.Equal("sessionId", exception.ParamName); + } + + [Fact] + public void ForceMuteStreamExceptionWithEmptyStreamId() + { + string sessionId = "1234567890"; + string streamId = ""; + + OpenTok openTok = new OpenTok(ApiKey, ApiSecret); + + var exception = Assert.Throws(() => openTok.ForceMuteStream(sessionId, streamId)); + Assert.Contains("The streamId cannot be empty.", exception.Message); + Assert.Equal("streamId", exception.ParamName); + } + + [Fact] + public void ForceMuteStreamCorrectUrl() + { + string sessionId = "SESSIONID"; + string streamId = "1234567890"; + + string expectedUrl = $"v2/project/{this.ApiKey}/session/{sessionId}/stream/{streamId}/mute"; + + var mockClient = new Mock(MockBehavior.Strict); + mockClient + .Setup(httpClient => httpClient.Post(expectedUrl, It.IsAny>(), It.IsAny>())) + .Returns("") + .Verifiable(); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + opentok.ForceMuteStream(sessionId, streamId); + + mockClient.Verify(); + } + + [Fact] + public void ForceMuteSteamHeaderAndDataCorrect() + { + string sessionId = "SESSIONID"; + string streamId = "1234567890"; + + Dictionary headersSent = null; + Dictionary dataSent = null; + + var mockClient = new Mock(); + mockClient + .Setup(httpClient => httpClient.Post(It.IsAny(), It.IsAny>(), It.IsAny>())) + .Callback, Dictionary>((url, headers, data) => + { + headersSent = headers; + dataSent = data; + }); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + opentok.ForceMuteStream(sessionId, streamId); + + Assert.NotNull(headersSent); + Assert.Equal(new Dictionary { { "Content-Type", "application/json" } }, headersSent); + + Assert.Null(dataSent); + } + + [Fact] + public async Task ForceMuteStreamAsyncExceptionWithEmptySessionId() + { + string sessionId = ""; + string streamId = "1234567890"; + + OpenTok openTok = new OpenTok(ApiKey, ApiSecret); + + var exception = await Assert.ThrowsAsync(async () => await openTok.ForceMuteStreamAsync(sessionId, streamId)); + Assert.Contains("The sessionId cannot be empty.", exception.Message); + Assert.Equal("sessionId", exception.ParamName); + } + + [Fact] + public async Task ForceMuteStreamAsyncExceptionWithEmptyStreamId() + { + string sessionId = "1234567890"; + string streamId = ""; + + OpenTok openTok = new OpenTok(ApiKey, ApiSecret); + + var exception = await Assert.ThrowsAsync(async () => await openTok.ForceMuteStreamAsync(sessionId, streamId)); + Assert.Contains("The streamId cannot be empty.", exception.Message); + Assert.Equal("streamId", exception.ParamName); + } + + [Fact] + public async Task ForceMuteStreamAsyncCorrectUrl() + { + string sessionId = "SESSIONID"; + string streamId = "1234567890"; + + string expectedUrl = $"v2/project/{this.ApiKey}/session/{sessionId}/stream/{streamId}/mute"; + + var mockClient = new Mock(MockBehavior.Strict); + mockClient + .Setup(httpClient => httpClient.PostAsync(expectedUrl, It.IsAny>(), It.IsAny>())) + .ReturnsAsync("") + .Verifiable(); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + await opentok.ForceMuteStreamAsync(sessionId, streamId); + + mockClient.Verify(); + } + + [Fact] + public async Task ForceMuteSteamAsyncHeaderAndDataCorrect() + { + string sessionId = "SESSIONID"; + string streamId = "1234567890"; + + Dictionary headersSent = null; + Dictionary dataSent = null; + + var mockClient = new Mock(); + mockClient + .Setup(httpClient => httpClient.PostAsync(It.IsAny(), It.IsAny>(), It.IsAny>())) + .Callback, Dictionary>((url, headers, data) => + { + headersSent = headers; + dataSent = data; + }); + + OpenTok opentok = new OpenTok(ApiKey, ApiSecret); + opentok.Client = mockClient.Object; + await opentok.ForceMuteStreamAsync(sessionId, streamId); + + Assert.NotNull(headersSent); + Assert.Equal(new Dictionary { { "Content-Type", "application/json" } }, headersSent); + + Assert.Null(dataSent); + } + } +} \ No newline at end of file diff --git a/OpenTokTest/PlayDtmfTests.cs b/OpenTokTest/PlayDtmfTests.cs index ee1dbf6a..68a44c8a 100644 --- a/OpenTokTest/PlayDtmfTests.cs +++ b/OpenTokTest/PlayDtmfTests.cs @@ -27,7 +27,7 @@ public void PlayDfmtNoConnectionIdCorrectUrl() string sessionId = "SESSIONID"; string digits = "1234567890"; - var expectedUrl = $"v2/project//session/{sessionId}/play-dtmf"; + var expectedUrl = $"v2/project/{ApiKey}/session/{sessionId}/play-dtmf"; var mockClient = new Mock(MockBehavior.Strict); mockClient @@ -89,7 +89,7 @@ public async Task PlayDfmtAsyncNoConnectionIdCorrectUrl() string sessionId = "SESSIONID"; string digits = "1234567890"; - var expectedUrl = $"v2/project//session/{sessionId}/play-dtmf"; + var expectedUrl = $"v2/project/{ApiKey}/session/{sessionId}/play-dtmf"; var mockClient = new Mock(MockBehavior.Strict); mockClient From 48006ad4ca665ad8bc3c513ef5321e918aad6cce Mon Sep 17 00:00:00 2001 From: Jeff Swartz Date: Tue, 25 Jan 2022 01:34:55 -0800 Subject: [PATCH 6/8] Docs edits for v3.7.0 (#169) --- LICENSE | 2 +- OpenTok/OpenTok.cs | 2 +- README.md | 51 ++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/LICENSE b/LICENSE index a389d63b..0475932f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014-2016 TokBox, Inc. +Copyright (c) 2014-2022 TokBox, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/OpenTok/OpenTok.cs b/OpenTok/OpenTok.cs index 11fb71a9..f0b20d50 100644 --- a/OpenTok/OpenTok.cs +++ b/OpenTok/OpenTok.cs @@ -443,7 +443,7 @@ public StreamList ListStreams(string sessionId) } /// - /// Force disconnects a specific client connected to an OpenTok session. + /// Force a specific client to disconnect from an OpenTok session. /// /// The session ID corresponding to the session. /// The connectionId of the connection in a session. diff --git a/README.md b/README.md index 20723042..1d8f0c88 100644 --- a/README.md +++ b/README.md @@ -5,16 +5,18 @@ [![Build status](https://github.com/opentok/Opentok-.NET-SDK/workflows/.NET%20Core/badge.svg)](https://github.com/opentok/Opentok-.NET-SDK/actions?query=workflow%3A%22.NET+Core%22) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md) -The OpenTok .NET SDK lets you generate +The OpenTok .NET SDK provides methods for: + +* Generating [sessions](https://www.tokbox.com/opentok/tutorials/create-session/) and [tokens](https://www.tokbox.com/opentok/tutorials/create-token/) for -[OpenTok](https://www.tokbox.com/) applications that run on the .NET platform. The SDK also includes -support for working with [OpenTok archives](https://tokbox.com/opentok/tutorials/archiving). -working with [OpenTok live streaming broadcasts](https://tokbox.com/developer/guides/broadcast/live-streaming/), -[working with OpenTok SIP interconnect](https://tokbox.com/developer/guides/sip), -[sending signals to clients connected to a session](https://www.tokbox.com/developer/guides/signaling/), -and [disconnecting clients from sessions](https://tokbox.com/developer/guides/moderation/rest/). - +[OpenTok](https://www.tokbox.com/) applications that run on the .NET platform +* Working with [OpenTok archives](https://tokbox.com/opentok/tutorials/archiving) +* Working with [OpenTok live streaming broadcasts](https://tokbox.com/developer/guides/broadcast/live-streaming/) +* Working with [OpenTok SIP interconnect](https://tokbox.com/developer/guides/sip) +* [Sending signals to clients connected to a session](https://www.tokbox.com/developer/guides/signaling/) +* [Disconnecting clients from sessions](https://tokbox.com/developer/guides/moderation/rest/) +* [Forcing clients in a session to disconnect or mute published audio](https://tokbox.com/developer/guides/moderation/) ## Installation @@ -236,6 +238,39 @@ string connectionId = "CONNECTIONID"; OpenTok.Signal(sessionId, signalProperties, connectionId); ``` +### Working with live streaming broadcasts + +You can start a live-streaming broadcast of an OpenTok Session using a `OpenTokSDK.OpenTok` instance's +`StartBroadcast(sessionId, hls, rtmpList, resolution, maxDuration, layout)` method. This returns an +`OpenTokSDK.Broadcast` instance. + +Also see the documentation for the `Opentok.StopBroadcast()` and `OpenTok.GetBroadcast()` methods. + +### SIP + +You can connect a SIP platform to an OpenTok session using the +`Opentok.Dial(sessionId, token, sipUri, options)` or +`Opentok.DialAsync(sessionId, token, sipUri, options)` method. + +You can send DTMF digits to all participants an active OpenTok session, or to a specific client +connected to that session, using the `Opentok.PlayDTMF(sessionId, digits, connectionId)` or +`Opentok.PlayDTMFAsync(sessionId, digits, connectionId)` method. + +### Forcing clients in a session to disconnect or mute published audio + +You can force a specific client to disconnect from an OpenTok session using the +`Opentok.ForceDisconnect(sessionId, connectionId)` method. + +You can force the publisher of a specific stream to stop publishing audio using the +`Opentok.ForceMuteStream(sessionId, stream)` or `Opentok.ForceMuteStreamAsync(sessionId, stream)`method. + +You can force the publisher of all streams in a session ((except for an optional list of streams) +to stop publishing audio using the `Opentok.ForceMuteAll(sessionId, excludedStreamIds)` +or `Opentok.ForceMuteAllAsync(sessionId, excludedStreamIds)` method. +You can then disable the mute state of the session by calling the +`Opentok.DisableForceMute(sessionId)` or `Opentok.DisableForceMuteAsync(sessionId)` +method. + ### Changing the Timeout for Http Requests If you would like to adjust the timeouts for Http Requests sent by the client SDK you can by calling OpenTok.SetDefaultRequestTimeout(int timeout) - note timeout is in milliseconds From 1c2b71d3291fb56a3dc556baf52b1df51ef54c4d Mon Sep 17 00:00:00 2001 From: Matt Lethargic Date: Wed, 26 Jan 2022 10:44:33 +0000 Subject: [PATCH 7/8] Changing version number (#170) From 9a55b7daa517e83a006be3995fc0d9189de0ee16 Mon Sep 17 00:00:00 2001 From: Jeff Swartz Date: Wed, 26 Jan 2022 15:54:10 -0800 Subject: [PATCH 8/8] Docs corrections --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1d8f0c88..5db59c36 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ The OpenTok .NET SDK provides methods for: * Generating -[sessions](https://www.tokbox.com/opentok/tutorials/create-session/) and -[tokens](https://www.tokbox.com/opentok/tutorials/create-token/) for +[sessions](https://tokbox.com/developer/guides/create-session/) and +[tokens](https://tokbox.com/developer/guides/create-token/) for [OpenTok](https://www.tokbox.com/) applications that run on the .NET platform * Working with [OpenTok archives](https://tokbox.com/opentok/tutorials/archiving) * Working with [OpenTok live streaming broadcasts](https://tokbox.com/developer/guides/broadcast/live-streaming/)