2022-06-29 23:22:14 +00:00
|
|
|
import { getRealVideoStreams, getVideoTimebase } from './util/streams';
|
2022-02-28 07:07:51 +00:00
|
|
|
|
2023-02-05 11:08:28 +00:00
|
|
|
import { readKeyframesAroundTime, findNextKeyframe, findKeyframeAtExactTime } from './ffmpeg';
|
2024-03-20 08:02:34 +00:00
|
|
|
import { FFprobeStream } from '../ffprobe';
|
2022-02-28 07:07:51 +00:00
|
|
|
|
|
|
|
const { stat } = window.require('fs-extra');
|
|
|
|
|
2023-02-08 08:16:07 +00:00
|
|
|
|
2024-02-14 16:02:28 +00:00
|
|
|
const mapVideoCodec = (codec: string) => ({ av1: 'libsvtav1' }[codec] ?? codec);
|
2024-02-12 16:01:43 +00:00
|
|
|
|
2022-02-28 07:07:51 +00:00
|
|
|
// eslint-disable-next-line import/prefer-default-export
|
2024-02-12 16:01:43 +00:00
|
|
|
export async function getSmartCutParams({ path, videoDuration, desiredCutFrom, streams }: {
|
2024-03-20 08:02:34 +00:00
|
|
|
path: string, videoDuration: number, desiredCutFrom: number, streams: FFprobeStream[],
|
2024-02-12 16:01:43 +00:00
|
|
|
}) {
|
2022-02-28 07:07:51 +00:00
|
|
|
const videoStreams = getRealVideoStreams(streams);
|
|
|
|
if (videoStreams.length > 1) throw new Error('Can only smart cut video with exactly one video stream');
|
|
|
|
|
2024-03-20 08:02:34 +00:00
|
|
|
const [videoStream] = videoStreams;
|
|
|
|
|
|
|
|
if (videoStream == null) throw new Error('Smart cut only works on videos');
|
2022-02-28 07:07:51 +00:00
|
|
|
|
2024-02-12 16:01:43 +00:00
|
|
|
const readKeyframes = async (window: number) => readKeyframesAroundTime({ filePath: path, streamIndex: videoStream.index, aroundTime: desiredCutFrom, window });
|
2022-02-28 07:07:51 +00:00
|
|
|
|
|
|
|
let keyframes = await readKeyframes(10);
|
|
|
|
|
2023-02-05 11:08:28 +00:00
|
|
|
const keyframeAtExactTime = findKeyframeAtExactTime(keyframes, desiredCutFrom);
|
2022-02-28 07:07:51 +00:00
|
|
|
if (keyframeAtExactTime) {
|
|
|
|
console.log('Start cut is already on exact keyframe', keyframeAtExactTime.time);
|
|
|
|
|
|
|
|
return {
|
2024-02-14 13:12:16 +00:00
|
|
|
losslessCutFrom: keyframeAtExactTime.time,
|
2022-02-28 07:07:51 +00:00
|
|
|
videoStreamIndex: videoStream.index,
|
2023-02-19 06:15:35 +00:00
|
|
|
segmentNeedsSmartCut: false,
|
2022-02-28 07:07:51 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-02-05 11:08:28 +00:00
|
|
|
let nextKeyframe = findNextKeyframe(keyframes, desiredCutFrom);
|
|
|
|
|
|
|
|
if (nextKeyframe == null) {
|
|
|
|
// try again with a larger window
|
2022-02-28 07:07:51 +00:00
|
|
|
keyframes = await readKeyframes(60);
|
2023-02-05 11:08:28 +00:00
|
|
|
nextKeyframe = findNextKeyframe(keyframes, desiredCutFrom);
|
2022-02-28 07:07:51 +00:00
|
|
|
}
|
2023-02-05 11:08:28 +00:00
|
|
|
if (nextKeyframe == null) throw new Error('Cannot find any keyframe after the desired start cut point');
|
2022-02-28 07:07:51 +00:00
|
|
|
|
|
|
|
console.log('Smart cut from keyframe', { keyframe: nextKeyframe.time, desiredCutFrom });
|
|
|
|
|
2024-03-20 08:02:34 +00:00
|
|
|
let videoBitrate = parseInt(videoStream.bit_rate!, 10);
|
2022-02-28 07:07:51 +00:00
|
|
|
if (Number.isNaN(videoBitrate)) {
|
|
|
|
console.warn('Unable to detect input bitrate');
|
|
|
|
const stats = await stat(path);
|
2023-11-15 07:42:23 +00:00
|
|
|
videoBitrate = (stats.size * 8) / videoDuration;
|
2022-02-28 07:07:51 +00:00
|
|
|
}
|
|
|
|
|
2023-07-11 15:27:16 +00:00
|
|
|
// to account for inaccuracies and quality loss
|
|
|
|
// see discussion https://github.com/mifi/lossless-cut/issues/126#issuecomment-1602266688
|
|
|
|
videoBitrate = Math.floor(videoBitrate * 1.2);
|
|
|
|
|
2024-02-12 16:01:43 +00:00
|
|
|
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 });
|
2022-02-28 07:07:51 +00:00
|
|
|
|
2022-06-29 23:22:14 +00:00
|
|
|
const timebase = getVideoTimebase(videoStream);
|
2022-02-28 07:07:51 +00:00
|
|
|
if (timebase == null) console.warn('Unable to determine timebase', videoStream.time_base);
|
|
|
|
|
2023-02-08 08:16:07 +00:00
|
|
|
// seems like ffmpeg handles this itself well when encoding same source file
|
|
|
|
// const videoLevel = parseLevel(videoStream);
|
|
|
|
// const videoProfile = parseProfile(videoStream);
|
|
|
|
|
2022-02-28 07:07:51 +00:00
|
|
|
return {
|
2024-02-14 13:12:16 +00:00
|
|
|
losslessCutFrom: nextKeyframe.time,
|
2022-02-28 07:07:51 +00:00
|
|
|
videoStreamIndex: videoStream.index,
|
2023-02-19 06:15:35 +00:00
|
|
|
segmentNeedsSmartCut: true,
|
2022-02-28 07:07:51 +00:00
|
|
|
videoCodec,
|
|
|
|
videoBitrate: Math.floor(videoBitrate),
|
|
|
|
videoTimebase: timebase,
|
|
|
|
};
|
|
|
|
}
|