EdTech

eLearning Content Security: How to Protect Online Courses?

April 29, 2022 • 857 Views • 35 min read

author photo

Tetiana Stoyko

CTO & Co-Founder

All the content published online is a form of intellectual property and has its authors. Unfortunately, with the increasing number of data that is flowing through the Internet and social media, it is hard to constantly monitor if your content was posted unnamed or under someone’s name (it’s even worse). In fact, copyright infringement, plagiarism, and piracy are risks we take when doing any kind of business – especially when putting our intellectual property online.

Mostly, the online education industry might face such kind of situation. Lectors and institutions publish their custom-made online courses with different types of content (video/audio/texts) to the eLearning software and share access with the learners. Receiving access to learning materials, students can easily copy-paste the text, record the video or audio and share it freely with others. But, there are lots of means for eLearning content security, which let the app owners create a safe environment where no one's intellectual property will be stolen.

Hence, when intellectual property and content get distributed without the author’s indication and consent, we shouldn't make a fuss around it. Instead, it’s better to spend some of your time and money to ensure that you did everything to provide eLearning content security. So, what can you do to prevent copyright infringement in your eLearning software?

You might also be interested in a guide on

How to Protect Online Courses?

Mostly when people are motivated to get the course materials and share some of them by breaking a copyright infringement, they are searching for simple targets. So, if your eLearning software will have even simple protection - there is a high chance that you’ll dissuade a lot of them.

As a matter of fact, the answer to the question ‘How to Protect Online Courses?’ depends on the format of content you aim to protect. So, in this article, we are going to define the main methods for eLearning content security. And since the majority of eLearning software offers video materials in their courses, we will emphasize one of the possible tech solutions to keep your video content privacy.

But first, let’s start with the easy one: text and images. To save texts from being plagiarised, there are multiple options: from restricting copying to clipboard and using special pinging services to disabling text selection highlighting using CSS styles.

For the media content like images, the situation is pretty much the same, there are numerous options to apply. Probably the easiest way, which you have seen a lot in your experience - using watermarks. It is implemented using Adobe Photoshop, Digimark, Watermark, and other similar programs. But, there are more two options that should bring your attention: first - configuring your image so when it is downloaded, the user gets an empty file or a file of a nonexistent format, second - creating an anchor list consisting of unique text fragments (it accelerates content indexing).

Video Content Protection

With the growth of visualization and user-targeted humanized approaches in online education, video content became one of the most popular ways to share knowledge with students. And even if your platform won’t contain a button to download a video material, it is important to save it from downloading, screen recording, and other options of copyright infringement. Of, course you can also add a watermark or owner identifier on the videos. It marks videos as copyrighted even when screen-recoded.

One more technical option is to encrypt videos on your eLearning software. This way, even if downloaded, the plagiariser will need an encryption key to play it. However, to prevent situations when the plagiariser steals the encryption key, it’s more secure to set up DRM encryption. This may be done by the development team you are working with. So, if you think this is something that might help with your eLearning content security - show this tutorial to your developers, or just contact us.

Axinom DRM Solution to Protect Video Content

Before using our DRM service we need to create some protected content and upload it to the system. This is illustrated by the following diagram:

Axinom DRM.png

Basically, any content (video in our case) has to be provided for our DMR-capable encoder. The encoder then sends a request to Axinom DRM Key Service and receives one or more content keys in response. The key service produces fresh keys and securely delivers them, together with the key IDs, to the encoder/packager. The encoder/packager encrypts the content and packages it in a manner suited for DRM-enabled adaptive streaming, then uploads the result to an origin server using the key information.

Now when we have our DRM-protected content ready and we can start working on our decryption service. In order to play DRM-protected content, the player has to know how to read and play protected video. To be able to do so, it first needs to access the keys, which can be found in a license that is also responsible for the conditions under which these keys may be used (e.g. expiration). Both of them are generated by Axinom DRM License Service and sent to the player if it has the correct license token.

This process is illustrated by the following diagram:

Axinom DRM2.png

