Compare commits
2 Commits
2491988d23
...
1580b589c0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1580b589c0 | ||
|
|
f991125923 |
@@ -5,8 +5,12 @@ HOST=0.0.0.0
|
|||||||
AWS_REGION=us-east-1
|
AWS_REGION=us-east-1
|
||||||
AWS_ACCESS_KEY_ID=your_access_key
|
AWS_ACCESS_KEY_ID=your_access_key
|
||||||
AWS_SECRET_ACCESS_KEY=your_secret_key
|
AWS_SECRET_ACCESS_KEY=your_secret_key
|
||||||
|
S3_BUCKET_ADDRESS=https://s3.example.com/your_bucket_name
|
||||||
S3_BUCKET_NAME=your_s3_bucket_name
|
S3_BUCKET_NAME=your_s3_bucket_name
|
||||||
|
|
||||||
# For MinIO or other S3-compatible storage
|
# For MinIO or other S3-compatible storage
|
||||||
S3_ENDPOINT=http://127.0.0.1:9000
|
S3_ENDPOINT=http://127.0.0.1:9000
|
||||||
S3_FORCE_PATH_STYLE=true
|
S3_FORCE_PATH_STYLE=true
|
||||||
|
|
||||||
|
# Application Title
|
||||||
|
APP_TITLE=S3 Media Transcoder
|
||||||
|
|||||||
@@ -119,6 +119,96 @@ header p {
|
|||||||
min-width: 0;
|
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 {
|
.glass-panel {
|
||||||
background: var(--panel-bg);
|
background: var(--panel-bg);
|
||||||
backdrop-filter: blur(16px);
|
backdrop-filter: blur(16px);
|
||||||
@@ -129,6 +219,24 @@ header p {
|
|||||||
box-shadow: 0 20px 40px rgba(148, 163, 184, 0.15);
|
box-shadow: 0 20px 40px rgba(148, 163, 184, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.top-banner {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-banner.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -157,6 +265,39 @@ header p {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.credentials-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 180px;
|
||||||
|
flex: 1 1 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-field label {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-field input {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credential-field input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.codec-panel label {
|
.codec-panel label {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
|
|||||||
@@ -16,11 +16,25 @@
|
|||||||
<div class="glow-orb orb-2"></div>
|
<div class="glow-orb orb-2"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div id="login-screen" class="login-screen">
|
||||||
<header>
|
<div class="login-card">
|
||||||
<h1>S3 Media <span>Transcoder</span></h1>
|
<h2>Login</h2>
|
||||||
<p>Seamlessly transcode videos from AWS S3 to MP4 for browser playback</p>
|
<p>Please enter your S3 credentials to continue.</p>
|
||||||
</header>
|
<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">
|
<main class="dashboard">
|
||||||
<section class="video-list-section glass-panel">
|
<section class="video-list-section glass-panel">
|
||||||
@@ -35,6 +49,12 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div class="codec-panel">
|
||||||
<label for="codec-select">编码方式:</label>
|
<label for="codec-select">编码方式:</label>
|
||||||
<select id="codec-select">
|
<select id="codec-select">
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const loadingSpinner = document.getElementById('loading-spinner');
|
const loadingSpinner = document.getElementById('loading-spinner');
|
||||||
const refreshBtn = document.getElementById('refresh-btn');
|
const refreshBtn = document.getElementById('refresh-btn');
|
||||||
const resetCacheBtn = document.getElementById('reset-cache-btn');
|
const resetCacheBtn = document.getElementById('reset-cache-btn');
|
||||||
|
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 codecSelect = document.getElementById('codec-select');
|
||||||
const encoderSelect = document.getElementById('encoder-select');
|
const encoderSelect = document.getElementById('encoder-select');
|
||||||
const playerOverlay = document.getElementById('player-overlay');
|
const playerOverlay = document.getElementById('player-overlay');
|
||||||
@@ -15,13 +22,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const progressInfo = document.getElementById('progress-info');
|
const progressInfo = document.getElementById('progress-info');
|
||||||
const progressText = document.getElementById('progress-text');
|
const progressText = document.getElementById('progress-text');
|
||||||
const progressFill = document.getElementById('progress-fill');
|
const progressFill = document.getElementById('progress-fill');
|
||||||
|
const topBanner = document.getElementById('top-banner');
|
||||||
|
|
||||||
let currentPollInterval = null;
|
let currentPollInterval = null;
|
||||||
|
let selectedBucket = null;
|
||||||
let selectedKey = null;
|
let selectedKey = null;
|
||||||
let ws = null;
|
let ws = null;
|
||||||
let wsConnected = false;
|
let wsConnected = false;
|
||||||
let subscribedKey = null;
|
let subscribedKey = null;
|
||||||
let currentVideoKey = null;
|
let currentVideoKey = null;
|
||||||
|
let s3AuthHeaders = {};
|
||||||
|
|
||||||
const sendWsMessage = (message) => {
|
const sendWsMessage = (message) => {
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
@@ -51,6 +61,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadConfig = async () => {
|
||||||
|
if (!topBanner) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/config');
|
||||||
|
if (!res.ok) throw new Error('Failed to load config');
|
||||||
|
const data = await res.json();
|
||||||
|
const title = data.title || 'S3 Media Transcoder';
|
||||||
|
topBanner.textContent = title;
|
||||||
|
topBanner.classList.remove('hidden');
|
||||||
|
document.title = title;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Config load failed:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const connectWebSocket = () => {
|
const connectWebSocket = () => {
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
ws = new WebSocket(`${protocol}://${window.location.host}`);
|
ws = new WebSocket(`${protocol}://${window.location.host}`);
|
||||||
@@ -90,6 +115,87 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
progressFill.style.width = '0%';
|
progressFill.style.width = '0%';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setAuthHeaders = (username, password) => {
|
||||||
|
s3AuthHeaders = {};
|
||||||
|
if (username) s3AuthHeaders['X-S3-Username'] = username;
|
||||||
|
if (password) s3AuthHeaders['X-S3-Password'] = password;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 () => {
|
const resetCache = async () => {
|
||||||
if (!resetCacheBtn) return;
|
if (!resetCacheBtn) return;
|
||||||
resetCacheBtn.disabled = true;
|
resetCacheBtn.disabled = true;
|
||||||
@@ -143,13 +249,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch list of videos from the backend
|
// Fetch list of videos from the backend
|
||||||
const fetchVideos = async () => {
|
const fetchVideos = async (bucket) => {
|
||||||
|
if (!bucket) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
videoListEl.classList.add('hidden');
|
videoListEl.classList.add('hidden');
|
||||||
loadingSpinner.classList.remove('hidden');
|
loadingSpinner.classList.remove('hidden');
|
||||||
videoListEl.innerHTML = '';
|
videoListEl.innerHTML = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/videos');
|
const res = await fetch(`/api/videos?bucket=${encodeURIComponent(bucket)}`, { headers: s3AuthHeaders });
|
||||||
if (!res.ok) throw new Error('Failed to fetch videos. Check S3 Config.');
|
if (!res.ok) throw new Error('Failed to fetch videos. Check S3 Config.');
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -296,10 +405,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
try {
|
try {
|
||||||
const codec = codecSelect?.value || 'h264';
|
const codec = codecSelect?.value || 'h264';
|
||||||
const encoder = encoderSelect?.value || 'software';
|
const encoder = encoderSelect?.value || 'software';
|
||||||
|
if (!selectedBucket) throw new Error('No bucket selected');
|
||||||
const res = await fetch('/api/transcode', {
|
const res = await fetch('/api/transcode', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', ...s3AuthHeaders },
|
||||||
body: JSON.stringify({ key: selectedKey, codec, encoder })
|
body: JSON.stringify({ bucket: selectedBucket, key: selectedKey, codec, encoder })
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
@@ -370,9 +480,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Bind events
|
// Bind events
|
||||||
refreshBtn.addEventListener('click', 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Connect WebSocket and initial load
|
// Initial state: require login before loading data
|
||||||
connectWebSocket();
|
showLogin();
|
||||||
fetchVideos();
|
|
||||||
});
|
});
|
||||||
|
|||||||
100
server.js
100
server.js
@@ -7,7 +7,7 @@ const path = require('path');
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
const WebSocket = require('ws');
|
const WebSocket = require('ws');
|
||||||
const ffmpeg = require('fluent-ffmpeg');
|
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();
|
dotenv.config();
|
||||||
|
|
||||||
@@ -20,18 +20,44 @@ app.use(cors());
|
|||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.static('public'));
|
app.use(express.static('public'));
|
||||||
|
|
||||||
// Configure AWS S3 Client
|
const rawBucketAddress = process.env.S3_BUCKET_ADDRESS || process.env.S3_BUCKET_NAME || '';
|
||||||
const s3Client = new S3Client({
|
let BUCKET_NAME = rawBucketAddress;
|
||||||
region: process.env.AWS_REGION || 'us-east-1',
|
|
||||||
endpoint: process.env.S3_ENDPOINT,
|
const parsedBucketUrl = rawBucketAddress.includes('://') ? (() => {
|
||||||
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
|
try {
|
||||||
credentials: {
|
const parsed = new URL(rawBucketAddress);
|
||||||
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
const name = parsed.pathname.replace(/^\/+/, '');
|
||||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
if (name) {
|
||||||
}
|
BUCKET_NAME = name;
|
||||||
});
|
}
|
||||||
|
return parsed.origin;
|
||||||
|
} catch (err) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})() : undefined;
|
||||||
|
|
||||||
|
const defaultS3ClientConfig = {
|
||||||
|
region: process.env.AWS_REGION || 'us-east-1',
|
||||||
|
endpoint: process.env.S3_ENDPOINT || parsedBucketUrl,
|
||||||
|
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true'
|
||||||
|
};
|
||||||
|
|
||||||
|
const createS3Client = (credentials) => {
|
||||||
|
const clientConfig = { ...defaultS3ClientConfig };
|
||||||
|
if (credentials && credentials.username && credentials.password) {
|
||||||
|
clientConfig.credentials = {
|
||||||
|
accessKeyId: credentials.username,
|
||||||
|
secretAccessKey: credentials.password
|
||||||
|
};
|
||||||
|
} else if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
|
||||||
|
clientConfig.credentials = {
|
||||||
|
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return new S3Client(clientConfig);
|
||||||
|
};
|
||||||
|
|
||||||
const BUCKET_NAME = process.env.S3_BUCKET_NAME;
|
|
||||||
const progressMap = {};
|
const progressMap = {};
|
||||||
const wsSubscriptions = new Map();
|
const wsSubscriptions = new Map();
|
||||||
|
|
||||||
@@ -84,6 +110,15 @@ const shouldRetryWithSoftware = (message) => {
|
|||||||
return /Cannot load libcuda\.so\.1|Could not open encoder before EOF|Error while opening encoder|Operation not permitted|Invalid argument/i.test(message);
|
return /Cannot load libcuda\.so\.1|Could not open encoder before EOF|Error while opening encoder|Operation not permitted|Invalid argument/i.test(message);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extractS3Credentials = (req) => {
|
||||||
|
const username = req.headers['x-s3-username'] || req.body?.username || '';
|
||||||
|
const password = req.headers['x-s3-password'] || req.body?.password || '';
|
||||||
|
return {
|
||||||
|
username: typeof username === 'string' ? username.trim() : '',
|
||||||
|
password: typeof password === 'string' ? password : ''
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const wss = new WebSocket.Server({ server });
|
const wss = new WebSocket.Server({ server });
|
||||||
|
|
||||||
wss.on('connection', (ws) => {
|
wss.on('connection', (ws) => {
|
||||||
@@ -127,18 +162,36 @@ 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
|
// Endpoint to list videos in the bucket
|
||||||
app.get('/api/videos', async (req, res) => {
|
app.get('/api/videos', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!BUCKET_NAME) {
|
const bucket = req.query.bucket || BUCKET_NAME;
|
||||||
return res.status(500).json({ error: 'S3_BUCKET_NAME not configured' });
|
if (!bucket) {
|
||||||
|
return res.status(400).json({ error: 'Bucket name is required' });
|
||||||
}
|
}
|
||||||
const allObjects = [];
|
const allObjects = [];
|
||||||
|
const auth = extractS3Credentials(req);
|
||||||
|
const s3Client = createS3Client(auth);
|
||||||
let continuationToken;
|
let continuationToken;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
const command = new ListObjectsV2Command({
|
const command = new ListObjectsV2Command({
|
||||||
Bucket: BUCKET_NAME,
|
Bucket: bucket,
|
||||||
ContinuationToken: continuationToken,
|
ContinuationToken: continuationToken,
|
||||||
});
|
});
|
||||||
const response = await s3Client.send(command);
|
const response = await s3Client.send(command);
|
||||||
@@ -168,6 +221,11 @@ app.get('/api/videos', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/config', (req, res) => {
|
||||||
|
const title = process.env.APP_TITLE || 'S3 Media Transcoder';
|
||||||
|
res.json({ title });
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/reset-cache', (req, res) => {
|
app.post('/api/reset-cache', (req, res) => {
|
||||||
try {
|
try {
|
||||||
clearMp4Cache();
|
clearMp4Cache();
|
||||||
@@ -181,7 +239,13 @@ app.post('/api/reset-cache', (req, res) => {
|
|||||||
|
|
||||||
// Endpoint to transcode S3 video streaming to MP4
|
// Endpoint to transcode S3 video streaming to MP4
|
||||||
app.post('/api/transcode', async (req, res) => {
|
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) {
|
if (!key) {
|
||||||
return res.status(400).json({ error: 'Video key is required' });
|
return res.status(400).json({ error: 'Video key is required' });
|
||||||
@@ -215,8 +279,10 @@ app.post('/api/transcode', async (req, res) => {
|
|||||||
fs.mkdirSync(outputDir, { recursive: true });
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
|
||||||
// Get S3 stream
|
// Get S3 stream
|
||||||
|
const auth = extractS3Credentials(req);
|
||||||
|
const s3Client = createS3Client(auth);
|
||||||
const command = new GetObjectCommand({
|
const command = new GetObjectCommand({
|
||||||
Bucket: BUCKET_NAME,
|
Bucket: bucket,
|
||||||
Key: key
|
Key: key
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user