Skip to content

Commit 24340cc

Browse files
authored
Merge pull request #685 from TechnologyEnhancedLearning/releases/tucana
Merge Tucana release to Test
2 parents 0c9073e + f71a58c commit 24340cc

File tree

28 files changed

+2724
-2558
lines changed

28 files changed

+2724
-2558
lines changed

AdminUI/LearningHub.Nhs.AdminUI/Controllers/api/MediaManifestProxyController.cs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,27 @@ public string Get(string playBackUrl, string token)
5151
{
5252
using (var reader = new StreamReader(stream))
5353
{
54-
const string qualityLevelRegex = @"(QualityLevels\(\d+\))";
54+
const string qualityLevelRegex = @"(|)([^""\s]+\.m3u8\(encryption=cbc\))";
5555
const string fragmentsRegex = @"(Fragments\([\w\d=-]+,[\w\d=-]+\))";
56-
const string urlRegex = @"("")(https?:\/\/[\da-z\.-]+\.[a-z\.]{2,6}[\/\w \.-]*\/?[\?&][^&=]+=[^&=#]*)("")";
56+
const string urlRegex = @"(https?:\/\/[\da-z\.-]+\.[a-z\.]{2,6}[\/\w \.-]*\?[^,\s""]*)";
5757

5858
var baseUrl = playBackUrl.Substring(0, playBackUrl.IndexOf(".ism", System.StringComparison.OrdinalIgnoreCase)) + ".ism";
5959
this.logger.LogDebug($"baseUrl={baseUrl}");
6060

6161
var content = reader.ReadToEnd();
6262

63-
var newContent = Regex.Replace(content, urlRegex, string.Format(CultureInfo.InvariantCulture, "$1$2&token={0}$3", token));
63+
content = ReplaceUrisWithProxy(content, baseUrl);
64+
var newContent = Regex.Replace(content, urlRegex, match =>
65+
{
66+
string baseUrlWithQuery = match.Groups[1].Value; // URL including the query string
67+
68+
// Append the token correctly without modifying surrounding characters
69+
string newUrl = baseUrlWithQuery.Contains("?") ?
70+
$"{baseUrlWithQuery}&token={token}" :
71+
$"{baseUrlWithQuery}?token={token}";
72+
73+
return newUrl;
74+
});
6475
this.logger.LogDebug($"newContent={newContent}");
6576

6677
var match = Regex.Match(playBackUrl, qualityLevelRegex);
@@ -87,5 +98,33 @@ public string Get(string playBackUrl, string token)
8798

8899
return null;
89100
}
101+
102+
private static string ReplaceUrisWithProxy(string playlistContent, string proxyUrl)
103+
{
104+
// Split the playlist content into lines
105+
var lines = playlistContent.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);
106+
107+
// Process each line to replace media or map URIs
108+
for (int i = 0; i < lines.Length; i++)
109+
{
110+
if (lines[i].StartsWith("#EXT-X-MAP:URI=", StringComparison.OrdinalIgnoreCase))
111+
{
112+
// Extract the URI from the current line for EXT-X-MAP
113+
var existingUri = lines[i].Substring(lines[i].IndexOf('=') + 1).Trim('"');
114+
var newUri = $"{proxyUrl}/{existingUri}";
115+
lines[i] = lines[i].Replace(existingUri, newUri);
116+
}
117+
else if (lines[i].StartsWith("#EXTINF:", StringComparison.OrdinalIgnoreCase) && i + 1 < lines.Length)
118+
{
119+
// Get the URI from the next line for EXTINF
120+
var existingUri = lines[i + 1].Trim();
121+
var newUri = $"{proxyUrl}/{existingUri}";
122+
lines[i + 1] = newUri;
123+
}
124+
}
125+
126+
// Join the modified lines back into a single string
127+
return string.Join("\r\n", lines);
128+
}
90129
}
91130
}

AdminUI/LearningHub.Nhs.AdminUI/Scripts/vuesrc/content/cmsPageRow.vue

