pull/1899/head
Mikael Finstad 2024-02-13 00:01:43 +08:00
rodzic a61c83956d
commit 37af026932
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 25AB36E3E81CBC26
3 zmienionych plików z 38 dodań i 23 usunięć

Wyświetl plik

@ -54,7 +54,7 @@ export async function readFrames({ filePath, from, to, streamIndex }) {
const packetsFiltered = JSON.parse(stdout).packets
.map(p => ({
keyframe: p.flags[0] === 'K',
time: parseFloat(p.pts_time, 10),
time: parseFloat(p.pts_time),
createdAt: new Date(),
}))
.filter(p => !Number.isNaN(p.time));
@ -299,9 +299,7 @@ export async function readFileMeta(filePath) {
} catch (err) {
// Windows will throw error with code ENOENT if format detection fails.
if (isExecaFailure(err)) {
const err2 = new Error(`Unsupported file: ${err.message}`);
err2.code = 'LLC_FFPROBE_UNSUPPORTED_FILE';
throw err2;
throw Object.assign(new Error(`Unsupported file: ${err.message}`), { code: 'LLC_FFPROBE_UNSUPPORTED_FILE' });
}
throw err;
}
@ -340,7 +338,10 @@ function getPreferredCodecFormat(stream) {
return undefined;
}
async function extractNonAttachmentStreams({ customOutDir, filePath, streams, enableOverwriteOutput }) {
async function extractNonAttachmentStreams({ customOutDir, filePath, streams, enableOverwriteOutput }: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
customOutDir?: string, filePath: string, streams: any[], enableOverwriteOutput?: boolean,
}) {
if (streams.length === 0) return [];
const outStreams = streams.map((s) => ({
@ -354,7 +355,7 @@ async function extractNonAttachmentStreams({ customOutDir, filePath, streams, en
// console.log(outStreams);
let streamArgs = [];
let streamArgs: string[] = [];
const outPaths = await pMap(outStreams, async ({ index, codec, type, format: { format, ext } }) => {
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` });
if (!enableOverwriteOutput && await pathExists(outPath)) throw new RefuseOverwriteError();
@ -379,12 +380,15 @@ async function extractNonAttachmentStreams({ customOutDir, filePath, streams, en
return outPaths;
}
async function extractAttachmentStreams({ customOutDir, filePath, streams, enableOverwriteOutput }) {
async function extractAttachmentStreams({ customOutDir, filePath, streams, enableOverwriteOutput }: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
customOutDir?: string, filePath: string, streams: any[], enableOverwriteOutput?: boolean,
}) {
if (streams.length === 0) return [];
console.log('Extracting', streams.length, 'attachment streams');
let streamArgs = [];
let streamArgs: string[] = [];
const outPaths = await pMap(streams, async ({ index, codec_name: codec, codec_type: type }) => {
const ext = codec || 'bin';
const outPath = getSuffixedOutPath({ customOutDir, filePath, nameSuffix: `stream-${index}-${type}-${codec}.${ext}` });
@ -411,7 +415,7 @@ async function extractAttachmentStreams({ customOutDir, filePath, streams, enabl
} catch (err) {
// Unfortunately ffmpeg will exit with code 1 even though it's a success
// Note: This is kind of hacky:
if (err.exitCode === 1 && typeof err.stderr === 'string' && err.stderr.includes('At least one output file must be specified')) return outPaths;
if (err instanceof Error && 'exitCode' in err && 'stderr' in err && err.exitCode === 1 && typeof err.stderr === 'string' && err.stderr.includes('At least one output file must be specified')) return outPaths;
throw err;
}
return outPaths;
@ -474,7 +478,7 @@ export async function renderThumbnails({ filePath, from, duration, onThumbnail }
const numThumbs = Math.floor(Math.min(Math.max(3 / (endTime - startTime), 3), 10));
// console.log(numThumbs);
const thumbTimes = Array(numThumbs - 1).fill().map((unused, i) => (from + ((duration * (i + 1)) / (numThumbs))));
const thumbTimes = Array(numThumbs - 1).fill(undefined).map((_unused, i) => (from + ((duration * (i + 1)) / (numThumbs))));
// console.log(thumbTimes);
await pMap(thumbTimes, async (time) => {
@ -487,12 +491,12 @@ export async function extractWaveform({ filePath, outPath }) {
const numSegs = 10;
const duration = 60 * 60;
const maxLen = 0.1;
const segments = Array(numSegs).fill().map((unused, i) => [i * (duration / numSegs), Math.min(duration / numSegs, maxLen)]);
const segments = Array(numSegs).fill(undefined).map((_unused, i) => [i * (duration / numSegs), Math.min(duration / numSegs, maxLen)] as const);
// https://superuser.com/questions/681885/how-can-i-remove-multiple-segments-from-a-video-using-ffmpeg
let filter = segments.map(([from, len], i) => `[0:a]atrim=start=${from}:end=${from + len},asetpts=PTS-STARTPTS[a${i}]`).join(';');
filter += ';';
filter += segments.map((arr, i) => `[a${i}]`).join('');
filter += segments.map((_arr, i) => `[a${i}]`).join('');
filter += `concat=n=${segments.length}:v=0:a=1[out]`;
console.time('ffmpeg');
@ -592,18 +596,20 @@ export async function runFfmpegStartupCheck() {
}
// https://superuser.com/questions/543589/information-about-ffmpeg-command-line-options
export const getExperimentalArgs = (ffmpegExperimental) => (ffmpegExperimental ? ['-strict', 'experimental'] : []);
export const getExperimentalArgs = (ffmpegExperimental: boolean) => (ffmpegExperimental ? ['-strict', 'experimental'] : []);
export const getVideoTimescaleArgs = (videoTimebase) => (videoTimebase != null ? ['-video_track_timescale', videoTimebase] : []);
export const getVideoTimescaleArgs = (videoTimebase: number) => (videoTimebase != null ? ['-video_track_timescale', String(videoTimebase)] : []);
// inspired by https://gist.github.com/fernandoherreradelasheras/5eca67f4200f1a7cc8281747da08496e
export async function cutEncodeSmartPart({ filePath, cutFrom, cutTo, outPath, outFormat, videoCodec, videoBitrate, videoTimebase, allFilesMeta, copyFileStreams, videoStreamIndex, ffmpegExperimental }) {
function getVideoArgs({ streamIndex, outputIndex }) {
export async function cutEncodeSmartPart({ filePath, cutFrom, cutTo, outPath, outFormat, videoCodec, videoBitrate, videoTimebase, allFilesMeta, copyFileStreams, videoStreamIndex, ffmpegExperimental }: {
filePath: string, cutFrom: number, cutTo: number, outPath: string, outFormat: string, videoCodec: string, videoBitrate: number, videoTimebase: number, allFilesMeta, copyFileStreams, videoStreamIndex: number, ffmpegExperimental: boolean,
}) {
function getVideoArgs({ streamIndex, outputIndex }: { streamIndex: number, outputIndex: number }) {
if (streamIndex !== videoStreamIndex) return undefined;
const args = [
`-c:${outputIndex}`, videoCodec,
`-b:${outputIndex}`, videoBitrate,
`-b:${outputIndex}`, String(videoBitrate),
];
// seems like ffmpeg handles this itself well when encoding same source file

Wyświetl plik

@ -5,15 +5,20 @@ import { readKeyframesAroundTime, findNextKeyframe, findKeyframeAtExactTime } fr
const { stat } = window.require('fs-extra');
const mapVideoCodec = (codec: string) => codec;
// const mapVideoCodec = (codec: string) => ({ av1: 'libsvtav1' }[codec] ?? codec);
// eslint-disable-next-line import/prefer-default-export
export async function getSmartCutParams({ path, videoDuration, desiredCutFrom, streams }) {
export async function getSmartCutParams({ path, videoDuration, desiredCutFrom, streams }: {
path: string, videoDuration: number, desiredCutFrom: number, streams,
}) {
const videoStreams = getRealVideoStreams(streams);
if (videoStreams.length === 0) throw new Error('Smart cut only works on videos');
if (videoStreams.length > 1) throw new Error('Can only smart cut video with exactly one video stream');
const videoStream = videoStreams[0];
const readKeyframes = async (window) => readKeyframesAroundTime({ filePath: path, streamIndex: videoStream.index, aroundTime: desiredCutFrom, window });
const readKeyframes = async (window: number) => readKeyframesAroundTime({ filePath: path, streamIndex: videoStream.index, aroundTime: desiredCutFrom, window });
let keyframes = await readKeyframes(10);
@ -50,8 +55,11 @@ export async function getSmartCutParams({ path, videoDuration, desiredCutFrom, s
// see discussion https://github.com/mifi/lossless-cut/issues/126#issuecomment-1602266688
videoBitrate = Math.floor(videoBitrate * 1.2);
const { codec_name: videoCodec } = videoStream;
if (videoCodec == null) throw new Error('Unable to determine codec for smart cut');
const { codec_name: detectedVideoCodec } = videoStream;
if (detectedVideoCodec == null) throw new Error('Unable to determine codec for smart cut');
const videoCodec = mapVideoCodec(detectedVideoCodec);
console.log({ detectedVideoCodec, videoCodec });
const timebase = getVideoTimebase(videoStream);
if (timebase == null) console.warn('Unable to determine timebase', videoStream.time_base);

Wyświetl plik

@ -4,6 +4,7 @@ import ky from 'ky';
import prettyBytes from 'pretty-bytes';
import sortBy from 'lodash/sortBy';
import pRetry from 'p-retry';
import { ExecaError } from 'execa';
import isDev from './isDev';
import Swal, { toast } from './swal';
@ -298,10 +299,10 @@ export const mirrorTransform = 'matrix(-1, 0, 0, 1, 0, 0)';
// I *think* Windows will throw error with code ENOENT if ffprobe/ffmpeg fails (execa), but other OS'es will return this error code if a file is not found, so it would be wrong to attribute it to exec failure.
// see https://github.com/mifi/lossless-cut/issues/451
export const isExecaFailure = (err) => err.exitCode === 1 || (isWindows && err.code === 'ENOENT');
export const isExecaFailure = (err): err is ExecaError => err.exitCode === 1 || (isWindows && err.code === 'ENOENT');
// A bit hacky but it works, unless someone has a file called "No space left on device" ( ͡° ͜ʖ ͡°)
export const isOutOfSpaceError = (err) => (
export const isOutOfSpaceError = (err): err is ExecaError => (
err && isExecaFailure(err)
&& typeof err.stderr === 'string' && err.stderr.includes('No space left on device')
);