news 2026/5/13 3:11:31

Android 音视频实战:基于SmartMediakit实现RTSP/RTMP高性能透传、二次编码与动态水印

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android 音视频实战:基于SmartMediakit实现RTSP/RTMP高性能透传、二次编码与动态水印

在移动端音视频开发中,我们经常面临一个架构抉择:是追求极致的低延迟(如无人机图传、实时指挥),还是追求丰富的功能处理(如加水印、AI分析、画中画)?

通常,实现前者需要“透传(Relay)”,避免编解码的耗时;实现后者需要“转码(Transcoding)”,需要获取 YUV/RGB 数据。

本文将结合SmartPlayer.java核心代码,深入剖析如何利用大牛直播 SDK (SmartPlayer + SmartPublisher)的灵活性,在同一套架构中同时实现这两种截然不同的业务场景,并构建一个支持 RTSP/RTMP 拉流、推流、录像及轻量级 RTSP 服务的全能终端。

核心架构设计:Wrapper 模式与事件驱动

为了保证业务逻辑与底层 SDK 的解耦,以及多线程环境下的稳定性,我们在SmartPlayer与 Native JNI 之间构建了一层封装。

  • LibPlayerWrapper: 负责播放控制、参数配置及数据回调的线程安全封装。

  • LibPublisherWrapper: 负责推流、录像、RTSP 服务及图层处理的封装。

  • EventListener: 将底层的状态回调(连接成功、断开、快照结果等)透传至 UI 层,实现逻辑与视图分离。

SmartPlayer.javastartPlayLogic方法中,我们根据业务需求(isRelayMode)决定数据流向。这是整个系统的“路由”中心。如果不需要二次编码,那么点“开始播放”,我们只是做预览播放。

