feat: added smart auto-transcoding with probe-based routing and MKV/remux support
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user