Now let`s try and create a playback application for our DRM-protected demo videos.

Note: Axinom supports all the latest versions of the following browsers:

  • Google Chrome (Widevine)

  • Mozilla Firefox (Widevine)

  • Microsoft Edge (PlayReady and Widevine)

  • Microsoft Internet Explorer (PlayReady)

  • Apple Safari (FairPlay)

For demo, we will use ShakaPlayer. Now, first, let's create our index.html page:

<!DOCTYPE html>
<html>

<head>
    <title>Axinom DRM quick start demo player</title>
    <script type="text/javascript" src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/shaka-player/3.1.0/shaka-player.compiled.debug.js"></script>
    <style>
        video#videoPlayer {
            width: 640px;
            height: 360px;
        }
    </style>
</head>

<body>
    <div id="loadingPanel">
        <p>Loading. See the JavaScript console in case of problems.</p>
    </div>
    <div id="videoListPanel" style="display: none">
        <p>Select the video to play:</p>
        <ul id="videoList"></ul>
    </div>
    <div id="videoPlayerPanel" style="display: none">
        <video id="videoPlayer" controls autoplay></video>
        <p>Press play, if video doesn't auto-play.</p>
        <p>Refresh the page to go back to the video catalog.</p>
    </div>
    <div>
        <p>Supported browsers: Google Chrome, Mozilla Firefox, Microsoft Edge, Internet Explorer 11, Apple Safari</p>
        <p>Note: multi-key demo videos are not supported on IE11.</p>
    </div>

On initial start we check whether the browser supports FairPlay. In script tag of our index.html we add following code:

$(function () {
            var $loadingPanel = $("#loadingPanel");
            var $videoListPanel = $("#videoListPanel");
            var $videoList = $("#videoList");
            var $videoPlayerPanel = $("#videoPlayerPanel");
            var $videoPlayer = $("#videoPlayer");

            // FairPlay specific variables. These will be set on Safari.
            var isSafariDetected = false;
            var fairPlayCertificate = null;
            var fairPlayCertificateUrl = "https://vtb.axinom.com/FPScert/fairplay.cer";

            // Detect if we are using Safari. If yes, then only FairPlay is supported and we'll
            // activate FairPlay-specific workflows. This means that we will:
            // * Display only FairPlay-capable videos.
            // * Load the FairPlay certificate
            // * Assign a custom InitData transform to Shaka Player that extracts the Content ID
            //   from HLS manifest in the form that's expected by our FairPlay license services.
            if (typeof WebKitMediaKeys === "function" && WebKitMediaKeys.isTypeSupported("com.apple.fps.1_0", "video/mp4"))
            {
                console.log("FairPlay support detected.");
                isSafariDetected = true;
            }

Now lets add a function for loading FairPlayCertificate in case we are using Safari Browser:

// FairPlay-specific functionality.
            function loadFairPlayCertificate() {
                console.log("Requesting FairPlay certificate from " + fairPlayCertificateUrl)
                var request = new XMLHttpRequest();
                request.responseType = 'arraybuffer';
                request.addEventListener('load', onFairPlayCertificateLoaded, false);
                request.addEventListener('error', onFairPlayCertificateError, false);
                request.open('GET', fairPlayCertificateUrl, true);
                request.setRequestHeader('Pragma', 'Cache-Control: no-cache');
                request.setRequestHeader("Cache-Control", "max-age=0");
                request.send();
            }
        
            function onFairPlayCertificateLoaded(event) {
                console.log("FairPlay certificate received.");
                var request = event.target;
                fairPlayCertificate = new Uint8Array(request.response);
                
                $loadingPanel.hide();
                $videoListPanel.show();
            }
            
            function onFairPlayCertificateError(event) {
                console.error('FairPlay certificate request failed.');
            } 

And finally lets populate our video list with some content:

$.getJSON("/api/catalog/videos")
            .done(function(videos) {
            .done(function(videos) {
                console.log("Server provided " + videos.length + " videos.");

                $.each(videos, function(index, video) {
                    // On Safari let's display only videos that support FairPlay. On other browsers,
                    // let's just show everything else since our other demo videos support both
                    // Widevine and PlayReady.
                    if ((!video.tags && isSafariDetected) || (video.tags && video.tags.filter(function(tag) {
                        tag = tag.toLowerCase();
                        if (isSafariDetected && tag === "fairplay") {
                            return true;
                        } else if (!isSafariDetected && tag !== "fairplay") {
                            return true;
                        } else {
                            return false;
                        }
                    }).length == 0)) {
                        console.log("Video \"" + video.name + "\" is not supported on current platform, hiding it...");
                        return;
                    }
                    
                    console.log("Video \"" + video.name + "\" is at URL " + video.url);

                    // This makes the list item for this video.
                    var link = $("<a href=\"#\">");
                    link.text(video.name);
                    var listItem = $("<li>").append(link);
                    $videoList.append(listItem);
                    link.click(function () {
                        playVideo(video);
                        return false;
                    });
                });

                console.log("Loading complete.");

                // Load the FairPlay certificate if Safari is detected.
                if (isSafariDetected) {
                    loadFairPlayCertificate();
                } else {
                    $loadingPanel.hide();
                    $videoListPanel.show();
                }
            })
            .fail(function() {
                console.log("API call to get video catalog failed!");
            });

Now let`s create a simple Catalog API that will send us some DMR protected content

