深入探讨后台摄像头|麦克风采集与轻量级RTSP服务|RTMP推流架构设计

在 Android 音视频开发中,"后台推流"是一个经典且棘手的需求。常见的场景包括:行车记录仪(熄屏录像)、智能安全帽(后台回传)、执法记录仪等。

传统的做法是将推流逻辑写在 Activity 中,但一旦用户按 Home 键或熄屏,Activity 及其持有的 Camera 资源极易被系统回收。为了解决这个问题,我们需要构建一个基于前台服务(Foreground Service)的独立采集架构。

本文将结合大牛直播SDK(SmartMediakit)的 SmartPublisher 模块的源码,深度拆解如何实现这一系统。

一、 整体架构设计:UI 与业务的彻底解耦

为了保证推流服务的稳定性,我们采用 C/S(Client/Service)架构 模式:

  1. Service 层 (StreamMediaCameraService):

    • 作为后台驻留的核心载体。

    • 负责持有 Engine 实例。

    • 提升进程优先级(前台服务),防止 OOM Killer 查杀。

  2. Engine 层 (NTStreamMediaCameraEngineImpl):

    • 业务大脑。封装 Camera2 API、AudioRecord 以及 Native 推流库。

    • 管理数据采集(YUV/PCM)到编码推流的全流程。

  3. UI 层 (MainActivity):

    • 纯展示与控制。只负责显示预览画面(TextureView)和发送控制指令(开关相机、开始推流)。

    • 即使 Activity 销毁(如屏幕旋转、内存回收),Service 和 Engine 依然存活。

  4. 通信层 (NTStreamMediaBinder):

    • 通过 Binder 机制,让 Activity 获取 Service 中的 Engine 句柄。

二、 核心实现一:构建"杀不死"的前台服务

在 StreamMediaCameraService.java 中,最关键的逻辑是适配 Android 8.0 到 Android 14 的前台服务机制。

1. 为什么需要前台服务?

Android 系统对后台应用限制极严。如果不启动前台服务(在通知栏显示一条常驻通知),App 切后台后几分钟内网络和 CPU 就会被限制,导致推流中断。

2. 代码实现详解

我们需要根据 Android 版本动态设置 Notification Channel 和 ServiceType(特别是 Android 10+ 必须声明 Camera/Microphone 类型)。

java 复制代码
// [文件: StreamMediaCameraService.java]

public void start_foreground_service() {
    try {
        String channelId = "nt_camera_service";
        String channelName = "Background Camera Service";
        
        // 1. 创建 NotificationManager
        NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        
        // 2. 适配 Android 8.0+ (Oreo): 必须创建 NotificationChannel
        if (Build.VERSION.SDK_INT >= 26) {
            NotificationChannel channel = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH);
            manager.createNotificationChannel(channel);
        }

        // 3. 构建动态的 Notification 内容,实时显示当前状态
        StringBuilder sb = new StringBuilder(256);
        sb.append("流媒体采集服务正在运行;");
        if (engine_ != null) {
            if (engine_.is_opened_camera()) sb.append("摄像头已开启;");
            if (engine_.is_audio_record_running()) sb.append("麦克风已开启;");
            if (engine_.is_rtsp_stream_running()) sb.append("已输出RTSP流;");
        }

        Notification.Builder builder = new Notification.Builder(this)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle("摄像头采集前台服务")
                .setContentText(sb.toString());

        if (Build.VERSION.SDK_INT >= 26)
            builder.setChannelId(channelId);

        Notification notification = builder.build();

        // 4. 启动前台服务 (核心适配点)
        final int id = 111;
        if (Build.VERSION.SDK_INT < 29) {
            // Android 9.0 及以下
            startForeground(id, notification);
        } else {
            // Android 10.0+ (Q/R/S...): 必须显式声明服务类型
            int type = 0;
            // 动态检查权限,防止 Crash
            if (Build.VERSION.SDK_INT >= 30 && check_camera_permission())
                type |= ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
            
            if (Build.VERSION.SDK_INT >= 30 && check_record_audio_permission())
                type |= ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;

            startForeground(id, notification, type);
        }
    } catch (Exception e) {
        Log.e(TAG, "start_foreground Exception:", e);
        stopSelf();
    }
}

注意: 在 AndroidManifest.xml 中也必须对应声明:

java 复制代码
<service android:name=".StreamMediaCameraService"
    android:foregroundServiceType="camera|microphone" />

三、 核心实现二:Activity 与 Service 的安全绑定

