LosslessCut 表达式(JavaScript Expressions)完全指南:按表达式选择、批量编辑分段与文件名模板
【免费下载链接】lossless-cutThe swiss army knife of lossless video/audio editing项目地址: https://gitcode.com/gh_mirrors/lo/lossless-cut
导读
LosslessCut(无损音视频剪切工具)内置了一套 JavaScript 表达式引擎,允许你在特定对话框中编写 JS 表达式,实现"按条件批量选择分段""批量修改分段属性""在导出文件名模板中做动态计算""按条件过滤音视频轨道"等高级操作。本文以官方文档 docs/expressions.md 为主线,结合仓库源码深入讲解表达式的可用变量、求值机制、安全沙箱实现与实战示例,读完你即可用一行表达式完成原本需要手动逐条处理的批量编辑任务。
一、表达式系统总览:一个轻量 JS 求值环境
LosslessCut 在若干对话框(对话框)中提供 JavaScript 表达式支持,本质上是一个基于浏览器 Web Worker 的"基础 JavaScript 环境",内置了 核心 JavaScript 功能,也就是说你日常使用的Math、String、Array、RegExp、JSON等内置对象与语法(三元表达式、箭头函数、模板字符串、展开运算符等)都可以直接使用。
从源码结构看,整个表达式求值链路由两个文件支撑:
- 主线程侧的封装 src/renderer/src/worker/eval.ts:暴露
safeishEval(code, context)异步函数,它把表达式代码和以 JSON 序列化的上下文对象context通过postMessage发送给 Web Worker,并按请求id匹配返回结果;出错时以error字段回传并reject。 - Worker 侧的实际执行器 src/renderer/src/worker/evalWorker.ts:收到消息后执行
Function('with (this) { return (' + code + '); }').call(context),把上下文对象作为this暴露给表达式;随后将postMessage的结果(data或error)返回主线程。
因此,表达式本质上是一条"返回值即结果"的 JS 语句(用圆括号包裹后求值),上下文变量通过with (this)注入为可直接访问的全局名字。注意:表达式是一条语句,不是一段脚本,你不能在其中声明多条语句或使用return,而是直接写成一个有值的表达式(对象字面量需要加括号,如({ start: 1 }))。
二、按表达式选择分段(Select segments by expression)
2.1 用法与核心变量
官方文档 docs/expressions.md 说明:在"按表达式选择分段"对话框中,你会拿到一个全局变量segment(类型为Segment),并编写一个返回true或false的表达式;对所有分段逐一求值,结果为true的分段将被选中。
最经典的示例——选中所有时长小于 5 秒的分段:
segment.duration < 5Segment的完整类型定义见 docs/generated/types.md:
interface Segment { index: number; // 分段在分段列表中的下标,从 0 开始 label: string; // 分段名称(label) start: number; // 分段开始时间(秒) end?: number | undefined; // 分段结束时间(秒);marker(标记点)为 undefined duration: number; // 时长(秒),等于 end - start;marker 为 0 tags: Record<string, string>; // 与该分段关联的标签(tag) }2.2 官方对话框内置示例
在 src/renderer/src/hooks/useSegments.tsx 的selectSegmentsByExpr实现中,可以看到对话框内置的完整示例清单,均可一键填入(直接对应源码中的examples数组):
| 功能点 | 表达式 |
|---|---|
| 时长小于 5 秒 | segment.duration < 5 |
| 开始时间在 01:00 之后 | segment.start > 60 |
| 名称精确匹配 | segment.label === 'My label' |
| 名称以某前缀开头 | segment.label.startsWith('My lab') |
| 名称用正则匹配 | /^My label/.test(segment.label) |
| 某个 tag 的值匹配 | segment.tags.myTag === 'tag value' |
| 标记点(marker) | segment.end == null |
对话框底部还会列出可用变量segment.index、segment.label、segment.start、segment.end、segment.duration、segment.tags.*,其渲染组件为 src/renderer/src/components/ExpressionDialog.tsx。
2.3 求值细节与边界行为
从源码可以看出几个重要的行为边界:
- 表达式返回严格等于
true(=== true)才选中;返回其他真值不会选中(见matchSegment:(await safeishEval(expr, { segment })) === true)。 - 求值是并发的:使用
pMap(..., { concurrency: 5 })对全部分段并行求值。 - 空表达式会报错 "Please enter a JavaScript expression."。
- 没有分段匹配时提示 "No segments match this expression.";全部匹配时提示 "All segments match this expression."(防止无意义操作)。
- 表达式抛错时会提示 "Expression failed: ...",不会影响已有选中状态。
三、按表达式编辑分段(Edit segments by expression)
3.1 用法说明
官方文档 docs/expressions.md 说明:该功能对每一个已选中的分段给出segment变量,你返回一个包含被修改属性的新对象,返回对象中出现的属性会被写回分段。文档同时提示"更多示例见应用内"。
源码实现位于 src/renderer/src/hooks/useSegments.tsx 的mutateSegmentsByExpr,它通过mutateSegment校验返回结果:
- 表达式必须返回对象,否则报错 "The expression must return an object"。
- 返回对象中可识别并写回的属性是
label(字符串,写回分段名称)、start、end(数字)以及tags(对象)。其中start/end会经过有效性校验(例如开始时间必须小于结束时间,否则提示 "Segment start time must be less than end time" 之类错误)。 tags会合并写回:返回tags时将新标签合并进原分段标签。- 由于
segment对象在求值前被克隆(见getScopeSegment:tags: { ...tags }),你在表达式中修改segment.tags不会污染原始数据;只有返回的对象才会真正生效。
3.2 官方内置示例(可直接套用)
以下示例均来自源码中的examples数组,覆盖了最常见的批量编辑场景:
| 功能点 | 表达式 |
|---|---|
| 前后各扩展 5 秒 | { start: segment.start - 5, end: segment.end + 5 } |
| 前后各收缩 5 秒 | { start: segment.start + 5, end: segment.end - 5 } |
| 以 start 为中心对称 5 秒 | { start: segment.start - 5, end: segment.start + 5 } |
| 给名称追加编号后缀 | { label: `${segment.label} ${segment.index + 1}` } |
| 给偶数下标分段加 tag | { tags: (segment.index + 1) % 2 === 0 ? { ...segment.tags, even: 'true' } : segment.tags } |
| 全转成标记点(marker) | { end: undefined } |
| 标记点转成 5 秒分段 | { ...(segment.end == null && { end: segment.start + 5 }) } |
注意第三组示例利用对象展开:...(segment.end == null && { end: ... })在end存在时不展开任何键,保证已有分段不被破坏——这是"条件更新"的常用写法。
3.3 触发入口
该功能与"按表达式选择分段"一样,位于分段列表的右键上下文菜单中(见 src/renderer/src/SegmentList.tsx),分别对应菜单项 "Select segments by expression" 与 "Edit segments by expression"。
四、在导出文件名模板中使用表达式
4.1 基础用法
官方文档 docs/expressions.md 说明:在导出文件名模板中,${}内部支持完整的 JavaScript 表达式,例如:
${FILENAME.toLowerCase()}模板整体会作为一个 JavaScript 模板字符串 的interpolateOutFileName:await safeishEval(`${template}`, context))。因此凡是 JS 能做的字符串处理(大小写转换、正则替换、字符串补零、数组 join 等)都能用在这里。
4.2 模板中的全部变量
以下变量在模板求值时的上下文(类型为FileNameTemplateContext)中可用,完整说明见 docs/file-name-template.md:
| 变量 | 类型 | 说明 |
|---|---|---|
${FILENAME} | string | 原文件名(不含扩展名);合并导出时为第一个源文件名 |
${FILES} | SourceFile[] | 源文件信息数组,可用表达式查询,例如${new Date(FILES[0].ctime).toISOString()} |
${EXT} | string | 输出扩展名(如.mp4) |
${EPOCH_MS} | number | 自纪元起的毫秒数,用于生成唯一文件名 |
${EXPORT_COUNT} | number | 本次启动 LosslessCut 以来的导出次数(从 1 开始) |
${FILE_EXPORT_COUNT} | number | 打开当前文件以来的导出次数 |
${SEG_LABEL} | string/string[] | 分段名称;合并模式下为数组,可用${SEG_LABEL.filter(l => l).join(',')}拼接 |
${SEG_NUM} | string | 分段序号(补零字符串,如01) |
${SEG_NUM_INT} | number | 分段序号整数,可做算术,如${SEG_NUM_INT+100} |
${SELECTED_SEG_NUM}/${SELECTED_SEG_NUM_INT} | string/number | 仅统计已选中分段的序号 |
${SEG_SUFFIX} | string | 有名称时形如-Getting_Lunch,无名称时形如-seg1 |
${CUT_FROM}/${CUT_TO}/${CUT_DURATION} | string | 起止/时长,格式hh.mm.ss.sss |
${CUT_FROM_NUM}/${CUT_TO_NUM} | number | 起止时间的数值版,可参与算术 |
${SEG_TAGS.XX} | object | 按名取分段标签;标签不存在时输出undefined,可用${SEG_TAGS.foo ?? ''}兜底 |
4.3 实战技巧与约束
- 数字补零:
${String(FILE_EXPORT_COUNT).padStart(2, '0')}(先把数字转String再padStart)。 - 必须包含唯一标识:模板至少要含一个唯一标识(如
${SEG_NUM}、${CUT_FROM}),且以${EXT}结尾,否则播放器可能无法识别文件。 - 重名自动回退:若模板为两个及以上分段生成相同文件名,LosslessCut 会自动回退到默认模板(相关校验在 src/renderer/src/util/outputNameTemplate.ts 中实现并给出中文提示)。
- 源码中定义的默认模板见同文件:defaultCutFileTemplate 为
${FILENAME}-${CUT_FROM}-${CUT_TO}${SEG_SUFFIX}${EXT},合并类模板则带有-merged-与${EPOCH_MS}。
五、按表达式过滤轨道(Select tracks by expression)
官方文档 docs/expressions.md 说明:在Tracks(轨道)对话框中,对每条轨道打开Track info(轨道信息)即可看到全部可用变量,你可以据此编写过滤器(filter)表达式,实现按条件批量启用/禁用要复制的音视频轨道。
源码实现位于 src/renderer/src/hooks/useStreamsMeta.tsx:filterEnabledStreams对每一条轨道以{ track: stream }作为上下文调用safeishEval,=== true的轨道会被保留并写入"复制轨道"集合,进而影响导出时 ffmpeg 的轨道选择。这里的track即FFprobeStream类型(含index、codec_type、codec_name、width、height、language等 ffprobe 探测出的字段),例如可以用track.codec_type === 'audio'之类的表达式批量选中全部音频轨。
六、表达式安全沙箱:求值是怎么被保护的
由于表达式是用户可控代码,LosslessCut 将其放在 Web Worker 中执行,并在 src/renderer/src/worker/evalWorker.ts 中做了多层防护,可以将其视为"安全求值(safeish eval)":
- 白名单隔离:Worker 内定义了
wl白名单,仅放行Array、Math、JSON、String、RegExp等基础全局对象;对白名单之外的全局属性,通过Object.defineProperty覆盖为抛Security Exception的 getter,从根上阻止访问window、process等敏感对象。 - 禁用网络能力:显式将
fetch置为undefined,并禁止 XHR 等网络访问,表达式无法发起外部请求。 - 防内存炸弹:覆写
Array.prototype.join,当数组长度超过 500 时直接抛异常,防止类似Array(5000000000).join(...)拖垮浏览器标签页。 - 错误回传:表达式抛出的任何错误都会以字符串形式回传主线程并展示在对话框中(见 src/renderer/src/worker/eval.ts)。
需要说明的是,这套机制旨在防误操作与基本隔离,从注释(safeishEval的命名与 TODO 注释"todo terminate() and recreate in case of error?")可以推断它并非严格的完全沙箱,因此请只运行你自己信任的表达式。
七、小结
LosslessCut 的 JavaScript 表达式功能把"批量选择—批量编辑—导出命名—轨道过滤"四个高频操作统一到了同一种能力模型下:在对话框里输入一条返回值的 JS 表达式,配合文档级(docs/expressions.md)与类型级(docs/generated/types.md)的变量说明,即可对分段、文件与轨道做精细化批处理。其底层求值由 src/renderer/src/worker/eval.ts 与 src/renderer/src/worker/evalWorker.ts 组成的 Worker 沙箱完成,各功能入口与内置示例可在 src/renderer/src/hooks/useSegments.tsx、src/renderer/src/hooks/useStreamsMeta.tsx 和 src/renderer/src/util/outputNameTemplate.ts 中进一步查阅。
【免费下载链接】lossless-cutThe swiss army knife of lossless video/audio editing项目地址: https://gitcode.com/gh_mirrors/lo/lossless-cut
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考