用例 APK
用例 APK 版本:爱奇艺TV版 v12.1.0
原 APK 链接:
修改 APK 链接:
本人测试环境
广告的表现形式很多,可能是一个界面(activity),可能是局部在上方或下方的一个区域视图(view)等。以下是常见广告形式:
无论怎样形式、怎样来源的广告,在本地一定需要展示出来,展示就需要广告内容载体,如界面、视图等,对于这些容器,即可以利用静态的布局,也可以动态生成布局。如果能移除这些容器、或者破坏容器生成条件就可以达到去广告的地步。
本次案例是来自于第三方 SDK 软件的广告投放,通过发送请求包,从而获取相对应的广告 ID 与资源,对于这种情况,我们可以通过定位 SDK 的初始化、广告请求、广告展示等代码,来分析其逻辑,从而找到突破点。
首先对开屏广告页面进行分析,通过MT管理器发现该广告是处在 WelcomeActivity 类中,我们直接hook 类,得到其函数调用栈。
可以猜测 showHomePage() 就是展示我们的主页了,我们逐条分析广告发生前的函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
private void checkPermission() {
if
(lpt2.br(InitHelper.getInstance().checkInitPermission(this))) {
jumpToMain();
return
;
}
List
<String> checkInitPermission
=
InitHelper.getInstance().checkInitPermission(this);
androidx.core.app.aux.a(this, (String[]) checkInitPermission.toArray(new String[checkInitPermission.size()]),
1
);
}
/
/
检查初始化权限
public
List
<String> checkInitPermission(Context context) {
ArrayList<String> arrayList
=
new ArrayList();
ArrayList arrayList2
=
new ArrayList();
arrayList.add(
"android.permission.INTERNET"
);
/
/
访问网络的权限
if
(!org.qiyi.speaker.u.con.bMX()) {
arrayList.add(
"android.permission.READ_PHONE_STATE"
);
/
/
取手机状态的权限
}
arrayList.add(
"android.permission.WRITE_EXTERNAL_STORAGE"
);
/
/
写入外部存储设备的权限
arrayList.add(
"android.permission.ACCESS_NETWORK_STATE"
);
/
/
访问网络状态的权限
....
}
private void jumpToMain() {
Log.e(
"gzy"
,
"size:"
+
SpeakerApplication.getInstance().getCurrentActivitySize());
/
/
用户是否给软件授权
if
(!org.qiyi.speaker.o.con.bLa()) {
org.qiyi.speaker.o.con.a(this, this.mLisenceCallback);
/
/
显示免责声明并进行用户许可
/
/
加载splash启动页动画(没有后台进程)
}
else
if
(GuideController.INSTANCE.needShowSplashGuide()) {
showGuidePage();
}
else
{
/
/
launchMain(false);
}
}
/
/
首次打开,启动应用程序主界面
public void launchMain(final boolean z) {
/
/
如果当前Activity数量不等于
1
,那么显示主页。
if
(SpeakerApplication.getInstance().getCurrentActivitySize() !
=
1
) {
showHomePage(z);
return
;
}
/
/
注册一个启动画面的回调,请求广告并下载,当启动画面结束后, 显示广告。
com.qiyi.video.g.con.aXh().registerSplashCallback(new ISplashCallback() {
/
/
from
class
: com.qiyi.video.speaker.activity.WelcomeActivity.
2
@Override
/
/
org.qiyi.video.module.api.ISplashCallback
public void onAdAnimationStarted() {
}
@Override
/
/
org.qiyi.video.module.api.ISplashCallback
public void onAdCountdown(
int
i) {
}
@Override
/
/
org.qiyi.video.module.api.ISplashCallback
public void onAdOpenDetailVideo() {
}
@Override
/
/
org.qiyi.video.module.api.ISplashCallback
public void onAdStarted(String
str
) {
}
@Override
/
/
org.qiyi.video.module.api.ISplashCallback
public void onSplashFinished(
int
i) {
WelcomeActivity.this.showHomePage(z);
JobManagerUtils.a(new Runnable() {
/
/
from
class
: com.qiyi.video.speaker.activity.WelcomeActivity.
2.1
@Override
/
/
java.lang.Runnable
public void run() {
com.qiyi.video.qysplashscreen.ad.aux.aUv().aUE();
((ISplashScreenApi) ModuleManager.getModule(IModuleConstants.MODULE_NAME_SPLASH_SCREEN, ISplashScreenApi.
class
)).requestAdAndDownload();
}
},
500
, PageAutoScrollUtils.HANDLER_SWITCH_NEXT_TIPS_DELAY,
"splashAD_requestad"
, WelcomeActivity.TAG);
}
});
launchAppGuide();
}
|
可以看到当当前Activity数量不等于1时,就直接调 showHomePage 函数,我们可以将这个判断改为永真,让其直接显示主页。
重打包编译签名,运行程序,已去除开屏广告:
对于开屏广告,我们可以观察应用启动的 Acitivity 顺序 (先从主入口切入Main),寻找其函数调用顺序,找到其播送广告的页面,将其逻辑更改,就可以屏蔽掉开屏广告。
首先对视频广告页面进行分析,有暂停键、静音键、详情键、持续时间、会员关闭提示…,我们可以想到:
……
本人选择剩余时间作为破解入口,通过开发者助手查到显示时间的资源 ID 是 R.id.account_ads_time_pre_ad
,搜索资源ID可得三处引用该资源。
通过 hook 分析发现在视频启动时的广告,调用的是 aux 类的函数:
分析 aux 类里使用了R.id.account_ads_time_pre_ad
的方法,找到三处,分别分析:
第一、二处均用在 Xi()
函数中,该函数主要设置广告配置及布置广告界面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
private void Vz() {
......
this.bPB
=
(TextView) findViewById(R.
id
.account_ads_time_pre_ad);
}
private void Xi() {
...
/
/
获取当前广告播放器的状态
BaseState currentState
=
this.mAdInvoker.getCurrentState();
/
/
获取了广告播放器的UI策略
int
adUIStrategy
=
this.mAdInvoker.getAdUIStrategy();
/
/
打印日志
com.iqiyi.video.qyplayersdk.g.aux.i(
"PLAY_SDK_AD_ROLL"
,
"{GPhoneRollAdView}"
,
" show ad UI, current state = "
, currentState,
", adUiStrategy: "
, Integer.valueOf(adUIStrategy));
/
/
设置视图的背景,根据当前广告播放器的状态来选择不同的背景资源
this.bPy.setBackgroundResource(currentState.isOnPaused() ? R.drawable.qiyi_sdk_play_ads_player : R.drawable.qiyi_sdk_play_ads_pause);
/
/
获取了当前广告的交付类型
int
i
=
this.mDeliverType;
boolean z
=
i
=
=
3
|| i
=
=
7
|| i
=
=
4
;
/
/
获取广告播放器配置
QYPlayerADConfig adConfig
=
this.mAdInvoker.getAdConfig();
int
i2
=
8
;
/
/
根据UI策略的不同值,来设置一些视图的可见性或执行一些方法,
8
不可见,
0
可见
if
(adUIStrategy
=
=
1
) {
this.bPA.setVisibility(
8
);
this.bPy.setVisibility(
8
);
this.bPF.setVisibility(
8
);
this.bPz.setVisibility(
8
);
}
else
if
(adUIStrategy
=
=
2
) {
this.bPA.setVisibility(
8
);
this.bPy.setVisibility(
8
);
this.bPz.setVisibility(
8
);
this.bSv.setVisibility(
8
);
this.bSq.setVisibility(
8
);
this.bSq.setOnTouchListener(null);
}
else
if
(adUIStrategy
=
=
3
) {
this.bPA.setVisibility(
8
);
this.bPF.setVisibility(
8
);
boolean isMute
=
isMute();
/
/
检查广告是否处于静音状态
this.bPL
=
isMute;
setAdMute(isMute, false);
}
else
{
this.bPF.setVisibility(
0
);
TextView textView
=
this.bPA;
if
(!this.mIsLand) {
i2
=
0
;
}
textView.setVisibility(i2);
boolean isMute2
=
isMute();
this.bPL
=
isMute2;
setAdMute(isMute2, false);
Xk();
}
if
(this.mDeliverType !
=
6
) {
this.bPB.setVisibility(
0
);
/
/
设置时间视图可显
}
this.bPB.setText(String.valueOf(this.mAdInvoker.getAdDuration()));
/
/
给时间视图赋值
}
|
第三处位于 Xc()
函数中,根据 hook 到的函数调用栈,分析其运行过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
public void Xc() {
/
/
获取广告播放时长
int
adDuration
=
this.mAdInvoker.getAdDuration();
String
str
=
adDuration
+
"";
...
jv(adDuration);
/
/
判断能不能跳过广告
if
(XE()) {
XH();
}
TextView textView
=
this.bPB;
/
/
设置剩余时间
if
(textView !
=
null) {
textView.setText(
str
);
/
/
显示非VIP持续时间
}
int
i
=
this.mDeliverType;
if
(i
=
=
3
|| i
=
=
7
) {
/
/
如果交付类型是
3
或
7
(VIP广告),广告持续时间小于
1
,调用dz(false)
if
(adDuration <
1
) {
dz(false);
}
else
{
this.bSA.setText(
str
);
/
/
显示VIP持续时间
}
}
if
(this.mDeliverType
=
=
2
) {
/
/
允许跳过的广告
int
Xp
=
Xp();
/
/
广告可跳过的剩余时间
if
(Xp <
1
) {
/
/
允许跳过
Xl();
/
/
显示跳过按钮
}
else
{
this.bSG.setText(this.mContext.getString(R.string.trueview_accountime, Integer.valueOf(Xp)));
}
}
/
/
省流:根据不同的交付类型,为不同类型的广告进行时间配置与视图是否可显操作
...
}
/
/
处理广告的交互时间限制逻辑
private void jv(
int
i) {
/
/
判断是否为触摸广告,是否支持点击跳转,并且是否已经被点击过
if
(!this.bOR.isTouchAd() || this.bOR.getClickThroughType() !
=
0
|| this.bTn) {
return
;
/
/
是,直接返回
}
/
/
获取广告的预览信息
PreAD creativeObject
=
this.bOR.getCreativeObject();
/
/
getInterTouchTime()是广告中点击交互的时间间隔,返回
10
,表示用户需要等待至少
10
秒之后才能进行一次点击交互。小于
0
,说明可以点击。
/
/
后面一个条件是指当前时间加上最早允许交互的时间点,如果超过广告总时长,则不允许交互,比如总时长
120
秒,getInterTouchTime() 返回
40
,当前时间为
100
秒,大于总时长,不允许交互。
if
(creativeObject.getInterTouchTime() <
=
-
1
|| i
+
creativeObject.getInterTouchTime() > this.bTp) {
return
;
}
/
/
重置广告界面,继续播放
this.bSq.reset();
Wu();
}
/
/
判断当前广告是创意广告
private boolean XE() {
CupidAD<PreAD> cupidAD
=
this.bOR;
if
(cupidAD
=
=
null || cupidAD.getCreativeObject()
=
=
null) {
return
false;
}
return
this.bOR.getDeliverType()
=
=
10
|| this.bOR.getDeliverType()
=
=
11
;
}
/
/
计算广告可跳过的剩余时间
private
int
Xp() {
if
(this.bOR.getDeliverType() !
=
2
) {
return
0
;
}
return
(this.bOR.getSkippableTime()
/
1000
)
-
((this.bOR.getDuration()
/
1000
)
-
this.mAdInvoker.getAdDuration());
}
|
上面两个函数都是对布局文件进行操作,设置其 text 或者是否可显,并没有判断去掉广告的地方,我们还有继续寻找。
对比两个函数发现,获取持续时间的函数是 getAdDuration(),我们去寻找该函数声明,发现在 com.iqiyi.video.qyplayersdk.player.QYMediaPlayerProxy
类中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
public
int
getAdDuration() {
com.iqiyi.video.qyplayersdk.core.com1 com1Var
=
this.mPlayerCore;
if
(com1Var
=
=
null) {
return
0
;
}
return
com1Var.getAdsTimeLength();
}
/
/
位于 com.iqiyi.video.qyplayersdk.core.QYBigCorePlayer 类中
public
int
getAdsTimeLength() {
com8 com8Var
=
this.pumaPlayer;
if
(com8Var !
=
null) {
return
Math.
round
(com8Var.GetADCountDown()
/
1000.0f
);
/
/
转成整数
}
return
0
;
}
/
/
com.mcto.player.nativemediaplayer.NativeMediaPlayer 类中
public
int
GetADCountDown() {
int
GetADCountDown;
if
(IsCalledInPlayerThread()) {
/
/
判断是否在播放器线程中调用
return
this.native_media_player_bridge.GetADCountDown();
/
/
获取广告持续时间
}
synchronized (this) {
if
(!this.native_player_valid) {
/
/
判断播放器是否合法
throw new MctoPlayerInvalidException(puma_state_error_msg);
}
GetADCountDown
=
this.native_media_player_bridge.GetADCountDown();
}
return
GetADCountDown;
}
/
/
com.mcto.player.nativemediaplayer.NativeMediaPlayerBridge 类中
public
int
GetADCountDown() {
/
/
调用了一个指定
ID
为
43
的方法,该方法返回一个JSON格式的字符串,其中包含有关广告信息的数据
String InvokeMethod
=
InvokeMethod(
43
,
"{}"
);
if
(InvokeMethod.isEmpty()) {
/
/
返回的字符串为空,则表示当前没有广告,方法返回
0
。
return
0
;
}
try
{
/
/
返回的字符串不为空,则将其转换为JSONObject对象,并获取其中名为ad_count_down的值
return
new JSONObject(InvokeMethod).getInt(
"ad_count_down"
);
} catch (JSONException unused) {
return
0
;
}
}
|
跟进到 com.mcto.player.nativemediaplayer.NativeMediaPlayerBridge 我们就可以发现,该软件是在Native层利用 mediaplay 获取视频时间信息。到这里获取剩余时间的 Java 层分析就差不多可以了。我们可以看到的是在 NativeMediaPlayerBridge
这个类中调用了众多 native 方法去获取广告的各种信息供后续操作,但是将所有的方法全修改一遍不太现实,我们需要寻找判断是否显示广告界面的地方。
根据 hook 上层类的方法调用发现,QYMediaPlayerProxy
类中存在一些可能是与加载广告界面相关的函数。
几个重要的函数分析:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
|
/
/
setVVCollector():设置VVCollector,收集播放器的VV统计信息。
/
/
video view (VV),意思为视频播放次数,根据广告播放次数,统计盈利。
public void setVVCollector(com.iqiyi.video.qyplayersdk.module.a.f.con conVar) {
com.iqiyi.video.qyplayersdk.module.a.aux auxVar
=
this.mStatistics;
if
(auxVar !
=
null) {
auxVar.setVVCollector(conVar);
}
}
/
/
init(): 初始化播放器界面
/
/
获取了mControlConfig中的一些配置信息,例如编解码类型、是否自动跳过片头片尾、色盲模式等,然后调用prn.aux构造方法创建一个prn对象,并设置这些配置信息,最后通过a()方法将prn对象和mPassportAdapter对象一起传入a方法中,完成播放器的初始化。
public void init() {
this.mPlayerCore.a(new prn.aux(this.mControlConfig.getCodecType())
.eH(this.mControlConfig.isAutoSkipTitle())
.eI(this.mControlConfig.isAutoSkipTrailer())
.kR(this.mControlConfig.getColorBlindnessType())
.lX(this.mControlConfig.getExtendInfo())
.lY(this.mControlConfig.getExtraDecoderInfo())
.aie(), com.iqiyi.video.qyplayersdk.core.data.aux.a(this.mPassportAdapter));
}
/
/
检查 RC 策略是否需要执行
/
/
RC 策略是指在不同的地理位置或网络环境下,根据不同的版权限制或合作协议,播放不同的内容或提供不同的服务。
public PlayData checkRcIfRcStrategyNeeded(PlayData playData) {
if
(playData
=
=
null) {
com.iqiyi.video.qyplayersdk.g.aux.d(TAG,
"QYMediaPlayerProxy checkRcIfRcStrategyNeeded source == null!"
);
return
playData;
}
int
rCCheckPolicy
=
playData.getRCCheckPolicy();
com.iqiyi.video.qyplayersdk.g.aux.d(TAG,
"QYMediaPlayerProxy checkRcIfRcStrategyNeeded strategy == "
+
rCCheckPolicy);
if
(this.mPlayerRecordAdapter
=
=
null) {
this.mPlayerRecordAdapter
=
new PlayerRecordAdapter();
}
/
/
根据 RCCheckPolicy (即 RC 策略) 的值。
/
/
如果值为
2
,直接返回 playData;如果值为
1
或
0
,,则调用 PlayerRecordAdapter 的 retrievePlayerRecord 方法,获取播放记录,
return
rCCheckPolicy
=
=
2
? playData : (rCCheckPolicy
=
=
1
|| rCCheckPolicy
=
=
0
) ?
com.iqiyi.video.qyplayersdk.player.data.b.con.a(playData, this.mPlayerRecordAdapter.retrievePlayerRecord(playData)) : playData;
}
/
/
获取登录用户信息
void login() {
IPassportAdapter iPassportAdapter;
/
/
mPlayerCore 是播放器核心,mPassportAdapter 是用户身份验证适配器。
if
(this.mPlayerCore
=
=
null || (iPassportAdapter
=
this.mPassportAdapter)
=
=
null) {
return
;
}
/
/
判断是不是VIP用户,并获取相应用户信息
this.mPlayerCore.login(com.iqiyi.video.qyplayersdk.core.data.aux.a(iPassportAdapter));
}
/
/
准备播放器重要核心配置
private void prepareBigCorePlayback(PlayData playData) {
boolean z;
org.qiyi.android.coreplayer.d.com7.beginSection(
"QYMediaPlayerProxy.prepareBigCorePlayback"
);
/
/
检查是否需要预加载
com.iqiyi.video.qyplayersdk.h.con conVar
=
this.mPreload;
if
(conVar !
=
null) {
conVar.aoj();
}
/
/
根据播放数据和控制配置,选择一个播放策略,根据策略选择对应操作
int
a2
=
com.iqiyi.video.qyplayersdk.player.data.b.nul.a(playData, this.mContext, this.mControlConfig);
com.iqiyi.video.qyplayersdk.g.aux.e(
"PLAY_SDK"
,
"vplay strategy : "
+
a2);
switch (a2) {
case
1
:
performBigCorePlayback(playData);
break
;
case
2
:
z
=
true;
doVPlayBeforePlay(playData, z);
break
;
case
3
:
doVPlayFullBeforePlay(playData);
break
;
case
4
:
doVPlayAfterPlay(playData);
break
;
case
5
:
if
(com.iqiyi.video.qyplayersdk.g.aux.isDebug()) {
throw new RuntimeException(
"address & tvid & ctype are null"
);
}
com.iqiyi.video.qyplayersdk.g.aux.e(
"PLAY_SDK"
,
"address & tvid & ctype are null"
);
break
;
case
6
:
z
=
false;
doVPlayBeforePlay(playData, z);
break
;
}
org.qiyi.android.coreplayer.d.com7.endSection();
}
/
/
视频播放结束后,继续获取视频的相关信息。
public void doVPlayAfterPlay(final PlayData playData) {
performBigCorePlayback(playData);
lpt6 lpt6Var
=
this.mTaskExecutor;
if
(lpt6Var !
=
null) {
lpt6Var.q(new Runnable() {
/
/
from
class
: com.iqiyi.video.qyplayersdk.player.QYMediaPlayerProxy.
1
@Override
/
/
java.lang.Runnable
public void run() {
QYMediaPlayerProxy.this.requestVplayInfo(playData);
}
});
}
}
/
/
在获取视频源前获取一些与视频相关的信息
private void doVPlayBeforePlay(PlayData playData, boolean z) {
VPlayParam a2
=
com.iqiyi.video.qyplayersdk.player.data.b.con.a(playData, VPlayHelper.CONTENT_TYPE_PLAY_CONDITION, this.mPassportAdapter);
this.mVPlayHelper.cancel();
/
/
请求 VPlay 信息
this.mVPlayHelper.requestVPlay(this.mContext, a2, new aux(this, playData, this.mSigt, z), this.mBigcoreVplayInterceptor);
sendVPlayRequestPingback(true, playData, this.mSigt);
com.iqiyi.video.qyplayersdk.b.com3.b(playData);
com.iqiyi.video.qyplayersdk.g.aux.d(
"PLAY_SDK"
, TAG,
" doVPlayBeforePlay needRequestFull="
, Boolean.valueOf(z));
}
/
/
判断是否需要网络拦截
private boolean isNeedNetworkInterceptor(PlayerInfo playerInfo) {
/
/
是否需要忽略用户代理的拦截
if
(ignoreNetworkInterceptByUA()) {
com.iqiyi.video.qyplayersdk.g.aux.d(
"PLAY_SDK"
, TAG,
"ignoreNetworkInterceptByUA "
);
return
false;
}
/
/
判断当前是否处于离线状态,并且要播放的视频是在线视频
boolean gW
=
org.iqiyi.video.l.aux.gW(this.mContext);
boolean D
=
com.iqiyi.video.qyplayersdk.player.data.b.nul.D(playerInfo);
if
(gW && D) {
/
/
获取当前的错误码版本号,根据不同的版本号来执行不同的逻辑
int
errorCodeVersion
=
getErrorCodeVersion();
com.iqiyi.video.qyplayersdk.g.aux.d(
"PLAY_SDK"
, TAG,
"isNeedNetworkInterceptor isOffNetWork = "
, Boolean.valueOf(gW),
" isOnLineVideo = "
, Boolean.valueOf(D),
" errorCodeVer = "
+
errorCodeVersion);
if
(errorCodeVersion
=
=
1
) {
/
/
自定义错误码为
900400
的播放器错误
this.mInvokerQYMediaPlayer.onError(PlayerError.createCustomError(
900400
,
"current network is offline, but you want to play online video"
));
return
true;
/
/
进行网络拦截
}
else
if
(errorCodeVersion
=
=
2
) {
/
/
返回错误码和错误信息
org.iqiyi.video.data.com7 bbQ
=
org.iqiyi.video.data.com7.bbQ();
bbQ.xC(String.valueOf(
900400
));
bbQ.setDesc(
"current network is offline, but you want to play online video"
);
this.mInvokerQYMediaPlayer.onErrorV2(bbQ);
return
true;
}
}
return
false;
/
/
不需要进行网络拦截
}
|
我们重点分析 performBigCorePlayback
函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
|
/
/
执行播放器的核心播放功能
private void performBigCorePlayback(PlayData playData, PlayerInfo playerInfo, String
str
) {
int
i;
/
/
判断是否有自定义的播放拦截器(mDoPlayInterceptor),如果有且拦截器拦截了播放请求,则不播放视频。
com.iqiyi.video.qyplayersdk.f.con conVar
=
this.mDoPlayInterceptor;
if
(conVar !
=
null && conVar.e(playerInfo)) {
com.iqiyi.video.qyplayersdk.g.aux.d(
"PLAY_SDK"
, TAG,
"DoPlayInterceptor is intercept!"
);
lpt5 lpt5Var
=
this.mInvokerQYMediaPlayer;
if
(lpt5Var
=
=
null) {
return
;
}
lpt5Var.amX();
/
/
没有播放器信息,什么都不做
}
else
if
(this.mPlayerInfo
=
=
null) {
}
/
/
重点
else
{
org.qiyi.android.coreplayer.d.com7.beginSection(
"QYMediaPlayerProxy.performBigCorePlayback"
);
/
/
通过判断播放数据(playData)是否为空以及是否存在播放地址,空则i
=
0
。
if
(com.iqiyi.video.qyplayersdk.player.data.b.nul.A(playerInfo) || playData
=
=
null) {
i
=
0
;
}
else
{
/
/
如果有地址,根据该数据生成CupidVvId,并将该
ID
与广告相关的Ad对象(mAd)绑定。
/
/
所以这里就是去后台获取广告的
id
com.iqiyi.video.qyplayersdk.cupid.data.model.com9 a2
=
com.iqiyi.video.qyplayersdk.cupid.util.con.a(playData, playerInfo, false, this.mPlayerRecordAdapter,
0
);
a2.eV(isIgnoreFetchLastTimeSave());
int
generateCupidVvId
=
CupidAdUtils.generateCupidVvId(a2, playData.getPlayScene());
com.iqiyi.video.qyplayersdk.cupid.com4 com4Var
=
this.mAd;
if
(com4Var !
=
null) {
com4Var.la(generateCupidVvId);
/
/
更新当前的广告
ID
}
org.qiyi.android.coreplayer.d.aux.boe();
i
=
generateCupidVvId;
}
/
/
a3 存储广告信息
com.iqiyi.video.qyplayersdk.core.data.model.com1 a3
=
com.iqiyi.video.qyplayersdk.core.data.a.aux.a(this.mSigt, i, playData, playerInfo,
str
, this.mControlConfig);
com.iqiyi.video.qyplayersdk.g.aux.d(
"PLAY_SDK"
, TAG,
" performBigCorePlayback QYPlayerMovie="
, a3);
this.mPlayerInfo
=
new PlayerInfo.Builder().copyFrom(playerInfo).extraInfo(new PlayerExtraInfo.Builder().copyFrom(playerInfo.getExtraInfo()).sigt(a3.getSigt()).build()).build();
/
/
通知播放器信息已更改(在这里是指开始播放广告)
notifyPlayerInfoChanged();
/
/
判断是否断网
if
(!isNeedNetworkInterceptor(playerInfo)) {
if
(playData
=
=
null || (TextUtils.isEmpty(playData.getPlayAddress()) && (TextUtils.isEmpty(playData.getTvId()) ||
"0"
.equals(playData.getTvId())))) {
PlayerExceptionTools.report(
0
,
0.1f
,
"1"
, com.iqiyi.video.qyplayersdk.player.data.b.con.i(playData));
}
com.iqiyi.video.qyplayersdk.core.com1 com1Var
=
this.mPlayerCore;
if
(com1Var !
=
null) {
com1Var.setVideoPath(a3);
/
/
设置广告url
this.mPlayerCore.ahF();
}
}
org.qiyi.android.coreplayer.d.com7.endSection();
}
}
/
/
停止视频
public void amX() {
d dVar
=
this.mQYMediaPlayer;
if
(dVar !
=
null) {
dVar.stopPlayback();
}
}
/
/
判断是否获取到视频
public static boolean A(PlayerInfo playerInfo) {
return
z(playerInfo) || y(playerInfo);
}
/
/
获取PlayerExtraInfo对象的播放地址和播放地址类型
public static boolean z(PlayerInfo playerInfo) {
if
(playerInfo
=
=
null || playerInfo.getExtraInfo()
=
=
null) {
return
false;
}
PlayerExtraInfo extraInfo
=
playerInfo.getExtraInfo();
String playAddress
=
extraInfo.getPlayAddress();
int
playAddressType
=
extraInfo.getPlayAddressType();
if
(TextUtils.isEmpty(playAddress)) {
return
false;
}
return
playAddressType
=
=
9
|| playAddressType
=
=
4
|| playAddressType
=
=
8
;
}
/
/
判断是否有视频和专辑
ID
public static boolean y(PlayerInfo playerInfo) {
String s
=
s(playerInfo);
/
/
专辑
ID
String u
=
u(playerInfo);
/
/
视频
ID
if
((TextUtils.isEmpty(s) || TextUtils.equals(s,
"0"
)) && !((!TextUtils.isEmpty(u) && !TextUtils.equals(u,
"0"
)) || playerInfo
=
=
null || playerInfo.getExtraInfo()
=
=
null)) {
/
/
获取PlayerExtraInfo对象的播放地址和播放地址类型
PlayerExtraInfo extraInfo
=
playerInfo.getExtraInfo();
return
!TextUtils.isEmpty(extraInfo.getPlayAddress()) && extraInfo.getPlayAddressType()
=
=
6
;
}
return
false;
}
/
/
获取专辑
ID
public static String s(PlayerInfo playerInfo) {
String
id
;
return
(playerInfo
=
=
null || playerInfo.getAlbumInfo()
=
=
null || (
id
=
playerInfo.getAlbumInfo().getId())
=
=
null) ? "" :
id
;
}
/
/
获取视频
ID
public static String u(PlayerInfo playerInfo) {
String
id
;
return
(playerInfo
=
=
null || playerInfo.getVideoInfo()
=
=
null || (
id
=
playerInfo.getVideoInfo().getId())
=
=
null) ? "" :
id
;
}
/
/
一个广告控制器方法,用于更新当前的CupidvvId
public void la(
int
i) {
/
/
col
=
0
,则说明当前没有活跃的vvId,打印日志信息表示要更新当前的vvId
if
(this.col.getAndIncrement()
=
=
0
) {
com.iqiyi.video.qyplayersdk.g.aux.i(
"PLAY_SDK_AD_MAIN"
,
"{AdsController}"
,
" update current cupid vvId. current doesn't has active vvId."
);
}
else
{
com.iqiyi.video.qyplayersdk.g.aux.i(
"PLAY_SDK_AD_MAIN"
,
"{AdsController}"
,
" update current cupid vvId. but current has active vvId."
);
/
/
将旧的vvId赋值给coh变量
this.coh
=
this.coi;
}
/
/
将当前新的
ID
赋给coi
this.coi
=
i;
lc(i);
com5.aux auxVar
=
this.mQYAdPresenter;
if
(auxVar !
=
null) {
auxVar.lh(i);
/
/
为暂停播放函数与继续播放函数传递广告
ID
}
}
/
*
该方法用于注册广告委托和委托JSON,以展示广告
通过 qYPlayerADConfig3.checkRegister 方法判断是否需要注册广告
通过 Cupid.registerObjectAppDelegate 方法注册代理
广告类型包括:
中插广告(SlotType.SLOT_TYPE_BRIEF_ROLL)、
viewpoint广告(SlotType.SLOT_TYPE_VIEWPOINT)、
页面广告(SlotType.SLOT_TYPE_PAGE)等等
代码过长就不再此展示,需要请自行查看
*
/
private void lc(final
int
i) {
com.iqiyi.video.qyplayersdk.g.aux.d(
"PLAY_SDK_AD_CORE"
,
"{AdsController}"
,
"; registerCupidJsonDelegate vvId:"
, Integer.valueOf(i), "");
org.qiyi.android.coreplayer.d.aux.wr(com.qiyi.baselib.utils.d.nul.fJ(org.iqiyi.video.mode.com3.enn) ?
2
:
1
);
...
QYPlayerADConfig qYPlayerADConfig5
=
this.cog;
if
(qYPlayerADConfig5.checkRegister(
256
, qYPlayerADConfig5.getAddAdPolicy())) {
QYPlayerADConfig qYPlayerADConfig6
=
this.cog;
if
(!qYPlayerADConfig6.checkRegister(
256
, qYPlayerADConfig6.getRemoveAdPolicy())) {
Cupid.registerJsonDelegate(i, SlotType.SLOT_TYPE_VIEWPOINT.value(), this.cof);
}
}
...
}
|
我们可以发现这个函数就是判断是否显示广告界面的函数,可以猜测只有当是VIP账户时,播放数据(playData)才为空,才会使 i = 0(广告ID为0)。
到这里我们就可以尝试进行破解了,将 if 判断修改,使之进入 i=0 的分支中。
重打包编译签名,运行程序,已去除视频广告:
分析代码后发现,广告的生成、调用、配置大部分都是在 QYMediaPlayerProxy
类中完成的,并且播放器的核心功能也有一部分在代理类中调用。
对于第三方SDK动态导入视频广告,通常会通过网络请求向广告服务器发送请求以获取广告,流程参考下方 android 广告 SDK 原理流程图,常用方法使通过动态代理,通过动态代理这样的方法有一定的好处:
进一步分析,我们可以想到广告不太会是在软件刚出来时就加上,一定是后续附加上去的功能。后续除了广告之外肯定也会陆续附加其他功能,如何做到这些功能扩展呢?这就可以用 proxy 代理类了,将播放器核心功能(播放视频)融入到代理类中,让其负责对核心功能进行扩展(如在播放视频之前添加广告)。这样既方便后续软件更新,也会使逻辑更加清晰、出错时能快速定位。
android 广告 SDK 原理流程图
[参考链接]:
更多【某艺TV版 apk 破解去广告及源码分析】相关视频教程:www.yxfzedu.com