MainActivity 不直接 new Engine,而是通过 bindService 获取。这保证了 Engine 的生命周期跟随 Service 而非 Activity。

1. Binder 定义

java 复制代码
// [文件: NTStreamMediaBinder.java]
public class NTStreamMediaBinder extends Binder {
    // 使用 AtomicReference 保证多线程安全
    private final AtomicReference<NTStreamMediaCameraEngine> engine_ = new AtomicReference<>();

    public NTStreamMediaCameraEngine get_stream_media_camera_engine() {
        return engine_.get();
    }
    // ... attach/detach 逻辑
}

2. Service 中的绑定逻辑

java 复制代码
// [文件: StreamMediaCameraService.java]
@Override
public void onCreate() {
    super.onCreate();
    // Service 创建时,初始化 Engine,确保 Engine 和 Service 同生共死
    this.engine_ = new NTStreamMediaCameraEngineImpl(getApplication(), service_handler_, running_thread_, lib_publisher_);
}

@Override
public IBinder onBind(Intent intent) {
    // 当 Activity 绑定时,将 Engine 挂载到 Binder 上
    binder_.attach(engine_);
    return binder_; 
}

3. Activity 中的获取逻辑

java 复制代码
// [文件: MainActivity.java]
private void onServiceConnected(NTStreamMediaBinder binder) {
    media_binder_ = binder;
    // 关键点:从 Binder 中拿到了活着的 Engine 实例
    media_engine_ = get_stream_media_engine(); 

    if (media_engine_ != null) {
        // 重新注册回调(因为 Activity 重建后回调对象变了)
        media_engine_callback_ = new NTStreamEngineCallbackImpl();
        media_engine_callback_.reset(MainActivity.this);
        media_engine_.register_callback(media_engine_callback_);

        // 恢复预览 View(如果有的话)
        if (is_preview_camera_ && preview_view_.isAvailable())
            media_engine_.set_camera_preview_view(preview_view_);
    }
    // ...
}

四、 核心实现三:Camera2 数据采集与 Native 推流

这是整个系统的数据引擎。NTStreamMediaCameraEngineImpl 负责协调 Camera2Helper 采集数据,并将数据投递给 JNI 层。

1. 数据的产生 (Camera2 API)

在 Camera2Helper.java 中,我们通过 ImageReader 获取 YUV 数据(格式通常为 YUV_420_888)。

2. 数据的流转 (Java -> Native)

在 NTStreamMediaCameraEngineImpl 中,我们实现了 Camera2Listener。这是性能最敏感的部分,直接决定了推流的延迟和流畅度。

java 复制代码
// [文件: NTStreamMediaCameraEngineImpl.java]

@Override
public void onCameraImageData(Image image) {
    if (null == image) return;
    
    // 只有当推流开启时才处理数据,节省 CPU
    if (!stream_publisher_.is_publishing()) return;

    // 校验格式,必须是 YUV_420_888
    if (image.getFormat() != ImageFormat.YUV_420_888) return;

    Image.Plane[] planes = image.getPlanes();
    // 获取 Y, U, V 三个平面的数据指针和步长
    ByteBuffer y_buffer = planes[0].getBuffer();
    ByteBuffer u_buffer = planes[1].getBuffer();
    ByteBuffer v_buffer = planes[2].getBuffer();

    int y_row_stride = planes[0].getRowStride();
    int u_row_stride = planes[1].getRowStride();
    int v_row_stride = planes[2].getRowStride();
    int uv_pixel_stride = planes[1].getPixelStride(); // 关键参数,决定是 I420 还是 NV21 等

    int width = image.getWidth();
    int height = image.getHeight();
    
    // 获取旋转角度(处理横竖屏推流的关键)
    int rotation_degree = cameraImageRotationDegree_.get();

    // 【核心调用】直接将 ByteBuffer 传递给 Native 层 (JNI)
    // 避免了在 Java 层进行 byte[] 拷贝,极大提升性能
    stream_publisher_.PostLayerImageYUV420888ByteBuffer(
            0, 0, 0,
            y_buffer, 0, y_row_stride,
            u_buffer, 0, u_row_stride,
            v_buffer, 0, v_row_stride, 
            uv_pixel_stride,
            width, height, 
            0, 0, 0, 0, // 翻转和缩放参数
            0, rotation_degree
    );
}

