Android 应用集成乐橙 OpenSDK:视频预览 + 录像回放一站式接入

Android 应用集成乐橙 OpenSDK:视频预览 + 录像回放一站式接入


连锁巡店 App 的 Android 工程师小林把 bindDeviceLive 返回的 HLS 塞进 WebView,总部验收时实时画面延迟 8 秒、滑动列表还 OOM。换乐橙 OpenSDK 私有协议 后出流 1–2 秒——但录像页又全黑:他用预览参数去播 SD 卡片段,列表却是 queryLocalRecords 查出来的 recordId。预览和回放,在 SDK 里是两套窗口、两条参数链。


为什么移动端值得用 OpenSDK 而不是只接 HTTP 拉流

乐橙开放平台(open.imou.com)给移动场景提供了多种对接方式。开发总览对比很直观:

对接方式协议出流速度实时延迟录像回放语音对讲对接耗时
移动应用 + OpenSDK私有协议1–2 秒1–2 秒1–3 月
云直播(HLS)hls3–8 秒8–10 秒1–8 小时
轻应用hls/rtsp1–3 秒2–3 秒1–7 日

巡店、工地、社区类 App 的核心诉求是「低延迟预览 + 回放 + 对讲」——HLS 适合 Web 快速验证,不适合作为 Android 主播放链路。OpenSDK 把解码、渲染、手势、对讲封装进 LCOpenSDK_PlayRealWindow / LCOpenSDK_PlayBackWindow,开发者专注业务 UI 与权限。

但 OpenSDK 不会替你做录像列表查询。完整链路是「云云对接」:

┌──────────────────────────────────────────────────────────────┐
│  Android App(OpenSDK)                                       │
│  LCOpenSDK_Api.init  →  PlayRealWindow  /  PlayBackWindow   │
└────────────────────────────┬─────────────────────────────────┘
                             │ accessToken、playToken、录像 recordId
┌────────────────────────────▼─────────────────────────────────┐
│  你的业务后端(HTTP OpenAPI)                                  │
│  accessToken · listDeviceDetailsByPage · queryLocalRecords   │
│  · getCloudRecords                                           │
└────────────────────────────┬─────────────────────────────────┘
                             │
┌────────────────────────────▼─────────────────────────────────┐
│  乐橙云平台  openapi.lechange.cn                               │
└──────────────────────────────────────────────────────────────┘

标准化 Demo 说明特别强调:Demo 在客户端直连 OpenAPI 仅为演示;生产环境 appId/appSecret 必须在服务端,否则反编译即密钥泄露。

预览 vs 回放:别混窗口类

能力SDK 类列表数据来源(OpenAPI)关键参数字段
实时预览LCOpenSDK_PlayRealWindowlistDeviceDetailsByPageplayTokendeviceIdchannelpskplayToken
设备 SD 回放LCOpenSDK_PlayBackWindowqueryLocalRecordsrecordId按文件名 / 按时间两种 Param
云录像回放LCOpenSDK_PlayBackWindowgetCloudRecordsrecordIdrecordRegionIDrecordType

「能预览」不等于「能回放」——设备无 TF 卡时本地列表为空;云存 csStatus 非 using 时云列表为空。列表查 OpenAPI,播放调 SDK,是小林黑屏的根因。

前置条件

  1. open.imou.com 注册开发者,创建移动应用,获取 appId / appSecret
  2. 资源下载 获取 Android SDK(LCOpenSDK.aar)Android Demo(参考 DeviceOnlineMediaPlayActivityNew.javaDeviceRecordPlayActivityNew.java)。
  3. 设备已绑定开发者账号(乐橙 App 或 SDK Demo 添加)。
  4. 使用现行 OpenAPI / OpenSDK 文档,勿引用「旧版本协议、不再维护」栏目

1. 工程接入:AAR + 权限 + ABI

Step 1:解压 SDK,将 libs/LCOpenSDK.aar 拷入工程 app/libs/,在 build.gradle 引用:

android {
    defaultConfig {
        ndk {
            abiFilters "armeabi", "arm64-v8a"  // 按目标机型选择
        }
    }
}
dependencies {
    implementation files('libs/LCOpenSDK.aar')
}

