HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

关于隐私政策弹窗经验分享

应用上架 隐私政策 App

首先来说按文档配置相关隐私协议 https://ask.dcloud.net.cn/article/39073
我是用的HX3.2.3进行打包的,点击manifest.json=》APP启动页面配置+》使用原生隐私政策提示框,勾选后会在项目中自动添加androidPrivacy.json文件
注意!androidPrivacy.json不要添加注释,会影响隐私政策提示框的显示!!
androidPrivacy.json文件如下:
{
"version": "1", (这里个人建议和应用同步更新版本号,因为每次你更新应用后都需要更新版本号)
"prompt": "template",
"title": "服务协议和隐私政策",
"message": "  请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/>  你可阅读<a href=\"\">《服务协议》</a>和<a href=\"\">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。",
"buttonAccept": "同意并接受",
"buttonRefuse": "暂不同意",
"second": {
"title": "确认提示",
"message": "  进入应用前,你需先同意<a href=\"\">《服务协议》</a>和<a href=\"\">《隐私政策》</a>,否则将退出应用。",
"buttonAccept": "同意并继续",
"buttonRefuse": "退出应用"
},
"styles": {
"backgroundColor": "#00FF00",
"borderRadius":"5px",
"title": {
"color": "#ff00ff"
},
"buttonAccept": {
"color": "#ffff00"
},
"buttonRefuse": {
"color": "#00ffff"
}
}
}
message中的巨坑来了!!!划重点,一定一定一定(重要的事情说三遍)要注意这里,不然坑到你哭。
一定一定一定要将“/”做转义,例如:<a href=\"https:\/\/www.baidu.com\/\">(简直坑死人不偿命)普通的“/”就需要转义成“\/”,如果最后是以.html结束的后面不需要转义了,等下看截图。
系统给的背景颜色和按钮颜色,自己想改就改。
最后如果自定义基座调试运行没得弹窗,不要紧!!!安心的打个包,试一下就好了。
欢迎大家给与建议和更好的体验,我也是第一次用HX写APP。如果对你有帮助,记得一键三连。

继续阅读 »

首先来说按文档配置相关隐私协议 https://ask.dcloud.net.cn/article/39073
我是用的HX3.2.3进行打包的,点击manifest.json=》APP启动页面配置+》使用原生隐私政策提示框,勾选后会在项目中自动添加androidPrivacy.json文件
注意!androidPrivacy.json不要添加注释,会影响隐私政策提示框的显示!!
androidPrivacy.json文件如下:
{
"version": "1", (这里个人建议和应用同步更新版本号,因为每次你更新应用后都需要更新版本号)
"prompt": "template",
"title": "服务协议和隐私政策",
"message": "  请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/>  你可阅读<a href=\"\">《服务协议》</a>和<a href=\"\">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。",
"buttonAccept": "同意并接受",
"buttonRefuse": "暂不同意",
"second": {
"title": "确认提示",
"message": "  进入应用前,你需先同意<a href=\"\">《服务协议》</a>和<a href=\"\">《隐私政策》</a>,否则将退出应用。",
"buttonAccept": "同意并继续",
"buttonRefuse": "退出应用"
},
"styles": {
"backgroundColor": "#00FF00",
"borderRadius":"5px",
"title": {
"color": "#ff00ff"
},
"buttonAccept": {
"color": "#ffff00"
},
"buttonRefuse": {
"color": "#00ffff"
}
}
}
message中的巨坑来了!!!划重点,一定一定一定(重要的事情说三遍)要注意这里,不然坑到你哭。
一定一定一定要将“/”做转义,例如:<a href=\"https:\/\/www.baidu.com\/\">(简直坑死人不偿命)普通的“/”就需要转义成“\/”,如果最后是以.html结束的后面不需要转义了,等下看截图。
系统给的背景颜色和按钮颜色,自己想改就改。
最后如果自定义基座调试运行没得弹窗,不要紧!!!安心的打个包,试一下就好了。
欢迎大家给与建议和更好的体验,我也是第一次用HX写APP。如果对你有帮助,记得一键三连。

收起阅读 »

uniapp获取某段时间的通话记录

plusReady(){  
                // 在这里调用plus api    
                var CallLog = plus.android.importClass("android.provider.CallLog");  
                var main = plus.android.runtimeMainActivity();  
                var obj = main.getContentResolver();  
                plus.android.importClass(obj);  

                var selection = {  
                    mobile: '',  
                    startTime: new Date('2021-9-1').getTime(),  
                    endTime: new Date('2021-9-16').getTime(),  
                    type: ''  
                }  
                //查询  
                var sql = '';  
                var keyword = '';    
                for (let item of Object.keys(selection)) {    
                    if (selection[item] != '') {    
                        switch (item) {    
                            case 'mobile':    
                                sql = sql == '' ? sql : sql + ' and ';    
                                sql = sql + CallLog.Calls.NUMBER + " like '%" + selection[item] + "%'";    
                                keyword = selection[item];    
                                break;    
                            case 'startTime':    
                                sql = sql == '' ? sql : sql + ' and ';    
                                sql = sql + CallLog.Calls.DATE + '>=' + selection[item];    
                                break;    
                            case 'endTime':    
                                sql = sql == '' ? sql : sql + ' and ';    
                                sql = sql + CallLog.Calls.DATE + '<=' + selection[item];    
                                break;    
                            case 'type':    
                                sql = sql == '' ? sql : sql + ' and ';    
                                sql = sql + CallLog.Calls.TYPE + '=' + selection[item];    
                                break;    
                        }    
                    }    
                }    
                sql = sql != '' ? sql : null;    

                var cursor = obj.query(  
                    CallLog.Calls.CONTENT_URI,  
                    null,   
                    sql,   //传入条件  
                    null,   
                    null  
                );  
                plus.android.importClass(cursor);  
                var content = []; // 用来存储数据  
                var count = 0; // 记录多少条  
                if (cursor.moveToFirst()) {  
                    while (cursor.moveToNext()) {  
                        count++;  
                        //号码  
                        var number = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));  
                        //呼叫类型  
                        var type;  
                        switch (  
                            parseInt(cursor.getString(cursor.getColumnIndex(CallLog.Calls.TYPE))))  
                        // 判断通话类型  
                        {  
                            case CallLog.Calls.INCOMING_TYPE:  
                                type = "呼入";  
                                break;  
                            case CallLog.Calls.OUTGOING_TYPE:  
                                type = "呼出";  
                                break;  
                            case CallLog.Calls.MISSED_TYPE:  
                                type = "未接";  
                                break;  
                            default:  
                                type = "挂断"; //应该是挂断.根据我手机类型判断出的  
                                break;  
                        }  
                        // 获取时间  
                        var date = new Date(parseInt(  
                            cursor.getString(cursor.getColumnIndexOrThrow(CallLog.Calls.DATE))));  
                        // 联系人  
                        var Name_Col = cursor.getColumnIndexOrThrow(CallLog.Calls.CACHED_NAME);  
                        var name = cursor.getString(Name_Col);  
                        // 号码归属地 返回:北京 联通  
                        var numberLocation = cursor.getString(  
                            cursor.getColumnIndex(CallLog.Calls.GEOCODED_LOCATION)  
                        );  
                        //通话时间,单位:s  
                        var Duration_Col = cursor.getColumnIndexOrThrow(CallLog.Calls.DURATION);  
                        var duration = cursor.getString(Duration_Col);  
                        // 存入数组   
                        content.push({  
                            name: name, // 联系人的姓名  
                            mobile: number, // 联系人电话  
                            numberLocation: numberLocation, // 号码的归属地  
                            // callTime: new Date().getTime(date), // 呼入或呼出时间  
                            callTime: this.formatDate(date),  // 呼入或呼出时间  
                            talkTime: duration, // 通话时长  
                            type: type  
                        });  
                        // 查询50条 就跳出  
                        // if (count > 50) {  
                        //  break;  
                        // }  
                    }  
                }  
                console.log(JSON.stringify(content));  
                this.tapeList = content  
            },
