Skip to main content

裁剪视频并更新字幕时间

· 5 min read

ffmpeg来裁剪视频,用nodejs来处理字幕文件

几年前我在 B 站上传了纪录片《史蒂夫·乔布斯:机器人生 Steve Jobs: Man in the Machine (2015)》的最后一个片段。

当时并没有高清资源,而且也不懂视频剪切,直接用win + g录屏后上传。所以视频有好几处因加载造成的停顿,最后还有手动点击鼠标来暂停视频的画面。另外原视频用的是极其蹩脚的机翻,乔布斯翻变成了"贾布斯"。

等我知道了ffmpeg后,决定要用高清文件替换掉原视频,并上传对应的字幕文件。一番折腾后,我拿到了视频文件和字幕文件如下:

.
├── input.mkv # 超清视频,出于方便重命名为`input`
├── en.srt # 英文字幕
└── cn.srt # 简中字幕

1. 裁剪最后一节到视频片尾

ffmpeg -i input.mkv -ss 02:01:09 -c copy cut.mp4

注意:这里剪切的开始时间没有设置毫秒,之前加了毫秒后发现裁剪出来的视频上传到 B 站播放时会直接跳秒,可能是帧数出现问题了。

2. 截取字幕文件并更新每条字幕的开始结束时间

a. 在当前文件下创建 js 项目

npm init -y
npm add tsx
npm add @types/node
touch index.ts

package.json增加一条命令

package.json
"scripts": {
"start": "tsx index.ts",
}

b. 截取字幕并更新时间

index.ts
import fs from "fs"
import path from "path"

const srtNames = ["en", "cn"]
srtNames.forEach(srtUpdate)

function srtUpdate(filename: string) {
const srtContextText = fs.readFileSync(
path.resolve(__dirname, filename + ".srt"),
"utf16le"
)

const timeSeperator = " --> "
let startIndex: number | undefined

// 解析字幕文件内容为字幕对象数组
const subtitles = srtContextText
.split("\r\n\r\n")
.filter((v) => v)
.map((subtitle) => {
const [index, time, text] = subtitle.split("\r\n")
const [start, end] = time
.split(timeSeperator)
.map((time) => timeToMs(time))
return { index, start, end, text }
})

// 视频开始时间
const startTime = timeToMs("02:01:09,000")
// 字幕开始截取时间;截取后视频的第一个字幕有点多余,要去掉,以它为开始
let startCutTime: number | undefined

// 找到开始时间大于指定时间的字幕对象,拼接文本
let textAfterStartTime = ""
for (const { text, start, end, index } of subtitles) {
if (start > startTime) {
if (startCutTime == undefined) startCutTime = start

if (startCutTime != undefined && start > startCutTime) {
if (startIndex == undefined) startIndex = +index
// 重排弹幕索引
const srtIndex = +index - startIndex + 1
// 更新弹幕开始结束时间
const timeText = [start, end]
.map((v) => formatTime(v - startTime))
.join(timeSeperator)
// 移除`{\an8}`这样的弹幕定位标识
const textReplaced = text.replace(/\{\\an\d+\}/g, "")

textAfterStartTime +=
[srtIndex, timeText, textReplaced].join("\r\n") + "\r\n".repeat(2)
}
}
}

// 将拼接后的字幕文本写入输出文件
fs.writeFileSync(`${filename}_updated.srt`, Buffer.from(textAfterStartTime))
}

/**
* 将时间字符串转换为毫秒数
* @example `00:00:00.001` -> `1`
*/
function timeToMs(time: string) {
const [hh, mm, ssms] = time.split(":")
const [ss, ms] = ssms.split(",")
return (
parseInt(hh) * 3600000 +
parseInt(mm) * 60000 +
parseInt(ss) * 1000 +
parseInt(ms)
)
}

/**
* 将毫秒转换为时间字符串
* @example `1` -> `00:00:00.001`
*/
function formatTime(milliseconds: number) {
const seconds = Math.floor(milliseconds / 1000)
const minutes = Math.floor(seconds / 60)
const hours = Math.floor(minutes / 60)

const ms = milliseconds % 1000
const ss = seconds % 60
const mm = minutes % 60

return `${hours.toString().padStart(2, "0")}:${mm
.toString()
.padStart(2, "0")}:${ss.toString().padStart(2, "0")},${ms
.toString()
.padStart(3, "0")}`
}

c. 运行命令npm start

得到更新后的字幕文件:

1
00:00:03,010 --> 00:00:05,340
苹果很大

2
00:00:05,510 --> 00:00:09,800
在此时此刻 是全球最大的企业

3
00:00:12,270 --> 00:00:15,850
但每一次我们看到乔布斯 都好像变小了一点

...

完美 🥳,可以直接去稿件管理更换视频并上传字幕文件了。

来欣赏最后成果吧: