在移动端音视频开发中,我们经常面临一个架构抉择:是追求极致的低延迟 (如无人机图传、实时指挥),还是追求丰富的功能处理(如加水印、AI分析、画中画)?
通常,实现前者需要"透传(Relay)",避免编解码的耗时;实现后者需要"转码(Transcoding)",需要获取 YUV/RGB 数据。
本文将结合 SmartPlayer.java 核心代码,深入剖析如何利用 大牛直播 SDK (SmartPlayer + SmartPublisher) 的灵活性,在同一套架构中同时实现这两种截然不同的业务场景,并构建一个支持 RTSP/RTMP 拉流、推流、录像及轻量级 RTSP 服务的全能终端。
核心架构设计:Wrapper 模式与事件驱动
为了保证业务逻辑与底层 SDK 的解耦,以及多线程环境下的稳定性,我们在 SmartPlayer 与 Native JNI 之间构建了一层封装。
-
LibPlayerWrapper: 负责播放控制、参数配置及数据回调的线程安全封装。 -
LibPublisherWrapper: 负责推流、录像、RTSP 服务及图层处理的封装。 -
EventListener: 将底层的状态回调(连接成功、断开、快照结果等)透传至 UI 层,实现逻辑与视图分离。

在 SmartPlayer.java 的 startPlayLogic 方法中,我们根据业务需求(isRelayMode)决定数据流向。这是整个系统的"路由"中心。如果不需要二次编码,那么点"开始播放",我们只是做预览播放。
java
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():
java
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 获取编码数据
我们需要实现 NTVideoDataCallback 和 NTAudioDataCallback。在 SmartPlayer.java 中,PlayerVideoDataCallback 负责将数据直接投递给 LibPublisherWrapper。
java
/* 引用自 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),因为它不需要编码。
java
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));
}
此外,在透传模式下,严禁启动本地音频采集(麦克风)和图层线程,否则会造成数据冲突或资源浪费。
java
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 的渲染回调。
java
/* 引用自 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 (水印) 进行硬件或软件混合。
java
/* 引用自 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,相关逻辑如下:
java
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 中:
java
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 上。
java
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);
}
});
}
}
}
场景五:本地录像
录像功能与推流功能是解耦的。我们可以只录像不推流,也可以边推流边录像。底层支持自动切片(分段保存)。
java
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 的深度剖析,我们看到了一套成熟的移动端音视频解决方案。它不仅解决了单一的"播放"或"推流"问题,更通过灵活的架构设计,完美覆盖了从低延迟传输 到边缘计算处理的多样化需求。
bash
+-----------------------------------------------------------------------+
| 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博客