基于 SmartMediaKit 实现Android屏幕采集RTMP直播推流与轻量级RTSP服务技术实现
一、前言
随着移动互联网直播业务的高速发展,Android 端屏幕采集直播的需求日益旺盛:无纸化会议、远程协助、在线教育演示、移动端监控等场景都需要将手机屏幕内容实时推送到流媒体服务器,供用户观看。Android 从 5.0(API 21)开始引入 MediaProjection API,为屏幕采集提供了官方支持;但从采集到编码、推流,涉及的技术栈极为复杂。
大牛直播SDK(SmartMediaKit)针对上述场景提供了完整的解决方案,覆盖以下核心能力:
- Android 屏幕采集:基于 MediaProjection + VirtualDisplay 的稳定采集链路
- 音视频采集:麦克风采集(NTAudioRecordV2)与 Android 10+ 内部音频采集(NTAudioPlaybackCapture)
- 编码:支持 H.264/H.265 软硬编码,AAC 音频编码
- RTMP 推流:标准 RTMP 协议推送,支持 Enhanced RTMP
- 轻量级 RTSP Server:内置 RTSP 服务,局域网低延迟分发
- 本地录像:推流与录像同步进行,灵活切片
本文将结合项目实际代码,从架构设计、协议规范、关键实现细节三个维度深入讲解。
二、整体架构设计

2.1 分层架构
┌─────────────────────────────────────────────────────────┐
│ MainActivity (UI层) │
│ 分辨率选择 / 编码选择 / 推流控制 / RTSP控制 │
└───────────────────┬─────────────────────────────────────┘
│ bindService
┌───────────────────▼─────────────────────────────────────┐
│ StreamMediaDemoService (Service层) │
│ 前台服务保活 / HandlerThread管理 / SDK初始化 │
│ onBind → NTStreamMediaBinder │
└───────────────────┬─────────────────────────────────────┘
│
┌───────────────────▼─────────────────────────────────────┐
│ NTStreamMediaProjectionEngineImpl (引擎层) │
│ 实现 NTStreamMediaEngine 接口 │
│ 视频采集链路 / 音频采集链路 / 推流/录像控制 │
└─────┬──────────────────────────────────────┬────────────┘
│ │
┌─────▼──────────┐ ┌──────────▼──────────┐
│ 视频采集链路 │ │ 音频采集链路 │
│ │ │ │
│ NTMediaProjection │ NTAudioRecordV2 │
│ Capture │ │ (麦克风) │
│ ↓ │ │ │
│ NTVirtualDisplay │ NTAudioPlayback │
│ SurfaceSinker │ │ Capture │
│ ↓ Image │ │ (内部音频 Android10+)│
│ LibPublisher │ │ ↓ PCM │
│ Wrapper │ │ LibPublisherWrapper │
└────────────────┘ └──────────────────────┘
│ │
┌─────▼──────────────────────────────────────▼────────────┐
│ LibPublisherWrapper (SDK封装层) │
│ ReadWriteLock 并发保护 / 状态管理 │
│ SmartPublisherJniV2 (JNI → Native SDK) │
└─────────────────────────────────────────────────────────┘
2.2 核心组件说明
| 组件 | 职责 |
|---|---|
StreamMediaDemoService |
Android 前台 Service,持有所有资源的生命周期,提供 Binder 通信 |
NTStreamMediaBinder |
Service 与 Activity 的 Binder 通信桥梁,持有引擎弱引用 |
NTStreamMediaEngine |
引擎接口定义,定义所有推流、录像、采集操作的契约 |
NTStreamMediaProjectionEngineImpl |
引擎核心实现,双 HandlerThread 异步架构 |
NTVirtualDisplaySurfaceSinker |
VirtualDisplay Surface 图像接收器,ImageReader 封装 |
LibPublisherWrapper |
SDK Native 句柄封装,读写锁保护,状态机管理 |
SmartPublisherJniV2 |
JNI 接口层,所有 Native SDK 接口声明 |
2.3 双线程异步架构
引擎采用两个专用 HandlerThread,彻底解耦图像处理与控制逻辑:
// StreamMediaDemoService.java
if (Build.VERSION.SDK_INT < 28)
this.image_thread_ = new HandlerThread("NTScreenImageThread");
else
this.image_thread_ = new HandlerThread("NTScreenImageThread", -10); // THREAD_PRIORITY_VIDEO
this.service_thread_ = new HandlerThread("NTCaptureService");
this.image_thread_.start();
this.service_thread_.start();
this.engine_ = new NTStreamMediaProjectionEngineImpl(
getApplication(), image_thread_, service_thread_, lib_publisher_);
- image_thread(图像线程):专门处理 ImageReader 的
onImageAvailable回调,Android 8.0+ 以THREAD_PRIORITY_VIDEO(-10)优先级运行,确保图像投递的实时性。 - service_thread(控制线程):处理推流启动/停止、VirtualDisplay 创建/销毁、尺寸变化响应等控制操作,保证线程安全。
三、RTMP 协议基础与 SDK 推流实现