继续阅读 »
plusReady(){  
                // 在这里调用plus api    
                var CallLog = plus.android.importClass("android.provider.CallLog");  
                var main = plus.android.runtimeMainActivity();  
                var obj = main.getContentResolver();  
                plus.android.importClass(obj);  

                var selection = {  
                    mobile: '',  
                    startTime: new Date('2021-9-1').getTime(),  
                    endTime: new Date('2021-9-16').getTime(),  
                    type: ''  
                }  
                //查询  
                var sql = '';  
                var keyword = '';    
                for (let item of Object.keys(selection)) {    
                    if (selection[item] != '') {    
                        switch (item) {    
                            case 'mobile':    
                                sql = sql == '' ? sql : sql + ' and ';    
                                sql = sql + CallLog.Calls.NUMBER + " like '%" + selection[item] + "%'";    
                                keyword = selection[item];    
                                break;    
                            case 'startTime':    
                                sql = sql == '' ? sql : sql + ' and ';    
                                sql = sql + CallLog.Calls.DATE + '>=' + selection[item];    
                                break;    
                            case 'endTime':    
                                sql = sql == '' ? sql : sql + ' and ';    
                                sql = sql + CallLog.Calls.DATE + '<=' + selection[item];    
                                break;    
                            case 'type':    
                                sql = sql == '' ? sql : sql + ' and ';    
                                sql = sql + CallLog.Calls.TYPE + '=' + selection[item];    
                                break;    
                        }    
                    }    
                }    
                sql = sql != '' ? sql : null;    

                var cursor = obj.query(  
                    CallLog.Calls.CONTENT_URI,  
                    null,   
                    sql,   //传入条件  
                    null,   
                    null  
                );  
                plus.android.importClass(cursor);  
                var content = []; // 用来存储数据  
                var count = 0; // 记录多少条  
                if (cursor.moveToFirst()) {  
                    while (cursor.moveToNext()) {  
                        count++;  
                        //号码  
                        var number = cursor.getString(cursor.getColumnIndex(CallLog.Calls.NUMBER));  
                        //呼叫类型  
                        var type;  
                        switch (  
                            parseInt(cursor.getString(cursor.getColumnIndex(CallLog.Calls.TYPE))))  
                        // 判断通话类型  
                        {  
                            case CallLog.Calls.INCOMING_TYPE:  
                                type = "呼入";  
                                break;  
                            case CallLog.Calls.OUTGOING_TYPE:  
                                type = "呼出";  
                                break;  
                            case CallLog.Calls.MISSED_TYPE:  
                                type = "未接";  
                                break;  
                            default:  
                                type = "挂断"; //应该是挂断.根据我手机类型判断出的  
                                break;  
                        }  
                        // 获取时间  
                        var date = new Date(parseInt(  
                            cursor.getString(cursor.getColumnIndexOrThrow(CallLog.Calls.DATE))));  
                        // 联系人  
                        var Name_Col = cursor.getColumnIndexOrThrow(CallLog.Calls.CACHED_NAME);  
                        var name = cursor.getString(Name_Col);  
                        // 号码归属地 返回:北京 联通  
                        var numberLocation = cursor.getString(  
                            cursor.getColumnIndex(CallLog.Calls.GEOCODED_LOCATION)  
                        );  
                        //通话时间,单位:s  
                        var Duration_Col = cursor.getColumnIndexOrThrow(CallLog.Calls.DURATION);  
                        var duration = cursor.getString(Duration_Col);  
                        // 存入数组   
                        content.push({  
                            name: name, // 联系人的姓名  
                            mobile: number, // 联系人电话  
                            numberLocation: numberLocation, // 号码的归属地  
                            // callTime: new Date().getTime(date), // 呼入或呼出时间  
                            callTime: this.formatDate(date),  // 呼入或呼出时间  
                            talkTime: duration, // 通话时长  
                            type: type  
                        });  
                        // 查询50条 就跳出  
                        // if (count > 50) {  
                        //  break;  
                        // }  
                    }  
                }  
                console.log(JSON.stringify(content));  
                this.tapeList = content  
            },
收起阅读 »

Unipush后台推送使用指南

unipush

亲爱的开发者:

Unipush推送服务于2021年9月14日晚重磅升级,优化了整体页面布局,全方位提升消息推送用户体验。

【消息推送】简介

一、 功能介绍

  1. 推送的消息主要分为三种:通知消息、透传消息以及分组对比。

  2. 通知消息即是通知发送后会直接在手机通知栏(状态栏)展示一条消息,Unipush帮助将消息送达到客户端并处理消息的展示方式为通知栏消息。

  3. 透传消息,即是自定义消息,Unipush只负责将消息送达到客户端,而客户端接收到消息后需要自己处理消息的展示方式或后续动作。

  4. 分组对比,支持对比通知文案及对比用户群体,小规模测试最佳文案及最佳用户群。

二、 使用场景

  1. 推送活动通知,如促销、活动、红包、优惠券等

  2. 推送资讯热点,如资讯、新闻、热点、赛事等

  3. 推送关注动态,如关注、订阅、收藏、加购物车等

  4. 推送系统通知,如账户状态、安全、支付、物流等

三、 操作步骤

【消息推送】通知消息

功能介绍:

通知消息即是通知发送后会直接在手机通知栏(状态栏)展示一条消息,Unipush帮助将消息送达到客户端并处理消息的展示方式为通知栏消息,并且支持启动应用、打开第三方链接、打开应用内页面、附加消息四种后续动作形式。

操作步骤:

Step 1:点击【创建推送】-【通知消息】,根据小图标的解释说明填写好目标平台、通知标题、通知内容、目标选择、消息有效时长、短信补量、广告投放等信息。


Step 2:设置通知消息在各个平台的通知展示形式。在Android平台,可通过【通知渠道模板】设置响铃、震动、浮动、唤醒屏幕等通知提醒方式;通过【后续动作】设置后续动作为启动应用、打开第三方链接、打开应用内特定页面及附加消息,附加消息的形式为透传消息;通过【展开式通知】设置通知消息下拉展开的形式为文本、大图;通过【通知栏图标】设置通知消息到达通知栏的图标为默认图标、APP本地图标、上传图片、指定图片路径;通过【角标数字】设置通知消息到达客户端后,角标数字的增量。在iOS平台,也可进行相应设置。

Step 3:查看推送预览,检查确认各项设置是否正确,也可以选择发送测试推送到手机上进行测试预览。确认无误后,点击【确定】,正式发送推送。

【消息推送】透传消息

功能介绍:

透传消息,即是自定义消息,Unipush只负责将消息送达到客户端,而客户端接收到消息后需要自己处理消息的展示方式或后续动作。

操作步骤:

Step 1:点击【创建推送】-【透传消息】,根据小图标的解释说明填写好目标平台、通知标题、通知内容、目标选择、消息有效时长等信息以及在各个平台的通知展示方式及后续动作。然后点击【预览】


Step 2:查看推送预览,检查确认各项设置是否正确,也可以选择发送测试推送到手机上进行测试预览。确认无误后,点击【确定】,正式发送推送。

【消息推送】分组对比

功能介绍:

分组对比功能支持在同一个推送计划中,添加2-5条文案/人群进行对比测试,通过对各测试组的到达、展示、点击数据的监测分析,筛选出最优文案/人群,并支持实时及定时地按最优测试组补发剩余用户,提高通知消息点击率,助力运营提效。

操作步骤:

Step 1:点击【创建推送】-【分组对比】,填写测试名称、选择测试模式、目标平台、测试人群、补发策略、推送设置等 ,点击【测试预览】


Step 2:查看推送预览,检查确认各项设置是否正确。确认无误后,点击【确定】,正式发送推送。

Step 3:点击【数据统计】-【推送记录】,管理测试任务,可选择停止测试、取消补发、手动补发等

【数据统计】推送数据

功能介绍:

【推送数据】记录所有推送任务的记录(包括推送通知记录、透传记录、分组对比记录、API数据记录),显示每个推送任务的详情、目标用户、后续动作、状态等相关信息,也能够查看某次推送的详细数据及转化情况。

【折损原因分析】针对单个推送任务,提供消息从下发-到达-展示-点击各阶段,消息未成功触达的折损原因分析,比如:卸载、关闭通知、推送超限、参数超限、参数无效等。快速诊断定位折损原因,并给出针对性的建议解决方案。同时支持开发者导出未收到消息的用户折损列表,便于针对这部分用户做后续触达转化,提高运营效率。

【大盘分析】针对单个推送任务,提供每个任务的消息到达率和点击率,与应用自身的平均值及APP所属分类Top20个推应用的均值做比较,高效评估该推送任务的转化水平,调整推送运营策略。

名词解释:

  1. 百日内联网用户数(可推送用户数):3个月内活跃的用户数,即连网过Unipush平台的用户数。

  2. 实际下发数:本次推送任务中,由Unipush服务器实际发出的消息总数。该数据实时更新。

  3. 到达数:通知到达用户手机后,UnipushSDK上报给Unipush服务器的到达回执总数。该数据实时更新。

  4. 展示数:通知到达用户手机后由UnipushSDK正确解析并展示在用户手机通知栏的展示回执总数。该数据实时更新。对于透传消息由于是APP客户端自行处理展示,所以Unipush无法统计其展示数,可以用自定义事件来统计,先定义好自定义事件的名称及编号,在需要埋点的地方调用对应方法来提交统计(详情咨询Unipush技术支持)。

  5. 点击数:通知展示在用户手机通知栏后,点击该通知的用户总数。该数据实时更新。对于透传消息由于是APP客户端自行处理展示,所以Unipush无法统计其点击数,可以用自定义事件来统计,先定义好自定义事件的名称及编号,在需要埋点的地方调用对应方法来提交统计(详情咨询Unipush技术支持)。


【数据统计】日推送数

