feat: added smart auto-transcoding with probe-based routing and MKV/remux support
This commit is contained in:
Generated
+130
-1
@@ -9,11 +9,11 @@
|
||||
"version": "1.0.0",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@ffprobe-installer/ffprobe": "^2.1.2",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"express": "^4.18.2",
|
||||
"express-session": "^1.18.2",
|
||||
"ffmpeg-static": "^5.3.0",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
@@ -44,6 +44,135 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ffprobe-installer/darwin-arm64": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@ffprobe-installer/darwin-arm64/-/darwin-arm64-5.0.1.tgz",
|
||||
"integrity": "sha512-vwNCNjokH8hfkbl6m95zICHwkSzhEvDC3GVBcUp5HX8+4wsX10SP3B+bGur7XUzTIZ4cQpgJmEIAx6TUwRepMg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"hasInstallScript": true,
|
||||
"license": "LGPL-2.1",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@ffprobe-installer/darwin-x64": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ffprobe-installer/darwin-x64/-/darwin-x64-5.1.0.tgz",
|
||||
"integrity": "sha512-J+YGscZMpQclFg31O4cfVRGmDpkVsQ2fZujoUdMAAYcP0NtqpC49Hs3SWJpBdsGB4VeqOt5TTm1vSZQzs1NkhA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@ffprobe-installer/ffprobe": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@ffprobe-installer/ffprobe/-/ffprobe-2.1.2.tgz",
|
||||
"integrity": "sha512-ZNvwk4f2magF42Zji2Ese16SMj9BS7Fui4kRjg6gTYTxY3gWZNpg85n4MIfQyI9nimHg4x/gT6FVkp/bBDuBwg==",
|
||||
"license": "LGPL-2.1",
|
||||
"engines": {
|
||||
"node": ">=14.21.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@ffprobe-installer/darwin-arm64": "5.0.1",
|
||||
"@ffprobe-installer/darwin-x64": "5.1.0",
|
||||
"@ffprobe-installer/linux-arm": "5.2.0",
|
||||
"@ffprobe-installer/linux-arm64": "5.2.0",
|
||||
"@ffprobe-installer/linux-ia32": "5.2.0",
|
||||
"@ffprobe-installer/linux-x64": "5.2.0",
|
||||
"@ffprobe-installer/win32-ia32": "5.1.0",
|
||||
"@ffprobe-installer/win32-x64": "5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ffprobe-installer/linux-arm": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ffprobe-installer/linux-arm/-/linux-arm-5.2.0.tgz",
|
||||
"integrity": "sha512-PF5HqEhCY7WTWHtLDYbA/+rLS+rhslWvyBlAG1Fk8VzVlnRdl93o6hy7DE2kJgxWQbFaR3ZktPQGEzfkrmQHvQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@ffprobe-installer/linux-arm64": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ffprobe-installer/linux-arm64/-/linux-arm64-5.2.0.tgz",
|
||||
"integrity": "sha512-X1VvWtlLs6ScP73biVLuHD5ohKJKsMTa0vafCESOen4mOoNeLAYbxOVxDWAdFz9cpZgRiloFj5QD6nDj8E28yQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@ffprobe-installer/linux-ia32": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ffprobe-installer/linux-ia32/-/linux-ia32-5.2.0.tgz",
|
||||
"integrity": "sha512-TFVK5sasXyXhbIG7LtPRDmtkrkOsInwKcL43iEvEw+D9vCS2rc//mn9/0Q+BR0UoJEiMK4+ApYr/3LLVUBPOCQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@ffprobe-installer/linux-x64": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ffprobe-installer/linux-x64/-/linux-x64-5.2.0.tgz",
|
||||
"integrity": "sha512-D3UeqTLYPNs7pBWPLUYGehPdRVqU8eACox4OZy3pZUZatxye2YKlvBwEfaLdL1v2Z4FOAlLUhms0kY8m8kqSRA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@ffprobe-installer/win32-ia32": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ffprobe-installer/win32-ia32/-/win32-ia32-5.1.0.tgz",
|
||||
"integrity": "sha512-5O3vOoNRxmut0/Nu9vSazTdSHasrr+zPT2B3Hm7kjmO3QVFcIfVImS6ReQnZeSy8JPJOqXts5kX5x/3KOX54XQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "GPL-3.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@ffprobe-installer/win32-x64": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@ffprobe-installer/win32-x64/-/win32-x64-5.1.0.tgz",
|
||||
"integrity": "sha512-jMGYeAgkrdn4e2vvYt/qakgHRE3CPju4bn5TmdPfoAm1BlX1mY9cyMd8gf5vSzI8gH8Zq5WQAyAkmekX/8TSTg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "GPL-3.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "10.17.60",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"dev": "node --watch server/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ffprobe-installer/ffprobe": "^2.1.2",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"express": "^4.18.2",
|
||||
|
||||
@@ -451,6 +451,17 @@
|
||||
|
||||
<div class="settings-section">
|
||||
<h3>Streaming</h3>
|
||||
<div class="setting-item">
|
||||
<label class="setting-toggle">
|
||||
<input type="checkbox" id="setting-auto-transcode">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">Auto Transcode (Smart)</span>
|
||||
<span class="setting-hint">Automatically detect stream codecs and only transcode/remux when needed.
|
||||
Adds ~1-3s probe delay on first play.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label class="setting-toggle">
|
||||
<input type="checkbox" id="setting-force-proxy">
|
||||
|
||||
@@ -45,6 +45,7 @@ class VideoPlayer {
|
||||
forceProxy: false,
|
||||
forceTranscode: false,
|
||||
forceRemux: false,
|
||||
autoTranscode: true,
|
||||
streamFormat: 'm3u8',
|
||||
epgRefreshInterval: '24'
|
||||
};
|
||||
@@ -667,10 +668,55 @@ class VideoPlayer {
|
||||
// Determine if HLS or direct stream
|
||||
this.currentUrl = streamUrl;
|
||||
|
||||
// CHECK: Auto Transcode (Smart) - probe first, then decide
|
||||
if (this.settings.autoTranscode) {
|
||||
console.log('[Player] Auto Transcode enabled. Probing stream...');
|
||||
try {
|
||||
const probeRes = await fetch(`/api/probe?url=${encodeURIComponent(streamUrl)}`);
|
||||
const info = await probeRes.json();
|
||||
console.log(`[Player] Probe result: video=${info.video}, audio=${info.audio}, compatible=${info.compatible}`);
|
||||
|
||||
if (info.needsTranscode) {
|
||||
// Incompatible audio (AC3/EAC3/DTS) - use transcode
|
||||
console.log('[Player] Auto: Using transcode (incompatible audio)');
|
||||
const transcodeUrl = `/api/transcode?url=${encodeURIComponent(streamUrl)}`;
|
||||
this.currentUrl = transcodeUrl;
|
||||
this.video.src = transcodeUrl;
|
||||
this.video.play().catch(e => {
|
||||
if (e.name !== 'AbortError') console.log('[Player] Autoplay prevented:', e);
|
||||
});
|
||||
this.updateNowPlaying(channel);
|
||||
this.showNowPlayingOverlay();
|
||||
this.fetchEpgData(channel);
|
||||
window.dispatchEvent(new CustomEvent('channelChanged', { detail: channel }));
|
||||
return;
|
||||
} else if (info.needsRemux) {
|
||||
// Raw .ts container - use remux
|
||||
console.log('[Player] Auto: Using remux (.ts container)');
|
||||
const remuxUrl = `/api/remux?url=${encodeURIComponent(streamUrl)}`;
|
||||
this.currentUrl = remuxUrl;
|
||||
this.video.src = remuxUrl;
|
||||
this.video.play().catch(e => {
|
||||
if (e.name !== 'AbortError') console.log('[Player] Autoplay prevented:', e);
|
||||
});
|
||||
this.updateNowPlaying(channel);
|
||||
this.showNowPlayingOverlay();
|
||||
this.fetchEpgData(channel);
|
||||
window.dispatchEvent(new CustomEvent('channelChanged', { detail: channel }));
|
||||
return;
|
||||
}
|
||||
// Compatible - fall through to normal HLS.js path
|
||||
console.log('[Player] Auto: Using HLS.js (compatible)');
|
||||
} catch (err) {
|
||||
console.warn('[Player] Probe failed, using normal playback:', err.message);
|
||||
// Continue with normal playback on probe failure
|
||||
}
|
||||
}
|
||||
|
||||
// CHECK: Force Transcode Priority - transcoded streams bypass HLS.js
|
||||
if (this.settings.forceTranscode) {
|
||||
console.log('[Player] Force Transcode enabled. Routing through ffmpeg...');
|
||||
const transcodeUrl = this.getTranscodeUrl(streamUrl);
|
||||
const transcodeUrl = `/api/transcode?url=${encodeURIComponent(streamUrl)}`;
|
||||
this.currentUrl = transcodeUrl;
|
||||
|
||||
// Transcoded streams are fragmented MP4 - play directly with <video> element
|
||||
|
||||
@@ -34,6 +34,7 @@ class SettingsPage {
|
||||
const forceProxyToggle = document.getElementById('setting-force-proxy');
|
||||
const forceTranscodeToggle = document.getElementById('setting-force-transcode');
|
||||
const forceRemuxToggle = document.getElementById('setting-force-remux');
|
||||
const autoTranscodeToggle = document.getElementById('setting-auto-transcode');
|
||||
|
||||
// Load current settings
|
||||
if (this.app.player?.settings) {
|
||||
@@ -52,6 +53,9 @@ class SettingsPage {
|
||||
if (forceRemuxToggle) {
|
||||
forceRemuxToggle.checked = this.app.player.settings.forceRemux || false;
|
||||
}
|
||||
if (autoTranscodeToggle) {
|
||||
autoTranscodeToggle.checked = this.app.player.settings.autoTranscode || false;
|
||||
}
|
||||
}
|
||||
|
||||
// Arrow keys toggle
|
||||
@@ -106,6 +110,12 @@ class SettingsPage {
|
||||
this.app.player.saveSettings();
|
||||
});
|
||||
|
||||
// Auto transcode (smart) toggle
|
||||
autoTranscodeToggle?.addEventListener('change', () => {
|
||||
this.app.player.settings.autoTranscode = autoTranscodeToggle.checked;
|
||||
this.app.player.saveSettings();
|
||||
});
|
||||
|
||||
// EPG refresh interval
|
||||
const epgRefreshSelect = document.getElementById('epg-refresh-interval');
|
||||
if (epgRefreshSelect && this.app.player?.settings) {
|
||||
@@ -271,6 +281,7 @@ class SettingsPage {
|
||||
const forceProxyToggle = document.getElementById('setting-force-proxy');
|
||||
const forceTranscodeToggle = document.getElementById('setting-force-transcode');
|
||||
const forceRemuxToggle = document.getElementById('setting-force-remux');
|
||||
const autoTranscodeToggle = document.getElementById('setting-auto-transcode');
|
||||
const epgRefreshSelect = document.getElementById('epg-refresh-interval');
|
||||
const streamFormatSelect = document.getElementById('setting-stream-format');
|
||||
|
||||
@@ -283,6 +294,7 @@ class SettingsPage {
|
||||
if (forceProxyToggle) forceProxyToggle.checked = s.forceProxy || false;
|
||||
if (forceTranscodeToggle) forceTranscodeToggle.checked = s.forceTranscode || false;
|
||||
if (forceRemuxToggle) forceRemuxToggle.checked = s.forceRemux || false;
|
||||
if (autoTranscodeToggle) autoTranscodeToggle.checked = s.autoTranscode || false;
|
||||
if (epgRefreshSelect) epgRefreshSelect.value = s.epgRefreshInterval || '24';
|
||||
if (streamFormatSelect) streamFormatSelect.value = s.streamFormat || 'm3u8';
|
||||
}
|
||||
|
||||
@@ -229,6 +229,41 @@ class WatchPage {
|
||||
const isRawTs = url.includes('.ts') && !url.includes('.m3u8');
|
||||
const isDirectVideo = url.includes('.mp4') || url.includes('.mkv') || url.includes('.avi');
|
||||
|
||||
// Priority 0: Auto Transcode (Smart) - probe first, then decide
|
||||
if (settings.autoTranscode) {
|
||||
console.log('[WatchPage] Auto Transcode enabled. Probing stream...');
|
||||
try {
|
||||
const probeRes = await fetch(`/api/probe?url=${encodeURIComponent(url)}`);
|
||||
const info = await probeRes.json();
|
||||
console.log(`[WatchPage] Probe result: video=${info.video}, audio=${info.audio}, compatible=${info.compatible}`);
|
||||
|
||||
if (info.needsTranscode) {
|
||||
console.log('[WatchPage] Auto: Using transcode (incompatible audio)');
|
||||
const finalUrl = `/api/transcode?url=${encodeURIComponent(url)}`;
|
||||
this.video.src = finalUrl;
|
||||
this.video.play().catch(e => {
|
||||
if (e.name !== 'AbortError') console.error('[WatchPage] Autoplay error:', e);
|
||||
});
|
||||
this.setVolumeFromStorage();
|
||||
return;
|
||||
} else if (info.needsRemux) {
|
||||
console.log('[WatchPage] Auto: Using remux (.ts container)');
|
||||
const finalUrl = `/api/remux?url=${encodeURIComponent(url)}`;
|
||||
this.video.src = finalUrl;
|
||||
this.video.play().catch(e => {
|
||||
if (e.name !== 'AbortError') console.error('[WatchPage] Autoplay error:', e);
|
||||
});
|
||||
this.setVolumeFromStorage();
|
||||
return;
|
||||
}
|
||||
// Compatible - fall through to normal playback
|
||||
console.log('[WatchPage] Auto: Using normal playback (compatible)');
|
||||
} catch (err) {
|
||||
console.warn('[WatchPage] Probe failed, using normal playback:', err.message);
|
||||
// Continue with normal playback on probe failure
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 1: Force Transcode - route through FFmpeg
|
||||
if (settings.forceTranscode) {
|
||||
console.log('[WatchPage] Force Transcode enabled');
|
||||
|
||||
@@ -65,6 +65,7 @@ function getDefaultSettings() {
|
||||
forceProxy: false,
|
||||
forceTranscode: false,
|
||||
forceRemux: false,
|
||||
autoTranscode: true,
|
||||
streamFormat: 'm3u8',
|
||||
epgRefreshInterval: '24'
|
||||
};
|
||||
|
||||
@@ -52,7 +52,33 @@ function findFFmpeg() {
|
||||
}
|
||||
}
|
||||
|
||||
function findFFprobe() {
|
||||
// Try system ffprobe first
|
||||
try {
|
||||
execSync('ffprobe -version', { stdio: 'ignore' });
|
||||
console.log('FFprobe binary configured at: ffprobe (system)');
|
||||
return 'ffprobe';
|
||||
} catch (e) {
|
||||
// Not found in system
|
||||
}
|
||||
|
||||
// Try @ffprobe-installer/ffprobe package
|
||||
try {
|
||||
const ffprobePath = require('@ffprobe-installer/ffprobe').path;
|
||||
if (ffprobePath) {
|
||||
console.log('FFprobe binary configured at:', ffprobePath);
|
||||
return ffprobePath;
|
||||
}
|
||||
} catch (err) {
|
||||
// Package not available
|
||||
}
|
||||
|
||||
console.warn('FFprobe not available - auto transcode will fallback to always transcode');
|
||||
return null;
|
||||
}
|
||||
|
||||
app.locals.ffmpegPath = findFFmpeg();
|
||||
app.locals.ffprobePath = findFFprobe();
|
||||
|
||||
// API Routes
|
||||
app.use('/api/auth', require('./routes/auth'));
|
||||
@@ -62,6 +88,7 @@ app.use('/api/channels', require('./routes/channels'));
|
||||
app.use('/api/favorites', require('./routes/favorites'));
|
||||
app.use('/api/transcode', require('./routes/transcode'));
|
||||
app.use('/api/remux', require('./routes/remux'));
|
||||
app.use('/api/probe', require('./routes/probe'));
|
||||
app.use('/api/settings', require('./routes/settings'));
|
||||
|
||||
// SPA fallback - serve index.html for all non-API routes
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
/**
|
||||
* Probe endpoint - detects stream codecs and container
|
||||
* GET /api/probe?url=...
|
||||
*
|
||||
* Returns:
|
||||
* {
|
||||
* video: "h264",
|
||||
* audio: "aac",
|
||||
* container: "mpegts",
|
||||
* compatible: true,
|
||||
* needsRemux: false,
|
||||
* needsTranscode: false
|
||||
* }
|
||||
*/
|
||||
|
||||
// Probe cache (URL → result)
|
||||
const probeCache = new Map();
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
// Browser-compatible codecs
|
||||
const BROWSER_VIDEO_CODECS = ['h264', 'avc', 'avc1'];
|
||||
const BROWSER_AUDIO_CODECS = ['aac', 'mp3', 'opus', 'vorbis'];
|
||||
|
||||
/**
|
||||
* Probe stream with ffprobe
|
||||
*/
|
||||
function probeStream(url, ffprobePath, timeout = 15000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = [
|
||||
'-v', 'error',
|
||||
'-print_format', 'json',
|
||||
'-show_streams',
|
||||
'-show_format',
|
||||
'-probesize', '5000000',
|
||||
'-analyzeduration', '5000000',
|
||||
url
|
||||
];
|
||||
|
||||
const proc = spawn(ffprobePath, args);
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
proc.kill('SIGKILL');
|
||||
reject(new Error('Probe timeout'));
|
||||
}, timeout);
|
||||
|
||||
proc.stdout.on('data', (data) => { stdout += data; });
|
||||
proc.stderr.on('data', (data) => { stderr += data; });
|
||||
|
||||
proc.on('close', (code) => {
|
||||
clearTimeout(timer);
|
||||
if (code !== 0) {
|
||||
reject(new Error(`ffprobe exited with code ${code}: ${stderr}`));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = JSON.parse(stdout);
|
||||
resolve(result);
|
||||
} catch (e) {
|
||||
reject(new Error('Failed to parse ffprobe output'));
|
||||
}
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze probe result and determine compatibility
|
||||
*/
|
||||
function analyzeProbeResult(probeResult, url) {
|
||||
const streams = probeResult.streams || [];
|
||||
const format = probeResult.format || {};
|
||||
|
||||
const videoStream = streams.find(s => s.codec_type === 'video');
|
||||
const audioStream = streams.find(s => s.codec_type === 'audio');
|
||||
|
||||
const videoCodec = videoStream?.codec_name?.toLowerCase() || 'unknown';
|
||||
const audioCodec = audioStream?.codec_name?.toLowerCase() || 'unknown';
|
||||
const container = format.format_name?.toLowerCase() || 'unknown';
|
||||
|
||||
// Check codec compatibility
|
||||
const videoOk = BROWSER_VIDEO_CODECS.some(c => videoCodec.includes(c));
|
||||
const audioOk = BROWSER_AUDIO_CODECS.some(c => audioCodec.includes(c));
|
||||
|
||||
// Browser-safe containers
|
||||
// Note: We exclude 'webm' because ffprobe reports MKV as "matroska,webm",
|
||||
// and H.264/AAC in MKV/WebM is not universally supported. Best to remux to MP4.
|
||||
const BROWSER_CONTAINERS = ['hls', 'mp4', 'mov'];
|
||||
const containerOk = BROWSER_CONTAINERS.some(c => container.includes(c));
|
||||
|
||||
// Check if it's a raw TS stream (not HLS)
|
||||
const isRawTs = (container.includes('mpegts') || url.endsWith('.ts')) && !url.includes('.m3u8');
|
||||
|
||||
// Determine what processing is needed
|
||||
// 1. Incompatible audio -> Transcode (highest priority)
|
||||
const needsTranscode = !audioOk;
|
||||
|
||||
// 2. Compatible audio/video but incompatible container -> Remux
|
||||
// This catches raw TS, MKV, AVI, etc. that have H.264/AAC but wrong container
|
||||
const needsRemux = !needsTranscode && (!containerOk || isRawTs);
|
||||
|
||||
const compatible = !needsTranscode && !needsRemux;
|
||||
|
||||
return {
|
||||
video: videoCodec,
|
||||
audio: audioCodec,
|
||||
container: container,
|
||||
compatible: compatible,
|
||||
needsRemux: needsRemux,
|
||||
needsTranscode: needsTranscode
|
||||
};
|
||||
}
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const { url } = req.query;
|
||||
if (!url) {
|
||||
return res.status(400).json({ error: 'URL parameter is required' });
|
||||
}
|
||||
|
||||
const ffprobePath = req.app.locals.ffprobePath;
|
||||
|
||||
if (!ffprobePath) {
|
||||
// No ffprobe available - assume needs transcoding to be safe
|
||||
console.log('[Probe] FFprobe not available, assuming transcode needed');
|
||||
return res.json({
|
||||
video: 'unknown',
|
||||
audio: 'unknown',
|
||||
container: 'unknown',
|
||||
compatible: false,
|
||||
needsRemux: false,
|
||||
needsTranscode: true
|
||||
});
|
||||
}
|
||||
|
||||
// Check cache
|
||||
const cached = probeCache.get(url);
|
||||
if (cached && (Date.now() - cached.timestamp < CACHE_TTL)) {
|
||||
console.log(`[Probe] Cache hit for: ${url.substring(0, 50)}...`);
|
||||
return res.json(cached.result);
|
||||
}
|
||||
|
||||
console.log(`[Probe] Probing: ${url.substring(0, 80)}...`);
|
||||
|
||||
try {
|
||||
const probeResult = await probeStream(url, ffprobePath);
|
||||
const analysis = analyzeProbeResult(probeResult, url);
|
||||
|
||||
// Cache result
|
||||
probeCache.set(url, { result: analysis, timestamp: Date.now() });
|
||||
|
||||
console.log(`[Probe] Result: video=${analysis.video}, audio=${analysis.audio}, ` +
|
||||
`container=${analysis.container}, compatible=${analysis.compatible}, ` +
|
||||
`needsRemux=${analysis.needsRemux}, needsTranscode=${analysis.needsTranscode}`);
|
||||
|
||||
res.json(analysis);
|
||||
} catch (err) {
|
||||
console.error('[Probe] Failed:', err.message);
|
||||
|
||||
// On error, assume transcode needed to be safe
|
||||
res.json({
|
||||
video: 'unknown',
|
||||
audio: 'unknown',
|
||||
container: 'unknown',
|
||||
compatible: false,
|
||||
needsRemux: false,
|
||||
needsTranscode: true,
|
||||
error: err.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -40,9 +40,13 @@ router.get('/', (req, res) => {
|
||||
'-reconnect_streamed', '1',
|
||||
'-reconnect_delay_max', '5',
|
||||
'-i', url,
|
||||
// Map all streams explicitly
|
||||
'-map', '0',
|
||||
// Copy ALL streams without re-encoding
|
||||
// STRICT MAPPING: Only map video and audio, ignore subtitles/data/attachments
|
||||
// This prevents remux failure when source container has incompatible subtitle tracks (e.g. MKV -> MP4)
|
||||
'-map', '0:v',
|
||||
'-map', '0:a',
|
||||
// Drop subtitles (-sn) and data (-dn) explicitly
|
||||
'-sn', '-dn',
|
||||
// Copy streams without re-encoding
|
||||
'-c', 'copy',
|
||||
// NOTE: We intentionally do NOT use -bsf:a aac_adtstoasc here
|
||||
// That filter only works for AAC audio and breaks AC3/EAC3/MP3.
|
||||
|
||||
Reference in New Issue
Block a user