CatalogApi.js:

(function () {
	"use strict";

	let express = require("express");
	let videoDatabase = require("./VideoDatabase");

	module.exports = {
		"createRouter": function createRouter() {
			let router = express.Router();

			// This API call returns a JSON list with basic info about all the videos on the website.
			router.get("/videos", function processGet(request, response) {
				// We do not want our API calls to get cached.
				response.header("Cache-Control", "no-cache");
				
				let videoList = [];

				videoDatabase.getAllVideos().forEach(function mapVideo(video) {
					// Only name, URL and an optional list tags are exposed to the browser.
					// Everything else is for internal use only.
					videoList.push({
						"name": video.name,
						"url": video.url,
						"tags": video.tags
					});
				});				

				response.json(videoList);
			});

			return router;
		}
	};
})();

VideoDatabase.js will contain our videos list:

let allVideos = [
        // Uncomment and copy-paste the block below as an example for adding custom
        // videos to the list.
        /*
        {
            "name": "My video 1",
            "url": "https://example.com/Manifest.mpd",
            "keys": [
                {
                    "keyId": "1c817fed-0686-45b6-bce2-d6a4eb873588",
                }
            ]
        }
        */

        // The following entries are for the pre-generated demo videos. To add your own videos, 
        // copy the sample video above, append to the list and adjust the fields as needed.
        //
        // Note: The demo videos have hardcoded license tokens for maximum ease of use of the
        // sample app. Never do this in production - always generate a new license token on
        // every request.

        // Note: the "tags" property is optional. The demo player uses this to filter the video
        // list -- for example, to only display FairPlay-compliant videos on Safari.
        {
            "name": "Axinom demo video - single key (DASH; cenc)",
            "url": "https://media.axprod.net/VTB/DrmQuickStart/AxinomDemoVideo-SingleKey/Encrypted_Cenc/Manifest.mpd",
            "licenseToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiNjllNTQwODgtZTllMC00NTMwLThjMWEtMWViNmRjZDBkMTRlIiwibWVzc2FnZSI6eyJ2ZXJzaW9uIjoyLCJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImxpY2Vuc2UiOnsiYWxsb3dfcGVyc2lzdGVuY2UiOnRydWV9LCJjb250ZW50X2tleXNfc291cmNlIjp7ImlubGluZSI6W3siaWQiOiIyMTFhYzFkYy1jOGEyLTQ1NzUtYmFmNy1mYTRiYTU2YzM4YWMiLCJ1c2FnZV9wb2xpY3kiOiJUaGVPbmVQb2xpY3kifV19LCJjb250ZW50X2tleV91c2FnZV9wb2xpY2llcyI6W3sibmFtZSI6IlRoZU9uZVBvbGljeSIsInBsYXlyZWFkeSI6eyJwbGF5X2VuYWJsZXJzIjpbIjc4NjYyN0Q4LUMyQTYtNDRCRS04Rjg4LTA4QUUyNTVCMDFBNyJdfX1dfX0.D9FM9sbTFxBmcCOC8yMHrEtTwm0zy6ejZUCrlJbHz_U",
        },
        {
            "name": "Axinom demo video - single key (HLS; cbcs)",
            "url": "https://media.axprod.net/VTB/DrmQuickStart/AxinomDemoVideo-SingleKey/Encrypted_Cbcs/Manifest.m3u8",
            "licenseToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiNjllNTQwODgtZTllMC00NTMwLThjMWEtMWViNmRjZDBkMTRlIiwibWVzc2FnZSI6eyJ2ZXJzaW9uIjoyLCJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImxpY2Vuc2UiOnsiYWxsb3dfcGVyc2lzdGVuY2UiOnRydWV9LCJjb250ZW50X2tleXNfc291cmNlIjp7ImlubGluZSI6W3siaWQiOiIyMTFhYzFkYy1jOGEyLTQ1NzUtYmFmNy1mYTRiYTU2YzM4YWMiLCJ1c2FnZV9wb2xpY3kiOiJUaGVPbmVQb2xpY3kifV19LCJjb250ZW50X2tleV91c2FnZV9wb2xpY2llcyI6W3sibmFtZSI6IlRoZU9uZVBvbGljeSIsInBsYXlyZWFkeSI6eyJwbGF5X2VuYWJsZXJzIjpbIjc4NjYyN0Q4LUMyQTYtNDRCRS04Rjg4LTA4QUUyNTVCMDFBNyJdfX1dfX0.D9FM9sbTFxBmcCOC8yMHrEtTwm0zy6ejZUCrlJbHz_U",
            "tags": ["FairPlay"]
        },
        {
            "name": "Axinom demo video - multikey (DASH; cenc)",
            "url": "https://media.axprod.net/VTB/DrmQuickStart/AxinomDemoVideo-MultiKey/Encrypted_Cenc/Manifest.mpd",
            "licenseToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiNjllNTQwODgtZTllMC00NTMwLThjMWEtMWViNmRjZDBkMTRlIiwibWVzc2FnZSI6eyJ2ZXJzaW9uIjoyLCJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImxpY2Vuc2UiOnsiYWxsb3dfcGVyc2lzdGVuY2UiOnRydWV9LCJjb250ZW50X2tleXNfc291cmNlIjp7ImlubGluZSI6W3siaWQiOiJmM2Q1ODhjNy1jMTdhLTQwMzMtOTAzNS04ZGIzMTczOTBiZTYiLCJ1c2FnZV9wb2xpY3kiOiJUaGVPbmVQb2xpY3kifSx7ImlkIjoiNDRiMThhMzItNmQzNi00OTlkLThiOTMtYTIwZjk0OGFjNWYyIiwidXNhZ2VfcG9saWN5IjoiVGhlT25lUG9saWN5In0seyJpZCI6ImFlNmU4N2UyLTNjM2MtNDZkMS04ZTlkLWVmNGM0NjFkNDY4MSIsInVzYWdlX3BvbGljeSI6IlRoZU9uZVBvbGljeSJ9XX0sImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjpbeyJuYW1lIjoiVGhlT25lUG9saWN5IiwicGxheXJlYWR5Ijp7InBsYXlfZW5hYmxlcnMiOlsiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3Il19fV19fQ.DpwBd1ax4Z7P0cCOZ7ZJMotqVWfLFCj2DYdH37xjGxM",
        },
        {
            "name": "Axinom demo video - multikey (HLS; cbcs)",
            "url": "https://media.axprod.net/VTB/DrmQuickStart/AxinomDemoVideo-MultiKey/Encrypted_Cbcs/Manifest.m3u8",
            "licenseToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiNjllNTQwODgtZTllMC00NTMwLThjMWEtMWViNmRjZDBkMTRlIiwibWVzc2FnZSI6eyJ2ZXJzaW9uIjoyLCJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImxpY2Vuc2UiOnsiYWxsb3dfcGVyc2lzdGVuY2UiOnRydWV9LCJjb250ZW50X2tleXNfc291cmNlIjp7ImlubGluZSI6W3siaWQiOiJmM2Q1ODhjNy1jMTdhLTQwMzMtOTAzNS04ZGIzMTczOTBiZTYiLCJ1c2FnZV9wb2xpY3kiOiJUaGVPbmVQb2xpY3kifSx7ImlkIjoiNDRiMThhMzItNmQzNi00OTlkLThiOTMtYTIwZjk0OGFjNWYyIiwidXNhZ2VfcG9saWN5IjoiVGhlT25lUG9saWN5In0seyJpZCI6ImFlNmU4N2UyLTNjM2MtNDZkMS04ZTlkLWVmNGM0NjFkNDY4MSIsInVzYWdlX3BvbGljeSI6IlRoZU9uZVBvbGljeSJ9XX0sImNvbnRlbnRfa2V5X3VzYWdlX3BvbGljaWVzIjpbeyJuYW1lIjoiVGhlT25lUG9saWN5IiwicGxheXJlYWR5Ijp7InBsYXlfZW5hYmxlcnMiOlsiNzg2NjI3RDgtQzJBNi00NEJFLThGODgtMDhBRTI1NUIwMUE3Il19fV19fQ.DpwBd1ax4Z7P0cCOZ7ZJMotqVWfLFCj2DYdH37xjGxM",
            "tags": ["FairPlay"]
        }
    ];