功能介绍:

您所有推送任务(包括推送通知、透传消息、AB测试以及API 推送)的所有数据总和(过滤无效用户后)。

名词解释:

  1. 推送趋势:过去24小时内,所有推送任务的总体下发情况。每半小时刷新一次。

  2. 到达趋势:过去24小时内,所有推送任务的总体到达情况。每半小时刷新一次。

  3. 点击趋势:过去24小时内,所有推送任务的总体点击情况。每半小时刷新一次。

【数据统计】用户数据

功能介绍:

用于展示当前所选择应用的用户数据,包括新增用户数(峰值)、次日留存率、在线用户数(峰值)、日联网用户数、关闭通知率、卸载用户数的数据。通过用户数据可以更加了解您应用的在线情况。

名词解释

  1. 新增用户数:当日新增与Unipush服务器保持连接的用户数。
  2. 次日留存率:新用户在第二天的使用情况比例。
  3. 在线用户数(峰值):当前与Unipush服务器保持连接的用户数的峰值,每10分钟统计一次。
  4. 活跃用户数:当日内Unipush推送SDK与Unipush服务器建立过长链接的用户总数量
  5. 日联网用户数:当日所有连接过Unipush服务器的用户总数。
  6. 关闭通知率:活跃用户中,通知栏消息提示关闭的用户数占比。
  7. 卸载用户数:每周卸载该应用的用户数(此处统计的为iOS用户的卸载数)。

【应用概览】

提供用户数据和应用维度的推送数据的总览

【配置管理】故障排查

功能介绍:

您可以查询CID是否在线,CID绑定的devicetoken等信息,还可以进行推送测试,推送路径查询

状态查询:检测当前应用下的CID是否在线、包名、devicetoken等信息

推送测试:在集成过程中进行单推测试,可推送通知和透传消息

消息链路查询:查询某任务下某cid的具体实时推送路径情况

【配置管理】用户分组

功能介绍:

用户分组指利用Unipush提供的大数据能力,根据用户性别、偏好、所在区域等属性将用户进行分组,形成分组后,在创建推送消息时,可以将推送消息发给该分组,免去每次筛选的麻烦。

操作步骤:

Step 1:点击【配置管理】-【用户分组】-【添加用户分组】


Step 2:填写用户分组名称,选择用户属性设置,点击【确定】

【配置管理】别名管理

功能介绍:

Unipush使用ClientID来标识每个独立的用户,但ClientID不等于开发者应用上的用户名。别名是开发者根据自身需求为每个用户设定的标识,建议对不同用户设定不同别名,保证可通过别名来唯一确认某特定用户。适用于和APP内部数据打通,例如可将用户的邮箱、昵称、手机号等设为别名,即可通过邮箱、昵称、手机号指定目标用户。Unipush支持单个添加或批量导入别名。

别名规则:

  1. 有效的别名组成:字母(区分大小写)、数字、下划线、汉字。

  2. 任务备注名长度限制为 40 字节。( UTF-8 )

  3. 一个别名最多允许绑定10个ClientID。

  4. 批量上传文件格式如下,每行一条,前面为别名,后面为ClientID,使用英文逗号分隔。(示例:别名1,14c3a47a37461ssass7384e88de32d8)

操作步骤:

点击【配置管理】-【别名管理】-【添加别名】或者【批量导入】-【确定】


【配置管理】应用标签

功能介绍:

支持APP应用通过UnipushSDK接口自行设置应用标签,每个用户(通过CID来标识 )最多支持100个标签,例如“喜爱足球”,“喜爱动漫”。推送时指定某一标签推送,即可推送给所有打上此标签的用户。

【配置管理】渠道模板

功能介绍:

渠道模板用于设置消息下发的提醒方式:响铃、震动、浮动、唤醒屏幕等

操作步骤:

点击【配置管理】-【渠道模板】-【添加渠道模板】,填写模板名称、渠道ID、渠道名称、通知优先级、提醒方式,点击【确定】


【配置管理】短信配置

功能介绍:

Unipush有提供短信补量服务(在创建推送设置里面可以设置选择),针对 App 侧一段时间内无法收到推送的消息(可能由于断网、后台禁止运行、消息延迟等原因)的用户,进行短信通道信息补充。而针对提供的短信通道补充服务,则支持对短信签名以及短信模板进行设置。

操作步骤:

Step 1:配置短信签名

点击【配置管理】-【短信配置】-【签名配置】-【新增签名】,然后填写签名内容,点击【确定】


Step 2:配置短信模板

点击【配置管理】-【短信配置】-【模板配置】-【新增模板】,填写好签名内容、模板类型、模板名称、模板内容等信息,点击【确定】


【自定义事件】

功能介绍:

Unipush透传及部分厂商返回的推送报表数据不包含展示数和点击数。可通过自定义事件来统计,先定义好自定义事件的名称及编号,在需要埋点的地方,调用对应方法来提交统计,补全相应数据。也可用于应用内部的某个事件统计,如点击某个按钮、跳转动作等。


继续阅读 »

亲爱的开发者:

Unipush推送服务于2021年9月14日晚重磅升级,优化了整体页面布局,全方位提升消息推送用户体验。

【消息推送】简介

一、 功能介绍

  1. 推送的消息主要分为三种:通知消息、透传消息以及分组对比。

  2. 通知消息即是通知发送后会直接在手机通知栏(状态栏)展示一条消息,Unipush帮助将消息送达到客户端并处理消息的展示方式为通知栏消息。

  3. 透传消息,即是自定义消息,Unipush只负责将消息送达到客户端,而客户端接收到消息后需要自己处理消息的展示方式或后续动作。

  4. 分组对比,支持对比通知文案及对比用户群体,小规模测试最佳文案及最佳用户群。

二、 使用场景

  1. 推送活动通知,如促销、活动、红包、优惠券等

  2. 推送资讯热点,如资讯、新闻、热点、赛事等

  3. 推送关注动态,如关注、订阅、收藏、加购物车等

  4. 推送系统通知,如账户状态、安全、支付、物流等

三、 操作步骤

【消息推送】通知消息

功能介绍:

通知消息即是通知发送后会直接在手机通知栏(状态栏)展示一条消息,Unipush帮助将消息送达到客户端并处理消息的展示方式为通知栏消息,并且支持启动应用、打开第三方链接、打开应用内页面、附加消息四种后续动作形式。

操作步骤:

Step 1:点击【创建推送】-【通知消息】,根据小图标的解释说明填写好目标平台、通知标题、通知内容、目标选择、消息有效时长、短信补量、广告投放等信息。


Step 2:设置通知消息在各个平台的通知展示形式。在Android平台,可通过【通知渠道模板】设置响铃、震动、浮动、唤醒屏幕等通知提醒方式;通过【后续动作】设置后续动作为启动应用、打开第三方链接、打开应用内特定页面及附加消息,附加消息的形式为透传消息;通过【展开式通知】设置通知消息下拉展开的形式为文本、大图;通过【通知栏图标】设置通知消息到达通知栏的图标为默认图标、APP本地图标、上传图片、指定图片路径;通过【角标数字】设置通知消息到达客户端后,角标数字的增量。在iOS平台,也可进行相应设置。

Step 3:查看推送预览,检查确认各项设置是否正确,也可以选择发送测试推送到手机上进行测试预览。确认无误后,点击【确定】,正式发送推送。

【消息推送】透传消息

功能介绍:

透传消息,即是自定义消息,Unipush只负责将消息送达到客户端,而客户端接收到消息后需要自己处理消息的展示方式或后续动作。

操作步骤:

Step 1:点击【创建推送】-【透传消息】,根据小图标的解释说明填写好目标平台、通知标题、通知内容、目标选择、消息有效时长等信息以及在各个平台的通知展示方式及后续动作。然后点击【预览】


Step 2:查看推送预览,检查确认各项设置是否正确,也可以选择发送测试推送到手机上进行测试预览。确认无误后,点击【确定】,正式发送推送。

【消息推送】分组对比

功能介绍:

分组对比功能支持在同一个推送计划中,添加2-5条文案/人群进行对比测试,通过对各测试组的到达、展示、点击数据的监测分析,筛选出最优文案/人群,并支持实时及定时地按最优测试组补发剩余用户,提高通知消息点击率,助力运营提效。

操作步骤:

Step 1:点击【创建推送】-【分组对比】,填写测试名称、选择测试模式、目标平台、测试人群、补发策略、推送设置等 ,点击【测试预览】


Step 2:查看推送预览,检查确认各项设置是否正确。确认无误后,点击【确定】,正式发送推送。

Step 3:点击【数据统计】-【推送记录】,管理测试任务,可选择停止测试、取消补发、手动补发等

【数据统计】推送数据

功能介绍:

【推送数据】记录所有推送任务的记录(包括推送通知记录、透传记录、分组对比记录、API数据记录),显示每个推送任务的详情、目标用户、后续动作、状态等相关信息,也能够查看某次推送的详细数据及转化情况。

