You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android中混流或编码后为视频帧烧录时间戳文本的实现方案

Android视频帧烧录时间戳方案(含原生API与FFmpeg实现)

不使用FFmpeg的实现方案

可以通过Android原生API实现,核心是在**原始像素帧(YUV/RGB)**上绘制时间戳,再进行编码混流——因为编码后的H.264压缩数据无法直接修改像素内容。

场景1:处理编码前的原始YUV帧(推荐,性能更高)

如果你的流程能拿到编码前的原始YUV帧(比如RTSP解码后的帧),直接对YUV数据做文本绘制:

  • 高效方式:用RenderScript或Native层算法直接修改YUV分量(比如NV12格式的Y通道),计算文本区域的像素值并替换,避免Bitmap转换的性能开销。
  • 简易方式:将YUV转成Bitmap,用Canvas绘制时间戳后再转回YUV,适合低分辨率或非实时场景。

场景2:处理已编码的H.264帧

如果你的frame.getData()是H.264编码后的NAL单元,需要先解码为YUV,绘制时间戳后重新编码:

  1. 用MediaCodec创建解码器,输入H.264数据解码得到YUV帧。
  2. 对YUV帧执行时间戳绘制(同上两种方式)。
  3. 用MediaCodec编码器将处理后的YUV重新编码为H.264,再送入混流逻辑。

使用FFmpeg的实现方案

FFmpeg的drawtext滤镜可以快速实现时间戳烧录,支持实时混流和离线处理两种模式。

方式1:实时混流阶段烧录

通过FFmpeg滤镜链直接在编码/混流过程中绘制时间戳:

  • 命令行示例(适用于提前保存的H.264文件或RTSP拉流):
    ffmpeg -i input.h264 -vf "drawtext=fontfile=/system/fonts/Roboto-Regular.ttf:text='%{localtime\:%Y-%m-%d %H\\\:%M\\\:%S}':x=10:y=10:fontsize=24:fontcolor=white:shadowcolor=black:shadowx=2:shadowy=2" -c:v libx264 output.mp4
    
    参数说明:
    • fontfile:指定Android系统字体路径(如/system/fonts/Roboto-Regular.ttf)
    • text:时间戳格式,%{localtime:...}表示用本地时间,也可替换为自定义时间字符串
    • x/y:文本绘制坐标,fontsize/fontcolor控制字体样式,shadow*添加阴影提升可读性
  • 代码集成:通过FFmpeg API初始化滤镜图,将drawtext加入视频处理链,读取输入帧后经滤镜处理再编码混流。

方式2:编码完成后离线烧录

如果已保存无时间戳的MP4和时间戳文件(每行对应一帧的时间字符串),可通过textfile参数读取时间戳:

ffmpeg -i input.mp4 -vf "drawtext=fontfile=/system/fonts/Roboto-Regular.ttf:textfile=timestamps.txt:x=10:y=10:fontsize=24:fontcolor=white" -c:a copy output.mp4

针对你现有代码的修改建议

你的当前代码直接处理编码后的帧,要实现时间戳烧录,需在编码前插入帧处理逻辑:

private void muxFrame(FrameQueue.Frame frame, int encodeIndex, boolean isVideoEOS) {
    // 1. 先处理原始帧,绘制时间戳
    byte[] processedFrameData = drawTimestampOnFrame(frame.getData(), frame.getTimestamp());
    
    long presentationTimeUs = computePresentationTimeNsec(encodeIndex);

    int inputBufferIndex = codec.dequeueInputBuffer(-1);
    if (inputBufferIndex >= 0) {
        ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferIndex);
        inputBuffer.put(processedFrameData); // 送入处理后的帧数据

        codec.queueInputBuffer(inputBufferIndex, frame.getOffset(), processedFrameData.length, frame.getTimestamp(), 0);
    }

    // 后续混流逻辑保持不变...
}

// 简易实现:YUV转Bitmap绘制时间戳(注意:实时场景建议用RenderScript优化)
private byte[] drawTimestampOnFrame(byte[] yuvData, long timestampUs) {
    int width = 1280; // 替换为你的视频实际宽度
    int height = 720; // 替换为你的视频实际高度
    
    // NV21转Bitmap
    YuvImage yuvImage = new YuvImage(yuvData, ImageFormat.NV21, width, height, null);
    ByteArrayOutputStream jpegOut = new ByteArrayOutputStream();
    yuvImage.compressToJpeg(new Rect(0, 0, width, height), 100, jpegOut);
    Bitmap bitmap = BitmapFactory.decodeByteArray(jpegOut.toByteArray(), 0, jpegOut.size());
    
    // 绘制时间戳
    Canvas canvas = new Canvas(bitmap);
    Paint paint = new Paint();
    paint.setColor(Color.WHITE);
    paint.setTextSize(48);
    paint.setShadowLayer(2, 2, 2, Color.BLACK);
    String timeText = formatTimestamp(timestampUs);
    canvas.drawText(timeText, 20, 60, paint);
    
    // Bitmap转回NV21(示例,实际需避免JPEG压缩损失)
    ByteArrayOutputStream yuvOut = new ByteArrayOutputStream();
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, yuvOut);
    bitmap.recycle();
    return yuvOut.toByteArray();
}

private String formatTimestamp(long timestampUs) {
    long timestampMs = timestampUs / 1000;
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault());
    return sdf.format(new Date(timestampMs));
}

内容的提问来源于stack exchange,提问作者Charles Semaan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 04:00:46