HBuilderX

HBuilderX

极客开发工具
uni-app

uni-app

开发一次,多端覆盖
uniCloud

uniCloud

云开发平台
HTML5+

HTML5+

增强HTML5的功能体验
MUI

MUI

上万Star的前端框架

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

揭秘盒马鲜生 Android 短视频秒播优化方案

短视频 Android

短视频作为内容重要的承载方式,是吸引用户的重点,短视频的内容与体验直接关系到用户是否愿意长时停留。因此,体验的优化就显得尤为重要。上一篇我们分享了 iOS 短视频秒播优化,这篇我们来聊聊 Android 端的优化。

作者|少阳
审校|泰一

头图

优化前的盒马沉浸式短视频播放页面,体感和流畅度上与主流短视频 App 有明显差距。主要问题有播放封面闪屏、出流速度慢两个问题。所以优化的目标是解决盒马沉浸式短视频现有短板,与主流 App 的沉浸式短视频体验对齐,如抖音、手淘等。具体指标有:

  • 满足硬性指标:播放成功率、首帧时长、秒开率。
  • 满足用户体感流畅度。(为反应用户观看短视频过程中的真实体验,盒马新增秒播体感指标:从用户划到视频,到视频首帧播放的时间。)

优化效果对比

首先我们来看一下优化前后与其他 App 的效果对比:
https://v.youku.com/v_show/id_XNTgwMzAwNDQ0OA==.html

环境

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

问题分析

首页闪屏

盒马最初为了保证进入画面时不是空白页面而增加了封面图显示,在播放时隐藏。从体感指标可以看出,即便是优化前,体感播放时间很短,只有 200ms 左右(不包含滑动过程)。由于滑动过程中,到视频正式播放有约 600ms 左右时间显示封面,随后又迅速显示播放画面,此时用户仍有强烈的屏幕闪烁和顿挫感,体验极差。
解决思路:在滑动过程中就显示视频首帧画面,不再显示封面,则播放时不再产生顿挫感。这里的优化需要结合出流慢问题一起优化。

闪屏

出流速度慢(播放体感慢)

服务端:服务端造成的出流速度慢,一般是文件大,网络链路差造成。可用 H.265CDN 加速优化
客户端:客户端播放需要经历下载 -> 加载 + 解码 -> 渲染三个步骤,并且三个步骤为线性执行。所以在窗口播放画面前必然需要经过 1s 左右的准备工作。这里可以考虑提前执行下载 -> 加载 + 解码
播放

优化方案选型

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

方案一:双播放器 + 预下载
优点:占内存小,思路简单。
缺点:优化力度有限,无法同时兼顾上滑和下滑。

方案二:自定义三播放器管理 + 预下载
优点:同时兼顾上下翻页,体验接近抖音。
缺点:播放器管理与回收实现复杂,容易错乱;占用内存高。

方案三:三播放器(基于 RecyclerView 的缓存机制实现)+ 预下载
优点:同时兼顾上下翻页,体验接近抖音,缓存机制由 RecyclerView 托管。
缺点:占用内存高,频繁创建和销毁播放器。

最终因为性价比因素,选择了第三个方案。

方案三原理:翻页前

  • current 播放器开始播放视频 1。
  • pre,next 播放器分别加载视频数据 0 和 2。
  • 同时视频数据 3-7 加入预下载队列。

翻页前

方案三原理:翻页后

  • 被 RecyclerView 回收的 holder 销毁播放器。
  • RecyclerView onBind 中的 holder 创建新的 next 播放器。
  • current 播放器开始播放视频 2。
  • pre 播放器 seek 0 并暂停, next 播放器创建并加载视频 3 文件。
  • 同时预下载清除未消费的队列,视频数据 4-8 加入预下载队列开始下载(此处已有缓存的视频不会被重复下载)。

翻页后

具体优化方案

多播放器改造

为了解决体感上的顿挫和出流慢的问题,采用多播放器结合 RecyclerView 方案进行改造,步骤如下:

  1. 设置缓存数量:利用 RecyclerView 特性,配置 setItemViewCacheSize,确保内存中存在 3 个 holder(缓存的 1 个 holder,预创建 1 个 holder, 当前 holder)。
    mRecyclerView.setItemViewCacheSize(1); // 设置缓存数量
  2. 重写 Adapter 的 onBindViewHolder, 用于创建播放器,并预加载解码视频内容,播放器控制解析到首帧时暂停。此时 onSurfaceCreated 尚未回调,画面未渲染至屏幕。
  3. 监听 onPageRelase 控制即将移除屏幕的播放器暂停,并 seekTo (0),方便滑回屏幕时立即播放。
  4. 监听 onPageSelected 控制即将进入屏幕的播放器开始播放。注意:由于在 onBindViewHolder 期间已解码完成,这里只需要进入屏幕 1px,就会立即触发 Surface 的绘制(只会执行一次),即进入窗口的内容会显示视频的首帧画面。
  5. 重写 Adapter 的 onViewRecycled, 由于当前 holder 即将移出屏幕,移出方向上屏幕外的 holder 将被回收。此时回收并销毁播放器。