【折损原因分析】针对单个推送任务,提供消息从下发-到达-展示-点击各阶段,消息未成功触达的折损原因分析,比如:卸载、关闭通知、推送超限、参数超限、参数无效等。快速诊断定位折损原因,并给出针对性的建议解决方案。同时支持开发者导出未收到消息的用户折损列表,便于针对这部分用户做后续触达转化,提高运营效率。

【大盘分析】针对单个推送任务,提供每个任务的消息到达率和点击率,与应用自身的平均值及APP所属分类Top20个推应用的均值做比较,高效评估该推送任务的转化水平,调整推送运营策略。

名词解释:

  1. 百日内联网用户数(可推送用户数):3个月内活跃的用户数,即连网过Unipush平台的用户数。

  2. 实际下发数:本次推送任务中,由Unipush服务器实际发出的消息总数。该数据实时更新。

  3. 到达数:通知到达用户手机后,UnipushSDK上报给Unipush服务器的到达回执总数。该数据实时更新。

  4. 展示数:通知到达用户手机后由UnipushSDK正确解析并展示在用户手机通知栏的展示回执总数。该数据实时更新。对于透传消息由于是APP客户端自行处理展示,所以Unipush无法统计其展示数,可以用自定义事件来统计,先定义好自定义事件的名称及编号,在需要埋点的地方调用对应方法来提交统计(详情咨询Unipush技术支持)。

  5. 点击数:通知展示在用户手机通知栏后,点击该通知的用户总数。该数据实时更新。对于透传消息由于是APP客户端自行处理展示,所以Unipush无法统计其点击数,可以用自定义事件来统计,先定义好自定义事件的名称及编号,在需要埋点的地方调用对应方法来提交统计(详情咨询Unipush技术支持)。


【数据统计】日推送数

功能介绍:

您所有推送任务(包括推送通知、透传消息、AB测试以及API 推送)的所有数据总和(过滤无效用户后)。

名词解释:

  1. 推送趋势:过去24小时内,所有推送任务的总体下发情况。每半小时刷新一次。

  2. 到达趋势:过去24小时内,所有推送任务的总体到达情况。每半小时刷新一次。

  3. 点击趋势:过去24小时内,所有推送任务的总体点击情况。每半小时刷新一次。

【数据统计】用户数据

功能介绍:

用于展示当前所选择应用的用户数据,包括新增用户数(峰值)、次日留存率、在线用户数(峰值)、日联网用户数、关闭通知率、卸载用户数的数据。通过用户数据可以更加了解您应用的在线情况。

名词解释

  1. 新增用户数:当日新增与Unipush服务器保持连接的用户数。
  2. 次日留存率:新用户在第二天的使用情况比例。
  3. 在线用户数(峰值):当前与Unipush服务器保持连接的用户数的峰值,每10分钟统计一次。
  4. 活跃用户数:当日内Unipush推送SDK与Unipush服务器建立过长链接的用户总数量
  5. 日联网用户数:当日所有连接过Unipush服务器的用户总数。
  6. 关闭通知率:活跃用户中,通知栏消息提示关闭的用户数占比。
  7. 卸载用户数:每周卸载该应用的用户数(此处统计的为iOS用户的卸载数)。

【应用概览】

提供用户数据和应用维度的推送数据的总览

【配置管理】故障排查

功能介绍:

您可以查询CID是否在线,CID绑定的devicetoken等信息,还可以进行推送测试,推送路径查询

状态查询:检测当前应用下的CID是否在线、包名、devicetoken等信息

推送测试:在集成过程中进行单推测试,可推送通知和透传消息

消息链路查询:查询某任务下某cid的具体实时推送路径情况

【配置管理】用户分组

功能介绍:

用户分组指利用Unipush提供的大数据能力,根据用户性别、偏好、所在区域等属性将用户进行分组,形成分组后,在创建推送消息时,可以将推送消息发给该分组,免去每次筛选的麻烦。

操作步骤:

Step 1:点击【配置管理】-【用户分组】-【添加用户分组】


Step 2:填写用户分组名称,选择用户属性设置,点击【确定】

【配置管理】别名管理

功能介绍:

Unipush使用ClientID来标识每个独立的用户,但ClientID不等于开发者应用上的用户名。别名是开发者根据自身需求为每个用户设定的标识,建议对不同用户设定不同别名,保证可通过别名来唯一确认某特定用户。适用于和APP内部数据打通,例如可将用户的邮箱、昵称、手机号等设为别名,即可通过邮箱、昵称、手机号指定目标用户。Unipush支持单个添加或批量导入别名。

别名规则:

  1. 有效的别名组成:字母(区分大小写)、数字、下划线、汉字。

  2. 任务备注名长度限制为 40 字节。( UTF-8 )

  3. 一个别名最多允许绑定10个ClientID。

  4. 批量上传文件格式如下,每行一条,前面为别名,后面为ClientID,使用英文逗号分隔。(示例:别名1,14c3a47a37461ssass7384e88de32d8)

操作步骤:

点击【配置管理】-【别名管理】-【添加别名】或者【批量导入】-【确定】


【配置管理】应用标签

功能介绍:

支持APP应用通过UnipushSDK接口自行设置应用标签,每个用户(通过CID来标识 )最多支持100个标签,例如“喜爱足球”,“喜爱动漫”。推送时指定某一标签推送,即可推送给所有打上此标签的用户。

【配置管理】渠道模板

功能介绍:

渠道模板用于设置消息下发的提醒方式:响铃、震动、浮动、唤醒屏幕等

操作步骤:

点击【配置管理】-【渠道模板】-【添加渠道模板】,填写模板名称、渠道ID、渠道名称、通知优先级、提醒方式,点击【确定】


【配置管理】短信配置

功能介绍:

Unipush有提供短信补量服务(在创建推送设置里面可以设置选择),针对 App 侧一段时间内无法收到推送的消息(可能由于断网、后台禁止运行、消息延迟等原因)的用户,进行短信通道信息补充。而针对提供的短信通道补充服务,则支持对短信签名以及短信模板进行设置。

操作步骤:

Step 1:配置短信签名

点击【配置管理】-【短信配置】-【签名配置】-【新增签名】,然后填写签名内容,点击【确定】


Step 2:配置短信模板

点击【配置管理】-【短信配置】-【模板配置】-【新增模板】,填写好签名内容、模板类型、模板名称、模板内容等信息,点击【确定】


【自定义事件】

功能介绍:

Unipush透传及部分厂商返回的推送报表数据不包含展示数和点击数。可通过自定义事件来统计,先定义好自定义事件的名称及编号,在需要埋点的地方,调用对应方法来提交统计,补全相应数据。也可用于应用内部的某个事件统计,如点击某个按钮、跳转动作等。


收起阅读 »

解决ios14以上DLNA投屏搜索不到设备问题~

iOS iOS14

iOS开发申请组播广播权限
  iOS14以后,使用组播广播功能需要申请权限。
  1、申请地址:https://developer.apple.com/contact/request/networking-multicast;
  2、填写相应的App Information,提交了等待审核;
  3、等待几天,审核通过了会给你发邮件:Your request to use Multicast Networking was approved. You can now add the Multicast Networking entitlement with your Provisioning Profile.
  4、登录开发者账号,证书管理界面,在AppID的配置界面,增加对应权限:Additional Capabilities中,把Multicast Networking给勾上;
  5、重新生成Profiles里对应的证书;
  6、在工程里配置.entitlements文件,增加一项。Key:com.apple.developer.networking.multicast  type:boolean  Value:YES
  7、在Info.plist文件中,添加本地网络使用权限:Privacy - Local Network Usage Description
  权限问题解决完毕,组播广播功能可以正常开发使用了。

uniapp没有这个entitlements文件,那这个时候要怎么办?请加QQ:320675819(备注:ios14投屏搜索不到设备问题)

继续阅读 »

iOS开发申请组播广播权限
  iOS14以后,使用组播广播功能需要申请权限。
  1、申请地址:https://developer.apple.com/contact/request/networking-multicast;
  2、填写相应的App Information,提交了等待审核;
  3、等待几天,审核通过了会给你发邮件:Your request to use Multicast Networking was approved. You can now add the Multicast Networking entitlement with your Provisioning Profile.
  4、登录开发者账号,证书管理界面,在AppID的配置界面,增加对应权限:Additional Capabilities中,把Multicast Networking给勾上;
  5、重新生成Profiles里对应的证书;
  6、在工程里配置.entitlements文件,增加一项。Key:com.apple.developer.networking.multicast  type:boolean  Value:YES
  7、在Info.plist文件中,添加本地网络使用权限:Privacy - Local Network Usage Description
  权限问题解决完毕,组播广播功能可以正常开发使用了。

uniapp没有这个entitlements文件,那这个时候要怎么办?请加QQ:320675819(备注:ios14投屏搜索不到设备问题)

收起阅读 »

如何实现 iOS 短视频跨页面的无痕续播?