Lines changed: 73 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@
7171
import { AzureMediaAssetModel } from '../models/content/videoAssetModel';
7272
import { MKPlayer } from '@mediakind/mkplayer';
7373
import { MKPlayerType, MKStreamType } from '../MKPlayerConfigEnum';
74+
//import { getPlayerConfig, getSourceConfig, initializePlayer } from '../mkiomediaplayer';
75+
import { buildControlbar } from '../mkioplayer-controlbar';
7476
7577
export default Vue.extend({
7678
props: {
@@ -89,14 +91,20 @@
8991
player: null,
9092
videoContainer: null,
9193
mkioKey: '',
94+
isIphone: false,
95+
requestURL: ''
9296
};
9397
},
94-
async created(): Promise<void> {
98+
async created(): Promise<void> {
9599
await this.getMKIOPlayerKey();
96100
this.load();
97101
this.getDisplayAVFlag();
98102
this.getAudioVideoUnavailableView();
99103
},
104+
mounted() {
105+
this.checkIfIphone();
106+
this.requestURL = window.location.origin;
107+
},
100108
computed: {
101109
getStyle() {
102110
console.log("getLinkStyle", (this.pageSectionDetail || {}).draftHidden);
@@ -168,11 +176,37 @@
168176
this.audioVideoUnavailableView = response;
169177
});
170178
},
179+
//onSubtitleAdded() {
180+
// this.player.subtitles.enable("subtitle" + this.section.id.toString());
181+
//},
171182
onPlayerReady() {
172-
const videoElement = document.getElementById("bitmovinplayer-video-" + this.getPlayerUniqueId) as HTMLVideoElement;
173-
if (videoElement) {
174-
videoElement.controls = true;
175-
}
183+
var contanierId = this.section.id.toString();
184+
var uniquePlayer = this.player;// ([email protected]);
185+
buildControlbar(contanierId, uniquePlayer);
186+
187+
// [BY] When we set UI to false we need to manually add the controls to the video element
188+
//const videoElement = document.getElementById("bitmovinplayer-video-" + this.getPlayerUniqueId) as HTMLVideoElement;
189+
//if (videoElement) {
190+
// videoElement.controls = true;
191+
//}
192+
193+
// var subtitleTrack;
194+
//if (this.pageSectionDetail.videoAsset.azureMediaAsset && this.pageSectionDetail.videoAsset.closedCaptionsFile) {
195+
// const captionsInfo = this.pageSectionDetail.videoAsset.closedCaptionsFile;
196+
// var srcPath = "file/download/" + captionsInfo.filePath + "/" + captionsInfo.fileName;
197+
// //srcPath = '@requestURL' + srcPath;
198+
// srcPath = "https://bitdash-a.akamaihd.net/content/sintel/subtitles/subtitles_en.vtt";
199+
200+
// subtitleTrack = {
201+
// id: "subtitle" + this.section.id.toString(),
202+
// lang: "en",
203+
// label: "english",
204+
// url: srcPath,
205+
// kind: "subtitle"
206+
// };
207+
//};
208+
209+
//this.player.addSubtitle(subtitleTrack);
176210
},
177211
async getMKIOPlayerKey(): Promise<void> {
178212
this.mkioKey = await contentData.getMKPlayerKey();
@@ -189,14 +223,14 @@
189223
// Grab the video container
190224
this.videoContainer = document.getElementById(this.getPlayerUniqueId);
191225
192-
if(!this.mkioKey) {
226+
if (!this.mkioKey) {
193227
this.getMKIOPlayerKey();
194228
}
195229
196230
// Prepare the player configuration
197231
const playerConfig = {
198232
key: this.mkioKey,
199-
ui: false,
233+
ui: true,
200234
playback: {
201235
muted: false,
202236
autoplay: false,
@@ -205,15 +239,33 @@
205239
theme: "dark",
206240
events: {
207241
ready: this.onPlayerReady,
242+
//subtitleadded: this.onSubtitleAdded,
208243
}
209244
};
210245
211246
// Initialize the player with video container and player configuration
212247
this.player = new MKPlayer(this.videoContainer, playerConfig);
213248
249+
var subtitleTrack = null;
250+
var sectionId = this.section.id.toString();
251+
if (this.pageSectionDetail.videoAsset.azureMediaAsset && this.pageSectionDetail.videoAsset.closedCaptionsFile) {
252+
var captionsInfo = this.pageSectionDetail.videoAsset.closedCaptionsFile;;
253+
254+
if (captionsInfo) {
255+
var srcPath = "/file/download/" + captionsInfo.filePath + "/" + captionsInfo.fileName;
256+
subtitleTrack = {
257+
id: "subtitle" + sectionId,
258+
lang: "en",
259+
label: "english",
260+
url: this.requestURL + srcPath,
261+
kind: "subtitle"
262+
};
263+
}
264+
}
214265
// Load source
215266
const sourceConfig = {
216267
hls: this.getMediaPlayUrl(this.pageSectionDetail.videoAsset.azureMediaAsset.locatorUri),
268+
subtitleTracks: [subtitleTrack],
217269
drm: {
218270
clearkey: {
219271
LA_URL: "HLS_AES",
@@ -282,8 +334,18 @@
282334
},
283335
getMediaPlayUrl(url: string): string {
284336
let sourceUrl = url.substring(0, url.lastIndexOf("manifest")) + "manifest(format=m3u8-cmaf,encryption=cbc)";
337+
338+
if (this.isIphone) {
339+
var token = this.pageSectionDetail.videoAsset.azureMediaAsset.authenticationToken;
340+
sourceUrl = "/Media/MediaManifest?playBackUrl=" + sourceUrl + "&token=" + token;
341+
}
342+
285343
return sourceUrl;
286344
},
345+
checkIfIphone() {
346+
const userAgent = navigator.userAgent || navigator.vendor;
347+
this.isIphone = /iPhone/i.test(userAgent);
348+
},
287349
},
288350
watch: {
289351
section() {
@@ -318,4 +380,8 @@
318380
video[id^="bitmovinplayer-video"] {
319381
width: 100%;
320382
}
383+
384+
.bmpui-ui-controlbar .control-right {
385+
float: right;
386+
}
321387
</style>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { MKPlayer, MKPlayerConfig } from '@mediakind/mkplayer';
2+
import { MKPlayerType, MKStreamType } from './MKPlayerConfigEnum';
3+
interface ClearKeyConfig {
4+
LA_URL: string;
5+
headers: {
6+
Authorization: string;
7+
};
8+
}
9+
10+
interface PlayerConfig {
11+
key: string;
12+
ui: boolean;
13+
playback: {
14+
muted: boolean;
15+
autoplay: boolean;
16+
preferredTech: Array<{ player: string; streaming: string }>;
17+
};
18+
theme: string;
19+
events: {
20+
ready: () => void;
21+
};
22+
}
23+
24+
interface SourceConfig {
25+
hls: string;
26+
drm: {
27+
clearkey: ClearKeyConfig;
28+
};
29+
}
30+
31+
function getBearerToken(authenticationToken: string): string {
32+
// Replace this with your actual logic to get the bearer token
33+
return `Bearer ${authenticationToken}`;
34+
}
35+
36+
function getPlayerConfig(
37+
mkioKey: string,
38+
onPlayerReady: () => void
39+
): PlayerConfig {
40+
return {
41+
key: mkioKey,
42+
ui: true,
43+
playback: {
44+
muted: false,
45+
autoplay: false,
46+
preferredTech: [{ player: "Html5", streaming: "Hls" }] // Adjust these strings if you have specific types
47+
},
48+
theme: "dark",
49+
events: {
50+
ready: onPlayerReady,
51+
}
52+
};
53+
}
54+
55+
function getSourceConfig(
56+
locatorUri: string,
57+
authenticationToken: string
58+
): SourceConfig {
59+
return {
60+
hls: locatorUri,
61+
drm: {
62+
clearkey: {
63+
LA_URL: "HLS_AES",
64+
headers: {
65+
Authorization: getBearerToken(authenticationToken)
66+
}
67+
}
68+
}
69+
};
70+
}
71+
72+
function initializePlayer(videoContainer: HTMLElement, playerConfig: MKPlayerConfig, playBackUrl: string, bearerToken: string): any {
73+
const player = new MKPlayer(videoContainer, playerConfig);
74+
75+
var clearKeyConfig = {
76+
//LA_URL: "https://ottapp-appgw-amp.prodc.mkio.tv3cloud.com/drm/clear-key?ownerUid=azuki",
77+
LA_URL: "HLS_AES",
78+
headers: {
79+
"Authorization": bearerToken
80+
}
81+
};
82+
83+
const sourceConfig: SourceConfig = {
84+
hls: playBackUrl,
85+
drm: {
86+
clearkey: clearKeyConfig
87+
}
88+
};
89+
90+
player.load(sourceConfig)
91+
.then(() => {
92+
console.log("Source loaded successfully!");
93+
})
94+
.catch(() => {
95+
console.error("An error occurred while loading the source!");
96+
});
97+
98+
return player;
99+
};
100+
101+
export { getPlayerConfig, getSourceConfig, initializePlayer };
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* Constructs and configures the control bar for the UI.
3+
*
4+
* This function performs the following tasks:
5+
* 1. Selects the titlebar and controlbar elements from the DOM.
6+
* 2. Creates a playback toggle button with an initial "Play" state and appends it to the controlbar.
7+
* 3. Adds an event listener to the playback toggle button to handle play/pause functionality.
8+
* 4. Retrieves all buttons from the titlebar, aligns them to the right (except for the "Mute" button), and appends them to the controlbar.
9+
* 5. Selects the UI container element and sets up a MutationObserver to monitor changes in the container's class attribute.
10+
* 6. Updates the playback toggle button state based on the player's state (playing or paused) when the container's class changes.
11+
*/
12+
13+
function buildControlbar(id: string, player: { isPlaying: () => boolean; pause: () => void; play: () => void; }): void {
14+
const mediacontainerId = 'videoContainer_' + id;
15+
16+
// Select the titlebar and controlbar elements from the DOM
17+
const titlebar = document.querySelector(`#${mediacontainerId} .bmpui-ui-titlebar`) as HTMLElement;
18+
const controlbar = document.querySelector(`#${mediacontainerId} .bmpui-ui-controlbar`) as HTMLElement;
19+
20+
// Check if both titlebar and controlbar elements exist
21+
if (titlebar && controlbar) {
22+
23+
// Create a playback toggle button and set its initial state and appearance
24+
const playbackToggleButton = document.createElement('button');
25+
playbackToggleButton.classList.add('bmpui-ui-playbacktogglebutton', 'bmpui-off');
26+
playbackToggleButton.setAttribute('aria-label', 'Play');
27+
playbackToggleButton.innerHTML = '<span class="bmpui-label">Play</span>';
28+
playbackToggleButton.id = 'playback-toggle-btn-' + id;
29+
controlbar.appendChild(playbackToggleButton);
30+
31+
// Add an event listener to the playback toggle button
32+
playbackToggleButton.addEventListener('click', function () {
33+
// Toggle playback state based on the current state
34+
if (player.isPlaying()) {
35+
player.pause();
36+
playbackToggleButton.classList.remove('bmpui-on');
37+
playbackToggleButton.classList.add('bmpui-off');
38+
playbackToggleButton.innerHTML = '<span class="bmpui-label">Play</span>';
39+
} else {
40+
player.play();
41+
playbackToggleButton.classList.remove('bmpui-off');
42+
playbackToggleButton.classList.add('bmpui-on');
43+
playbackToggleButton.innerHTML = '<span class="bmpui-label">Pause</span>';
44+
}
45+
});
46+
47+
// Get all button elements from the titlebar
48+
const buttons = titlebar.querySelectorAll('button');
49+
50+
// Reverse the button list and append each button to the controlbar
51+
Array.from(buttons).reverse().forEach(button => {
52+
if (button.textContent !== "Mute") {
53+
button.classList.add('control-right'); // Add a class to align buttons to the right
54+
}
55+
controlbar.appendChild(button); // Append the button to the controlbar
56+
});
57+
58+
// Select the UI container element
59+
const uiOverlayElement = document.querySelector(`#${mediacontainerId} .bmpui-ui-playbacktoggle-overlay`) as HTMLElement;
60+
uiOverlayElement.addEventListener('click', function () {
61+
const uiContainerElement = document.querySelector(`#${mediacontainerId} .bmpui-ui-uicontainer`) as HTMLElement;
62+
// Update the playback toggle button state based on the player's state
63+
if (uiContainerElement.classList.contains('bmpui-player-state-playing')) {
64+
playbackToggleButton.classList.remove('bmpui-on');
65+
playbackToggleButton.classList.add('bmpui-off');
66+
} else {
67+
playbackToggleButton.classList.remove('bmpui-off');
68+
playbackToggleButton.classList.add('bmpui-on');
69+
}
70+
});
71+
72+
} else {
73+
console.error('UI container element not found');
74+
}
75+
}
76+
77+
export { buildControlbar };

0 commit comments

Comments
 (0)