代码亮点:

  • 零拷贝 (Zero Copy) : 使用 ByteBuffer 直接在 Java 和 C++ 之间传递内存地址,避免了 byte[] 数组的反复拷贝,这对于 1080P/30fps 的高清流至关重要。

  • 动态旋转 : 通过 rotation_degree 参数,底层 SDK 自动处理图像旋转,确保观看端看到的画面方向正确。


五、 核心实现四:RTSP Server 与多路分发

系统不仅支持 RTMP 推流,还内置了一个轻量级的 RTSP Server。这意味着手机本身变成了一个 IPC(网络摄像机)。

java 复制代码
// [文件: NTStreamMediaCameraEngineImpl.java]

private boolean start_rtsp_stream_internal(String stream_name) {
    // 1. 确保 RTSP Server 端口已监听
    long rtsp_server_handle = rtsp_server_.get_native();
    if(0 == rtsp_server_handle) return false;

    // 2. 创建 SDK 推流实例
    if (!test_and_create_sdk_instance()) return false;

    // 3. 设置流名称 (例如 rtsp://192.168.1.100:8554/stream1)
    stream_publisher_.SetRtspStreamName(stream_name);

    // 4. 将推流实例挂载到 Server 上
    stream_publisher_.ClearRtspStreamServer();
    stream_publisher_.AddRtspStreamServer(rtsp_server_handle);

    // 5. 启动流
    if (!stream_publisher_.StartRtspStream()) {
        return false;
    }
    
    // 6. 启动水印线程 (可选)
    start_video_layer_post_thread();

    return true;
}

六、 工业级细节:电池优化白名单

仅仅依靠前台服务,在某些深度定制的 ROM(如 MIUI、EMUI)或 Android 高版本 Doze 模式下仍可能被限制网络。在 MainActivity 中,我们必须请求用户豁免电池优化。

java 复制代码
// [文件: MainActivity.java]

// 判断是否已在白名单中
private boolean isIgnoringBatteryOptimizations(){
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        String packageName = getPackageName();
        PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
        return pm.isIgnoringBatteryOptimizations(packageName);
    }
    return false;
}

// 跳转系统设置申请白名单
private void gotoSettingIgnoringBatteryOptimizations() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        try {
            Intent intent = new Intent();
            String packageName = getPackageName();
            intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
            intent.setData(Uri.parse("package:" + packageName));
            startActivityForResult(intent, REQUEST_IGNORE_BATTERY_CODE);
        } catch (Exception e) {
            Log.e(TAG, "gotoSettingIgnoringBatteryOptimizations Exception:", e);
        }
    }
}

七、 总结

通过上述代码的拆解,我们利用大牛直播SDK的轻量级RTSP服务模块,构建了一个完整的 Android 后台流媒体采集系统:

  1. 稳定性 :利用 Notification + Service + ForegroundServiceType 适配,确保进程在后台不被查杀。

  2. 高性能 :通过 Camera2 API + ByteBuffer + JNI 实现 YUV 数据的零拷贝传输。

  3. 灵活性 :Service 承载业务,Activity 仅作展示,实现了完美的解耦,支持断线重连和后台静默推流。

  4. 功能全:同时支持 RTMP 推流、RTSP 服务端、水印叠加、软硬编码自动切换。

这套架构不仅适用于 Demo 演示,更是开发行车记录仪、远程协助、无人机图传等商业级产品的坚实基础。

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

相关推荐
微帧Visionular3 小时前
AI视频单帧清晰,播放时却闪烁,原因是什么?
人工智能·深度学习·音视频
辰辰观影5 小时前
Windows网盘视频播放器推荐:无广告4K HDR怎么选
音视频
2601_966827457 小时前
电视端云盘在线播放怎么选?支持4K HDR的应用推荐
音视频
开开心心就好8 小时前
音频降噪剪辑工具,音量裁剪一键处理
运维·服务器·前端·spring·ocr·音视频·1024程序员节
AI视频工具指南11 小时前
AI视频的声音和画面怎么配合?先分清谁在传递信息
人工智能·aigc·音视频
辰辰观影14 小时前
无广告播放器推荐来啦
音视频
TrueDei14 小时前
GStreamer 使用 valve 分别控制音视频流的暂停与恢复
音视频
一条破秋裤15 小时前
基于 Hi3516CV610 的立定跳远视频测量系统调研与实施方案
arm开发·音视频
2601_9668274515 小时前
视频里的特效字幕怎么翻译?外挂字幕与AI字幕一键处理教程
音视频
八月的雨季 最後的冰吻16 小时前
音视频面试--延迟优化
面试·职场和发展·音视频