视频截取片段

在时间条上拖出需要的那一段,用 MediaRecorder 录成视频文件下载。录制是实时的:截 10 秒就要花 10 秒,期间页面别切到后台。

视频只在这台浏览器里解码和重新编码,不上传也不联网。能读什么格式取决于浏览器自带的解码器,mp4(H.264)最稳。

选择视频后可以圈定区间

使用说明

这一页做的不是「裁一刀」,而是重新录一遍:先让播放器播放选中的那一段,同时用 captureStream 把画面(以及可选的音轨)接成一条直播流,交给 MediaRecorder 按你选的码率编码,停止后再把收到的数据块拼成文件下载。之所以绕这么大一圈,是因为浏览器里没有「只复制某一段码流」的能力——视频里的预测帧依赖前面的帧才能解出来,想从中间切开而不重编码,得解析容器和帧索引再重写索引表,那是 ffmpeg 那种工具做的事。

由此带出这个工具最重要的两个后果。第一,录制是实时的:MediaRecorder 按墙上时钟一帧一帧收画面,截 10 秒就一定要花 10 秒左右,它不是「导出」而是「录制」。第二,输出一定是重新编码的有损结果,画质由码率决定而不是原始画质:2 Mbps 相当于每秒写进约 250 KB,5 Mbps 约 625 KB,10 Mbps 约 1.25 MB,上面的结果区会把实际体积、实际时长和实际容器一并列出来,方便和估算值对照。

输出格式没法由页面决定,只能用 MediaRecorder.isTypeSupported 逐个探测:候选顺序是先 video/mp4(H.264 画面加 AAC 音轨),再退到 video/webm(VP9 或 VP8 加 Opus)。Chrome 和 Safari 能命中前者,Firefox 目前只支持后者,所以同一段视频在不同浏览器上可能一个出 MP4、一个出 WebM。界面上的「输出容器」显示的就是探测结果,下载文件的扩展名跟着实际产出一致,不会出现改了后缀却打不开的文件。

画面需要缩放时(选了小于源分辨率的宽度),播放器自己捕获出来的流恒为原始分辨率,改不了尺寸,于是先把每一帧画到画布上,再把画布的流交给录制器;音轨则从播放器那条流上单独接过来,两边合成一条流,所以缩放之后声音还在。要注意 captureStream 取的是播放器的输出,静音很可能把音轨一起压掉,所以勾选保留音轨时录制期间会外放声音,录完再还原原来的静音状态。

起止点会有一点点误差:seek 只能定位到解码器能找到的位置,停止判定则同时挂在动画帧回调(前台每帧 16 到 33 毫秒)和一个 250 毫秒的定时器上,MediaRecorder 又是在帧边界上收尾,几项加起来通常是几十毫秒。外挂定时器是为了别把判定压在动画帧回调上:那个回调在后台标签页会被节流到每秒一两次,只靠它的话进度会冻住、收尾也会拖后几秒。判定不用 timeupdate,是因为它的触发间隔在 150 到 250 毫秒之间,对几秒的片段来说太粗了。

截出来的片段通常还要接着用:只要画面不要声音,可以用视频截图取帧导出单张 PNG,或者用视频转 GIF做成动图;想在剪辑软件里先挑位置,用视频缩略图九宫格拼一张预览图;只要声音的话用视频提取音频把音轨单独导成 WAV。

常见问题

为什么截 10 秒要等 10 秒?

因为 MediaRecorder 录的是一条实时流:它按墙上时钟一帧一帧地收画面,浏览器没有「跳着编码」的接口,没法把 30 秒的内容在 1 秒里写完。所以截取多长就要花多长时间,期间页面要一直开着。切到后台不至于让录制断掉,进度和收尾也不会卡住(判定除了动画帧回调还挂了一个 250 毫秒的定时器兜底),但后台会掉帧:后台的动画帧回调和定时器都会被钳到大约一秒一次,画布那条流靠定时器补画,也就只有每秒一两帧。想秒级完成又不在乎命令行,用 ffmpeg 的 -ss 和 -to 参数做无损截取。

为什么有时导出的是 WebM 而不是 MP4?

因为录制格式由浏览器决定,页面只能用 MediaRecorder.isTypeSupported 去问。Chrome 和 Safari 支持把流直接录成 MP4,会优先选它;Firefox 目前只认 WebM,所以只能给 WebM。界面上的「输出容器」一栏显示的就是实际探测到的结果,下载文件的扩展名也跟着它走,这样不会出现改了后缀却打不开的情况。

画质会变差吗?

会,因为这是重新编码而不是原样复制:录下来的画面要按你选的码率再压一遍。2 Mbps 大约是每秒 250 KB 的数据量,适合 720p 的屏幕录制和网课;10 Mbps 是每秒 1.25 MB,留给 1080p 的动作画面和还要再剪辑的素材。反过来,原视频本身只有 1 Mbps 时选 10 Mbps 也不会变清晰,只是把已经损失过的画面用更大的体积再存一遍。

缩放过之后声音还在吗?

在(前提是勾选了保留音轨)。视频元素自己捕获出来的流是原始分辨率,改不了尺寸,所以只要选了缩放,画面就先画到画布上再录;声音则从播放器那条流上单独接过来,合成的流里既有缩放后的画面也有音轨。要提醒的是录制期间视频会外放声音:捕获的是播放器的输出,静音很可能把音轨一起压掉,所以想保留声音就不能静音。

为什么录出来的时长和填的差几十毫秒?

三个来源叠加:一是 seek 只能定位到解码器能找到的位置,通常落在目标点前后一帧内;二是停止判定有两条:动画帧回调(前台每帧约 16 到 33 毫秒)和一个 250 毫秒的定时器兜底,两条谁先发现就到谁那儿收尾;三是 MediaRecorder 会在帧边界上收尾,不同容器记录时长的方式也不一样。所以起止点有几十毫秒的浮动属于正常,要做到帧级精确得用 ffmpeg 的 -c copy 或者逐帧重编码。

页面必须一直开着吗?

必须。录制接的是实时流,页面关掉、播放器被暂停,录制就断了(暂停会被当成提前收尾,结果里只有已经录到的那部分)。切到后台录制不会断,进度和收尾也不会卡住,但会掉帧:后台的动画帧回调和定时器都会被钳到大约一秒一次,视频元素那条流帧率会明显下降,画布那条流靠定时器补画也只有每秒一两帧。要录长片段、又在意画面流畅度,就把这个标签页留在前台,别同时开一堆吃 CPU 的页面。

为什么不能只裁一刀、不重新编码?

因为浏览器里没有「只复制某一段码流」的能力。视频里的帧分关键帧和预测帧,预测帧要依赖前面的帧才能解出来,想从中间切开而不重编码,得解析容器与帧索引、再重写索引表,这是 ffmpeg 那种工具做的事。浏览器只能先解码再编码,所以这个工具一定会重新编码,也一定是有损的——画质由码率决定,而不是原始画质。

截出来的片段还想再小一点怎么办?

两条路:把码率降到 2 Mbps、宽度缩到 640 或 854 像素,体积大致按比例往下掉;或者先用这里截出真正需要的部分,再拿去别处二次压缩。如果只要画面不要声音,可以改用视频转 GIF 做动图,或者用视频截图取帧导出单张 PNG,体积会小一个量级。

相关:视频缩略图九宫格视频截图取帧视频提取音频