private boolean startPlayLogic() { if (isPlaying) return false; if (!mPlayerWrapper.open()) return false; // 设置通用参数 mPlayerWrapper.setUrl(mPlaybackUrl); mPlayerWrapper.setSurface(mSurfaceView); mPlayerWrapper.setRenderScaleMode(1); mPlayerWrapper.setFastStartup(true); mPlayerWrapper.setAudioOutputType(1); mPlayerWrapper.setMute(isMute); mPlayerWrapper.setRotation(mRotateDegrees); mPlayerWrapper.setRTSPConfig(10, 1); if (!isRelayMode) { Log.i(TAG, "二次编码模式: 设置 ExternalRender"); mPlayerWrapper.setExternalRender(new I420ExternalRender(mPublisherArray)); } // 硬解配置 mPlayerWrapper.setHWDecoder(isHardwareDecoder, isHardwareDecoder); if (!mPlayerWrapper.startPlay()) { Log.e(TAG, "StartPlay failed"); mPlayerWrapper.close(); return false; } isPlaying = true; return true; }

如果需要透传转发,调用startPullLogic()/stopPullLogic():

private boolean startPullLogic() { if (isPulling) return false; if (!mPlayerWrapper.open()) return false; mPlayerWrapper.setUrl(mPlaybackUrl); mPlayerWrapper.setRTSPConfig(10, 1); // 拉流模式强制设置数据回调 mPlayerWrapper.setAudioDataCallback(new PlayerAudioDataCallback(mStreamPublisher)); mPlayerWrapper.setPullStreamAudioTranscodeAAC(true); mPlayerWrapper.setVideoDataCallback(new PlayerVideoDataCallback(mStreamPublisher)); if (!mPlayerWrapper.startPullStream()) { if (!isPlaying) mPlayerWrapper.close(); return false; } isPulling = true; return true; } private void stopPullLogic() { if (!isPulling) return; isPulling = false; mPlayerWrapper.stopPullStream(); if (!isPlaying) mPlayerWrapper.close(); } private void stopPlayLogic() { if (!isPlaying) return; isPlaying = false; mPlayerWrapper.stopPlay(); if (!isPulling) { mPlayerWrapper.close(); } }

场景一:高性能透传(Relay Mode)

透传模式的精髓在于“拿来主义”。我们不需要解码视频帧,而是直接从播放器底层 hook 住编码后的数据包(AVPacket),直接喂给推流器。

1.1 获取编码数据

我们需要实现NTVideoDataCallbackNTAudioDataCallback。在SmartPlayer.java中,PlayerVideoDataCallback负责将数据直接投递给LibPublisherWrapper

/* 引用自 SmartPlayer.java Inner Classes */ class PlayerVideoDataCallback implements NTVideoDataCallback { private WeakReference<LibPublisherWrapper> publisher_; private int video_buffer_size = 0; private ByteBuffer video_buffer_ = null; // ... 构造函数 ... @Override public ByteBuffer getVideoByteBuffer(int size) { // 动态管理 Buffer,复用内存,减少 GC if( size < 1 ) return null; if ( size <= video_buffer_size && video_buffer_ != null ) { return video_buffer_; } video_buffer_size = size + 1024; video_buffer_size = (video_buffer_size+0xf) & (~0xf); // 16字节对齐 video_buffer_ = ByteBuffer.allocateDirect(video_buffer_size); return video_buffer_; } @Override public void onVideoDataCallback(int ret, int video_codec_id, int sample_size, int is_key_frame, long timestamp, int width, int height, long presentation_timestamp) { if ( video_buffer_ == null) return; LibPublisherWrapper publisher = publisher_.get(); if (null == publisher || !publisher.is_publishing()) return; video_buffer_.rewind(); // 【关键】直接投递编码后的数据,不进行解码 publisher.PostVideoEncodedData(video_codec_id, video_buffer_, sample_size, is_key_frame, timestamp, presentation_timestamp); } }

1.2 推流端配置(避坑指南)

在透传模式下,推流器不需要配置编码器参数(如码率、GOP、FPS),因为它不需要编码。

private void initAndSetConfig() { if (libPublisher == null || !mStreamPublisher.empty()) return; long handle = libPublisher.SmartPublisherOpen(mContext, mAudioOpt, mVideoOpt, mVideoWidth, mVideoHeight); if (handle == 0) return; int fps = 25; int gop = fps * 2; initializePublisher(libPublisher, handle, mVideoWidth, mVideoHeight, fps, gop, isRelayMode); mStreamPublisher.set(libPublisher, handle); } private void initializePublisher(SmartPublisherJniV2 lib, long handle, int width, int height, int fps, int gop, boolean isRelay) { // 【关键修改】如果是透传模式,不需要配置编码器参数,因为数据已经是编码好的 if (!isRelay) { if (mVideoEncodeType == 1) { // HW H.264 int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, true); // 成功设置硬编后,进一步设置详细参数 if (lib.SetSmartPublisherVideoHWEncoder(handle, kbps) == 0) { lib.SetNativeMediaNDK(handle, 0); // 默认0 lib.SetVideoHWEncoderBitrateMode(handle, 1); // 1:VBR, 0:CQ lib.SetVideoHWEncoderQuality(handle, 39); // 质量参数 lib.SetAVCHWEncoderProfile(handle, 0x08); // High Profile lib.SetAVCHWEncoderLevel(handle, 0x1000); // Level 4.1 } } else if (mVideoEncodeType == 2) { // HW H.265 int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, false); lib.SetSmartPublisherVideoHevcHWEncoder(handle, kbps); lib.SetVideoHWEncoderBitrateMode(handle, 1); lib.SetVideoHWEncoderQuality(handle, 39); } else { // SW H.264 int quality = LibPublisherWrapper.estimate_video_software_quality(width, height, true); int maxKbps = LibPublisherWrapper.estimate_video_vbr_max_kbps(width, height, fps); lib.SmartPublisherSetSwVBRMode(handle, 1, quality, maxKbps); } lib.SmartPublisherSetGopInterval(handle, gop); lib.SmartPublisherSetFPS(handle, fps); lib.SmartPublisherSetAudioCodecType(handle, 1); // AAC } // 关键点:设置更新后的 EventHandlePublisherV2 lib.SetSmartPublisherEventCallbackV2(handle, new EventHandlePublisherV2(mUiHandler)); }

此外,在透传模式下,严禁启动本地音频采集(麦克风)和图层线程,否则会造成数据冲突或资源浪费。

private boolean startPushRtmpLogic() { initAndSetConfig(); if (!mStreamPublisher.SetURL(mRelayStreamUrl)) return false; if (!mStreamPublisher.StartPublisher()) { mStreamPublisher.try_release(); return false; } if (!isRelayMode) { startAudioRecorder(); startLayerPostThread(); } return true; } private void stopPushLogic() { mStreamPublisher.StopPublisher(); mStreamPublisher.try_release(); if (!mStreamPublisher.is_publishing()) { stopAudioRecorder(); stopLayerPostThread(); } }

场景二:二次编码与动态水印(Transcoding Mode)

当需要给视频加水印、跑马灯或者做画中画时,我们必须拿到YUV数据。SDK 提供了NTExternalRender接口,结合LayerPostThread实现多图层叠加。

2.1 获取 YUV 数据并投递

我们定义I420ExternalRender类,它实现了 SDK 的渲染回调。

/* 引用自 SmartPlayer.java Inner Classes */ private static class I420ExternalRender implements NTExternalRender { // ... 变量定义 ... @Override public int getNTFrameFormat() { return NT_FRAME_FORMAT_I420; // 指定回调格式为 I420 } @Override public void onNTFrameSizeChanged(int width, int height) { // 初始化 ByteBuffer,分配 Y, U, V 平面的内存 width_ = width; height_ = height; y_row_bytes_ = width; u_row_bytes_ = (width + 1) / 2; v_row_bytes_ = (width + 1) / 2; y_buffer_ = ByteBuffer.allocateDirect(y_row_bytes_ * height_); u_buffer_ = ByteBuffer.allocateDirect(u_row_bytes_ * ((height_ + 1) / 2)); v_buffer_ = ByteBuffer.allocateDirect(v_row_bytes_ * ((height_ + 1) / 2)); } @Override public void onNTRenderFrame(int width, int height, long timestamp) { // ... Buffer rewind ... if (publisher_list_ != null) { for (WeakReference<LibPublisherWrapper> ref : publisher_list_) { LibPublisherWrapper p = ref.get(); if (p != null && !p.empty()) { // 【核心】将 Player 解码后的 YUV 数据投递给 Publisher 的视频层(Layer 0) p.PostLayerImageI420ByteBuffer(0, 0, 0, y_buffer_, 0, y_row_bytes_, u_buffer_, 0, u_row_bytes_, v_buffer_, 0, v_row_bytes_, width_, height_, 0, 0, 0, 0, 0, 0); } } } } }

2.2 动态水印(多图层叠加)

LayerPostThread是一个独立的线程,用于定期生成时间戳位图、Logo 位图,并投递到 Publisher 的上层(Layer 1, Layer 2...)。SDK 内部会负责将 Layer 0 (视频) 和 Layer X (水印) 进行硬件或软件混合。

/* 引用自 LayerPostThread.java */ private void on_update_layers(List<LibPublisherWrapper> publisher_list, boolean is_run_on_thread, int w, int h) { // ... 省略部分逻辑 ... // 1. 投递时间戳水印 if (is_text_) { // 生成时间戳 Bitmap Bitmap text_bitmap = makeTextBitmap(makeTimestampString(), getFontSize(video_w), Color.argb(255, 0, 0, 0), true, Color.argb(255, 255, 255, 255),true); // 投递到指定索引的层 (timestamp_index_) for (LibPublisherWrapper i : publisher_list) i.PostLayerBitmap(timestamp_index_, 0, cur_h, text_bitmap, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); text_bitmap.recycle(); } // 2. 投递图片水印 (Logo) if (is_picture_) { // ... 获取/生成 Logo Bitmap ... for (LibPublisherWrapper i : publisher_list) i.PostLayerImageRGBA8888ByteBuffer(picture_index_, 0, cur_h, buffer, 0, bitmap.getRowBytes(), w, h, 0, 0, scale_w, scale_h, scale_filter_mode, 0); } }

注意:在二次编码模式下,我们需要在startPushRtmpLogic中调用startAudioRecorder()来采集麦克风音频,因为此时我们将画面和声音重新编码合成。


场景三:转推RTMP

前端拉取的RTSP或RTMP流,可以通过大牛直播SDK的RTMP推送模块,转推到自建RTMP服务器或CDN,相关逻辑如下:

private void handlePushRtmp() { if (mStreamPublisher.is_rtmp_publishing()) { stopPushLogic(); runOnUiThread(new Runnable() { @Override public void run() { mBtnPushRtmp.setText("推送RTMP"); } }); } else { if (startPushRtmpLogic()) { runOnUiThread(new Runnable() { @Override public void run() { mBtnPushRtmp.setText("停止推流"); } }); } } }

场景四:轻量级 RTSP 服务

除了推流到 RTMP 服务器,大牛直播SDK 还允许将 Android 设备变成一个RTSP Server,供内网其他设备直接拉流。

4.1 启动 RTSP Server

这部分逻辑在handleRtspService中:

private void handleRtspService() { if (isRTSPServiceRunning) { if (libPublisher != null && mRtspServerHandle != 0) { libPublisher.StopRtspServer(mRtspServerHandle); libPublisher.CloseRtspServer(mRtspServerHandle); mRtspServerHandle = 0; } isRTSPServiceRunning = false; runOnUiThread(new Runnable() { @Override public void run() { mBtnRtspService.setText("启动RTSP服务"); mBtnRtspPublish.setEnabled(false); } }); } else { mRtspServerHandle = libPublisher.OpenRtspServer(0); if (mRtspServerHandle == 0) return; libPublisher.SetRtspServerPort(mRtspServerHandle, 28554); if (libPublisher.StartRtspServer(mRtspServerHandle, 0) == 0) { isRTSPServiceRunning = true; runOnUiThread(new Runnable() { @Override public void run() { mBtnRtspService.setText("停止RTSP服务"); mBtnRtspPublish.setEnabled(true); } }); } else { libPublisher.CloseRtspServer(mRtspServerHandle); mRtspServerHandle = 0; } } }

4.2 发布流到 RTSP Server

启动 Server 后,我们需要将当前的 Publisher(无论是透传的还是二次编码的)挂载到 Server 上。

private void handleRtspPublish() { if (mStreamPublisher.is_rtsp_publishing()) { mStreamPublisher.StopRtspStream(); mStreamPublisher.try_release(); if (!mStreamPublisher.is_publishing()) { stopAudioRecorder(); stopLayerPostThread(); } runOnUiThread(new Runnable() { @Override public void run() { mBtnRtspPublish.setText("发布RTSP流"); mBtnRtspService.setEnabled(true); mBtnRtspSession.setEnabled(false); } }); } else { initAndSetConfig(); mStreamPublisher.SetRtspStreamName("stream1"); mStreamPublisher.ClearRtspStreamServer(); mStreamPublisher.AddRtspStreamServer(mRtspServerHandle); if (mStreamPublisher.StartRtspStream()) { // 【关键】透传模式不启动本地采集 if (!isRelayMode) { startAudioRecorder(); startLayerPostThread(); } runOnUiThread(new Runnable() { @Override public void run() { mBtnRtspPublish.setText("停止RTSP流"); mBtnRtspService.setEnabled(false); mBtnRtspSession.setEnabled(true); } }); } } }

场景五:本地录像

录像功能与推流功能是解耦的。我们可以只录像不推流,也可以边推流边录像。底层支持自动切片(分段保存)。

private void handleRecord() { if (mStreamPublisher.is_recording()) { mStreamPublisher.StopRecorder(); mStreamPublisher.try_release(); if (!mStreamPublisher.is_publishing()) { stopAudioRecorder(); stopLayerPostThread(); } isPauseRecording = true; runOnUiThread(new Runnable() { @Override public void run() { mBtnRecord.setText("录像"); mBtnPauseRecord.setText("暂停"); mBtnPauseRecord.setEnabled(false); } }); } else { initAndSetConfig(); configRecorderParam(); if (mStreamPublisher.StartRecorder()) { // 【关键】透传模式不启动本地采集 if (!isRelayMode) { startAudioRecorder(); startLayerPostThread(); } isPauseRecording = true; runOnUiThread(new Runnable() { @Override public void run() { mBtnRecord.setText("停止录像"); mBtnPauseRecord.setEnabled(true); } }); } } }

总结

通过对SmartRelayDemo的深度剖析,我们看到了一套成熟的移动端音视频解决方案。它不仅解决了单一的“播放”或“推流”问题,更通过灵活的架构设计,完美覆盖了从低延迟传输边缘计算处理的多样化需求。

+-----------------------------------------------------------------------+ | Android 音视频网关 (SmartPlayer) | +-----------------------------------------------------------------------+ | v +------------------------[ 输入源 (Input) ]-----------------------------+ | | | [ 网络流 (RTSP/RTMP) ] [ 麦克风 (AudioRecord) ] | | | | | +-------------|-------------------------------------|-------------------+ | | v | (仅二次编码模式启用) +---[ 播放器封装 (LibPlayerWrapper) ] | | | | | v | | < 模式判断 (isRelayMode?) > | | | | | +--------+---------+ | | | (YES: 透传) | (NO: 二次编码) | | | | | | v v | | [回调 Encoded Data] [解码 & 回调 YUV 数据] | | (H.264/AAC 数据包) (I420ExternalRender) | | | | | | | v | | | [ 图层处理 (LayerPostThread) ] | | | (叠加时间戳/Logo/AI画框) | | | | | +----|------------------|---------------------------|-------------------+ | 零拷贝直传 | YUV+水印 | 混合音频 | | v +----v------------------v-----------------------------------------------+ | 推流器封装 (LibPublisherWrapper) | +-----------------------------------------------------------------------+ | | | v v v +----------------+ +----------------+ +----------------+ | RTMP 推流 | | 轻量级 RTSP Svr| | 本地 MP4 录像 | | (CDN/服务器) | | (局域网分发) | | (切片存储) | +----------------+ +----------------+ +----------------+

以下是对该技术方案优势的升华总结:

  • 架构的灵活性(Architectural Flexibility): 通过Wrapper层与回调机制的精妙设计,开发者可以在透传模式(Relay)与转码模式(Transcoding)之间毫秒级切换。既能满足无人机图传对 100-200ms 级低延迟的苛刻要求,也能满足安防行业对视频OSD 水印、AI 分析的业务刚需。

  • 性能的极致优化(Performance Optimization): 在透传模式下,通过VideoDataCallback+PostVideoEncodedData实现全链路零解码(Zero-Decoding)转发,将 CPU 占用率降至最低,大幅延长设备续航,彻底解决了移动设备发热降频的痛点。

  • 全栈式的协议栈(Full-Stack Protocol Support): 一套代码打通了RTSP/RTMP 拉流RTMP 推流轻量级 RTSP 服务端以及本地 MP4 录像。这种“拉、推、录、发”四位一体的能力,使得 Android 设备不再仅仅是视频的消费者,更是边缘视频网络的核心节点

这种“进可攻(二次编辑、AI处理),退可守(极速透传、低功耗)”的技术设计,让开发者在面对复杂的异构网络环境和多变的业务场景时,能够游刃有余,构建出真正高可用、工业级的音视频应用。

📎 CSDN官方博客:音视频牛哥-CSDN博客

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/12 5:49:04

Langchain-Chatchat用于书法艺术智能鉴赏

Langchain-Chatchat 用于书法艺术智能鉴赏 在博物馆的数字化展厅里&#xff0c;一位年轻观众站在《兰亭序》复制品前轻声提问&#xff1a;“这幅字为什么被称为‘天下第一行书’&#xff1f;” 如果此刻有个声音能娓娓道来王羲之酒后挥毫的历史情境、笔法中的“飘逸与顿挫”、历…

作者头像 李华
网站建设 2026/5/13 3:08:54

Spring Boot Web入门:从零开始构建web程序

Spring Boot作为当前Java领域最流行的框架之一&#xff0c;极大地简化了Spring应用的初始搭建和开发过程。本文将带你从零开始&#xff0c;创建一个简单的Spring Boot Web应用&#xff0c;并通过详细的步骤解释整个过程。一、Spring Boot简介Spring Boot是由Pivotal团队提供的全…

作者头像 李华
网站建设 2026/5/12 9:34:07

Langchain-Chatchat推动数字政府服务能力升级

Langchain-Chatchat 推动数字政府服务能力升级 在政务服务日益智能化的今天&#xff0c;公众对政策咨询的响应速度与准确性提出了更高要求。面对海量非结构化政策文件和不断更新的办事指南&#xff0c;传统信息检索方式显得力不从心——关键词匹配难以理解语义&#xff0c;人工…

作者头像 李华
网站建设 2026/5/12 6:46:26

Langchain-Chatchat实现繁体字与简体字互转问答

Langchain-Chatchat 实现简繁体字互转问答 在企业级智能问答系统日益普及的今天&#xff0c;如何在保障数据安全的前提下&#xff0c;提升系统的语言适应能力&#xff0c;成为开发者关注的核心问题。尤其是在中文使用场景中&#xff0c;简体与繁体并存于不同地区——中国大陆广…

作者头像 李华
网站建设 2026/5/12 9:33:19

Langchain-Chatchat打造虚拟偶像互动系统

Langchain-Chatchat 打造虚拟偶像互动系统 在数字人、元宇宙和 AIGC 技术交织演进的今天&#xff0c;虚拟偶像早已不再是简单的动画形象或预录语音。她们需要“有记忆”、“懂情绪”&#xff0c;能与粉丝进行自然对话&#xff0c;甚至记住某位忠实支持者的名字和喜好——这种拟…

作者头像 李华