Step 2AndroidManifest.xml 声明权限(摘自 OpenSDK 组件文档):

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

未开 RECORD_AUDIO → 对讲失败;未开存储权限 → 本地录制/下载失败。

Step 3:播放布局——SDK 播放类继承 FrameLayout,需要父容器 + 渲染子容器:

<FrameLayout
    android:id="@+id/live_window"
    android:layout_width="match_parent"
    android:layout_height="200dp">
    <FrameLayout
        android:id="@+id/live_window_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</FrameLayout>

2. 全局初始化:OpenSDK 第一个必调接口

// ImouSdkHolder.java —— 建议在 Application.onCreate 调用
public final class ImouSdkHolder {
    private static volatile boolean inited;

    public static void init(Application app, String accessToken) {
        if (inited) return;
        String host = "openapi.lechange.cn:443"; // 不带 https://
        InitParams params = new InitParams(app, host, accessToken);
        int ret = LCOpenSDK_Api.initOpenApi(params);
        if (ret != 0) throw new IllegalStateException("initOpenApi failed: " + ret);
        LCOpenSDK_Utils.enableLogPrint(); // 联调期开启,上线可关
        LCOpenSDK_Util.setLogLevel(4);    // debug
        inited = true;
    }
}

accessToken你的后端https://openapi.lechange.cn/openapi/accessToken 换取(有效期 3 天,TK1002 时刷新),不要把 appSecret 写进 APK

踩坑 Ahost 写成 https://openapi.lechange.cn 带协议 → 初始化异常。官方格式为 openapi.lechange.cn:443


3. 后端下发播放凭证:deviceId + playToken + psk

App 登录后请求你自己的接口 /api/devices/{id}/play-auth,后端聚合 OpenAPI:

// 服务端:listDeviceDetailsByPage 取 playToken(加速出流,非必传但强烈建议)
const data = await platformCall('listDeviceDetailsByPage', {
  token, pageSize: 10, page: 1, source: 'bind',
});
const dev = data.deviceList.find(d => d.deviceId === deviceId);
return {
  accessToken,           // 子账号 token 亦可
  deviceId: dev.deviceId,
  channelId: dev.channelList[0].channelId,
  playToken: dev.playToken,   // 设备播放码,opensdk 使用
  psk: dev.encryptMode === '1' ? userCustomKey : dev.deviceId,
  encryptMode: dev.encryptMode,
};

playToken 字段说明见 批量获取设备详细信息非必传,传入可加速拉流——小林验收时补上后,首帧从 5 秒降到 1 秒级。


4. 实时预览:LCOpenSDK_PlayRealWindow

public class LivePlayFragment extends Fragment {
    private LCOpenSDK_PlayRealWindow playWindow;

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        ViewGroup container = view.findViewById(R.id.live_window_content);
        playWindow = new LCOpenSDK_PlayRealWindow();
        playWindow.initPlayWindow(requireContext(), container, 0, false);
        playWindow.setWindowListener(new LCOpenSDK_EventListener() {
            @Override
            public void onPlayerResult(String code, int type, int index) {
                Log.d("IMOU", "preview result code=" + code);
            }
            @Override
            public void onPlayBegan(int index) { /* 出画 */ }
        });
        playWindow.openTouchListener(); // 双指缩放等手势
    }

    public void startPreview(PlayAuth auth) {
        LCOpenSDK_ParamReal param = new LCOpenSDK_ParamReal();
        param.accessToken = auth.accessToken;
        param.deviceId = auth.deviceId;
        param.channel = Integer.parseInt(auth.channelId);
        param.psk = auth.psk;
        param.playToken = auth.playToken;
        param.isOpt = true;                    // 拉流优化,建议 true
        param.streamType = 0;                  // 0 高清主码流,1 辅码流
        playWindow.playRtspReal(param);
    }

    public void stopPreview() {
        if (playWindow != null) {
            playWindow.stopRtspReal(true);
            playWindow.uninitPlayWindow();
        }
    }

    @Override
    public void onDestroyView() {
        stopPreview();
        super.onDestroyView();
    }
}

