diff --git a/apps/mux/contentful-app-manifest.json b/apps/mux/contentful-app-manifest.json index d2c9b7e415..74633420e6 100644 --- a/apps/mux/contentful-app-manifest.json +++ b/apps/mux/contentful-app-manifest.json @@ -17,6 +17,15 @@ "entryFile": "functions/src/getSignedUrlTokens.ts", "allowNetworks": ["api.contentful.com", "https://api.mux.com"], "accepts": ["appaction.call"] + }, + { + "id": "getDRMLicenseTokens", + "name": "Get DRM license tokens", + "description": "This action returns DRM license tokens for securing Mux video playback with DRM protection.", + "path": "functions/src/getDRMLicenseTokens.js", + "entryFile": "functions/src/getDRMLicenseTokens.ts", + "allowNetworks": ["api.contentful.com", "https://api.mux.com"], + "accepts": ["appaction.call"] } ] } diff --git a/apps/mux/frontend/package-lock.json b/apps/mux/frontend/package-lock.json index 321b73f936..bd8714d893 100644 --- a/apps/mux/frontend/package-lock.json +++ b/apps/mux/frontend/package-lock.json @@ -46,6 +46,7 @@ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -97,7 +98,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -404,7 +404,6 @@ "version": "4.12.0", "resolved": "https://registry.npmjs.org/@contentful/app-sdk/-/app-sdk-4.12.0.tgz", "integrity": "sha512-Y8kL+S/ozFZ0QL/lR9EvNKX7be+2jf4I0Q0YcUNyFyd3Q4h8soez7nRt8lcY4RomyF2fJPO+QIxJ+7n7INS7Uw==", - "peer": true, "peerDependencies": { "contentful-management": ">=7.30.0" } @@ -1628,6 +1627,7 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, + "peer": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -1651,6 +1651,7 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true, + "peer": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -1660,6 +1661,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, + "peer": true, "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", @@ -1674,6 +1676,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "peer": true, "engines": { "node": ">=12.22" }, @@ -1686,7 +1689,8 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@jest/expect-utils": { "version": "28.1.3", @@ -1927,7 +1931,6 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -2698,8 +2701,7 @@ "version": "18.7.14", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.14.tgz", "integrity": "sha512-6bbDaETVi8oyIARulOE9qF1/Qdi/23z6emrUh0fNJRUmjznqrixD4MpGDdgOFk5Xb0m2H6Xu42JGdvAxaJR/wA==", - "devOptional": true, - "peer": true + "devOptional": true }, "node_modules/@types/parse-json": { "version": "4.0.2", @@ -2715,7 +2717,6 @@ "version": "17.0.39", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.39.tgz", "integrity": "sha512-UVavlfAxDd/AgAacMa60Azl7ygyQNRwC/DsHZmKgNvPmRR5p70AJ5Q9EAmL2NWOJmeV+vVUI4IAP7GZrN8h8Ug==", - "peer": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -2727,7 +2728,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz", "integrity": "sha512-f96K3k+24RaLGVu/Y2Ng3e1EbZ8/cVJvypZWd7cy0ofCBaf2lcM46xNhycMZ2xGwbBjRql7hOlZ+e2WlJ5MH3Q==", "devOptional": true, - "peer": true, "dependencies": { "@types/react": "*" } @@ -2777,7 +2777,8 @@ "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@types/yargs": { "version": "17.0.32", @@ -2833,7 +2834,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -2987,7 +2987,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", @@ -3133,6 +3134,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "peer": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -3161,6 +3163,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3223,7 +3226,8 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "peer": true }, "node_modules/aria-query": { "version": "5.1.3", @@ -3347,7 +3351,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/base64-js": { "version": "1.5.1", @@ -3412,6 +3417,7 @@ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3448,7 +3454,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -3713,13 +3718,13 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/contentful-management": { "version": "11.35.1", "resolved": "https://registry.npmjs.org/contentful-management/-/contentful-management-11.35.1.tgz", "integrity": "sha512-PBOFpeOCzwx7+PQtHhgFRNB8wnlgUKUj+3rTucaMIYot5l9YA4804P9VYWq6Mg8/PJnFjavQrtay6HtqWDyYMw==", - "peer": true, "dependencies": { "@contentful/rich-text-types": "^16.6.1", "axios": "^1.7.4", @@ -3889,7 +3894,6 @@ "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "peer": true, "dependencies": { "@babel/runtime": "^7.21.0" }, @@ -3972,7 +3976,8 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/defaults": { "version": "1.0.4", @@ -4065,6 +4070,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, + "peer": true, "dependencies": { "esutils": "^2.0.2" }, @@ -4380,6 +4386,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -4392,6 +4399,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -4408,6 +4416,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "peer": true, "engines": { "node": ">=4.0" } @@ -4417,6 +4426,7 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "peer": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -4434,6 +4444,7 @@ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, + "peer": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -4446,6 +4457,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "peer": true, "engines": { "node": ">=4.0" } @@ -4494,6 +4506,7 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4561,7 +4574,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "peer": true }, "node_modules/fast-glob": { "version": "3.3.2", @@ -4595,13 +4609,15 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/fastq": { "version": "1.17.1", @@ -4631,6 +4647,7 @@ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, + "peer": true, "dependencies": { "flat-cache": "^3.0.4" }, @@ -4660,6 +4677,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "peer": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -4676,6 +4694,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, + "peer": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -4689,7 +4708,8 @@ "version": "3.2.9", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/focus-lock": { "version": "1.3.5", @@ -4749,7 +4769,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -4832,6 +4853,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -4852,6 +4874,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "peer": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -4873,6 +4896,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "peer": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -4888,6 +4912,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -4945,6 +4970,7 @@ "integrity": "sha512-qn+rKOW7KWpVTtgIUi6RVmTBZJSe2k0Db0vh1f7CWrWclkkc7/Q+FrOfkZIb2eiErLyqu5AXEzE7XthO9JVxRA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@types/node": "^20.0.0", "@types/whatwg-mimetype": "^3.0.2", @@ -4960,6 +4986,7 @@ "integrity": "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -5130,6 +5157,7 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "peer": true, "engines": { "node": ">=0.8.19" } @@ -5148,6 +5176,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, + "peer": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -5414,6 +5443,7 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, + "peer": true, "engines": { "node": ">=8" } @@ -5753,6 +5783,7 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -5845,7 +5876,8 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -5856,13 +5888,15 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/json5": { "version": "2.2.3", @@ -5881,6 +5915,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "peer": true, "dependencies": { "json-buffer": "3.0.1" } @@ -5899,6 +5934,7 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "peer": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -5917,6 +5953,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "peer": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -5946,7 +5983,8 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/log-symbols": { "version": "4.1.0", @@ -6102,6 +6140,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6147,7 +6186,8 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/natural-compare-lite": { "version": "1.4.0", @@ -6249,6 +6289,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "peer": true, "dependencies": { "wrappy": "1" } @@ -6288,6 +6329,7 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, + "peer": true, "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -6335,6 +6377,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "peer": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -6350,6 +6393,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "peer": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -6420,6 +6464,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "peer": true, "engines": { "node": ">=8" } @@ -6429,6 +6474,7 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6545,6 +6591,7 @@ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "peer": true, "engines": { "node": ">= 0.8.0" } @@ -6649,7 +6696,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -6698,7 +6744,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -6906,6 +6951,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -7276,6 +7322,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "peer": true, "engines": { "node": ">=8" }, @@ -7314,7 +7361,8 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/through": { "version": "2.3.8", @@ -7452,6 +7500,7 @@ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "peer": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -7475,7 +7524,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7489,7 +7537,8 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/update-browserslist-db": { "version": "1.1.3", @@ -7526,6 +7575,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "peer": true, "dependencies": { "punycode": "^2.1.0" } @@ -7593,7 +7643,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -7797,6 +7846,7 @@ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", "optional": true, + "peer": true, "engines": { "node": ">=12" } @@ -7911,7 +7961,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "dev": true, + "peer": true }, "node_modules/ws": { "version": "8.18.0", @@ -7984,6 +8035,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, diff --git a/apps/mux/frontend/src/components/AssetConfiguration/MuxAssetConfigurationModal.tsx b/apps/mux/frontend/src/components/AssetConfiguration/MuxAssetConfigurationModal.tsx index c3f973b353..e3570c25de 100644 --- a/apps/mux/frontend/src/components/AssetConfiguration/MuxAssetConfigurationModal.tsx +++ b/apps/mux/frontend/src/components/AssetConfiguration/MuxAssetConfigurationModal.tsx @@ -22,6 +22,7 @@ interface MuxAssetConfigurationModalProps { onConfirm: (data: ModalData) => void; installationParams: { muxEnableSignedUrls: boolean; + muxEnableDRM?: boolean; }; isEditMode?: boolean; asset?: MuxContentfulObject; @@ -37,7 +38,7 @@ const ModalContent: FC = ({ asset, sdk, }) => { - const { muxEnableSignedUrls } = installationParams; + const { muxEnableSignedUrls, muxEnableDRM } = installationParams; const [modalData, setModalData] = useState({ videoQuality: 'plus', @@ -65,7 +66,11 @@ const ModalContent: FC = ({ if (isEditMode && asset) { setModalData({ videoQuality: 'plus', - playbackPolicies: asset.signedPlaybackId ? ['signed'] : ['public'], + playbackPolicies: asset.drmPlaybackId + ? ['drm'] + : asset.signedPlaybackId + ? ['signed'] + : ['public'], captionsConfig: { captionsType: 'off', languageCode: null, @@ -136,6 +141,7 @@ const ModalContent: FC = ({ setModalData((prev) => ({ ...prev, playbackPolicies: policies })) } enableSignedUrls={muxEnableSignedUrls} + enableDRM={muxEnableDRM} onValidationChange={(isValid) => handleValidationChange('playbackPolicies', isValid) } diff --git a/apps/mux/frontend/src/components/AssetConfiguration/Playback/PlaybackSwitcher.tsx b/apps/mux/frontend/src/components/AssetConfiguration/Playback/PlaybackSwitcher.tsx index 5c4263659c..801d068317 100644 --- a/apps/mux/frontend/src/components/AssetConfiguration/Playback/PlaybackSwitcher.tsx +++ b/apps/mux/frontend/src/components/AssetConfiguration/Playback/PlaybackSwitcher.tsx @@ -8,12 +8,17 @@ interface PlaybackSwitcherProps { value: MuxContentfulObject; onSwapPlaybackIDs: (policy: PolicyType) => void; enableSignedUrls: boolean; + enableDRM?: boolean; } function isUsingSigned(value: MuxContentfulObject): boolean { return !!(value && value.signedPlaybackId && !value.playbackId); } +function isUsingDRM(value: MuxContentfulObject): boolean { + return !!(value && value.drmPlaybackId); +} + function getCurrentPolicy(value: MuxContentfulObject): PolicyType { if (value?.pendingActions?.create) { const playbackCreateAction = value.pendingActions.create.find( @@ -23,13 +28,18 @@ function getCurrentPolicy(value: MuxContentfulObject): PolicyType { return playbackCreateAction.data?.policy as PolicyType; } } - return isUsingSigned(value) ? 'signed' : 'public'; + // Priority: public > signed > drm + if (value?.playbackId) return 'public'; + if (isUsingSigned(value)) return 'signed'; + if (isUsingDRM(value)) return 'drm'; + return 'public'; } export const PlaybackSwitcher: React.FC = ({ value, onSwapPlaybackIDs, enableSignedUrls, + enableDRM = false, }) => { const selectedPolicy = getCurrentPolicy(value); @@ -44,6 +54,7 @@ export const PlaybackSwitcher: React.FC = ({ } }} enableSignedUrls={enableSignedUrls} + enableDRM={enableDRM} /> ); diff --git a/apps/mux/frontend/src/components/AssetConfiguration/PlaybackPolicySelector.tsx b/apps/mux/frontend/src/components/AssetConfiguration/PlaybackPolicySelector.tsx index 4af0c04ef4..43742fce47 100644 --- a/apps/mux/frontend/src/components/AssetConfiguration/PlaybackPolicySelector.tsx +++ b/apps/mux/frontend/src/components/AssetConfiguration/PlaybackPolicySelector.tsx @@ -7,6 +7,7 @@ interface PlaybackPolicySelectorProps { selectedPolicies: PolicyType[]; onPoliciesChange: (policies: PolicyType[]) => void; enableSignedUrls: boolean; + enableDRM?: boolean; onValidationChange?: (isValid: boolean) => void; } @@ -17,6 +18,7 @@ export const PlaybackPolicySelector: FC = ({ selectedPolicies, onPoliciesChange, enableSignedUrls, + enableDRM = false, onValidationChange, }) => { const handlePolicyChange = (event: React.ChangeEvent) => { @@ -65,6 +67,22 @@ export const PlaybackPolicySelector: FC = ({ /> + + + + DRM Protected + + + Highest level of content protection using industry-standard encryption. Requires DRM to be enabled in app configuration. + } + variant="secondary" + href="https://www.mux.com/blog/protect-your-video-content-with-drm-now-ga" + target="_blank" + rel="noopener noreferrer" + /> + + {selectedPolicies.length === 0 && ( diff --git a/apps/mux/frontend/src/components/PlayerCode.tsx b/apps/mux/frontend/src/components/PlayerCode.tsx index bec923616b..e2e2cdffd9 100644 --- a/apps/mux/frontend/src/components/PlayerCode.tsx +++ b/apps/mux/frontend/src/components/PlayerCode.tsx @@ -27,6 +27,7 @@ const PlayerCode: React.FC = ({ params }) => { const playbackToken = getParam('playback-token'); const thumbnailToken = getParam('thumbnail-token'); const storyboardToken = getParam('storyboard-token'); + const drmToken = getParam('drm-token'); const audio = getParam('audio'); const customDomain = getParam('custom-domain'); const streamType = getParam('stream-type'); @@ -38,6 +39,7 @@ const PlayerCode: React.FC = ({ params }) => { playbackToken ? `playback-token="${playbackToken}"` : '', thumbnailToken ? `thumbnail-token="${thumbnailToken}"` : '', storyboardToken ? `storyboard-token="${storyboardToken}"` : '', + drmToken ? `drm-token="${drmToken}"` : '', audio ? `audio="${audio}"` : '', customDomain ? `custom-domain="${customDomain}"` : '', autoplay ? 'autoplay' : '', @@ -48,6 +50,8 @@ const PlayerCode: React.FC = ({ params }) => { .filter(Boolean) .join(' '); + const muxPlayerCode = `\n\n`; + let iframeSrcBase = customDomain && customDomain !== 'mux.com' ? `https://${customDomain}/${playbackId}` @@ -80,7 +84,6 @@ const PlayerCode: React.FC = ({ params }) => { .filter(Boolean) .join(' '); - const muxPlayerCode = `\n\n`; const iframeCode = ``; const codeSnippet = codeType === 'mux-player' ? muxPlayerCode : iframeCode; diff --git a/apps/mux/frontend/src/index.tsx b/apps/mux/frontend/src/index.tsx index ba392075e0..47b1686507 100755 --- a/apps/mux/frontend/src/index.tsx +++ b/apps/mux/frontend/src/index.tsx @@ -35,6 +35,7 @@ import { type AppState, AppProps, ResolutionType, + PolicyType, Track, ResyncParams, PendingActions, @@ -103,6 +104,7 @@ export class App extends React.Component { fileInputRef = React.createRef(); muxPlayerRef = React.createRef(); // eslint-disable-line @typescript-eslint/no-explicit-any getSignedTokenActionId: string; + getDRMLicenseTokenActionId: string; private pollPending = false; constructor(props: AppProps) { @@ -124,6 +126,7 @@ export class App extends React.Component { ); this.getSignedTokenActionId = ''; + this.getDRMLicenseTokenActionId = ''; const field = props.sdk.field.getValue(); this.state = { @@ -135,8 +138,9 @@ export class App extends React.Component { "It doesn't look like you've specified your Mux Access Token ID or Secret in the extension configuration.", errorShowResetAction: false, playerPlaybackId: - field && ('playbackId' in field || 'signedPlaybackId' in field) - ? field.playbackId || field.signedPlaybackId + field && + ('playbackId' in field || 'signedPlaybackId' in field || 'drmPlaybackId' in field) + ? field.playbackId || field.signedPlaybackId || field.drmPlaybackId : undefined, modalAssetConfigurationVisible: false, file: null, @@ -149,6 +153,7 @@ export class App extends React.Component { playbackToken: undefined, posterToken: undefined, storyboardToken: undefined, + drmLicenseToken: undefined, raw: undefined, }; } @@ -210,8 +215,12 @@ export class App extends React.Component { environmentId: this.props.sdk.ids.environment, spaceId: this.props.sdk.ids.space, }); + this.getSignedTokenActionId = appActionsResponse.items.find((x) => x.name === 'getSignedUrlTokens')?.sys.id ?? ''; + + this.getDRMLicenseTokenActionId = + appActionsResponse.items.find((x) => x.name === 'getDRMLicenseTokens')?.sys.id ?? ''; this.props.sdk.window.startAutoResizer(); @@ -247,7 +256,10 @@ export class App extends React.Component { if (this.state.value.ready) { await this.checkForValidAsset(); - if (this.isUsingSigned() && this.state.value.signedPlaybackId) { + if (this.isUsingDRM() && this.state.value.drmPlaybackId) { + await this.setDRMPlayback(this.state.value.drmPlaybackId); + this.setState({ playerPlaybackId: this.state.value.drmPlaybackId }); + } else if (this.isUsingSigned() && this.state.value.signedPlaybackId) { await this.setSignedPlayback(this.state.value.signedPlaybackId); this.setState({ playerPlaybackId: this.state.value.signedPlaybackId }); } @@ -294,6 +306,11 @@ export class App extends React.Component { : false; }; + isUsingDRM = (): boolean => { + // Check if DRM playback ID is set + return this.state.value && this.state.value.drmPlaybackId ? true : false; + }; + getSwitchCheckedState = (): boolean => { // If there are pending actions of playback, use the state of the pending action if (this.state.value?.pendingActions?.create) { @@ -412,6 +429,13 @@ export class App extends React.Component { options, async (res) => await this.responseCheck(res) ); + + if (!muxUploadUrl) { + // Adding this fallback so the upload won't fail when the DRM Configuration ID is invalid + this.setState({ modalAssetConfigurationVisible: false }); + return; + } + const uploader = this.muxUploaderRef.current!; uploader.endpoint = muxUploadUrl; @@ -543,6 +567,67 @@ export class App extends React.Component { this.setState({ playbackToken, posterToken, storyboardToken }); }; + private fetchDRMLicenseToken = async (playbackId: string): Promise<{ licenseToken: string; playbackToken: string; posterToken: string; storyboardToken: string }> => { + this.setState({ isTokenLoading: true }); + + try { + if (!this.getDRMLicenseTokenActionId) { + throw new Error('App Action for Get DRM License Token not found.'); + } + + const { + response: { body }, + } = await this.cmaClient.appActionCall.createWithResponse( + { + organizationId: this.props.sdk.ids.organization, + appDefinitionId: this.props.sdk.ids.app!, + appActionId: this.getDRMLicenseTokenActionId, + }, + { parameters: { playbackId } } + ); + const parsedBody = JSON.parse(body); + if (!parsedBody.ok) { + const errorMessage = parsedBody.error || parsedBody.message || 'Unknown error'; + throw new Error(errorMessage); + } + + const licenseToken = parsedBody.data.licenseToken as string; + const playbackToken = parsedBody.data.playbackToken as string; + const posterToken = parsedBody.data.posterToken as string; + const storyboardToken = parsedBody.data.storyboardToken as string; + this.setState({ + isTokenLoading: false, + drmLicenseToken: licenseToken, + playbackToken: playbackToken, + posterToken: posterToken, + storyboardToken: storyboardToken, + }); + return { licenseToken, playbackToken, posterToken, storyboardToken }; + } catch (e) { + console.error('Error fetching DRM license token:', e); + const errorMessage = e instanceof Error ? e.message : 'Failed to fetch DRM license token'; + this.setState({ + isTokenLoading: false, + error: `Error: ${errorMessage}. Please verify that the App Action is correctly linked to the getDRMLicenseTokens function.` + }); + return { licenseToken: '', playbackToken: '', posterToken: '', storyboardToken: '' }; + } + }; + + setDRMPlayback = async (drmPlaybackId: string) => { + const { muxSigningKeyId, muxSigningKeyPrivate } = this.props.sdk.parameters + .installation as InstallationParams; + if (!(muxSigningKeyId && muxSigningKeyPrivate)) { + this.setState({ + error: + 'Error: this asset was created with DRM protection, but signing keys do not exist for your account', + errorShowResetAction: true, + }); + return; + } + await this.fetchDRMLicenseToken(drmPlaybackId); + }; + getAsset = async (assetId: string) => { if (!assetId) { throw Error('Something went wrong, we cannot getAsset without an assetId.'); @@ -572,9 +657,20 @@ export class App extends React.Component { ); return false; - case !res.ok: - this.props.sdk.notifier.error(`API Error. ${res.status} ${res.statusText}`); + case !res.ok: { + // Try to get the specific error message from the response + try { + const errorData = await res.clone().json(); + if (errorData?.error?.messages?.[0]) { + this.props.sdk.notifier.error(errorData.error.messages[0]); + } else { + this.props.sdk.notifier.error(`API Error. ${res.status} ${res.statusText}`); + } + } catch { + this.props.sdk.notifier.error(`API Error. ${res.status} ${res.statusText}`); + } return false; + } default: return true; @@ -647,6 +743,9 @@ export class App extends React.Component { const signedPlayback = asset.playback_ids?.find( ({ policy }: { policy: string }) => policy === 'signed' ); + const drmPlayback = asset.playback_ids?.find( + ({ policy }: { policy: string }) => policy === 'drm' + ); const audioOnly = 'max_stored_resolution' in asset && asset.max_stored_resolution === 'Audio only'; @@ -694,6 +793,7 @@ export class App extends React.Component { assetId: this.state.value.assetId, playbackId: (publicPlayback && publicPlayback.id) || undefined, signedPlaybackId: (signedPlayback && signedPlayback.id) || undefined, + drmPlaybackId: (drmPlayback && drmPlayback.id) || undefined, ready: asset.status === 'ready', ratio: asset.aspect_ratio || undefined, max_stored_resolution: asset.max_stored_resolution || undefined, @@ -717,7 +817,9 @@ export class App extends React.Component { await this.props.sdk.field.setValue(newValue); } - if (publicPlayback && publicPlayback.id) { + if (drmPlayback && drmPlayback.id) { + this.setState({ playerPlaybackId: drmPlayback.id }); + } else if (publicPlayback && publicPlayback.id) { this.setState({ playerPlaybackId: publicPlayback.id }); } else if (signedPlayback && signedPlayback.id) { this.setState({ playerPlaybackId: signedPlayback.id }); @@ -725,6 +827,8 @@ export class App extends React.Component { if (signedPlayback) { await this.setSignedPlayback(signedPlayback.id); + } else if (drmPlayback) { + await this.setDRMPlayback(drmPlayback.id); } const renditionPreparing = asset.static_renditions?.files @@ -840,7 +944,10 @@ export class App extends React.Component { const params = [ { name: 'playback-id', - value: this.state.value.playbackId || this.state.value.signedPlaybackId, + value: + this.state.value.playbackId || + this.state.value.signedPlaybackId || + this.state.value.drmPlaybackId, }, { name: 'stream-type', @@ -867,6 +974,26 @@ export class App extends React.Component { } ); } + if (this.isUsingDRM() && this.state.playbackToken) { + params.push( + { + name: 'playback-token', + value: this.state.playbackToken, + }, + { + name: 'thumbnail-token', + value: this.state.posterToken, + }, + { + name: 'storyboard-token', + value: this.state.storyboardToken, + }, + { + name: 'drm-token', + value: this.state.drmLicenseToken, + } + ); + } if (this.state.value.audioOnly) { params.push({ name: 'audio', @@ -882,11 +1009,12 @@ export class App extends React.Component { return params; }; - swapPlaybackIDs = async (policy: 'public' | 'signed') => { + swapPlaybackIDs = async (policy: PolicyType) => { if (!this.state.value) return; const currentValue = this.state.value; - const currentPlaybackId = currentValue.playbackId || currentValue.signedPlaybackId; + const currentPlaybackId = + currentValue.playbackId || currentValue.signedPlaybackId || currentValue.drmPlaybackId; const totalPendingActions = (currentValue.pendingActions?.create?.length || 0) + (currentValue.pendingActions?.delete?.length || 0); @@ -901,33 +1029,32 @@ export class App extends React.Component { update: currentValue.pendingActions?.update ?? [], }; - const hasPending = - updatedPendingActions.create.length + updatedPendingActions.delete.length < - totalPendingActions; - - if (hasPending) { - await this.props.sdk.field.setValue( - updatePendingActions(currentValue, updatedPendingActions) + // Determine current policy considering pending actions + let currentPolicy: PolicyType; + if (currentValue.pendingActions?.create) { + const playbackCreateAction = currentValue.pendingActions.create.find( + (action) => action.type === 'playback' ); - return; - } - - const isCurrentlySigned = this.isUsingSigned(); - const currentPolicy = isCurrentlySigned ? 'signed' : 'public'; - let targetPolicy: 'public' | 'signed'; - - if (policy) { - if (policy === currentPolicy) return; - targetPolicy = policy; + if (playbackCreateAction) { + currentPolicy = playbackCreateAction.data?.policy as PolicyType; + } else { + const isCurrentlyDRM = this.isUsingDRM(); + const isCurrentlySigned = this.isUsingSigned(); + currentPolicy = isCurrentlyDRM ? 'drm' : isCurrentlySigned ? 'signed' : 'public'; + } } else { - targetPolicy = isCurrentlySigned ? 'public' : 'signed'; + const isCurrentlyDRM = this.isUsingDRM(); + const isCurrentlySigned = this.isUsingSigned(); + currentPolicy = isCurrentlyDRM ? 'drm' : isCurrentlySigned ? 'signed' : 'public'; } + if (policy === currentPolicy) return; + updatedPendingActions.delete.push({ type: 'playback', id: currentPlaybackId, retry: 0 }); updatedPendingActions.create.push({ type: 'playback', data: { - policy: targetPolicy, + policy: policy, assetId: currentValue.assetId, }, retry: 0, @@ -1171,16 +1298,45 @@ export class App extends React.Component { ); } - if (this.state.value && (this.state.value.playbackId || this.state.value.signedPlaybackId)) { + if ( + this.state.value && + (this.state.value.playbackId || this.state.value.signedPlaybackId || this.state.value.drmPlaybackId) + ) { const { muxDomain } = this.props.sdk.parameters.installation as InstallationParams; const showPlayer = (this.state.value.ready && this.state.value.playbackId) || - (this.isUsingSigned() && !this.state.isTokenLoading); + (this.isUsingSigned() && !this.state.isTokenLoading) || + (this.isUsingDRM() && this.state.value.ready); return ( <> {modal}
+ {this.isUsingDRM() && ( + + + DRM-protected videos cannot be displayed in the Contentful preview due to security restrictions. However, the generated playback code is fully functional and will work correctly in your production environment. + + + )} + + {this.isUsingDRM() && + !this.state.drmLicenseToken && + !this.state.isTokenLoading && ( + + + {(() => { + const { muxSigningKeyId, muxSigningKeyPrivate } = this.props.sdk.parameters + .installation as InstallationParams; + if (!muxSigningKeyId || !muxSigningKeyPrivate) { + return 'No signing key to create a DRM license token. Please configure signing keys in the app settings.'; + } + return 'DRM license token could not be generated. The video may not play in preview. Make sure the getDRMLicenseTokens function is deployed.'; + })()} + + + )} + {this.isUsingSigned() && ( @@ -1234,11 +1390,22 @@ export class App extends React.Component { 'user' in this.props.sdk ? this.props.sdk.user.sys.id : undefined, page_type: 'Preview Player', }} - tokens={{ - playback: this.isUsingSigned() ? this.state.playbackToken : undefined, - thumbnail: this.isUsingSigned() ? this.state.posterToken : undefined, - storyboard: this.isUsingSigned() ? this.state.storyboardToken : undefined, - }} + tokens={ + this.isUsingSigned() + ? { + playback: this.state.playbackToken, + thumbnail: this.state.posterToken, + storyboard: this.state.storyboardToken, + } + : this.isUsingDRM() && this.state.drmLicenseToken && this.state.playbackToken + ? { + playback: this.state.playbackToken, + thumbnail: this.state.posterToken, + storyboard: this.state.storyboardToken, + drm: this.state.drmLicenseToken, + } + : undefined + } /> )} @@ -1303,7 +1470,11 @@ export class App extends React.Component { tracks={(this.state.value?.captions || []) as Track[]} type="caption" title="Add Caption" - playbackId={this.state.value?.playbackId || this.state.value?.signedPlaybackId} + playbackId={ + this.state.value?.playbackId || + this.state.value?.signedPlaybackId || + this.state.value?.drmPlaybackId + } domain={this.props.sdk.parameters.installation.domain} token={this.state.playbackToken} isSigned={this.isUsingSigned()} @@ -1322,7 +1493,11 @@ export class App extends React.Component { tracks={(this.state.value?.audioTracks || []) as Track[]} type="audio" title="Add Audio Track" - playbackId={this.state.value?.playbackId || this.state.value?.signedPlaybackId} + playbackId={ + this.state.value?.playbackId || + this.state.value?.signedPlaybackId || + this.state.value?.drmPlaybackId + } domain={this.props.sdk.parameters.installation.domain} token={this.state.playbackToken} isSigned={this.isUsingSigned()} @@ -1337,11 +1512,12 @@ export class App extends React.Component { - {this.isUsingSigned() && ( + {(this.isUsingSigned() || this.isUsingDRM()) && ( - This code snippet is for limited testing and expires after about 12 hours. - Tokens should be generated seperately. + {this.isUsingDRM() + ? 'DRM-protected content requires license tokens to be generated on your server. This code snippet is for reference only.' + : 'This code snippet is for limited testing and expires after about 12 hours. Tokens should be generated seperately.'} )} @@ -1356,6 +1532,9 @@ export class App extends React.Component { (this.props.sdk.parameters.installation as InstallationParams) .muxEnableSignedUrls } + enableDRM={ + (this.props.sdk.parameters.installation as InstallationParams).muxEnableDRM + } /> @@ -1444,6 +1623,6 @@ init((sdk) => { }); // Enabling hot reload -if (module.hot) { +if (typeof module !== 'undefined' && module.hot) { module.hot.accept(); } diff --git a/apps/mux/frontend/src/locations/config.tsx b/apps/mux/frontend/src/locations/config.tsx index 729fbe55d7..0818546491 100755 --- a/apps/mux/frontend/src/locations/config.tsx +++ b/apps/mux/frontend/src/locations/config.tsx @@ -41,6 +41,8 @@ interface IParameters { muxSigningKeyPrivate?: string; muxEnableAudioNormalize?: boolean; muxDomain?: string; + muxEnableDRM?: boolean; + muxDRMConfigurationId?: string; } interface IState { @@ -220,6 +222,8 @@ class Config extends React.Component { muxSigningKeyId, muxEnableAudioNormalize, muxDomain, + muxEnableDRM, + muxDRMConfigurationId, }, contentTypes, compatibleFields, @@ -362,6 +366,62 @@ class Config extends React.Component { )}
+
+ Advanced: DRM + + + DRM provides the highest level of content protection using industry-standard + encryption. To use DRM, you must first request + access in your Mux dashboard under Settings → Digital Rights Management. Once + approved, you'll receive a DRM Configuration ID.{' '} + + Learn more about DRM + + . + + + + this.setState({ + parameters: { + ...this.state.parameters, + muxEnableDRM: (e.target as HTMLInputElement).checked, + }, + }) + }> + Enable DRM + + {muxEnableDRM && ( + + DRM Configuration ID + + this.setState({ + parameters: { + ...this.state.parameters, + muxDRMConfigurationId: (e.target as HTMLTextAreaElement).value, + }, + }) + } + placeholder="Enter your DRM Configuration ID from Mux dashboard" + /> + + This ID is provided by Mux after DRM access is approved. + + + )} +
+
; video_quality: string; meta?: { title?: string; @@ -40,13 +44,27 @@ interface AssetInput { name?: string; } -function buildAssetSettings(options: ModalData): AssetSettings { +function buildAssetSettings( + options: ModalData, + drmConfigurationId?: string +): AssetSettings { + const hasDRM = options.playbackPolicies.includes('drm'); const settings: AssetSettings = { - playback_policies: options.playbackPolicies, video_quality: options.videoQuality, inputs: [], }; + if (hasDRM && drmConfigurationId) { + settings.advanced_playback_policies = [ + { + policy: 'drm', + drm_configuration_id: drmConfigurationId, + }, + ]; + } else if (!hasDRM) { + settings.playback_policies = options.playbackPolicies; + } + // Metadata case if (options.metadataConfig.standardMetadata) { const { title, externalId } = options.metadataConfig.standardMetadata; @@ -107,7 +125,15 @@ export async function addByURL({ setAssetError, pollForAssetDetails, }: AddByURLConfig) { - const settings = buildAssetSettings(options); + const { muxDRMConfigurationId } = sdk.parameters.installation as InstallationParams; + + // Validate DRM configuration if DRM is selected + if (options.playbackPolicies.includes('drm') && !muxDRMConfigurationId) { + setAssetError('DRM is selected but DRM Configuration ID is not set in app configuration.'); + return; + } + + const settings = buildAssetSettings(options, muxDRMConfigurationId); const requestBody = { ...settings, @@ -162,15 +188,26 @@ export async function getUploadUrl( options: ModalData, responseCheck: (res: Response) => boolean | Promise ) { - const { muxEnableAudioNormalize } = sdk.parameters.installation as InstallationParams; - const settings = buildAssetSettings(options); + const { muxEnableAudioNormalize, muxDRMConfigurationId } = + sdk.parameters.installation as InstallationParams; + + // Validate DRM configuration if DRM is selected + if (options.playbackPolicies.includes('drm') && !muxDRMConfigurationId) { + const errorMsg = 'DRM is selected but DRM Configuration ID is not set in app configuration.'; + sdk.notifier.error(errorMsg); + return; + } + + const settings = buildAssetSettings(options, muxDRMConfigurationId); + + const newAssetSettings = { + ...settings, + normalize_audio: muxEnableAudioNormalize || false, + }; const requestBody = { cors_origin: window.location.origin, - new_asset_settings: { - ...settings, - normalize_audio: muxEnableAudioNormalize || false, - }, + new_asset_settings: newAssetSettings, }; const res = await apiClient.post('/video/v1/uploads', JSON.stringify(requestBody)); diff --git a/apps/mux/frontend/src/util/types.tsx b/apps/mux/frontend/src/util/types.tsx index 46759f2742..ee94020d15 100755 --- a/apps/mux/frontend/src/util/types.tsx +++ b/apps/mux/frontend/src/util/types.tsx @@ -14,6 +14,8 @@ export interface InstallationParams { muxSigningKeyPrivate?: string; muxEnableAudioNormalize: boolean; muxDomain?: string; + muxEnableDRM?: boolean; + muxDRMConfigurationId?: string; } export interface AppState { @@ -25,6 +27,7 @@ export interface AppState { playbackToken?: string; posterToken?: string; storyboardToken?: string; + drmLicenseToken?: string; captionname?: string; audioName?: string; playerPlaybackId?: string; @@ -40,7 +43,7 @@ export interface AppState { export type ResolutionType = 'highest' | 'audio-only'; -export type PolicyType = 'signed' | 'public'; +export type PolicyType = 'signed' | 'public' | 'drm'; export interface PendingAction { type: 'playback' | 'asset' | 'caption' | 'staticRendition' | 'audio' | 'metadata'; @@ -65,6 +68,7 @@ export interface MuxContentfulObject { assetId: string; playbackId?: string; signedPlaybackId?: string; + drmPlaybackId?: string; ready: boolean; ratio?: string; error?: string; diff --git a/apps/mux/functions/contentful-app-manifest.json b/apps/mux/functions/contentful-app-manifest.json index 2c4539acb5..b36c5fb78c 100644 --- a/apps/mux/functions/contentful-app-manifest.json +++ b/apps/mux/functions/contentful-app-manifest.json @@ -17,6 +17,15 @@ "entryFile": "src/getSignedUrlTokens.ts", "allowNetworks": ["api.contentful.com", "https://api.mux.com"], "accepts": ["appaction.call"] + }, + { + "id": "getDRMLicenseTokens", + "name": "Get DRM license tokens", + "description": "This action returns DRM license tokens for securing Mux video playback with DRM protection.", + "path": "src/getDRMLicenseTokens.js", + "entryFile": "src/getDRMLicenseTokens.ts", + "allowNetworks": ["api.contentful.com", "https://api.mux.com"], + "accepts": ["appaction.call"] } ] } diff --git a/apps/mux/functions/src/getDRMLicenseTokens.ts b/apps/mux/functions/src/getDRMLicenseTokens.ts new file mode 100644 index 0000000000..d2eebddd59 --- /dev/null +++ b/apps/mux/functions/src/getDRMLicenseTokens.ts @@ -0,0 +1,106 @@ +import { FunctionEventHandler } from '@contentful/node-apps-toolkit'; +import { + AppActionRequest, + FunctionEventContext, + FunctionTypeEnum, +} from '@contentful/node-apps-toolkit/lib/requests/typings'; +import { Mux } from '@mux/mux-node'; + +type Parameters = { + playbackId: string; +}; + +interface DRMLicenseReturn { + licenseToken: string; + playbackToken: string; + thumbnailToken: string; + storyboardToken: string; +} + +async function generateDRMLicenseToken( + mux: Mux, + playbackId: string, + signingKeyId: string, + signingKeyPrivate: string +): Promise { + // For DRM, we need to generate multiple tokens: + // - playback token: authorizes access to the stream + // - license token: used to authorize DRM license requests + // - thumbnail token: used to access thumbnail/poster images + // - storyboard token: used to access storyboard images + const baseOptions = { + keyId: signingKeyId, + keySecret: signingKeyPrivate, + expiration: '12h', + }; + + // Generate playback token (type 'video') to authorize stream access + const playbackToken = await mux.jwt.signPlaybackId(playbackId, { + ...baseOptions, + type: 'video', + }); + + // Generate DRM license token (type 'drm_license') for DRM license requests + const licenseToken = await mux.jwt.signPlaybackId(playbackId, { + ...baseOptions, + type: 'drm_license', + }); + + // Generate thumbnail token (type 'thumbnail') for poster images + const thumbnailToken = await mux.jwt.signPlaybackId(playbackId, { + ...baseOptions, + type: 'thumbnail', + }); + + // Generate storyboard token (type 'storyboard') for storyboard images + const storyboardToken = await mux.jwt.signPlaybackId(playbackId, { + ...baseOptions, + type: 'storyboard', + }); + + return { + licenseToken, + playbackToken, + thumbnailToken, + storyboardToken, + }; +} + +export const handler: FunctionEventHandler = async ( + event: AppActionRequest<'Custom', Parameters>, + context: FunctionEventContext +) => { + const { playbackId } = event.body; + const { + appInstallationParameters: { + muxSigningKeyId, + muxSigningKeyPrivate, + muxAccessTokenId, + muxAccessTokenSecret, + }, + } = context; + const mux = new Mux({ tokenId: muxAccessTokenId, tokenSecret: muxAccessTokenSecret }); + + if (typeof muxSigningKeyId !== 'string' || typeof muxSigningKeyPrivate !== 'string') { + throw new TypeError('missing required mux signing key id or signing key private'); + } + + const drmTokens = await generateDRMLicenseToken( + mux, + playbackId, + muxSigningKeyId, + muxSigningKeyPrivate + ); + + return { + ok: true, + data: { + licenseToken: drmTokens.licenseToken, + playbackToken: drmTokens.playbackToken, + posterToken: drmTokens.thumbnailToken, + storyboardToken: drmTokens.storyboardToken, + }, + }; +}; + +