From 668804bbf61054226f167ee4808276bc78f52a93 Mon Sep 17 00:00:00 2001 From: binon Date: Thu, 2 May 2024 16:56:21 +0100 Subject: [PATCH 01/22] MediaKind settings added --- .../Configuration/MediaKindSettings.cs | 43 +++++++++++++++++++ .../Configuration/Settings.cs | 5 +++ .../LearningHub.Nhs.WebUI.csproj | 1 + LearningHub.Nhs.WebUI/appsettings.json | 9 ++++ 4 files changed, 58 insertions(+) create mode 100644 LearningHub.Nhs.WebUI/Configuration/MediaKindSettings.cs diff --git a/LearningHub.Nhs.WebUI/Configuration/MediaKindSettings.cs b/LearningHub.Nhs.WebUI/Configuration/MediaKindSettings.cs new file mode 100644 index 000000000..02d1b0965 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Configuration/MediaKindSettings.cs @@ -0,0 +1,43 @@ +namespace LearningHub.Nhs.WebUI.Configuration +{ + /// + /// Config AzureMediaSettings. + /// + public class MediaKindSettings + { + /// + /// Gets or sets subscription name. + /// + public string SubscriptionName { get; set; } + + /// + /// Gets or sets token. + /// + public string Token { get; set; } + + /// + /// Gets or sets storage media account name. + /// + public string StorageAccountName { get; set; } + + /// + /// Gets or sets media kind media service issuer. + /// + public string Issuer { get; set; } + + /// + /// Gets or sets media kind media service audience. + /// + public string Audience { get; set; } + + /// + /// Gets or sets the contentkey policyname. + /// + public string ContentKeyPolicyName { get; set; } + + /// + /// Gets or sets media kind media service jwt primary key secret. + /// + public string JWTPrimaryKeySecret { get; set; } + } +} diff --git a/LearningHub.Nhs.WebUI/Configuration/Settings.cs b/LearningHub.Nhs.WebUI/Configuration/Settings.cs index 7797a36c2..d107ac2d5 100644 --- a/LearningHub.Nhs.WebUI/Configuration/Settings.cs +++ b/LearningHub.Nhs.WebUI/Configuration/Settings.cs @@ -240,5 +240,10 @@ public Settings() /// Gets or sets the FindwiseSettings. /// public FindwiseSettings FindwiseSettings { get; set; } = new FindwiseSettings(); + + /// + /// Gets or sets the MediaKindSettings. + /// + public MediaKindSettings MediaKindSettings { get; set; } = new MediaKindSettings(); } } \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj index 817199786..acea5791b 100644 --- a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj +++ b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj @@ -124,6 +124,7 @@ + all diff --git a/LearningHub.Nhs.WebUI/appsettings.json b/LearningHub.Nhs.WebUI/appsettings.json index d8b526e39..de85fc608 100644 --- a/LearningHub.Nhs.WebUI/appsettings.json +++ b/LearningHub.Nhs.WebUI/appsettings.json @@ -145,5 +145,14 @@ "FeatureManagement": { "ContributeAudioVideoResource": true, "DisplayAudioVideoResource": true + }, + "MediaKindSettings": { + "StorageAccountName": "", + "Token": "", + "SubscriptionName": "", + "Issuer": "LearningHub", + "Audience": "LearningHubUsers", + "ContentKeyPolicyName": "LHSharedContentKeyPolicy", + "JWTPrimaryKeySecret": "" } } From 738162c6b27939a7ef7010dad3bf60430fb3ea9d Mon Sep 17 00:00:00 2001 From: binon Date: Fri, 3 May 2024 14:46:09 +0100 Subject: [PATCH 02/22] Updates appsettins fo rMKIO --- .gitignore | 1 + .../Startup/ServiceMappings.cs | 2 +- LearningHub.Nhs.WebUI/appsettings.json | 20 +++++++++---------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 0afa44753..4119589ad 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ obj /WebAPI/LearningHub.Nhs.Database/LearningHub.Nhs.Database.jfm /WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Database/LearningHub.Nhs.Migration.Staging.Database.dbmdl /WebAPI/MigrationTool/LearningHub.Nhs.Migration.Staging.Database/LearningHub.Nhs.Migration.Staging.Database.jfm +/LearningHub.Nhs.WebUI.AutomatedUiTests/appsettings.Development.json diff --git a/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs b/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs index cf3877a99..a0cc16237 100644 --- a/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs +++ b/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs @@ -85,7 +85,7 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/LearningHub.Nhs.WebUI/appsettings.json b/LearningHub.Nhs.WebUI/appsettings.json index de85fc608..d1205c3f3 100644 --- a/LearningHub.Nhs.WebUI/appsettings.json +++ b/LearningHub.Nhs.WebUI/appsettings.json @@ -99,6 +99,15 @@ "ResourceSearchPageSize": 10, "CatalogueSearchPageSize": 3 }, + "MediaKindSettings": { + "StorageAccountName": "", + "Token": "", + "SubscriptionName": "", + "Issuer": "LearningHub", + "Audience": "LearningHubUsers", + "ContentKeyPolicyName": "LHSharedContentKeyPolicy", + "JWTPrimaryKeySecret": "" + }, "EnableTempDebugging": "false", "LimitScormToAdmin": "false" }, @@ -145,14 +154,5 @@ "FeatureManagement": { "ContributeAudioVideoResource": true, "DisplayAudioVideoResource": true - }, - "MediaKindSettings": { - "StorageAccountName": "", - "Token": "", - "SubscriptionName": "", - "Issuer": "LearningHub", - "Audience": "LearningHubUsers", - "ContentKeyPolicyName": "LHSharedContentKeyPolicy", - "JWTPrimaryKeySecret": "" - } + } } From beee082d6f5239d252c37e6811fa74641d138042 Mon Sep 17 00:00:00 2001 From: binon Date: Fri, 3 May 2024 16:00:38 +0100 Subject: [PATCH 03/22] Added MKIO player dummy vue page --- .../components/MKIOVideoPlayer.vue | 121 +++++++++ .../Services/ContributeService.cs | 7 +- .../Services/MKIOMediaService.cs | 248 ++++++++++++++++++ 3 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/components/MKIOVideoPlayer.vue create mode 100644 LearningHub.Nhs.WebUI/Services/MKIOMediaService.cs diff --git a/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/components/MKIOVideoPlayer.vue b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/components/MKIOVideoPlayer.vue new file mode 100644 index 000000000..d8939f580 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Scripts/vuesrc/contribute-resource/components/MKIOVideoPlayer.vue @@ -0,0 +1,121 @@ + + + + + \ No newline at end of file diff --git a/LearningHub.Nhs.WebUI/Services/ContributeService.cs b/LearningHub.Nhs.WebUI/Services/ContributeService.cs index 0936801ee..66722af9b 100644 --- a/LearningHub.Nhs.WebUI/Services/ContributeService.cs +++ b/LearningHub.Nhs.WebUI/Services/ContributeService.cs @@ -24,6 +24,7 @@ public class ContributeService : BaseService, IContributeService { private readonly IAzureMediaService azureMediaService; + private readonly IAzureMediaService mediaService; private readonly IFileService fileService; private readonly IResourceService resourceService; @@ -33,14 +34,16 @@ public class ContributeService : BaseService, IContributeServ /// File service. /// Resource service. /// Azure media service. + /// MKIO media service. /// Learning hub http client. /// Logger. - public ContributeService(IFileService fileService, IResourceService resourceService, IAzureMediaService azureMediaService, ILearningHubHttpClient learningHubHttpClient, ILogger logger) + public ContributeService(IFileService fileService, IResourceService resourceService, IAzureMediaService azureMediaService, ILearningHubHttpClient learningHubHttpClient, ILogger logger, IAzureMediaService mediaService) : base(learningHubHttpClient, logger) { this.fileService = fileService; this.resourceService = resourceService; this.azureMediaService = azureMediaService; + this.mediaService = mediaService; } /// @@ -508,7 +511,7 @@ public async Task ProcessResourceFileAsync(int resourceVersion // if file is media (video or audio) then upload at Azure Media Storage if (fileType != null && (fileType.DefaultResourceTypeId == (int)ResourceTypeEnum.Video || fileType.DefaultResourceTypeId == (int)ResourceTypeEnum.Audio)) { - filelocation = await this.azureMediaService.CreateMediaInputAsset(file); + filelocation = await this.mediaService.CreateMediaInputAsset(file); } // upload at Azure File Storage diff --git a/LearningHub.Nhs.WebUI/Services/MKIOMediaService.cs b/LearningHub.Nhs.WebUI/Services/MKIOMediaService.cs new file mode 100644 index 000000000..e76254e82 --- /dev/null +++ b/LearningHub.Nhs.WebUI/Services/MKIOMediaService.cs @@ -0,0 +1,248 @@ +namespace LearningHub.Nhs.WebUI.Services +{ + using System; + using System.Globalization; + using System.IdentityModel.Tokens.Jwt; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.Cache; + using System.Security.Claims; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + using System.Web; + using Azure.Storage.Blobs; + using Azure.Storage.Blobs.Specialized; + using LearningHub.Nhs.WebUI.Configuration; + using LearningHub.Nhs.WebUI.Interfaces; + using Microsoft.AspNetCore.Http; + using Microsoft.Azure.Management.Media; + using Microsoft.Azure.Management.Media.Models; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + using Microsoft.IdentityModel.Clients.ActiveDirectory; + using Microsoft.IdentityModel.Tokens; + using Microsoft.Rest; + using Microsoft.Rest.Azure.Authentication; + using MK.IO; + + /// + /// Defines the . + /// + public class MKIOMediaService : IAzureMediaService + { + private readonly ILogger logger; + private readonly Settings settings; + private IAzureMediaServicesClient azureMediaServicesClient; + + /// + /// Initializes a new instance of the class. + /// + /// Settings. + /// Logger. + public MKIOMediaService(IOptions settings, ILogger logger) + { + this.settings = settings.Value; + this.logger = logger; + } + + /// + /// Create AzureMedia InputAsset from file upload. + /// + /// File. + /// . + public async Task CreateMediaInputAsset(IFormFile file) + { + string uniqueness = Guid.NewGuid().ToString().Substring(0, 13); + string inputAssetName = $"input-{uniqueness}"; + + var client = this.GetMKIOServicesClientAsync(); + string containerName = "asset-" + Guid.NewGuid().ToString(); + var asset = await client.Assets.CreateOrUpdateAsync(inputAssetName, containerName, this.settings.MediaKindSettings.StorageAccountName); + + BlobServiceClient blobServiceClient = new BlobServiceClient(this.settings.AzureBlobSettings.ConnectionString); + + // Get a reference to the container + BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient(asset.Properties.Container); + + var filename = Regex.Replace(file.FileName, "[^a-zA-Z0-9.]", string.Empty); + filename = string.IsNullOrEmpty(filename) ? "file.txt" : filename; + + // Get a reference to the blob + BlobClient blobClient = containerClient.GetBlobClient(filename); + + await blobClient.UploadAsync(file.OpenReadStream()); + + return asset.Name; + } + + /// + /// The GetContentAuthenticationTokenAsync. + /// + /// The encodedAssetId. + /// The . + public async Task GetContentAuthenticationTokenAsync(string encodedAssetId) + { + byte[] tokenSigningKey = Convert.FromBase64String(this.settings.AzureMediaJWTPrimaryKeySecret); + + var keyidentifier = await this.GetContentKeyIdentifier(encodedAssetId); + + return GetJWTToken(this.settings.AzureMediaIssuer, this.settings.AzureMediaAudience, keyidentifier, tokenSigningKey, this.settings.AzureMediaJWTTokenExpiryMinutes); + } + + /// + /// The GetTopLevelManifestForToken. + /// + /// The manifestProxyUrl. + /// The topLeveLManifestUrl. + /// The token. + /// The . + public string GetTopLevelManifestForToken(string manifestProxyUrl, string topLeveLManifestUrl, string token) + { + ServicePointManager.Expect100Continue = true; + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; + + this.logger.LogDebug($"topLeveLManifestUrl={topLeveLManifestUrl}"); + var httpRequest = (HttpWebRequest)WebRequest.Create(new Uri(topLeveLManifestUrl)); + httpRequest.CachePolicy = new HttpRequestCachePolicy(HttpRequestCacheLevel.NoCacheNoStore); + httpRequest.Timeout = 30000; + + this.logger.LogDebug($"Calling httpRequest.GetResponse()"); + var httpResponse = httpRequest.GetResponse(); + + try + { + this.logger.LogDebug($"Calling httpResponse.GetResponseStream()"); + var stream = httpResponse.GetResponseStream(); + + if (stream != null) + { + using (var reader = new StreamReader(stream)) + { + const string qualityLevelRegex = @"(QualityLevels\(\d+\)/Manifest\(.+\))"; + + var toplevelmanifestcontent = reader.ReadToEnd(); + + var topLevelManifestBaseUrl = topLeveLManifestUrl.Substring(0, topLeveLManifestUrl.IndexOf(".ism", System.StringComparison.OrdinalIgnoreCase)) + ".ism"; + this.logger.LogDebug($"topLevelManifestBaseUrl={topLevelManifestBaseUrl}"); + var urlEncodedTopLeveLManifestBaseUrl = HttpUtility.UrlEncode(topLevelManifestBaseUrl); + var urlEncodedToken = HttpUtility.UrlEncode(token); + + var newContent = Regex.Replace( + toplevelmanifestcontent, + qualityLevelRegex, + string.Format( + CultureInfo.InvariantCulture, + "{0}?playBackUrl={1}/$1&token={2}", + manifestProxyUrl, + urlEncodedTopLeveLManifestBaseUrl, + urlEncodedToken)); + + this.logger.LogDebug($"newContent={newContent}"); + + return newContent; + } + } + } + catch (Exception ex) + { + this.logger.LogDebug($"Exception: {ex.Message}"); + } + finally + { + httpResponse.Close(); + } + + return null; + } + + /// + /// Creates the AzureMediaServicesClient object based on the credentials + /// supplied in local configuration file. + /// + /// . + public async Task CreateMediaServicesClientAsync() + { + if (this.azureMediaServicesClient != null) + { + return this.azureMediaServicesClient; + } + + var credentials = await this.GetCredentialsAsync(); + + this.azureMediaServicesClient = new AzureMediaServicesClient(this.settings.AzureMediaArmEndpoint, credentials) + { + SubscriptionId = this.settings.AzureMediaSubscriptionId, + }; + + return this.azureMediaServicesClient; + } + + /// + /// The GetJWTToken. + /// + /// The issuer. + /// The audience. + /// The keyIdentifier. + /// The tokenVerificationKey. + /// The ExpiryMinutes. + /// The . + private static string GetJWTToken(string issuer, string audience, string keyIdentifier, byte[] tokenVerificationKey, int expiryMinutes) + { + var tokenSigningKey = new SymmetricSecurityKey(tokenVerificationKey); + + SigningCredentials cred = new SigningCredentials( + tokenSigningKey, + SecurityAlgorithms.HmacSha256, // Use the HmacSha256 and not the HmacSha256Signature option, or the token will not work! + SecurityAlgorithms.Sha256Digest); + + Claim[] claims = new Claim[] + { + new Claim(ContentKeyPolicyTokenClaim.ContentKeyIdentifierClaim.ClaimType, keyIdentifier), + }; + + JwtSecurityToken token = new JwtSecurityToken( + issuer: issuer, + audience: audience, + claims: claims, + notBefore: DateTime.Now.AddMinutes(-5), + expires: DateTime.Now.AddMinutes(expiryMinutes), + signingCredentials: cred); + + JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler(); + + return handler.WriteToken(token); + } + + /// + /// The GetContentKeyIdentifier. + /// + /// The encodedAssetId. + /// The . + private async Task GetContentKeyIdentifier(string encodedAssetId) + { + var client = await this.CreateMediaServicesClientAsync(); + + var streamingLocatorsResponse = await client.Assets.ListStreamingLocatorsAsync(this.settings.AzureMediaResourceGroup, this.settings.AzureMediaAccountName, encodedAssetId); + var contentKeyResponse = await client.StreamingLocators.ListContentKeysAsync(this.settings.AzureMediaResourceGroup, this.settings.AzureMediaAccountName, streamingLocatorsResponse.StreamingLocators.First().Name); + string keyIdentifier = contentKeyResponse.ContentKeys.First().Id.ToString(); + + return keyIdentifier; + } + + private MKIOClient GetMKIOServicesClientAsync() + { + return new MKIOClient(this.settings.MediaKindSettings.SubscriptionName, this.settings.MediaKindSettings.Token); + } + + /// + /// Get AzureMedia Credentials. + /// + /// . + private async Task GetCredentialsAsync() + { + ClientCredential clientCredential = new ClientCredential(this.settings.AzureMediaAadClientId, this.settings.AzureMediaAadSecret); + return await ApplicationTokenProvider.LoginSilentAsync(this.settings.AzureMediaAadTenantId, clientCredential, ActiveDirectoryServiceSettings.Azure); + } + } +} From 5c7b915e7d48016242f3c38990c0506b11115264 Mon Sep 17 00:00:00 2001 From: binon Date: Mon, 20 May 2024 10:45:33 +0100 Subject: [PATCH 04/22] MKIO changes before refactoring --- .../Configuration/MediaKindSettings.cs | 43 + .../Configuration/WebSettings.cs | 5 + .../LearningHub.Nhs.AdminUI.csproj | 3 +- .../Scripts/vuesrc/content/cmsPageRow.vue | 342 +- .../ServiceCollectionExtension.cs | 2 +- .../Services/MKIOMediaService.cs | 278 + .../LearningHub.Nhs.AdminUI/package-lock.json | 16601 +------------- AdminUI/LearningHub.Nhs.AdminUI/package.json | 3 +- .../components/MKIOVideoPlayer.vue | 411 +- .../components/VideoPlayerContainer.vue | 2 +- .../Scripts/vuesrc/mkiomediaplayer.d.ts | 2328 ++ .../vuesrc/resource/ResourceContent.vue | 18 +- .../Services/MKIOMediaService.cs | 23 +- LearningHub.Nhs.WebUI/package-lock.json | 18116 +--------------- LearningHub.Nhs.WebUI/package.json | 1 + LearningHub.Nhs.WebUI/wwwroot/js/mkplayer.js | 1 + 16 files changed, 3414 insertions(+), 34763 deletions(-) create mode 100644 AdminUI/LearningHub.Nhs.AdminUI/Configuration/MediaKindSettings.cs create mode 100644 AdminUI/LearningHub.Nhs.AdminUI/Services/MKIOMediaService.cs create mode 100644 LearningHub.Nhs.WebUI/Scripts/vuesrc/mkiomediaplayer.d.ts create mode 100644 LearningHub.Nhs.WebUI/wwwroot/js/mkplayer.js diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Configuration/MediaKindSettings.cs b/AdminUI/LearningHub.Nhs.AdminUI/Configuration/MediaKindSettings.cs new file mode 100644 index 000000000..a032efda7 --- /dev/null +++ b/AdminUI/LearningHub.Nhs.AdminUI/Configuration/MediaKindSettings.cs @@ -0,0 +1,43 @@ +namespace LearningHub.Nhs.AdminUI.Configuration +{ + /// + /// Config AzureMediaSettings. + /// + public class MediaKindSettings + { + /// + /// Gets or sets subscription name. + /// + public string SubscriptionName { get; set; } + + /// + /// Gets or sets token. + /// + public string Token { get; set; } + + /// + /// Gets or sets storage media account name. + /// + public string StorageAccountName { get; set; } + + /// + /// Gets or sets media kind media service issuer. + /// + public string Issuer { get; set; } + + /// + /// Gets or sets media kind media service audience. + /// + public string Audience { get; set; } + + /// + /// Gets or sets the contentkey policyname. + /// + public string ContentKeyPolicyName { get; set; } + + /// + /// Gets or sets media kind media service jwt primary key secret. + /// + public string JWTPrimaryKeySecret { get; set; } + } +} diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Configuration/WebSettings.cs b/AdminUI/LearningHub.Nhs.AdminUI/Configuration/WebSettings.cs index ace067941..508e85497 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Configuration/WebSettings.cs +++ b/AdminUI/LearningHub.Nhs.AdminUI/Configuration/WebSettings.cs @@ -146,5 +146,10 @@ public class WebSettings /// Gets or sets the FileUploadSettings. /// public FileUploadSettingsModel FileUploadSettings { get; set; } = new FileUploadSettingsModel(); + + /// + /// Gets or sets the MediaKindSettings. + /// + public MediaKindSettings MediaKindSettings { get; set; } = new MediaKindSettings(); } } \ No newline at end of file diff --git a/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj index a6109a60f..7bef84029 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj +++ b/AdminUI/LearningHub.Nhs.AdminUI/LearningHub.Nhs.AdminUI.csproj @@ -105,7 +105,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + + diff --git a/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content/cmsPageRow.vue b/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content/cmsPageRow.vue index 79769cdf3..841214774 100644 --- a/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content/cmsPageRow.vue +++ b/AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content/cmsPageRow.vue @@ -19,13 +19,35 @@
- + --> +
+
+
+
+
+
+
+
+ + + + + {{ currentTime }}/{{ duration }} + +
+
+
+
-
@@ -26,28 +25,8 @@ To view this media please enable JavaScript, and consider upgrading to a web browser that supports HTML5 video

--> -
-
-
-
-
-
-
-
- - - - - {{ currentTime }}/{{ duration }} - -
-
-
-
+
+