Andorid平台实现高性能低延迟的多路RTSP播放器

​在当今的视频监控、流媒体传输等领域,RTSP(Real Time Streaming Protocol)协议被广泛用于音视频数据的实时传输。为了满足多路 RTSP 流的同时播放需求,基于大牛直播SDK开发了一款功能丰富、性能稳定的多路 RTSP 播放器。本文将深入解析该播放器的实现原理、代码架构以及关键功能模块。

一、项目背景与需求

随着视频监控系统的规模不断扩大,用户需要一个能够同时处理多路 RTSP 流的播放器,以实现对多个监控摄像头或流媒体源的集中监控与管理。传统的单路播放器已无法满足此类需求,因此开发一个多路 RTSP 播放器显得尤为必要。

该播放器主要面向以下场景:

  • 视频监控中心 :对多个监控摄像头进行实时监控,要求低延迟、高稳定性。

  • 流媒体服务器测试 :在测试流媒体服务器时,需要模拟多个客户端同时播放 RTSP 流,以评估服务器性能。

  • 多媒体展示系统 :在某些展览、展示场景中,需要在多个屏幕上同时播放不同的 RTSP 流媒体内容。

二、整体架构设计

(一)核心组件

  1. SDK 封装层 :利用大牛直播SDK的SmartMediakit框架提供的 SmartPlayerJniV2 类,通过 JNI 调用原生库实现 RTSP 流的底层处理,包括播放、截图、录像等功能。

  2. 播放器封装类(LibPlayerWrapper) :对 SDK 的功能进行二次封装,提供更简洁易用的接口,管理播放器的生命周期、状态以及与 SDK 的交互逻辑。

  3. UI 层(SmartPlayer) :基于 Android 的 Activity 构建,负责与用户交互,展示播放画面,控制播放器的启动、停止、录像等操作。

(二)架构图

markdown 复制代码
UI 层(SmartPlayer)
    │
    ├─ 播放控制(按钮点击事件)
    ├─ 播放画面展示(SurfaceView)
    └─ 事件显示(文本信息)
LibPlayerWrapper 层
    │
    ├─ 播放器生命周期管理
    ├─ 播放状态控制(播放、暂停、停止)
    ├─ 录像功能管理
    ├─ 截图功能实现
    └─ 事件回调处理
SDK 封装层(SmartPlayerJniV2)
    │
    ├─ RTSP 流处理(播放、暂停、停止)
    ├─ 视频渲染(Surface 设置)
    ├─ 音频处理
    ├─ 录像功能实现
    └─ 截图功能实现

三、关键代码解析

(一)播放器初始化

在 SmartPlayer 类的 onCreate 方法中,完成播放器的初始化工作。

scss 复制代码
libPlayer = new SmartPlayerJniV2();
context_ = this.getApplicationContext();

initView();
initPlayUrls();
setupSurfaceCallbacks();
createPlayerInstances();
setupButtonListeners();

首先,创建 SmartPlayerJniV2 对象,这是与 SDK 进行交互的入口。接着,初始化 UI 组件,加载播放地址列表,并为 SurfaceView 设置回调函数。然后,创建多个 LibPlayerWrapper 实例,每个实例对应一个播放器实例,用于管理单个 RTSP 流的播放。

(二)播放控制

在 LibPlayerWrapper 类中,startPlayer 方法实现了播放功能。

kotlin 复制代码
public boolean startPlayer(boolean is_hardware_decoder, boolean is_enable_hardware_render_mode, boolean is_mute) {
    if (is_playing()) {
        Log.e(TAG, "already playing, native_handle:" + get());
        return false;
    }

    setPlayerParam(is_hardware_decoder, is_enable_hardware_render_mode, is_mute);

    int ret = lib_player_.SmartPlayerStartPlay(get());
    if (ret != OK) {
        Log.e(TAG, "call StartPlay failed, native_handle:" + get() + ", ret:" + ret);
        return false;
    }

    write_lock_.lock();
    try {
        this.is_playing_ = true;
    } finally {
        write_lock_.unlock();
    }

    Log.i(TAG, "call StartPlayer OK, native_handle:" + get());
    return true;
}

在 startPlayer 方法中,首先检查播放器是否已经在播放状态。然后,通过 setPlayerParam 方法设置播放器参数,如硬件解码、硬件渲染模式和静音等。接着,调用 SDK 提供的 SmartPlayerStartPlay 方法开始播放 RTSP 流。如果播放成功,更新播放器的状态为正在播放。

(三)录像功能

LibPlayerWrapper 类中的 configRecorderParam 方法用于配置录像参数。