多播放器 + RecyclerView 原理图
多播放器 + RecyclerView 原理图

三播放器让沉浸式短视频的体验大幅提高,主要解决了以下问题:

  • 上下滑动过程中,进入屏幕的画面为视频的第一帧画面,并且不会有视觉上的顿挫。
  • 正式播放前预创建播放器,并加载和解码,节省了播放视频之前的准备工作。(ps:这里还包括了下载的过程)。
  • 由于提前加载和解码,进入屏幕时,触发 Surface 瞬间渲染,视觉上无感知,因此播放视频前不再需要封面图,避免了封面图和首帧不一致导致的闪屏问题。

    预下载优化

    前面讲到了多播放器实现翻页秒播能力,在体验上有了非常大的改善,但由于预创建的播放器在加载时,同时需要下载视频文件,导致这里的下一个播放器准备好视频的时间会增加到 1s 左右。如果用户在播放器加载解码完成前滑至该视频,则会出现明显的黑屏,带来非常差的体验。

由于预加载的时间过长,且无法预知用户是否会快速滑动。这里需要提前进行下载和快速滑动检测。

关于预下载,我们首先要知道播放器内部播放过程。这里的本地代理是视频缓存机制实现的,具体参照下一章节。
播放器内部流程
播放器内部流程

预下载策略

这里,我们为了节约请求网络数据的过程,在播放之前提前下载视频的首帧片段,采用如下策略:

  • 文件大小:下载 1MB 视频文件的方式进行提前首帧下载。(ps:经测试 1MB 已包含了首帧,且文件相对较小)。
  • 提前量:提前 5 个下载量(pageSize 为 10 的情况)。
  • 并发情况:下载采用同步队列下载(避免异步下载导致带宽占用,正常播放的视频卡顿)。
  • 快滑优化:快滑清除下载队列,避免快滑过程中频繁触发下载。
  • 下载时机:loadMore 时将前 5 个推入队列;onPageSelected 时,跳过下一个开始起算 5 个视频推入队列(下一个视频由预加载的播放器自动下载,这里重复下载会导致视频花屏)。

    快滑定义

    当用户快速翻页时(onPageSelected 调用之前又滑了一下),onPageSelected 不会触发,onPageRelease 会触发多次。在 onPageRelease 中判断 release position 与 current postion 的差值如果 > 1 则表示用户至少快速翻页 1 次,此时定义为快滑状态,应当停止预下载和播放器预加载。
    当 onPageSelected 回调时,说明用户没有继续翻页,此时取消快滑状态。开始执行预下载和恢复播放器预加载。
    预下载流程图
    预下载流程图

    缓存优化

    目前盒马使用的播放器为淘宝内部播放器。 播放器本身不存在文件缓存和预下载功能。在播放器重新创建后,即便是同一个视频也不会有文件缓存,需要重新下载。这里引入一个开源缓存框架 “com.danikula:videocache”。

方案原理

播放器播放的地址代理给本地的缓存服务,缓存服务负责转发数据流的同时进行文件保存如:
视频地址为:https://****.mp4
本地代理地址为:http://127.0.0.1:8888 (假设端口号分配为 8888)。
代理后的地址为: 本地代理地址 + 视频地址(URL 编码)。
即:http://127.0.0.1:8888/https%3A%2F%2F****.mp4

方案

后续优化展望

关于多媒体的优化工作还有很多可以做。除了沉浸式秒播的场景外,我们还可以:

  1. 对播放器的一般性场景进行秒播优化,如首页列表的卡片视频。目前首页平均首帧画面需要 550ms,较长的有 1000s 以上,有明显的顿挫感。在沉浸式的方案基础上,我们可以提供一般性的预下载能力,从而减少播放器的下载渲染时长。
  2. 续播和内存优化。续播是另一个提升体验的方面,用户能够非常直观的感受连贯与否。
  3. 页面单播放器托管。大多场景下,一个页面只有一个播放器在播放,这就可以通过管理唯一的播放器实现页面播放器复用,从而优化内存和体验。

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

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

