Skip to content

Commit dace77e

Browse files
ignasio-muxR-Delfino95david-shibley-contentful
authored
feat: add DRM support mux app (#10376)
* drm support * Delete apps/mux/exampledrm.html * not defaulting drm * fixing error when DRM ID invalid * external player for preview * Fixed DRM not being created when switching between DRM and Public/Signed. Fixed alert case. * Refactor. Removed getDRMLicenseToken action and use getSignedURLTokens instead. Fixed iframe loading conflict with chromecast. * Updated onPublish action to set the drmPlaybackId * Added restriction to not let the user use DRM for audio-only * Fixed case when trying to play DRM video with no signing keys. Modified playback DRM text for that. * Changed description message * Fixed case when no signed or DRM are selected in the app setup. * Fixed prettier errors * Fixed prettier errors --------- Co-authored-by: Renzo Delfino <75499398+R-Delfino95@users.noreply.github.com> Co-authored-by: david-shibley-contentful <149433784+david-shibley-contentful@users.noreply.github.com>
1 parent 0a5148a commit dace77e

File tree

12 files changed

+665
-117
lines changed

12 files changed

+665
-117
lines changed

apps/mux/frontend/package-lock.json

Lines changed: 87 additions & 35 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/mux/frontend/src/components/AssetConfiguration/MuxAssetConfigurationModal.tsx

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { FC, useState, useEffect } from 'react';
2-
import { Modal, Button, Accordion } from '@contentful/f36-components';
1+
import { FC, useState, useEffect, useMemo } from 'react';
2+
import { Modal, Button, Accordion, Note } from '@contentful/f36-components';
33
import { VideoQualitySelector } from './VideoQualitySelector';
44
import { PlaybackPolicySelector } from './PlaybackPolicySelector';
55
import { CaptionsConfiguration, CaptionsConfig } from './CaptionsConfiguration';
@@ -8,6 +8,41 @@ import MetadataConfiguration, { MetadataConfig } from './MetadataConfiguration';
88
import { MuxContentfulObject, PolicyType } from '../../util/types';
99
import { FieldExtensionSDK } from '@contentful/app-sdk';
1010

11+
// Audio file extensions for detection
12+
const AUDIO_EXTENSIONS = [
13+
'.mp3',
14+
'.wav',
15+
'.m4a',
16+
'.aac',
17+
'.ogg',
18+
'.flac',
19+
'.wma',
20+
'.aiff',
21+
'.opus',
22+
];
23+
24+
/**
25+
* Detects if the input is an audio-only file based on:
26+
* - File MIME type (for uploaded files)
27+
* - URL extension (for remote URLs)
28+
*/
29+
const isAudioFile = (file: File | null, url: string | null): boolean => {
30+
// Check file MIME type
31+
if (file) {
32+
return file.type.startsWith('audio/');
33+
}
34+
35+
// Check URL extension
36+
if (url) {
37+
const urlLower = url.toLowerCase();
38+
// Remove query params and hash for extension check
39+
const cleanUrl = urlLower.split('?')[0].split('#')[0];
40+
return AUDIO_EXTENSIONS.some((ext) => cleanUrl.endsWith(ext));
41+
}
42+
43+
return false;
44+
};
45+
1146
export interface ModalData {
1247
videoQuality: string;
1348
playbackPolicies: PolicyType[];
@@ -22,10 +57,15 @@ interface MuxAssetConfigurationModalProps {
2257
onConfirm: (data: ModalData) => void;
2358
installationParams: {
2459
muxEnableSignedUrls: boolean;
60+
muxEnableDRM?: boolean;
2561
};
2662
isEditMode?: boolean;
2763
asset?: MuxContentfulObject;
2864
sdk: FieldExtensionSDK;
65+
/** File being uploaded (from drag & drop or file picker) */
66+
file?: File | null;
67+
/** URL for remote upload */
68+
pendingUploadURL?: string | null;
2969
}
3070

3171
const ModalContent: FC<MuxAssetConfigurationModalProps> = ({
@@ -36,12 +76,29 @@ const ModalContent: FC<MuxAssetConfigurationModalProps> = ({
3676
isEditMode = false,
3777
asset,
3878
sdk,
79+
file = null,
80+
pendingUploadURL = null,
3981
}) => {
40-
const { muxEnableSignedUrls } = installationParams;
82+
// Use explicit defaults to handle undefined values from SDK
83+
const muxEnableSignedUrls = installationParams.muxEnableSignedUrls ?? false;
84+
const muxEnableDRM = installationParams.muxEnableDRM ?? false;
85+
86+
// Detect if the input is an audio-only file
87+
const isAudioOnly = useMemo(() => isAudioFile(file, pendingUploadURL), [file, pendingUploadURL]);
88+
89+
// DRM is disabled for audio files
90+
const effectiveDRMEnabled = muxEnableDRM && !isAudioOnly;
91+
92+
// Determine default policy: if DRM was enabled but this is audio, fall back to signed (if available) or public
93+
const getDefaultPolicy = (): PolicyType => {
94+
if (effectiveDRMEnabled) return 'drm';
95+
if (muxEnableSignedUrls) return 'signed';
96+
return 'public';
97+
};
4198

4299
const [modalData, setModalData] = useState<ModalData>({
43100
videoQuality: 'plus',
44-
playbackPolicies: muxEnableSignedUrls ? ['signed'] : ['public'],
101+
playbackPolicies: [getDefaultPolicy()],
45102
captionsConfig: {
46103
captionsType: 'off',
47104
languageCode: null,
@@ -61,11 +118,30 @@ const ModalContent: FC<MuxAssetConfigurationModalProps> = ({
61118
},
62119
});
63120

121+
// Update policy when audio detection changes (e.g., when modal opens with new file)
122+
useEffect(() => {
123+
setModalData((prev) => {
124+
if (isAudioOnly && prev.playbackPolicies.includes('drm')) {
125+
// If current policy is DRM but this is audio, switch to signed or public
126+
const fallbackPolicy: PolicyType = muxEnableSignedUrls ? 'signed' : 'public';
127+
return { ...prev, playbackPolicies: [fallbackPolicy] };
128+
}
129+
return prev;
130+
});
131+
}, [isAudioOnly, muxEnableSignedUrls]);
132+
64133
useEffect(() => {
65134
if (isEditMode && asset) {
135+
// Determine the current playback policy
136+
const currentPolicy: PolicyType = asset.drmPlaybackId
137+
? 'drm'
138+
: asset.signedPlaybackId
139+
? 'signed'
140+
: 'public';
141+
66142
setModalData({
67143
videoQuality: 'plus',
68-
playbackPolicies: asset.signedPlaybackId ? ['signed'] : ['public'],
144+
playbackPolicies: [currentPolicy],
69145
captionsConfig: {
70146
captionsType: 'off',
71147
languageCode: null,
@@ -130,15 +206,23 @@ const ModalContent: FC<MuxAssetConfigurationModalProps> = ({
130206

131207
{!isEditMode && (
132208
<Accordion.Item title="Privacy Settings">
209+
{isAudioOnly && (
210+
<Note variant="warning" style={{ marginBottom: '1rem' }}>
211+
Audio files do not support DRM protection. Please use the Protected option for
212+
secure playback.
213+
</Note>
214+
)}
133215
<PlaybackPolicySelector
134216
selectedPolicies={modalData.playbackPolicies}
135217
onPoliciesChange={(policies) =>
136218
setModalData((prev) => ({ ...prev, playbackPolicies: policies }))
137219
}
138220
enableSignedUrls={muxEnableSignedUrls}
221+
enableDRM={effectiveDRMEnabled}
139222
onValidationChange={(isValid) =>
140223
handleValidationChange('playbackPolicies', isValid)
141224
}
225+
isAudioOnly={isAudioOnly}
142226
/>
143227
</Accordion.Item>
144228
)}

apps/mux/frontend/src/components/AssetConfiguration/Playback/PlaybackSwitcher.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ interface PlaybackSwitcherProps {
88
value: MuxContentfulObject;
99
onSwapPlaybackIDs: (policy: PolicyType) => void;
1010
enableSignedUrls: boolean;
11+
enableDRM?: boolean;
1112
}
1213

1314
function isUsingSigned(value: MuxContentfulObject): boolean {
1415
return !!(value && value.signedPlaybackId && !value.playbackId);
1516
}
1617

18+
function isUsingDRM(value: MuxContentfulObject): boolean {
19+
return !!(value && value.drmPlaybackId);
20+
}
21+
1722
function getCurrentPolicy(value: MuxContentfulObject): PolicyType {
1823
if (value?.pendingActions?.create) {
1924
const playbackCreateAction = value.pendingActions.create.find(
@@ -23,15 +28,21 @@ function getCurrentPolicy(value: MuxContentfulObject): PolicyType {
2328
return playbackCreateAction.data?.policy as PolicyType;
2429
}
2530
}
26-
return isUsingSigned(value) ? 'signed' : 'public';
31+
// Priority: public > signed > drm
32+
if (value?.playbackId) return 'public';
33+
if (isUsingSigned(value)) return 'signed';
34+
if (isUsingDRM(value)) return 'drm';
35+
return 'public';
2736
}
2837

2938
export const PlaybackSwitcher: React.FC<PlaybackSwitcherProps> = ({
3039
value,
3140
onSwapPlaybackIDs,
3241
enableSignedUrls,
42+
enableDRM = false,
3343
}) => {
3444
const selectedPolicy = getCurrentPolicy(value);
45+
const isAudioOnly = value?.audioOnly ?? false;
3546

3647
return (
3748
<Flex>
@@ -44,6 +55,8 @@ export const PlaybackSwitcher: React.FC<PlaybackSwitcherProps> = ({
4455
}
4556
}}
4657
enableSignedUrls={enableSignedUrls}
58+
enableDRM={enableDRM}
59+
isAudioOnly={isAudioOnly}
4760
/>
4861
</Flex>
4962
);

apps/mux/frontend/src/components/AssetConfiguration/PlaybackPolicySelector.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ import { PolicyType } from '../../util/types';
66
interface PlaybackPolicySelectorProps {
77
selectedPolicies: PolicyType[];
88
onPoliciesChange: (policies: PolicyType[]) => void;
9-
enableSignedUrls: boolean;
9+
enableSignedUrls?: boolean;
10+
enableDRM?: boolean;
1011
onValidationChange?: (isValid: boolean) => void;
12+
/** When true, DRM option is disabled because audio files don't support DRM */
13+
isAudioOnly?: boolean;
1114
}
1215

1316
const playbackPolicyLink =
@@ -16,8 +19,10 @@ const playbackPolicyLink =
1619
export const PlaybackPolicySelector: FC<PlaybackPolicySelectorProps> = ({
1720
selectedPolicies,
1821
onPoliciesChange,
19-
enableSignedUrls,
22+
enableSignedUrls = false,
23+
enableDRM = false,
2024
onValidationChange,
25+
isAudioOnly = false,
2126
}) => {
2227
const handlePolicyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
2328
const value = event.target.value as PolicyType;
@@ -65,6 +70,23 @@ export const PlaybackPolicySelector: FC<PlaybackPolicySelectorProps> = ({
6570
/>
6671
</FormControl.HelpText>
6772
</FormControl>
73+
74+
<FormControl marginBottom="none">
75+
<Radio value="drm" isDisabled={!enableDRM || isAudioOnly}>
76+
DRM Protected
77+
</Radio>
78+
<FormControl.HelpText>
79+
Highest level of content protection using industry-standard encryption. Requires DRM to
80+
be enabled in app configuration.
81+
<TextLink
82+
icon={<ExternalLinkIcon />}
83+
variant="secondary"
84+
href="https://www.mux.com/blog/protect-your-video-content-with-drm-now-ga"
85+
target="_blank"
86+
rel="noopener noreferrer"
87+
/>
88+
</FormControl.HelpText>
89+
</FormControl>
6890
</Radio.Group>
6991
{selectedPolicies.length === 0 && (
7092
<FormControl.ValidationMessage>

apps/mux/frontend/src/components/PlayerCode.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const PlayerCode: React.FC<PlayerCodeProps> = ({ params }) => {
2727
const playbackToken = getParam('playback-token');
2828
const thumbnailToken = getParam('thumbnail-token');
2929
const storyboardToken = getParam('storyboard-token');
30+
const drmToken = getParam('drm-token');
3031
const audio = getParam('audio');
3132
const customDomain = getParam('custom-domain');
3233
const streamType = getParam('stream-type');
@@ -38,6 +39,7 @@ const PlayerCode: React.FC<PlayerCodeProps> = ({ params }) => {
3839
playbackToken ? `playback-token="${playbackToken}"` : '',
3940
thumbnailToken ? `thumbnail-token="${thumbnailToken}"` : '',
4041
storyboardToken ? `storyboard-token="${storyboardToken}"` : '',
42+
drmToken ? `drm-token="${drmToken}"` : '',
4143
audio ? `audio="${audio}"` : '',
4244
customDomain ? `custom-domain="${customDomain}"` : '',
4345
autoplay ? 'autoplay' : '',
@@ -48,6 +50,8 @@ const PlayerCode: React.FC<PlayerCodeProps> = ({ params }) => {
4850
.filter(Boolean)
4951
.join(' ');
5052

53+
const muxPlayerCode = `<script src="https://unpkg.com/@mux/mux-player"></script>\n\n<mux-player ${muxPlayerAttrs} style="width:100%" />`;
54+
5155
let iframeSrcBase =
5256
customDomain && customDomain !== 'mux.com'
5357
? `https://${customDomain}/${playbackId}`
@@ -80,7 +84,6 @@ const PlayerCode: React.FC<PlayerCodeProps> = ({ params }) => {
8084
.filter(Boolean)
8185
.join(' ');
8286

83-
const muxPlayerCode = `<script src="https://unpkg.com/@mux/mux-player"></script>\n\n<mux-player ${muxPlayerAttrs} style="width:100%" />`;
8487
const iframeCode = `<iframe ${iframeAttrs} style="aspect-ratio: 16/9; width: 100%; border: 0;"></iframe>`;
8588

8689
const codeSnippet = codeType === 'mux-player' ? muxPlayerCode : iframeCode;

0 commit comments

Comments
 (0)