csharp 复制代码
public boolean configRecorderParam(String rec_dir, int file_max_size, int is_transcode_aac,
                                   int is_record_video, int is_record_audio) {

    if(!check_native_handle())
        return false;

    if (null == rec_dir || rec_dir.isEmpty())
        return false;

    int ret = lib_player_.SmartPlayerCreateFileDirectory(rec_dir);
    if (ret != 0) {
        Log.e(TAG, "Create record dir failed, path:" + rec_dir);
        return false;
    }

    if (lib_player_.SmartPlayerSetRecorderDirectory(get(), rec_dir) != 0) {
        Log.e(TAG, "Set record dir failed , path:" + rec_dir);
        return false;
    }

    if (lib_player_.SmartPlayerSetRecorderFileMaxSize(get(),file_max_size) != 0) {
        Log.e(TAG, "SmartPlayerSetRecorderFileMaxSize failed.");
        return false;
    }

    lib_player_.SmartPlayerSetRecorderAudioTranscodeAAC(get(), is_transcode_aac);

    // 更细粒度控制录像的, 一般情况无需调用
    lib_player_.SmartPlayerSetRecorderVideo(get(), is_record_video);
    lib_player_.SmartPlayerSetRecorderAudio(get(), is_record_audio);
    return true;
}

该方法首先检查原生句柄是否有效以及录像目录是否合法。然后,调用 SDK 的相关方法创建录像目录、设置录像文件最大大小、音频转码 AAC 参数以及是否录制视频和音频。通过这些参数的配置,可以灵活地控制录像的各个方面。

startRecorder 方法用于启动录像功能。

csharp 复制代码
public boolean startRecorder() {

    if (is_recording()) {
        Log.e(TAG, "already recording, native_handle:" + get());
        return false;
    }

    int ret = lib_player_.SmartPlayerStartRecorder(get());
    if (ret != OK) {
        Log.e(TAG, "call SmartPlayerStartRecorder failed, native_handle:" + get() + ", ret:" + ret);
        return false;
    }

    write_lock_.lock();
    try {
        this.is_recording_ = true;
    } finally {
        write_lock_.unlock();
    }

    Log.i(TAG, "call SmartPlayerStartRecorder OK, native_handle:" + get());
    return true;
}

在启动录像之前,检查是否已经在录像状态。然后,调用 SDK 的 SmartPlayerStartRecorder 方法开始录像,并更新播放器的录像状态。

(四)截图功能

LibPlayerWrapper 类中的 captureImage 方法实现了截图功能。

arduino 复制代码
public boolean captureImage(int compress_format, int quality, String file_name, String user_data_string) {
    if (!check_native_handle())
        return false;

    return OK == lib_player_.CaptureImage(get(), compress_format, quality, file_name, user_data_string);
}

该方法通过调用 SDK 的 CaptureImage 方法进行截图操作。参数包括压缩格式(JPEG 或 PNG)、图片质量、文件名和用户自定义字符串。用户可以根据需要选择截图的格式和质量,并指定截图保存的路径和文件名。

(五)事件回调

在 SmartPlayer 类中,实现了 EventListener 接口的 onPlayerEventCallback 方法,用于处理播放器的各种事件回调。

scss 复制代码
@Override
public void onPlayerEventCallback(long handle, int id, long param1, long param2, String param3, String param4, Object param5) {
    StringBuilder sb = new StringBuilder(256);
    sb.append("PlayerHandle: ").append(handle).append(" ");

    switch (id) {
        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_STARTED:
            sb.append("开始..");
            break;
        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTING:
            sb.append("连接中..");
            break;
        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTION_FAILED:
            sb.append("连接失败..");
            break;
        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTED:
            sb.append("连接成功..");
            break;
        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_DISCONNECTED:
            sb.append("连接断开..");
            break;
        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_STOP:
            sb.append("连接播放..");
            break;
        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_RESOLUTION_INFO:
            sb.append("分辨率信息: width: ").append(param1).append(", height: ").append(param2);
            break;
        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_NO_MEDIADATA_RECEIVED:
            sb.append("收不到媒体数据,可能是url错误..");
            break;
        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_SWITCH_URL:
            sb.append("切换播放URL..");
            break;
        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CAPTURE_IMAGE:
            sb.append("快照: ").append(param1).append(" 路径: ").append(param3);

            if (param1 == 0)
                sb.append("截取快照成功");
            else
                sb.append("截取快照失败");

            if (param4 != null && !param4.isEmpty())
                sb.append(", user data:").append(param4);
            break;

        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_RECORDER_START_NEW_FILE:
            sb.append("[record]开始一个新的录像文件 :").append(param3);
            break;
        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_ONE_RECORDER_FILE_FINISHED:
            sb.append("[record]已生成一个录像文件 :").append(param3);
            break;

        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_START_BUFFERING:
            sb.append("Start Buffering");
            break;

        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_BUFFERING:
            sb.append("Buffering: ").append(param1).append("%");
            break;

        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_STOP_BUFFERING:
            sb.append("Stop Buffering");
            break;

        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_DOWNLOAD_SPEED:
            sb.append("download_speed:").append(param1).append("Byte/s, ").append((param1 * 8 / 1000)).append("kbps").append((param1 / 1024)).append("KB/s");
            break;

        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_RTSP_STATUS_CODE:
            Log.e(TAG, "RTSP error code received, please make sure username/password is correct, error code:" + param1);
            sb.append("RTSP error code: ").append(param1);
            break;

        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_NEED_KEY:
            Log.e(TAG, "RTMP加密流,请设置播放需要的Key..");
            sb.append("RTMP加密流,请设置播放需要的Key..");
            break;

        case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_KEY_ERROR:
            Log.e(TAG, "RTMP加密流,Key错误,请重新设置..");
            sb.append("RTMP加密流,Key错误,请重新设置..");
            break;
    }

    Log.i(TAG, "onPlayerEventCallback: " + sb.toString());

    Message message = new Message();
    message.what = PLAYER_EVENT_MSG;
    message.obj =  sb.toString();
    handler_.sendMessage(message);
}

