fixed choppy video on iPhone - mp4 encoding
This commit is contained in:
147
server/index.js
147
server/index.js
@@ -7,6 +7,7 @@ import multer from 'multer';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
// Generate a unique link ID
|
||||
const generateUniqueId = () => {
|
||||
@@ -17,7 +18,7 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
const PORT = 3001;
|
||||
const PORT = process.env.PORT ? Number(process.env.PORT) : 3005;
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
@@ -30,13 +31,22 @@ if (!fs.existsSync(mediaDir)) {
|
||||
}
|
||||
|
||||
// Configure multer for video uploads
|
||||
const getExtensionForMime = (mimetype) => {
|
||||
if (!mimetype) return '.webm';
|
||||
if (mimetype.includes('mp4')) return '.mp4';
|
||||
if (mimetype.includes('webm')) return '.webm';
|
||||
if (mimetype.includes('quicktime')) return '.mov';
|
||||
return '.webm';
|
||||
};
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
cb(null, mediaDir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const timestamp = Date.now();
|
||||
const safeName = `recording_${timestamp}.webm`;
|
||||
const ext = getExtensionForMime(file.mimetype);
|
||||
const safeName = `recording_${timestamp}${ext}`;
|
||||
cb(null, safeName);
|
||||
}
|
||||
});
|
||||
@@ -56,6 +66,61 @@ const upload = multer({
|
||||
// Initialize SQLite database
|
||||
let db;
|
||||
|
||||
const transcodeToMp4 = (inputPath, outputPath) => new Promise((resolve, reject) => {
|
||||
const args = [
|
||||
'-y',
|
||||
'-i', inputPath,
|
||||
'-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
|
||||
'-c:v', 'libx264',
|
||||
'-profile:v', 'baseline',
|
||||
'-level', '3.0',
|
||||
'-pix_fmt', 'yuv420p',
|
||||
'-c:a', 'aac',
|
||||
'-b:a', '128k',
|
||||
'-movflags', '+faststart',
|
||||
outputPath
|
||||
];
|
||||
|
||||
const ffmpeg = spawn('ffmpeg', args, { stdio: 'ignore' });
|
||||
ffmpeg.on('error', reject);
|
||||
ffmpeg.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`ffmpeg exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const isMp4Upload = (file) => {
|
||||
if (!file) return false;
|
||||
if (file.mimetype && file.mimetype.includes('mp4')) return true;
|
||||
return path.extname(file.filename).toLowerCase() === '.mp4';
|
||||
};
|
||||
|
||||
const transcodeQueue = [];
|
||||
let isTranscoding = false;
|
||||
|
||||
const processTranscodeQueue = async () => {
|
||||
if (isTranscoding) return;
|
||||
const job = transcodeQueue.shift();
|
||||
if (!job) return;
|
||||
isTranscoding = true;
|
||||
try {
|
||||
await job();
|
||||
} catch (error) {
|
||||
console.error('Transcode job failed:', error);
|
||||
} finally {
|
||||
isTranscoding = false;
|
||||
processTranscodeQueue();
|
||||
}
|
||||
};
|
||||
|
||||
const enqueueTranscode = (job) => {
|
||||
transcodeQueue.push(job);
|
||||
processTranscodeQueue();
|
||||
};
|
||||
|
||||
async function initializeDatabase() {
|
||||
db = await open({
|
||||
filename: path.join(__dirname, 'signsync.db'),
|
||||
@@ -69,14 +134,32 @@ async function initializeDatabase() {
|
||||
name TEXT NOT NULL,
|
||||
email TEXT NOT NULL,
|
||||
recorded_video_path TEXT NOT NULL,
|
||||
recorded_video_processing INTEGER DEFAULT 0,
|
||||
youtube_video_url TEXT NOT NULL,
|
||||
name_2 TEXT,
|
||||
email_2 TEXT,
|
||||
recorded_video_path_2 TEXT,
|
||||
recorded_video_processing_2 INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
const addColumnIfMissing = async (columnDef) => {
|
||||
try {
|
||||
await db.exec(`ALTER TABLE recordings ADD COLUMN ${columnDef}`);
|
||||
} catch (error) {
|
||||
if (!String(error.message).toLowerCase().includes('duplicate column')) {
|
||||
console.error('Error adding column:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await addColumnIfMissing('recorded_video_processing INTEGER DEFAULT 0');
|
||||
await addColumnIfMissing('recorded_video_processing_2 INTEGER DEFAULT 0');
|
||||
|
||||
await db.run('UPDATE recordings SET recorded_video_processing = 0 WHERE recorded_video_processing IS NULL');
|
||||
await db.run('UPDATE recordings SET recorded_video_processing_2 = 0 WHERE recorded_video_processing_2 IS NULL');
|
||||
|
||||
console.log('Database initialized');
|
||||
}
|
||||
|
||||
@@ -95,11 +178,13 @@ app.post('/api/recordings', upload.single('video'), async (req, res) => {
|
||||
|
||||
const recordedVideoPath = `/media/${req.file.filename}`;
|
||||
const uniqueLink = generateUniqueId();
|
||||
const needsTranscode = !isMp4Upload(req.file);
|
||||
const recordedVideoProcessing = needsTranscode ? 1 : 0;
|
||||
|
||||
const result = await db.run(
|
||||
`INSERT INTO recordings (unique_link, name, email, recorded_video_path, youtube_video_url)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
[uniqueLink, name, email, recordedVideoPath, youtubeVideoUrl || '']
|
||||
`INSERT INTO recordings (unique_link, name, email, recorded_video_path, recorded_video_processing, youtube_video_url)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
[uniqueLink, name, email, recordedVideoPath, recordedVideoProcessing, youtubeVideoUrl || '']
|
||||
);
|
||||
|
||||
res.json({
|
||||
@@ -107,8 +192,30 @@ app.post('/api/recordings', upload.single('video'), async (req, res) => {
|
||||
id: result.lastID,
|
||||
uniqueLink,
|
||||
recordedVideoPath,
|
||||
recordedVideoProcessing,
|
||||
message: 'Recording saved successfully'
|
||||
});
|
||||
|
||||
if (needsTranscode) {
|
||||
const inputPath = req.file.path;
|
||||
const outputPath = inputPath.replace(/\.[^.]+$/, '.mp4');
|
||||
enqueueTranscode(async () => {
|
||||
try {
|
||||
await transcodeToMp4(inputPath, outputPath);
|
||||
await db.run(
|
||||
`UPDATE recordings SET recorded_video_path = ?, recorded_video_processing = 0 WHERE unique_link = ?`,
|
||||
[`/media/${path.basename(outputPath)}`, uniqueLink]
|
||||
);
|
||||
fs.unlink(inputPath, () => {});
|
||||
} catch (transcodeError) {
|
||||
console.error('Transcode failed, keeping original file:', transcodeError);
|
||||
await db.run(
|
||||
`UPDATE recordings SET recorded_video_processing = 0 WHERE unique_link = ?`,
|
||||
[uniqueLink]
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving recording:', error);
|
||||
res.status(500).json({ error: 'Failed to save recording' });
|
||||
@@ -193,17 +300,43 @@ app.post('/api/share/:uniqueLink/second-video', upload.single('video'), async (r
|
||||
}
|
||||
|
||||
const recordedVideoPath2 = `/media/${req.file.filename}`;
|
||||
const needsTranscode = !isMp4Upload(req.file);
|
||||
const recordedVideoProcessing2 = needsTranscode ? 1 : 0;
|
||||
|
||||
await db.run(
|
||||
`UPDATE recordings SET name_2 = ?, email_2 = ?, recorded_video_path_2 = ? WHERE unique_link = ?`,
|
||||
[name, email, recordedVideoPath2, uniqueLink]
|
||||
`UPDATE recordings
|
||||
SET name_2 = ?, email_2 = ?, recorded_video_path_2 = ?, recorded_video_processing_2 = ?
|
||||
WHERE unique_link = ?`,
|
||||
[name, email, recordedVideoPath2, recordedVideoProcessing2, uniqueLink]
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
recordedVideoPath2,
|
||||
recordedVideoProcessing2,
|
||||
message: 'Second recording saved successfully'
|
||||
});
|
||||
|
||||
if (needsTranscode) {
|
||||
const inputPath = req.file.path;
|
||||
const outputPath = inputPath.replace(/\.[^.]+$/, '.mp4');
|
||||
enqueueTranscode(async () => {
|
||||
try {
|
||||
await transcodeToMp4(inputPath, outputPath);
|
||||
await db.run(
|
||||
`UPDATE recordings SET recorded_video_path_2 = ?, recorded_video_processing_2 = 0 WHERE unique_link = ?`,
|
||||
[`/media/${path.basename(outputPath)}`, uniqueLink]
|
||||
);
|
||||
fs.unlink(inputPath, () => {});
|
||||
} catch (transcodeError) {
|
||||
console.error('Transcode failed, keeping original file:', transcodeError);
|
||||
await db.run(
|
||||
`UPDATE recordings SET recorded_video_processing_2 = 0 WHERE unique_link = ?`,
|
||||
[uniqueLink]
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving second recording:', error);
|
||||
res.status(500).json({ error: 'Failed to save second recording' });
|
||||
|
||||
Reference in New Issue
Block a user