预览流程(官方 OpenSDK 组件 · 6.1):

initPlayWindow → setWindowListener → playRtspReal
  → onPlayBegan / onPlayerResult
  → stopRtspReal → uninitPlayWindow

踩坑 B:Activity 旋转或切后台未 stopRtspReal → 占用并发带宽(开放平台按播放端累计并发数)。

踩坑 C:NVR 多通道设备 channel 须传具体通道号,单 IPC 一般为 0


5. 录像列表:OpenAPI 查,SDK 播

5.1 设备本地 SD:queryLocalRecords
// 后端:单日查询,勿跨天(易超时,见 Demo 注意事项)
const records = await platformCall('queryLocalRecords', {
  token,
  deviceId: 'TESTQWERXXXX',
  channelId: '0',
  beginTime: '2026-06-10 08:00:00',
  endTime: '2026-06-10 18:00:00',
  type: 'All',
  count: 50,
});
// records[].recordId, beginTime, endTime, fileLength...

分页策略:下次 beginTime = 上次结果最大 endTime + 1 秒(见 queryLocalRecords 文档)。

5.2 云录像:getCloudRecords
const cloud = await platformCall('getCloudRecords', {
  token,
  deviceId: 'TESTQWERXXXX',
  channelId: '0',
  beginTime: '2026-06-10 08:00:00',
  endTime: '2026-06-10 18:00:00',
  count: 30,  // 单次最大 30
});
// cloud.records[].recordId, beginTime, endTime...

通道 csStatus === 'using' 时再查,否则列表常为空。


6. 录像回放:LCOpenSDK_PlayBackWindow

设备录像(按 recordId,推荐)

public void playLocalRecord(PlayAuth auth, String recordId) {
    LCOpenSDK_PlayBackWindow playWindow = new LCOpenSDK_PlayBackWindow();
    playWindow.initPlayWindow(this, findViewById(R.id.live_window_content), 0, true);
    playWindow.setWindowListener(eventListener);
    playWindow.openTouchListener();

    LCOpenSDK_ParamDeviceRecord param = new LCOpenSDK_ParamDeviceRecord();
    param.accessToken = auth.accessToken;
    param.deviceId = auth.deviceId;
    param.channel = Integer.parseInt(auth.channelId);
    param.psk = auth.psk;
    param.fileName = recordId;      // queryLocalRecords 返回的 recordId
    param.offsetTime = 0;
    param.isOpt = true;
    param.isPlayBack = true;
    playWindow.playRtspPlayback(param);
}

云录像

public void playCloudRecord(PlayAuth auth, CloudRecordItem item) {
    LCOpenSDK_PlayBackWindow playWindow = new LCOpenSDK_PlayBackWindow();
    playWindow.initPlayWindow(this, findViewById(R.id.live_window_content), 0, true);
    playWindow.setWindowListener(eventListener);

    LCOpenSDK_ParamCloudRecord param = new LCOpenSDK_ParamCloudRecord();
    param.accessToken = auth.accessToken;
    param.deviceId = auth.deviceId;
    param.channel = Integer.parseInt(auth.channelId);
    param.psk = auth.psk;
    param.recordRegionID = item.recordId;  // getCloudRecords 的 recordId
    param.offsetTime = 0;
    param.isOpt = true;
    playWindow.playCloud(param);
}

停止与释放:

playWindow.stopRtspPlayback(true);  // 本地回放
// 或 playWindow.stopCloud(true);   // 云回放
playWindow.uninitPlayWindow();

踩坑 D(小林黑屏根因):把 LCOpenSDK_ParamReal 或预览窗口用于回放;或 fileName 填了时间字符串而非 recordId

踩坑 E:云回放调 playRtspPlayback、本地回放调 playCloud → 方法用反。


7. 端到端时序(验收用)