在该方法中,根据不同类型的事件 ID,构建相应的事件信息字符串,并通过 Handler 将事件信息发送到 UI 线程进行显示。这样用户可以在界面上实时查看播放器的各种状态变化和事件信息。

四、性能优化与注意事项

(一)硬件加速

在播放高清视频流时,开启硬件解码可以显著降低设备的 CPU 负载,提高播放性能。在 LibPlayerWrapper 类的 setPlayerParam 方法中,通过调用 SDK 的相关方法设置硬件解码和硬件渲染模式。

scss 复制代码
if (is_hardware_decoder && is_enable_hardware_render_mode) {
    lib_player_.SmartPlayerSetHWRenderMode(get(), 1);
}

(二)低延迟模式

为了满足实时性要求较高的场景,可以启用低延迟模式。在 configurePlayer 方法中设置低延迟模式。

ini 复制代码
boolean isLowLatency = true;
lib_player_.SmartPlayerSetLowLatencyMode(get(), isLowLatency ? 1 : 0);

(三)资源管理

在播放器的生命周期管理中,合理地分配和释放资源至关重要。在 LibPlayerWrapper 类的 release 方法中,释放播放器占用的资源。

ini 复制代码
public void release() {
    if (empty())
        return;

    if(is_playing())
        stopPlayer();

    if (is_recording())
        stopRecorder();

    long handle;
    write_lock_.lock();
    try {
        handle = this.native_handle_;
        this.native_handle_ = 0;
        clear_all_playing_flags();
    } finally {
        write_lock_.unlock();
    }

    if (lib_player_ != null && handle != 0)
        lib_player_.SmartPlayerClose(handle);

    event_listener_ = null;
}

在释放资源时,先停止播放和录像操作,然后将原生句柄设置为 0,并调用 SDK 的 SmartPlayerClose 方法关闭播放器实例,最后将事件监听器设置为 null,避免内存泄漏。

(四)线程安全

在多线程环境下,对播放器状态和资源的访问需要保证线程安全。在 LibPlayerWrapper 类中,使用 ReentrantReadWriteLock 来保护对播放器状态和原生句柄的访问。

java 复制代码
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();

在修改播放器状态或原生句柄时,获取写锁;在读取这些变量时,获取读锁,确保线程安全。

五、总结与展望

通过以上基于大牛直播 SDK 的多路 RTSP 播放器的实现与解析,我们深入了解了其架构设计、关键功能模块以及性能优化策略。该播放器具有以下优势:

  • 多路播放能力 :能够同时播放多路 RTSP 流,满足视频监控、流媒体测试等场景的需求。

  • 功能丰富 :支持播放、停止、截图、录像等多种功能,满足不同用户的使用需求。

  • 性能优化 :采用硬件加速、低延迟模式等技术手段,提高播放性能和实时性。

  • 良好的资源管理 :合理管理播放器的生命周期和资源,避免内存泄漏和资源浪费。

在未来的工作中,我们可以进一步扩展该播放器的功能,如支持更多的视频格式、实现自适应 bitrate 播放、优化在弱网络环境下的播放体验等。

相关推荐
声知视界5 小时前
音视频基础能力之 Android 音频篇 (六):音频编解码到底哪家强?
android·音视频开发
深海丧鱼2 天前
什么!只靠前端实现视频分片? - 网络篇
前端·音视频开发
深海丧鱼3 天前
什么!只靠前端实现视频分片?
前端·音视频开发
心走3 天前
WebRTC系列 WebGL 绘制YUV 画面
前端·音视频开发
北有花开3 天前
Android音视频-Lame编译(Android 15 16K)对齐 和 addr2line使用
android·前端·音视频开发
非典型程序猿3 天前
【Vulkan 入门系列】创建交换链、图像视图和渲染通道(四)
gpu·音视频开发
长沙红胖子Qt3 天前
live555开发笔记(二):live555创建RTSP服务器源码剖析,创建rtsp服务器的基本流程总结
c++·音视频开发
非典型程序猿4 天前
【Vulkan 入门系列】创建描述符集布局和图形管线(五)
gpu·音视频开发
NodeMedia6 天前
如何用WHIP协议WebRTC推流到NodeMediaServer
webrtc·音视频开发