继续阅读 »

短视频作为内容重要的承载方式,是吸引用户的重点,短视频的内容与体验直接关系到用户是否愿意长时停留。因此,体验的优化就显得尤为重要。上一篇我们分享了 iOS 短视频秒播优化,这篇我们来聊聊 Android 端的优化。

作者|少阳
审校|泰一

头图

优化前的盒马沉浸式短视频播放页面,体感和流畅度上与主流短视频 App 有明显差距。主要问题有播放封面闪屏、出流速度慢两个问题。所以优化的目标是解决盒马沉浸式短视频现有短板,与主流 App 的沉浸式短视频体验对齐,如抖音、手淘等。具体指标有:

  • 满足硬性指标:播放成功率、首帧时长、秒开率。
  • 满足用户体感流畅度。(为反应用户观看短视频过程中的真实体验,盒马新增秒播体感指标:从用户划到视频,到视频首帧播放的时间。)

优化效果对比

首先我们来看一下优化前后与其他 App 的效果对比:
https://v.youku.com/v_show/id_XNTgwMzAwNDQ0OA==.html

环境

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

问题分析

首页闪屏

盒马最初为了保证进入画面时不是空白页面而增加了封面图显示,在播放时隐藏。从体感指标可以看出,即便是优化前,体感播放时间很短,只有 200ms 左右(不包含滑动过程)。由于滑动过程中,到视频正式播放有约 600ms 左右时间显示封面,随后又迅速显示播放画面,此时用户仍有强烈的屏幕闪烁和顿挫感,体验极差。
解决思路:在滑动过程中就显示视频首帧画面,不再显示封面,则播放时不再产生顿挫感。这里的优化需要结合出流慢问题一起优化。

闪屏

出流速度慢(播放体感慢)

服务端:服务端造成的出流速度慢,一般是文件大,网络链路差造成。可用 H.265CDN 加速优化
客户端:客户端播放需要经历下载 -> 加载 + 解码 -> 渲染三个步骤,并且三个步骤为线性执行。所以在窗口播放画面前必然需要经过 1s 左右的准备工作。这里可以考虑提前执行下载 -> 加载 + 解码
播放

优化方案选型

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

方案一:双播放器 + 预下载
优点:占内存小,思路简单。
缺点:优化力度有限,无法同时兼顾上滑和下滑。

方案二:自定义三播放器管理 + 预下载
优点:同时兼顾上下翻页,体验接近抖音。
缺点:播放器管理与回收实现复杂,容易错乱;占用内存高。

方案三:三播放器(基于 RecyclerView 的缓存机制实现)+ 预下载
优点:同时兼顾上下翻页,体验接近抖音,缓存机制由 RecyclerView 托管。
缺点:占用内存高,频繁创建和销毁播放器。

最终因为性价比因素,选择了第三个方案。

方案三原理:翻页前

  • current 播放器开始播放视频 1。
  • pre,next 播放器分别加载视频数据 0 和 2。
  • 同时视频数据 3-7 加入预下载队列。

翻页前

方案三原理:翻页后

  • 被 RecyclerView 回收的 holder 销毁播放器。
  • RecyclerView onBind 中的 holder 创建新的 next 播放器。
  • current 播放器开始播放视频 2。
  • pre 播放器 seek 0 并暂停, next 播放器创建并加载视频 3 文件。
  • 同时预下载清除未消费的队列,视频数据 4-8 加入预下载队列开始下载(此处已有缓存的视频不会被重复下载)。

翻页后

具体优化方案

多播放器改造

为了解决体感上的顿挫和出流慢的问题,采用多播放器结合 RecyclerView 方案进行改造,步骤如下:

  1. 设置缓存数量:利用 RecyclerView 特性,配置 setItemViewCacheSize,确保内存中存在 3 个 holder(缓存的 1 个 holder,预创建 1 个 holder, 当前 holder)。
    mRecyclerView.setItemViewCacheSize(1); // 设置缓存数量
  2. 重写 Adapter 的 onBindViewHolder, 用于创建播放器,并预加载解码视频内容,播放器控制解析到首帧时暂停。此时 onSurfaceCreated 尚未回调,画面未渲染至屏幕。
  3. 监听 onPageRelase 控制即将移除屏幕的播放器暂停,并 seekTo (0),方便滑回屏幕时立即播放。
  4. 监听 onPageSelected 控制即将进入屏幕的播放器开始播放。注意:由于在 onBindViewHolder 期间已解码完成,这里只需要进入屏幕 1px,就会立即触发 Surface 的绘制(只会执行一次),即进入窗口的内容会显示视频的首帧画面。
  5. 重写 Adapter 的 onViewRecycled, 由于当前 holder 即将移出屏幕,移出方向上屏幕外的 holder 将被回收。此时回收并销毁播放器。

