一、为什么要在视频上显示时间戳?
说实话,我第一次接到这个需求时,觉得挺简单的——不就是把当前时间画到画面上吗?
但真正动手做才发现,这里面的坑比想象中多。你想想看,视频是连续的,每一帧都要显示正确的时间。如果只是简单地把系统时间写上去,那播放时时间就乱了。
我曾在项目里遇到过客户投诉,说监控视频回放时时间对不上。排查了半天,发现是时间戳写入的时机不对。嗯,从那以后我对这块就特别上心。
二、核心思路:MediaMuxer + MediaCodec 协同工作
要在视频上叠加时间戳,说白了就是两步:
- 用 MediaCodec 解码视频帧
- 在每一帧上绘制当前时间
- 再用 MediaMuxer 重新编码输出
这里有个关键点——时间不是随便取的。每一帧的时间必须和它的 PTS(显示时间戳)对应。否则播放时就会出现时间跳跃或者停滞。
核心公式:
显示时间 = 当前帧的 PTS / 1000000 (微秒转秒)
然后格式化成 HH:mm:ss.SSS 这样的字符串
三、整体架构设计
我习惯把整个流程画成一张图,这样思路更清晰。你看下面这个结构:
这个流程看起来不复杂,但每一步都有讲究。我重点说说时间戳叠加这块。
四、关键代码实现
4.1 获取帧的PTS
解码时,每一帧都会带一个 bufferInfo,里面就有 presentationTimeUs。这个值就是微秒级的时间戳。
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 10000);
if (outputIndex >= 0) {
// 获取当前帧的微秒时间戳
long ptsMicros = bufferInfo.presentationTimeUs;
// 转换成毫秒
long ptsMillis = ptsMicros / 1000;
// 格式化成时间字符串
String timeText = formatTime(ptsMillis);
}
4.2 在帧上绘制时间
拿到时间字符串后,我们需要把它画到图像上。这里我用的是 Canvas + SurfaceTexture 的方式。
private Bitmap drawTimestampOnFrame(Bitmap frameBitmap, String timeText) {
Bitmap result = frameBitmap.copy(Bitmap.Config.ARGB_8888, true);
Canvas canvas = new Canvas(result);
// 设置画笔
Paint paint = new Paint();
paint.setColor(Color.WHITE);
paint.setTextSize(48);
paint.setAntiAlias(true);
paint.setShadowLayer(4, 2, 2, Color.BLACK);
// 在右上角绘制时间
float x = canvas.getWidth() - 280;
float y = 80;
canvas.drawText(timeText, x, y, paint);
return result;
}
private String formatTime(long millis) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault());
// 注意:这里的时间基准是视频开始时间
long baseTime = videoStartTime + millis;
return sdf.format(new Date(baseTime));
}
⚠️ 重要提醒:
时间基准一定要搞清楚。视频里的时间戳是相对时间(从0开始),而我们要显示的是绝对时间。所以需要加上视频的起始录制时间。
我曾经犯过这个错——直接把PTS当绝对时间用,结果视频播放时时间显示的是1970年...那场面,尴尬得很。
4.3 重新编码并写入MediaMuxer
绘制完成后,需要把 Bitmap 重新编码成视频帧,然后交给 MediaMuxer 写入。
// 将Bitmap转成YUV或直接使用Surface输入
// 这里以Surface输入为例
MediaCodec encoder = MediaCodec.createEncoderByType(MediaFormat.MIME_TYPE_AVC);
MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIME_TYPE_AVC, width, height);
format.setInteger(MediaFormat.KEY_BIT_RATE, 2000000);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface inputSurface = encoder.createInputSurface();
encoder.start();
// 将绘制好的帧通过Surface送入编码器
Canvas canvas = inputSurface.lockCanvas(null);
// ... 绘制时间戳
inputSurface.unlockCanvasAndPost(canvas);
// 编码器输出
MediaCodec.BufferInfo outInfo = new MediaCodec.BufferInfo();
int outputIndex = encoder.dequeueOutputBuffer(outInfo, 10000);
if (outputIndex >= 0) {
ByteBuffer outputData = encoder.getOutputBuffer(outputIndex);
// 写入MediaMuxer
mediaMuxer.writeSampleData(trackIndex, outputData, outInfo);
encoder.releaseOutputBuffer(outputIndex, false);
}
五、避坑指南
做这个功能,有几个地方特别容易出问题。我一个个说:
💡 经验之谈:
- 帧率匹配:解码和编码的帧率必须一致。我习惯用30fps,这样时间显示比较平滑。
- 颜色格式:MediaCodec 默认输出的是 YUV 格式,但 Canvas 操作需要 ARGB。记得做格式转换。
- 内存管理:每一帧都创建 Bitmap 很吃内存。我建议用 BitmapPool 复用。
⚠️ 我曾经踩过的坑:
有一次我发现时间显示总是慢半拍。查了半天,原来是编码器内部有缓存队列,导致时间戳和实际帧不对应。解决方案是在编码前手动设置正确的 PTS:
// 手动设置PTS,确保和原始帧一致
outInfo.presentationTimeUs = originalPtsMicros;
mediaMuxer.writeSampleData(trackIndex, outputData, outInfo);
六、性能优化建议
实时叠加时间戳对性能有要求。特别是高分辨率视频,每一帧都要做图像处理。我个人的经验是:
| 分辨率 | 建议做法 | 预期性能 |
|---|---|---|
| 720p 以下 | 直接 Bitmap 操作 | 流畅 |
| 1080p | 使用 Surface 直接绘制 | 较流畅 |
| 4K | 建议用 OpenGL 或 Shader | 需要优化 |
说白了,如果只是加个时间戳,用 Surface 方式就够了。没必要上 OpenGL,除非你要做复杂的特效叠加。
七、完整流程总结
嗯,到这里你应该对视频加时间戳的整体流程有数了。我最后再捋一遍:
- 用 MediaExtractor 读取视频文件,分离出视频轨道
- 用 MediaCodec 解码每一帧,拿到原始图像数据和 PTS
- 根据 PTS 计算出当前时间,绘制到帧上
- 用另一个 MediaCodec 编码器重新压缩
- 用 MediaMuxer 写入新的视频文件
这个流程看起来步骤多,但每一步都是必要的。我在项目里用这套方案处理过上千个视频,稳定性还是不错的。
记住最关键的一点:时间戳必须和 PTS 严格对应。这是整个功能的灵魂。
公众号:蓝海资料掘金营,微信deep3321