And simple check for all required data in video list:


// Verifies that all critical information is present on a video.
    // Automatically performs sanity checks to avoid making mistakes in the above list. 
    function verifyVideoIntegrity(video) {
        if (!video)
            throw new Error("A video was expected but was not present.");
        if (!video.name || !video.name.length)
            throw new Error("A video is missing its name.");

        console.log("Verifying integrity of video definition: " + video.name);

        if (!video.url || !video.url.length)
            throw new Error("The video is missing its URL.");

        // Either a hardcoded license token or the keys structure must exist. Not both.
        if (video.licenseToken && video.keys)
            throw new Error("The video has both a hardcoded license token and a content key list - pick only one.");
        if (!video.licenseToken && !video.keys)
            throw new Error("The video is missing the content key list.");

        if (video.keys) {
            if (!video.keys.length)
                throw new Error("The content key list for this video is empty.");

            // Verify that each item in the keys list has all the required data.
            video.keys.forEach(function verifyKey(item) {
                if (!item.keyId)
                    throw new Error("A content key is missing the key ID.");
            });
        }
    }

    // Verify all videos on startup.
    allVideos.forEach(verifyVideoIntegrity);


And finally exporting Catalog API module:

module.exports = {
        "getAllVideos": function getAllVideos() {
            return allVideos;
        },
        "getVideoByName": function getVideoByName(name) {
            return allVideos.find(function filter(item) {
                return item.name === name;
            });
        }
    };

