无
This commit is contained in:
@@ -119,6 +119,96 @@ header p {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.login-screen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(15, 23, 42, 0.92);
|
||||
z-index: 50;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: min(480px, 100%);
|
||||
background: rgba(15, 23, 42, 0.96);
|
||||
border: 1px solid rgba(148, 163, 184, 0.15);
|
||||
border-radius: 24px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 40px 80px rgba(15, 23, 42, 0.4);
|
||||
}
|
||||
|
||||
.login-card h2 {
|
||||
margin: 0 0 1rem;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.login-card p {
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.login-card label {
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.login-card label span {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.login-card input {
|
||||
width: 100%;
|
||||
border: 1px solid var(--panel-border);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--text-primary);
|
||||
border-radius: 12px;
|
||||
padding: 0.85rem 1rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.login-card .primary-btn {
|
||||
width: 100%;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
color: #f97316;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.bucket-panel {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.bucket-panel label {
|
||||
display: block;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.bucket-panel select {
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 10px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.login-screen.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: var(--panel-bg);
|
||||
backdrop-filter: blur(16px);
|
||||
|
||||
@@ -16,7 +16,24 @@
|
||||
<div class="glow-orb orb-2"></div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div id="login-screen" class="login-screen">
|
||||
<div class="login-card">
|
||||
<h2>Login</h2>
|
||||
<p>Please enter your S3 credentials to continue.</p>
|
||||
<label>
|
||||
<span>Access Key</span>
|
||||
<input id="login-username" type="text" autocomplete="username" placeholder="Access key" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Secret Key</span>
|
||||
<input id="login-password" type="password" autocomplete="current-password" placeholder="Secret key" />
|
||||
</label>
|
||||
<button id="login-btn" class="primary-btn">Login</button>
|
||||
<p id="login-error" class="form-error hidden"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container hidden" id="app-container">
|
||||
<div id="top-banner" class="top-banner hidden"></div>
|
||||
|
||||
<main class="dashboard">
|
||||
@@ -32,15 +49,11 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="credentials-panel">
|
||||
<div class="credential-field">
|
||||
<label for="s3-username">Username</label>
|
||||
<input id="s3-username" type="text" placeholder="Access key" autocomplete="username">
|
||||
</div>
|
||||
<div class="credential-field">
|
||||
<label for="s3-password">Password</label>
|
||||
<input id="s3-password" type="password" placeholder="Secret key" autocomplete="current-password">
|
||||
</div>
|
||||
<div class="bucket-panel">
|
||||
<label for="bucket-select">Select Bucket</label>
|
||||
<select id="bucket-select">
|
||||
<option value="" disabled selected>Choose a bucket</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="codec-panel">
|
||||
<label for="codec-select">编码方式:</label>
|
||||
|
||||
@@ -3,8 +3,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const loadingSpinner = document.getElementById('loading-spinner');
|
||||
const refreshBtn = document.getElementById('refresh-btn');
|
||||
const resetCacheBtn = document.getElementById('reset-cache-btn');
|
||||
const usernameInput = document.getElementById('s3-username');
|
||||
const passwordInput = document.getElementById('s3-password');
|
||||
const bucketSelect = document.getElementById('bucket-select');
|
||||
const loginScreen = document.getElementById('login-screen');
|
||||
const appContainer = document.getElementById('app-container');
|
||||
const loginUsernameInput = document.getElementById('login-username');
|
||||
const loginPasswordInput = document.getElementById('login-password');
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
const loginError = document.getElementById('login-error');
|
||||
const codecSelect = document.getElementById('codec-select');
|
||||
const encoderSelect = document.getElementById('encoder-select');
|
||||
const playerOverlay = document.getElementById('player-overlay');
|
||||
@@ -20,11 +25,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const topBanner = document.getElementById('top-banner');
|
||||
|
||||
let currentPollInterval = null;
|
||||
let selectedBucket = null;
|
||||
let selectedKey = null;
|
||||
let ws = null;
|
||||
let wsConnected = false;
|
||||
let subscribedKey = null;
|
||||
let currentVideoKey = null;
|
||||
let s3AuthHeaders = {};
|
||||
|
||||
const sendWsMessage = (message) => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
@@ -108,19 +115,86 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
progressFill.style.width = '0%';
|
||||
};
|
||||
|
||||
const getS3AuthHeaders = () => {
|
||||
const headers = {};
|
||||
const username = usernameInput?.value?.trim();
|
||||
const password = passwordInput?.value || '';
|
||||
if (username) headers['X-S3-Username'] = username;
|
||||
if (password) headers['X-S3-Password'] = password;
|
||||
return headers;
|
||||
const setAuthHeaders = (username, password) => {
|
||||
s3AuthHeaders = {};
|
||||
if (username) s3AuthHeaders['X-S3-Username'] = username;
|
||||
if (password) s3AuthHeaders['X-S3-Password'] = password;
|
||||
};
|
||||
|
||||
const getS3AuthPayload = () => ({
|
||||
username: usernameInput?.value?.trim() || '',
|
||||
password: passwordInput?.value || ''
|
||||
const showLogin = () => {
|
||||
if (loginScreen) loginScreen.classList.remove('hidden');
|
||||
if (appContainer) appContainer.classList.add('hidden');
|
||||
};
|
||||
|
||||
const showApp = () => {
|
||||
if (loginScreen) loginScreen.classList.add('hidden');
|
||||
if (appContainer) appContainer.classList.remove('hidden');
|
||||
};
|
||||
|
||||
const renderBuckets = (buckets) => {
|
||||
if (!bucketSelect) return;
|
||||
bucketSelect.innerHTML = '<option value="" disabled selected>Choose a bucket</option>';
|
||||
buckets.forEach(bucket => {
|
||||
const option = document.createElement('option');
|
||||
option.value = bucket.Name;
|
||||
option.textContent = bucket.Name;
|
||||
bucketSelect.appendChild(option);
|
||||
});
|
||||
};
|
||||
|
||||
const showLoginError = (message) => {
|
||||
if (!loginError) return;
|
||||
loginError.textContent = message;
|
||||
loginError.classList.remove('hidden');
|
||||
};
|
||||
|
||||
const clearLoginError = () => {
|
||||
if (!loginError) return;
|
||||
loginError.textContent = '';
|
||||
loginError.classList.add('hidden');
|
||||
};
|
||||
|
||||
const login = async () => {
|
||||
if (!loginUsernameInput || !loginPasswordInput) return;
|
||||
const username = loginUsernameInput.value.trim();
|
||||
const password = loginPasswordInput.value;
|
||||
if (!username || !password) {
|
||||
showLoginError('Please enter both access key and secret key.');
|
||||
return;
|
||||
}
|
||||
loginBtn.disabled = true;
|
||||
loginBtn.textContent = 'Logging in...';
|
||||
clearLoginError();
|
||||
|
||||
try {
|
||||
setAuthHeaders(username, password);
|
||||
const res = await fetch('/api/buckets', { headers: s3AuthHeaders });
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || 'Login failed');
|
||||
}
|
||||
const data = await res.json();
|
||||
if (!Array.isArray(data.buckets) || data.buckets.length === 0) {
|
||||
throw new Error('No buckets available for this account');
|
||||
}
|
||||
renderBuckets(data.buckets);
|
||||
showApp();
|
||||
selectedBucket = data.buckets[0].Name;
|
||||
if (bucketSelect) bucketSelect.value = selectedBucket;
|
||||
loadConfig();
|
||||
connectWebSocket();
|
||||
await fetchVideos(selectedBucket);
|
||||
} catch (err) {
|
||||
console.error('Login error:', err);
|
||||
showLoginError(err.message);
|
||||
setAuthHeaders('', '');
|
||||
} finally {
|
||||
if (loginBtn) {
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.textContent = 'Login';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resetCache = async () => {
|
||||
if (!resetCacheBtn) return;
|
||||
@@ -175,13 +249,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
// Fetch list of videos from the backend
|
||||
const fetchVideos = async () => {
|
||||
const fetchVideos = async (bucket) => {
|
||||
if (!bucket) {
|
||||
return;
|
||||
}
|
||||
videoListEl.classList.add('hidden');
|
||||
loadingSpinner.classList.remove('hidden');
|
||||
videoListEl.innerHTML = '';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/videos', { headers: getS3AuthHeaders() });
|
||||
const res = await fetch(`/api/videos?bucket=${encodeURIComponent(bucket)}`, { headers: s3AuthHeaders });
|
||||
if (!res.ok) throw new Error('Failed to fetch videos. Check S3 Config.');
|
||||
|
||||
const data = await res.json();
|
||||
@@ -328,11 +405,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
const codec = codecSelect?.value || 'h264';
|
||||
const encoder = encoderSelect?.value || 'software';
|
||||
const authPayload = getS3AuthPayload();
|
||||
if (!selectedBucket) throw new Error('No bucket selected');
|
||||
const res = await fetch('/api/transcode', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', ...getS3AuthHeaders() },
|
||||
body: JSON.stringify({ key: selectedKey, codec, encoder, ...authPayload })
|
||||
headers: { 'Content-Type': 'application/json', ...s3AuthHeaders },
|
||||
body: JSON.stringify({ bucket: selectedBucket, key: selectedKey, codec, encoder })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
@@ -403,10 +480,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
};
|
||||
|
||||
// Bind events
|
||||
refreshBtn.addEventListener('click', fetchVideos);
|
||||
|
||||
// Connect WebSocket and initial load
|
||||
connectWebSocket();
|
||||
loadConfig();
|
||||
fetchVideos();
|
||||
refreshBtn.addEventListener('click', () => fetchVideos(selectedBucket));
|
||||
if (loginBtn) {
|
||||
loginBtn.addEventListener('click', login);
|
||||
}
|
||||
if (bucketSelect) {
|
||||
bucketSelect.addEventListener('change', async (event) => {
|
||||
selectedBucket = event.target.value;
|
||||
await fetchVideos(selectedBucket);
|
||||
});
|
||||
}
|
||||
|
||||
// Initial state: require login before loading data
|
||||
showLogin();
|
||||
});
|
||||
|
||||
34
server.js
34
server.js
@@ -7,7 +7,7 @@ const path = require('path');
|
||||
const http = require('http');
|
||||
const WebSocket = require('ws');
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const { S3Client, ListObjectsV2Command, GetObjectCommand } = require('@aws-sdk/client-s3');
|
||||
const { S3Client, ListBucketsCommand, ListObjectsV2Command, GetObjectCommand } = require('@aws-sdk/client-s3');
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -162,11 +162,27 @@ const clearMp4Cache = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Endpoint to list available buckets
|
||||
app.get('/api/buckets', async (req, res) => {
|
||||
try {
|
||||
const auth = extractS3Credentials(req);
|
||||
const s3Client = createS3Client(auth);
|
||||
const command = new ListBucketsCommand({});
|
||||
const response = await s3Client.send(command);
|
||||
const buckets = response.Buckets || [];
|
||||
res.json({ buckets });
|
||||
} catch (error) {
|
||||
console.error('Error listing buckets:', error);
|
||||
res.status(500).json({ error: 'Failed to list buckets', detail: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Endpoint to list videos in the bucket
|
||||
app.get('/api/videos', async (req, res) => {
|
||||
try {
|
||||
if (!BUCKET_NAME) {
|
||||
return res.status(500).json({ error: 'S3_BUCKET_NAME not configured' });
|
||||
const bucket = req.query.bucket || BUCKET_NAME;
|
||||
if (!bucket) {
|
||||
return res.status(400).json({ error: 'Bucket name is required' });
|
||||
}
|
||||
const allObjects = [];
|
||||
const auth = extractS3Credentials(req);
|
||||
@@ -175,7 +191,7 @@ app.get('/api/videos', async (req, res) => {
|
||||
|
||||
do {
|
||||
const command = new ListObjectsV2Command({
|
||||
Bucket: BUCKET_NAME,
|
||||
Bucket: bucket,
|
||||
ContinuationToken: continuationToken,
|
||||
});
|
||||
const response = await s3Client.send(command);
|
||||
@@ -223,7 +239,13 @@ app.post('/api/reset-cache', (req, res) => {
|
||||
|
||||
// Endpoint to transcode S3 video streaming to MP4
|
||||
app.post('/api/transcode', async (req, res) => {
|
||||
const { key, codec, encoder } = req.body;
|
||||
const { bucket, key, codec, encoder } = req.body;
|
||||
if (!bucket) {
|
||||
return res.status(400).json({ error: 'Bucket name is required' });
|
||||
}
|
||||
if (!key) {
|
||||
return res.status(400).json({ error: 'Video key is required' });
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
return res.status(400).json({ error: 'Video key is required' });
|
||||
@@ -260,7 +282,7 @@ app.post('/api/transcode', async (req, res) => {
|
||||
const auth = extractS3Credentials(req);
|
||||
const s3Client = createS3Client(auth);
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: BUCKET_NAME,
|
||||
Bucket: bucket,
|
||||
Key: key
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user