Linux versino and Gif output
This commit is contained in:
61
web/app.js
61
web/app.js
@@ -120,6 +120,7 @@ const tcCurEl = $('#tcCur'), markInEl = $('#markIn'), markOutEl = $('#markOut');
|
||||
const DEFAULTS = {
|
||||
mode: 'copy', join: true, output: '', allAudio: true, noAudio: false,
|
||||
vcodec: 'libx264', quality: 20, speed: 'balanced', height: 0, fps: '', acodec: 'aac', abitrate: 192,
|
||||
gifHeight: 480, gifFps: '15', gifDither: 'sierra2_4a', gifLoop: true,
|
||||
lastDir: '', volume: 1, muted: false,
|
||||
};
|
||||
const PALETTE = ['#4c8dff', '#3ccf91', '#ff8a4c', '#c77dff', '#ffd23f', '#ff5d8f', '#2ec4d6', '#a3d14b'];
|
||||
@@ -248,7 +249,7 @@ async function addFiles(builtin = false) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (r.error) toast('The Windows file dialog could not be opened, using the built-in browser instead.', 'error');
|
||||
if (r.error) toast('The file dialog could not be opened, using the built-in browser instead.', 'error');
|
||||
} catch (e) {
|
||||
if (e.status === 409) return;
|
||||
}
|
||||
@@ -1197,6 +1198,12 @@ function copyContainerChoices() {
|
||||
return exts;
|
||||
}
|
||||
|
||||
function exportExts(mode) {
|
||||
if (mode === 'copy') return copyContainerChoices();
|
||||
if (mode === 'gif') return ['gif'];
|
||||
return S.info.containers.map(c => c.ext);
|
||||
}
|
||||
|
||||
function firstClipSource() { return srcById(S.clips[0] && S.clips[0].src) || cur(); }
|
||||
|
||||
function defaultOutput() {
|
||||
@@ -1207,6 +1214,7 @@ function defaultOutput() {
|
||||
}
|
||||
|
||||
function defaultExt() {
|
||||
if (S.settings.mode === 'gif') return 'gif';
|
||||
if (S.settings.mode !== 'copy') return 'mp4';
|
||||
const first = firstClipSource();
|
||||
const e = first ? extOf(first.path) : '';
|
||||
@@ -1240,7 +1248,7 @@ function fillExport() {
|
||||
$$('input[name=mode]').forEach(r => { r.checked = r.value === st.mode; });
|
||||
$$('input[name=join]').forEach(r => { r.checked = (r.value === '1') === st.join; });
|
||||
|
||||
const exts = st.mode === 'copy' ? copyContainerChoices() : info.containers.map(c => c.ext);
|
||||
const exts = exportExts(st.mode);
|
||||
if (st.output && !exts.includes(extOf(st.output))) st.output = replaceExt(st.output, exts.includes(defaultExt()) ? defaultExt() : exts[0]);
|
||||
setOptions($('#exContainer'), exts.map(e => [e, '.' + e]), extOf(st.output));
|
||||
if (document.activeElement !== $('#exOutput')) $('#exOutput').value = st.output;
|
||||
@@ -1269,6 +1277,11 @@ function fillExport() {
|
||||
setOptions($('#exACodec'), [...acs.map(c => [c.name, c.label]), ['none', 'No audio']], st.acodec);
|
||||
$('#exABitrate').value = String(st.abitrate);
|
||||
$('#exABitrate').disabled = st.acodec === 'none' || st.acodec === 'flac';
|
||||
|
||||
$('#exGifHeight').value = String(st.gifHeight);
|
||||
$('#exGifFPS').value = st.gifFps;
|
||||
$('#exGifDither').value = st.gifDither;
|
||||
$('#exGifLoop').value = st.gifLoop ? '1' : '0';
|
||||
updateExportView();
|
||||
}
|
||||
|
||||
@@ -1297,12 +1310,14 @@ function formatSig(m, allAudio) {
|
||||
|
||||
function updateExportView() {
|
||||
const st = S.settings;
|
||||
const copy = st.mode === 'copy';
|
||||
const copy = st.mode === 'copy', gif = st.mode === 'gif';
|
||||
const n = S.clips.length;
|
||||
const total = S.clips.reduce((s, c) => s + c.out - c.in, 0);
|
||||
$('#exCopyOpts').classList.toggle('hidden', !copy);
|
||||
$('#exEncOpts').classList.toggle('hidden', copy);
|
||||
$('#exSummary').textContent = `${n} clip${n !== 1 ? 's' : ''} · ${fmtLen(total)} total → ${st.join || n === 1 ? 'one video file' : n + ' separate files'}`;
|
||||
$('#exEncOpts').classList.toggle('hidden', st.mode !== 'encode');
|
||||
$('#exGifOpts').classList.toggle('hidden', !gif);
|
||||
const result = st.join || n === 1 ? (gif ? 'one animated GIF' : 'one video file') : n + (gif ? ' separate GIFs' : ' separate files');
|
||||
$('#exSummary').textContent = `${n} clip${n !== 1 ? 's' : ''} · ${fmtLen(total)} total → ${result}`;
|
||||
|
||||
let outHint = '';
|
||||
if (!st.join && n > 1 && st.output) {
|
||||
@@ -1319,14 +1334,16 @@ function updateExportView() {
|
||||
let encHint = '';
|
||||
const firstV = S.clips.map(c => srcById(c.src)).find(m => m && m.video);
|
||||
if (!copy && firstV) {
|
||||
const [w, hh] = outSize(firstV, +st.height);
|
||||
encHint = `Output: ${w}×${hh}, ${st.fps ? $('#exFPS').selectedOptions[0].textContent + ' fps' : fmtFps(firstV.video.fps)}`;
|
||||
const [w, hh] = outSize(firstV, gif ? +st.gifHeight : +st.height);
|
||||
const fps = gif ? st.gifFps + ' fps, no sound' : st.fps ? $('#exFPS').selectedOptions[0].textContent + ' fps' : fmtFps(firstV.video.fps);
|
||||
encHint = `Output: ${w}×${hh}, ${fps}`;
|
||||
if (st.join) {
|
||||
const shapes = new Set(S.clips.map(c => srcById(c.src)).filter(m => m.video).map(m => (m.video.displayWidth / m.video.displayHeight).toFixed(3)));
|
||||
if (shapes.size > 1) encHint += ' · clips with a different shape get black bars';
|
||||
}
|
||||
}
|
||||
$('#exEncHint').textContent = encHint;
|
||||
$('#exEncHint').textContent = gif ? '' : encHint;
|
||||
$('#exGifHint').textContent = gif ? encHint : '';
|
||||
|
||||
const warns = [];
|
||||
if (copy) {
|
||||
@@ -1346,6 +1363,11 @@ function updateExportView() {
|
||||
}
|
||||
}
|
||||
if (S.clips.some(c => S.kf[c.src] && S.kf[c.src].status === 'pending')) warns.push('Still reading keyframe positions…');
|
||||
} else if (gif) {
|
||||
const longest = st.join ? total : Math.max(...S.clips.map(c => c.out - c.in));
|
||||
if (longest > 30) {
|
||||
warns.push(`${st.join || n === 1 ? 'The GIF' : 'The longest GIF'} will be ${fmtLen(longest)} long. GIF files get large quickly; a lower resolution or frame rate keeps them smaller.`);
|
||||
}
|
||||
} else if (!S.info.codecsReady) {
|
||||
warns.push('Checking which encoders (including GPU encoders) are available…');
|
||||
}
|
||||
@@ -1391,12 +1413,15 @@ async function startExport(overwrite = false) {
|
||||
const st = S.settings;
|
||||
st.output = $('#exOutput').value.trim();
|
||||
if (!st.output) return toast('Choose where to save the export.', 'error');
|
||||
const gif = st.mode === 'gif';
|
||||
const body = {
|
||||
clips: S.clips.map(c => ({ src: c.src, in: c.in, out: c.out })),
|
||||
mode: st.mode, join: st.join, output: st.output, overwrite,
|
||||
allAudio: st.allAudio, noAudio: st.mode === 'copy' ? st.noAudio : st.acodec === 'none',
|
||||
vcodec: st.vcodec, quality: +st.quality, speed: st.speed, height: +st.height, fps: st.fps,
|
||||
vcodec: st.vcodec, quality: +st.quality, speed: st.speed,
|
||||
height: gif ? +st.gifHeight : +st.height, fps: gif ? st.gifFps : st.fps,
|
||||
acodec: st.acodec, abitrate: +st.abitrate,
|
||||
dither: st.gifDither, loop: st.gifLoop,
|
||||
};
|
||||
saveSoon();
|
||||
try {
|
||||
@@ -1466,6 +1491,10 @@ function bindExport() {
|
||||
$('#exFPS').addEventListener('change', e => { st().fps = e.target.value; refill(); });
|
||||
$('#exACodec').addEventListener('change', e => { st().acodec = e.target.value; refill(); });
|
||||
$('#exABitrate').addEventListener('change', e => { st().abitrate = +e.target.value; refill(); });
|
||||
$('#exGifHeight').addEventListener('change', e => { st().gifHeight = +e.target.value; refill(); });
|
||||
$('#exGifFPS').addEventListener('change', e => { st().gifFps = e.target.value; refill(); });
|
||||
$('#exGifDither').addEventListener('change', e => { st().gifDither = e.target.value; refill(); });
|
||||
$('#exGifLoop').addEventListener('change', e => { st().gifLoop = e.target.value === '1'; refill(); });
|
||||
$('#exGo').addEventListener('click', () => startExport(false));
|
||||
$('#exCancel').addEventListener('click', () => {
|
||||
if (S.exportJob) api('/api/job/cancel', { id: S.exportJob.id }).catch(() => {});
|
||||
@@ -1480,7 +1509,7 @@ function setMode(mode) {
|
||||
const st = S.settings;
|
||||
if (st.mode === mode) return;
|
||||
st.mode = mode;
|
||||
const exts = mode === 'copy' ? copyContainerChoices() : S.info.containers.map(c => c.ext);
|
||||
const exts = exportExts(mode);
|
||||
if (st.output && !exts.includes(extOf(st.output))) st.output = replaceExt(st.output, defaultExt());
|
||||
if (dlgEx.open) fillExport();
|
||||
changed();
|
||||
@@ -1519,7 +1548,10 @@ async function fbLoad(path) {
|
||||
Object.assign(fb, { path: r.path, entries: r.entries, parent: r.parent });
|
||||
fb.sel.clear();
|
||||
$('#fbPath').value = r.path;
|
||||
const root = r.roots.find(x => r.path.toUpperCase().startsWith(x.toUpperCase())) || r.roots[0];
|
||||
// The deepest root containing the folder (on Linux, / contains the mounted drives too).
|
||||
const P = r.path.toUpperCase(), sep = S.info.sep || '/';
|
||||
const inRoot = x => { const X = x.toUpperCase(); return P === X || P.startsWith(X.replace(/[\\/]$/, '') + sep); };
|
||||
const root = r.roots.filter(inRoot).sort((a, b) => b.length - a.length)[0] || r.roots[0];
|
||||
setOptions($('#fbRoots'), r.roots.map(x => [x, x]), root);
|
||||
$('#fbUp').disabled = !r.parent;
|
||||
fbRender();
|
||||
@@ -1861,10 +1893,11 @@ async function init() {
|
||||
const win = S.info.os === 'windows';
|
||||
const b = $('#banner');
|
||||
b.replaceChildren(h('b', {}, 'ffmpeg was not found. '),
|
||||
win ? 'Download a Windows build (for example "ffmpeg-release-essentials.zip" from ' : 'Install ffmpeg (for example from ',
|
||||
h('a', { href: 'https://www.gyan.dev/ffmpeg/builds/', target: '_blank', rel: 'noopener' }, 'gyan.dev/ffmpeg/builds'),
|
||||
win ? 'Download a Windows build (for example "ffmpeg-release-essentials.zip" from ' : 'Install ffmpeg with your package manager (or put ffmpeg and ffprobe from ',
|
||||
win ? h('a', { href: 'https://www.gyan.dev/ffmpeg/builds/', target: '_blank', rel: 'noopener' }, 'gyan.dev/ffmpeg/builds')
|
||||
: h('a', { href: 'https://ffmpeg.org/download.html', target: '_blank', rel: 'noopener' }, 'ffmpeg.org/download.html'),
|
||||
win ? '), copy ffmpeg.exe and ffprobe.exe from its bin folder next to vibedit.exe, then restart VibEdit.'
|
||||
: ' or your package manager) and restart VibEdit.');
|
||||
: ' next to vibedit) and restart VibEdit.');
|
||||
b.classList.remove('hidden');
|
||||
}
|
||||
$('#btnBrowse').classList.toggle('hidden', !S.info.nativeDialogs);
|
||||
|
||||
Reference in New Issue
Block a user