Responsive Frontend #5

Merged
tpd94 merged 6 commits from frontend into main 2025-04-28 18:43:55 +00:00
11 changed files with 878 additions and 11 deletions
Showing only changes of commit 8172c2122e - Show all commits

View File

@ -11,7 +11,9 @@
"@tailwindcss/vite": "^4.1.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-helmet": "^6.1.0",
"react-router-dom": "^7.5.2",
"shaka-player": "^4.14.9",
"tailwindcss": "^4.1.4"
},
"devDependencies": {
@ -1872,6 +1874,12 @@
"dev": true,
"license": "ISC"
},
"node_modules/eme-encryption-scheme-polyfill": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.2.1.tgz",
"integrity": "sha512-GzgrLuZPYGijd8oaKuBqYv3Tc2oruDZM3V2982KOuy/PA1N3zwMe+/oIXJYfZ3BH3PwW5nONdBBE+VY6jlwbrw==",
"license": "Apache-2.0"
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@ -2358,7 +2366,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@ -2696,6 +2703,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@ -2758,6 +2777,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -2897,6 +2925,17 @@
"node": ">= 0.8.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -2928,6 +2967,42 @@
"react": "^19.1.0"
}
},
"node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
"license": "MIT"
},
"node_modules/react-helmet": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
"integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
"license": "MIT",
"dependencies": {
"object-assign": "^4.1.1",
"prop-types": "^15.7.2",
"react-fast-compare": "^3.1.1",
"react-side-effect": "^2.1.0"
},
"peerDependencies": {
"react": ">=16.3.0"
}
},
"node_modules/react-helmet/node_modules/react-side-effect": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz",
"integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==",
"license": "MIT",
"peerDependencies": {
"react": "^16.3.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-refresh": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
@ -3048,6 +3123,18 @@
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/shaka-player": {
"version": "4.14.9",
"resolved": "https://registry.npmjs.org/shaka-player/-/shaka-player-4.14.9.tgz",
"integrity": "sha512-0YZJ+UUHBz3meAzN/eOvjNoLH7eCG1yHP3BFH8ZnFbGf3K50DgLWNMoV6bm6pH4cndvXem+SdcQRXabItD4RBA==",
"license": "Apache-2.0",
"dependencies": {
"eme-encryption-scheme-polyfill": "^2.2.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@ -13,7 +13,9 @@
"@tailwindcss/vite": "^4.1.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-helmet": "^6.1.0",
"react-router-dom": "^7.5.2",
"shaka-player": "^4.14.9",
"tailwindcss": "^4.1.4"
},
"devDependencies": {

View File

@ -1,5 +1,8 @@
import { useState } from "react";
import Home from "./components/Pages/HomePage";
import Cache from "./components/Pages/Cache";
import API from "./components/Pages/API";
import TestPlayer from "./components/Pages/TestPlayer";
import NavBar from "./components/NavBar";
import NavBarMain from "./components/NavBarMain";
import SideMenu from "./components/SideMenu"; // Add this import
@ -13,18 +16,21 @@ function App() {
{/* The SideMenu should be visible when isMenuOpen is true */}
<SideMenu isMenuOpen={isMenuOpen} setIsMenuOpen={setIsMenuOpen} />
<div id="navbarcontainer" className="hidden lg:flex lg:w-2xs bg-gray-950/55 border-r border-white/5 shadow-lg shadow-blue-900/50">
<div id="navbarcontainer" className="hidden lg:flex lg:w-2xs bg-gray-950/55 border-r border-white/5 0">
<NavBar />
</div>
<div id="maincontainer" className="w-full lg:w-5/6 bg-gray-950/50 flex flex-col">
<div id="navbarmaincontainer" className="w-full lg:hidden h-16 bg-gray-950/10 border-b border-white/5 shadow-md shadow-blue-900/35 sticky top-0 z-10">
<div id="maincontainer" className="w-full lg:w-5/6 bg-gray-950/50 flex flex-col grow">
<div id="navbarmaincontainer" className="w-full lg:hidden h-16 bg-gray-950/10 border-b border-white/5 sticky top-0 z-10">
<NavBarMain setIsMenuOpen={setIsMenuOpen} />
</div>
<div id="maincontentcontainer" className="w-full grow">
<div id="maincontentcontainer" className="w-full grow overflow-y-auto">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/cache" element={<Cache />} />
<Route path="/api" element={<API />} />
<Route path="/testplayer" element={<TestPlayer />} />
</Routes>
</div>
</div>

View File

@ -0,0 +1,173 @@
import "./protobuf.min.js";
import "./license_protocol.min.js";
const { SignedMessage, LicenseRequest } = protobuf.roots.default.license_protocol;
function uint8ArrayToBase64(uint8Array) {
const binaryString = Array.from(uint8Array)
.map(b => String.fromCharCode(b))
.join('');
return btoa(binaryString);
}
function parseFetch(fetchString) {
// Remove `await` if it exists in the string
fetchString = fetchString.replace(/^await\s+/, "");
// Use a more lenient regex to capture the fetch pattern (including complex bodies)
const fetchRegex = /fetch\(['"](.+?)['"],\s*(\{.+?\})\)/s; // Non-greedy match for JSON
const lines = fetchString.split('\n').map(line => line.trim()).filter(Boolean);
const result = {
method: 'UNDEFINED',
url: '',
headers: {},
body: null,
};
// Try matching the regex
const fetchMatch = fetchString.match(fetchRegex);
if (!fetchMatch) {
console.log(fetchString);
throw new Error("Invalid 'Copy as fetch' string.");
}
// Extract URL from the match
result.url = fetchMatch[1];
// Parse the options JSON from the match (this will include headers, body, etc.)
const optionsString = fetchMatch[2];
const options = JSON.parse(optionsString);
// Assign method, headers, and body if available
if (options.method) result.method = options.method;
if (options.headers) result.headers = options.headers;
if (options.body) result.body = options.body;
return result;
}
const WIDEVINE_SYSTEM_ID = new Uint8Array([0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d, 0x21, 0xed]);
const PLAYREADY_SYSTEM_ID = new Uint8Array([0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86, 0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95]);
const PSSH_MAGIC = new Uint8Array([0x70, 0x73, 0x73, 0x68]);
function intToUint8Array(num, endian) {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, num, endian);
return new Uint8Array(buffer);
}
function shortToUint8Array(num, endian) {
const buffer = new ArrayBuffer(2);
const view = new DataView(buffer);
view.setUint16(0, num, endian);
return new Uint8Array(buffer);
}
function psshDataToPsshBoxB64(pssh_data, system_id) {
const dataLength = pssh_data.length;
const totalLength = dataLength + 32;
const pssh = new Uint8Array([
...intToUint8Array(totalLength, false),
...PSSH_MAGIC,
...new Uint8Array(4),
...system_id,
...intToUint8Array(dataLength, false),
...pssh_data
]);
return uint8ArrayToBase64(pssh);
}
function wrmHeaderToPlayReadyHeader(wrm_header){
const playready_object = new Uint8Array([
...shortToUint8Array(1, true),
...shortToUint8Array(wrm_header.length, true),
...wrm_header
]);
return new Uint8Array([
...intToUint8Array(playready_object.length + 2 + 4, true),
...shortToUint8Array(1, true),
...playready_object
]);
}
function encodeUtf16LE(str) {
const buffer = new Uint8Array(str.length * 2);
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
buffer[i * 2] = code & 0xff;
buffer[i * 2 + 1] = code >> 8;
}
return buffer;
}
function stringToUint8Array(string) {
return Uint8Array.from(string.split("").map(x => x.charCodeAt()));
}
export async function readTextFromClipboard() {
try {
// Request text from the clipboard
const clipboardText = await navigator.clipboard.readText();
const result = parseFetch(clipboardText);
let pssh_data_string;
let payload_string;
if (result.body.startsWith("<")) {
// If body starts with "<", process it as PlayReady content
payload_string = result.body;
const wrmHeaderMatch = payload_string.match(/.*(<WRMHEADER.*<\/WRMHEADER>).*/);
const wrmHeader = wrmHeaderMatch ? wrmHeaderMatch[1] : null;
const encodedWrmHeader = encodeUtf16LE(wrmHeader);
const playreadyHeader = wrmHeaderToPlayReadyHeader(encodedWrmHeader);
pssh_data_string = psshDataToPsshBoxB64(playreadyHeader, PLAYREADY_SYSTEM_ID);
} else {
// If body is in a different format, process as Widevine content
const uint8Array = stringToUint8Array(result.body);
let signed_message;
let license_request;
try {
signed_message = SignedMessage.decode(uint8Array);
license_request = LicenseRequest.decode(signed_message.msg);
} catch (decodeError) {
// If error occurs during decoding, return an empty pssh
console.error('Decoding failed, returning empty pssh', decodeError);
pssh_data_string = ''; // Empty pssh if decoding fails
}
if (license_request && license_request.contentId && license_request.contentId.widevinePsshData) {
const pssh_data = license_request.contentId.widevinePsshData.psshData[0];
pssh_data_string = psshDataToPsshBoxB64(pssh_data, WIDEVINE_SYSTEM_ID);
}
// Check if the body contains binary data (non-UTF-8 characters)
if (isBinary(uint8Array)) {
payload_string = uint8ArrayToBase64(uint8Array);
} else {
// If it's text, return it as is
payload_string = result.body;
}
}
// Output the result
document.getElementById("licurl").value = result.url;
document.getElementById("headers").value = JSON.stringify(result.headers);
document.getElementById("pssh").value = pssh_data_string;
document.getElementById("data").value = payload_string;
} catch (error) {
console.error('Failed to read clipboard contents:', error);
}
}
// Helper function to check if the data is binary
function isBinary(uint8Array) {
// Check for non-text (non-ASCII) bytes (basic heuristic)
return uint8Array.some(byte => byte > 127); // Non-ASCII byte indicates binary
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,134 @@
import React, { useState, useEffect } from 'react';
import { Helmet } from 'react-helmet'; // Import Helmet
const { protocol, hostname, port } = window.location;
let fullHost = `${protocol}//${hostname}`;
if (
(protocol === 'http:' && port !== '80') ||
(protocol === 'https:' && port !== '443' && port !== '')
) {
fullHost += `:${port}`;
}
function API() {
const [deviceInfo, setDeviceInfo] = useState({
device_type: '',
system_id: '',
security_level: '',
host: '',
secret: '',
device_name: ''
});
const [prDeviceInfo, setPrDeviceInfo] = useState({
security_level: '',
host: '',
secret: '',
device_name: ''
});
useEffect(() => {
// Fetch Widevine info
fetch('/remotecdm/widevine/deviceinfo')
.then(response => response.json())
.then(data => {
setDeviceInfo({
device_type: data.device_type,
system_id: data.system_id,
security_level: data.security_level,
host: data.host,
secret: data.secret,
device_name: data.device_name
});
})
.catch(error => console.error('Error fetching Widevine info:', error));
// Fetch PlayReady info
fetch('/remotecdm/playready/deviceinfo')
.then(response => response.json())
.then(data => {
setPrDeviceInfo({
security_level: data.security_level,
host: data.host,
secret: data.secret,
device_name: data.device_name
});
})
.catch(error => console.error('Error fetching PlayReady info:', error));
}, []);
return (
<div className="flex flex-col w-full overflow-y-auto p-4 text-white">
<Helmet>
<title>API</title>
</Helmet>
<details open className='w-full list-none'>
<summary className='text-2xl'>Sending a decryption request</summary>
<div className='mt-5 p-5 rounded-lg border-2 border-indigo-500/50'>
<pre className='rounded-lg font-mono whitespace-pre-wrap text-white overflow-auto'>
{`import requests
print(requests.post(
url='${fullHost}/api/decrypt',
headers={
'Content-Type': 'application/json',
},
json={
'pssh': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==',
'licurl': 'https://cwip-shaka-proxy.appspot.com/no_auth',
'headers': str({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0',
'Accept': '*/*',
'Accept-Language': 'en-US,en;q=0.5',
})
}
).json()['message'])`}
</pre>
</div>
</details>
<details open className='w-full list-none mt-5'>
<summary className='text-2xl'>Sending a search request</summary>
<div className='mt-5 border-2 border-indigo-500/50 p-5 rounded-lg'>
<pre className="rounded-lg font-mono whitespace-pre text-white overflow-x-auto max-w-full p-5">
{`import requests
print(requests.post(
url='${fullHost}/api/cache/search',
json={
'input': 'AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA=='
}
).json())`}
</pre>
</div>
</details>
<details open className='w-full list-none mt-5'>
<summary className='text-2xl'>PyWidevine RemoteCDM info</summary>
<div className='mt-5 border-2 border-indigo-500 p-5 rounded-lg overflow-x-auto'>
<p>
<strong>Device Type:</strong> '{deviceInfo.device_type}'<br />
<strong>System ID:</strong> {deviceInfo.system_id}<br />
<strong>Security Level:</strong> {deviceInfo.security_level}<br />
<strong>Host:</strong> {fullHost}/remotecdm/widevine<br />
<strong>Secret:</strong> {deviceInfo.secret}<br />
<strong>Device Name:</strong> {deviceInfo.device_name}
</p>
</div>
</details>
<details open className='w-full list-none mt-5'>
<summary className='text-2xl'>PyPlayready RemoteCDM info</summary>
<div className='mt-5 border-2 border-indigo-500 p-5 rounded-lg overflow-x-auto'>
<p>
<strong>Security Level:</strong> {prDeviceInfo.security_level}<br />
<strong>Host:</strong> {fullHost}/remotecdm/playready<br />
<strong>Secret:</strong> {prDeviceInfo.secret}<br />
<strong>Device Name:</strong> {prDeviceInfo.device_name}
</p>
</div>
</details>
</div>
);
}
export default API;

View File

@ -0,0 +1,107 @@
import { useState, useEffect, useRef } from 'react';
import { Helmet } from 'react-helmet'; // Import Helmet
function Cache() {
const [searchQuery, setSearchQuery] = useState('');
const [cacheData, setCacheData] = useState([]);
const [keyCount, setKeyCount] = useState(0); // New state to store the key count
const debounceTimeout = useRef(null);
// Fetch the key count when the component mounts
useEffect(() => {
const fetchKeyCount = async () => {
try {
const response = await fetch('/api/cache/keycount');
const data = await response.json();
setKeyCount(data.count); // Update key count
} catch (error) {
console.error('Error fetching key count:', error);
}
};
fetchKeyCount();
}, []); // Run only once when the component mounts
const handleInputChange = (event) => {
const query = event.target.value;
setSearchQuery(query); // Update the search query
// Clear the previous timeout
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
}
// Set a new timeout to send the API call after 1 second of no typing
debounceTimeout.current = setTimeout(() => {
if (query.trim() !== '') {
sendApiCall(query); // Only call the API if the query is not empty
} else {
setCacheData([]); // Clear results if query is empty
}
}, 1000); // 1 second delay
};
const sendApiCall = (text) => {
fetch('/api/cache/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: text }),
})
.then((response) => response.json())
.then((data) => setCacheData(data)) // Update cache data with the results
.catch((error) => console.error('Error:', error));
};
return (
<div className="flex flex-col w-full h-full overflow-y-auto p-4">
<Helmet>
<title>Cache</title>
</Helmet>
<div className="flex flex-col lg:flex-row w-full lg:h-12 items-center">
<input
type="text"
value={searchQuery}
onChange={handleInputChange}
placeholder={`Search ${keyCount} keys...`} // Dynamic placeholder
className="lg:grow w-full border-2 border-emerald-500/25 rounded-xl h-10 self-center m-2 text-white p-1 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 transition-all duration-200 ease-in-out"
/>
<a
href="/api/cache/download"
className="bg-emerald-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-10 truncate w-full text-center flex items-center justify-center m-2"
>
Download Cache
</a>
</div>
<div className="w-full grow p-4 border-2 border-emerald-500/50 rounded-2xl mt-5 overflow-y-auto">
<table className="min-w-full text-white">
<thead>
<tr>
<th className="p-2 border border-black">PSSH</th>
<th className="p-2 border border-black">KID</th>
<th className="p-2 border border-black">Key</th>
</tr>
</thead>
<tbody>
{cacheData.length > 0 ? (
cacheData.map((item, index) => (
<tr key={index}>
<td className="p-2 border border-black">{item.PSSH}</td>
<td className="p-2 border border-black">{item.KID}</td>
<td className="p-2 border border-black">{item.Key}</td>
</tr>
))
) : (
<tr>
<td colSpan="3" className="p-2 border border-black text-center">
No data found
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
);
}
export default Cache;

View File

@ -1,10 +1,192 @@
import React, { useState, useEffect, useRef } from 'react';
import { readTextFromClipboard } from '../Functions/ParseChallenge'
import { Helmet } from 'react-helmet'; // Import Helmet
function HomePage() {
const [pssh, setPssh] = useState('');
const [licurl, setLicurl] = useState('');
const [proxy, setProxy] = useState('');
const [headers, setHeaders] = useState('');
const [cookies, setCookies] = useState('');
const [data, setData] = useState('');
const [message, setMessage] = useState('');
const [isVisible, setIsVisible] = useState(false);
const bottomRef = useRef(null);
const messageRef = useRef(null); // Reference to result container
const handleReset = () => {
if (isVisible) {
setIsVisible(false);
}
setPssh('');
setLicurl('');
setProxy('');
setHeaders('');
setCookies('');
setData('');
};
const handleSubmitButton = (event) => {
event.preventDefault();
fetch('/api/decrypt', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
pssh: pssh,
licurl: licurl,
proxy: proxy,
headers: headers,
cookies: cookies,
data: data
}),
})
.then(response => response.json())
.then(data => {
const resultMessage = data['message'].replace(/\n/g, '<br />');
setMessage(resultMessage);
setIsVisible(true);
})
.catch((error) => {
console.error('Error during decryption request:', error);
setMessage('Error: Unable to process request.');
setIsVisible(true);
});
};
const handleCopy = (event) => {
event.preventDefault();
if (messageRef.current) {
const textToCopy = messageRef.current.innerText; // Grab the plain text (with visual line breaks)
navigator.clipboard.writeText(textToCopy).catch(err => {
alert('Failed to copy!');
console.error(err);
});
}
};
return (
<div className="flex flex-col w-full h-full overflow-y-auto p-4">
</div>
)
const handleFetchPaste = () => {
event.preventDefault();
readTextFromClipboard().then(() => {
setPssh(document.getElementById("pssh").value);
setLicurl(document.getElementById("licurl").value);
setHeaders(document.getElementById("headers").value);
setData(document.getElementById("data").value);
}).catch(err => {
alert('Failed to paste from fetch!');
});
}
export default HomePage
useEffect(() => {
if (isVisible && bottomRef.current) {
bottomRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [message, isVisible]);
return (
<>
<div className="flex flex-col w-full overflow-y-auto p-4 min-h-full">
<Helmet>
<title>CDRM-Project</title>
</Helmet>
<form className="flex flex-col w-full h-full bg-black/5 p-4 overflow-y-auto">
<label htmlFor="pssh" className="text-white w-8/10 self-center">PSSH: </label>
<input
type="text"
id="pssh"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={pssh}
onChange={(e) => setPssh(e.target.value)}
/>
<label htmlFor="licurl" className="text-white w-8/10 self-center">License URL: </label>
<input
type="text"
id="licurl"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={licurl}
onChange={(e) => setLicurl(e.target.value)}
/>
<label htmlFor="proxy" className="text-white w-8/10 self-center">Proxy: </label>
<input
type="text"
id="proxy"
className="w-8/10 border-2 border-sky-500/25 rounded-xl h-10 self-center m-2 text-white p-1"
value={proxy}
onChange={(e) => setProxy(e.target.value)}
/>
<label htmlFor="headers" className="text-white w-8/10 self-center">Headers: </label>
<textarea
id="headers"
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={headers}
onChange={(e) => setHeaders(e.target.value)}
/>
<label htmlFor="cookies" className="text-white w-8/10 self-center">Cookies: </label>
<textarea
id="cookies"
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={cookies}
onChange={(e) => setCookies(e.target.value)}
/>
<label htmlFor="data" className="text-white w-8/10 self-center">Data: </label>
<textarea
id="data"
className="w-8/10 border-2 border-sky-500/25 rounded-xl self-center m-2 text-white p-1 h-48"
value={data}
onChange={(e) => setData(e.target.value)}
/>
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
<button
type="button"
className="bg-sky-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
onClick={handleSubmitButton}
>
Submit
</button>
<button onClick={handleFetchPaste} className="bg-yellow-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0">
Paste from fetch
</button>
<button
type="button"
className="bg-red-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate mt-5 w-1/2 lg:mt-0"
onClick={handleReset}
>
Reset
</button>
</div>
</form>
</div>
{isVisible && (
<div id="main_content" className="flex-col w-full h-full p-10 items-center justify-center self-center">
<div className="flex flex-col w-full h-full overflow-y-auto items-center">
<div className='w-8/10 grow p-4 text-white text-bold text-center text-xl md:text-3xl border-2 border-sky-500/25 rounded-xl bg-black/5'>
<p className="w-full border-b-2 border-white/75 pb-2">Results:</p>
<p
className="w-full grow pt-10 break-words overflow-y-auto"
ref={messageRef}
dangerouslySetInnerHTML={{ __html: message }}
/>
<div ref={bottomRef} />
</div>
</div>
<div className="flex flex-col lg:flex-row w-full self-center mt-5 items-center lg:justify-around lg:items-stretch">
<button
className="bg-green-500/50 rounded-xl text-white text-bold text-xl p-1 lg:w-1/5 lg:h-12 truncate w-1/2"
onClick={handleCopy}
>
Copy Results
</button>
</div>
</div>
)}
</>
);
}
export default HomePage;

View File

@ -0,0 +1,158 @@
import React, { useState, useEffect, useRef } from 'react';
import shaka from 'shaka-player';
import { Helmet } from 'react-helmet'; // Import Helmet
function TestPlayer() {
const [mpdUrl, setMpdUrl] = useState(''); // State to hold the MPD URL
const [kids, setKids] = useState(''); // State to hold KIDs (separated by line breaks)
const [keys, setKeys] = useState(''); // State to hold Keys (separated by line breaks)
const [headers, setHeaders] = useState(''); // State to hold request headers
const videoRef = useRef(null); // Ref for the video element
const playerRef = useRef(null); // Ref for Shaka Player instance
// Function to update the MPD URL state
const handleInputChange = (event) => {
setMpdUrl(event.target.value);
};
// Function to update KIDs and Keys
const handleKidsChange = (event) => {
setKids(event.target.value);
};
const handleKeysChange = (event) => {
setKeys(event.target.value);
};
const handleHeadersChange = (event) => {
setHeaders(event.target.value);
};
// Function to initialize Shaka Player
const initializePlayer = () => {
if (videoRef.current) {
// Initialize Shaka Player only if it's not already initialized
if (!playerRef.current) {
const player = new shaka.Player(videoRef.current);
playerRef.current = player;
// Add error listener
player.addEventListener('error', (event) => {
console.error('Error code', event.detail.code, 'object', event.detail);
});
}
}
};
// Function to handle submit and configure player with DRM keys and headers
const handleSubmit = () => {
if (mpdUrl && kids && keys) {
// Split the KIDs and Keys by new lines
const kidsArray = kids.split("\n").map((k) => k.trim());
const keysArray = keys.split("\n").map((k) => k.trim());
if (kidsArray.length !== keysArray.length) {
console.error("The number of KIDs and Keys must be the same.");
return;
}
// Initialize Shaka Player only when the submit button is pressed
const player = new shaka.Player(videoRef.current);
// Widevine DRM configuration with the provided KIDs and Keys
const config = {
drm: {
clearKeys: {},
},
};
// Map KIDs to Keys
kidsArray.forEach((kid, index) => {
config.drm.clearKeys[kid] = keysArray[index];
});
console.log("Configuring player with the following DRM config and headers:", config);
// Configure the player with ClearKey DRM and custom headers
player.configure(config);
// Load the video stream with MPD URL
player.load(mpdUrl).then(() => {
console.log('Video loaded');
}).catch((error) => {
console.error('Error loading the video', error);
});
} else {
console.error('MPD URL, KIDs, and Keys are required.');
}
};
// Load the video stream whenever the MPD URL changes
useEffect(() => {
initializePlayer(); // Initialize the player if it's not initialized already
}, []); // This effect runs only once on mount
// Helper function to parse headers from the textarea input
const parseHeaders = (headersText) => {
const headersArr = headersText.split('\n');
const headersObj = {};
headersArr.forEach((line) => {
const [key, value] = line.split(':');
if (key && value) {
headersObj[key.trim()] = value.trim();
}
});
return headersObj;
};
return (
<div className="flex flex-col items-center w-full p-4">
<Helmet>
<title>Test Player</title>
</Helmet>
<div className="w-full flex flex-col">
<video
ref={videoRef}
width="100%"
height="auto"
controls
className="h-96"
/>
<input
type="text"
value={mpdUrl}
onChange={handleInputChange}
placeholder="MPD URL"
className="border-2 border-rose-700/50 mt-2 text-white p-1 rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
<textarea
placeholder="KIDs (one per line)"
value={kids}
onChange={handleKidsChange}
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
<textarea
placeholder="Keys (one per line)"
value={keys}
onChange={handleKeysChange}
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
<textarea
placeholder="Headers (one per line)"
value={headers}
onChange={handleHeadersChange}
className="border-2 border-rose-700/50 mt-2 text-white p-1 overflow-y-auto rounded transition-all ease-in-out focus:outline-none focus:ring-2 focus:ring-rose-700/50 duration-200"
/>
<button
onClick={handleSubmit}
className="mt-4 p-2 bg-blue-500 text-white rounded"
>
Submit
</button>
</div>
</div>
);
}
export default TestPlayer;

View File

@ -1 +1,10 @@
@import "tailwindcss";
details summary::-webkit-details-marker {
display: none;
}
details summary {
list-style: none;
cursor: pointer;
}