视频 短视频 iOS

在一切皆可视频化的今天,短视频内容作为移动端产品新的促活点,受到了越来越多的重视与投入。盒马在秒播、卡顿率、播放成功率等基础优化之外,在用户使用体验上引入了无痕续播能力,提升用户观看视频内容的延续性。本篇将分享盒马在 iOS 短视频方面的实践干货。

作者|神捕

审校|泰一

跨页面续播是除秒播外另一个可以从体感上增加用户体验的能力。由于一些业务场景需要在不同页面上播放同一个视频内容的场景,而这些场景页面切换往往是连续的,这就要求短视频的播放也是连续。这样才能使得体验上会有连贯性,让用户在进入沉浸式页面时,能流畅的过度,且无感知的继续播放,从而产生连续不间断的感受。下面我们开始介绍盒马短视频的跨页面续播能力和流畅的动画切换效果的流畅性。

!

https://v.youku.com/v_show/id_XNTgwNTk4NzQ4MA==.html

如上视频所示,视频在列表页预览观看后,用户很可能继续点击跳到下一个全屏页面,进入沉浸式体验。在这过程中,视频窗口平滑变大至全屏,视频进度是延续的,中间没有感觉到视频或音频的停顿感。在页面返回后,视频窗口也有相应的还原效果。

目标

接入简单,只需要关心并加一个参数,其它逻辑内聚。
适配性好,支持裁剪模式的切换。
视频、音频无缝衔接,不能有任何停顿感。
页面间播放状态隔离,互不干扰。

实现方案

在方案选择上,主要考虑了以下三种:

目前盒马采用的是第 3 种 ——playerView 的复用方式,具体来说,无痕续播的实现,至少需要以下几个步骤:

  1. 用户点击,从 A 页面跳转到 B 页面,如:domain/path?reusedPlayerView=0xyyyyyy, 在原有业务参数的基础上,添加一个 reusedPlayerView 参数,把 playerView 传给下个页面 。
  2. B 页面 HMTBPlayerView 的实例化:内部实例化一个或复用 A 页面的 reusedPlayerView。
  3. playerView 的大小位置换算,实现切换动画。
  4. 从 B 页面返回 A 时,实现退出动画并返还 playerView。

以上步骤不多,但具体实现起来是比较复杂的,下面我们将围绕 4 个主要问题的解决过程,来说明具体实现方式。

尺寸变化的动画

正常来说,只要计算好 playerView 的原始 Rect,以及最终 Rect,基于 UIView 做 frame 动画就可以简单实现窗口变大效果。但实现时发现,手淘播放器内部重写了 setFrame 方法,只要修改了 frame,playerView 将直接显示为终态,动画没有效果。

于是,这里采用了 CGAffineTransform 的 scale 实现:先把 playerView 的 frame 设置为终态,计算好变化前后的尺寸比例 ratio,设置 playerView.transform = CGAffineTransformMakeScale (ratio, ratio),将其尺寸等比缩小为初始位置大小,而后就可以执行 transform 的动画实现从起点到终点的变换。

需要注意的是,此处 ratio 的计算方式,是以 playerView 内真实渲染的视频尺寸计算,而不是 playerView 本身大小。

渲染 mode 的切换

视频渲染本身可以设置为 ScaleAspectFit 或 ScaleAspectFill,目前在盒马的场景中,存在一种 A 页面的播放器为 fill mode,且 playerView 固定正方形,但跳转到 B 页面时,变成 fit mode,这样就出现了一个在尺寸变化动画进行时的 mode 切换的问题。

上述通过 setFrame 并修改 transform 的方式,可以实现把 playerView 大小变换成与动画前的初始大小一致,但是,如果此时存在 mode 切换需求就有可能出现计算后的大小不一致,比如从一个 9:16 长方形的 playerview 变成一个 1 : 1 且 mode 为 fill 的正方形 playerView,此时宽度一致,但高度明显多出了,直接做动画会导致初始状态闪动。

这里的解决方式,我们使用了 maskView 进行 mode 切换过渡:首先,计算 maskView 分别在宽高上的 scale,然后设置 playerView.maskView.transform。计算方式如下:

CGAffineTransformMakeScale(originalRect.size.width/(destRect.size.width*ratio), originalRect.size.height/(destRect.size.height*ratio))

这样就实现利用 maskView,把 9:16 的长方形显示成 1:1 的可见区域,实现动画的起始位置重合。最后,结合上述 playerView.transform 动画,再添加一个 maskView.transform 动画,二者配合,模拟出带 mode 切换场景下的动画过渡效果。

主动回收与主动归还策略

在实现了进场动画之后,最重要的是需要考虑 playerView 复用逻辑,其中比较重要的一点就是 playerView 什么时候归还给 A 页面。

目前我们采用的是租借思路:

  1. 有可借 playerView 时,进行借用;
  2. 复用的 playerview 不再使用时,及时主动归还;
  3. 当出租方自己要使用时,发现租方还未返还,此时进行主动回收。

具体场景来说:进场时,判断有 reusePlayerView,则进行复用;当沉浸式视频(B 页面,类似抖音)翻到下一个视频时,上一个视频进行主动归还操作,如果用户又划回到第 1 个视频,此时是 new 的 playerView 了;另外,当用户点击页面关闭时,主动归还(如果还未还的话);特别要注意的是,这里还增加了一个主动回收机制,场景比如用户通过一些我们未知的方式,回到了页面 A,此时 reusePlayerView 是没有主动归还的,但页面 A 自己又需要 play,此时就触发了主动回收机制,保证当前页面可用。

有一点需要提一下的是,在页面返回时也有动画,实现方面与上述类似,唯一区别是,返回时页面可能 dealloc 了,动画会有问题,所以我们做法是先把 playerView 从 B 页面,添加到 window, 做好缩放动画,结束后,再主动归还给页面 A。

状态隔离

在使用播放器复用时,需要考虑一个重要的问题,就是复用后,播放器状态、设置的隔离。比如,在页面 A 进入页面 B 后,播放器无痕续播,但播放器的状态对 A 来说是暂停,对于 B 来说必须是播放状态,虽然二者使用的是同一个 playerView。

这种隔离是很有必要的,比如业务想要引导用户进入页面 A 的业务,在这里观看视频可以得积分,那么在他进入页面 B 时,就不应该继续结算积分(业务依赖了播放状态通知)。还有,A 与 B 页面的播放设置可能不同,A 可能是静音,B 是有声音,设置不同,也需要隔离。我们是这样做的,如下图(视图层级):

图中,最外层的 view 是盒马自己封装的播放器 HMTBPlayerView,内部有一个手淘的 TBMPBPlayerView,大小一样。我们拿来做复用的其实是 TBMPBPlayerView 这一层,而把业务层的所有设置放在 HMTBPlayerView,这样的话,在 TBMPBPlayerView 被移走时,重新根据新的 HMTBPlayerView 设置它,做好关联,而旧的 HMTBPlayerView 设置不受影响,包括播放器回调。

总结

综上,我们实现了一种播放器复用方式,在播放器内部实现了窗口切换、状态隔离等逻辑,对 App 使用方来说是几乎无感的。该方案不仅可用在无痕续播场景上,今后也可以用在 App 内全局播放器实例复用优化方向。

「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。公众号后台回复【技术】可加入阿里云视频云产品技术交流群,和业内大咖一起探讨音视频技术,获取更多行业最新信息。

继续阅读 »

在一切皆可视频化的今天,短视频内容作为移动端产品新的促活点,受到了越来越多的重视与投入。盒马在秒播、卡顿率、播放成功率等基础优化之外,在用户使用体验上引入了无痕续播能力,提升用户观看视频内容的延续性。本篇将分享盒马在 iOS 短视频方面的实践干货。

作者|神捕

审校|泰一

跨页面续播是除秒播外另一个可以从体感上增加用户体验的能力。由于一些业务场景需要在不同页面上播放同一个视频内容的场景,而这些场景页面切换往往是连续的,这就要求短视频的播放也是连续。这样才能使得体验上会有连贯性,让用户在进入沉浸式页面时,能流畅的过度,且无感知的继续播放,从而产生连续不间断的感受。下面我们开始介绍盒马短视频的跨页面续播能力和流畅的动画切换效果的流畅性。

!

https://v.youku.com/v_show/id_XNTgwNTk4NzQ4MA==.html

如上视频所示,视频在列表页预览观看后,用户很可能继续点击跳到下一个全屏页面,进入沉浸式体验。在这过程中,视频窗口平滑变大至全屏,视频进度是延续的,中间没有感觉到视频或音频的停顿感。在页面返回后,视频窗口也有相应的还原效果。

目标

接入简单,只需要关心并加一个参数,其它逻辑内聚。
适配性好,支持裁剪模式的切换。
视频、音频无缝衔接,不能有任何停顿感。
页面间播放状态隔离,互不干扰。

实现方案

在方案选择上,主要考虑了以下三种:

目前盒马采用的是第 3 种 ——playerView 的复用方式,具体来说,无痕续播的实现,至少需要以下几个步骤:

  1. 用户点击,从 A 页面跳转到 B 页面,如:domain/path?reusedPlayerView=0xyyyyyy, 在原有业务参数的基础上,添加一个 reusedPlayerView 参数,把 playerView 传给下个页面 。
  2. B 页面 HMTBPlayerView 的实例化:内部实例化一个或复用 A 页面的 reusedPlayerView。
  3. playerView 的大小位置换算,实现切换动画。
  4. 从 B 页面返回 A 时,实现退出动画并返还 playerView。

以上步骤不多,但具体实现起来是比较复杂的,下面我们将围绕 4 个主要问题的解决过程,来说明具体实现方式。

尺寸变化的动画

正常来说,只要计算好 playerView 的原始 Rect,以及最终 Rect,基于 UIView 做 frame 动画就可以简单实现窗口变大效果。但实现时发现,手淘播放器内部重写了 setFrame 方法,只要修改了 frame,playerView 将直接显示为终态,动画没有效果。

于是,这里采用了 CGAffineTransform 的 scale 实现:先把 playerView 的 frame 设置为终态,计算好变化前后的尺寸比例 ratio,设置 playerView.transform = CGAffineTransformMakeScale (ratio, ratio),将其尺寸等比缩小为初始位置大小,而后就可以执行 transform 的动画实现从起点到终点的变换。

需要注意的是,此处 ratio 的计算方式,是以 playerView 内真实渲染的视频尺寸计算,而不是 playerView 本身大小。

渲染 mode 的切换

视频渲染本身可以设置为 ScaleAspectFit 或 ScaleAspectFill,目前在盒马的场景中,存在一种 A 页面的播放器为 fill mode,且 playerView 固定正方形,但跳转到 B 页面时,变成 fit mode,这样就出现了一个在尺寸变化动画进行时的 mode 切换的问题。

上述通过 setFrame 并修改 transform 的方式,可以实现把 playerView 大小变换成与动画前的初始大小一致,但是,如果此时存在 mode 切换需求就有可能出现计算后的大小不一致,比如从一个 9:16 长方形的 playerview 变成一个 1 : 1 且 mode 为 fill 的正方形 playerView,此时宽度一致,但高度明显多出了,直接做动画会导致初始状态闪动。

这里的解决方式,我们使用了 maskView 进行 mode 切换过渡:首先,计算 maskView 分别在宽高上的 scale,然后设置 playerView.maskView.transform。计算方式如下:

CGAffineTransformMakeScale(originalRect.size.width/(destRect.size.width*ratio), originalRect.size.height/(destRect.size.height*ratio))

这样就实现利用 maskView,把 9:16 的长方形显示成 1:1 的可见区域,实现动画的起始位置重合。最后,结合上述 playerView.transform 动画,再添加一个 maskView.transform 动画,二者配合,模拟出带 mode 切换场景下的动画过渡效果。

主动回收与主动归还策略

在实现了进场动画之后,最重要的是需要考虑 playerView 复用逻辑,其中比较重要的一点就是 playerView 什么时候归还给 A 页面。

目前我们采用的是租借思路:

  1. 有可借 playerView 时,进行借用;
  2. 复用的 playerview 不再使用时,及时主动归还;
  3. 当出租方自己要使用时,发现租方还未返还,此时进行主动回收。

具体场景来说:进场时,判断有 reusePlayerView,则进行复用;当沉浸式视频(B 页面,类似抖音)翻到下一个视频时,上一个视频进行主动归还操作,如果用户又划回到第 1 个视频,此时是 new 的 playerView 了;另外,当用户点击页面关闭时,主动归还(如果还未还的话);特别要注意的是,这里还增加了一个主动回收机制,场景比如用户通过一些我们未知的方式,回到了页面 A,此时 reusePlayerView 是没有主动归还的,但页面 A 自己又需要 play,此时就触发了主动回收机制,保证当前页面可用。

有一点需要提一下的是,在页面返回时也有动画,实现方面与上述类似,唯一区别是,返回时页面可能 dealloc 了,动画会有问题,所以我们做法是先把 playerView 从 B 页面,添加到 window, 做好缩放动画,结束后,再主动归还给页面 A。

状态隔离

在使用播放器复用时,需要考虑一个重要的问题,就是复用后,播放器状态、设置的隔离。比如,在页面 A 进入页面 B 后,播放器无痕续播,但播放器的状态对 A 来说是暂停,对于 B 来说必须是播放状态,虽然二者使用的是同一个 playerView。

这种隔离是很有必要的,比如业务想要引导用户进入页面 A 的业务,在这里观看视频可以得积分,那么在他进入页面 B 时,就不应该继续结算积分(业务依赖了播放状态通知)。还有,A 与 B 页面的播放设置可能不同,A 可能是静音,B 是有声音,设置不同,也需要隔离。我们是这样做的,如下图(视图层级):

图中,最外层的 view 是盒马自己封装的播放器 HMTBPlayerView,内部有一个手淘的 TBMPBPlayerView,大小一样。我们拿来做复用的其实是 TBMPBPlayerView 这一层,而把业务层的所有设置放在 HMTBPlayerView,这样的话,在 TBMPBPlayerView 被移走时,重新根据新的 HMTBPlayerView 设置它,做好关联,而旧的 HMTBPlayerView 设置不受影响,包括播放器回调。

总结

综上,我们实现了一种播放器复用方式,在播放器内部实现了窗口切换、状态隔离等逻辑,对 App 使用方来说是几乎无感的。该方案不仅可用在无痕续播场景上,今后也可以用在 App 内全局播放器实例复用优化方向。

「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。公众号后台回复【技术】可加入阿里云视频云产品技术交流群,和业内大咖一起探讨音视频技术,获取更多行业最新信息。

收起阅读 »

编译器终究是bug太多

编译器终究是bug太多,各种不稳定,一会儿不能编辑,一会调试器空白的

编译器终究是bug太多,各种不稳定,一会儿不能编辑,一会调试器空白的

dcloud官方应该没有很擅长写样式的人,要弄暗黑模式的话要好好整理整理uni.scss和组件

ui组件 UI

很多组件样式颜色背景都写死了还是不同的默认样式,而且可能因为是不同的人写的,
虽然有uni.scss,但是都没怎么引用各写各的。
这也导致了ui框架很难直接写一套暗黑模式

如果分类管理的好,uni.scss写一套浅色,一套暗黑,引用那里批量变一下就可以。
我用过quasar framework,组件样式方面写的很好,都不需要自己写css,根组件一个mode="dark"直接全部子组件跟着变色

hello-uniapp中组件演示也写的很乱,不应该写乱七八糟的其他的padding,margin,display,color,backgroud
flex就应该只用uni-flex的class
一组一组就是uni-group
一个个小标题就应该是uni-section

ui这个活吃力不讨好,不知道dcloud会花多大力气继续在uni-ui上面

继续阅读 »

很多组件样式颜色背景都写死了还是不同的默认样式,而且可能因为是不同的人写的,
虽然有uni.scss,但是都没怎么引用各写各的。
这也导致了ui框架很难直接写一套暗黑模式

如果分类管理的好,uni.scss写一套浅色,一套暗黑,引用那里批量变一下就可以。
我用过quasar framework,组件样式方面写的很好,都不需要自己写css,根组件一个mode="dark"直接全部子组件跟着变色

hello-uniapp中组件演示也写的很乱,不应该写乱七八糟的其他的padding,margin,display,color,backgroud
flex就应该只用uni-flex的class
一组一组就是uni-group
一个个小标题就应该是uni-section

ui这个活吃力不讨好,不知道dcloud会花多大力气继续在uni-ui上面

收起阅读 »

uni.showToast方法设置icon属性值为error时在微信开发者工具中图标失效,显示属性值为success时的图标

uni小程序

uni.showToast方法设置icon属性值为error时在微信开发者工具中图标失效,显示属性值为success时的图标

uni.showToast方法设置icon属性值为error时在微信开发者工具中图标失效,显示属性值为success时的图标

scroll-view在pc端不起作用吗

scroll-view在pc端不起作用吗?

scroll-view在pc端不起作用吗?

解决 uni-admin 左侧菜单折叠的问题

uni_app

uni-nav-menu.vue

约190行
// 关闭所有
closeAll() {
let url = this.active.substring(1);
this.subChildrens.forEach((item) => {
let childrens = item.index.children;
let num = 0;
for (let i=0; i<childrens.length; i++){
let obj = childrens[i];
console.log(obj.value);
if (obj.value == url){
num ++;
}
}
if (item.isOpen && num==0) {
item.isOpen = false
}
})
}

继续阅读 »

uni-nav-menu.vue