多播放器 + RecyclerView 原理图
多播放器 + RecyclerView 原理图

三播放器让沉浸式短视频的体验大幅提高,主要解决了以下问题:

  • 上下滑动过程中,进入屏幕的画面为视频的第一帧画面,并且不会有视觉上的顿挫。
  • 正式播放前预创建播放器,并加载和解码,节省了播放视频之前的准备工作。(ps:这里还包括了下载的过程)。
  • 由于提前加载和解码,进入屏幕时,触发 Surface 瞬间渲染,视觉上无感知,因此播放视频前不再需要封面图,避免了封面图和首帧不一致导致的闪屏问题。

    预下载优化

    前面讲到了多播放器实现翻页秒播能力,在体验上有了非常大的改善,但由于预创建的播放器在加载时,同时需要下载视频文件,导致这里的下一个播放器准备好视频的时间会增加到 1s 左右。如果用户在播放器加载解码完成前滑至该视频,则会出现明显的黑屏,带来非常差的体验。

由于预加载的时间过长,且无法预知用户是否会快速滑动。这里需要提前进行下载和快速滑动检测。

关于预下载,我们首先要知道播放器内部播放过程。这里的本地代理是视频缓存机制实现的,具体参照下一章节。
播放器内部流程
播放器内部流程

预下载策略

这里,我们为了节约请求网络数据的过程,在播放之前提前下载视频的首帧片段,采用如下策略:

  • 文件大小:下载 1MB 视频文件的方式进行提前首帧下载。(ps:经测试 1MB 已包含了首帧,且文件相对较小)。
  • 提前量:提前 5 个下载量(pageSize 为 10 的情况)。
  • 并发情况:下载采用同步队列下载(避免异步下载导致带宽占用,正常播放的视频卡顿)。
  • 快滑优化:快滑清除下载队列,避免快滑过程中频繁触发下载。
  • 下载时机:loadMore 时将前 5 个推入队列;onPageSelected 时,跳过下一个开始起算 5 个视频推入队列(下一个视频由预加载的播放器自动下载,这里重复下载会导致视频花屏)。

    快滑定义

    当用户快速翻页时(onPageSelected 调用之前又滑了一下),onPageSelected 不会触发,onPageRelease 会触发多次。在 onPageRelease 中判断 release position 与 current postion 的差值如果 > 1 则表示用户至少快速翻页 1 次,此时定义为快滑状态,应当停止预下载和播放器预加载。
    当 onPageSelected 回调时,说明用户没有继续翻页,此时取消快滑状态。开始执行预下载和恢复播放器预加载。
    预下载流程图
    预下载流程图

    缓存优化

    目前盒马使用的播放器为淘宝内部播放器。 播放器本身不存在文件缓存和预下载功能。在播放器重新创建后,即便是同一个视频也不会有文件缓存,需要重新下载。这里引入一个开源缓存框架 “com.danikula:videocache”。

方案原理

播放器播放的地址代理给本地的缓存服务,缓存服务负责转发数据流的同时进行文件保存如:
视频地址为:https://****.mp4
本地代理地址为:http://127.0.0.1:8888 (假设端口号分配为 8888)。
代理后的地址为: 本地代理地址 + 视频地址(URL 编码)。
即:http://127.0.0.1:8888/https%3A%2F%2F****.mp4

方案

后续优化展望

关于多媒体的优化工作还有很多可以做。除了沉浸式秒播的场景外,我们还可以:

  1. 对播放器的一般性场景进行秒播优化,如首页列表的卡片视频。目前首页平均首帧画面需要 550ms,较长的有 1000s 以上,有明显的顿挫感。在沉浸式的方案基础上,我们可以提供一般性的预下载能力,从而减少播放器的下载渲染时长。
  2. 续播和内存优化。续播是另一个提升体验的方面,用户能够非常直观的感受连贯与否。
  3. 页面单播放器托管。大多场景下,一个页面只有一个播放器在播放,这就可以通过管理唯一的播放器实现页面播放器复用,从而优化内存和体验。

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

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

收起阅读 »