Appearance
4.2 IPN接口使用
iPanel TV软终端APK支持的私有JS功能接口
4.2.1 IPNBridge对象
4.2.1.1 属性
支持属性常量
javascript{ //RN core版本号 coreVer: string, // Base bundle version //Homed related token: string, // login token deviceNo: string, // login deviceno host: string, // homed host e.g. homed.tv smartCard: string, // ca 卡号 mac: string, // MAC 地址 ip: string, // IP 地址 deviceType: string, // login deviceType uid: string, // homed userId homedLogin: object, // 完整登录信息json对象 //iPanelCloud related deviceId: string, // TV ID appId: string, // APP ID oid: string, // dongle 鉴权后分配的设备ID caId: string, // dongle 鉴权后分配的CAID appKey: string, // APP KEY operatorId: string, // Operator UUID areaId: string, // 用户选择的地址区域ID cloudHost: string, // ipanelCloud 域名 e.g. ipanelcloud2.ipanel.cn cloudUid: string, // iPanelCloud userId roomInfo: object, // 电视所在房间信息:https://git.ipanel.cn/git/liubob/ApiDoc/src/master/accountsrv/2_1_0_accountApi.md#112-roominfo-%E5%AF%B9%E8%B1%A1 loginInfo: object, // Homed授权登录信息, result对象:https://git.ipanel.cn/git/liubob/ApiDoc/src/master/accountsrv/2_1_0_accountApi.md#26-homed%E6%8E%88%E6%9D%83%E7%99%BB%E5%BD%95 //播放器相关 player: string, // 当前版本的播放器类型 dvb, ijk, ctc //系统相关 sysUser: string // 应用是否有系统权限 }属性使用说明
- IPNBridge属性是常量。并不是所有属性一定能取到值,涉及接口交互的数据(如homed登录相关和cloud登录相关)就存在取不到的情况。
- IPNBridge对象在RN引擎启动时创建。如果此时homed和cloud还未登录则相关信息都取不到。
- theme可以配置RN引擎在apk启动过程中(提前)预加载 homed和cloud用户大概率未登录。由于RN引擎在homed和cloud登录前就已经加载完成。IPNBridge对象下属性已经生成。原生后面不会再赋值重写。经常出现进入RN应用后取不到homed和cloud相关信息。
- theme可以配置RN引擎在homed登录后再预加载 基本上可以保证homed和cloud相关数据获取正常。但是也存在登录失败的情况。进入RN应用后,必须要homed或者cloud相关数据,则还是需要做重新登录相关流程。这就涉及到IPNBridge的方法和事件功能的调用支持。
- 常用实例
javascriptimport { View, Text, StyleSheet } from 'react-native' import React from 'react' import IPNBridge from '../../../native/IPNBridge';//相对路径可调整 export default function IPNBridge_prop() { const data = [ { name: 'coreVer', value: IPNBridge.coreVer }, { name: 'deviceId', value: IPNBridge.deviceId }, { name: 'token', value: IPNBridge.token }, { name: 'host', value: IPNBridge.host }, { name: 'cloudUid', value: IPNBridge.cloudUid }, { name: 'appKey', value: IPNBridge.appKey }, { name: 'operatorId', value: IPNBridge.operatorId } ] return ( <View style={styles.app}> { data.map((item, index) => { return (<View key={'list' + index} style={styles.list}> <Text style={styles.name}>{item.name + ':'}</Text><Text style={styles.value}>{item.value}</Text> </View>) }) } </View> ) } const styles = StyleSheet.create({ app: { alignItems: 'center', justifyContent: 'center', }, list: { flexDirection: 'row', width: '80%', marginVertical: 5 }, name: { color: '#fff' }, value: { color: '#ff0' } })
4.2.1.2 方法
支持功能方法
javascript/** * 打印日志到logcat * @param msg */ @ReactMethod function log(String msg) /** * 通过intent uri 进行原生页面跳转 * @param intent */ @ReactMethod function startIntent(String intent) /** * 跳转homed返回的媒资信息 * @param program homed的媒资信息对象 */ @ReactMethod function openProgram(ReadableMap program) /** * 执行函数并异步回调结果 * @param func func uri * @param promise */ @ReactMethod function evalFunc(String func, Promise promise) /** * 发送Otto消息到Java层 * e.g. 发送设备蓝牙操作指令 * clsName com.ipanel.join.homed.stb.otto.OttoBleCmd * data { * 'mac': 'aa-bb-cc-dd-ee', //设备mac * 'cmd': '{"cmd\":"start"}' //json格式的字符串, 具体指令不同设备不同 * } * e.g. 发送消息到playcube模块 * clsName com.ipanel.join.homed.stb.otto.OttoMsgToArt * data { * 'name': '', * 'message': '' * } * @param clsName 原生消息类 * @param data 消息数据结构,需与原生类匹配 */ @ReactMethod function sendOttoMsg(String clsName, ReadableMap data) /** * 将指定按键事件发送到JS处理,处理完成后JS必须调用onKeyEventResult通知结果 * [ * 'KEYCODE_MENU' * ] * @param array 键值名称列表 */ @ReactMethod function setEmitKeyEvents(ReadableArray array) /** * 按键事件处理结果 * @param handled 已处理,原生无需再处理 * @param id 按键id */ @ReactMethod function onKeyEventResult(boolean handled, int id) /** * 退出应用界面 */ @ReactMethod function exitApp() /** * 读取app kv 属性 * @param key * @return */ @ReactMethod(isBlockingSynchronousMethod = true) function appKVGet(String key) /** * 设置app kv 属性 * @param key * @param value */ @ReactMethod(isBlockingSynchronousMethod = true) function appKVSet(String key, String value) /** * 读取系统属性 * @param key * @return String */ @ReactMethod(isBlockingSynchronousMethod = true) function sysPropGet(String key) /** * 设置系统属性 * @param key * @param value */ @ReactMethod function sysPropSet(String key, String value) /** * 读取app 属性 * @param key * @return String */ @ReactMethod(isBlockingSynchronousMethod = true) function appPropGet(String key) /** * 设置app属性 * @param key * @param value */ @ReactMethod function appPropSet(String key, String value) /** * Homed 登录参数 * @return 同 IPNHomedLogin 事件(异步响应Promise promise) */ @ReactMethod function getHomedLogin() /** * Http 代理数据回应 * @param id 唯一请求id * @param status http 状态码 如 200, 404 * @param statusDes http 状态描述 如 OK, NOT FOUND * @param mime content类型, 如 application/json, text/plain * @param text 返回内容 */ @ReactMethod function notifyProxyResp(long id, int status, String statusDes, String mime, String text) /** * 清除未完成的图片加载任务 */ @ReactMethod function clearImageTasks() /** * 获取系统安装的原生apk列表 * @return Promise promise(异步) */ @ReactMethod function getNativeApps()方法使用说明
- IPNBridge.log输出只能到logcat日志;metro服务器实时日志只能通过console.log输出
- 返回Promise对象的代表是异步方法回调返回结果。如:IPNBridge.evalFunc()、IPNBridge.getHomedLogin()。调用前要加await,并且所属函数定义需要加async。
- 常用用例
javascriptimport { Text, StyleSheet, Dimensions, NativeModules, NativeEventEmitter, ToastAndroid } from 'react-native' import React, { Component } from 'react' import IPNBridge from '../../../native/IPNBridge'; import IPNView from '../../../native/IPNView'; import IPNImage from '../../../native/IPNImage'; const data = [ { name: '搜索', iconImg: { normal: 'icon_top_search_normal', focus: 'icon_top_search_focus', }, intent: 'intent://?action=host.OPEN&es__target_=search', //跳转地址 }, { name: '模式', iconImg: { normal: 'icon_top_mode_normal', focus: 'icon_top_mode_focus', }, intent: 'func://com.ipanel.join.homed.stb.ModeSwitchPopup.open', }, { name: 'VIP', iconImg: { normal: 'icon_top_vip_normal', focus: 'icon_top_vip_focus', }, intent: 'intent://send_broadcast/?action=ipanel.bg.ACTION&es_action=local&es_url=func://com.ipanel.join.homed.stb.pay.PaymentManager.show', }, { name: '设置', iconImg: { normal: 'icon_top_setting_normal', focus: 'icon_top_setting_focus', }, type:1 } ] const utils = { WIDTH: Dimensions.get('window').width,//应用显示屏幕宽度 HEIGHT: Dimensions.get('window').height,////应用显示屏幕高度 px: (size, pageWidth = 1920) => {//按1920设计图尺寸进行开发转换 return Math.round(size * (utils.WIDTH / pageWidth)); } } export default class TopEntranceList extends Component { constructor(props) { super(props); this.state = { }; } //组件挂载 componentDidMount() { IPNBridge.setEmitKeyEvents([//注册按键捕获:按键索引需要找旭哥定义的按键映射规则 'KEYCODE_MENU', //菜单键 'KEYCODE_HOME', //主页键(捕获不到) // 'KEYCODE_BACK', //返回键 'KEYCODE_DPAD_RIGHT', //右键 'KEYCODE_DPAD_LEFT', //左键 'KEYCODE_DPAD_UP', //上键 'KEYCODE_DPAD_DOWN', //下键 'KEYCODE_ENTER', //确认键 'KEYCODE_DPAD_CENTER', //确认键 ]); const nativeToRNEmitter = new NativeEventEmitter(NativeModules.IPNBridge); this.nativeKeyEvent = nativeToRNEmitter.addListener( //按键监听 'IPNKeyEvent', (event) => { console.log('IPNKeyEvent---event=', event); if (event.action === 0) {//0:keydown,1:keyup switch (event.name) { case 'KEYCODE_MENU': IPNBridge.onKeyEventResult(true, event.id);//通知apk不要处理菜单键逻辑,RN处理 ToastAndroid.showWithGravity('您按菜单键了', ToastAndroid.SHORT, ToastAndroid.CENTER);//弹提示信息 break; } } }) } //组件销毁 componentWillUnmount() { IPNBridge.setEmitKeyEvents([]);//取消按键捕获注册 this.nativeKeyEvent?.remove();//清理按键监听事件 } // 获取homedLogin数据:异步函数 async getHomedLogin() { let result = await IPNBridge.getHomedLogin(); if (result) { ToastAndroid.showWithGravity(JSON.stringify(result), ToastAndroid.SHORT, ToastAndroid.BOTTOM);//弹提示信息 } } render() { return ( <IPNView style={styles.top} blocked_edges={['left', 'right']}//设置焦点区域内最左或者最右不能继续往左或右移动 focus_finder={true} focus_on_selected_first={true} focus_selector={{//设置焦点图片样式 theme_focus: 'rrect_r30', on_top: true, force_front: true, zoom: 0, }} focus_tag={3}> {data.map((item, index) => { return ( <IPNView key={'topleftbtn' + index} focusable={true} requestTVFocus={index ? false : true}//设置默认焦点,true获焦 onFocus={() => { //获得焦点触发事件 IPNBridge.log('onFocus--IPNBridge.log--index=' + index)//只能输出到logcat日志,metro服务器日志看不到 console.log('onFocus--console.log--index=' + index)//logcat和metro服务器日志都可以看到 }} onClick={() => { // 确定键触发事件 if (item.type === 1) { this.getHomedLogin(); } else if (item?.intent) { IPNBridge.startIntent(item?.intent); } }} onBlur={() => { //失去焦点触发事件 console.log('onBlur---index=' + index) }} style={styles.btnBox} state_bg={{ gradient: {//背景渐变色 normal: {//常态 colors: [ 'rgba(255,255,255,0.08)', 'rgba(255,255,255,0.08)', ], orientation: 'left_right', cornerRadius: utils.px(30), }, focus: {//获焦 colors: ['#667EFF', '#BE52EB'], orientation: 'left_right', cornerRadius: utils.px(30), }, }, }}> {item.iconImg ? ( <IPNImage style={{ width: utils.px(36), height: utils.px(36) }} source={{ theme: { normal: item.iconImg.normal, focus: item.iconImg.focus, }, default: 'theme', }} /> ) : null} <Text style={styles.btn}>{item.name}</Text> </IPNView> ); })} </IPNView> ); } } const styles = StyleSheet.create({ top: { position: 'absolute', top: 0, height: utils.px(100), flexDirection: 'row', alignItems: 'center', marginLeft: utils.px(48), }, btnBox: { height: utils.px(60), alignItems: 'center', justifyContent: 'center', flexDirection: 'row', paddingHorizontal: utils.px(24), marginRight: utils.px(24) }, btn: { fontSize: utils.px(30), color: 'rgba(255,255,255,0.6)', textAlignVertical: 'center', paddingLeft: utils.px(8) } }) ```
4.2.1.3 事件
支持全局事件
javascript/** * <pre> * state 0xCB - 实时数据指令, 0xCC - 返回结果指令 * hasHB 心跳 (只有 0 或 1)(当为 1 时检测到心跳,0 时没有检测到心跳) * press 压力值 * systolic 收缩压(高压)(单位 mmHg) * diastolic 舒张压(低压)(单位 mmHg) * pulse 心率值 * heartAnomaly 心率不齐 (0 或者 1)(1 表示心率不齐) * </pre> */ String HEALTH_BPM = "IPNHealthBPM"; /** * <pre> * deviceType 设备类型:CF 表示脂肪秤,CE 表示人体秤,CB 表示婴儿秤,CA 表示厨房秤 * weight 重量,单位 kg * state HR state 0x80 正在测试, 0xC0 测试完成 * heartRate 次/分钟 * resistent 加密电极阻抗 * </pre> */ String HEALTH_WEIGHT = "IPNHealthWeight"; /** * <pre> * state 0 未连接, 1 已连接, 2 连接异常 * type 0 idu, 3 smit * rcState 0 离线,1 在线 * rcType 0 ble, 1 IOT * rcBattery 电量百分比 * rcCharge 0 未充电, 1 充电中, 2 已充满 * otherState 其他蓝牙外设 0 未连接, 1 已连接 * earphoneState 蓝牙耳机 0 未连接, 1 已连接 * </pre> */ String DONGLE_STATE = "IPNDongleState"; /** * <pre> * mac 设备 mac地址 * type 设备类型 * data 设备数据,不同设备数据结构不同 * </pre> */ String BLE_DEVICE_DATA = "IPNBleDeviceData"; /** * <pre> * 加载业务bundle时发送, 内容为bundle目录的 file uri路径。用于JS端静态资源文件的路径解析 * e.g. file:///data/data/com.ipanel.join.homed.stb/files/react_native/tvsport/2/ * </pre> */ String BUNDLE_CHANGE = "sm-bundle-changed"; /** * 启动参数 * 参数 _new_intent_ 1(仅当onNewIntent时存在) * 参数 _asset_root_ 本地资源根路径 e.g. file:///data/data/com.ipanel.join.homed.stb/files/react_native/tvsport/v1.1.91/ */ String APP_START = "IPNAppStart"; /** * 应用状态变化 * 参数 app_name 应用名称 * 参数 state active/background */ String APP_STATE = "IPNAppState"; /** * Homed登录参数变化, 包含新的登录参数 */ String HOMED_LOGIN = "IPNHomedLogin"; /** * iPanelCloud登录参数变化, 包含新的登录参数 */ String CLOUD_LOGIN = "IPNCloudLogin"; /** * key action * action 0 down 1 up * name e.g. KEYCODE_MENU * code android keyCode * modifier ctrl,alt, shift, meta 状态位 * id keyEvent id, for result notify */ String KEY_EVENT = "IPNKeyEvent"; /** * 具体定义见playcube文档 * name 消息类型名称 * message 消息数据 json 字符串 */ String ART_MESSAGE = "IPNArtMessage"; /** * <pre> * connected true/false 连接状态 * type 网络类型 1000 usb 代理网络 * connectTime 网络连接的系统相对时间 * disconnectTime 网络连接断开的系统相对时间 * </pre> */ String NET_STATE = "IPNNetState"; /** * 支付状态信息 json字符串 */ String PAY_RESULT_Message = "PayResultMessage"; /** * 原生广播 * action 广播动作 * android.intent.action.PACKAGE_ADDED 新的应用安装成功 * android.intent.action.PACKAGE_REPLACED 已安装的应用更新 * android.intent.action.PACKAGE_REMOVED 应用被删除 * ipanel.action.INSTALL_RESULT 应用安装结果, 参数名: result 类型:boolean * extras 参数 */ String BROADCAST = "IPNBroadcast";事件使用说明
- 事件监听模块固定为NativeModules.IPNBridge。
- React-Navigation切换页面时,尽量切换前后对事件进行释放和重新监听操作。否则A页面跳转到B页面,A页面监听的事件还能收到。
- 常用用例
javascriptimport { NativeEventEmitter, NativeModules } from 'react-native'; import { name as APPNAME } from '../../package.json';//读取应用模块名 let IPNAppStateEvent = null; /* 开启事件监听*/ function listenerIPNAppState() { console.log('[IPNAppState]---listener---AppState---'); /** * 应用状态变化 * 参数 app_name 应用名称 * 参数 state active/background */ const nativeToRNEmitter = new NativeEventEmitter(NativeModules.IPNBridge); IPNAppStateEvent = nativeToRNEmitter.addListener('IPNAppState', (event) => { console.log('[IPNAppState]---IPNAppState---event=' + JSON.stringify(event)); if (event?.app_name === APPNAME) {//每个RN应用前后台切换时都会发送该事件。需要记录本RN应用的显示状态 global[APPNAME]['appState'] = event?.state; } }); } // 释放事件监听 function releaseIPNAppState() { console.log('[IPNAppState]---releaseNativeMessage---'); IPNAppStateEvent?.remove(); } export { listenerIPNAppState, releaseIPNAppState };
4.2.2 返回键统一逻辑
目前RN模块是单引擎机制,当多个RN应用都在运行时,只要监听事件,都能收到同一个消息。
后台运行时交给原生处理 1、前台应用如果需要处理逻辑,则会将按键截止,不会将消息流到后台应用;如果不处理留下来则就是希望原生底层去处理,返回到上一个页面场景。 2、基于IPNAppState消息每个RN应用记录当前是否在前台/后台状态。在前台时,才需处理返回键逻辑;在后台时,则需将按键交给原生处理并终止逻辑。(否则react navigation会执行导航的返回逻辑及多次执行返回逻辑问题)
实例源码
//监听返回键事件
this.backHandler = BackHandler.addEventListener('hardwareBackPress',this.backHandlerListener);
//返回键处理函数
backHandlerListener = () => {
//global是全局存储对象,APPNAME是当前应用名称,appState是状态变量
if (global[APPNAME]['appState'] === 'background') {//在后台时
BackHandler.exitApp();
return true; //20240726后台运行时,直接调用原生退出方法由原生处理并return true。否则react navigation会执行导航的返回逻辑。
}
//其他返回键逻辑
...
}作业
- 题目 开发一个RN应用,实现一个首页(直接用前一堂课的首页页面)和一个点播播放页,可相互跳转。
- 要求 1、采用react-navigation导航跳转RN页面。 2、首页用上一节课实现的页面,增加确定键功能,对接点播、回看、应用和直播功能。点播节目进入点播播放页;应用节目读取extend_content.androidurl是intent://通过IPNBridge.startIntent跳转;其他节目通过IPNBridge.openProgram跳转。 3、点播播放页实现接收节目id参数和剧集标记。剧集通过接口/media/series/get_info获取剧集列表video_list中第一个单集id,再通过/media/video/get_info获取playtoken进行拼接播放地址播放;单集直接通过/media/video/get_info接口获取playtoken后拼接播放地址播放。 4、通过IPNKeyEvent事件捕获左右键,实现点播播放进度条左右按键可进行seek播放,确定键可暂停和恢复播放等基本功能。并且播放结束接着从头播放。 5、accesstoken取IPNBridge.token,verifycode取IPNBridge.deviceId 6、点播播放地址取http://拼接规则:7、播放页效果如下图
function joinPlayUrl(_proObj){ let url = ''; const urls = _proObj.demand_url.filter(item => item.includes('http://')); if (urls&&urls[0]) { url += '?protocol=http&playtype=demand&accesstoken='+IPNBridge.token+'&playtoken='+_proObj.play_token+'&verifycode='+IPNBridge.deviceId } return url; }
