mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-22 02:12:30 +01:00
parent
c495012db2
commit
818a323e7e
@ -59,6 +59,7 @@ The main feature is lossless trimming and cutting of video and audio files, whic
|
|||||||
- Speed up / slow down video or audio file ([changing FPS](https://github.com/mifi/lossless-cut/issues/1712))
|
- Speed up / slow down video or audio file ([changing FPS](https://github.com/mifi/lossless-cut/issues/1712))
|
||||||
- Basic [CLI](cli.md) and [HTTP API](api.md)
|
- Basic [CLI](cli.md) and [HTTP API](api.md)
|
||||||
- Show (DJI) embedded GPS track on a map
|
- Show (DJI) embedded GPS track on a map
|
||||||
|
- Losslessly Download videos over HTTP (e.g. HLS `.m3u8`)
|
||||||
|
|
||||||
## Example lossless use cases
|
## Example lossless use cases
|
||||||
|
|
||||||
|
@ -11,8 +11,8 @@
|
|||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"dev": "electron-vite dev -w",
|
"dev": "electron-vite dev -w",
|
||||||
"icon-gen": "mkdirp icon-build build-resources/appx && tsx script/icon-gen.mts",
|
"icon-gen": "mkdirp icon-build build-resources/appx && tsx script/icon-gen.mts",
|
||||||
"download-ffmpeg-darwin-x64": "mkdirp ffmpeg/darwin-x64 && cd ffmpeg/darwin-x64 && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffmpeg-macos-X64 -O ffmpeg && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffprobe-macos-X64 -O ffprobe && chmod +x ffmpeg && chmod +x ffprobe",
|
"download-ffmpeg-darwin-x64": "mkdirp ffmpeg/darwin-x64 && cd ffmpeg/darwin-x64 && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-5/ffmpeg-macos-X64 -O ffmpeg && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-5/ffprobe-macos-X64 -O ffprobe && chmod +x ffmpeg && chmod +x ffprobe",
|
||||||
"download-ffmpeg-darwin-arm64": "mkdirp ffmpeg/darwin-arm64 && cd ffmpeg/darwin-arm64 && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffmpeg-macos-ARM64 -O ffmpeg && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-1/ffprobe-macos-ARM64 -O ffprobe && chmod +x ffmpeg && chmod +x ffprobe",
|
"download-ffmpeg-darwin-arm64": "mkdirp ffmpeg/darwin-arm64 && cd ffmpeg/darwin-arm64 && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-5/ffmpeg-macos-ARM64 -O ffmpeg && wget https://github.com/mifi/ffmpeg-build-script/releases/download/6.0-5/ffprobe-macos-ARM64 -O ffprobe && chmod +x ffmpeg && chmod +x ffprobe",
|
||||||
"download-ffmpeg-linux-x64": "mkdirp ffmpeg/linux-x64 && cd ffmpeg/linux-x64 && wget https://github.com/mifi/ffmpeg-builds/releases/download/6.0/ffmpeg-n6.0-12-ga6dc92968a-linux64-gpl-shared-6.0.tar.xz -O ffmpeg-ffprobe.xz && tar -xv -f ffmpeg-ffprobe.xz && mv ffmpeg-n6.0-12-ga6dc92968a-linux64-gpl-shared-6.0 extracted && mkdirp lib && mv extracted/bin/ffmpeg extracted/bin/ffprobe extracted/lib/lib*.so* lib",
|
"download-ffmpeg-linux-x64": "mkdirp ffmpeg/linux-x64 && cd ffmpeg/linux-x64 && wget https://github.com/mifi/ffmpeg-builds/releases/download/6.0/ffmpeg-n6.0-12-ga6dc92968a-linux64-gpl-shared-6.0.tar.xz -O ffmpeg-ffprobe.xz && tar -xv -f ffmpeg-ffprobe.xz && mv ffmpeg-n6.0-12-ga6dc92968a-linux64-gpl-shared-6.0 extracted && mkdirp lib && mv extracted/bin/ffmpeg extracted/bin/ffprobe extracted/lib/lib*.so* lib",
|
||||||
"download-ffmpeg-win32-x64": "mkdirp ffmpeg/win32-x64 && cd ffmpeg/win32-x64 && npx download-cli https://github.com/mifi/ffmpeg-builds/releases/download/6.0/ffmpeg-n6.0-12-ga6dc92968a-win64-gpl-shared-6.0.zip --out . --filename ffmpeg-ffprobe.zip && 7z x ffmpeg-ffprobe.zip && mkdirp lib && cd ffmpeg-n6.0-12-ga6dc92968a-win64-gpl-shared-6.0/bin && npx shx mv ffmpeg.exe ffprobe.exe *.dll ../../lib",
|
"download-ffmpeg-win32-x64": "mkdirp ffmpeg/win32-x64 && cd ffmpeg/win32-x64 && npx download-cli https://github.com/mifi/ffmpeg-builds/releases/download/6.0/ffmpeg-n6.0-12-ga6dc92968a-win64-gpl-shared-6.0.zip --out . --filename ffmpeg-ffprobe.zip && 7z x ffmpeg-ffprobe.zip && mkdirp lib && cd ffmpeg-n6.0-12-ga6dc92968a-win64-gpl-shared-6.0/bin && npx shx mv ffmpeg.exe ffprobe.exe *.dll ../../lib",
|
||||||
"build": "yarn icon-gen && electron-vite build",
|
"build": "yarn icon-gen && electron-vite build",
|
||||||
|
@ -664,5 +664,19 @@ export function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIn
|
|||||||
return execa(getFfmpegPath(), args, { encoding: null, buffer: false, stderr: enableLog ? 'inherit' : 'pipe' });
|
return execa(getFfmpegPath(), args, { encoding: null, buffer: false, stderr: enableLog ? 'inherit' : 'pipe' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function downloadMediaUrl(url: string, outPath: string) {
|
||||||
|
// User agent taken from https://techblog.willshouse.com/2012/01/03/most-common-user-agents/
|
||||||
|
const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36';
|
||||||
|
const args = [
|
||||||
|
'-hide_banner', '-loglevel', 'error',
|
||||||
|
'-user_agent', userAgent,
|
||||||
|
'-i', url,
|
||||||
|
'-c', 'copy',
|
||||||
|
outPath,
|
||||||
|
];
|
||||||
|
|
||||||
|
await runFfmpegProcess(args);
|
||||||
|
}
|
||||||
|
|
||||||
// Don't pass complex objects over the bridge (process)
|
// Don't pass complex objects over the bridge (process)
|
||||||
export const runFfmpeg = async (...args: Parameters<typeof runFfmpegProcess>) => runFfmpegProcess(...args);
|
export const runFfmpeg = async (...args: Parameters<typeof runFfmpegProcess>) => runFfmpegProcess(...args);
|
||||||
|
@ -40,6 +40,8 @@ export { isLinux, isWindows, isMac, platform } from './util.js';
|
|||||||
|
|
||||||
export { pathToFileURL } from 'node:url';
|
export { pathToFileURL } from 'node:url';
|
||||||
|
|
||||||
|
export { downloadMediaUrl } from './ffmpeg.js';
|
||||||
|
|
||||||
|
|
||||||
const electronUnhandled = import('electron-unhandled');
|
const electronUnhandled = import('electron-unhandled');
|
||||||
|
|
||||||
|
@ -35,6 +35,13 @@ export default ({ app, mainWindow, newVersion, isStoreBuild }: {
|
|||||||
mainWindow.webContents.send('openDirDialog');
|
mainWindow.webContents.send('openDirDialog');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: esc(t('Open URL')),
|
||||||
|
async click() {
|
||||||
|
mainWindow.webContents.send('promptDownloadMediaUrl');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: esc(t('Close')),
|
label: esc(t('Close')),
|
||||||
accelerator: 'CmdOrCtrl+W',
|
accelerator: 'CmdOrCtrl+W',
|
||||||
|
@ -74,13 +74,14 @@ import {
|
|||||||
deleteFiles, isOutOfSpaceError, readFileSize, readFileSizes, checkFileSizes, setDocumentTitle, getOutFileExtension, getSuffixedFileName, mustDisallowVob, readVideoTs, readDirRecursively, getImportProjectType,
|
deleteFiles, isOutOfSpaceError, readFileSize, readFileSizes, checkFileSizes, setDocumentTitle, getOutFileExtension, getSuffixedFileName, mustDisallowVob, readVideoTs, readDirRecursively, getImportProjectType,
|
||||||
calcShouldShowWaveform, calcShouldShowKeyframes, mediaSourceQualities, getFrameDuration, isExecaError, getStdioString,
|
calcShouldShowWaveform, calcShouldShowKeyframes, mediaSourceQualities, getFrameDuration, isExecaError, getStdioString,
|
||||||
isMuxNotSupported,
|
isMuxNotSupported,
|
||||||
|
getDownloadMediaOutPath,
|
||||||
} from './util';
|
} from './util';
|
||||||
import { toast, errorToast } from './swal';
|
import { toast, errorToast } from './swal';
|
||||||
import { formatDuration, parseDuration } from './util/duration';
|
import { formatDuration, parseDuration } from './util/duration';
|
||||||
import { adjustRate } from './util/rate-calculator';
|
import { adjustRate } from './util/rate-calculator';
|
||||||
import { askExtractFramesAsImages } from './dialogs/extractFrames';
|
import { askExtractFramesAsImages } from './dialogs/extractFrames';
|
||||||
import { askForHtml5ifySpeed } from './dialogs/html5ify';
|
import { askForHtml5ifySpeed } from './dialogs/html5ify';
|
||||||
import { askForOutDir, askForImportChapters, promptTimecode, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showExportFailedDialog, showConcatFailedDialog, openYouTubeChaptersDialog, showRefuseToOverwrite, openDirToast, openExportFinishedToast, openConcatFinishedToast, showOpenDialog, showMuxNotSupported } from './dialogs';
|
import { askForOutDir, askForImportChapters, promptTimecode, askForFileOpenAction, confirmExtractAllStreamsDialog, showCleanupFilesDialog, showDiskFull, showExportFailedDialog, showConcatFailedDialog, openYouTubeChaptersDialog, showRefuseToOverwrite, openDirToast, openExportFinishedToast, openConcatFinishedToast, showOpenDialog, showMuxNotSupported, promptDownloadMediaUrl } from './dialogs';
|
||||||
import { openSendReportDialog } from './reporting';
|
import { openSendReportDialog } from './reporting';
|
||||||
import { fallbackLng } from './i18n';
|
import { fallbackLng } from './i18n';
|
||||||
import { createSegment, getCleanCutSegments, findSegmentsAtCursor, sortSegments, convertSegmentsToChapters, hasAnySegmentOverlap, isDurationValid, playOnlyCurrentSegment, getSegmentTags } from './segments';
|
import { createSegment, getCleanCutSegments, findSegmentsAtCursor, sortSegments, convertSegmentsToChapters, hasAnySegmentOverlap, isDurationValid, playOnlyCurrentSegment, getSegmentTags } from './segments';
|
||||||
@ -178,7 +179,7 @@ function App() {
|
|||||||
|
|
||||||
// Store "working" in a ref so we can avoid race conditions
|
// Store "working" in a ref so we can avoid race conditions
|
||||||
const workingRef = useRef(!!working);
|
const workingRef = useRef(!!working);
|
||||||
const setWorking = useCallback((valOrBool: { text: string, abortController?: AbortController } | true | undefined) => {
|
const setWorking = useCallback((valOrBool?: { text: string, abortController?: AbortController } | true | undefined) => {
|
||||||
workingRef.current = !!valOrBool;
|
workingRef.current = !!valOrBool;
|
||||||
const val = valOrBool === true ? { text: t('Loading') } : valOrBool;
|
const val = valOrBool === true ? { text: t('Loading') } : valOrBool;
|
||||||
setWorkingState(val ? { text: val.text, abortController: val.abortController } : undefined);
|
setWorkingState(val ? { text: val.text, abortController: val.abortController } : undefined);
|
||||||
@ -2149,6 +2150,23 @@ function App() {
|
|||||||
onEditSegmentTags(currentSegIndexSafe);
|
onEditSegmentTags(currentSegIndexSafe);
|
||||||
}, [currentSegIndexSafe, onEditSegmentTags]);
|
}, [currentSegIndexSafe, onEditSegmentTags]);
|
||||||
|
|
||||||
|
const promptDownloadMediaUrlWrapper = useCallback(async () => {
|
||||||
|
if (customOutDir == null) {
|
||||||
|
errorToast(i18n.t('Please select a working directory first'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const outPath = getDownloadMediaOutPath(customOutDir, `downloaded-media-${Date.now()}.mkv`);
|
||||||
|
try {
|
||||||
|
setWorking(true);
|
||||||
|
const downloaded = await promptDownloadMediaUrl(outPath);
|
||||||
|
if (downloaded) await loadMedia({ filePath: outPath });
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err);
|
||||||
|
} finally {
|
||||||
|
setWorking();
|
||||||
|
}
|
||||||
|
}, [customOutDir, loadMedia, setWorking]);
|
||||||
|
|
||||||
type MainKeyboardAction = Exclude<KeyboardAction, 'closeActiveScreen' | 'toggleKeyboardShortcuts' | 'goToTimecodeDirect'>;
|
type MainKeyboardAction = Exclude<KeyboardAction, 'closeActiveScreen' | 'toggleKeyboardShortcuts' | 'goToTimecodeDirect'>;
|
||||||
|
|
||||||
const mainActions = useMemo(() => {
|
const mainActions = useMemo(() => {
|
||||||
@ -2486,6 +2504,7 @@ function App() {
|
|||||||
// todo separate actions per type and move them into mainActions? https://github.com/mifi/lossless-cut/issues/254#issuecomment-932649424
|
// todo separate actions per type and move them into mainActions? https://github.com/mifi/lossless-cut/issues/254#issuecomment-932649424
|
||||||
importEdlFile,
|
importEdlFile,
|
||||||
exportEdlFile: tryExportEdlFile,
|
exportEdlFile: tryExportEdlFile,
|
||||||
|
promptDownloadMediaUrl: promptDownloadMediaUrlWrapper,
|
||||||
}).map(([key, fn]) => [
|
}).map(([key, fn]) => [
|
||||||
key,
|
key,
|
||||||
async (...args: unknown[]) => {
|
async (...args: unknown[]) => {
|
||||||
@ -2544,7 +2563,7 @@ function App() {
|
|||||||
ipcActions.forEach(([key, action]) => electron.ipcRenderer.off(key, action));
|
ipcActions.forEach(([key, action]) => electron.ipcRenderer.off(key, action));
|
||||||
electron.ipcRenderer.off('apiAction', tryApiAction);
|
electron.ipcRenderer.off('apiAction', tryApiAction);
|
||||||
};
|
};
|
||||||
}, [checkFileOpened, customOutDir, detectedFps, filePath, getFrameCount, getKeyboardAction, goToTimecodeDirect, loadCutSegments, mainActions, selectedSegments, toggleKeyboardShortcuts, userOpenFiles]);
|
}, [checkFileOpened, customOutDir, detectedFps, filePath, getFrameCount, getKeyboardAction, goToTimecodeDirect, loadCutSegments, mainActions, promptDownloadMediaUrlWrapper, selectedSegments, toggleKeyboardShortcuts, userOpenFiles]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function onDrop(ev: DragEvent) {
|
async function onDrop(ev: DragEvent) {
|
||||||
|
@ -15,7 +15,10 @@ import Checkbox from '../components/Checkbox';
|
|||||||
import { isWindows, showItemInFolder } from '../util';
|
import { isWindows, showItemInFolder } from '../util';
|
||||||
import { ParseTimecode, SegmentBase } from '../types';
|
import { ParseTimecode, SegmentBase } from '../types';
|
||||||
|
|
||||||
const { dialog, shell } = window.require('@electron/remote');
|
const remote = window.require('@electron/remote');
|
||||||
|
const { dialog, shell } = remote;
|
||||||
|
|
||||||
|
const { downloadMediaUrl } = remote.require('./index.js');
|
||||||
|
|
||||||
|
|
||||||
export async function promptTimecode({ initialValue, title, text, inputPlaceholder, parseTimecode, allowRelative = false }: {
|
export async function promptTimecode({ initialValue, title, text, inputPlaceholder, parseTimecode, allowRelative = false }: {
|
||||||
@ -710,3 +713,18 @@ export async function askForPlaybackRate({ detectedFps, outputPlaybackRate }) {
|
|||||||
|
|
||||||
return parseValue(value);
|
return parseValue(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function promptDownloadMediaUrl(outPath: string) {
|
||||||
|
const { value } = await Swal.fire<string>({
|
||||||
|
title: i18n.t('Open media from URL'),
|
||||||
|
input: 'text',
|
||||||
|
inputPlaceholder: 'https://example.com/video.m3u8',
|
||||||
|
text: i18n.t('Losslessly download a whole media file from the specified URL, mux it into an mkv file and open it in LosslessCut. This can be useful if you need to download a video from a website, e.g. a HLS streaming video. For example in Chrome you can open Developer Tools and view the network traffic, find the playlist (e.g. m3u8) and copy paste its URL here.'),
|
||||||
|
showCancelButton: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!value) return false;
|
||||||
|
|
||||||
|
await downloadMediaUrl(value, outPath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
@ -52,6 +52,8 @@ export function getOutPath({ customOutDir, filePath, fileName }: { customOutDir?
|
|||||||
return join(getOutDir(customOutDir, filePath), fileName);
|
return join(getOutDir(customOutDir, filePath), fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getDownloadMediaOutPath = (customOutDir: string, fileName: string) => join(customOutDir, fileName);
|
||||||
|
|
||||||
export const getSuffixedFileName = (filePath: string | undefined, nameSuffix: string) => `${getFileBaseName(filePath)}-${nameSuffix}`;
|
export const getSuffixedFileName = (filePath: string | undefined, nameSuffix: string) => `${getFileBaseName(filePath)}-${nameSuffix}`;
|
||||||
|
|
||||||
export function getSuffixedOutPath<T extends string | undefined>(a: { customOutDir?: string | undefined, filePath?: T | undefined, nameSuffix: string }): T extends string ? string : undefined;
|
export function getSuffixedOutPath<T extends string | undefined>(a: { customOutDir?: string | undefined, filePath?: T | undefined, nameSuffix: string }): T extends string ? string : undefined;
|
||||||
|
Loading…
Reference in New Issue
Block a user