3.1 RTMP 协议规范概述
RTMP(Real-Time Messaging Protocol)由 Adobe 提出,基于 TCP,默认端口 1935。协议核心概念:
- Chunk Stream:RTMP 以 Chunk 为传输单元,每个 Chunk 属于某个 Chunk Stream(CS)
- Message Stream:逻辑上的数据流,携带音频、视频、控制消息
- 握手(Handshake):C0→S0→S1→S2→C1→C2,完成版本协商与时间戳同步
- 连接流程:Handshake → connect → createStream → publish
- 音视频数据:Video Message(Type=9),Audio Message(Type=8)
- 时间戳:32-bit,单位毫秒,Chunk Header 中携带
H.264 in RTMP 的关键规范:
- 关键帧前必须先发送 AVCDecoderConfigurationRecord(SPS/PPS)
- 视频 Tag 头:FrameType(4bit)| CodecID(4bit)| AVCPacketType(1byte)| CompositionTime(3bytes)
大牛直播 SDK 完整实现了 RTMP 规范,并支持 Enhanced RTMP(支持 H.265/HEVC)。
3.2 推流初始化:SmartPublisherOpen
// NTStreamMediaProjectionEngineImpl.java - create_sdk_instance()
long handle = publisher_sdk.SmartPublisherOpen(
get_application_context(),
audio_opt_, // 1: 推送编码前PCM音频
video_opt_, // 3: 层叠加模式
size.getWidth(),
size.getHeight()
);
video_opt_ = 3 表示使用层叠加模式,这是屏幕采集场景的最优选择:通过 PostLayerImageRGBX8888ByteBuffer 直接将 ImageReader 输出的 RGBA 帧投递到 SDK 第 0 层,SDK 内部完成编码,零拷贝高效。
3.3 视频编码器配置
SDK 提供软编码和硬编码(MediaCodec)两种方式:
H.264 硬编码配置:
// NTStreamMediaProjectionEngineImpl.java
if (1 == video_encoder_type_) {
int hw_kbps = LibPublisherWrapper.estimate_video_hardware_kbps(
size.getWidth(), size.getHeight(), fps, true);
hw_kbps = (int)(hw_kbps * 1.25);
int isSupportH264HWEncoder = publisher_sdk.SetSmartPublisherVideoHWEncoder(handle, hw_kbps);
if (isSupportH264HWEncoder == 0) {
publisher_sdk.SetNativeMediaNDK(handle, 0);
publisher_sdk.SetVideoHWEncoderBitrateMode(handle, 1); // VBR 模式
publisher_sdk.SetVideoHWEncoderQuality(handle, 39);
publisher_sdk.SetAVCHWEncoderProfile(handle, 0x08); // High Profile
publisher_sdk.SetAVCHWEncoderLevel(handle, 0x1000); // Level 4.1
}
}
H.265 硬编码配置:
} else if (2 == video_encoder_type_) {
int hw_kbps = LibPublisherWrapper.estimate_video_hardware_kbps(
size.getWidth(), size.getHeight(), fps, false);
int isSupportHevcHWEncoder = publisher_sdk.SetSmartPublisherVideoHevcHWEncoder(handle, hw_kbps);
if (isSupportHevcHWEncoder == 0) {
publisher_sdk.SetNativeMediaNDK(handle, 0);
publisher_sdk.SetVideoHWEncoderBitrateMode(handle, 0); // CQ 模式
publisher_sdk.SetVideoHWEncoderQuality(handle, 39);
}
}
软编码 VBR 配置:
if (is_sw_vbr_mode) {
int is_enable_vbr = 1;
int video_quality = LibPublisherWrapper.estimate_video_software_quality(
size.getWidth(), size.getHeight(), true);
int vbr_max_bitrate = LibPublisherWrapper.estimate_video_vbr_max_kbps(
size.getWidth(), size.getHeight(), fps);
publisher_sdk.SmartPublisherSetSwVBRMode(handle, is_enable_vbr, video_quality, vbr_max_bitrate);
}
SDK 内置了自适应码率估算工具方法,开发者无需手动计算:
// LibPublisherWrapper.java - 硬编码码率估算
public static int estimate_video_hardware_kbps(int width, int height, int fps, boolean is_h264) {
int kbps;
int area = width * height;
if (area <= (320 * 300))
kbps = is_h264 ? 350 : 280;
else if (area <= (640 * 360))
kbps = is_h264 ? 850 : 650;
else if (area <= (1280 * 720))
kbps = is_h264 ? 2100 : 1500;
else if (area <= (1920 * 1088))
kbps = is_h264 ? 4200 : 2800;
else
kbps = is_h264 ? 4500 : 3500;
kbps = (int)(kbps * fps * 1.0 / 25.0 + 0.5);
return kbps;
}
3.4 音频编码配置
// AAC 编码,96kbps(考虑到同时采集麦克风和系统音频的场景)
lib_publisher_.SmartPublisherSetAudioCodecType(handle, 1); // 1: AAC
lib_publisher_.SmartPublisherSetAudioBitRate(handle, 96);
3.5 RTMP 推流启动与停止
通过 LibPublisherWrapper 封装 SDK 调用,内置推流状态机:
// LibPublisherWrapper.java
public boolean StartPublisher() {
if (!check_native_handle()) return false;
if (is_rtmp_publishing()) {
Log.e(TAG, "already publishing rtmp, native_handle:" + get());
return false;
}
int ret = lib_publisher_.SmartPublisherStartPublisher(get());
if (ret != OK) {
Log.e(TAG, "call SmartPublisherStartPublisher failed, ret:" + ret);
return false;
}
write_lock_.lock();
try {
this.is_rtmp_publishing_ = true;
} finally {
write_lock_.unlock();
}
return true;
}
public boolean StopPublisher() {
if (!check_native_handle()) return false;
if (!is_rtmp_publishing()) return false;
boolean is_need_call = false;
write_lock_.lock();
try {
if (this.is_rtmp_publishing_) {
this.is_rtmp_publishing_ = false;
is_need_call = true;
}
} finally {
write_lock_.unlock();
}
if (is_need_call)
lib_publisher_.SmartPublisherStopPublisher(get());
return true;
}
在引擎层,start_rtmp_stream 对外提供防重入保护,并将实际启动操作 post 到 service_thread 执行:
// NTStreamMediaProjectionEngineImpl.java
@Override
public boolean start_rtmp_stream(String url) {
if (stream_publisher_.is_rtmp_publishing()) {
Log.e(TAG, "start_rtmp_stream rtmp stream already exists");
return false;
}
if (!is_video_capture_running()) {
Log.e(TAG, "start_rtmp_stream please start_video_capture first");
return false;
}
Runnable r = new Runnable() {
private String url_;
@Override
public void run() {
if (!start_rtmp_stream_internal(this.url_)) {
// notify failure ...
}
}
Runnable set(String url) { this.url_ = url; return this; }
}.set(url);
post_or_execute(r);
return true;
}
stop_rtmp_stream 停止后调用 try_release() 尝试释放 SDK 实例(仅在没有其他流/录像时才真正释放):
@Override
public void stop_rtmp_stream() {
if (!stream_publisher_.is_rtmp_publishing()) return;
rtmp_url_.getAndSet(null);
Runnable r = new Runnable() {
@Override
public void run() {
stream_publisher_.StopPublisher();
stream_publisher_.try_release();
test_and_disable_post_audio();
}
};
post_or_execute(r);
}
四、RTSP 协议规范与轻量级 RTSP Server 实现
4.1 RTSP 协议概述
RTSP(Real Time Streaming Protocol,RFC 2326)是一种应用层协议,用于控制流媒体服务器:
- 方法:OPTIONS、DESCRIBE、SETUP、PLAY、PAUSE、TEARDOWN
- SDP(Session Description Protocol):DESCRIBE 响应中携带,描述流的编码、采样率、分辨率等
- RTP/RTCP:RTSP 控制,RTP 承载媒体数据,RTCP 负责质量反馈
- 传输:支持 UDP 单播/组播、TCP 交错(Interleaved)模式
典型 RTSP 交互流程:
Client → Server: OPTIONS rtsp://192.168.1.100:8554/stream1 RTSP/1.0
Server → Client: 200 OK, Public: OPTIONS, DESCRIBE, SETUP, PLAY, TEARDOWN
Client → Server: DESCRIBE rtsp://192.168.1.100:8554/stream1 RTSP/1.0
Server → Client: 200 OK, Content-Type: application/sdp
v=0
m=video 0 RTP/AVP 96
a=rtpmap:96 H264/90000
m=audio 0 RTP/AVP 97
a=rtpmap:97 mpeg4-generic/44100/1
Client → Server: SETUP ... Transport: RTP/AVP;unicast;client_port=xxxxx
Server → Client: 200 OK, Session: 12345678
Client → Server: PLAY rtsp://... Session: 12345678
Server → Client: 200 OK, RTP-Info: ...
[Server starts sending RTP packets]
4.2 RTSP Server 初始化
SDK 内置了轻量级 RTSP Server,支持多路流、用户名密码鉴权。初始化在 Service onCreate 时完成:
// StreamMediaDemoService.java - onCreate()
LibPublisherWrapper.RTSPServer.initialize_sdk(
this.lib_publisher_, getApplication().getApplicationContext());
RTSPServer.initialize_sdk 调用的是线程安全的单例初始化:
// LibPublisherWrapper.java - RTSPServer.SDKContext
public boolean initialize(SmartPublisherJniV2 lib_publisher, android.content.Context context) {
if (initialized_) return initialized_result_;
synchronized (this) {
if (initialized_) return initialized_result_;
try {
int sdk_ret = lib_publisher.InitRtspServer(context);
initialized_result_ = (0 == sdk_ret);
} catch (Exception e) {
initialized_result_ = false;
}
initialized_ = true;
return initialized_result_;
}
}
4.3 RTSP Server 启动
// NTStreamMediaProjectionEngineImpl.java
@Override
public boolean start_rtsp_server(int port, String user_name, String password) {
if (!rtsp_server_.empty()) {
int current_port = rtsp_server_.port();
if (current_port != port) {
Log.e(TAG, "already running, port mismatch");
return false;
}
return true; // 已在运行,同端口直接返回成功
}
LibPublisherWrapper.RTSPServer.Handle handle =
LibPublisherWrapper.RTSPServer.create_and_start_server(
lib_publisher_, port, user_name, password);
if (null == handle) {
Log.e(TAG, "create_and_start_server failed");
return false;
}
rtsp_server_.reset(handle);
return true;
}
create_and_start_server 封装了开启 RTSP Server 的完整流程:
// LibPublisherWrapper.java
public static Handle create_and_start_server(
SmartPublisherJniV2 lib_publisher, int port,
String user_name, String password) {
Handle handle = new Handle(lib_publisher);
// 1. Open Server
if (!handle.open(port, user_name, password)) {
handle.close(); return null;
}
// 2. Start Server
if (!handle.start()) {
handle.close(); return null;
}
return handle;
}
private boolean open(int port, String user_name, String password) {
long handle = lib_publisher_.OpenRtspServer(0);
if (0 == handle) return false;
if (lib_publisher_.SetRtspServerPort(handle, port) != 0) {
close(handle); return false;
}
// 可选:设置鉴权
if (user_name != null && !user_name.isEmpty()
&& password != null && !password.isEmpty()) {
if (lib_publisher_.SetRtspServerUserNamePassword(handle, user_name, password) != 0) {
close(handle); return false;
}
}
port_ = port;
native_handle_.set(handle);
return true;
}
4.4 RTSP 流发布
RTSP Server 启动后,需要将推流实例关联到 Server 并设置流名称,之后客户端便可通过 rtsp://{ip}:{port}/{stream_name} 访问:
// NTStreamMediaProjectionEngineImpl.java
private boolean start_rtsp_stream_internal(String stream_name) {
long rtsp_server_handle = rtsp_server_.get_native();
if (0 == rtsp_server_handle) {
Log.e(TAG, "please start rtsp server first");
return false;
}
if (!test_and_create_sdk_instance()) return false;
// 1. 设置流名称(对应 RTSP URL 路径)
if (!stream_publisher_.SetRtspStreamName(stream_name)) {
stream_publisher_.try_release();
return false;
}
// 2. 绑定 RTSP Server
stream_publisher_.ClearRtspStreamServer();
if (!stream_publisher_.AddRtspStreamServer(rtsp_server_handle)) {
stream_publisher_.try_release();
return false;
}
// 3. 启动 RTSP 流
if (!stream_publisher_.StartRtspStream()) {
stream_publisher_.try_release();
return false;
}
switch_audio_output_type(audio_output_type_);
return true;
}
SDK 会通过事件回调返回完整的 RTSP URL:
// NTStreamMediaProjectionEngineImpl.java - SDKEventCallbackImpl
case NTSmartEventID.EVENT_DANIULIVE_ERC_PUBLISHER_RTSP_URL:
sb.append(" RTSP发布URL:");
if (param3 != null) sb.append(param3);
{
NTStreamMediaProjectionEngineImpl engine = get_engine();
if (engine != null)
engine.on_rtsp_url(handle, param3);
}
break;
URL 通过回调链路传递到 UI 层展示:
// NTStreamMediaProjectionEngineImpl.java
public void on_rtsp_url(long handle, String url) {
if (handle != stream_publisher_.native_handle()) return;
Runnable r = new Runnable() {
private long handle_;
private String url_;
@Override
public void run() {
if (handle_ != stream_publisher_.native_handle()) return;
rtsp_url_.getAndSet(this.url_);
if (is_null_or_empty(this.url_)) return;
// 通知所有注册的回调
for (Callback c : callbacks_)
c.on_nt_rtsp_stream_url(this.url_);
}
Runnable set(long handle, String url) {
this.handle_ = handle; this.url_ = url; return this;
}
}.set(handle, url);
post(r);
}
五、屏幕采集核心链路详解
5.1 MediaProjection 采集流程
用户授权 → MediaProjectionManager.createScreenCaptureIntent()
→ startActivityForResult(REQUEST_MEDIA_PROJECTION)
→ onActivityResult → start_video_capture(resultCode, data)
→ NTMediaProjectionCapture.start()
→ 创建 VirtualDisplay
→ ImageReader.onImageAvailable 回调
→ on_virtual_display_image 处理帧数据
5.2 分辨率自适应设计
SDK 支持三档分辨率,采用黄金比例(φ ≈ 0.618)作为缩放系数:
// NTStreamMediaProjectionEngineImpl.java
private static Size get_output_display_size(Size in_size, int resolution_level) {
double scale_rate;
if (RESOLUTION_MEDIUM == resolution_level)
scale_rate = 0.6180339887; // 黄金比例,画质/带宽最优平衡
else if (RESOLUTION_LOW == resolution_level)
scale_rate = 0.3819660113; // 约 0.618²,低带宽场景
else
return new Size(in_size.getWidth(), in_size.getHeight()); // 原始分辨率
int width = (int)(in_size.getWidth() * scale_rate + 0.5);
width = align(width, 16); // 16字节对齐,硬编码要求
double r = width * 1.0 / in_size.getWidth();
int height = (int)(r * in_size.getHeight() + 0.5);
height = align(height, 2); // 2字节对齐,YUV格式要求
return new Size(width, height);
}
private static int align(int d, int a) {
return (((d) + (a - 1)) & ~(a - 1));
}
5.3 NTVirtualDisplaySurfaceSinker:图像接收器
NTVirtualDisplaySurfaceSinker 封装了 ImageReader,提供了精细的帧管理能力:
// NTVirtualDisplaySurfaceSinker.java - 创建 Surface
public Surface create_surface(Size size) {
ImageReader reader = ImageReader.newInstance(
size.getWidth(), size.getHeight(),
IMAGE_FORMAT, // PixelFormat.RGBA_8888
DEFAULT_MAX_IMAGES); // 3 buffers
this.reader_.set(reader);
reader.setOnImageAvailableListener(this, handler_); // 注册到 image_thread
return reader.getSurface();
}
帧投递逻辑(保证在 image_thread 上执行):
public boolean post_image(ImageReader reader, Image image) {
if (pause_post_image_) return false;
if (!check_thread()) return false; // 严格校验线程
reset_last_image(null); // 释放上一帧
if (!reader_lock_.tryLock()) {
Log.i(TAG, "post_image tryLock false");
return false;
}
try {
if (reader_.get() != reader) return false;
if (!callback.on_virtual_display_image(image)) return false;
} finally {
reader_lock_.unlock();
}
reset_last_image(image); // 持有当前帧用于补帧
set_compensate_timer(); // 启动补帧定时器
return true;
}
补帧机制:若超过 max_post_frame_interval_ 毫秒没有新帧,将重复发送最后一帧,防止编码器因无数据而产生异常:
private class CompensateRunnable implements Runnable {
private final int id_;
public CompensateRunnable(int id) { this.id_ = id; }
@Override
public void run() {
if (post_timer_id_ != this.id_) return; // 已有新帧,取消补帧
Image image = last_image_.getAndSet(null);
if (null == image) return;
if (!post_image(reader_.get(), image)) {
close_image(image);
return;
}
Log.v(TAG, "compensation frame.");
}
}
补帧间隔基于 fps 动态计算:
// NTStreamMediaProjectionEngineImpl.java
private void update_max_post_frame_interval(int fps) {
double d = 1000.0 / fps;
d = d * 1.6180339887; // 1.5~2 帧周期,适当容忍延迟
if (d > 1040.0) d = 1040;
if (d < 30.0) d = 30.0;
int interval = (int)(d + 0.5);
// post 到 running_thread 更新 sinker
...
}
5.4 图像帧投递到 SDK
图像帧通过 on_virtual_display_image 回调处理,使用 Layer 0 投递 RGBX8888 数据:
// NTStreamMediaProjectionEngineImpl.java
@Override
public boolean on_virtual_display_image(Image image) {
if (video_opt_ != 3) return false;
if (!stream_publisher_.is_publishing()) return false;
final Image.Plane[] planes = image.getPlanes();
final int plane_row_stride = planes[0].getRowStride();
final int image_w = image.getWidth();
final int image_h = image.getHeight();
// 处理 CropRect(设备相关)
int crop_left = 0, crop_top = 0;
int crop_width = image_w, crop_height = image_h;
if (Build.VERSION.SDK_INT >= 21) {
android.graphics.Rect crop_rect = image.getCropRect();
if (crop_rect != null && !crop_rect.isEmpty()) {
crop_left = crop_rect.left;
crop_top = crop_rect.top;
crop_width = crop_rect.width();
crop_height = crop_rect.height();
}
}
// 计算裁剪偏移(RGBA 每像素 4 字节)
int offset = (crop_top * plane_row_stride) + (crop_left * 4);
boolean ret = stream_publisher_.PostLayerImageRGBX8888ByteBuffer(
0, 0, 0, // 第 0 层,左上角坐标 (0,0)
planes[0].getBuffer(), offset, plane_row_stride,
crop_width, crop_height,
0, 0, // 不翻转
0, 0, 0, // 不缩放
0); // 不旋转
return ret;
}
六、音频采集与混音
6.1 音频输出类型控制
SDK 支持三种音频输出模式,通过 audio_output_type_ 控制:
// 0: 不输出音频
// 1: 输出麦克风音频
// 2: 输出系统播放音频(Android 10+)
private void switch_audio_output_type(int type) {
if (audio_opt_ != 1 || 0 == type) {
// 静音模式或音频未开启
if (audio_record_callback_ != null)
audio_record_callback_.enable_post(false);
if (audio_playback_capture_callback_ != null)
audio_playback_capture_callback_.enable_post(false);
return;
}
if (1 == type) {
// 输出麦克风
if (audio_playback_capture_callback_ != null)
audio_playback_capture_callback_.enable_post(false);
if (audio_record_callback_ != null)
audio_record_callback_.enable_post(true);
} else if (2 == type) {
// 输出系统音频
if (audio_record_callback_ != null)
audio_record_callback_.enable_post(false);
if (audio_playback_capture_callback_ != null)
audio_playback_capture_callback_.enable_post(true);
}
}
6.2 麦克风采集
@Override
public boolean start_audio_record(int sample_rate, int channels) {
if (!check_record_audio_permission()) return false;
NTAudioRecordV2 audio_record = new NTAudioRecordV2(get_application_context());
// audio_record.IsMicSource(true); // 可选,解决音量过小问题
if (!audio_record.Start(sample_rate, channels)) return false;
if (audio_record_callback_ != null)
audio_record.AddCallback(audio_record_callback_);
NTAudioRecordV2 old = audio_record_.getAndSet(audio_record);
if (old != null) {
if (audio_record_callback_ != null)
old.RemoveCallback(audio_record_callback_);
old.Stop();
}
return true;
}
PCM 数据回调到 SDK:
private static class AudioRecordCallbackImpl implements NTAudioRecordV2Callback {
private volatile boolean enable_post_;
@Override
public void onNTAudioRecordV2Frame(ByteBuffer data, int size,
int sampleRate, int channel, int per_channel_sample_number) {
if (!enable_post_) return;
LibPublisherWrapper sdk_wrapper = get_publisher_wrapper();
if (null == sdk_wrapper) return;
sdk_wrapper.OnPCMData(data, size, sampleRate, channel, per_channel_sample_number);
}
}
6.3 系统音频采集(Android 10+)
@Override
public boolean start_audio_playback_capture(int sample_rate, int channels) {
if (Build.VERSION.SDK_INT < 29) {
Log.e(TAG, "需要 Android 10 (API 29) 及以上版本");
return false;
}
MediaProjection media_projection = get_media_projection();
if (null == media_projection) {
Log.e(TAG, "需要先启动屏幕采集以获取 MediaProjection");
return false;
}
NTAudioPlaybackCapture capture = new NTAudioPlaybackCapture();
if (!capture.start(get_application_context(), media_projection, sample_rate, channels)) {
capture.close();
return false;
}
if (audio_playback_capture_callback_ != null)
capture.register_callback(audio_playback_capture_callback_);
NTAudioPlaybackCapture old = audio_playback_capture_.getAndSet(capture);
if (old != null) old.close();
return true;
}
七、LibPublisherWrapper:线程安全的 SDK 封装
7.1 读写锁设计
LibPublisherWrapper 使用公平读写锁(ReentrantReadWriteLock(true))保证并发安全:
private final ReadWriteLock rw_lock_ = new ReentrantReadWriteLock(true);
private final java.util.concurrent.locks.Lock write_lock_ = rw_lock_.writeLock();
private final java.util.concurrent.locks.Lock read_lock_ = rw_lock_.readLock();
- 写锁:用于修改
native_handle_和推流状态标志(StartPublisher/StopPublisher 等控制操作) - 读锁(tryLock):用于高频数据投递(PostLayerImage、OnPCMData 等),
tryLock非阻塞,丢帧比卡顿更好
// 数据投递:tryLock 非阻塞
public boolean PostLayerImageRGBX8888ByteBuffer(...) {
if (!check_native_handle()) return false;
if (!read_lock_.tryLock()) return false; // 控制操作时跳过本帧
try {
...
return OK == lib_publisher_.PostLayerImageRGBX8888ByteBuffer(...);
} finally {
read_lock_.unlock();
}
}
7.2 try_release 智能释放
SDK 实例在所有推流/录像都停止后才真正释放,避免重复创建:
public boolean try_release() {
if (empty()) return false;
if (is_publishing()) {
Log.i(TAG, "try_release it is publishing");
return false; // 还有其他活跃流,不释放
}
long handle;
write_lock_.lock();
try {
if (is_publishing()) return false;
handle = this.native_handle_;
this.native_handle_ = 0;
} finally {
write_lock_.unlock();
}
if (lib_publisher_ != null && handle != 0)
lib_publisher_.SmartPublisherClose(handle);
return true;
}
7.3 AutoCloseable 与 finalize 双重保障
public class LibPublisherWrapper implements AutoCloseable {
@Override
public void close() { release(); }
@Override
protected void finalize() throws Throwable {
try {
if (check_native_handle()) {
// 确保 finalize 时资源不泄漏
if (is_rtmp_publishing()) {
lib_publisher_.SmartPublisherStopPublisher(get());
}
// ... 停止其他流 ...
lib_publisher_.SmartPublisherClose(this.native_handle_);
this.native_handle_ = 0;
}
} catch (Exception e) { }
super.finalize();
}
}
八、Service 生命周期与前台保活
8.1 前台 Service 策略
Android 8.0+ 后台 Service 限制严格,屏幕采集必须以前台 Service 运行:
// StreamMediaDemoService.java
@Override
public void start_foreground_service(boolean is_has_projection_token) {
final String notification_id_ = "nt-capture-service-cid";
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= 26) {
NotificationChannel channel = new NotificationChannel(
notification_id_, "屏幕录制前台服务Demo",
NotificationManager.IMPORTANCE_HIGH);
notificationManager.createNotificationChannel(channel);
}
Notification notification = new Notification.Builder(this)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("屏幕录制前台服务Demo通知")
.setContentText(is_has_projection_token ?
"屏幕录制前台服务已启动, 已获取投影权限" : "屏幕录制前台服务已启动")
.setChannelId(notification_id_)
.build();
if (Build.VERSION.SDK_INT < 29) {
startForeground(131, notification);
} else {
int type = 0;
if (is_has_projection_token)
type |= ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION;
if (Build.VERSION.SDK_INT >= 30 && check_record_audio_permission())
type |= ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
startForeground(131, notification, type);
}
}
8.2 Service 关闭时的优雅销毁
// StreamMediaDemoService.java - onDestroy()
@Override
public void onDestroy() {
// 1. 在 service_thread 上关闭引擎(确保线程安全)
AtomicBoolean is_done = new AtomicBoolean(false);
if (projection_impl.post(close_runnable)) {
// 等待最多 3 秒
long wait_begin_t = android.os.SystemClock.elapsedRealtime();
while (!is_done.get()) {
Thread.yield();
if (android.os.SystemClock.elapsedRealtime() - wait_begin_t > 3000)
break;
}
} else {
close_runnable.run(); // post 失败则同步执行
}
// 2. 退出 service_thread(quitSafely 处理完队列中的消息)
service_thread_.quitSafely();
service_thread_.join(1000);
// 3. 退出 image_thread
image_thread_.quit();
image_thread_.join(32);
}
8.3 屏幕旋转响应
// StreamMediaDemoService.java
public void onConfigurationChanged(Configuration newConfig) {
if (Build.VERSION.SDK_INT < 34) {
// Android 14 以下需要手动响应旋转
if (engine_ != null)
engine_.test_and_resize(null);
}
}
test_and_resize 内部检测当前屏幕尺寸是否变化,若变化则重建 VirtualDisplay 和 ImageReader:
// NTStreamMediaProjectionEngineImpl.java
private final boolean resize(VideoSinkerCapturePair pair,
NTMediaProjectionCapture capture, Size size) {
Size output_size = get_output_display_size(size, resolution_level_);
NTVirtualDisplaySurfaceSinker new_sinker = new NTVirtualDisplaySurfaceSinker(
image_thread_id_, image_handler_, max_post_frame_interval_, true);
new_sinker.set_callback(this);
Surface surface = new_sinker.create_surface(output_size);
if (!capture.resize(size, output_size, surface)) {
new_sinker.close(); return false;
}
// 原子切换 sinker,旧 sinker 延迟 368ms 关闭(等待 resize 稳定)
NTVirtualDisplaySurfaceSinker old_sinker = pair.get_and_set_sinker(new_sinker);
if (old_sinker != null) {
old_sinker.pause_post_image(true);
running_handler_.postDelayed(() -> old_sinker.close(), 368);
}
new_sinker.pause_post_image(false);
return true;
}
九、UI 控制层设计
9.1 Activity 与 Service 绑定
// MainActivity.java
private void start_media_service() {
Intent intent = new Intent(getApplicationContext(), StreamMediaDemoService.class);
if (Build.VERSION.SDK_INT >= 26)
startForegroundService(intent);
else
startService(intent);
bindService(intent, service_connection_, Context.BIND_AUTO_CREATE);
}
ServiceConnection 通过弱引用持有 Activity,避免内存泄漏:
private static class ServiceConnectionImpl implements ServiceConnection {
private WeakReference<MainActivity> activity_;
@Override
public void onServiceConnected(ComponentName className, IBinder binder) {
if (binder instanceof NTStreamMediaBinder) {
MainActivity activity = get_activity();
if (activity != null)
activity.onServiceConnected((NTStreamMediaBinder) binder);
}
}
// ...
}
9.2 推流按钮逻辑
RTMP 推流启动时序:
// MainActivity.java - ButtonRTMPPublisherListener
public void onClick(View v) {
if (media_engine_.is_rtmp_stream_running()) {
media_engine_.stop_rtmp_stream();
button_rtmp_publisher_.setText("开始RTMP推送");
return;
}
// 必须先启动屏幕采集
if (!media_engine_.is_video_capture_running()) return;
String rtmp_url;
if (input_rtmp_url_ != null && input_rtmp_url_.length() > 1) {
rtmp_url = input_rtmp_url_;
} else {
rtmp_url = baseURL + (int)(System.currentTimeMillis() % 1000000);
}
// 设置编码参数
media_engine_.set_fps(fps_);
media_engine_.set_gop(gop_);
media_engine_.set_video_encoder_type(video_encoder_type);
if (!media_engine_.start_rtmp_stream(rtmp_url)) return;
button_rtmp_publisher_.setText("停止RTMP推送");
text_view_rtmp_url_.setText("RTMP URL:" + rtmp_url);
}
RTSP 发布需先启动 Server:
// 启动 RTSP Server
private class ButtonRTSPServiceListener implements OnClickListener {
public void onClick(View v) {
if (media_engine_.is_rtsp_server_running()) {
media_engine_.stop_rtsp_stream();
media_engine_.stop_rtsp_server();
return;
}
if (!media_engine_.start_rtsp_server(rtsp_port_, null, null)) return;
button_rtsp_service_.setText("停止RTSP服务");
}
}
// 发布 RTSP 流(stream_name 即 RTSP URL 路径)
private class ButtonRtspPublisherListener implements OnClickListener {
public void onClick(View v) {
if (media_engine_.is_rtsp_stream_running()) {
media_engine_.stop_rtsp_stream();
return;
}
if (!media_engine_.is_video_capture_running()) return;
media_engine_.set_fps(fps_);
media_engine_.set_gop(gop_);
media_engine_.set_video_encoder_type(video_encoder_type);
if (!media_engine_.start_rtsp_stream("stream1")) return;
button_rtsp_publisher_.setText("停止RTSP流");
}
}
十、性能优化要点
10.1 GOP 设置与直播延迟
RTMP/RTSP 直播中,GOP(Group of Pictures)大小直接影响首帧延迟与画质:
private int fps_ = 25;
private int gop_ = fps_ * 2; // 2秒一个 I 帧,直播常用值
- GOP 过大:新观众需等待更长时间才能看到画面(首帧延迟高)
- GOP 过小:I 帧比例大,码率增加,同等码率下画质下降
- 2s GOP(50帧)是直播场景的最优实践
10.2 VBR vs CBR 选择
// 软编码默认使用 VBR(可变码率)
private boolean is_sw_vbr_mode = true;
- CBR(恒定码率):适合 RTMP 推送到带宽受限网络,抗网络抖动能力强
- VBR(可变码率):适合录像或局域网 RTSP,画质优先
- 硬编码 H.264 推荐 VBR(
BitrateMode = 1),H.265 推荐 CQ(BitrateMode = 0)
10.3 省电优化白名单
// MainActivity.java - onCreate()
if (Build.VERSION.SDK_INT >= 26) {
if (!isIgnoringBatteryOptimizations())
gotoSettingIgnoringBatteryOptimizations();
}
Android 8.0+ 会在后台超过 1 分钟后杀掉 Service,加入省电白名单后可长时间稳定推流。
10.4 音视频编码同步
per_channel_sample_number = sample_rate / 100,即每 10ms 一包音频数据,与视频帧周期(40ms@25fps)的最小公倍数对齐,减少 A/V 同步抖动。
十一、本地录像功能
SDK 支持推流与录像同步进行,互不干扰:
// NTStreamMediaProjectionEngineImpl.java
private boolean start_record_internal(String record_directory, int file_max_size) {
if (!test_and_create_sdk_instance()) return false;
if (!config_record(record_directory, file_max_size)) {
stream_publisher_.try_release();
return false;
}
if (!stream_publisher_.StartRecorder()) {
stream_publisher_.try_release();
return false;
}
switch_audio_output_type(audio_output_type_);
return true;
}
private boolean config_record(String record_directory, int file_max_size) {
// 自动创建目录
int ret = lib_publisher_.SmartPublisherCreateFileDirectory(record_directory);
if (ret != 0) return false;
// 设置录像目录
if (!stream_publisher_.SetRecorderDirectory(record_directory)) return false;
// 设置单文件最大大小(MB),超过后自动切片
if (!stream_publisher_.SetRecorderFileMaxSize(file_max_size)) return false;
return true;
}
录像管理界面通过 RecorderManager 实现文件列表浏览和删除,RecorderPlayback 实现 MP4 文件本地播放回看。
安卓无纸化同屏延迟测试之轻量级RTSP方案
十二、大牛直播 SDK 核心优势总结
| 特性 | 说明 |
|---|---|
| 低延迟 | RTSP 局域网延迟 < 200ms,RTMP 端到端延迟 < 200ms |
| 高性能 | 双 HandlerThread 隔离图像与控制,image_thread 以 THREAD_PRIORITY_VIDEO 优先运行,确保帧率稳定 |
| 协议完整 | 完整实现 RTMP 规范(含 Enhanced RTMP),RTSP/RTP 规范,GB28181 |
| 编码灵活 | H.264/H.265 硬编码 + x264 软编码,VBR/CBR/CQ 多种码率模式 |
| 音频全覆盖 | 麦克风采集 + Android 10+ 内部音频采集,支持混音 |
| 内置 RTSP Server | 无需搭建流媒体服务器,设备即服务器,局域网直推 |
| 架构设计灵活 | NTStreamMediaEngine 接口解耦 UI 与引擎,LibPublisherWrapper 读写锁+状态机保证并发安全 |
| 资源管理严谨 | AutoCloseable + finalize 双重保障,try_release 智能释放,防止内存/句柄泄漏 |
| 屏幕旋转无缝 | onConfigurationChanged + VirtualDisplay 动态 resize,推流不中断 |
| 补帧机制 | 静止画面自动补帧,编码器不挨饿,码流连续 |
十三、接入注意事项
AndroidManifest.xml 权限声明
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Android 10+ 内部音频采集 -->
<uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" />
<service
android:name=".StreamMediaDemoService"
android:foregroundServiceType="mediaProjection|microphone"
android:exported="false" />
最低 SDK 版本
- 屏幕采集:Android 5.0(API 21)
- 系统音频采集:Android 10(API 29)
- 前台 Service 类型声明:Android 10(API 29)
- 屏幕旋转自动处理:Android 14(API 34)可省略
onConfigurationChanged
十四、总结
本文基于大牛直播 SDK(SmartMediaKit)的实际 Demo 代码,完整呈现了 Android 屏幕采集直播推流的工程实现:
- 双 HandlerThread 异步架构彻底解耦图像采集与控制逻辑,是稳定高性能的核心
- LibPublisherWrapper 的读写锁+状态机设计,优雅解决了多路流并发控制难题
- NTVirtualDisplaySurfaceSinker 的补帧机制,保证了静止场景下码流的连续性
- RTMP 与内置 RTSP Server 双协议支持,覆盖互联网直播与局域网低延迟分发两大场景
- 完善的 Service 生命周期管理,确保长时间推流稳定不崩溃
大牛直播 SDK(SmartMediaKit) 凭借低延迟、高性能、协议完整、架构灵活的优势,是 Android 端屏幕采集直播的高质量选择。
📎 CSDN官方博客:音视频牛哥-CSDN博客
更多推荐
所有评论(0)