一、前言

随着移动互联网直播业务的高速发展,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 屏幕采集直播推流的工程实现:

  1. 双 HandlerThread 异步架构彻底解耦图像采集与控制逻辑,是稳定高性能的核心
  2. LibPublisherWrapper 的读写锁+状态机设计,优雅解决了多路流并发控制难题
  3. NTVirtualDisplaySurfaceSinker 的补帧机制,保证了静止场景下码流的连续性
  4. RTMP 与内置 RTSP Server 双协议支持,覆盖互联网直播与局域网低延迟分发两大场景
  5. 完善的 Service 生命周期管理,确保长时间推流稳定不崩溃

大牛直播 SDK(SmartMediaKit) 凭借低延迟、高性能、协议完整、架构灵活的优势,是 Android 端屏幕采集直播的高质量选择。


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

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