[Обучалово][Вайб-лонгинг] Максимально быстро нарезаем клипы
В этом посте мы научимся максимально быстро нарезать клипы при помощи видеоплеера MPC, программы хоткеев AutoHotkey и FFMpeg.
Продолжение поста про FFmpeg:
Как часто вы нарезаете клипы для постов или "гифки" для комментов? Я вот делаю это очень часто. И для меня это стало рутиной задачей: вот есть видео, надо из него выдернуть клип с определённым временным интервалом. Для нарезки клипов можно использовать любой видеоредактор. Но самый быстрый вариант - это скидывать видео на батник с скриптом для FFMpeg:
set "START_TS=00:00:05"
set "TIME=5"
set "START_TS_STR=%START_TS::=_%"
ffmpeg -i %1 -ss %START_TS% -t %TIME% -c copy "%~n1_%START_TS_STR%_clip.mp4"
Данный способ не очень удобный: надо заполнять параметры для клипа, время начала и продолжительность клипа.
Данный код вообще не подходит для больших видеофайлов: долго, дорого и избыточно.
Вот бы можно было в видеоплеере нажать одну кнопку и сразу получить клип!
Немного поресерчив я нашла решение.
Решение
1. Нам потребуется видеоплеер Media Player Classic (MPC)
Это единственный доступный плеер из которого можно "прочитать" путь к файлу и позицию.
Скорее всего он у Вас уже стоит, если Вы хоть раз скачивали K-Lite Codec Pack. Он ставился по умолчанию при установке. Если у Вас его нет, то скачайте и установите последний K-Lite Codec Pack:
Нам необходимо получать информацию о пути к текущему видеофайлу и о позиции проигрывания. В настройках программы (клавиша O) нам необходимо включить веб-интерфейс:
По желанию измените номер порта, главное запомните его
Это мощная программа, позволяющая автоматизировать многие рутинные процессы вашем жизни.
Создаём файл "make clip.ahk" с кодом:
; Путь к Node.js (замените на свой путь)
nodePath := "D:\Program Files\nodejs\node.exe"
; Путь к Node.js скрипту (замените на свой путь)
nodeScript := "E:\Development\NodeJS\vlc\main.js"
; Продолжительность клипа в секундах
seconds := 1
; Нужно ли аудио?
audio_enable := false
#SingleInstance force
; Горячие клавишы F1 будет работать только когда окно MPC-HC активно
#If WinActive("ahk_exe mpc-hc64.exe") or WinActive("ahk_exe mpc-hc.exe") or WinActive("ahk_exe mpc-be64.exe") or WinActive("ahk_exe mpc-be.exe")
{
Tab::
if audio_enable{
audio_enable := false
SoundBeep 1000
}
else{
audio_enable := true
SoundBeep 1500
}
ToolTip, Аудио: %audio_enable%, 10, 10
SetTimer, RemoveToolTip, -5000
return
RemoveToolTip:
ToolTip
return
F1::
seconds := 1
Goto, Execute
F2::
seconds := 2
Goto, Execute
F3::
seconds := 3
Goto, Execute
F4::
seconds := 4
Goto, Execute
F5::
seconds := 5
Goto, Execute
F6::
seconds := 6
Goto, Execute
F7::
seconds := 7
Goto, Execute
F8::
seconds := 8
Goto, Execute
F9::
; Запрашиваем у пользователя параметр
InputBox, seconds, Введите количество секунд для клипа:,Введите количество секунд для клипа:, 400, 30
if (ErrorLevel) {
return
}
If seconds is not number
{
return
}
Goto, Execute
Execute:
Run, %nodePath% "%nodeScript%" --seconds "%seconds%" --audio_enable %audio_enable%
return
}
#IfWinActive ; Завершение контекстно-зависимой секции
Горячими клавишами "F1"-"F8" мы будем задавать продолжительность клипа. Клавиша "F9" - устанавливает вводимую в инпут продолжительность. А "Tab" переключает параметр звука: можно выбирать будет ли в клипе звук. Замените параметры "nodePath" и "nodeScript" на свой пути к своему рантайму и скрипту.
3. Пишим скрипт для нарезки клипа FFmpeg
Я использую NodeJS. Если Вы используете другой язык: скопируйте код нейронке и попросите портировать. ¯\_(ツ)_/¯
Попросите с уважением.
const http = require('http');
const fs = require('fs');
const path = require('path');
const { exec, spawn } = require('child_process');
// Конфигурация
const OUTPUT_DIR = 'D:\\Downloads\\clips'; //директория для сохранения клипов
const MPC_WEB_INTERFACE = 'http://localhost:13579'; //путь до веб-интерейса с инфой MPC
// Выходное видео
const TARGET_VIDEO_WIDTH = 592; //ширина видео 854/480 ~= 592/336
const TARGET_VIDEO_HEIGHT = 336; //высота видео
const TARGET_VIDEO_FPS = 30; //кадры в секунду
const TARGET_VIDEO_CRT = 12; //"качество" кодирования [0-51] 0-lossless, 23-default
const TARGET_VIDEO_ENCODE_PRESET = 'slower'; //пресет кодирования, время на задачу - ultrafast superfast veryfast faster fast medium slow slower veryslow
const TARGET_VIDEO_BITRATE_MAX = '1500K'; //максимальный битрейт
let ARGS = {}; //параметры из процесса "--seconds 4" => {seconds:4}
async function main() {
try {
// Проверяем наличие ffmpeg
exec('ffmpeg -version', async (error) => {
if (error) {
throw new Error('❌ FFmpeg не найден. Установите FFmpeg и добавьте в PATH');
}
});
console.log('✅ FFmpeg найден');
// Определяем входные параметры
define_args();
if (!ARGS.seconds) {
throw new Error('❌ не указан параметр секунд');
}
if (ARGS.audio_enable === undefined) {
throw new Error('❌ не указан параметр аудио');
}
ARGS.audio_enable = ARGS.audio_enable === '1';
// Делаем клип
await create_сlip()
.then(() => process.exit(0))
.catch((error) => {
throw new Error(error);
});
} catch (error) {
console.error('❌ Error:', error.message);
await delay(60000);
process.exit(1);
}
}
main();
// Функция для получения данных с веб-интерфейса MPC-HC
async function get_MPC_Data() {
return new Promise((resolve, reject) => {
const url = `${MPC_WEB_INTERFACE}/variables.html`;
http.get(url, (response) => {
let data = '';
// Проверяем статус ответа
if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
return;
}
response.on('data', (chunk) => {
data += chunk;
});
response.on('end', () => {
try {
// Парсим HTML и извлекаем данные из тегов <p>
const variables = {};
// Регулярное выражение для поиска всех тегов <p> с id и содержимым
const regex = /<p id="([^"]+)">([^<]+)<\/p>/g;
let match;
while ((match = regex.exec(data)) !== null) {
variables[match[1]] = match[2];
}
if (Object.keys(variables).length === 0) {
reject(new Error('Не удалось извлечь данные из variables.html'));
return;
}
resolve(variables);
} catch (error) {
reject(error);
}
});
}).on('error', (error) => {
reject(new Error(`Не удалось подключиться к MPC-HC: ${error.message}`));
});
});
}
// Функция для получения информации о текущем файле и позиции в видео
async function get_MPC_Info() {
try {
const data = await get_MPC_Data();
console.log('Полученные данные от MPC-HC:', data);
// Извлекаем нужную информацию
const filepath = data.filepath;
const position_ms = parseInt(data.position) || 0;
const duration_ms = parseInt(data.duration) || 0;
const position_seconds = position_ms / 1000; // преобразуем мс в секунды
const duration_seconds = duration_ms / 1000; // преобразуем мс в секунды
const state = parseInt(data.state) || 0;
if (!filepath) {
throw new Error('Файл не воспроизводится или данные недоступны');
}
return {
filepath,
position: position_seconds,
duration: duration_seconds,
position_ms,
duration_ms,
state,
state_string: data.statestring,
is_playing: state === 2, // 2 = воспроизведение
position_string: data.positionstring,
duration_string: data.durationstring
};
} catch (error) {
throw new Error(`Ошибка получения данных MPC-HC: ${error.message}`);
}
}
// Функция для создания клипа с помощью ffmpeg
async function create_clip_ffmpeg(input_filepath, start_time_ms, duration_s, output_filepath) {
return new Promise(async (resolve, reject) => {
try {
let ffmpeg_args = [
'-y', //перезаписывать существующий файл
'-ss', start_time_ms / 1000, //стартовое время
'-t', duration_s, //продолжительность в секундах
'-i', input_filepath, //входное видео
'-filter_complex', `[0:v]scale=${TARGET_VIDEO_WIDTH}:${TARGET_VIDEO_HEIGHT}:force_original_aspect_ratio=decrease,pad=${TARGET_VIDEO_WIDTH}:${TARGET_VIDEO_HEIGHT}:-1:-1:color=black,fps=${TARGET_VIDEO_FPS}[video]`
]
if (ARGS.audio_enable) {
const ffmpeg_args_audio_params = [
'-map', '0:a', //какой аудиопоток отдаём на выход
'-c:a', 'aac', //кодек aудио
]
ffmpeg_args = [...ffmpeg_args, ...ffmpeg_args_audio_params];
}
const ffmpeg_args_params = [
'-c:v', 'libx264', //кодек видео
'-map', '[video]', //какой видеопоток отдаём на выход
'-preset', TARGET_VIDEO_ENCODE_PRESET, //время кодирования
'-crf', TARGET_VIDEO_CRT, //качество кодирования
'-maxrate', TARGET_VIDEO_BITRATE_MAX, //максимальный битрейт
'-bufsize', '2M', //размер буфера
output_filepath
]
ffmpeg_args = [...ffmpeg_args, ...ffmpeg_args_params];
console.log('Запуск ffmpeg с аргументами:', ffmpeg_args.join(' '));
const ffmpeg = spawn('ffmpeg', ffmpeg_args, {
stdio: ['ignore', 'pipe', 'pipe']
});
let stderr_data = '';
ffmpeg.stderr.on('data', (data) => {
stderr_data += data.toString();
process.stdout.write(data.toString());
});
ffmpeg.stdout.on('data', (data) => {
process.stdout.write(data.toString());
});
ffmpeg.on('close', (code) => {
if (code === 0) {
resolve(output_filepath);
} else {
console.error('stderr:', stderr_data);
reject(new Error(`ffmpeg завершился с кодом ${code}`));
}
});
ffmpeg.on('error', (err) => {
reject(new Error(`Ошибка запуска ffmpeg: ${err.message}`));
});
}
catch (error) {
reject(new Error(error.message));
}
});
}
// Создание клипа
async function create_сlip() {
try {
// Создаем директорию для клипов, если она не существует
if (!fs.existsSync(OUTPUT_DIR)) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
}
console.log('Получаем информацию из MPC-HC...');
const mpc_info = await get_MPC_Info();
console.log('📊 Информация MPC-HC:');
console.log(`- Файл: ${mpc_info.filepath}`);
console.log(`- Текущая позиция: ${mpc_info.position.toFixed(2)} сек (${mpc_info.position_string})`);
console.log(`- Длительность файла: ${mpc_info.duration.toFixed(2)} сек (${mpc_info.duration_string})`);
console.log(`- Статус: ${mpc_info.state_string}`);
// Проверяем, что позиция валидна
if (mpc_info.position >= mpc_info.duration) {
throw new Error('Текущая позиция превышает длительность файла');
}
// Проверяем, что клип помещается в оставшееся время
const remaining_time = mpc_info.duration - mpc_info.position;
if (remaining_time < ARGS.seconds) {
console.warn(`⚠️Внимание: осталось только ${remaining_time.toFixed(2)} секунд, создаем клип этой длительности`);
}
const clip_duration_s = Math.min(ARGS.seconds, remaining_time);
// Генерируем имя выходного файла
const filename = path.basename(mpc_info.filepath, path.extname(mpc_info.filepath));
//const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const output_filepath = path.join(OUTPUT_DIR, `${filename}_${Math.floor(mpc_info.position)}s_${clip_duration_s}s_clip${path.extname(mpc_info.filepath)}`);
// Создаем клип
console.log(`🎬 Создаем ${clip_duration_s}-секундный клип начиная с ${mpc_info.position.toFixed(2)} сек...`);
const resultFile = await create_clip_ffmpeg(
mpc_info.filepath,
mpc_info.position_ms,
clip_duration_s,
output_filepath
);
console.log(`✅ Клип успешно создан: ${resultFile}`);
return resultFile;
} catch (error) {
console.error('❌ Ошибка:', error.message);
console.log('🔧 Убедитесь, что:');
console.log('1. MPC-HC запущен с параметром: mpc-hc64.exe /webport 13579');
console.log('2. В настройках MPC-HC включен веб-интерфейс (Options → Web Interface)');
console.log('3. MPC-HC воспроизводит файл');
console.log('4. Файл не находится в сети или на съемном носителе');
throw new Error(`Ошибка при создании клипа: ${error.message}`);
}
}
// Пауза
async function delay(milliseconds) {
return new Promise(resolve => {
setTimeout(resolve, milliseconds);
});
}
// Обрабатываем входные параметры
function define_args() {
const args = process.argv.slice(2);
for (let i = 0; i < args.length; i++) {
if (args[i].startsWith('--')) {
const key = args[i].substring(2);
// Проверяем, есть ли следующее значение (не начинается с --)
if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
ARGS[key] = args[i + 1];
i++; // Пропускаем следующее значение
} else {
ARGS[key] = true; // Флаг без значения
}
}
}
}
В коде ничего сложного нет: считываем входные параметры, парсим веб-интерфейс плеера для получения пути к файлу и позиции, запускаем FFMpeg.
4. Всё готово. Вы великолепны
Мы проигрываем видео в плеере, запускаем ahk скрипт и нажимая на горящие клавиши F1-F8 мы передаём нашему скрипту продолжительность клипа 1-8 секунд в качестве параметра, скрипт передаёт параметр FFmpeg для нарезки. Просто? Да!