TS转MP4后帧数量异常:原30帧转后仅28/27帧的问题求助
问题背景
将TS视频转为MP4后,帧数量出现明显差异:
- 原TS视频共30帧,OpenCV可完整读取
- 转换后的MP4经ffprobe检测仅28帧,OpenCV读取仅27帧
转换命令
ffmpeg -i test.ts -fflags +genpts -c copy -y test.mp4
帧数量检测命令及结果
- 检测TS帧数量:
ffprobe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets -of csv=p=0 tmp.ts
返回结果:30
- 检测MP4帧数量:
ffprobe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets -of csv=p=0 tmp.mp4
返回结果:28
OpenCV读取代码及结果
cap = cv2.VideoCapture(tmp_path) readMat = [] while cap.isOpened(): ret, frame = cap.read() if not ret: break readMat.append(frame)
- TS文件读取30帧,MP4仅读取27帧
转换日志
ffmpeg version N-111746-gd53acf452f Copyright (c) 2000-2023 the FFmpeg developers built with gcc 11.3.0 (GCC) configuration: --ld=g++ --bindir=/bin --extra-libs='-lpthread -lm' --pkg-config-flags=--static --enable-static --enable-gpl --enable-libaom --enable-libass --enable-libfreetype --enable-libmp3lame --enable-libopus --enable-libsvtav1 --enable-libdav1d --enable-libvorbis --enable-libvpx --enable-libx264 --enable-libx265 --enable-nonfree --enable-cuda-nvcc --enable-cuvid --enable-nvenc --enable-libnpp libavutil 58. 16.101 / 58. 16.101 libavcodec 60. 23.100 / 60. 23.100 libavformat 60. 10.100 / 60. 10.100 libavdevice 60. 2.101 / 60. 2.101 libavfilter 9. 10.100 / 9. 10.100 libswscale 7. 3.100 / 7. 3.100 libswresample 4. 11.100 / 4. 11.100 libpostproc 57. 2.100 / 57. 2.100 [mpegts @ 0x4237240] DTS discontinuity in stream 0: packet 5 with DTS 306003, packet 6 with DTS 396001 Input #0, mpegts, from 'tmp.ts': Duration: 00:00:21.33, start: 3.400000, bitrate: 15 kb/s Program 1 Metadata: service_name : Service01 service_provider: FFmpeg Stream #0:0[0x100]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(progressive), 300x300, 1 fps, 3 tbr, 90k tbn Output #0, mp4, to 'test.mp4': Metadata: encoder : Lavf60.10.100 Stream #0:0: Video: h264 (High) (avc1 / 0x31637661), yuv420p(progressive), 300x300, q=2-31, 1 fps, 3 tbr, 90k tbn Stream mapping: Stream #0:0 -> #0:0 (copy) Press [q] to stop, [?] for help [out#0/mp4 @ 0x423e280] video:25kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 4.192123% frame= 30 fps=0.0 q=-1.0 Lsize= 26kB time=00:00:21.00 bitrate= 10.3kbits/s speed=1e+04x
TS视频生成方式
该TS视频由以下Python类的update方法拼接静态图片生成(补充了原代码缺失的必要导入):
import cv2 import os import subprocess import shutil import logging import numpy as np from tempfile import NamedTemporaryFile from typing import Optional LOGGER = logging.getLogger(__name__) class VideoUpdater: def __init__( self, video_path: str, framerate: int, timePerFrame: Optional[int] = None ): """ Video updater takes in a video path, and updates it using a supplied frame, based on a given framerate. Args: video_path: str: Specify the path to the video file framerate: int: Set the frame rate of the video """ if not video_path.endswith(".mp4"): LOGGER.warning( f"File type {os.path.splitext(video_path)[1]} not supported for streaming, switching to ts" ) video_path = os.path.splitext(video_path)[0] + ".mp4" self._ps = None self.env = {} self.ffmpeg = "/usr/bin/ffmpeg " self.video_path = video_path self.ts_path = video_path.replace(".mp4", ".ts") self.tfile = None self.framerate = framerate self._video = None self.last_frame = None self.curr_frame = None def update(self, frame: np.ndarray): if len(frame.shape) == 2: frame = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR) else: frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) self.writeFrame(frame) def writeFrame(self, frame: np.ndarray): """ The writeFrame function takes a frame and writes it to the video file. Args: frame: np.ndarray: Write the frame to a temporary file """ tImLFrame = NamedTemporaryFile(suffix=".png") tVidLFrame = NamedTemporaryFile(suffix=".ts") cv2.imwrite(tImLFrame.name, frame) ps = subprocess.Popen( self.ffmpeg + rf"-loop 1 -r {self.framerate} -i {tImLFrame.name} -t {self.framerate} -vcodec libx264 -pix_fmt yuv420p -y {tVidLFrame.name}", env=self.env, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) ps.communicate() if os.path.isfile(self.ts_path): # this does not work to watch, as timestamps are not updated ps = subprocess.Popen( self.ffmpeg + rf'-i "concat:{self.ts_path}|{tVidLFrame.name}" -c copy -y {self.ts_path.replace(".ts", ".bak.ts")}', env=self.env, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) ps.communicate() shutil.move(self.ts_path.replace(".ts", ".bak.ts"), self.ts_path) else: shutil.copyfile(tVidLFrame.name, self.ts_path) # fixing timestamps, we dont have to wait for this operation ps = subprocess.Popen( self.ffmpeg + rf"-i {self.ts_path} -fflags +genpts -c copy -y {self.video_path}", env=self.env, shell=True, # stdout=subprocess.PIPE, # stderr=subprocess.PIPE, ) tImLFrame.close() tVidLFrame.close()
核心原因分析
1. TS文件存在DTS时间戳不连续
从转换日志的警告[mpegts @ 0x4237240] DTS discontinuity in stream 0可知,原TS文件内部解码时间戳(DTS)存在跳变。TS是流媒体容器,对时间戳容错性高,播放器/读取库可通过逻辑跳过异常;但MP4作为本地容器,ffmpeg在流拷贝时会过滤不符合MP4时间戳规范的帧,导致帧数量减少。
2. TS片段拼接时的时间戳冲突
VideoUpdater生成单帧TS片段的命令参数不合理:-loop 1 -r {self.framerate} -t {self.framerate}会生成时长等于帧率的视频(比如帧率为1时生成1秒1帧,帧率为3时生成3秒3帧),而非预期的单帧。拼接时直接用concat协议拷贝流,每个片段的时间戳独立(如第二个片段DTS从0开始),导致拼接后时间戳混乱,即使后续用+genpts生成PTS,也无法修复DTS本身的不连续问题。
3. OpenCV读取MP4的额外丢失
OpenCV的VideoCapture依赖ffmpeg后端,但它的读取逻辑偏向按时间戳顺序采样,而非严格读取所有帧。当MP4内部存在时间戳异常时,OpenCV可能跳过帧或提前终止读取,导致比ffprobe检测的帧数量更少。
4. 异步时间戳修复的隐患
原代码中修复时间戳的ffmpeg进程是异步启动的(未调用ps.communicate()等待完成),可能导致TS文件未完成修复就被读取,时间戳异常未被修正。
解决建议
- 修复单帧TS生成命令:改为每次生成1帧的TS片段,参数调整为:
ffmpeg -loop 1 -r 1 -i {tImLFrame.name} -t 1 -vcodec libx264 -pix_fmt yuv420p -y {tVidLFrame.name}
- 拼接时重新编码修复时间戳:避免流拷贝,重新编码让ffmpeg生成连续时间戳:
ffmpeg -i "concat:{self.ts_path}|{tVidLFrame.name}" -vcodec libx264 -pix_fmt yuv420p -y {self.ts_path.replace(".ts", ".bak.ts")}
- 转MP4时强制重置时间戳:添加
+igndts忽略原有异常DTS,强制生成连续PTS:
ffmpeg -i test.ts -fflags +igndts +genpts -c copy -y test.mp4
- 同步执行时间戳修复:将修复时间戳的ffmpeg进程改为同步执行,确保操作完成后再进行后续步骤。
内容的提问来源于stack exchange,提问作者Vasilis Lemonidis