约190行
// 关闭所有
closeAll() {
let url = this.active.substring(1);
this.subChildrens.forEach((item) => {
let childrens = item.index.children;
let num = 0;
for (let i=0; i<childrens.length; i++){
let obj = childrens[i];
console.log(obj.value);
if (obj.value == url){
num ++;
}
}
if (item.isOpen && num==0) {
item.isOpen = false
}
})
}

收起阅读 »

如何实现 Android 短视频跨页面的流畅续播?

播放 视频 短视频 Android

在一切皆可视频化的今天,短视频内容作为移动端产品新的促活点,受到了越来越多的重视与投入,同时短视频也是增加用户粘性、增加用户停留时长的一把利器。那么如何快速实现移动端短视频功能呢?前两篇我们介绍了盒马短视频秒播优化(iOS 篇 / Android 篇),本篇我们聊聊秒播之外,另一个从体感上增加短视频用户体验的能力 - 续播。

作者|少阳

审校|泰一

短视频作为内容重要的承载方式,是吸引用户的重点,短视频的内容与体验直接关系到用户是否愿意长时停留。因此,体验的优化就显得尤为重要。

 android 续播

跨页面续播

跨页面续播是除秒播外另一个可以从体感上增加用户体验的能力。由于一些业务场景需要在不同页面上播放同一个视频内容的场景,而这些场景页面切换往往是连续的,这就要求短视频的播放也是连续。这样才能使得体验上会有连贯性,让用户在进入沉浸式页面时,能流畅的过度,并无感知的继续播放,从而产生连续不间断的感受。

在优化前,盒马沉浸式短视频播放页面的体验与主流短视频 App 有明显差距。从卡片列表页面跳转视频沉浸式页面时,相同视频无法续播,影响用户观赏和体验。下面主要介绍盒马短视频从普通展示页进入沉浸式页面时跨页面续播能力和流畅的动画切换效果的实现过程。

环境

  • 手机:Pixel 4
  • os:Android 10
  • 播放器:淘宝播放器

效果对比

首先我们来看一下盒马优化前后与主流短视频 App 的效果对比

!

https://v.youku.com/v_show/id_XNTgwNDIwOTM1Ng==.html

问题分析

从对比可以看出,续播的关键在于视频流的复用以及页面转场动画。

视频流的复用

要解决流的复用,同时又要保证进入新的页面时可以立即播放,不产生声音和画面的顿挫,这里根据上一篇《揭秘盒马鲜生 APP Android 短视频秒播优化方案》的分析,必须要解决视频下载,加载解码的耗时。

  • 根据《揭秘盒马鲜生 APP Android 短视频秒播优化方案》里讲到缓存原理,这里可以利用播放器播放同一个视频(注意统一 URL,盒马全部转为 H.265)来避免多次下载。
  • 加载解码的耗时则需要播放器复用来解决。这里涉及到实现方案,可参照下一章的续播方案选型。

转场动画

转场动画能显著提高体感流畅度,但实现过程中需要考虑各种兼容问题。

续播方案选型

在优化前期,我们考虑了三种续播方案。

  1. 播放器 View 跨页面传递
    优点:思路简单,体验效果好。
    缺点:业务侵入严重,不具通用性,播放器业务回调无法隔离,不利于续播放器管控。

  2. 基于 Surface (View) 级别的全局播放器管理
    优点:体验效果好,能扩展内存管控,侵入性低。
    缺点:实现复杂,需要改写底层 HMVideoView 的封装逻辑;改造中易出现内存泄漏,较难排查。

  3. 基于 MediaPlayer 级别的全局播放器管理
    优点:无侵入,能扩展内存管控,实现快(可复用和扩展淘宝播放器底层 token 机制)
    缺点:需要一定的改造,体验比方案 1、2 略差(声音有一瞬间的顿挫,不明显)

盒马最终选择方案 3,这里方案 2 和 3 原理是相同的,没有明显的优劣之分,最终选择方案 3 是因为这是目前稳定性最高,成本最低的方法。后续的播放器续播、复用、管理的分析同样适用于方案 2

播放器续播、复用和管理

业务上,我们需要实现续播,通过问题分析,我们已经知道,通过视频流的复用即可实现,而视频流的复用这里选择通过复用 MediaPlayer 实现(也可以复用 Surface+MediaPlayer)。

解耦播放器 View 与 MediaPlayer 层

将 MediaPlayer 从 TaobaoPlayerView 中拆解出来,通过 MediaPlayerManager 进行全局管理。全局管理后,所有的播放器的 MediaPlayer 都由 MediaPlayerManager 分配和控制。


各组建间关系

业务流程

确保业务流程中,只需要关心业务与 VideoView 之间的交互,底层播放器复用由 MediaPlayerManager 实现。

播放器复用(管理)原理

播放器复用是管理的一个子集,所以这里一起介绍。主要原来有以下几个原则:

  1. 全局播放器(MediaPlayer)控制最多创建 4 个。
  2. 超过 4 个播放器,创建第 5 个时,先销毁最少使用的播放器的 MediaPlayer。
  3. 每个播放器随机分配一个 token(时间戳 + 随机数),也可以开发者指定。
  4. 相同 token 的播放器,共享 MediaPlayer。
  5. 一个 MediaPlayer 同时只能被 1 个播放器 Surface 所绑定和持有。
  6. 存在相同 token 的播放器,当前播放器在销毁时,保留 MediaPlayer 实例。
  7. 已创建的播放器恢复播放,但 MediaPlayer 被其他后创建的播放器占用时,解绑 MediaPlayer 并重新绑定当前播放器。

场景模拟

场景一:APP 共创建 4 个及以内播放器。

场景二:创建超过 4 个播放器时。

场景三:新创建的播放器 token 已存在时,复用 MediaPlayer。

场景四:存在 token 与当前即将被销毁的播放器 token 一致时(或已被解除 MediaPlayer 的播放器播放时)。


逻辑流程图

从场景总结,MediaPlayer 主要提供 复用、恢复、销毁、驱逐(创建) 四个能力。

转场动画

目前转场动画有两个方案可选:

  1. Android 自带的元素动画
    优点:动画流畅顺滑,无需实现动画逻辑,由系统自己实现。
    缺点:侵入严重,需要改写 Nav 层,在 View 复用的方案下有白屏和黑屏。

  2. 自定义实现属性动画
    优点:侵入小,只需要前置页极少的坐标信息,如果是 View 复用方案,甚至不需要前置页提供坐标信息;兼容性好,适用于各种播放器复用场景。
    缺点:需要自己实现动画,有一定的闪烁感。

动画原理

  1. 前置页跳转到沉浸式,传递播放器坐标 Rect 信息。
  2. 沉浸式默认透明,并根据 Rect 坐标信息创建播放器(复用)。
  3. 开始动画,将播放器 View 放大至正确位置,同时背景不透明度增加。
    (注意:这里最后要将沉浸式页的主题设为不透明,否则前置页不会执行 onStop () 具体参考下一节,生命周期填坑。)

ps:返回动画同理,过程相反即可。

生命周期填坑

属性动画原理存在一个坑。

问题描述
假设页面为 A->B,方案 3 要求 B 页面在动画过程中是全透明的。当 B 的 theme 中 windowIsTranslucent 为 true 时,A->B 过程 A 的生命周期无法走向 stop(即便 B 页面动画结束,完全遮盖 A 页面)。因此,A 的生命周期没有按照预期执行,一些需要 onStop 执行的场景下,业务就无法正常执行

B Ativity 的样式(注:示例代码):

<style name="MyTransparent" parent="xxxx">  
 <item name="android:windowFullscreen">false</item>  
  <item name="android:windowNoTitle">true</item>  
  <item name="android:windowBackground">@android:color/transparent</item>  
  <item name="android:colorBackgroundCacheHint">@null</item>  
  <item name="android:windowIsTranslucent">true</item>  
  <item name="android:background">@android:color/transparent</item>  
  <item name="android:windowAnimationStyle">@style/noAnimation</item>  
 </style>  

解决方案

  1. 进入动画结束时,通过反射调用 Activity 的 convertFromTranslucent 方法, 使 activity 不透明。
  2. 返回动画开始时,通过反射调用 Activity 的 convertToTranslucent 方法,使 activity 透明。

后续优化展望

关于多媒体的优化工作还有很多可以做。除了续播和沉浸式秒播等场景外,我们还可以:
**1. 对播放器的一般性场景进行秒播优化,如首页列表的卡片视频。

  1. 对播放器的全局实例管控,控制播放器创建数量,从而优化内存。**

未优化:
操作:连续开启 30~50 个页面及播放器。
现象:内存飙升,手机发烫,影响手机正常使用。

优化后:
操作:每秒开启 1 个页面和播放器,连续开启 100 个。
现象:内存呈锯齿状正常上升,无明显飙升现象,软件运行正常。

下一期我们将继续分享盒马 iOS 端短视频续播的体验优化实践。

「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。公众号后台回复【技术】可加入阿里云视频云产品技术交流群,和业内大咖一起探讨音视频技术,获取更多行业最新信息。

