Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cuddly-showers-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@theoplayer/react-native-drm': major
---

Added PlayReady integration for MediaKind DRM
5 changes: 5 additions & 0 deletions .changeset/legal-guests-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@theoplayer/react-native-drm': major
---

Added Fairplay integration for MediaKind DRM
5 changes: 5 additions & 0 deletions .changeset/solid-grapes-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@theoplayer/react-native-drm': major
---

Added Widevine integration for MediaKind DRM
26 changes: 26 additions & 0 deletions drm/src/mediakind/MediaKindDrmConfiguration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { DRMConfiguration } from 'react-native-theoplayer';

/**
* The identifier of the MediaKind integration.
*/
export type MediaKindIntegrationID = 'customMediaKindDrm';

/**
* Describes the configuration of the MediaKind DRM integration.
*
* ```
* const drmConfiguration = {
* integration : 'mediakind',
* fairplay: {
* certificateURL: 'yourMediaKindCertificateUrl',
* licenseAcquisitionURL: 'yourMediaKindLicenseAcquisitionURL'
* }
* }
* ```
*/
export interface MediaKindDrmConfiguration extends DRMConfiguration {
/**
* The identifier of the DRM integration.
*/
integration: MediaKindIntegrationID;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type {
CertificateRequest,
CertificateResponse,
ContentProtectionIntegration,
LicenseRequest,
LicenseResponse,
MaybeAsync,
} from 'react-native-theoplayer';
import {
fromUint8ArrayToString,
fromBase64StringToUint8Array,
fromStringToUint8Array,
fromUint8ArrayToBase64String,
BufferSource,
} from 'react-native-theoplayer';
import type { MediaKindDrmConfiguration } from './MediaKindDrmConfiguration';
import { getFairplayCertificateURL, getFairplayLicenseAcquisitionURL } from '../utils/drmUtils';

export class MediaKindDrmFairplayContentProtectionIntegration implements ContentProtectionIntegration {
static readonly DEFAULT_CERTIFICATE_URL = 'insert default certificate url here';
static readonly DEFAULT_LICENSE_URL = 'insert default license url here';

private readonly contentProtectionConfiguration: MediaKindDrmConfiguration;
private contentId: string | undefined = undefined;

constructor(configuration: MediaKindDrmConfiguration) {
this.contentProtectionConfiguration = configuration;
}

onCertificateRequest(request: CertificateRequest): MaybeAsync<Partial<CertificateRequest> | BufferSource> {
request.url = getFairplayCertificateURL(
this.contentProtectionConfiguration,
MediaKindDrmFairplayContentProtectionIntegration.DEFAULT_CERTIFICATE_URL,
);
return request;
}

onCertificateResponse(response: CertificateResponse): MaybeAsync<BufferSource> {
let certificate = fromUint8ArrayToString(response.body);
const match = certificate.match(/<cert>([\s\S]*?)<\/cert>/);
if (match) {
certificate = match[1].replace(/\s/g, '');
}
return fromBase64StringToUint8Array(certificate);
}

onLicenseRequest(request: LicenseRequest): MaybeAsync<Partial<LicenseRequest> | BufferSource> {
request.url = getFairplayLicenseAcquisitionURL(
this.contentProtectionConfiguration,
MediaKindDrmFairplayContentProtectionIntegration.DEFAULT_LICENSE_URL,
);
if (request.body !== null) {
let spc = fromUint8ArrayToBase64String(request.body);
request.body = fromStringToUint8Array(spc);
}
return request;
}

onLicenseResponse(response: LicenseResponse): MaybeAsync<BufferSource> {
let license = fromUint8ArrayToString(response.body);
const match = license.match(/<ckc>([\s\S]*?)<\/ckc>/);
if (match) {
license = match[1].replace(/\s/g, '');
}
return fromBase64StringToUint8Array(license);
}

extractFairplayContentId(skdUrl: string): string {
// skdUrl without the "skd://" scheme
this.contentId = skdUrl.substring(6);
return this.contentId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ContentProtectionIntegration, ContentProtectionIntegrationFactory } from 'react-native-theoplayer';
import type { MediaKindDrmConfiguration } from './MediaKindDrmConfiguration';
import { MediaKindDrmFairplayContentProtectionIntegration } from './MediaKindDrmFairplayContentProtectionIntegration';

export class MediaKindDrmFairplayContentProtectionIntegrationFactory implements ContentProtectionIntegrationFactory {
build(configuration: MediaKindDrmConfiguration): ContentProtectionIntegration {
return new MediaKindDrmFairplayContentProtectionIntegration(configuration);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { CertificateRequest, ContentProtectionIntegration, LicenseRequest, MaybeAsync } from 'react-native-theoplayer';
import { BufferSource } from 'react-native-theoplayer';
import type { MediaKindDrmConfiguration } from './MediaKindDrmConfiguration';
import { getPlayReadyLicenseAcquisitionURL } from '../utils/drmUtils';

export class MediaKindDrmPlayReadyContentProtectionIntegration implements ContentProtectionIntegration {
static readonly DEFAULT_CERTIFICATE_URL = 'insert default certificate url here';
static readonly DEFAULT_LICENSE_URL = 'insert default license url here';

private readonly contentProtectionConfiguration: MediaKindDrmConfiguration;

constructor(configuration: MediaKindDrmConfiguration) {
this.contentProtectionConfiguration = configuration;
}

onCertificateRequest(request: CertificateRequest): MaybeAsync<Partial<CertificateRequest> | BufferSource> {
request.url = getPlayReadyLicenseAcquisitionURL(
this.contentProtectionConfiguration,
MediaKindDrmPlayReadyContentProtectionIntegration.DEFAULT_CERTIFICATE_URL,
);
return request;
}

onLicenseRequest(request: LicenseRequest): MaybeAsync<Partial<LicenseRequest> | BufferSource> {
request.url = getPlayReadyLicenseAcquisitionURL(
this.contentProtectionConfiguration,
MediaKindDrmPlayReadyContentProtectionIntegration.DEFAULT_LICENSE_URL,
);
return request;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ContentProtectionIntegration, ContentProtectionIntegrationFactory } from 'react-native-theoplayer';
import type { MediaKindDrmConfiguration } from './MediaKindDrmConfiguration';
import { MediaKindDrmPlayReadyContentProtectionIntegration } from './MediaKindDrmPlayReadyContentProtectionIntegration';

export class MediaKindDrmPlayReadyContentProtectionIntegrationFactory implements ContentProtectionIntegrationFactory {
build(configuration: MediaKindDrmConfiguration): ContentProtectionIntegration {
return new MediaKindDrmPlayReadyContentProtectionIntegration(configuration);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { CertificateRequest, ContentProtectionIntegration, LicenseRequest, MaybeAsync } from 'react-native-theoplayer';
import { BufferSource } from 'react-native-theoplayer';
import type { MediaKindDrmConfiguration } from './MediaKindDrmConfiguration';
import { getWidevineLicenseAcquisitionURL } from '../utils/drmUtils';

export class MediaKindDrmWidevineContentProtectionIntegration implements ContentProtectionIntegration {
static readonly DEFAULT_CERTIFICATE_URL = 'insert default certificate url here';
static readonly DEFAULT_LICENSE_URL = 'insert default license url here';

private readonly contentProtectionConfiguration: MediaKindDrmConfiguration;

constructor(configuration: MediaKindDrmConfiguration) {
this.contentProtectionConfiguration = configuration;
}

onCertificateRequest(request: CertificateRequest): MaybeAsync<Partial<CertificateRequest> | BufferSource> {
request.url = getWidevineLicenseAcquisitionURL(
this.contentProtectionConfiguration,
MediaKindDrmWidevineContentProtectionIntegration.DEFAULT_CERTIFICATE_URL,
);
return request;
}

onLicenseRequest(request: LicenseRequest): MaybeAsync<Partial<LicenseRequest> | BufferSource> {
request.url = getWidevineLicenseAcquisitionURL(
this.contentProtectionConfiguration,
MediaKindDrmWidevineContentProtectionIntegration.DEFAULT_LICENSE_URL,
);
return request;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ContentProtectionIntegration, ContentProtectionIntegrationFactory } from 'react-native-theoplayer';
import type { MediaKindDrmConfiguration } from './MediaKindDrmConfiguration';
import { MediaKindDrmWidevineContentProtectionIntegration } from './MediaKindDrmWidevineContentProtectionIntegration';

export class MediaKindDrmWidevineContentProtectionIntegrationFactory implements ContentProtectionIntegrationFactory {
build(configuration: MediaKindDrmConfiguration): ContentProtectionIntegration {
return new MediaKindDrmWidevineContentProtectionIntegration(configuration);
}
}
7 changes: 7 additions & 0 deletions drm/src/mediakind/barrel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from './MediaKindDrmConfiguration';
export * from './MediaKindDrmFairplayContentProtectionIntegration';
export * from './MediaKindDrmFairplayContentProtectionIntegrationFactory';
export * from './MediaKindDrmWidevineContentProtectionIntegration';
export * from './MediaKindDrmWidevineContentProtectionIntegrationFactory';
export * from './MediaKindDrmPlayReadyContentProtectionIntegration';
export * from './MediaKindDrmPlayReadyContentProtectionIntegrationFactory';
89 changes: 89 additions & 0 deletions drm/src/utils/drmUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type {
DRMConfiguration,
FairPlayKeySystemConfiguration,
WidevineKeySystemConfiguration,
PlayReadyKeySystemConfiguration,
KeySystemConfiguration,
} from 'react-native-theoplayer';

/**
* Appends query parameters to a URL, merging global DRM-level params with key-system-specific params.
* Key-system-specific params take precedence over global params; no duplicate keys are added.
*
* @param url - The base URL to append query parameters to.
* @param config - The top-level DRMConfiguration containing optional global `queryParameters`.
* @param keySystemConfig - The key-system-specific configuration containing optional `queryParameters`.
* @returns The URL with merged query parameters appended.
*/
function appendQueryParameters(url: string, config: DRMConfiguration, keySystemConfig: KeySystemConfiguration | undefined): string {
const merged: { [key: string]: any } = {
...config.queryParameters,
...keySystemConfig?.queryParameters,
};

const entries = Object.entries(merged);
if (entries.length === 0) {
return url;
}

const separator = url.includes('?') ? '&' : '?';
const queryString = entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
return `${url}${separator}${queryString}`;
}

/**
* Returns the FairPlay certificate URL from the DRM configuration with query parameters appended.
*
* @param config - The DRM configuration.
* @param defaultUrl - Fallback URL if no certificateURL is set on the fairplay config.
* @returns The resolved certificate URL with merged query parameters.
*/
export function getFairplayCertificateURL(config: DRMConfiguration & { fairplay?: FairPlayKeySystemConfiguration }, defaultUrl: string): string {
const baseUrl = config.fairplay?.certificateURL ?? defaultUrl;
return appendQueryParameters(baseUrl, config, config.fairplay);
}

/**
* Returns the FairPlay license acquisition URL from the DRM configuration with query parameters appended.
*
* @param config - The DRM configuration.
* @param defaultUrl - Fallback URL if no licenseAcquisitionURL is set on the fairplay config.
* @returns The resolved license acquisition URL with merged query parameters.
*/
export function getFairplayLicenseAcquisitionURL(
config: DRMConfiguration & { fairplay?: FairPlayKeySystemConfiguration },
defaultUrl: string,
): string {
const baseUrl = config.fairplay?.licenseAcquisitionURL ?? defaultUrl;
return appendQueryParameters(baseUrl, config, config.fairplay);
}

/**
* Returns the Widevine license acquisition URL from the DRM configuration with query parameters appended.
*
* @param config - The DRM configuration.
* @param defaultUrl - Fallback URL if no licenseAcquisitionURL is set on the widevine config.
* @returns The resolved license acquisition URL with merged query parameters.
*/
export function getWidevineLicenseAcquisitionURL(
config: DRMConfiguration & { widevine?: WidevineKeySystemConfiguration },
defaultUrl: string,
): string {
const baseUrl = config.widevine?.licenseAcquisitionURL ?? defaultUrl;
return appendQueryParameters(baseUrl, config, config.widevine);
}

/**
* Returns the PlayReady license acquisition URL from the DRM configuration with query parameters appended.
*
* @param config - The DRM configuration.
* @param defaultUrl - Fallback URL if no licenseAcquisitionURL is set on the playready config.
* @returns The resolved license acquisition URL with merged query parameters.
*/
export function getPlayReadyLicenseAcquisitionURL(
config: DRMConfiguration & { playready?: PlayReadyKeySystemConfiguration },
defaultUrl: string,
): string {
const baseUrl = config.playready?.licenseAcquisitionURL ?? defaultUrl;
return appendQueryParameters(baseUrl, config, config.playready);
}
Loading