用户登录 App
  → 后端 accessToken + 设备列表(含 playToken)
  → LCOpenSDK_Api.initOpenApi
  → 点「实时预览」:PlayRealWindow.playRtspReal
  → 点「录像」Tab:
       后端 queryLocalRecords / getCloudRecords
  → 点某片段:PlayBackWindow.playRtspPlayback / playCloud
  → onDestroy:stop + uninit

验收清单

□ appSecret 不在 APK 内;token 由服务端下发
□ 预览用 PlayRealWindow,回放用 PlayBackWindow
□ playToken 已从 listDeviceDetailsByPage 带入
□ 本地查询不跨天;云存 csStatus = using
□ 页面销毁必 stop + uninitPlayWindow
□ 联调日志 LCOpenSDK_Utils.enableLogPrint()

生产环境:生命周期、并发与架构

  • 云云对接:OpenAPI(列表、云台、告警)走服务端;App 只拿 短期 token + playToken + 录像元数据应用开发 推荐架构)。
  • Token 刷新:管理员 token 3 天有效;SDK 内 accessToken 过期会 TK1002,App 应静默向后端换 token 并 initOpenApi 或更新 Param。
  • 并发与带宽:同一设备多窗口播放计多个并发;列表页缩略图用 picUrl + LCOpenSDK_Utils.decryptPic 解密,勿对未可见通道预拉流。
  • 子账号:多租户 SaaS 用 subAccountToken + 设备授权,预览/回放 Param 传子账号 token。
  • 权限合规:SDK 涉及网络、存储、麦克风、位置等(见资源下载隐私说明),隐私政策须明示。

与 HLS / 轻应用的边界

场景推荐
Android/iOS 原生 App,要低延迟 + 对讲 + 回放OpenSDK
运营后台 Web、快速 PoC云直播 HLS / 轻应用
小程序小程序组件 / 插件

不要在同一 App 里混用 WebView HLS 与 OpenSDK 播同一路设备——用户体验与并发统计都会乱。

排错速查

现象可能原因处理
initOpenApi 失败host 带 https、token 空host=openapi.lechange.cn:443
预览慢未传 playTokenlistDeviceDetailsByPage 取 playToken
回放黑屏窗口类/方法用错PlayBackWindow + 正确 Param
本地列表空无 SD / 跨天查询确认 TF 卡;单日 queryLocalRecords
云列表空云存未生效csStatus === using
TK1002token 过期后端刷新 accessToken
对讲无声未开 RECORD_AUDIOManifest + 运行时权限

Demo 移植建议

官方 Android 标准化 Demo 模块划分清晰:

  • 预览:DeviceOnlineMediaPlayActivityNew.java
  • 回放:DeviceRecordPlayActivityNew.java
  • OpenAPI 封装:DeviceInfoOpenApiManager.java(生产改为服务端)
  • 设备添加:可整模块 DeviceAddModule 接入,含 LCOpenSDK_Api 初始化

下载 Demo 后真机调试,文档 体验 Demo 说明:启动页输入 AppId/AppSecret 仅为体验管理员模式。


小结

Android 集成乐橙视频能力,可以压缩成三句话:

  1. 工程层:AAR + 权限 + LCOpenSDK_Api.initOpenApi,host 为 openapi.lechange.cn:443
  2. 预览层LCOpenSDK_PlayRealWindow + playToken + playRtspReal
  3. 回放层:后端 queryLocalRecords / getCloudRecords 查列表,App 用 LCOpenSDK_PlayBackWindow 分别 playRtspPlayback / playCloud

远程巡店、智慧工地、社区安防——原生 App 要低延迟预览与回放,OpenSDK 私有协议通常是比 WebView HLS 更稳的路径;很多「能播直播不能播录像」的问题,并不是设备故障,而是 列表 API 与 SDK 播放类没有配对

如何开始:访问 乐橙开放平台 open.imou.com 注册开发者并创建移动应用,免费领取 10 路设备接入额度1 Mbps 媒体带宽;在 资源下载 获取 Android SDK 与 Demo,对照 OpenSDK 组件文档标准化 Demo 说明 联调。如需企业并发扩容或多租户方案,可通过平台 商务咨询 / 企业微信 获取支持。以上为项目实践记录,生产以最新文档为准。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值