继续阅读 »

在一切皆可视频化的今天,短视频内容作为移动端产品新的促活点,受到了越来越多的重视与投入,同时短视频也是增加用户粘性、增加用户停留时长的一把利器。那么如何快速实现移动端短视频功能呢?前两篇我们介绍了盒马短视频秒播优化(iOS 篇 / Android 篇),本篇我们聊聊秒播之外,另一个从体感上增加短视频用户体验的能力 - 续播。

作者|少阳

审校|泰一

短视频作为内容重要的承载方式,是吸引用户的重点,短视频的内容与体验直接关系到用户是否愿意长时停留。因此,体验的优化就显得尤为重要。

 android 续播

跨页面续播

跨页面续播是除秒播外另一个可以从体感上增加用户体验的能力。由于一些业务场景需要在不同页面上播放同一个视频内容的场景,而这些场景页面切换往往是连续的,这就要求短视频的播放也是连续。这样才能使得体验上会有连贯性,让用户在进入沉浸式页面时,能流畅的过度,并无感知的继续播放,从而产生连续不间断的感受。

在优化前,盒马沉浸式短视频播放页面的体验与主流短视频 App 有明显差距。从卡片列表页面跳转视频沉浸式页面时,相同视频无法续播,影响用户观赏和体验。下面主要介绍盒马短视频从普通展示页进入沉浸式页面时跨页面续播能力和流畅的动画切换效果的实现过程。

环境

  • 手机:Pixel 4
  • os:Android 10
  • 播放器:淘宝播放器

效果对比

首先我们来看一下盒马优化前后与主流短视频 App 的效果对比

!

https://v.youku.com/v_show/id_XNTgwNDIwOTM1Ng==.html

问题分析

从对比可以看出,续播的关键在于视频流的复用以及页面转场动画。

视频流的复用

要解决流的复用,同时又要保证进入新的页面时可以立即播放,不产生声音和画面的顿挫,这里根据上一篇《揭秘盒马鲜生 APP Android 短视频秒播优化方案》的分析,必须要解决视频下载,加载解码的耗时。

  • 根据《揭秘盒马鲜生 APP Android 短视频秒播优化方案》里讲到缓存原理,这里可以利用播放器播放同一个视频(注意统一 URL,盒马全部转为 H.265)来避免多次下载。
  • 加载解码的耗时则需要播放器复用来解决。这里涉及到实现方案,可参照下一章的续播方案选型。

转场动画

转场动画能显著提高体感流畅度,但实现过程中需要考虑各种兼容问题。

续播方案选型

在优化前期,我们考虑了三种续播方案。

  1. 播放器 View 跨页面传递
    优点:思路简单,体验效果好。
    缺点:业务侵入严重,不具通用性,播放器业务回调无法隔离,不利于续播放器管控。

  2. 基于 Surface (View) 级别的全局播放器管理
    优点:体验效果好,能扩展内存管控,侵入性低。
    缺点:实现复杂,需要改写底层 HMVideoView 的封装逻辑;改造中易出现内存泄漏,较难排查。

  3. 基于 MediaPlayer 级别的全局播放器管理
    优点:无侵入,能扩展内存管控,实现快(可复用和扩展淘宝播放器底层 token 机制)
    缺点:需要一定的改造,体验比方案 1、2 略差(声音有一瞬间的顿挫,不明显)

盒马最终选择方案 3,这里方案 2 和 3 原理是相同的,没有明显的优劣之分,最终选择方案 3 是因为这是目前稳定性最高,成本最低的方法。后续的播放器续播、复用、管理的分析同样适用于方案 2

播放器续播、复用和管理

业务上,我们需要实现续播,通过问题分析,我们已经知道,通过视频流的复用即可实现,而视频流的复用这里选择通过复用 MediaPlayer 实现(也可以复用 Surface+MediaPlayer)。

解耦播放器 View 与 MediaPlayer 层

将 MediaPlayer 从 TaobaoPlayerView 中拆解出来,通过 MediaPlayerManager 进行全局管理。全局管理后,所有的播放器的 MediaPlayer 都由 MediaPlayerManager 分配和控制。


各组建间关系

业务流程

确保业务流程中,只需要关心业务与 VideoView 之间的交互,底层播放器复用由 MediaPlayerManager 实现。

播放器复用(管理)原理

播放器复用是管理的一个子集,所以这里一起介绍。主要原来有以下几个原则:

  1. 全局播放器(MediaPlayer)控制最多创建 4 个。
  2. 超过 4 个播放器,创建第 5 个时,先销毁最少使用的播放器的 MediaPlayer。
  3. 每个播放器随机分配一个 token(时间戳 + 随机数),也可以开发者指定。
  4. 相同 token 的播放器,共享 MediaPlayer。
  5. 一个 MediaPlayer 同时只能被 1 个播放器 Surface 所绑定和持有。
  6. 存在相同 token 的播放器,当前播放器在销毁时,保留 MediaPlayer 实例。
  7. 已创建的播放器恢复播放,但 MediaPlayer 被其他后创建的播放器占用时,解绑 MediaPlayer 并重新绑定当前播放器。

场景模拟

场景一:APP 共创建 4 个及以内播放器。

场景二:创建超过 4 个播放器时。

场景三:新创建的播放器 token 已存在时,复用 MediaPlayer。

场景四:存在 token 与当前即将被销毁的播放器 token 一致时(或已被解除 MediaPlayer 的播放器播放时)。


逻辑流程图

从场景总结,MediaPlayer 主要提供 复用、恢复、销毁、驱逐(创建) 四个能力。

转场动画

目前转场动画有两个方案可选:

  1. Android 自带的元素动画
    优点:动画流畅顺滑,无需实现动画逻辑,由系统自己实现。
    缺点:侵入严重,需要改写 Nav 层,在 View 复用的方案下有白屏和黑屏。

  2. 自定义实现属性动画
    优点:侵入小,只需要前置页极少的坐标信息,如果是 View 复用方案,甚至不需要前置页提供坐标信息;兼容性好,适用于各种播放器复用场景。
    缺点:需要自己实现动画,有一定的闪烁感。

动画原理

  1. 前置页跳转到沉浸式,传递播放器坐标 Rect 信息。
  2. 沉浸式默认透明,并根据 Rect 坐标信息创建播放器(复用)。
  3. 开始动画,将播放器 View 放大至正确位置,同时背景不透明度增加。
    (注意:这里最后要将沉浸式页的主题设为不透明,否则前置页不会执行 onStop () 具体参考下一节,生命周期填坑。)

ps:返回动画同理,过程相反即可。

生命周期填坑

属性动画原理存在一个坑。

问题描述
假设页面为 A->B,方案 3 要求 B 页面在动画过程中是全透明的。当 B 的 theme 中 windowIsTranslucent 为 true 时,A->B 过程 A 的生命周期无法走向 stop(即便 B 页面动画结束,完全遮盖 A 页面)。因此,A 的生命周期没有按照预期执行,一些需要 onStop 执行的场景下,业务就无法正常执行

B Ativity 的样式(注:示例代码):

<style name="MyTransparent" parent="xxxx">  
 <item name="android:windowFullscreen">false</item>  
  <item name="android:windowNoTitle">true</item>  
  <item name="android:windowBackground">@android:color/transparent</item>  
  <item name="android:colorBackgroundCacheHint">@null</item>  
  <item name="android:windowIsTranslucent">true</item>  
  <item name="android:background">@android:color/transparent</item>  
  <item name="android:windowAnimationStyle">@style/noAnimation</item>  
 </style>  

解决方案

  1. 进入动画结束时,通过反射调用 Activity 的 convertFromTranslucent 方法, 使 activity 不透明。
  2. 返回动画开始时,通过反射调用 Activity 的 convertToTranslucent 方法,使 activity 透明。

后续优化展望

关于多媒体的优化工作还有很多可以做。除了续播和沉浸式秒播等场景外,我们还可以:
**1. 对播放器的一般性场景进行秒播优化,如首页列表的卡片视频。

  1. 对播放器的全局实例管控,控制播放器创建数量,从而优化内存。**

未优化:
操作:连续开启 30~50 个页面及播放器。
现象:内存飙升,手机发烫,影响手机正常使用。

优化后:
操作:每秒开启 1 个页面和播放器,连续开启 100 个。
现象:内存呈锯齿状正常上升,无明显飙升现象,软件运行正常。

下一期我们将继续分享盒马 iOS 端短视频续播的体验优化实践。

「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。公众号后台回复【技术】可加入阿里云视频云产品技术交流群,和业内大咖一起探讨音视频技术,获取更多行业最新信息。

收起阅读 »

专业ios 安卓原生开发工程师,承接各种uniapp原生插件定制开发。

uniapp原生插件

本人专业原生开发,承接各种定制插件,有意者帖子回帖联系!QQ 1149769831

本人专业原生开发,承接各种定制插件,有意者帖子回帖联系!QQ 1149769831