Now when we run our index.html in Chrome we will se the following page:

content3.png

And dev console will look like this:

content2.png

Now lets play video that we loaded in our player. Let`s add playVideo function that will be call when we click a video from the list.

When video is clicked we need to get licenseToken from our authorization service:

 $.getJSON("/api/authorization/" + encodeURIComponent(video.name))
                .done(function(licenseToken) {
                    // We got a license token! We are all set to start playback.
                    // We just pass it on to the player and have it take care of the rest.
                    

Now let`s have a look at our license provider:



// This API call returns the license token for playing back a video.
			// The web app provides the name of the video as a parameter in the URL.
			router.get("/:videoName", function processGet(request, response) {
				// We do not want our API calls to get cached.
				response.header("Cache-Control", "no-cache");

				let video = videoDatabase.getVideoByName(request.params.videoName);

				if (!video) {
					response.status(NO_SUCH_VIDEO_STATUS_CODE).send("No such video");
					return;
				}

				// TODO: Check here if the user is actually authorized to watch this video. For example, you could
				// check a database of purchases to see if the currently logged-in user made a relevant purchase
				// for this product. For demo purposes, however, everyone is always authorized to watch every video.

				if (video.licenseToken) {
					// If the video has a license token hardcoded, just give that to all callers.
					// Strictly for demo purposes only - never do this in real world usage.
					response.json(video.licenseToken);
					return;
				}

As you can see in the code above, if video has its license token we simply return it to the browser, but more complex checks can be done here.

In case we don`t have token we can generate our own. First we create our secrets manager service - SecretManagement.js:

(function () {
	"use strict";

	const COMMUNICATION_KEY_LENGTH_IN_BYTES = 32;
	const SECRETS_FILE_NAME = "Secrets.json";

	// Global variable with loaded secrets.
	let secrets = null;

	module.exports = {
		"getSecrets": function getSecrets() {
			return secrets;
		},
		"areSecretsAvailable": function areSecretsAvailable() {
			return secrets !== null;
		},
		// Attempts to load the secrets from Secrets.json, if the file exists.
		// It is okay if it does not exist - hardcoded sample videos will still work.
		"tryLoadSecrets": function tryLoadSecrets() {
			let fs = require("fs");

			if (!fs.existsSync(SECRETS_FILE_NAME)) {
				console.log("No " + SECRETS_FILE_NAME + " file found - only the built-in sample videos can be viewed.");
				return;
			} else {
				console.log("Loading " + SECRETS_FILE_NAME + " file.");

				secrets = require("./" + SECRETS_FILE_NAME);
			}

			if (!secrets.communicationKeyId)
				throw new Error(SECRETS_FILE_NAME + " validation failed: communicationKeyId field is missing.");
			if (!secrets.communicationKey)
				throw new Error(SECRETS_FILE_NAME + " validation failed: communicationKey field is missing.");

			var communicationKeyBuffer = Buffer.from(secrets.communicationKey, 'base64');
			if (communicationKeyBuffer.length !== COMMUNICATION_KEY_LENGTH_IN_BYTES)
				throw new Error(SECRETS_FILE_NAME + " validation failed: communicationKey did not contain " + COMMUNICATION_KEY_LENGTH_IN_BYTES + " bytes of base64-encoded data.");

			if (secrets.communicationKeyId === "00000000-0000-0000-0000-000000000000"
				|| secrets.communicationKey === "00000000000000000000000000000000000000000w==")
				throw new Error("You need to replace the example values in " + SECRETS_FILE_NAME + " with your own!");
		}
	};
})();

We store and load our secrets in local Secrets.json file. Now we can load them in our license provider:

if (!secretManagement.areSecretsAvailable()) {
					console.log("ERROR: You must configure the secrets file to generate license tokens.");
					response.status(NEED_TO_KNOW_SECRETS_STATUS_CODE)
						.send("You must configure the secrets file to generate license tokens.");
					return;
				}

				let secrets = secretManagement.getSecrets();
				let communicationKeyAsBuffer = Buffer.from(secrets.communicationKey, "base64");

				// We allow this token to be used within plus or minus 24 hours. This allows for a lot of
				// clock drift, as your demo server might not be properly real-time synced across the world.
				// In production scenarios, you should limit the use of the license token much more strictly.
				// The time limit defined here applies both to the license token and to any generated licenses,
				// though it is possible to control them separately in situations where that is desired.
				let now = moment();
				let validFrom = now.clone().subtract(1, "days");
				let validTo = now.clone().add(1, "days");

				// For detailed information about these fields, refer to Axinom DRM documentation.
				// There exist many possibilities for further customization of the license token - the settings
				// shown here are only the bare minimum to create a license token suitable for realistic use.
				let message = {
					"type": "entitlement_message",
					"version": 2,
					"license": {
						"start_datetime": validFrom.toISOString(),
						"expiration_datetime": validTo.toISOString(),
						"allow_persistence": true
					},

					// The keys list will be filled separately by the next code block.
					"content_keys_source": {
						"inline": [
						]
					},
					
					// License configuration should be as permissive as possible for the scope of this guide.
					// For this reason, some PlayReady-specific restrictions are relaxed below.
					// There is no need to relax the default Widevine or FairPlay specific restrictions.
					"content_key_usage_policies": [
						{
							"name": "Policy A",
							"playready": {
								// Allow playback on non-production devices.
								"min_device_security_level": 150,
								// Allow playback in virtual machines.
								"play_enablers": [
									"786627D8-C2A6-44BE-8F88-08AE255B01A7"
								]
							}
						}
					]
				};

Now we embed the content key information (IDs and usage policies) into the license token

video.keys.forEach(function (key) {
					// A key ID is always required. In this demo, we'll also reference the previously defined
					// key usage policy.
					let inlineKey = {
						"id": key.keyId,
						"usage_policy": "Policy A"		
					} 

					message.content_keys_source.inline.push(inlineKey);
				});

				// For detailed information about these fields, refer to Axinom DRM documentation.
				let envelope = {
					"version": 1,
					"com_key_id": secrets.communicationKeyId,
					"message": message,
					"begin_date": validFrom.toISOString(),
					"expiration_date": validTo.toISOString()
				};

				console.log("Creating license token with payload: " + JSON.stringify(envelope));

				// The license token must be digitally signed to prove that it came from the token service.
				let licenseToken = jwt.sign(envelope, communicationKeyAsBuffer, {
					"algorithm": "HS256",
					"noTimestamp": true
				});

				response.json(licenseToken);

Now we can call this service to get token on video click:

$.getJSON("/api/authorization/" + encodeURIComponent(video.name))
                .done(function(licenseToken) {
                    // We got a license token! We are all set to start playback.
                    // We just pass it on to the player and have it take care of the rest.

                    console.log("Starting Shaka player playback with license token: " + licenseToken)

After we get a token we setup our Shaka Player and load FairPlayCertificate if needed:

var player = new shaka.Player($videoPlayer[0]);

                        // Attach player to the window to make it easy to access in the JS console.
                        window.player = player;

                        // Listen for error events.
                        player.addEventListener('error', onErrorEvent);

                        // Configure Axinom DRM license services.
                        player.configure({
                            drm: {
                                servers: {
                                    'com.widevine.alpha': 'https://drm-widevine-licensing.axtest.net/AcquireLicense',
                                    'com.microsoft.playready': 'https://drm-playready-licensing.axtest.net/AcquireLicense',
                                    'com.apple.fps.1_0': 'https://drm-fairplay-licensing.axtest.net/AcquireLicense'
                                }
                            }
                        });

                        if (isSafariDetected === true) {
                            player.configure('drm.advanced.com\\.apple\\.fps\\.1_0.serverCertificate', fairPlayCertificate);
                        }

Now we simply set our license token and load the video:

 // Add the "X-AxDRM-Message" header, containing the license token, to the header
                        // of all license requests.
                        player.getNetworkingEngine().registerRequestFilter(function(type, request) {
                            if (type == shaka.net.NetworkingEngine.RequestType.LICENSE) {
                                // This is the specific header name and value the server wants:
                                request.headers['X-AxDRM-Message'] = licenseToken;
                            }
                            
                        });

                        // Try to load a manifest. This is an asynchronous process.
                        player.load(video.url).then(function() {
                            // This runs if the asynchronous load is successful.
                            // onError() is executed if the asynchronous load fails.
                            console.log('The video has now been loaded!');
                        }).catch(onError);  


And now let's try our Player - when we click on a video from the list we see the following content:

content1.png
content4.png

So, to summarize, let’s go through the steps that are happening during Axinom DRM integration:

  1. Browser-side JavasScript code first checks if the browser supports FairPlay.
  2. If FairPlay is enabled, the Axinom FairPlay Test Certificate is downloaded from an Axinom server, and the integrated player is set up to support FairPlay.
  3. To get a list of movies, the Catalog API is activated.
  4. Videos are returned through the API.
  5. When a user clicks on a video link to begin playing it, the following happens.
  6. The client requests a license token. In production, this service should also authorize the user.
  7. The Entitlement Service grants a token.
  8. After we get the token, the code activates the embedded Shaka Player and provides both the video URL and the DRM configuration. For the rest of the process, the player code will be in control.
  9. If the video is protected player requests a license from the service, and also attaches the license token to it.
  10. If an authorization was successful License service returns a license based on the information in the token.
  11. Playback starts (provided that the content keys in the license are correct and playback rules are met).

Summing up

When developing an eLearning software, developing a strategy for the content protection is a must-have. Obviously, if you won’t protect online courses on your platform, they wouldn’t be so unique and exclusive anymore. And as a result, this might decrese profit. So, to stay afloat, and stay a desired platform for any learner, make sure you offer custom-made profeccional materials, and protect them from plagiarism. The last task just leave to us!

Share this post

Tags

Tech
Expertise
Guide
Case Studies

What’s your impression after reading this?

Love it!

Valuable

Exciting

Unsatisfied

Got no clue where to start? Why don’t we discuss your idea?

Let’s talk!

Contact us

chat photo
privacy policy

© 2015-2024 Incora LLC

offices

Ukrainian office

116, Bohdana Khmel'nyts'koho, Lviv, Lviv Oblast, 79019

USA office

16192 Coastal Hwy, Lewes, DE 19958 USA

follow us

This site uses cookies to improve your user experience.Read our Privacy Policy

Accept