From ada5f595778bd5f02045bdfe991f414cca21ae9a Mon Sep 17 00:00:00 2001 From: Xavier Pouyat Date: Wed, 24 Jul 2024 06:33:49 +0200 Subject: [PATCH 1/4] Unit tests for live event creation. Test presence of encoding property and SRT not used with pass-through. --- MK.IO.Tests/LiveEventsOperationsTests.cs | 195 +++++++++++++++++++++++ MK.IO/LiveEvent/LiveEventsOperations.cs | 22 ++- 2 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 MK.IO.Tests/LiveEventsOperationsTests.cs diff --git a/MK.IO.Tests/LiveEventsOperationsTests.cs b/MK.IO.Tests/LiveEventsOperationsTests.cs new file mode 100644 index 0000000..0401a68 --- /dev/null +++ b/MK.IO.Tests/LiveEventsOperationsTests.cs @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Moq; +using MK.IO.Models; +using MK.IO.Operations; +using Newtonsoft.Json; + +namespace MK.IO.Tests +{ + public class LiveEventsOperationsTests + { + private Mock _mockClient; + private readonly LiveEventProperties _properties; + + public LiveEventsOperationsTests() + { + _mockClient = new Mock("subscriptionname", Constants.jwtFakeToken); + _properties = new LiveEventProperties() + { + Encoding = new LiveEventEncoding { EncodingType = LiveEventEncodingType.PassthroughBasic } + }; + + _mockClient.Setup(client => client.CreateObjectPutAsync( + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .Returns(Task.FromResult("{}")) + .Verifiable("CreateObjectPutAsync was not called with the expected parameters."); + } + + [Theory] + [InlineData(null)] + public void Create_WithNull(string name) + { + // Arrange + var liveEventsOperations = new LiveEventsOperations(_mockClient.Object); + + // act & assert + Assert.Throws(() => liveEventsOperations.Create(name, "name", _properties)); + Assert.Throws(() => liveEventsOperations.Create("name", name, _properties)); + Assert.Throws(() => liveEventsOperations.Create("name", "name", null)); + } + + [Theory] + [InlineData("")] + public void Create_WithEmpty(string name) + { + // Arrange + var liveEventsOperations = new LiveEventsOperations(_mockClient.Object); + + // act & assert + Assert.Throws(() => liveEventsOperations.Create(name, "name", _properties)); + Assert.Throws(() => liveEventsOperations.Create("name", name, _properties)); + } + + [Theory] + [InlineData("name")] + public void Create_LiveEventSRTWithPasstroughError(string name) + { + // Arrange + var liveEventsOperations = new LiveEventsOperations(_mockClient.Object); + + var prop = new LiveEventProperties() + { + Encoding = new LiveEventEncoding { EncodingType = LiveEventEncodingType.PassthroughBasic }, + Input = new LiveEventInput { StreamingProtocol = LiveEventInputProtocol.SRT } + }; + + // act & assert + Assert.Throws(() => liveEventsOperations.Create(name, "francecentral", prop)); + } + + [Theory] + [InlineData("name")] + public void Create_LiveEventWithoutEncodingPropError(string name) + { + // Arrange + var liveEventsOperations = new LiveEventsOperations(_mockClient.Object); + + var prop = new LiveEventProperties() + { + Input = new LiveEventInput { StreamingProtocol = LiveEventInputProtocol.SRT } + }; + + // act & assert + Assert.Throws(() => liveEventsOperations.Create(name, "francecentral", prop)); + } + + [Theory] + [InlineData("name")] + public async Task Create_LiveEventSRTWithEncodingOK(string name) + { + // Arrange + var liveEventsOperations = new LiveEventsOperations(_mockClient.Object); + + var mop = new Mock(_mockClient.Object); + + var prop = new LiveEventProperties() + { + Encoding = new LiveEventEncoding { EncodingType = LiveEventEncodingType.Premium1080p }, + Input = new LiveEventInput { StreamingProtocol = LiveEventInputProtocol.SRT }, + }; + + // Act + await mop.Object.CreateAsync(name, "francecentral", prop); + + // Assert + mop.Verify(); // Verify that CreateOrUpdateAsync was called as expected + } + + [Theory] + [InlineData("name with space")] + [InlineData("kgMRYYUBi2Le3LAtuVDq220v4914MHud0Tmj6onzumNzkJP9gDPlZNmNunDp7lomsS0DUucyMAcSFzmxHFfH2wTgEVnCpzAamHMNTGyfsbk4WdB9LAlVPmmSlg3vhAduj2VQTsvohfWXqDiBTPaHoSHm9Zt4dbKWlAPxplgh3rdBiRVS45X9XELTgDC1bumc70icB4vQNVQ00cxcP9rkRNFXa2guqQQ5aZ0DiMGnN2qVXoXIvHD7rpdCDMD8WwxRWlaib")] // 261 chars + public void Create_LiveEventErrorInName(string name) + { + // Arrange + var liveEventsOperations = new LiveEventsOperations(_mockClient.Object); + + // act & assert + Assert.Throws(() => liveEventsOperations.Create(name, "francecentral", _properties)); + } + + [Theory] + [InlineData("name-123")] + [InlineData("name_123")] + [InlineData("name.123")] + [InlineData("nname--123")] + [InlineData("kgMRYYUBi2Le3LAtuVDq220v4914MHud0Tmj6onzumNzkJP9gDPlZNmNunDp7lomsS0DUucyMAcSFzmxHFfH2wTgEVnCpzAamHMNTGyfsbk4WdB9LAlVPmmSlg3vhAduj2VQTsvohfWXqDiBTPaHoSHm9Zt4dbKWlAPxplgh3rdBiRVS45X9XELTgDC1bumc70icB4vQNVQ00cxcP9rkRNFXa2guqQQ5aZ0DiMGnN2qVXoXIvHD7rpdCDMD8WwxRWlai")] // 260 chars + public async Task Create_LiveEventNameOK(string name) + { + // Arrange + var liveEventsOperations = new LiveEventsOperations(_mockClient.Object); + + var mop = new Mock(_mockClient.Object); + + // Act + await mop.Object.CreateAsync(name, "francecentral", _properties); + + // Assert + mop.Verify(); // Verify that CreateOrUpdateAsync was called as expected + } + + [Theory] + [InlineData("{\"name\":\"liveevent-6de535d0\",\"id\":\"/subscriptions/52907a2e-ab43-43ba-8b9f-8cb78285f665/resourceGroups/default/providers/Microsoft.Media/mediaservices/mkiotest/liveEvents/liveevent-6de535d0\",\"type\":\"Microsoft.Media/mediaservices/liveevents\",\"location\":\"francecentral\",\"tags\":{},\"properties\":{\"created\":\"2024-06-26T20:37:45.398939Z\",\"lastModified\":\"2024-07-23T08:02:06.178290Z\",\"useStaticHostname\":false,\"streamOptions\":[\"Default\"],\"input\":{\"accessControl\":{\"ip\":{\"allow\":[{\"name\":\"everyone\",\"address\":\"0.0.0.0\",\"subnetPrefixLength\":0}]}},\"endpoints\":[{\"url\":\"rtmp://in-742d58f3-3492-4909-a7d2-965a61157b10.francecentral.streaming.mediakind.com:1935/0dcea5b2-4eed-4e80-8e37-df951aa9138d\",\"protocol\":\"RTMP\"}],\"keyFrameIntervalDuration\":\"PT2S\",\"streamingProtocol\":\"RTMP\",\"accessToken\":\"0dcea5b2-4eed-4e80-8e37-df951aa9138d\"},\"encoding\":{\"encodingType\":\"PassthroughBasic\",\"presetName\":\"\",\"keyFrameInterval\":\"PT2S\",\"stretchMode\":\"AutoSize\"},\"crossSiteAccessPolicies\":{\"clientAccessPolicy\":null,\"crossDomainPolicy\":null},\"provisioningState\":\"Succeeded\",\"resourceState\":\"Stopped\",\"preview\":{\"accessControl\":{\"ip\":{\"allow\":[]}},\"streamingPolicyName\":\"Predefined_ClearStreamingOnly\",\"previewLocator\":\"03e93aca-0576-40e2-92c3-ce2f0f100998\",\"endpoints\":[{\"url\":\"https://liveevent-6de535d0-mkiotest-preview.francecentral.streaming.mediakind.com/03e93aca-0576-40e2-92c3-ce2f0f100998/manifest.mpd\",\"protocol\":\"FragmentedMP4\"}]}},\"systemData\":{\"createdBy\":\"email@domain.com\",\"createdByType\":\"User\",\"createdAt\":\"2024-06-26T20:37:45.398939Z\",\"lastModifiedBy\":\"email@domain.com\",\"lastModifiedByType\":\"User\",\"lastModifiedAt\":\"2024-07-23T08:02:06.178290Z\"},\"supplemental\":{\"operation\":\"get\",\"subscription\":{\"id\":\"bf747f59-771a-4e9b-a6cd-59351c4a71d2\",\"name\":\"mkiotest\"}}}")] + [InlineData("{\"name\":\"liveevent-srt\",\"id\":\"/subscriptions/52907a2e-ab43-43ba-8b9f-8cb78285f665/resourceGroups/default/providers/Microsoft.Media/mediaservices/mkiotest/liveEvents/liveevent-srt\",\"type\":\"Microsoft.Media/mediaservices/liveevents\",\"location\":\"francecentral\",\"tags\":{},\"properties\":{\"created\":\"2024-07-24T04:07:50.363640Z\",\"lastModified\":\"2024-07-24T04:08:23.339112Z\",\"useStaticHostname\":false,\"streamOptions\":[\"Default\"],\"input\":{\"accessControl\":{\"ip\":{\"allow\":[{\"name\":\"AllowAll\",\"address\":\"0.0.0.0\",\"subnetPrefixLength\":0}]}},\"endpoints\":[{\"url\":\"srt://in-35b482ce-0a1a-41f6-88aa-47dcf590d3c5.francecentral.streaming.mediakind.com:6000?passphrase=abcdgdhfqsfh45gdsqsdksdfn&pkbkeylen=16\",\"protocol\":\"SRT\"}],\"keyFrameIntervalDuration\":\"PT2S\",\"streamingProtocol\":\"SRT\",\"accessToken\":\"abcdgdhfqsfh45gdsqsdksdfn\"},\"encoding\":{\"encodingType\":\"Standard\",\"presetName\":\"Default720p\",\"keyFrameInterval\":\"PT2S\",\"stretchMode\":\"AutoSize\"},\"crossSiteAccessPolicies\":{\"clientAccessPolicy\":null,\"crossDomainPolicy\":null},\"provisioningState\":\"Succeeded\",\"resourceState\":\"Stopped\",\"preview\":{\"accessControl\":{\"ip\":{\"allow\":[{\"name\":\"AllowAll\",\"address\":\"0.0.0.0\",\"subnetPrefixLength\":0}]}},\"streamingPolicyName\":\"Predefined_ClearStreamingOnly\",\"previewLocator\":\"9550a87c-2561-4073-8c81-167418bcea89\",\"endpoints\":[{\"url\":\"https://liveevent-srt-mkiotest-preview.francecentral.streaming.mediakind.com/9550a87c-2561-4073-8c81-167418bcea89/manifest.mpd\",\"protocol\":\"FragmentedMP4\"}]}},\"systemData\":{\"createdBy\":\"user@domain.com\",\"createdByType\":\"User\",\"createdAt\":\"2024-07-24T04:07:50.363640Z\",\"lastModifiedBy\":\"user@domain.com\",\"lastModifiedByType\":\"User\",\"lastModifiedAt\":\"2024-07-24T04:08:23.339112Z\"},\"supplemental\":{\"operation\":\"get\",\"subscription\":{\"id\":\"bf747f59-771a-4e9b-a6cd-59351c4a71d2\",\"name\":\"mkiotest\"}}}")] + public async Task Create_DeserializationOK(string json) + { + var mockClient2 = new Mock("subscriptionname", Constants.jwtFakeToken); + + mockClient2.Setup(client => client.CreateObjectPutAsync( + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .Returns(Task.FromResult(json)) + .Verifiable("CreateObjectPutAsync was not called with the expected parameters."); + + var mop = new Mock(mockClient2.Object); + + // Act + await mop.Object.CreateAsync("name", "name", _properties); + + // Assert + mop.Verify(); + } + + [Theory] + [InlineData("{\"name\":\"liveevent-6de535d0\",\"id\":\"/subscriptions/52907a2e-ab43-43ba-8b9f-8cb78285f665/resourceGroups/default/providers/Microsoft.Media/mediaservices/mkiotest/liveEvents/liveevent-6de535d0\",\"type\":\"Microsoft.Media/mediaservices/liveevents\",\"location\":\"francecentral\",\"tags\":{},\"properties\":{\"created\":\"2024-06-26T20:37:45.398939Z\",\"lastModified\":\"2024-07-23T08:02:06.178290Z\",\"useStaticHostname\":false,\"streamOptions\":[\"Default\"],\"input\":{\"accessControl\":{\"ip\":{\"allow\":[{\"name\":\"everyone\",\"address\":\"0.0.0.0\",\"subnetPrefixLength\":0}]}},\"endpoints\":[{\"url\":\"rtmp://in-742d58f3-3492-4909-a7d2-965a61157b10.francecentral.streaming.mediakind.com:1935/0dcea5b2-4eed-4e80-8e37-df951aa9138d\",\"protocol\":\"RTMP\"}],\"keyFrameIntervalDuration\":\"PT2S\",\"streamingProtocol\":\"RTMPERROR\",\"accessToken\":\"0dcea5b2-4eed-4e80-8e37-df951aa9138d\"},\"encoding\":{\"encodingType\":\"PassthroughBasic\",\"presetName\":\"\",\"keyFrameInterval\":\"PT2S\",\"stretchMode\":\"AutoSize\"},\"crossSiteAccessPolicies\":{\"clientAccessPolicy\":null,\"crossDomainPolicy\":null},\"provisioningState\":\"Succeeded\",\"resourceState\":\"Stopped\",\"preview\":{\"accessControl\":{\"ip\":{\"allow\":[]}},\"streamingPolicyName\":\"Predefined_ClearStreamingOnly\",\"previewLocator\":\"03e93aca-0576-40e2-92c3-ce2f0f100998\",\"endpoints\":[{\"url\":\"https://liveevent-6de535d0-mkiotest-preview.francecentral.streaming.mediakind.com/03e93aca-0576-40e2-92c3-ce2f0f100998/manifest.mpd\",\"protocol\":\"FragmentedMP4\"}]}},\"systemData\":{\"createdBy\":\"email@domain.com\",\"createdByType\":\"User\",\"createdAt\":\"2024-06-26T20:37:45.398939Z\",\"lastModifiedBy\":\"email@domain.com\",\"lastModifiedByType\":\"User\",\"lastModifiedAt\":\"2024-07-23T08:02:06.178290Z\"},\"supplemental\":{\"operation\":\"get\",\"subscription\":{\"id\":\"bf747f59-771a-4e9b-a6cd-59351c4a71d2\",\"name\":\"mkiotest\"}}}")] + public void Create_DeserializationError(string json) + { + var mockClient2 = new Mock("subscriptionname", Constants.jwtFakeToken); + + mockClient2.Setup(client => client.CreateObjectPutAsync( + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .Returns(Task.FromResult(json)) + .Verifiable("CreateObjectPutAsync was not called with the expected parameters."); + + var mop = new Mock(mockClient2.Object); + + // act & assert + Assert.Throws(() => mop.Object.Create("name", "name", _properties)); + + // Assert + mop.Verify(); + } + } +} \ No newline at end of file diff --git a/MK.IO/LiveEvent/LiveEventsOperations.cs b/MK.IO/LiveEvent/LiveEventsOperations.cs index 37f2a4a..24d6154 100644 --- a/MK.IO/LiveEvent/LiveEventsOperations.cs +++ b/MK.IO/LiveEvent/LiveEventsOperations.cs @@ -143,7 +143,7 @@ public LiveEventSchema Create(string liveEventName, string location, LiveEventPr } /// - public async Task CreateAsync(string liveEventName, string location, LiveEventProperties properties, Dictionary? tags, CancellationToken cancellationToken = default) + public async Task CreateAsync(string liveEventName, string location, LiveEventProperties properties, Dictionary? tags = null, CancellationToken cancellationToken = default) { Argument.AssertNotNullOrEmpty(liveEventName, nameof(liveEventName)); Argument.AssertNotContainsSpace(liveEventName, nameof(liveEventName)); @@ -151,15 +151,25 @@ public async Task CreateAsync(string liveEventName, string loca Argument.AssertNotNullOrEmpty(location, nameof(location)); Argument.AssertNotNull(properties, nameof(properties)); - return await CreateOrUpdateAsync(liveEventName, location, properties, tags, Client.CreateObjectPutAsync, cancellationToken); - } + if (properties.Encoding == null) + { + throw new ArgumentException("Encoding property should not be null."); + } + + // SRT not supported for pass-through + if ( + (properties.Encoding.EncodingType == LiveEventEncodingType.PassthroughBasic || properties.Encoding.EncodingType == LiveEventEncodingType.PassthroughStandard) + && + (properties.Input != null && properties.Input.StreamingProtocol == LiveEventInputProtocol.SRT) + ) + { + throw new ArgumentException("SRT is not supported for pass-through encoding."); + } - internal async Task CreateOrUpdateAsync(string liveEventName, string location, LiveEventProperties properties, Dictionary? tags, Func> func, CancellationToken cancellationToken) - { var url = Client.GenerateApiUrl(_liveEventApiUrl, liveEventName); tags ??= new Dictionary(); var content = new LiveEventSchema { Location = location, Tags = tags, Properties = properties }; - string responseContent = await func(url, content.ToJson(), cancellationToken); + string responseContent = await Client.CreateObjectPutAsync(url, content.ToJson(), cancellationToken); return JsonConvert.DeserializeObject(responseContent, ConverterLE.Settings) ?? throw new Exception("Error with live event deserialization"); } From 0671c7e7c853166f96d90cdd510ef91b48b227fe Mon Sep 17 00:00:00 2001 From: Xavier Pouyat Date: Fri, 26 Jul 2024 04:25:21 +0200 Subject: [PATCH 2/4] Large update to reflect OpenAI definition from 7/25/2024 --- MK.IO.Tests/AssetsOperationsTests.cs | 49 +++++++++++ MK.IO.Tests/LiveEventsOperationsTests.cs | 55 +++++++++++- MK.IO/Argument.cs | 32 +++++++ MK.IO/Asset/AssetsOperations.cs | 5 ++ .../ContentKeyPoliciesOperations.cs | 2 + MK.IO/CsharpDotNet2/Model/HlsSettings.cs | 67 +++++++++++++++ MK.IO/CsharpDotNet2/Model/JobOutputAsset.cs | 4 +- MK.IO/CsharpDotNet2/Model/JobProperties.cs | 2 +- .../CsharpDotNet2/Model/LiveEventEncoding.cs | 2 +- MK.IO/CsharpDotNet2/Model/LiveEventInput.cs | 5 +- MK.IO/CsharpDotNet2/Model/LiveEventPreview.cs | 2 +- MK.IO/CsharpDotNet2/Model/LiveOutputHls.cs | 2 +- .../Model/PresentationTimeRange.cs | 2 +- .../Model/StreamingEndpointProperties.cs | 2 +- .../Model/StreamingLocatorContentKey.cs | 4 +- .../Model/TransformProperties.cs | 8 -- MK.IO/Job/Models/AbsoluteClipTime.cs | 4 +- MK.IO/Job/Models/JobInputAsset.cs | 16 ++-- MK.IO/Job/Models/UtcClipTime.cs | 6 +- MK.IO/LiveEvent/LiveEventsOperations.cs | 5 +- .../Models/LiveEventInputProtocol.cs | 8 +- MK.IO/LiveOutput/LiveOutputsOperations.cs | 5 +- MK.IO/MKIOClient.cs | 15 ++-- .../StorageAccountsOperations.cs | 1 + .../StreamingEndpointsOperations.cs | 4 +- .../StreamingLocatorsOperations.cs | 1 + MK.IO/Transform/Models/AIPipelinePreset.cs | 49 +++++++++++ .../Models/ThumbnailGeneratorConfiguration.cs | 8 +- .../Models/ThumbnailGeneratorPreset.cs | 3 + MK.IO/Transform/Models/TrackInserter.cs | 21 +++++ MK.IO/Transform/Models/TrackInserterPreset.cs | 75 +++++++++++++++++ .../Models/TrackInserterPresetTextTrack.cs | 84 +++++++++++++++++++ ...InserterPresetTextTrackPlayerVisibility.cs | 23 +++++ MK.IO/Transform/Models/TransformPreset.cs | 2 + 34 files changed, 522 insertions(+), 51 deletions(-) create mode 100644 MK.IO/CsharpDotNet2/Model/HlsSettings.cs create mode 100644 MK.IO/Transform/Models/AIPipelinePreset.cs create mode 100644 MK.IO/Transform/Models/TrackInserter.cs create mode 100644 MK.IO/Transform/Models/TrackInserterPreset.cs create mode 100644 MK.IO/Transform/Models/TrackInserterPresetTextTrack.cs create mode 100644 MK.IO/Transform/Models/TrackInserterPresetTextTrackPlayerVisibility.cs diff --git a/MK.IO.Tests/AssetsOperationsTests.cs b/MK.IO.Tests/AssetsOperationsTests.cs index 8f89ef0..9f1bc1e 100644 --- a/MK.IO.Tests/AssetsOperationsTests.cs +++ b/MK.IO.Tests/AssetsOperationsTests.cs @@ -4,6 +4,7 @@ using Moq; using MK.IO.Operations; using Newtonsoft.Json; +using System.Xml.Linq; namespace MK.IO.Tests { @@ -134,5 +135,53 @@ public void Create_DeserializationError(string json) // Assert mockClient2.Verify(); // Verify that CreateOrUpdateAsync was called as expected } + + [Theory] + [InlineData(256,33)] + [InlineData(257,32)] + public void Create_AssetErrorInTags(int sizeValue, int numberEntries) + { + // Arrange + var assetsOperations = new AssetsOperations(mockClient.Object); + + var tags = new Dictionary(); + for (int i = 0; i < numberEntries; i++) + { + tags.Add(MKIOClient.GenerateUniqueName(null, sizeValue), MKIOClient.GenerateUniqueName(null, sizeValue)); + } + + // act & assert + Assert.Throws(() => assetsOperations.CreateOrUpdate("name", "containername", "storagename", labels: tags)); + } + + [Theory] + [InlineData(256, 32)] + public void Create_AssetNoErrorInTags(int sizeValue, int numberEntries) + { + var mockClient2 = new Mock("subscriptionname", Constants.jwtFakeToken); + + var tags = new Dictionary(); + for (int i = 0; i < numberEntries; i++) + { + tags.Add(MKIOClient.GenerateUniqueName(null, sizeValue), MKIOClient.GenerateUniqueName(null, sizeValue)); + } + + mockClient2.Setup(client => client.CreateObjectPutAsync( + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .Returns(Task.FromResult("{}")) + .Verifiable("CreateObjectPutAsync was not called with the expected parameters."); + + var mop = new Mock(mockClient2.Object); + + // Act + mop.Object.CreateOrUpdate("name", "containername", "storagename", labels: tags); + + // Assert + mockClient2.Verify(); // Verify that CreateOrUpdateAsync was called as expected + } } } \ No newline at end of file diff --git a/MK.IO.Tests/LiveEventsOperationsTests.cs b/MK.IO.Tests/LiveEventsOperationsTests.cs index 0401a68..d428d39 100644 --- a/MK.IO.Tests/LiveEventsOperationsTests.cs +++ b/MK.IO.Tests/LiveEventsOperationsTests.cs @@ -5,6 +5,7 @@ using MK.IO.Models; using MK.IO.Operations; using Newtonsoft.Json; +using System.Xml.Linq; namespace MK.IO.Tests { @@ -112,8 +113,9 @@ public async Task Create_LiveEventSRTWithEncodingOK(string name) } [Theory] + [InlineData("n")] // 1 char [InlineData("name with space")] - [InlineData("kgMRYYUBi2Le3LAtuVDq220v4914MHud0Tmj6onzumNzkJP9gDPlZNmNunDp7lomsS0DUucyMAcSFzmxHFfH2wTgEVnCpzAamHMNTGyfsbk4WdB9LAlVPmmSlg3vhAduj2VQTsvohfWXqDiBTPaHoSHm9Zt4dbKWlAPxplgh3rdBiRVS45X9XELTgDC1bumc70icB4vQNVQ00cxcP9rkRNFXa2guqQQ5aZ0DiMGnN2qVXoXIvHD7rpdCDMD8WwxRWlaib")] // 261 chars + [InlineData("2VQTsvohfWXqDiBTPaHoSHm9Zt4dbKWlb")] // 33 chars public void Create_LiveEventErrorInName(string name) { // Arrange @@ -124,11 +126,12 @@ public void Create_LiveEventErrorInName(string name) } [Theory] + [InlineData("n1")] [InlineData("name-123")] [InlineData("name_123")] [InlineData("name.123")] [InlineData("nname--123")] - [InlineData("kgMRYYUBi2Le3LAtuVDq220v4914MHud0Tmj6onzumNzkJP9gDPlZNmNunDp7lomsS0DUucyMAcSFzmxHFfH2wTgEVnCpzAamHMNTGyfsbk4WdB9LAlVPmmSlg3vhAduj2VQTsvohfWXqDiBTPaHoSHm9Zt4dbKWlAPxplgh3rdBiRVS45X9XELTgDC1bumc70icB4vQNVQ00cxcP9rkRNFXa2guqQQ5aZ0DiMGnN2qVXoXIvHD7rpdCDMD8WwxRWlai")] // 260 chars + [InlineData("2VQTsvohfWXqDiBTPaHoSHm9Zt4dbKWl")] // 32 chars public async Task Create_LiveEventNameOK(string name) { // Arrange @@ -191,5 +194,53 @@ public void Create_DeserializationError(string json) // Assert mop.Verify(); } + + [Theory] + [InlineData(64, 17)] + [InlineData(65, 16)] + public void Create_LiveEventErrorInTags(int sizeValue, int numberEntries) + { + // Arrange + var liveEventsOperations = new LiveEventsOperations(_mockClient.Object); + + var tags = new Dictionary(); + for (int i = 0; i < numberEntries; i++) + { + tags.Add(MKIOClient.GenerateUniqueName(null, sizeValue), MKIOClient.GenerateUniqueName(null, sizeValue)); + } + + // act & assert + Assert.Throws(() => liveEventsOperations.Create("name", "francecentral", _properties, tags)); + } + + [Theory] + [InlineData(64, 16)] + public void Create_LiveEventNoErrorInTags(int sizeValue, int numberEntries) + { + var mockClient2 = new Mock("subscriptionname", Constants.jwtFakeToken); + + mockClient2.Setup(client => client.CreateObjectPutAsync( + It.IsAny(), + It.IsAny(), + It.IsAny() + ) + ) + .Returns(Task.FromResult("{}")) + .Verifiable("CreateObjectPutAsync was not called with the expected parameters."); + + var mop = new Mock(mockClient2.Object); + + var tags = new Dictionary(); + for (int i = 0; i < numberEntries; i++) + { + tags.Add(MKIOClient.GenerateUniqueName(null, sizeValue), MKIOClient.GenerateUniqueName(null, sizeValue)); + } + + // Act + mop.Object.Create("name", "name", _properties, tags); + + // Assert + mop.Verify(); + } } } \ No newline at end of file diff --git a/MK.IO/Argument.cs b/MK.IO/Argument.cs index c6b1d10..b3fdeb1 100644 --- a/MK.IO/Argument.cs +++ b/MK.IO/Argument.cs @@ -72,6 +72,21 @@ public static void AssertNotMoreThanLength(string? value, string name, int lengt } } + /// + /// Asserts that the value does not have a length lower than the specified value. + /// + /// + /// + /// + /// + public static void AssertNotLessThanLength(string? value, string name, int length) + { + if (value != null && value.Length < length) + { + throw new ArgumentException($"Value length cannot be less than {length}.", name); + } + } + /// /// Assert that value is conform to regex pattern. /// @@ -104,5 +119,22 @@ public static void AssertJwtToken(string authToken, string name) throw new ArgumentException("Value is not a JWT Token. Please read https://docs.mk.io/docs/personal-access-tokens to learn how to generate a personal access token.", name); } } + + internal static void AssertTagsNullOrCompliant(Dictionary? tags, int maxNumber, int maxLength) + { + if (tags != null) + { + if (tags.Count > maxNumber) + { + throw new ArgumentException($"Tags are limited to {maxNumber} entries."); + } + + foreach (var tag in tags) + { + AssertNotMoreThanLength(tag.Key, "tag.Key", maxLength); + AssertNotMoreThanLength(tag.Value, "tag.Value", maxLength); + } + } + } } } \ No newline at end of file diff --git a/MK.IO/Asset/AssetsOperations.cs b/MK.IO/Asset/AssetsOperations.cs index a6a470d..640b20f 100644 --- a/MK.IO/Asset/AssetsOperations.cs +++ b/MK.IO/Asset/AssetsOperations.cs @@ -121,8 +121,13 @@ public async Task CreateOrUpdateAsync(string assetName, string? con Argument.AssertNotNullOrEmpty(assetName, nameof(assetName)); Argument.AssertNotMoreThanLength(assetName, nameof(assetName), 260); Argument.AssertNotMoreThanLength(containerName, nameof(containerName), 63); + Argument.AssertNotLessThanLength(containerName, nameof(containerName), 3); Argument.AssertRespectRegex(containerName, nameof(containerName), @"^(?=.{3,63}$)[a-z0-9]+(-[a-z0-9]+)*$"); Argument.AssertNotNullOrEmpty(storageName, nameof(storageName)); + Argument.AssertNotMoreThanLength(storageName, nameof(storageName), 255); + Argument.AssertNotMoreThanLength(description, nameof(description), 4096); + Argument.AssertNotMoreThanLength(alternateId, nameof(alternateId), 64); + Argument.AssertTagsNullOrCompliant(labels, 32, 256); var url = Client.GenerateApiUrl(_assetApiUrl, assetName); AssetSchema content = new() diff --git a/MK.IO/ContentKeyPolicy/ContentKeyPoliciesOperations.cs b/MK.IO/ContentKeyPolicy/ContentKeyPoliciesOperations.cs index 678c06e..813dbc3 100644 --- a/MK.IO/ContentKeyPolicy/ContentKeyPoliciesOperations.cs +++ b/MK.IO/ContentKeyPolicy/ContentKeyPoliciesOperations.cs @@ -140,7 +140,9 @@ public async Task CreateAsync(string contentKeyPolicyNam { Argument.AssertNotNullOrEmpty(contentKeyPolicyName, nameof(contentKeyPolicyName)); Argument.AssertNotContainsSpace(contentKeyPolicyName, nameof(contentKeyPolicyName)); + Argument.AssertNotMoreThanLength(contentKeyPolicyName, nameof(contentKeyPolicyName), 260); Argument.AssertNotNull(properties, nameof(properties)); + Argument.AssertNotMoreThanLength(properties.Description, nameof(properties.Description), 1024); var url = Client.GenerateApiUrl(_contentKeyPolicyApiUrl, contentKeyPolicyName); var content = new ContentKeyPolicySchema { Properties = properties }; diff --git a/MK.IO/CsharpDotNet2/Model/HlsSettings.cs b/MK.IO/CsharpDotNet2/Model/HlsSettings.cs new file mode 100644 index 0000000..99c0523 --- /dev/null +++ b/MK.IO/CsharpDotNet2/Model/HlsSettings.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Newtonsoft.Json; +using System.Runtime.Serialization; +using System.Text; + +namespace MK.IO.Models +{ + + /// + /// + /// + [DataContract] + public class HlsSettings + { + /// + /// The characteristics for the HLS setting. + /// + /// The characteristics for the HLS setting. + [DataMember(Name = "characteristics", EmitDefaultValue = false)] + [JsonProperty(PropertyName = "characteristics")] + public string Characteristics { get; set; } + + /// + /// Default track? + /// + /// Default track? + [DataMember(Name = "default", EmitDefaultValue = false)] + [JsonProperty(PropertyName = "default")] + public bool? Default { get; set; } = false; + + /// + /// Forced track? + /// + /// Forced track? + [DataMember(Name = "forced", EmitDefaultValue = false)] + [JsonProperty(PropertyName = "forced")] + public bool? Forced { get; set; } = false; + + + /// + /// Get the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append("class HlsSettings {\n"); + sb.Append(" Characteristics: ").Append(Characteristics).Append("\n"); + sb.Append(" Default: ").Append(Default).Append("\n"); + sb.Append(" Forced: ").Append(Forced).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Get the JSON string presentation of the object + /// + /// JSON string presentation of the object + public string ToJson() + { + return JsonConvert.SerializeObject(this, Formatting.Indented); + } + + } +} diff --git a/MK.IO/CsharpDotNet2/Model/JobOutputAsset.cs b/MK.IO/CsharpDotNet2/Model/JobOutputAsset.cs index 7590fea..31384b1 100644 --- a/MK.IO/CsharpDotNet2/Model/JobOutputAsset.cs +++ b/MK.IO/CsharpDotNet2/Model/JobOutputAsset.cs @@ -59,7 +59,7 @@ public class JobOutputAsset /// If the JobOutput is in a Processing state, this contains the Job completion percentage. The value is an estimate and not intended to be used to predict Job completion times. [DataMember(Name = "progress", EmitDefaultValue = false)] [JsonProperty(PropertyName = "progress")] - public int? Progress { get; set; } + public int? Progress { get; set; } = 0; /// /// The UTC date and time at which this Output began processing. @@ -75,7 +75,7 @@ public class JobOutputAsset /// The current state of the job. [DataMember(Name = "state", EmitDefaultValue = false)] [JsonProperty(PropertyName = "state")] - public string State { get; set; } + public JobState State { get; set; } = JobState.Queued; /// diff --git a/MK.IO/CsharpDotNet2/Model/JobProperties.cs b/MK.IO/CsharpDotNet2/Model/JobProperties.cs index 9405e01..9ce63d0 100644 --- a/MK.IO/CsharpDotNet2/Model/JobProperties.cs +++ b/MK.IO/CsharpDotNet2/Model/JobProperties.cs @@ -92,7 +92,7 @@ public class JobProperties /// The current state of the job. [DataMember(Name = "state", EmitDefaultValue = false)] [JsonProperty(PropertyName = "state")] - public JobState State { get; private set; } + public JobState State { get; private set; } = JobState.Queued; /// diff --git a/MK.IO/CsharpDotNet2/Model/LiveEventEncoding.cs b/MK.IO/CsharpDotNet2/Model/LiveEventEncoding.cs index 5041d63..b1a487d 100644 --- a/MK.IO/CsharpDotNet2/Model/LiveEventEncoding.cs +++ b/MK.IO/CsharpDotNet2/Model/LiveEventEncoding.cs @@ -28,7 +28,7 @@ public class LiveEventEncoding /// Use an ISO 8601 time value between 1 and 10 seconds to specify the output fragment length for the video and audio tracks of an encoding live event. For example, use PT2S to indicate 2 seconds. For the video track it also defines the key frame interval, or the length of a GoP (group of pictures). If this value is not set for an encoding live event, the fragment duration defaults to 2 seconds. The value cannot be set for pass-through live events. [DataMember(Name = "keyFrameInterval", EmitDefaultValue = false)] [JsonProperty(PropertyName = "keyFrameInterval")] - public TimeSpan? KeyFrameInterval { get; set; } + public TimeSpan? KeyFrameInterval { get; set; } = TimeSpan.FromSeconds(2); /// /// Defaults to either Default720p or Default1080p depending on encoding type. May be used to specify alternative encoding templates - contact support for assistance if your needs are complex. diff --git a/MK.IO/CsharpDotNet2/Model/LiveEventInput.cs b/MK.IO/CsharpDotNet2/Model/LiveEventInput.cs index b996d99..147badc 100644 --- a/MK.IO/CsharpDotNet2/Model/LiveEventInput.cs +++ b/MK.IO/CsharpDotNet2/Model/LiveEventInput.cs @@ -54,14 +54,13 @@ public class LiveEventInput /// /// The input protocol for the live event. /// This is specified at creation time and cannot be updated. - /// Must be one of RTMP or SRT. FragmentedMp4 is not supported. /// /// The input protocol for the live event. /// This is specified at creation time and cannot be updated. - /// Must be one of RTMP or SRT. FragmentedMp4 is not supported. + /// [DataMember(Name = "streamingProtocol", EmitDefaultValue = false)] [JsonProperty(PropertyName = "streamingProtocol")] - public LiveEventInputProtocol StreamingProtocol { get; set; } + public LiveEventInputProtocol StreamingProtocol { get; set; } = LiveEventInputProtocol.RTMP; /// /// The metadata endpoints for the live event. diff --git a/MK.IO/CsharpDotNet2/Model/LiveEventPreview.cs b/MK.IO/CsharpDotNet2/Model/LiveEventPreview.cs index af4f147..86cf3ef 100644 --- a/MK.IO/CsharpDotNet2/Model/LiveEventPreview.cs +++ b/MK.IO/CsharpDotNet2/Model/LiveEventPreview.cs @@ -51,7 +51,7 @@ public class LiveEventPreview /// The name of the DRM streaming policy for the live event preview. Defaults to Predefined_ClearStreamingOnly and no other value is presently supported. [DataMember(Name = "streamingPolicyName", EmitDefaultValue = false)] [JsonProperty(PropertyName = "streamingPolicyName")] - public string StreamingPolicyName { get; set; } + public string StreamingPolicyName { get; set; } = PredefinedStreamingPolicy.ClearStreamingOnly; /// diff --git a/MK.IO/CsharpDotNet2/Model/LiveOutputHls.cs b/MK.IO/CsharpDotNet2/Model/LiveOutputHls.cs index 85ce4a3..1f1dfd3 100644 --- a/MK.IO/CsharpDotNet2/Model/LiveOutputHls.cs +++ b/MK.IO/CsharpDotNet2/Model/LiveOutputHls.cs @@ -20,7 +20,7 @@ public class LiveOutputHls /// The number of fragments per HLS segment. [DataMember(Name = "fragmentsPerTsSegment", EmitDefaultValue = false)] [JsonProperty(PropertyName = "fragmentsPerTsSegment")] - public int? FragmentsPerTsSegment { get; set; } + public int? FragmentsPerTsSegment { get; set; } = 1; /// diff --git a/MK.IO/CsharpDotNet2/Model/PresentationTimeRange.cs b/MK.IO/CsharpDotNet2/Model/PresentationTimeRange.cs index 65c3693..5a293a0 100644 --- a/MK.IO/CsharpDotNet2/Model/PresentationTimeRange.cs +++ b/MK.IO/CsharpDotNet2/Model/PresentationTimeRange.cs @@ -60,7 +60,7 @@ public class PresentationTimeRange /// Defines the unit of time for all the values in this object. The value is expressed in ticks per second. The default value of 10,000,000 increments per second (or 10 MHz) is used if this parameter is not specified. IF you're a video engineer doing Serious Business, consider setting this to 48,000 or 90,000 representing 90Khz and 48Khz respectively. If you're a mere mortal, a value of 1 is sensible and would represent seconds. For example, with a timescale set to \"1\", the startTimestamp and endTimestamp values are expressed in seconds. So a startTimestamp of 10 and an endTimestamp of 20 would represent a 10 second window of content. Segments overlapping this window would be included in the output. [DataMember(Name = "timescale", EmitDefaultValue = false)] [JsonProperty(PropertyName = "timescale")] - public int? Timescale { get; set; } + public int? Timescale { get; set; } = 10000000; /// diff --git a/MK.IO/CsharpDotNet2/Model/StreamingEndpointProperties.cs b/MK.IO/CsharpDotNet2/Model/StreamingEndpointProperties.cs index 1cc903f..71aa0d8 100644 --- a/MK.IO/CsharpDotNet2/Model/StreamingEndpointProperties.cs +++ b/MK.IO/CsharpDotNet2/Model/StreamingEndpointProperties.cs @@ -35,7 +35,7 @@ public class StreamingEndpointProperties /// Indicates if CDN is enabled for the streaming endpoint. [DataMember(Name = "cdnEnabled", EmitDefaultValue = false)] [JsonProperty(PropertyName = "cdnEnabled")] - public bool? CdnEnabled { get; set; } + public bool? CdnEnabled { get; set; } = false; /// /// If CDN is enabled, the optional CDN profile name for the streaming endpoint. diff --git a/MK.IO/CsharpDotNet2/Model/StreamingLocatorContentKey.cs b/MK.IO/CsharpDotNet2/Model/StreamingLocatorContentKey.cs index fa3e241..6fa910b 100644 --- a/MK.IO/CsharpDotNet2/Model/StreamingLocatorContentKey.cs +++ b/MK.IO/CsharpDotNet2/Model/StreamingLocatorContentKey.cs @@ -36,7 +36,7 @@ public class StreamingLocatorContentKey /// The name of the policy [DataMember(Name = "policyName", EmitDefaultValue = false)] [JsonProperty(PropertyName = "policyName")] - public string PolicyName { get; set; } + public string PolicyName { get; private set; } /// /// Not currently supported. The tracks which use this content key @@ -52,7 +52,7 @@ public class StreamingLocatorContentKey /// The streaming locator content key type [DataMember(Name = "type", EmitDefaultValue = false)] [JsonProperty(PropertyName = "type")] - public StreamingLocatorContentKeyType Type { get; set; } + public StreamingLocatorContentKeyType Type { get; private set; } /// /// The value of the content key diff --git a/MK.IO/CsharpDotNet2/Model/TransformProperties.cs b/MK.IO/CsharpDotNet2/Model/TransformProperties.cs index 814478b..e24802c 100644 --- a/MK.IO/CsharpDotNet2/Model/TransformProperties.cs +++ b/MK.IO/CsharpDotNet2/Model/TransformProperties.cs @@ -46,13 +46,6 @@ public class TransformProperties [JsonProperty(PropertyName = "outputs")] public List Outputs { get; set; } - /// - /// Gets or Sets Pipeline - /// - [DataMember(Name = "pipeline", EmitDefaultValue = false)] - [JsonProperty(PropertyName = "pipeline")] - public PipelineArguments Pipeline { get; set; } - /// /// Get the string presentation of the object @@ -66,7 +59,6 @@ public override string ToString() sb.Append(" Description: ").Append(Description).Append("\n"); sb.Append(" LastModified: ").Append(LastModified).Append("\n"); sb.Append(" Outputs: ").Append(Outputs).Append("\n"); - sb.Append(" Pipeline: ").Append(Pipeline).Append("\n"); sb.Append("}\n"); return sb.ToString(); } diff --git a/MK.IO/Job/Models/AbsoluteClipTime.cs b/MK.IO/Job/Models/AbsoluteClipTime.cs index db4472a..cc42c79 100644 --- a/MK.IO/Job/Models/AbsoluteClipTime.cs +++ b/MK.IO/Job/Models/AbsoluteClipTime.cs @@ -22,9 +22,9 @@ public AbsoluteClipTime(TimeSpan time) internal override string OdataType => "#Microsoft.Media.AbsoluteClipTime"; /// - /// The time position on the timeline of the input media. It is usually specified as an ISO8601 period. e.g PT30S for 30 seconds. + /// The time position on the timeline of the input media. Specified as an ISO8601 period. e.g PT30S for 30 seconds. /// - /// The time position on the timeline of the input media. It is usually specified as an ISO8601 period. e.g PT30S for 30 seconds. + /// The time position on the timeline of the input media. Specified as an ISO8601 period. e.g PT30S for 30 seconds. [DataMember(Name = "time", EmitDefaultValue = false)] [JsonProperty(PropertyName = "time")] public TimeSpan Time { get; set; } diff --git a/MK.IO/Job/Models/JobInputAsset.cs b/MK.IO/Job/Models/JobInputAsset.cs index 43bf942..bc2d558 100644 --- a/MK.IO/Job/Models/JobInputAsset.cs +++ b/MK.IO/Job/Models/JobInputAsset.cs @@ -25,16 +25,16 @@ public JobInputAsset(string assetName, List files) public string AssetName { get; set; } /// - /// (NOT IMPLEMENTED) Defines a point on the timeline of the input media at which processing will end. Defaults to the end of the input media. + /// Defines a point on the timeline of the input media at which processing will end. Defaults to the end of the input media. /// - /// (NOT IMPLEMENTED) Defines a point on the timeline of the input media at which processing will end. Defaults to the end of the input media. + /// Defines a point on the timeline of the input media at which processing will end. Defaults to the end of the input media. [JsonProperty(PropertyName = "end")] - [Obsolete] public JobInputTime? End { get; set; } + public JobInputTime? End { get; set; } /// - /// List of files. Required for JobInputAsset. + /// List of files. /// - /// List of files. Required for JobInputAsset. + /// List of files. [JsonProperty(PropertyName = "files")] public List Files { get; set; } @@ -53,10 +53,10 @@ public JobInputAsset(string assetName, List files) public string Label { get; set; } /// - /// (NOT IMPLEMENTED) Defines a point on the timeline of the input media at which processing will start. Defaults to the beginning of the input media. + /// Defines a point on the timeline of the input media at which processing will start. Defaults to the beginning of the input media. /// - /// (NOT IMPLEMENTED) Defines a point on the timeline of the input media at which processing will start. Defaults to the beginning of the input media. + /// Defines a point on the timeline of the input media at which processing will start. Defaults to the beginning of the input media. [JsonProperty("start")] - [Obsolete] public JobInputTime? Start { get; set; } + public JobInputTime? Start { get; set; } } } \ No newline at end of file diff --git a/MK.IO/Job/Models/UtcClipTime.cs b/MK.IO/Job/Models/UtcClipTime.cs index f0787b8..9b8260a 100644 --- a/MK.IO/Job/Models/UtcClipTime.cs +++ b/MK.IO/Job/Models/UtcClipTime.cs @@ -22,12 +22,12 @@ public UtcClipTime(DateTime time) internal override string OdataType => "#Microsoft.Media.UtcClipTime"; /// - /// The time position on the timeline of the input media based on Utc time. + /// The time position on the timeline of the input media based on UTC time. /// - /// The time position on the timeline of the input media based on Utc time. + /// The time position on the timeline of the input media based on UTC time. [DataMember(Name = "time", EmitDefaultValue = false)] [JsonProperty(PropertyName = "time")] - public DateTime Time { get; set; } + public DateTime? Time { get; set; } /// diff --git a/MK.IO/LiveEvent/LiveEventsOperations.cs b/MK.IO/LiveEvent/LiveEventsOperations.cs index 24d6154..0d3d392 100644 --- a/MK.IO/LiveEvent/LiveEventsOperations.cs +++ b/MK.IO/LiveEvent/LiveEventsOperations.cs @@ -147,9 +147,12 @@ public async Task CreateAsync(string liveEventName, string loca { Argument.AssertNotNullOrEmpty(liveEventName, nameof(liveEventName)); Argument.AssertNotContainsSpace(liveEventName, nameof(liveEventName)); - Argument.AssertNotMoreThanLength(liveEventName, nameof(liveEventName), 260); + Argument.AssertNotLessThanLength(liveEventName, nameof(liveEventName), 2); + Argument.AssertNotMoreThanLength(liveEventName, nameof(liveEventName), 32); Argument.AssertNotNullOrEmpty(location, nameof(location)); Argument.AssertNotNull(properties, nameof(properties)); + Argument.AssertNotMoreThanLength(properties.Description, nameof(properties.Description), 4096); + Argument.AssertTagsNullOrCompliant(tags, 16, 64); if (properties.Encoding == null) { diff --git a/MK.IO/LiveEvent/Models/LiveEventInputProtocol.cs b/MK.IO/LiveEvent/Models/LiveEventInputProtocol.cs index 38368bd..fbc5c39 100644 --- a/MK.IO/LiveEvent/Models/LiveEventInputProtocol.cs +++ b/MK.IO/LiveEvent/Models/LiveEventInputProtocol.cs @@ -11,7 +11,7 @@ namespace MK.IO.Models /// The input protocol for the live event. /// This is specified at creation time and cannot be updated. /// - /// Must be one of RTMP or SRT. fmp4 smooth input is not supported. + /// The input protocol for the live event. This is specified at creation time and cannot be updated. [JsonConverter(typeof(StringEnumConverter))] public enum LiveEventInputProtocol { @@ -21,6 +21,12 @@ public enum LiveEventInputProtocol [EnumMember(Value = "RTMP")] RTMP, + /// + /// Enum RTMPS for value: RTMPS + /// + [EnumMember(Value = "RTMPS")] + RTMPS, + /// /// Enum SRT for value: SRT /// diff --git a/MK.IO/LiveOutput/LiveOutputsOperations.cs b/MK.IO/LiveOutput/LiveOutputsOperations.cs index c8f5862..0658649 100644 --- a/MK.IO/LiveOutput/LiveOutputsOperations.cs +++ b/MK.IO/LiveOutput/LiveOutputsOperations.cs @@ -127,11 +127,12 @@ public async Task CreateAsync(string liveEventName, string liv { Argument.AssertNotNullOrEmpty(liveEventName, nameof(liveEventName)); Argument.AssertNotNullOrEmpty(liveOutputName, nameof(liveOutputName)); - Argument.AssertNotMoreThanLength(liveOutputName, nameof(liveOutputName), 260); + Argument.AssertNotMoreThanLength(liveOutputName, nameof(liveOutputName), 256); Argument.AssertNotNull(properties, nameof(properties)); + Argument.AssertNotMoreThanLength(properties.Description, nameof(properties.Description), 4096); + Argument.AssertRespectRegex(properties.ManifestName, nameof(properties.ManifestName), @"^[a-zA-Z0-9\\-]+[a-zA-Z0-9]$"); var url = Client.GenerateApiUrl(_liveOutputApiUrl, liveEventName, liveOutputName); - //tags ??= new Dictionary(); var content = new LiveOutputSchema { Properties = properties }; string responseContent = await Client.CreateObjectPutAsync(url, content.ToJson(), cancellationToken); return JsonConvert.DeserializeObject(responseContent, ConverterLE.Settings) ?? throw new Exception("Error with live output deserialization"); diff --git a/MK.IO/MKIOClient.cs b/MK.IO/MKIOClient.cs index 0ffae15..92679aa 100644 --- a/MK.IO/MKIOClient.cs +++ b/MK.IO/MKIOClient.cs @@ -421,16 +421,19 @@ internal static string AddParametersToUrl(string url, string name, string? value /// /// Generates a unique name based on a prefix. Useful for creating unique names for assets, locators, etc. /// - /// Prefix of the name - /// Lenght of the unique name (without the '-' before) + /// Prefix of the name (optional) + /// Length of the unique name after the prefix (and '-'). For example, with 8 and a prefix 'asset', name will be something like 'asset-12345678' /// - public static string GenerateUniqueName(string prefix, int length = 8) + public static string GenerateUniqueName(string? prefix, int length = 8) { // return a string of length "length" containing random characters + string unique = Guid.NewGuid().ToString("N"); - return (string.IsNullOrEmpty(prefix) ? string.Empty : prefix + "-") + Guid.NewGuid().ToString("N").Substring(0, length); + while (unique.Length < length) + { + unique += Guid.NewGuid().ToString("N"); + } + return (string.IsNullOrEmpty(prefix) ? string.Empty : prefix + "-") + unique.Substring(0, length); } - - } } \ No newline at end of file diff --git a/MK.IO/StorageAccount/StorageAccountsOperations.cs b/MK.IO/StorageAccount/StorageAccountsOperations.cs index 147bbe9..35c5b78 100644 --- a/MK.IO/StorageAccount/StorageAccountsOperations.cs +++ b/MK.IO/StorageAccount/StorageAccountsOperations.cs @@ -54,6 +54,7 @@ public StorageResponseSchema Create(StorageSchema storage) public async Task CreateAsync(StorageSchema storage, CancellationToken cancellationToken = default) { Argument.AssertNotNull(storage, nameof(storage)); + Argument.AssertNotMoreThanLength(storage.Description, nameof(storage.Description), 1024); var storageSchema = new StorageRequestSchema() { Spec = storage diff --git a/MK.IO/StreamingEndpoint/StreamingEndpointsOperations.cs b/MK.IO/StreamingEndpoint/StreamingEndpointsOperations.cs index f9fb75a..e43e5ec 100644 --- a/MK.IO/StreamingEndpoint/StreamingEndpointsOperations.cs +++ b/MK.IO/StreamingEndpoint/StreamingEndpointsOperations.cs @@ -151,9 +151,11 @@ public async Task CreateAsync(string streamingEndpointN Argument.AssertNotNullOrEmpty(streamingEndpointName, nameof(streamingEndpointName)); Argument.AssertNotContainsSpace(streamingEndpointName, nameof(streamingEndpointName)); Argument.AssertNotMoreThanLength(streamingEndpointName, nameof(streamingEndpointName), 24); - Argument.AssertRespectRegex(streamingEndpointName, nameof(streamingEndpointName), @"^[a-zA-Z0-9-]+$"); + Argument.AssertRespectRegex(streamingEndpointName, nameof(streamingEndpointName), @"^[a-zA-Z0-9]+(-*[a-zA-Z0-9])*$"); Argument.AssertNotNullOrEmpty(location, nameof(location)); Argument.AssertNotNull(properties, nameof(properties)); + Argument.AssertNotMoreThanLength(properties.Description, nameof(properties.Description), 1024); + Argument.AssertNotMoreThanLength(properties.CdnProfile, nameof(properties.CdnProfile), 100); var url = Client.GenerateApiUrl(_streamingEndpointApiUrl + "?autoStart=" + autoStart.ToString(), streamingEndpointName); tags ??= new Dictionary(); diff --git a/MK.IO/StreamingLocator/StreamingLocatorsOperations.cs b/MK.IO/StreamingLocator/StreamingLocatorsOperations.cs index a0fc32c..e80ea0c 100644 --- a/MK.IO/StreamingLocator/StreamingLocatorsOperations.cs +++ b/MK.IO/StreamingLocator/StreamingLocatorsOperations.cs @@ -129,6 +129,7 @@ public async Task CreateAsync(string streamingLocatorNam Argument.AssertNotNullOrEmpty(streamingLocatorName, nameof(streamingLocatorName)); Argument.AssertNotMoreThanLength(streamingLocatorName, nameof(streamingLocatorName), 260); Argument.AssertNotNull(properties, nameof(properties)); + Argument.AssertNotMoreThanLength(properties.DefaultContentKeyPolicyName, nameof(properties.DefaultContentKeyPolicyName), 260); var url = Client.GenerateApiUrl(_streamingLocatorApiUrl, streamingLocatorName); var content = new StreamingLocatorSchema { Properties = properties }; diff --git a/MK.IO/Transform/Models/AIPipelinePreset.cs b/MK.IO/Transform/Models/AIPipelinePreset.cs new file mode 100644 index 0000000..f5ad54d --- /dev/null +++ b/MK.IO/Transform/Models/AIPipelinePreset.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Newtonsoft.Json; +using System.Text; + +namespace MK.IO.Models +{ + public class AIPipelinePreset : TransformPreset + { + /// + /// The discriminator for derived types. Must be set to #MediaKind.AIPipelinePreset + /// + /// The discriminator for derived types. Must be set to #MediaKind.AIPipelinePreset + [JsonProperty("@odata.type")] + internal override string OdataType => "#MediaKind.AIPipelinePreset"; + + /// + /// Gets or Sets Pipeline + /// + [JsonProperty(PropertyName = "pipeline")] + public PipelineArguments Pipeline { get; set; } + + + /// + /// Get the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append("class AIPipelinePreset {\n"); + sb.Append(" OdataType: ").Append(OdataType).Append("\n"); + sb.Append(" Pipeline: ").Append(Pipeline).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + /// + /// Get the JSON string presentation of the object + /// + /// JSON string presentation of the object + public string ToJson() + { + return JsonConvert.SerializeObject(this, Formatting.Indented); + } + + } +} diff --git a/MK.IO/Transform/Models/ThumbnailGeneratorConfiguration.cs b/MK.IO/Transform/Models/ThumbnailGeneratorConfiguration.cs index 33a9f80..16e09be 100644 --- a/MK.IO/Transform/Models/ThumbnailGeneratorConfiguration.cs +++ b/MK.IO/Transform/Models/ThumbnailGeneratorConfiguration.cs @@ -38,14 +38,14 @@ public class ThumbnailGeneratorConfiguration /// /// The compression quality for JPEG images. Between 0-100, default: 70. [JsonProperty(PropertyName = "quality")] - public int? Quality { get; set; } + public int? Quality { get; set; } = 70; /// /// Either an ISO8601 duration, or a percentage of the asset duration, or the value '1'. The default is '1', a single thumbnail is produced. /// /// Either an ISO8601 duration, or a percentage of the asset duration, or the value '1'. The default is '1', a single thumbnail is produced. [JsonProperty(PropertyName = "range")] - public string Range { get; set; } + public string Range { get; set; } = "1"; /// /// The number of columns used if you want a thumbnail sprite image. Default: Single image output files. @@ -59,14 +59,14 @@ public class ThumbnailGeneratorConfiguration /// /// Either an ISO8601 duration, or a percentage of the asset duration. Default: PT10S. [JsonProperty(PropertyName = "start")] - public string Start { get; set; } + public string Start { get; set; } = "PT10S"; /// /// The intervals at which thumbnails are generated. Either an ISO8601 duration, or a percentage of the asset duration. /// /// The intervals at which thumbnails are generated. Either an ISO8601 duration, or a percentage of the asset duration. [JsonProperty(PropertyName = "step")] - public string Step { get; set; } + public string Step { get; set; } = "10%"; /// /// Either an integer size in pixels, or a percentage of the input resolution. If only one of width/height is present, the aspect ratio from the source is preserved. diff --git a/MK.IO/Transform/Models/ThumbnailGeneratorPreset.cs b/MK.IO/Transform/Models/ThumbnailGeneratorPreset.cs index 7a9d523..7698aee 100644 --- a/MK.IO/Transform/Models/ThumbnailGeneratorPreset.cs +++ b/MK.IO/Transform/Models/ThumbnailGeneratorPreset.cs @@ -15,6 +15,9 @@ public class ThumbnailGeneratorPreset : TransformPreset /// Used to create the output filename as `{BaseFilename}_{Label}{Index}{Extension}`. The default is the name of the input file. If the name of the input file is too long then it will be truncated to 64 characters. public ThumbnailGeneratorPreset(List thumbnails, string baseFileName = null) { + Argument.AssertNotMoreThanLength(baseFileName, nameof(baseFileName), 64); + Argument.AssertRespectRegex(baseFileName, nameof(baseFileName), @"^[A-Za-z0-9_-]+$"); + BaseFilename = baseFileName; Thumbnails = thumbnails; } diff --git a/MK.IO/Transform/Models/TrackInserter.cs b/MK.IO/Transform/Models/TrackInserter.cs new file mode 100644 index 0000000..2f65811 --- /dev/null +++ b/MK.IO/Transform/Models/TrackInserter.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using JsonSubTypes; +using Newtonsoft.Json; +namespace MK.IO.Models +{ + + [JsonConverter(typeof(JsonSubtypes), "@odata.type")] + [JsonSubtypes.KnownSubType(typeof(TrackInserterPresetTextTrack), "#MediaKind.TextTrack")] + + // + // Summary: + // Base class for Transform Output preset configuration. A derived class must be used + // to create a configuration. + public class TrackInserter + { + [JsonProperty("@odata.type")] + internal virtual string OdataType { get; set; } + } +} \ No newline at end of file diff --git a/MK.IO/Transform/Models/TrackInserterPreset.cs b/MK.IO/Transform/Models/TrackInserterPreset.cs new file mode 100644 index 0000000..d9f0245 --- /dev/null +++ b/MK.IO/Transform/Models/TrackInserterPreset.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Newtonsoft.Json; +using System.Runtime.Serialization; +using System.Text; + +namespace MK.IO.Models +{ + public class TrackInserterPreset : TransformPreset + { + /// + /// Constructor for TrackInserterPreset + /// + /// The set of tracks to be inserted. Currently limited to one. + /// Used to create the output filename as `{BaseFilename}.cmft`. The default is the name of the input .vtt file minus the extension, e.g. `subtitles.vtt` -> `subtitles.cmft`. + public TrackInserterPreset(List tracks, string baseFileName = null) + { + Argument.AssertNotMoreThanLength(baseFileName, nameof(baseFileName), 64); + Argument.AssertRespectRegex(baseFileName, nameof(baseFileName), @"^[A-Za-z0-9_-]+$"); + if (tracks.Count != 1) + { + throw new ArgumentException("tracks parameter can only have one track."); + } + + BaseFilename = baseFileName; + Tracks = tracks; + } + + [JsonProperty("@odata.type")] + internal override string OdataType => "#MediaKind.TrackInserterPreset"; + + /// + /// Used to create the output filename as `{BaseFilename}.cmft`. The default is the name of the input .vtt file minus the extension, e.g. `subtitles.vtt` -> `subtitles.cmft`. + /// + /// Used to create the output filename as `{BaseFilename}.cmft`. The default is the name of the input .vtt file minus the extension, e.g. `subtitles.vtt` -> `subtitles.cmft`. + [DataMember(Name = "baseFilename", EmitDefaultValue = false)] + [JsonProperty(PropertyName = "baseFilename")] + public string BaseFilename { get; set; } + + /// + /// The set of tracks to be inserted. Currently limited to one. + /// + /// The set of tracks to be inserted. Currently limited to one. + [DataMember(Name = "tracks", EmitDefaultValue = false)] + [JsonProperty(PropertyName = "tracks")] + public List Tracks { get; set; } + + + /// + /// Get the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append("class TrackInserterPreset {\n"); + sb.Append(" OdataType: ").Append(OdataType).Append("\n"); + sb.Append(" BaseFilename: ").Append(BaseFilename).Append("\n"); + sb.Append(" Tracks: ").Append(Tracks).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + + /// + /// Get the JSON string presentation of the object + /// + /// JSON string presentation of the object + public string ToJson() + { + return JsonConvert.SerializeObject(this, ConverterLE.Settings); + } + } +} \ No newline at end of file diff --git a/MK.IO/Transform/Models/TrackInserterPresetTextTrack.cs b/MK.IO/Transform/Models/TrackInserterPresetTextTrack.cs new file mode 100644 index 0000000..60a0778 --- /dev/null +++ b/MK.IO/Transform/Models/TrackInserterPresetTextTrack.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Newtonsoft.Json; +using System.Runtime.Serialization; +using System.Text; + +namespace MK.IO.Models +{ + public class TrackInserterPresetTextTrack : TrackInserter + { + + [JsonProperty("@odata.type")] + internal override string OdataType => "#MediaKind.TextTrack"; + + /// + /// The display name of the track on a video player. In HLS, this maps to the NAME attribute of EXT-X-MEDIA. + /// + /// The display name of the track on a video player. In HLS, this maps to the NAME attribute of EXT-X-MEDIA. + [DataMember(Name = "displayName", EmitDefaultValue = false)] + [JsonProperty(PropertyName = "displayName")] + public string DisplayName { get; set; } + + /// + /// Gets or Sets HlsSettings + /// + [DataMember(Name = "hlsSettings", EmitDefaultValue = false)] + [JsonProperty(PropertyName = "hlsSettings")] + public HlsSettings HlsSettings { get; set; } + + /// + /// The RFC5646 language code for the track. + /// + /// The RFC5646 language code for the track. + [DataMember(Name = "languageCode", EmitDefaultValue = false)] + [JsonProperty(PropertyName = "languageCode")] + public string LanguageCode { get; set; } + + /// + /// When PlayerVisibility is set to 'Visible', the track will be present in the DASH manifest or HLS playlist when requested by a client. When the PlayerVisibility is set to 'Hidden', the track will not be available to the client. The default value is 'Visible'. + /// + /// When PlayerVisibility is set to 'Visible', the track will be present in the DASH manifest or HLS playlist when requested by a client. When the PlayerVisibility is set to 'Hidden', the track will not be available to the client. The default value is 'Visible'. + [DataMember(Name = "playerVisibility", EmitDefaultValue = false)] + [JsonProperty(PropertyName = "playerVisibility")] + public TrackInserterPresetTextTrackPlayerVisibility PlayerVisibility { get; set; } = TrackInserterPresetTextTrackPlayerVisibility.Visible; + + /// + /// The name of the track in the manifest. + /// + /// The name of the track in the manifest. + [DataMember(Name = "trackName", EmitDefaultValue = false)] + [JsonProperty(PropertyName = "trackName")] + public string TrackName { get; set; } + + + /// + /// Get the string presentation of the object + /// + /// String presentation of the object + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append("class TrackInserterPresetTextTrack {\n"); + sb.Append(" OdataType: ").Append(OdataType).Append("\n"); + sb.Append(" DisplayName: ").Append(DisplayName).Append("\n"); + sb.Append(" HlsSettings: ").Append(HlsSettings).Append("\n"); + sb.Append(" LanguageCode: ").Append(LanguageCode).Append("\n"); + sb.Append(" PlayerVisibility: ").Append(PlayerVisibility).Append("\n"); + sb.Append(" TrackName: ").Append(TrackName).Append("\n"); + sb.Append("}\n"); + return sb.ToString(); + } + + + /// + /// Get the JSON string presentation of the object + /// + /// JSON string presentation of the object + public string ToJson() + { + return JsonConvert.SerializeObject(this, ConverterLE.Settings); + } + } +} \ No newline at end of file diff --git a/MK.IO/Transform/Models/TrackInserterPresetTextTrackPlayerVisibility.cs b/MK.IO/Transform/Models/TrackInserterPresetTextTrackPlayerVisibility.cs new file mode 100644 index 0000000..7d09dc1 --- /dev/null +++ b/MK.IO/Transform/Models/TrackInserterPresetTextTrackPlayerVisibility.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; + +namespace MK.IO.Models +{ + /// + /// When PlayerVisibility is set to 'Visible', the track will be present in the DASH manifest or HLS playlist when requested by a client. When the PlayerVisibility is set to 'Hidden', the track will not be available to the client. The default value is 'Visible'. + /// + /// When PlayerVisibility is set to 'Visible', the track will be present in the DASH manifest or HLS playlist when requested by a client. When the PlayerVisibility is set to 'Hidden', the track will not be available to the client. The default value is 'Visible'. + [JsonConverter(typeof(StringEnumConverter))] + public enum TrackInserterPresetTextTrackPlayerVisibility + { + [EnumMember(Value = "Visible")] + Visible, + + [EnumMember(Value = "Hidden")] + Hidden + } +} \ No newline at end of file diff --git a/MK.IO/Transform/Models/TransformPreset.cs b/MK.IO/Transform/Models/TransformPreset.cs index 3b501f3..55f6af1 100644 --- a/MK.IO/Transform/Models/TransformPreset.cs +++ b/MK.IO/Transform/Models/TransformPreset.cs @@ -10,6 +10,8 @@ namespace MK.IO.Models [JsonSubtypes.KnownSubType(typeof(BuiltInStandardEncoderPreset), "#Microsoft.Media.BuiltInStandardEncoderPreset")] [JsonSubtypes.KnownSubType(typeof(BuiltInAssetConverterPreset), "#Microsoft.Media.BuiltInAssetConverterPreset")] [JsonSubtypes.KnownSubType(typeof(ThumbnailGeneratorPreset), "#MediaKind.ThumbnailGeneratorPreset")] + [JsonSubtypes.KnownSubType(typeof(TrackInserterPreset), "#MediaKind.TrackInserterPreset")] + [JsonSubtypes.KnownSubType(typeof(AIPipelinePreset), "#MediaKind.AIPipelinePreset")] // // Summary: From b7b82f260f3b1e656cbf65462d382345f4a1bfa4 Mon Sep 17 00:00:00 2001 From: Xavier Pouyat Date: Wed, 31 Jul 2024 12:23:58 +0200 Subject: [PATCH 3/4] Version update --- MK.IO.Tests/AssetsOperationsTests.cs | 1 - MK.IO/MK.IO.csproj | 6 +++--- SampleNet8.0/SampleNet8.0.csproj | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/MK.IO.Tests/AssetsOperationsTests.cs b/MK.IO.Tests/AssetsOperationsTests.cs index 9f1bc1e..341b078 100644 --- a/MK.IO.Tests/AssetsOperationsTests.cs +++ b/MK.IO.Tests/AssetsOperationsTests.cs @@ -4,7 +4,6 @@ using Moq; using MK.IO.Operations; using Newtonsoft.Json; -using System.Xml.Linq; namespace MK.IO.Tests { diff --git a/MK.IO/MK.IO.csproj b/MK.IO/MK.IO.csproj index 5e52a0c..0d2924b 100644 --- a/MK.IO/MK.IO.csproj +++ b/MK.IO/MK.IO.csproj @@ -11,12 +11,12 @@ A .NET client SDK for MediaKind MK.IO. LICENSE.txt README.md - 2.0.1 + 2.1.0 https://github.com/microsoft/MK.IO git https://github.com/microsoft/MK.IO/blob/main/README.md - 2.0.1.0 - 2.0.1.0 + 2.1.0.0 + 2.1.0.0 A .NET client SDK for MediaKind MK.IO. True snupkg diff --git a/SampleNet8.0/SampleNet8.0.csproj b/SampleNet8.0/SampleNet8.0.csproj index 1d4c59f..56b1312 100644 --- a/SampleNet8.0/SampleNet8.0.csproj +++ b/SampleNet8.0/SampleNet8.0.csproj @@ -20,7 +20,7 @@ - + From 76ad8b6f38423d9670ec85007bcdf8594c97b3a8 Mon Sep 17 00:00:00 2001 From: Xavier Pouyat Date: Wed, 31 Jul 2024 12:44:37 +0200 Subject: [PATCH 4/4] Package update/switch. --- MK.IO/Argument.cs | 9 +++------ MK.IO/MK.IO.csproj | 2 +- SampleNet8.0/SimpleEncodingAndPublishing.cs | 6 +++--- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/MK.IO/Argument.cs b/MK.IO/Argument.cs index b3fdeb1..42752be 100644 --- a/MK.IO/Argument.cs +++ b/MK.IO/Argument.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.JsonWebTokens; using System.Text.RegularExpressions; namespace MK.IO @@ -110,11 +110,8 @@ public static void AssertRespectRegex(string? value, string name, string regexPa /// public static void AssertJwtToken(string authToken, string name) { - try - { - var jwtSecurityToken = new JwtSecurityToken(authToken); - } - catch (Exception ex) + var jsonWebTokenHandler = new JsonWebTokenHandler(); + if (!jsonWebTokenHandler.CanReadToken(authToken)) { throw new ArgumentException("Value is not a JWT Token. Please read https://docs.mk.io/docs/personal-access-tokens to learn how to generate a personal access token.", name); } diff --git a/MK.IO/MK.IO.csproj b/MK.IO/MK.IO.csproj index 0d2924b..c031240 100644 --- a/MK.IO/MK.IO.csproj +++ b/MK.IO/MK.IO.csproj @@ -53,9 +53,9 @@ + - diff --git a/SampleNet8.0/SimpleEncodingAndPublishing.cs b/SampleNet8.0/SimpleEncodingAndPublishing.cs index 2e347b3..8554489 100644 --- a/SampleNet8.0/SimpleEncodingAndPublishing.cs +++ b/SampleNet8.0/SimpleEncodingAndPublishing.cs @@ -168,6 +168,9 @@ private static async Task CreateInputAssetAsync(MKIOClient client, ); Console.WriteLine($"Input asset '{inputAsset.Name}' created."); + // We wait 2 seconds to let time to MK.IO create the container + await Task.Delay(2000); + // Create an interactive browser credential which will use the system authentication broker var blobContainerClient = new BlobContainerClient( new Uri($"https://{inputAsset.Properties.StorageAccountName}.blob.core.windows.net/{inputAsset.Properties.Container}"), @@ -178,9 +181,6 @@ private static async Task CreateInputAssetAsync(MKIOClient client, ) ); - // We wait 2 seconds to let time to MK.IO create the container - await Task.Delay(2000); - // User or app must have Storage Blob Data Contributor on the account for the upload to work! // Upload a blob (e.g., from a local file) var blobClient = blobContainerClient.GetBlobClient(Path.GetFileName(fileToUpload));