mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-21 18:02:35 +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))
|
||||
- Basic [CLI](cli.md) and [HTTP API](api.md)
|
||||
- Show (DJI) embedded GPS track on a map
|
||||
- Losslessly Download videos over HTTP (e.g. HLS `.m3u8`)
|
||||
|
||||
## Example lossless use cases
|
||||
|
||||
|
@ -11,8 +11,8 @@
|
||||
"start": "electron-vite preview",
|
||||
"dev": "electron-vite dev -w",
|
||||
"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-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-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-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-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",
|
||||
|
@ -664,5 +664,19 @@ export function createMediaSourceProcess({ path, videoStreamIndex, audioStreamIn
|
||||
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)
|
||||
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 { downloadMediaUrl } from './ffmpeg.js';
|
||||
|
||||
|
||||
const electronUnhandled = import('electron-unhandled');
|
||||
|
||||
|
@ -35,6 +35,13 @@ export default ({ app, mainWindow, newVersion, isStoreBuild }: {
|
||||
mainWindow.webContents.send('openDirDialog');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: esc(t('Open URL')),
|
||||
async click() {
|
||||
mainWindow.webContents.send('promptDownloadMediaUrl');
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: esc(t('Close')),
|
||||
accelerator: 'CmdOrCtrl+W',
|
||||
|
@ -74,13 +74,14 @@ import {
|
||||
deleteFiles, isOutOfSpaceError, readFileSize, readFileSizes, checkFileSizes, setDocumentTitle, getOutFileExtension, getSuffixedFileName, mustDisallowVob, readVideoTs, readDirRecursively, getImportProjectType,
|
||||
calcShouldShowWaveform, calcShouldShowKeyframes, mediaSourceQualities, getFrameDuration, isExecaError, getStdioString,
|
||||
isMuxNotSupported,
|
||||
getDownloadMediaOutPath,
|
||||
} from './util';
|
||||
import { toast, errorToast } from './swal';
|
||||
import { formatDuration, parseDuration } from './util/duration';
|
||||
import { adjustRate } from './util/rate-calculator';
|
||||
import { askExtractFramesAsImages } from './dialogs/extractFrames';
|
||||
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 { fallbackLng } from './i18n';
|
||||
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
|
||||
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;
|
||||
const val = valOrBool === true ? { text: t('Loading') } : valOrBool;
|
||||
setWorkingState(val ? { text: val.text, abortController: val.abortController } : undefined);
|
||||
@ -2149,6 +2150,23 @@ function App() {
|
||||
onEditSegmentTags(currentSegIndexSafe);
|
||||
}, [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'>;
|
||||
|
||||
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
|
||||
importEdlFile,
|
||||
exportEdlFile: tryExportEdlFile,
|
||||
promptDownloadMediaUrl: promptDownloadMediaUrlWrapper,
|
||||
}).map(([key, fn]) => [
|
||||
key,
|
||||
async (...args: unknown[]) => {
|
||||
@ -2544,7 +2563,7 @@ function App() {
|
||||
ipcActions.forEach(([key, action]) => electron.ipcRenderer.off(key, action));
|
||||
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(() => {
|
||||
async function onDrop(ev: DragEvent) {
|
||||
|
@ -15,7 +15,10 @@ import Checkbox from '../components/Checkbox';
|
||||
import { isWindows, showItemInFolder } from '../util';
|
||||
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 }: {
|
||||
@ -710,3 +713,18 @@ export async function askForPlaybackRate({ detectedFps, outputPlaybackRate }) {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export const getDownloadMediaOutPath = (customOutDir: string, fileName: string) => join(customOutDir, fileName);
|
||||
|
||||
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;
|
||||
|
Loading…
Reference in New Issue
Block a user