diff --git a/plugin/lib/dialog/service.js b/plugin/lib/dialog/service.js
index 746f467..3753801 100644
--- a/plugin/lib/dialog/service.js
+++ b/plugin/lib/dialog/service.js
@@ -322,7 +322,7 @@ const showProcessBar = async (options = {}) => {
throw new Error("onPause 和 onResume 必须同时配置");
}
- const windowWidth = 300;
+ const windowWidth = 350;
const windowHeight = 60;
// 计算窗口位置
diff --git a/plugin/lib/dialog/style.css b/plugin/lib/dialog/style.css
index f92567c..09f1e72 100644
--- a/plugin/lib/dialog/style.css
+++ b/plugin/lib/dialog/style.css
@@ -606,7 +606,16 @@ textarea:focus {
.process-text {
font-size: 13px;
margin-bottom: 8px;
- padding-right: 20px;
+ white-space: nowrap;
+ overflow-x: auto;
+ overflow-y: hidden;
+ scrollbar-width: none;
+ width: calc(100% - 30px);
+ box-sizing: border-box;
+}
+
+.process-text::-webkit-scrollbar {
+ display: none;
}
.process-bar {
@@ -633,6 +642,21 @@ textarea:focus {
display: flex;
gap: 8px;
-webkit-app-region: no-drag;
+ z-index: 1;
+}
+
+/* 添加按钮区域的渐变遮罩 */
+.process-buttons::before {
+ content: "";
+ position: absolute;
+ right: -8px;
+ /* 对应#process的padding-right */
+ top: -6px;
+ /* 对应#process的padding-top */
+ width: 80px;
+ height: 30px;
+ background: linear-gradient(to right, transparent, var(--button-bg) 60%);
+ pointer-events: none;
}
/* 进度条关闭按钮 */
diff --git a/plugin/lib/quickcomposer.js b/plugin/lib/quickcomposer.js
index 2052497..8c05e61 100644
--- a/plugin/lib/quickcomposer.js
+++ b/plugin/lib/quickcomposer.js
@@ -12,6 +12,7 @@ const quickcomposer = {
macos: require("./quickcomposer/macos"),
status: require("./quickcomposer/status"),
browser: require("./quickcomposer/browser"),
+ video: require("./quickcomposer/video"),
};
module.exports = quickcomposer;
diff --git a/plugin/lib/quickcomposer/video/ffmpeg.js b/plugin/lib/quickcomposer/video/ffmpeg.js
new file mode 100644
index 0000000..6f048d4
--- /dev/null
+++ b/plugin/lib/quickcomposer/video/ffmpeg.js
@@ -0,0 +1,969 @@
+/**
+ * 运行FFmpeg命令并显示进度条
+ * @param {string[]} args FFmpeg命令参数
+ * @param {object} options 选项
+ * @param {string} options.title 进度条标题
+ * @param {string} options.text 进度条文本
+ * @param {string} options.position 进度条位置
+ * @param {function} options.onPause 暂停回调
+ * @param {function} options.onResume 恢复回调
+ * @param {boolean} options.isShowProcessBar 是否显示进度条
+ * @returns {Promise} 返回Promise
+ */
+async function runFFmpeg(args, options = {}) {
+ const {
+ title = "FFmpeg处理",
+ text = "处理中...",
+ position = "bottom-right",
+ onPause,
+ onResume,
+ isShowProcessBar = true,
+ } = options;
+
+ let ffmpegRun;
+
+ // 显示进度条
+ const processBar = isShowProcessBar
+ ? await quickcommand.showProcessBar({
+ title,
+ text,
+ value: 0,
+ position,
+ onPause,
+ onResume,
+ onClose: () => {
+ // 关闭时终止FFmpeg
+ if (ffmpegRun) {
+ try {
+ ffmpegRun.quit();
+ } catch (error) {
+ console.log(error);
+ }
+ }
+ ffmpegRun = null;
+ },
+ })
+ : null;
+
+ // 运行FFmpeg
+ ffmpegRun = utools.runFFmpeg(args, (progress) => {
+ // 更新进度条
+ if (progress.percent !== undefined) {
+ const intProgress = Math.round(progress.percent);
+ if (processBar) {
+ quickcommand.updateProcessBar(
+ {
+ value: intProgress,
+ text: `${text} (${intProgress}%) - 速度: ${progress.speed}`,
+ },
+ processBar
+ );
+ }
+ } else {
+ // 如果没有进度百分比,显示已处理时间
+ if (processBar) {
+ quickcommand.updateProcessBar(
+ {
+ text: `${text} - 已处理: ${progress.time} - 速度: ${progress.speed}`,
+ },
+ processBar
+ );
+ }
+ }
+ });
+
+ return new Promise((resolve, reject) => {
+ ffmpegRun
+ .then(() => {
+ if (processBar) {
+ quickcommand.updateProcessBar(
+ {
+ value: 100,
+ text: "处理完成, 即将关闭进度条",
+ },
+ processBar
+ );
+ quickcommand.asyncSleep(500).then(() => {
+ processBar.close();
+ });
+ }
+ resolve();
+ })
+ .catch((error) => {
+ if (processBar) {
+ quickcommand.updateProcessBar(
+ {
+ text: `处理出错: ${error.message}`,
+ },
+ processBar
+ );
+ }
+ reject(error);
+ });
+ });
+}
+
+/**
+ * 压缩视频
+ * @param {string} input 输入文件路径
+ * @param {string} output 输出文件路径
+ * @param {object} options 压缩选项
+ * @param {string} options.encoder 视频编码器
+ * @param {string} options.preset 压缩预设
+ * @param {number} options.crf 视频质量(0-51)
+ * @param {string} options.resolution 分辨率(keep/3840:2160/2560:1440/1920:1080/1280:720/854:480)
+ * @param {boolean} options.overwrite 是否覆盖已存在的文件
+ */
+async function compressVideo(input, output, options = {}) {
+ const {
+ encoder = "libx264",
+ preset = "medium",
+ crf = 23,
+ resolution = "keep",
+ overwrite = false,
+ } = options;
+
+ const args = [];
+
+ // 覆盖参数必须在最前面
+ if (overwrite) {
+ args.push("-y");
+ }
+
+ args.push("-i", input);
+
+ // 如果需要调整分辨率
+ if (resolution !== "keep") {
+ const [width, height] = resolution.split(":");
+ args.push(
+ "-vf",
+ `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`
+ );
+ }
+
+ args.push(
+ "-c:v",
+ encoder,
+ "-preset",
+ preset,
+ "-crf",
+ crf.toString(),
+ "-c:a",
+ "copy",
+ output
+ );
+
+ await runFFmpeg(args, {
+ title: "视频压缩",
+ text: "正在压缩视频...",
+ });
+}
+
+/**
+ * 视频转GIF
+ * @param {string} input 输入文件路径
+ * @param {string} output 输出文件路径
+ * @param {object} options 转换选项
+ * @param {number} options.fps 帧率
+ * @param {number} options.width 宽度
+ * @param {boolean} options.overwrite 是否覆盖已存在的文件
+ */
+async function convertToGif(input, output, options = {}) {
+ const { fps = 15, width = 480, overwrite = false } = options;
+
+ const args = [];
+
+ // 覆盖参数必须在最前面
+ if (overwrite) {
+ args.push("-y");
+ }
+
+ args.push(
+ "-i",
+ input,
+ "-vf",
+ `fps=${fps},scale=${width}:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse`,
+ "-loop",
+ "0",
+ output
+ );
+
+ await runFFmpeg(args, {
+ title: "GIF转换",
+ text: "正在转换为GIF...",
+ });
+}
+
+/**
+ * 提取音频
+ * @param {string} input 输入文件路径
+ * @param {string} output 输出文件路径
+ * @param {object} options 提取选项
+ * @param {number} options.quality 音频质量(0-9)
+ * @param {boolean} options.overwrite 是否覆盖已存在的文件
+ */
+async function extractAudio(input, output, options = {}) {
+ const { quality = 0, overwrite = false } = options;
+
+ const args = [];
+
+ // 覆盖参数必须在最前面
+ if (overwrite) {
+ args.push("-y");
+ }
+
+ args.push("-i", input, "-q:a", quality.toString(), "-map", "a", output);
+
+ await runFFmpeg(args, {
+ title: "音频提取",
+ text: "正在提取音频...",
+ });
+}
+
+/**
+ * 录制屏幕
+ * @param {string} output 输出文件路径
+ * @param {object} options 录制选项
+ * @param {number} options.fps 帧率
+ * @param {boolean} options.overwrite 是否覆盖已存在的文件
+ *
+ * utools接口目前好像有问题,无法结束录制
+ */
+async function recordScreen(output, options = {}) {
+ const { fps = 30, overwrite = false } = options;
+
+ const args = [];
+
+ // 覆盖参数必须在最前面
+ if (overwrite) {
+ args.push("-y");
+ }
+
+ // 根据操作系统选择录屏命令
+ if (process.platform === "win32") {
+ args.push("-f", "gdigrab", "-framerate", fps.toString(), "-i", "desktop");
+ } else if (process.platform === "darwin") {
+ args.push(
+ "-f",
+ "avfoundation",
+ "-framerate",
+ fps.toString(),
+ "-i",
+ "default"
+ );
+ } else {
+ args.push("-f", "x11grab", "-framerate", fps.toString(), "-i", ":0.0");
+ }
+
+ // 添加输出文件
+ args.push(output);
+
+ const ffmpegRun = utools.runFFmpeg(args);
+
+ return new Promise((resolve, reject) => {
+ ffmpegRun.catch((e) => {
+ quickcommand.showSystemMessageBox("录制失败: " + e.message);
+ reject(e);
+ });
+
+ quickcommand
+ .showSystemWaitButton({
+ text: "结束录制",
+ })
+ .then((confirm) => {
+ console.log("结束录制", confirm, ffmpegRun.quit);
+ if (confirm) {
+ ffmpegRun.quit();
+ resolve();
+ } else {
+ ffmpegRun.kill();
+ reject(new Error("录制取消"));
+ }
+ });
+ });
+}
+
+/**
+ * 将时间字符串转换为秒数
+ * @param {string} timeStr 时间字符串 (格式: HH:mm:ss)
+ * @returns {number} 秒数
+ */
+function timeToSeconds(timeStr) {
+ if (typeof timeStr === "number") return timeStr;
+
+ const parts = timeStr.split(":").map(Number);
+ if (parts.length === 3) {
+ // HH:mm:ss
+ return parts[0] * 3600 + parts[1] * 60 + parts[2];
+ } else if (parts.length === 2) {
+ // mm:ss
+ return parts[0] * 60 + parts[1];
+ } else if (parts.length === 1) {
+ // ss
+ return parts[0];
+ }
+ return 0;
+}
+
+/**
+ * 截取视频片段
+ * @param {string} input 输入文件路径
+ * @param {string} output 输出文件路径
+ * @param {object} options 截取选项
+ * @param {string|number} options.start 开始时间(HH:mm:ss 或秒数)
+ * @param {string|number} options.duration 持续时长(HH:mm:ss 或秒数)
+ * @param {boolean} options.overwrite 是否覆盖已存在的文件
+ */
+async function cutVideo(input, output, options = {}) {
+ const {
+ start = "00:00:00",
+ duration = "00:00:10",
+ overwrite = false,
+ } = options;
+
+ const args = [];
+
+ // 覆盖参数必须在最前面
+ if (overwrite) {
+ args.push("-y");
+ }
+
+ // 转换时间格式为秒数
+ const startSeconds = timeToSeconds(start);
+ const durationSeconds = timeToSeconds(duration);
+
+ args.push(
+ "-i",
+ input,
+ "-ss",
+ startSeconds.toString(),
+ "-t",
+ durationSeconds.toString(),
+ "-c",
+ "copy",
+ output
+ );
+
+ await runFFmpeg(args, {
+ title: "视频剪切",
+ text: "正在截取片段...",
+ });
+}
+
+/**
+ * 旋转/翻转视频
+ * @param {string} input 输入文件路径
+ * @param {string} output 输出文件路径
+ * @param {object} options 选项
+ * @param {number} options.rotate 旋转角度(90/180/270)
+ * @param {boolean} options.flipH 水平翻转
+ * @param {boolean} options.flipV 垂直翻转
+ * @param {boolean} options.overwrite 是否覆盖已存在的文件
+ */
+async function rotateVideo(input, output, options = {}) {
+ const {
+ rotate = 0,
+ flipH = false,
+ flipV = false,
+ overwrite = false,
+ } = options;
+
+ const args = [];
+ if (overwrite) args.push("-y");
+
+ // 构建滤镜
+ let filter = "";
+ if (rotate) filter += `rotate=${(rotate * Math.PI) / 180}`;
+ if (flipH) filter += (filter ? "," : "") + "hflip";
+ if (flipV) filter += (filter ? "," : "") + "vflip";
+
+ args.push("-i", input, "-vf", filter, "-c:a", "copy", output);
+
+ await runFFmpeg(args, {
+ title: "视频旋转/翻转",
+ text: "正在处理视频...",
+ });
+}
+
+/**
+ * 添加水印
+ * @param {string} input 输入文件路径
+ * @param {string} watermark 水印图片路径
+ * @param {string} output 输出文件路径
+ * @param {object} options 选项
+ * @param {string} options.position 位置(topleft/topright/bottomleft/bottomright/center)
+ * @param {number} options.padding 边距
+ * @param {number} options.scale 缩放比例(0-1)
+ * @param {boolean} options.overwrite 是否覆盖已存在的文件
+ */
+async function addWatermark(input, watermark, output, options = {}) {
+ const {
+ position = "bottomright",
+ padding = 10,
+ scale = 0.1,
+ overwrite = false,
+ } = options;
+
+ const args = [];
+ if (overwrite) args.push("-y");
+
+ // 计算水印位置
+ let overlay;
+ switch (position) {
+ case "topleft":
+ overlay = `${padding}:${padding}`;
+ break;
+ case "topright":
+ overlay = `main_w-overlay_w-${padding}:${padding}`;
+ break;
+ case "bottomleft":
+ overlay = `${padding}:main_h-overlay_h-${padding}`;
+ break;
+ case "center":
+ overlay = "(main_w-overlay_w)/2:(main_h-overlay_h)/2";
+ break;
+ default: // bottomright
+ overlay = `main_w-overlay_w-${padding}:main_h-overlay_h-${padding}`;
+ }
+
+ args.push(
+ "-i",
+ input,
+ "-i",
+ watermark,
+ "-filter_complex",
+ `[1:v]scale=iw*${scale}:-1[watermark];[0:v][watermark]overlay=${overlay}`,
+ "-c:a",
+ "copy",
+ output
+ );
+
+ await runFFmpeg(args, {
+ title: "添加水印",
+ text: "正在添加水印...",
+ });
+}
+
+/**
+ * 合并视频
+ * @param {string[]} inputs 输入文件路径数组
+ * @param {string} output 输出文件路径
+ * @param {object} options 选项
+ * @param {boolean} options.overwrite 是否覆盖已存在的文件
+ */
+async function mergeVideos(inputs, output, options = {}) {
+ const { overwrite = false } = options;
+
+ // 先获取第一个视频的分辨率
+ let width, height;
+ try {
+ await new Promise((resolve, reject) => {
+ utools.runFFmpeg(["-i", inputs[0]]).catch((error) => {
+ // FFmpeg 在获取视频信息时会返回错误,但错误信息中包含视频信息
+ const match = error.message.match(/(\d{2,5})x(\d{2,5})/);
+ if (match) {
+ width = match[1];
+ height = match[2];
+ resolve();
+ } else {
+ reject(new Error("无法获取视频分辨率"));
+ }
+ });
+ });
+ } catch (error) {
+ throw new Error("获取视频分辨率失败: " + error.message);
+ }
+
+ const args = [];
+ if (overwrite) args.push("-y");
+
+ // 构建复杂的filter_complex命令
+ let filterComplex = "";
+
+ // 添加所有输入文件
+ inputs.forEach((_, index) => {
+ args.push("-i", inputs[index]);
+ if (index === 0) {
+ // 第一个视频不做处理
+ filterComplex += `[0:v]setsar=1[v0];`;
+ filterComplex += `[0:a]aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo[a0];`;
+ } else {
+ // 其他视频缩放到第一个视频的分辨率
+ filterComplex += `[${index}:v]scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2,setsar=1[v${index}];`;
+ filterComplex += `[${index}:a]aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo[a${index}];`;
+ }
+ });
+
+ // 添加concat
+ filterComplex += `${inputs
+ .map((_, i) => `[v${i}][a${i}]`)
+ .join("")}concat=n=${inputs.length}:v=1:a=1[outv][outa]`;
+
+ args.push(
+ "-filter_complex",
+ filterComplex,
+ "-map",
+ "[outv]",
+ "-map",
+ "[outa]",
+ "-c:v",
+ "libx264",
+ "-c:a",
+ "aac",
+ output
+ );
+
+ await runFFmpeg(args, {
+ title: "合并视频",
+ text: "正在合并视频...",
+ });
+}
+
+/**
+ * 视频调速
+ * @param {string} input 输入文件路径
+ * @param {string} output 输出文件路径
+ * @param {object} options 选项
+ * @param {number} options.speed 速度倍数(0.25-4)
+ * @param {boolean} options.keepPitch 是否保持音调
+ * @param {boolean} options.overwrite 是否覆盖已存在的文件
+ */
+async function changeSpeed(input, output, options = {}) {
+ const { speed = 1, keepPitch = true, overwrite = false } = options;
+
+ const args = [];
+ if (overwrite) args.push("-y");
+
+ // 构建滤镜
+ const tempo = 1 / speed;
+
+ // 构建音频滤镜串
+ let audioFilter;
+ if (keepPitch) {
+ if (speed <= 0.5) {
+ // 0.25-0.5倍速
+ audioFilter = `asetrate=44100*${speed},aresample=44100,atempo=2,atempo=${
+ speed * 2
+ }`;
+ } else if (speed > 2) {
+ // 2-4倍速
+ audioFilter = `asetrate=44100*${speed},aresample=44100,atempo=0.5,atempo=${
+ speed * 0.5
+ }`;
+ } else {
+ // 0.5-2倍速
+ audioFilter = `asetrate=44100*${speed},aresample=44100,atempo=${speed}`;
+ }
+ } else {
+ // 不保持音调时直接调整速度
+ if (speed <= 0.5) {
+ audioFilter = `atempo=2,atempo=${speed * 2}`;
+ } else if (speed > 2) {
+ audioFilter = `atempo=0.5,atempo=${speed * 0.5}`;
+ } else {
+ audioFilter = `atempo=${speed}`;
+ }
+ }
+
+ args.push(
+ "-i",
+ input,
+ "-filter_complex",
+ `[0:v]setpts=${tempo}*PTS[v];[0:a]${audioFilter}[a]`,
+ "-map",
+ "[v]",
+ "-map",
+ "[a]",
+ output
+ );
+
+ await runFFmpeg(args, {
+ title: "视频调速",
+ text: "正在调整速度...",
+ });
+}
+
+/**
+ * 调整视频分辨率
+ * @param {string} input 输入文件路径
+ * @param {string} output 输出文件路径
+ * @param {object} options 选项
+ * @param {number} options.width 宽度
+ * @param {number} options.height 高度
+ * @param {boolean} options.keepAspectRatio 保持宽高比
+ * @param {boolean} options.overwrite 是否覆盖已存在的文件
+ */
+async function resizeVideo(input, output, options = {}) {
+ const {
+ width = -1,
+ height = -1,
+ keepAspectRatio = true,
+ overwrite = false,
+ } = options;
+
+ const args = [];
+ if (overwrite) args.push("-y");
+
+ // 构建缩放滤镜
+ const scale = keepAspectRatio
+ ? `scale=${width}:${height}:force_original_aspect_ratio=decrease`
+ : `scale=${width}:${height}`;
+
+ args.push("-i", input, "-vf", scale, "-c:a", "copy", output);
+
+ await runFFmpeg(args, {
+ title: "调整分辨率",
+ text: "正在调整视频分辨率...",
+ });
+}
+
+/**
+ * 视频格式转换
+ * @param {string} input 输入文件路径
+ * @param {string} output 输出文件路径
+ * @param {object} options 选项
+ * @param {string} options.format 目标格式(mp4/webm/mkv/avi)
+ * @param {string} options.devicePreset 设备优化(general/mobile/tablet/tv)
+ * @param {string} options.resolution 分辨率(keep/3840:2160/2560:1440/1920:1080/1280:720/854:480)
+ * @param {number} options.fps 帧率
+ * @param {string} options.quality 视频质量(keep/high/medium/low)
+ * @param {string} options.preset 编码速度(keep/ultrafast/veryfast/fast/medium/slow/veryslow)
+ * @param {number} options.crf CRF值(0-51)
+ * @param {string} options.bitrateMode 码率控制模式(auto/cbr/vbr)
+ * @param {number} options.videoBitrate 视频码率(Kbps)
+ * @param {string} options.videoCodec 视频编码器
+ * @param {string} options.audioChannels 声道设置(keep/mono/stereo/5.1)
+ * @param {string} options.sampleRate 采样率(keep/44100/48000)
+ * @param {number} options.audioBitrate 音频码率(Kbps)
+ * @param {boolean} options.overwrite 是否覆盖已存在的文件
+ */
+async function convertFormat(input, output, options = {}) {
+ const {
+ format = "mp4",
+ devicePreset = "general",
+ resolution = "keep",
+ fps = null,
+ quality = "keep",
+ preset = "keep",
+ crf = null,
+ bitrateMode = "auto",
+ videoBitrate = null,
+ videoCodec = "copy",
+ audioChannels = "keep",
+ sampleRate = "keep",
+ audioBitrate = null,
+ overwrite = false,
+ } = options;
+
+ const args = [];
+ if (overwrite) args.push("-y");
+
+ args.push("-i", input);
+
+ // 根据设备预设设置参数
+ let targetWidth, targetHeight, targetVideoBitrate, targetAudioBitrate;
+ switch (devicePreset) {
+ case "mobile":
+ targetWidth = 1280;
+ targetHeight = 720;
+ targetVideoBitrate = 2000;
+ targetAudioBitrate = 128;
+ break;
+ case "tablet":
+ targetWidth = 1920;
+ targetHeight = 1080;
+ targetVideoBitrate = 4000;
+ targetAudioBitrate = 192;
+ break;
+ case "tv":
+ targetWidth = 3840;
+ targetHeight = 2160;
+ targetVideoBitrate = 8000;
+ targetAudioBitrate = 320;
+ break;
+ default: // general
+ targetWidth = null;
+ targetHeight = null;
+ targetVideoBitrate = null;
+ targetAudioBitrate = null;
+ }
+
+ // 分辨率设置
+ if (resolution !== "keep") {
+ const [width, height] = resolution.split(":");
+ targetWidth = parseInt(width);
+ targetHeight = parseInt(height);
+ }
+
+ if (targetWidth && targetHeight) {
+ args.push(
+ "-vf",
+ `scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease,pad=${targetWidth}:${targetHeight}:(ow-iw)/2:(oh-ih)/2`
+ );
+ }
+
+ // 帧率设置
+ if (fps) {
+ args.push("-r", fps.toString());
+ }
+
+ // 视频编码设置
+ let vcodec = videoCodec;
+ if (
+ videoCodec === "copy" &&
+ (devicePreset !== "general" || quality !== "keep")
+ ) {
+ // 如果选择了设备优化或质量设置,但编码器为copy,则根据格式选择合适的编码器
+ switch (format) {
+ case "webm":
+ vcodec = "libvpx-vp9";
+ break;
+ case "mp4":
+ case "mkv":
+ vcodec = "libx264";
+ break;
+ case "avi":
+ vcodec = "libx264";
+ break;
+ }
+ }
+ args.push("-c:v", vcodec);
+
+ // 如果不是直接复制视频流,添加编码参数
+ if (vcodec !== "copy") {
+ // 质量设置
+ if (quality !== "keep") {
+ let targetCRF;
+ switch (quality) {
+ case "high":
+ targetCRF = 18;
+ break;
+ case "medium":
+ targetCRF = 23;
+ break;
+ case "low":
+ targetCRF = 28;
+ break;
+ }
+ args.push("-crf", targetCRF.toString());
+ } else if (crf !== null) {
+ args.push("-crf", crf.toString());
+ }
+
+ // 编码速度设置
+ if (preset !== "keep") {
+ args.push("-preset", preset);
+ }
+
+ // 码率控制
+ if (bitrateMode !== "auto") {
+ const vbitrate = videoBitrate || targetVideoBitrate || 4000;
+ if (bitrateMode === "cbr") {
+ args.push(
+ "-b:v",
+ `${vbitrate}k`,
+ "-maxrate",
+ `${vbitrate}k`,
+ "-minrate",
+ `${vbitrate}k`,
+ "-bufsize",
+ `${vbitrate * 2}k`
+ );
+ } else if (bitrateMode === "vbr") {
+ args.push(
+ "-b:v",
+ `${vbitrate}k`,
+ "-maxrate",
+ `${vbitrate * 2}k`,
+ "-bufsize",
+ `${vbitrate * 4}k`
+ );
+ }
+ }
+ }
+
+ // 音频设置
+ if (audioChannels !== "keep") {
+ let ac;
+ switch (audioChannels) {
+ case "mono":
+ ac = 1;
+ break;
+ case "stereo":
+ ac = 2;
+ break;
+ case "5.1":
+ ac = 6;
+ break;
+ }
+ args.push("-ac", ac.toString());
+ }
+
+ if (sampleRate !== "keep") {
+ args.push("-ar", sampleRate.toString());
+ }
+
+ // 音频码率
+ const abitrate = audioBitrate || targetAudioBitrate;
+ if (abitrate) {
+ args.push("-b:a", `${abitrate}k`);
+ }
+
+ // 为特定格式添加额外参数
+ switch (format) {
+ case "webm":
+ if (vcodec !== "copy") {
+ args.push("-deadline", "good", "-cpu-used", "2");
+ }
+ break;
+ case "mp4":
+ args.push("-movflags", "+faststart");
+ break;
+ }
+
+ args.push(output);
+
+ await runFFmpeg(args, {
+ title: "格式转换",
+ text: "正在转换格式...",
+ });
+}
+
+/**
+ * 视频裁剪(画面)
+ * @param {string} input 输入文件路径
+ * @param {string} output 输出文件路径
+ * @param {object} options 选项
+ * @param {number} options.x 起始X坐标
+ * @param {number} options.y 起始Y坐标
+ * @param {number} options.width 裁剪宽度
+ * @param {number} options.height 裁剪高度
+ * @param {boolean} options.overwrite 是否覆盖已存在的文件
+ */
+async function cropVideo(input, output, options = {}) {
+ const { x = 0, y = 0, width = 0, height = 0, overwrite = false } = options;
+
+ const args = [];
+ if (overwrite) args.push("-y");
+
+ args.push(
+ "-i",
+ input,
+ "-vf",
+ `crop=${width}:${height}:${x}:${y}`,
+ "-c:a",
+ "copy",
+ output
+ );
+
+ await runFFmpeg(args, {
+ title: "视频裁剪",
+ text: "正在裁剪视频...",
+ });
+}
+
+/**
+ * 导出图片序列
+ * @param {string} input 输入文件路径
+ * @param {string} output 输出文件路径模式(例如: frame_%d.jpg)
+ * @param {object} options 选项
+ * @param {number} options.fps 每秒提取帧数
+ * @param {string} options.format 图片格式(jpg/png)
+ * @param {number} options.quality 图片质量(1-100)
+ * @param {boolean} options.overwrite 是否覆盖已存在的文件
+ */
+async function extractFrames(input, output, options = {}) {
+ const { fps = 1, format = "jpg", quality = 100, overwrite = false } = options;
+
+ const args = [];
+ if (overwrite) args.push("-y");
+
+ args.push("-i", input, "-vf", `fps=${fps}`, "-frame_pts", "1");
+
+ // 根据格式设置参数
+ if (format === "jpg") {
+ args.push("-q:v", Math.round(((100 - quality) / 100) * 31).toString());
+ } else if (format === "png") {
+ args.push(
+ "-compression_level",
+ Math.round(((100 - quality) / 100) * 9).toString()
+ );
+ }
+
+ args.push(output);
+
+ await runFFmpeg(args, {
+ title: "导出帧",
+ text: "正在导出图片序列...",
+ });
+}
+
+/**
+ * 生成缩略图
+ * @param {string} input 输入文件路径
+ * @param {string} output 输出文件路径
+ * @param {object} options 选项
+ * @param {number} options.time 截取时间点(秒)
+ * @param {number} options.width 缩略图宽度
+ * @param {string} options.format 图片格式(jpg/png)
+ * @param {number} options.quality 图片质量(1-100)
+ * @param {boolean} options.overwrite 是否覆盖已存在的文件
+ */
+async function generateThumbnail(input, output, options = {}) {
+ const {
+ time = 0,
+ width = 320,
+ format = "jpg",
+ quality = 90,
+ overwrite = false,
+ } = options;
+
+ const args = [];
+ if (overwrite) args.push("-y");
+
+ args.push(
+ "-ss",
+ time.toString(),
+ "-i",
+ input,
+ "-vframes",
+ "1",
+ "-vf",
+ `scale=${width}:-1`
+ );
+
+ // 根据格式设置参数
+ if (format === "jpg") {
+ args.push("-q:v", Math.round(((100 - quality) / 100) * 31).toString());
+ } else if (format === "png") {
+ args.push(
+ "-compression_level",
+ Math.round(((100 - quality) / 100) * 9).toString()
+ );
+ }
+
+ args.push(output);
+
+ await runFFmpeg(args, {
+ title: "生成缩略图",
+ text: "正在生成缩略图...",
+ });
+}
+
+module.exports = {
+ runFFmpeg,
+ compressVideo,
+ convertToGif,
+ extractAudio,
+ recordScreen,
+ cutVideo,
+ rotateVideo,
+ addWatermark,
+ mergeVideos,
+ changeSpeed,
+ resizeVideo,
+ convertFormat,
+ cropVideo,
+ extractFrames,
+ generateThumbnail,
+};
diff --git a/plugin/lib/quickcomposer/video/index.js b/plugin/lib/quickcomposer/video/index.js
new file mode 100644
index 0000000..9f52dd2
--- /dev/null
+++ b/plugin/lib/quickcomposer/video/index.js
@@ -0,0 +1,5 @@
+const ffmpeg = require("./ffmpeg");
+
+module.exports = {
+ ...ffmpeg,
+};
diff --git a/src/components/composer/MultiParams.vue b/src/components/composer/MultiParams.vue
index 0d13dcc..2730a41 100644
--- a/src/components/composer/MultiParams.vue
+++ b/src/components/composer/MultiParams.vue
@@ -179,15 +179,22 @@ export default defineComponent({
},
getAllInputValues(argvs) {
const flatArgvs = [];
+ if (!argvs) return flatArgvs;
+
argvs.forEach((item) => {
+ if (!item) return;
+
if (isVarInputVal(item) && item.value) {
flatArgvs.push(stringifyVarInputVal(item));
} else if (typeof item === "number") {
flatArgvs.push(item.toString());
} else if (Array.isArray(item)) {
flatArgvs.push(...this.getAllInputValues(item));
- } else if (typeof item === "object") {
- flatArgvs.push(...this.getAllInputValues(Object.values(item)));
+ } else if (typeof item === "object" && item !== null) {
+ const values = Object.values(item);
+ if (values.length > 0) {
+ flatArgvs.push(...this.getAllInputValues(values));
+ }
}
});
return flatArgvs;
@@ -226,7 +233,7 @@ export default defineComponent({
},
mounted() {
const argvs = this.modelValue.argvs || this.defaultArgvs;
- if (!this.modelValue.code) {
+ if (!this.modelValue.code && Array.isArray(argvs)) {
this.updateModelValue(this.funcName, argvs);
}
},
diff --git a/src/components/composer/common/TimeInput.vue b/src/components/composer/common/TimeInput.vue
new file mode 100644
index 0000000..594f161
--- /dev/null
+++ b/src/components/composer/common/TimeInput.vue
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/composer/param/ParamImporter.vue b/src/components/composer/param/ParamImporter.vue
index 9723a10..52a7d0b 100644
--- a/src/components/composer/param/ParamImporter.vue
+++ b/src/components/composer/param/ParamImporter.vue
@@ -22,6 +22,7 @@ import ButtonGroup from "components/composer/common/ButtonGroup.vue";
import ControlInput from "components/composer/common/ControlInput.vue";
import CheckGroup from "components/composer/common/CheckGroup.vue";
import CheckButton from "components/composer/common/CheckButton.vue";
+import TimeInput from "components/composer/common/TimeInput.vue";
import { QInput, QSelect, QToggle, QCheckbox } from "quasar";
const CodeEditor = defineAsyncComponent(() =>
import("components/composer/common/CodeEditor.vue")
@@ -42,6 +43,7 @@ export default defineComponent({
QInput,
QSelect,
QCheckbox,
+ TimeInput,
CodeEditor,
},
props: {
diff --git a/src/js/composer/commands/index.js b/src/js/composer/commands/index.js
index 961f9d2..f8dcea8 100644
--- a/src/js/composer/commands/index.js
+++ b/src/js/composer/commands/index.js
@@ -19,6 +19,7 @@ import { statusCommands } from "./statusCommands";
import { macosCommands } from "./macosCommands";
import { scriptCommands } from "./scriptCommands";
import { browserCommands } from "./browserCommands";
+import { videoCommands } from "./videoCommands";
const platformCommands = {
win32: [windowsCommands],
@@ -32,6 +33,7 @@ export const commandCategories = [
systemCommands,
audioCommands,
imageCommands,
+ ...(utools.runFFmpeg ? [videoCommands] : []),
utoolsCommands,
...platformCommands[window.processPlatform],
browserCommands,
diff --git a/src/js/composer/commands/videoCommands.js b/src/js/composer/commands/videoCommands.js
new file mode 100644
index 0000000..b0e0db8
--- /dev/null
+++ b/src/js/composer/commands/videoCommands.js
@@ -0,0 +1,1394 @@
+import { newVarInputVal } from "js/composer/varInputValManager";
+
+const getDesktopPath = (fileName) => {
+ return window.joinPath(utools.getPath("desktop"), fileName);
+};
+
+// 视频编码器选项
+const VIDEO_ENCODERS = [
+ { label: "H.264", value: "libx264" },
+ { label: "H.265", value: "libx265" },
+ { label: "VP8", value: "libvpx" },
+ { label: "VP9", value: "libvpx-vp9" },
+];
+
+// 音频编码器选项
+const AUDIO_ENCODERS = [
+ { label: "AAC", value: "aac" },
+ { label: "MP3", value: "libmp3lame" },
+ { label: "Opus", value: "libopus" },
+ { label: "Vorbis", value: "libvorbis" },
+];
+
+// 图片格式选项
+const IMAGE_FORMATS = [
+ { label: "JPG", value: "jpg" },
+ { label: "PNG", value: "png" },
+];
+
+// 编码器预设选项
+const ENCODER_PRESETS = [
+ { label: "保持不变", value: "keep" },
+ { label: "超快", value: "ultrafast" },
+ { label: "非常快", value: "veryfast" },
+ { label: "快速", value: "fast" },
+ { label: "中等", value: "medium" },
+ { label: "慢速", value: "slow" },
+ { label: "非常慢", value: "veryslow" },
+];
+
+// 视频质量选项
+const VIDEO_QUALITY = [
+ { label: "保持不变", value: "keep" },
+ { label: "高质量", value: "high" },
+ { label: "中等质量", value: "medium" },
+ { label: "低质量", value: "low" },
+];
+
+// 分辨率选项
+const RESOLUTIONS = [
+ { label: "保持不变", value: "keep" },
+ { label: "4K(3840x2160)", value: "3840:2160" },
+ { label: "2K(2560x1440)", value: "2560:1440" },
+ { label: "1080P(1920x1080)", value: "1920:1080" },
+ { label: "720P(1280x720)", value: "1280:720" },
+ { label: "480P(854x480)", value: "854:480" },
+];
+
+// 视频格式选项
+const VIDEO_FORMATS = [
+ { label: "MP4 (通用格式)", value: "mp4" },
+ { label: "WebM (网页视频)", value: "webm" },
+ { label: "MKV (高清视频)", value: "mkv" },
+ { label: "AVI (传统格式)", value: "avi" },
+];
+
+// 设备优化选项
+const DEVICE_PRESETS = [
+ { label: "通用", value: "general" },
+ { label: "手机", value: "mobile" },
+ { label: "平板", value: "tablet" },
+ { label: "电视", value: "tv" },
+];
+
+// 码率控制模式
+const BITRATE_MODES = [
+ { label: "自动", value: "auto" },
+ { label: "固定码率", value: "cbr" },
+ { label: "可变码率", value: "vbr" },
+];
+
+// 声道选项
+const AUDIO_CHANNELS = [
+ { label: "保持原有", value: "keep" },
+ { label: "单声道", value: "mono" },
+ { label: "立体声", value: "stereo" },
+ { label: "5.1环绕", value: "5.1" },
+];
+
+// 采样率选项
+const SAMPLE_RATES = [
+ { label: "保持原有", value: "keep" },
+ { label: "44.1kHz", value: "44100" },
+ { label: "48kHz", value: "48000" },
+];
+
+export const videoCommands = {
+ label: "视频操作",
+ icon: "video_library",
+ defaultOpened: false,
+ commands: [
+ {
+ value: "quickcomposer.video.convertFormat",
+ label: "格式转换",
+ icon: "transform",
+ asyncMode: "await",
+ config: [
+ {
+ label: "输入文件",
+ component: "VariableInput",
+ icon: "video_file",
+ width: 12,
+ options: {
+ dialog: {
+ type: "open",
+ options: {
+ title: "选择视频文件",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: [
+ "mp4",
+ "avi",
+ "mkv",
+ "mov",
+ "wmv",
+ "flv",
+ "webm",
+ ],
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ label: "输出文件",
+ component: "VariableInput",
+ icon: "save",
+ width: 12,
+ options: {
+ dialog: {
+ type: "save",
+ options: {
+ title: "保存视频",
+ filters: [
+ {
+ name: "MP4视频",
+ extensions: ["mp4"],
+ },
+ {
+ name: "WebM视频",
+ extensions: ["webm"],
+ },
+ {
+ name: "MKV视频",
+ extensions: ["mkv"],
+ },
+ {
+ name: "AVI视频",
+ extensions: ["avi"],
+ },
+ ],
+ defaultPath: "output.mp4",
+ },
+ },
+ },
+ defaultValue: newVarInputVal("str", getDesktopPath("output.mp4")),
+ },
+ {
+ component: "OptionEditor",
+ width: 12,
+ options: {
+ format: {
+ label: "目标格式",
+ component: "QSelect",
+ width: 3,
+ options: VIDEO_FORMATS,
+ },
+ devicePreset: {
+ label: "设备优化",
+ component: "QSelect",
+ width: 3,
+ options: DEVICE_PRESETS,
+ },
+ resolution: {
+ label: "分辨率",
+ component: "QSelect",
+ width: 3,
+ options: RESOLUTIONS,
+ },
+ fps: {
+ label: "帧率",
+ component: "NumberInput",
+ width: 3,
+ min: 1,
+ max: 60,
+ placeholder: "保持原有",
+ },
+ quality: {
+ label: "视频质量",
+ component: "QSelect",
+ width: 4,
+ options: VIDEO_QUALITY,
+ },
+ preset: {
+ label: "编码速度",
+ component: "QSelect",
+ width: 4,
+ options: ENCODER_PRESETS,
+ },
+ crf: {
+ label: "CRF(0-51)",
+ component: "NumberInput",
+ width: 4,
+ min: 0,
+ max: 51,
+ placeholder: "自动",
+ },
+ bitrateMode: {
+ label: "码率控制",
+ component: "QSelect",
+ width: 4,
+ options: BITRATE_MODES,
+ },
+ videoBitrate: {
+ label: "视频码率(Kbps)",
+ component: "NumberInput",
+ width: 4,
+ min: 100,
+ placeholder: "自动",
+ },
+ videoCodec: {
+ label: "视频编码器",
+ component: "QSelect",
+ width: 4,
+ options: [
+ { label: "自动选择", value: "copy" },
+ ...VIDEO_ENCODERS,
+ ],
+ },
+ audioChannels: {
+ label: "声道",
+ component: "QSelect",
+ width: 4,
+ options: AUDIO_CHANNELS,
+ },
+ sampleRate: {
+ label: "采样率",
+ component: "QSelect",
+ width: 4,
+ options: SAMPLE_RATES,
+ },
+ audioBitrate: {
+ label: "音频码率(Kbps)",
+ component: "NumberInput",
+ width: 4,
+ min: 32,
+ max: 320,
+ placeholder: "自动",
+ },
+ overwrite: {
+ label: "覆盖已存在目标文件",
+ component: "CheckButton",
+ width: 12,
+ },
+ },
+ defaultValue: {
+ format: "mp4",
+ devicePreset: "general",
+ resolution: "keep",
+ fps: null,
+ quality: "keep",
+ preset: "keep",
+ crf: null,
+ bitrateMode: "auto",
+ videoBitrate: null,
+ videoCodec: "copy",
+ audioChannels: "keep",
+ sampleRate: "keep",
+ audioBitrate: null,
+ overwrite: true,
+ },
+ },
+ ],
+ },
+ {
+ value: "quickcomposer.video.compressVideo",
+ label: "视频压缩",
+ icon: "compress",
+ asyncMode: "await",
+ config: [
+ {
+ label: "输入文件",
+ component: "VariableInput",
+ icon: "video_file",
+ width: 12,
+ options: {
+ dialog: {
+ type: "open",
+ options: {
+ title: "选择视频文件",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ properties: ["openFile", "showHiddenFiles"],
+ },
+ },
+ },
+ },
+ {
+ label: "输出文件",
+ component: "VariableInput",
+ icon: "save",
+ width: 12,
+ options: {
+ dialog: {
+ type: "save",
+ options: {
+ title: "保存视频",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ defaultPath: "output.mp4",
+ },
+ },
+ },
+ defaultValue: newVarInputVal("str", getDesktopPath("output.mp4")),
+ },
+ {
+ component: "OptionEditor",
+ width: 12,
+ options: {
+ encoder: {
+ label: "视频编码器",
+ component: "QSelect",
+ width: 3,
+ options: VIDEO_ENCODERS,
+ },
+ preset: {
+ label: "压缩预设",
+ component: "QSelect",
+ width: 3,
+ options: ENCODER_PRESETS,
+ },
+ crf: {
+ label: "质量(0-51)",
+ component: "NumberInput",
+ width: 3,
+ min: 0,
+ max: 51,
+ },
+ resolution: {
+ label: "分辨率",
+ component: "QSelect",
+ width: 3,
+ options: RESOLUTIONS,
+ },
+ overwrite: {
+ label: "覆盖已存在目标文件",
+ component: "CheckButton",
+ width: 12,
+ },
+ },
+ defaultValue: {
+ encoder: "libx264",
+ preset: "medium",
+ crf: 23,
+ resolution: "keep",
+ overwrite: true,
+ },
+ },
+ ],
+ },
+ {
+ value: "quickcomposer.video.convertToGif",
+ label: "视频转GIF",
+ icon: "gif",
+ asyncMode: "await",
+ config: [
+ {
+ label: "输入文件",
+ component: "VariableInput",
+ icon: "video_file",
+ width: 12,
+ options: {
+ dialog: {
+ type: "open",
+ options: {
+ title: "选择视频文件",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ label: "输出文件",
+ component: "VariableInput",
+ icon: "save",
+ width: 12,
+ options: {
+ dialog: {
+ type: "save",
+ options: {
+ title: "保存GIF",
+ filters: [
+ {
+ name: "GIF图片",
+ extensions: ["gif"],
+ },
+ ],
+ defaultPath: "output.gif",
+ },
+ },
+ },
+ defaultValue: newVarInputVal("str", getDesktopPath("output.gif")),
+ },
+ {
+ component: "OptionEditor",
+ width: 12,
+ options: {
+ fps: {
+ label: "帧率",
+ component: "NumberInput",
+ icon: "speed",
+ width: 4,
+ min: 1,
+ max: 60,
+ },
+ width: {
+ label: "宽度",
+ component: "NumberInput",
+ icon: "width",
+ width: 4,
+ min: 1,
+ },
+ overwrite: {
+ label: "覆盖已存在目标文件",
+ component: "CheckButton",
+ width: 4,
+ },
+ },
+ defaultValue: {
+ fps: 15,
+ width: 480,
+ overwrite: true,
+ },
+ },
+ ],
+ },
+ {
+ value: "quickcomposer.video.extractAudio",
+ label: "提取音频",
+ icon: "audio_file",
+ asyncMode: "await",
+ config: [
+ {
+ label: "输入文件",
+ component: "VariableInput",
+ icon: "video_file",
+ width: 12,
+ options: {
+ dialog: {
+ type: "open",
+ options: {
+ title: "选择视频文件",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ label: "输出文件",
+ component: "VariableInput",
+ icon: "save",
+ width: 12,
+ options: {
+ dialog: {
+ type: "save",
+ options: {
+ title: "保存音频",
+ filters: [
+ {
+ name: "音频文件",
+ extensions: ["mp3", "aac", "wav", "m4a"],
+ },
+ ],
+ defaultPath: "output.mp3",
+ },
+ },
+ },
+ defaultValue: newVarInputVal("str", getDesktopPath("output.mp3")),
+ },
+ {
+ component: "OptionEditor",
+ width: 12,
+ options: {
+ quality: {
+ label: "音频质量",
+ component: "NumberInput",
+ icon: "high_quality",
+ width: 6,
+ min: 0,
+ max: 9,
+ placeholder: "0-9,0为最高质量",
+ },
+ overwrite: {
+ label: "覆盖已存在目标文件",
+ component: "CheckButton",
+ width: 6,
+ },
+ },
+ defaultValue: {
+ quality: 0,
+ overwrite: true,
+ },
+ },
+ ],
+ },
+ // utools接口目前好像有问题,无法结束录制,暂时注释
+ // {
+ // value: "quickcomposer.video.recordScreen",
+ // label: "录制屏幕",
+ // icon: "screen_record",
+ // asyncMode: "await",
+ // config: [
+ // {
+ // label: "输出文件",
+ // component: "VariableInput",
+ // icon: "save",
+ // width: 12,
+ // options: {
+ // dialog: {
+ // type: "save",
+ // options: {
+ // title: "保存录屏",
+ // filters: [
+ // {
+ // name: "视频文件",
+ // extensions: ["mp4"],
+ // },
+ // ],
+ // defaultPath: "output.mp4",
+ // },
+ // },
+ // },
+ // defaultValue: newVarInputVal("str", getDesktopPath("output.mp4")),
+ // },
+ // {
+ // component: "OptionEditor",
+ // width: 12,
+ // options: {
+ // fps: {
+ // label: "帧率",
+ // component: "NumberInput",
+ // icon: "speed",
+ // width: 6,
+ // min: 1,
+ // max: 60,
+ // },
+ // overwrite: {
+ // label: "覆盖已存在目标文件",
+ // component: "CheckButton",
+ // width: 6,
+ // },
+ // },
+ // defaultValue: {
+ // fps: 30,
+ // overwrite: true,
+ // },
+ // },
+ // ],
+ // },
+ {
+ value: "quickcomposer.video.cutVideo",
+ label: "截取片段",
+ icon: "content_cut",
+ asyncMode: "await",
+ config: [
+ {
+ label: "输入文件",
+ component: "VariableInput",
+ icon: "video_file",
+ width: 12,
+ options: {
+ dialog: {
+ type: "open",
+ options: {
+ title: "选择视频文件",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ label: "输出文件",
+ component: "VariableInput",
+ icon: "save",
+ width: 12,
+ options: {
+ dialog: {
+ type: "save",
+ options: {
+ title: "保存视频",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ defaultPath: "output.mp4",
+ },
+ },
+ },
+ defaultValue: newVarInputVal("str", getDesktopPath("output.mp4")),
+ },
+ {
+ component: "OptionEditor",
+ width: 12,
+ options: {
+ start: {
+ label: "开始时间",
+ component: "TimeInput",
+ icon: "schedule",
+ width: 4,
+ placeholder: "00:00:00",
+ },
+ duration: {
+ label: "持续时长",
+ component: "TimeInput",
+ icon: "timer",
+ width: 4,
+ placeholder: "00:00:00",
+ },
+ overwrite: {
+ label: "覆盖已存在目标文件",
+ component: "CheckButton",
+ width: 4,
+ },
+ },
+ defaultValue: {
+ start: "00:00:00",
+ duration: "00:00:10",
+ overwrite: true,
+ },
+ },
+ ],
+ },
+ {
+ value: "quickcomposer.video.rotateVideo",
+ label: "旋转/翻转",
+ icon: "rotate_90_degrees_ccw",
+ asyncMode: "await",
+ config: [
+ {
+ label: "输入文件",
+ component: "VariableInput",
+ icon: "video_file",
+ width: 12,
+ options: {
+ dialog: {
+ type: "open",
+ options: {
+ title: "选择视频文件",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ label: "输出文件",
+ component: "VariableInput",
+ icon: "save",
+ width: 12,
+ options: {
+ dialog: {
+ type: "save",
+ options: {
+ title: "保存视频",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ defaultPath: "output.mp4",
+ },
+ },
+ },
+ defaultValue: newVarInputVal("str", getDesktopPath("output.mp4")),
+ },
+ {
+ component: "OptionEditor",
+ width: 12,
+ options: {
+ rotate: {
+ label: "旋转角度",
+ component: "QSelect",
+ width: 3,
+ options: [
+ { label: "不旋转", value: 0 },
+ { label: "90度", value: 90 },
+ { label: "180度", value: 180 },
+ { label: "270度", value: 270 },
+ ],
+ },
+ flipH: {
+ label: "水平翻转",
+ component: "CheckButton",
+ width: 3,
+ },
+ flipV: {
+ label: "垂直翻转",
+ component: "CheckButton",
+ width: 3,
+ },
+ overwrite: {
+ label: "覆盖已存在目标文件",
+ component: "CheckButton",
+ width: 3,
+ },
+ },
+ defaultValue: {
+ rotate: 0,
+ flipH: false,
+ flipV: false,
+ overwrite: true,
+ },
+ },
+ ],
+ },
+ {
+ value: "quickcomposer.video.addWatermark",
+ label: "添加水印",
+ icon: "branding_watermark",
+ asyncMode: "await",
+ config: [
+ {
+ label: "输入文件",
+ component: "VariableInput",
+ icon: "video_file",
+ width: 12,
+ options: {
+ dialog: {
+ type: "open",
+ options: {
+ title: "选择视频文件",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ label: "水印图片",
+ component: "VariableInput",
+ icon: "image",
+ width: 12,
+ options: {
+ dialog: {
+ type: "open",
+ options: {
+ title: "选择水印图片",
+ filters: [
+ {
+ name: "图片文件",
+ extensions: ["png", "jpg", "jpeg", "gif"],
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ label: "输出文件",
+ component: "VariableInput",
+ icon: "save",
+ width: 12,
+ options: {
+ dialog: {
+ type: "save",
+ options: {
+ title: "保存视频",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ defaultPath: "output.mp4",
+ },
+ },
+ },
+ defaultValue: newVarInputVal("str", getDesktopPath("output.mp4")),
+ },
+ {
+ component: "OptionEditor",
+ width: 12,
+ options: {
+ position: {
+ label: "位置",
+ component: "QSelect",
+ width: 3,
+ options: [
+ { label: "左上", value: "topleft" },
+ { label: "右上", value: "topright" },
+ { label: "左下", value: "bottomleft" },
+ { label: "右下", value: "bottomright" },
+ { label: "居中", value: "center" },
+ ],
+ },
+ padding: {
+ label: "边距",
+ component: "NumberInput",
+ width: 3,
+ min: 0,
+ },
+ scale: {
+ label: "缩放比例",
+ component: "NumberInput",
+ width: 3,
+ min: 0.1,
+ max: 1,
+ step: 0.1,
+ },
+ overwrite: {
+ label: "覆盖已存在目标文件",
+ component: "CheckButton",
+ width: 3,
+ },
+ },
+ defaultValue: {
+ position: "bottomright",
+ padding: 10,
+ scale: 0.1,
+ overwrite: true,
+ },
+ },
+ ],
+ },
+ {
+ value: "quickcomposer.video.mergeVideos",
+ label: "合并视频",
+ icon: "merge",
+ description: "将多个视频文件合并为一个,分辨率会统一为第一个视频的分辨率",
+ asyncMode: "await",
+ config: [
+ {
+ label: "输入文件",
+ component: "VariableInput",
+ icon: "video_file",
+ width: 12,
+ placeholder: "合并的视频顺序依据选择的视频顺序",
+ options: {
+ dialog: {
+ type: "open",
+ options: {
+ title: "选择视频文件",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ properties: ["openFile", "multiSelections"],
+ },
+ },
+ },
+ },
+ {
+ label: "输出文件",
+ component: "VariableInput",
+ icon: "save",
+ width: 12,
+ options: {
+ dialog: {
+ type: "save",
+ options: {
+ title: "保存视频",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ defaultPath: "output.mp4",
+ },
+ },
+ },
+ defaultValue: newVarInputVal("str", getDesktopPath("output.mp4")),
+ },
+ {
+ component: "OptionEditor",
+ width: 12,
+ options: {
+ overwrite: {
+ label: "覆盖已存在目标文件",
+ component: "CheckButton",
+ width: 12,
+ },
+ },
+ defaultValue: {
+ overwrite: true,
+ },
+ },
+ ],
+ },
+ {
+ value: "quickcomposer.video.changeSpeed",
+ label: "视频调速",
+ icon: "speed",
+ description: "调整视频播放速度,调速区间0.25-4",
+ asyncMode: "await",
+ config: [
+ {
+ label: "输入文件",
+ component: "VariableInput",
+ icon: "video_file",
+ width: 12,
+ options: {
+ dialog: {
+ type: "open",
+ options: {
+ title: "选择视频文件",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ label: "输出文件",
+ component: "VariableInput",
+ icon: "save",
+ width: 12,
+ options: {
+ dialog: {
+ type: "save",
+ options: {
+ title: "保存视频",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ defaultPath: "output.mp4",
+ },
+ },
+ },
+ defaultValue: newVarInputVal("str", getDesktopPath("output.mp4")),
+ },
+ {
+ component: "OptionEditor",
+ width: 12,
+ options: {
+ speed: {
+ label: "速度倍数",
+ component: "NumberInput",
+ width: 4,
+ min: 0.25,
+ max: 4,
+ step: 0.25,
+ placeholder: "0.25-4",
+ },
+ keepPitch: {
+ label: "保持音调",
+ component: "CheckButton",
+ width: 4,
+ },
+ overwrite: {
+ label: "覆盖已存在目标文件",
+ component: "CheckButton",
+ width: 4,
+ },
+ },
+ defaultValue: {
+ speed: 1,
+ keepPitch: true,
+ overwrite: true,
+ },
+ },
+ ],
+ },
+ {
+ value: "quickcomposer.video.resizeVideo",
+ label: "调整分辨率",
+ icon: "aspect_ratio",
+ asyncMode: "await",
+ config: [
+ {
+ label: "输入文件",
+ component: "VariableInput",
+ icon: "video_file",
+ width: 12,
+ options: {
+ dialog: {
+ type: "open",
+ options: {
+ title: "选择视频文件",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ label: "输出文件",
+ component: "VariableInput",
+ icon: "save",
+ width: 12,
+ options: {
+ dialog: {
+ type: "save",
+ options: {
+ title: "保存视频",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ defaultPath: "output.mp4",
+ },
+ },
+ },
+ defaultValue: newVarInputVal("str", getDesktopPath("output.mp4")),
+ },
+ {
+ component: "OptionEditor",
+ width: 12,
+ options: {
+ width: {
+ label: "宽度",
+ component: "NumberInput",
+ width: 3,
+ min: -1,
+ placeholder: "保持比例填-1",
+ },
+ height: {
+ label: "高度",
+ component: "NumberInput",
+ width: 3,
+ min: -1,
+ placeholder: "保持比例填-1",
+ },
+ keepAspectRatio: {
+ label: "保持宽高比",
+ component: "CheckButton",
+ width: 3,
+ },
+ overwrite: {
+ label: "覆盖已存在目标文件",
+ component: "CheckButton",
+ width: 3,
+ },
+ },
+ defaultValue: {
+ width: -1,
+ height: -1,
+ keepAspectRatio: true,
+ overwrite: true,
+ },
+ },
+ ],
+ },
+ {
+ value: "quickcomposer.video.cropVideo",
+ label: "裁剪画面",
+ icon: "crop",
+ asyncMode: "await",
+ config: [
+ {
+ label: "输入文件",
+ component: "VariableInput",
+ icon: "video_file",
+ width: 12,
+ options: {
+ dialog: {
+ type: "open",
+ options: {
+ title: "选择视频文件",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ label: "输出文件",
+ component: "VariableInput",
+ icon: "save",
+ width: 12,
+ options: {
+ dialog: {
+ type: "save",
+ options: {
+ title: "保存视频",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ defaultPath: "output.mp4",
+ },
+ },
+ },
+ defaultValue: newVarInputVal("str", getDesktopPath("output.mp4")),
+ },
+ {
+ component: "OptionEditor",
+ width: 12,
+ options: {
+ x: {
+ label: "X坐标",
+ component: "NumberInput",
+ width: 3,
+ min: 0,
+ placeholder: "起始X坐标",
+ },
+ y: {
+ label: "Y坐标",
+ component: "NumberInput",
+ width: 3,
+ min: 0,
+ placeholder: "起始Y坐标",
+ },
+ width: {
+ label: "宽度",
+ component: "NumberInput",
+ width: 3,
+ min: 1,
+ placeholder: "裁剪宽度",
+ },
+ height: {
+ label: "高度",
+ component: "NumberInput",
+ width: 3,
+ min: 1,
+ placeholder: "裁剪高度",
+ },
+ overwrite: {
+ label: "覆盖已存在目标文件",
+ component: "CheckButton",
+ width: 12,
+ },
+ },
+ defaultValue: {
+ x: 0,
+ y: 0,
+ width: 1920,
+ height: 1080,
+ overwrite: true,
+ },
+ },
+ ],
+ },
+ {
+ value: "quickcomposer.video.extractFrames",
+ label: "导出帧序列",
+ icon: "burst_mode",
+ asyncMode: "await",
+ config: [
+ {
+ label: "输入文件",
+ component: "VariableInput",
+ icon: "video_file",
+ width: 12,
+ options: {
+ dialog: {
+ type: "open",
+ options: {
+ title: "选择视频文件",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ label: "输出文件",
+ component: "VariableInput",
+ icon: "save",
+ width: 12,
+ description: "使用 %d 表示帧序号,例如: frame_%d.jpg",
+ options: {
+ dialog: {
+ type: "save",
+ options: {
+ title: "保存图片序列",
+ filters: [
+ {
+ name: "JPG图片",
+ extensions: ["jpg"],
+ },
+ {
+ name: "PNG图片",
+ extensions: ["png"],
+ },
+ ],
+ defaultPath: "frame_%d.jpg",
+ },
+ },
+ },
+ defaultValue: newVarInputVal("str", getDesktopPath("frame_%d.jpg")),
+ },
+ {
+ component: "OptionEditor",
+ width: 12,
+ options: {
+ fps: {
+ label: "每秒帧数",
+ component: "NumberInput",
+ width: 3,
+ min: 0.1,
+ max: 60,
+ step: 0.1,
+ },
+ format: {
+ label: "图片格式",
+ component: "QSelect",
+ width: 3,
+ options: IMAGE_FORMATS,
+ },
+ quality: {
+ label: "图片质量",
+ component: "NumberInput",
+ width: 3,
+ min: 1,
+ max: 100,
+ placeholder: "1-100",
+ },
+ overwrite: {
+ label: "覆盖已存在目标文件",
+ component: "CheckButton",
+ width: 3,
+ },
+ },
+ defaultValue: {
+ fps: 1,
+ format: "jpg",
+ quality: 90,
+ overwrite: true,
+ },
+ },
+ ],
+ },
+ {
+ value: "quickcomposer.video.generateThumbnail",
+ label: "生成缩略图",
+ icon: "photo",
+ asyncMode: "await",
+ config: [
+ {
+ label: "输入文件",
+ component: "VariableInput",
+ icon: "video_file",
+ width: 12,
+ options: {
+ dialog: {
+ type: "open",
+ options: {
+ title: "选择视频文件",
+ filters: [
+ {
+ name: "视频文件",
+ extensions: ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
+ },
+ ],
+ },
+ },
+ },
+ },
+ {
+ label: "输出文件",
+ component: "VariableInput",
+ icon: "save",
+ width: 12,
+ options: {
+ dialog: {
+ type: "save",
+ options: {
+ title: "保存缩略图",
+ filters: [
+ {
+ name: "JPG图片",
+ extensions: ["jpg"],
+ },
+ {
+ name: "PNG图片",
+ extensions: ["png"],
+ },
+ ],
+ defaultPath: "thumbnail.jpg",
+ },
+ },
+ },
+ defaultValue: newVarInputVal("str", getDesktopPath("thumbnail.jpg")),
+ },
+ {
+ component: "OptionEditor",
+ width: 12,
+ options: {
+ time: {
+ label: "时间点(秒)",
+ component: "NumberInput",
+ width: 3,
+ min: 0,
+ step: 0.1,
+ },
+ width: {
+ label: "宽度",
+ component: "NumberInput",
+ width: 3,
+ min: 1,
+ },
+ format: {
+ label: "图片格式",
+ component: "QSelect",
+ width: 3,
+ options: IMAGE_FORMATS,
+ },
+ quality: {
+ label: "图片质量",
+ component: "NumberInput",
+ width: 3,
+ min: 1,
+ max: 100,
+ placeholder: "1-100",
+ },
+ overwrite: {
+ label: "覆盖已存在目标文件",
+ component: "CheckButton",
+ width: 12,
+ },
+ },
+ defaultValue: {
+ time: 0,
+ width: 320,
+ format: "jpg",
+ quality: 90,
+ overwrite: true,
+ },
+ },
+ ],
+ },
+ ],
+};
diff --git a/src/js/composer/generateCode.js b/src/js/composer/generateCode.js
index 0102ff9..e3f73e5 100644
--- a/src/js/composer/generateCode.js
+++ b/src/js/composer/generateCode.js
@@ -131,7 +131,5 @@ export function generateCode(flow) {
const finalCode = code.join("\n");
- console.log(finalCode);
-
return finalCode;
}