一、为什么要在视频上显示时间戳?

说实话,我第一次接到这个需求时,觉得挺简单的——不就是把当前时间画到画面上吗?

但真正动手做才发现,这里面的坑比想象中多。你想想看,视频是连续的,每一帧都要显示正确的时间。如果只是简单地把系统时间写上去,那播放时时间就乱了。

我曾在项目里遇到过客户投诉,说监控视频回放时时间对不上。排查了半天,发现是时间戳写入的时机不对。嗯,从那以后我对这块就特别上心。

二、核心思路:MediaMuxer + MediaCodec 协同工作

要在视频上叠加时间戳,说白了就是两步:

  1. 用 MediaCodec 解码视频帧
  2. 在每一帧上绘制当前时间
  3. 再用 MediaMuxer 重新编码输出

这里有个关键点——时间不是随便取的。每一帧的时间必须和它的 PTS(显示时间戳)对应。否则播放时就会出现时间跳跃或者停滞。

核心公式:

显示时间 = 当前帧的 PTS / 1000000 (微秒转秒)

然后格式化成 HH:mm:ss.SSS 这样的字符串

三、整体架构设计

我习惯把整个流程画成一张图,这样思路更清晰。你看下面这个结构:

输入视频文件 MediaExtractor 分离视频轨道 MediaCodec 解码 输出原始帧 🎯 时间戳叠加处理 Canvas绘制当前时间 MediaCodec 编码 压缩成H.264 MediaMuxer 复用 输出带时间戳视频 PTS 时间字符串

这个流程看起来不复杂,但每一步都有讲究。我重点说说时间戳叠加这块。

四、关键代码实现

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,除非你要做复杂的特效叠加。

七、完整流程总结

嗯,到这里你应该对视频加时间戳的整体流程有数了。我最后再捋一遍:

  1. 用 MediaExtractor 读取视频文件,分离出视频轨道
  2. 用 MediaCodec 解码每一帧,拿到原始图像数据和 PTS
  3. 根据 PTS 计算出当前时间,绘制到帧上
  4. 用另一个 MediaCodec 编码器重新压缩
  5. 用 MediaMuxer 写入新的视频文件

这个流程看起来步骤多,但每一步都是必要的。我在项目里用这套方案处理过上千个视频,稳定性还是不错的。

记住最关键的一点:时间戳必须和 PTS 严格对应。这是整个功能的灵魂。


公众号:蓝海资料掘金营,微信deep3321