[Обучалово][Вайб-лонгинг] Максимально быстро нарезаем клипы

В этом посте мы научимся максимально быстро нарезать клипы при помощи видеоплеера 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"

Данный способ не очень удобный: надо заполнять параметры для клипа, время начала и продолжительность клипа.

В прошлом посте я нарезала клипы при помощи кода

set "SEGMENT_TIME=6" ffmpeg -i %1 -acodec copy -f segment -segment_time %SEGMENT_TIME% -vcodec copy -reset_timestamps 1 -map 0 "%~n1_%%d.mp4"

Данный код вообще не подходит для больших видеофайлов: долго, дорого и избыточно.

Вот бы можно было в видеоплеере нажать одну кнопку и сразу получить клип!

Немного поресерчив я нашла решение.

Решение

1. Нам потребуется видеоплеер Media Player Classic (MPC)

Это единственный доступный плеер из которого можно "прочитать" путь к файлу и позицию.

[Обучалово][Вайб-лонгинг] Максимально быстро нарезаем клипы

Скорее всего он у Вас уже стоит, если Вы хоть раз скачивали K-Lite Codec Pack. Он ставился по умолчанию при установке.
Если у Вас его нет, то скачайте и установите последний K-Lite Codec Pack:

В установщике не снимайте галку с установки последней версии MPC-HC.

Либо можете установить MPC-BE, параллельный форк этой же фриварной программы:

Нам необходимо получать информацию о пути к текущему видеофайлу и о позиции проигрывания. В настройках программы (клавиша O) нам необходимо включить веб-интерфейс:

По желанию измените номер порта, главное запомните его
По желанию измените номер порта, главное запомните его

Откройте в веб-браузере страницу

Увидите такую страницу:

<sarcasm>Спасибо что не JSON, бл*** в 2K25</sarcasm>
<sarcasm>Спасибо что не JSON, бл*** в 2K25</sarcasm>

Ничего не понятно, но очень интересно. Если посмотрим её код:

[Обучалово][Вайб-лонгинг] Максимально быстро нарезаем клипы

То можно увидеть что на странице есть элементы с именами и значениями параметров проигрываемого видео. Это нам надо!

2. Ставим AutoHotkey и делаем для него скрипт

Устанавливаем AutoHotkey.

Это мощная программа, позволяющая автоматизировать многие рутинные процессы вашем жизни.

Создаём файл "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 для нарезки. Просто? Да!

Если есть вопросы - пишите в комменты.

Вот вариант нарезки клипов для MPV плеера:

Он лучше чем мой.

Вариант нарезки клипов для VLC:

[Обучалово][Вайб-лонгинг] Максимально быстро нарезаем клипы
45
18
9
3
1
1
69 комментариев