前言
作为一名 哔哩哔哩的重度用户 ,我一直期待官方推出 3 倍速播放 功能。然而等了许久,这个功能始终没有上线 😮💨。
修改前效果: 
刚好我自己熟悉 iOS 逆向工程,于是决定 亲自动手,为 B 站加入 3 倍速播放 😆。
修改后效果: 
由于整个过程涉及 多处逻辑修改与多个模块的反汇编分析 ,为了让内容更加清晰易读,我将会分成多篇文章,逐步拆解 如何为 B 站增加 3 倍速播放能力。
场景
哔哩哔哩的视频播放页面

开发环境
- 哔哩哔哩版本:
8.41.0 - MonkeyDev
IDA Professional 9.0- 安装IDA插件:
patching Lookin
目标
视频最大播放速度改为4倍速播放
分析
- 我们知道哔哩哔哩开源了他们的视频播放器ijkplayer,我们可以从中了解到设置播放速度的方法,是
IJKFFMoviePlayerController的- (void)setPlaybackRate:(float)playbackRate
objc
@interface IJKFFMoviePlayerController : NSObject
@property (nonatomic) float playbackRate;
@end
- 从
Mach-O文件导出的IJKFFMoviePlayerController的OC头文件可以知道,它有一个maxPlaybackRate属性,应该是最大播放速度
objc
// IJKFFMoviePlayerController.h
@interface IJKFFMoviePlayerController : NSObject {
/* instance variables */
id _player;
}
...
@property (readonly, nonatomic) double realCurrentPlaybackTime;
@property (readonly, nonatomic) float realPlaybackRate;
@property (readonly, nonatomic) float maxPlaybackRate;
@property (readonly, nonatomic) IJKMediaPlayerItem *currentItem;
...
- 我们
hookIJKFFMoviePlayerController的- (void)setPlaybackRate:(float)playbackRate,添加断点并打印一些日志,其中就有打印maxPlaybackRateinputPlaybackRate:要设置的播放速度changedPlaybackRate:更改后的播放速度realPlaybackRate:真实播放速度maxPlaybackRate:最大播放速度
objc
%hook IJKFFMoviePlayerController
- (void)setPlaybackRate:(float)playbackRate {
%orig(playbackRate);
NSLog(@"%@:%@-%p-%s-inputPlaybackRate:%lf-changedPlaybackRate:%lf-realPlaybackRate%lf-maxPlaybackRate:%lf", nj_logPrefix, NSStringFromClass([(id)self class]), self, __FUNCTION__, playbackRate, self.playbackRate, self.realPlaybackRate, self.maxPlaybackRate);
}
%end
- 每次设置播放速度,
IJKFFMoviePlayerController的- (void)setPlaybackRate:(float)playbackRate的断点都会触发,我们从日志中可以看到,最大播放速度为3.0
objc
cxzcxz:IJKFFMoviePlayerController-0x280519220-_logos_method$App$IJKFFMoviePlayerController$setPlaybackRate$-inputPlaybackRate:1.500000-changedPlaybackRate:1.500000-realPlaybackRate0.000000-maxPlaybackRate:3.000000
- 因为倍速面板无法选择超过
2.0的速度,所以我们就设置,如果倍速面板选择的是2.0,我们就改成4.0,看是否会限制播放速度到3.0
objc
%hook IJKFFMoviePlayerController
- (void)setPlaybackRate:(float)playbackRate {
playbackRate = playbackRate == 2.0 ? 4.0 : playbackRate;
%orig(playbackRate);
NSLog(@"%@:%@-%p-%s-inputPlaybackRate:%lf-changedPlaybackRate:%lf-realPlaybackRate%lf-maxPlaybackRate:%lf", nj_logPrefix, NSStringFromClass([(id)self class]), self, __FUNCTION__, playbackRate, self.playbackRate, self.realPlaybackRate, self.maxPlaybackRate);
}
%end
- 倍速面板选择
2.0,从日志中可以看到,播放速度(changedPlaybackRate)改成了3.0而不是4.0,这也证明maxPlaybackRate就是最大播放速度
objc
cxzcxz:IJKFFMoviePlayerController-0x281304380-_logos_method$App$IJKFFMoviePlayerController$setPlaybackRate$-inputPlaybackRate:4.000000-changedPlaybackRate:3.000000-realPlaybackRate0.000000-maxPlaybackRate:3.000000
- 我们尝试
hookmaxPlaybackRate的getter方法,看能不能修改最大播放速度?
objc
%hook IJKFFMoviePlayerController
- (void)setPlaybackRate:(float)playbackRate {
playbackRate = playbackRate == 2.0 ? 4.0 : playbackRate;
%orig(playbackRate);
NSLog(@"%@:%@-%p-%s-inputPlaybackRate:%lf-changedPlaybackRate:%lf-realPlaybackRate%lf-maxPlaybackRate:%lf", nj_logPrefix, NSStringFromClass([(id)self class]), self, __FUNCTION__, playbackRate, self.playbackRate, self.realPlaybackRate, self.maxPlaybackRate);
}
- (float)maxPlaybackRate {
return 4.0;
}
%end
- 从日志可以看到,虽然
maxPlaybackRate的输出值改成了4.0,但是播放速度(changedPlaybackRate)还是3.0,所以修改无效
objc
cxzcxz:IJKFFMoviePlayerController-0x2808e2a40-_logos_method$App$IJKFFMoviePlayerController$setPlaybackRate$-inputPlaybackRate:4.000000-changedPlaybackRate:3.000000-realPlaybackRate0.000000-maxPlaybackRate:4.000000
- 我们从
IDA中查看IJKFFMoviePlayerController的- (void)setPlaybackRate:(float)playbackRate伪代码实现,发现调用的是[self->_player setPlaybackRate:]方法
objc
void __cdecl -[IJKFFMoviePlayerController setPlaybackRate:](IJKFFMoviePlayerController *self, SEL a2, float a3)
{
-[IJKMediaPlayback setPlaybackRate:](self->_player, "setPlaybackRate:");
}
- 我们给
IJKFFMoviePlayerController的- (void)setPlaybackRate:(float)playbackRate,添加断点,看看self->_player的值是什么?

- 断点触发,发现
self->_player的类型是IJKFFMoviePlayerControllerFFPlay
objc
(lldb) po self->_player
- 从
Mach-O文件导出的IJKFFMoviePlayerControllerFFPlay的OC头文件可以知道,IJKFFMoviePlayerControllerFFPlay是一个视频播放器,看来IJKFFMoviePlayerController是基于IJKFFMoviePlayerControllerFFPlay实现的
objc
@interface IJKFFMoviePlayerControllerFFPlay : NSObject {
/* instance variables */
struct IjkMediaPlayer * _mediaPlayer;
...
@property (weak, nonatomic) IJKMediaPlayerItem *item;
@property (retain, nonatomic) id fileOpenDelegate;
@property (retain, nonatomic) id segmentOpenDelegate;
@property (readonly, nonatomic) double fpsInMeta;
@property (readonly, nonatomic) double fpsAtOutput;
@property (nonatomic) _Bool shouldShowHudView;
@property (readonly, nonatomic) long long numberOfBytesTransferred;
@property (nonatomic) _Bool allowsMediaAirPlay;
@property (nonatomic) _Bool isDanmakuMediaAirPlay;
@property (readonly, nonatomic) _Bool airPlayMediaActive;
@property (readonly, nonatomic) int isSeekBuffering;
@property (readonly, nonatomic) int isAudioSync;
@property (readonly, nonatomic) int isVideoSync;
@property (readonly, nonatomic) int currentVideoIdentifier;
@property (readonly, nonatomic) int currentAudioIdentifier;
@property (readonly, nonatomic) double realCurrentPlaybackTime;
@property (readonly, nonatomic) float realPlaybackRate;
@property (readonly, nonatomic) float maxPlaybackRate;
@property (readonly, nonatomic) IJKMediaPlayerItem *currentItem;
...
- 我们从
IDA中查看IJKFFMoviePlayerControllerFFPlay的- (void)setPlaybackRate:(float)playbackRate伪代码实现,发现跟开源版本的IJKFFMoviePlayerController的- (void)setPlaybackRate:(float)playbackRate是一致的
objc
// 哔哩哔哩版本
void __cdecl -[IJKFFMoviePlayerControllerFFPlay setPlaybackRate:](
IJKFFMoviePlayerControllerFFPlay *self,
SEL a2,
float a3)
{
sub_10F3F5768(
0LL,
32LL,
"IJKFFMoviePlayerControllerFFPlay: setPlaybackRate ts = %lld, playbackRate = %f\n",
+[IJKFFUtils getIjkTickHR](&OBJC_CLASS___IJKFFUtils, "getIjkTickHR"),
a3);
if ( self->_mediaPlayer )
sub_10F0BAFD4(a3);
}
objc
// 开源版本
- (void)setPlaybackRate:(float)playbackRate
{
if (!_mediaPlayer)
return;
return ijkmp_set_playback_rate(_mediaPlayer, playbackRate);
}
- 我们从
IDA中查看sub_10F0BAFD4的伪代码实现,发现跟开源版本的ijkmp_set_playback_rate的实现是一致的
objc
// 哔哩哔哩版本
__int64 __fastcall sub_10F0BAFD4(__int64 a1, float a2)
{
printf("%s(%f)\n", "ijkmp_set_playback_rate", a2);
pthread_mutex_lock((pthread_mutex_t *)(a1 + 8));
sub_10F0A70B4(*(_QWORD *)(a1 + 136), a2);
pthread_mutex_unlock((pthread_mutex_t *)(a1 + 8));
return printf("%s()=void\n", "ijkmp_set_playback_rate");
}
objc
// 开源版本
void ijkmp_set_playback_rate(IjkMediaPlayer *mp, float rate)
{
assert(mp);
MPTRACE("%s(%f)\n", __func__, rate);
pthread_mutex_lock(&mp->mutex);
ffp_set_playback_rate(mp->ffplayer, rate);
pthread_mutex_unlock(&mp->mutex);
MPTRACE("%s()=void\n", __func__);
}
- 我们从
IDA中查看sub_10F0A70B4的伪代码实现,可以知道是从sub_10F101034获取最大播放速度的
objc
__int64 __fastcall sub_10F0A70B4(__int64 result, float a2)
{
__int64 v3; // x19
float v4; // s0
__int64 v5; // x8
float v6; // s1
float v7; // [xsp+1Ch] [xbp-24h] BYREF
if ( result )
{
v3 = result;
v7 = 0.0;
sub_10F101034(10LL, &v7, 4LL);
if ( v7 < a2 )
{
sub_10F3F5768(0LL, 32LL, "%s: origin %f, new %f \n", "adjust_playback_rate", a2, v7);
a2 = v7;
}
sub_10F3F5768((__int64 *)v3, 32LL, "Playback rate: %f\n", a2);
if ( a2 == 0.0 )
v4 = 1.0;
else
v4 = a2;
if ( v4 != 1.0 )
*(_DWORD *)(v3 + 884) = 1;
v5 = *(_QWORD *)(v3 + 8);
if ( v5 )
{
v6 = *(float *)(v3 + 876);
if ( v6 != v4 )
*(double *)(v5 + 8040) = vabds_f32(v6, v4);
}
if ( v4 > *(float *)(v3 + 6724) )
*(float *)(v3 + 6724) = v4;
*(float *)(v3 + 876) = v4;
*(_DWORD *)(v3 + 880) = 1;
return sub_10F100F98(8LL, v3 + 9272);
}
return result;
}
- 我们从
IDA中查看sub_10F101034的伪代码实现,再根据sub_10F101034(10LL, &v7, 4LL); 知道参数a1=10,a3=4,将伪代码和参数交给chatgpt分析,得出下面几个结论:sub_10F10449C(a9)计算最大播放速度- 该函数内部会将计算结果放入某个寄存器(例如
d0) - 解码器将其反编译成
v14 LABEL_38 → v13 = v14- 最终写入:
*(float*)a2 = v13
objc
void __fastcall sub_10F101034(
int a1,
double *a2,
__int64 a3,
__int64 a4,
__int64 a5,
__int64 a6,
__int64 a7,
__int64 a8,
_QWORD *a9,
__int64 a10,
unsigned int a11,
__int64 a12)
{
float v13; // s0
double v14; // d0
__int64 v15; // x20
int v16; // w0
__int64 *v17; // [xsp+18h] [xbp-68h] BYREF
__int64 v18; // [xsp+20h] [xbp-60h]
__int64 v19; // [xsp+30h] [xbp-50h]
unsigned int *v20; // [xsp+58h] [xbp-28h]
if ( a2 && a3 )
{
switch ( a1 )
{
...
case 10:
if ( a3 == 4 )
{
v17 = &a10;
sub_10F10449C((unsigned int)a9);
goto LABEL_38;
}
break;
...
LABEL_38:
v13 = v14;
}
LABEL_44:
*(float *)a2 = v13;
}
break;
...
- 我们从
IDA中查看sub_10F10449C的伪代码实现,发现有可能返回两个值,一个是2.0,一个是全局变量qword_117084280的值
objc
double __fastcall sub_10F10449C(__int64 a1)
{
__int64 v2; // x0
double result; // d0
double v4[6]; // [xsp+0h] [xbp-40h] BYREF
v2 = sub_10F104600();
sub_10F104B7C(v4, v2, a1);
result = 2.0;
if ( v4[0] < 50.0 )
return *(double *)&qword_117084280;
return result;
}
-
我们查看全局变量
qword_117084280的值,发现是3.0,而3.0就是最大播放速度-
qword_117084280的16进制值objc0000000117084280 00 00 00 00 00 00 08 40 00 00 00 00 00 00 00 00 -
关键是前
8字节:objc00 00 00 00 00 00 08 40 -
ARM64、Mach-O中double存储方式都是little-endian,所以按小端序解析:objc40 08 00 00 00 00 00 00 -
IEEE-754双精度格式:objc0x4008000000000000 → 3.0(double) -
所以
qword_117084280 = 3.0
-
-
我们添加符号断点
sub_10F10449C,看它的返回值多少

sub_10F10449C断点触发,返回值发现是3.0,这样就验证了全局变量qword_117084280的值是3.0
objc
(lldb) register read d0
d0 = 3
- 总结 :哔哩哔哩的最大播放速度方法是
sub_10F10449C,最大播放速度有可能是2倍速,也可能是3倍速,猜测跟视频有关。
越狱解决方案
我们hook sub_10F10449C,将最大值改为4.0
swift
// 视频最大播放速度
public let maxPlaybackRateValue = 4.0
// 声明原函数类型
public typealias orig_get_max_playback_rate_type = @convention(c) (_ a1: Int64) -> Double
// 定义全局函数指针变量,并绑定一个 C 名字
@_silgen_name("orig_get_max_playback_rate")
nonisolated(unsafe) public var orig_get_max_playback_rate: orig_get_max_playback_rate_type? = nil
// 获取最大播放速度方法
@_cdecl("my_get_max_playback_rate")
func my_get_max_playback_rate(a1: Int64) -> Double {
return maxPlaybackRateValue
}
objc
// 获取最大播放速度方法
long long get_max_playback_rate_address = g_slide+0x10F10449C;
NSLog(@"[%@] cal func get_max_playback_rate address:0x%llx", nj_logPrefix, get_max_playback_rate_address);
MSHookFunction((void *)get_max_playback_rate_address,
(void*)my_get_max_playback_rate,
(void**)&orig_get_max_playback_rate);
- 我们再
hookIJKFFMoviePlayerControllerFFPlay的- (void)setPlaybackRate:(float)playbackRate方法,用以打印日志
objc
%hook IJKFFMoviePlayerControllerFFPlay
- (void)setPlaybackRate:(float)playbackRate {
playbackRate = playbackRate == 2.0 ? 4.0 : playbackRate;
%orig(playbackRate);
NSLog(@"%@:%@-%p-%s-inputPlaybackRate:%lf-changedPlaybackRate:%lf-realPlaybackRate%lf-maxPlaybackRate:%lf", nj_logPrefix, NSStringFromClass([(id)self class]), self, __FUNCTION__, playbackRate, self.playbackRate, self.realPlaybackRate, self.maxPlaybackRate);
}
%end
- 倍速面板选择
2.0,从日志中可以看到,最大播放速度(maxPlaybackRate)改成了4.0,播放速度(changedPlaybackRate)改成了4.0,👍
objc
cxzcxz:IJKFFMoviePlayerControllerFFPlay-0x136428000-_logos_method$App$IJKFFMoviePlayerControllerFFPlay$setPlaybackRate$-inputPlaybackRate:4.000000-changedPlaybackRate:4.000000-realPlaybackRate0.000000-maxPlaybackRate:4.000000
非越狱解决方案
修改Mach-O文件的汇编指令
- 从
sub_10F10449C方法的伪代码可知,sub_10F10449C方法返回的就是最大播放速度
objc
double __fastcall sub_10F10449C(__int64 a1)
{
__int64 v2; // x0
double result; // d0
double v4[6]; // [xsp+0h] [xbp-40h] BYREF
v2 = sub_10F104600();
sub_10F104B7C(v4, v2, a1);
result = 2.0;
if ( v4[0] < 50.0 )
return *(double *)&qword_117084280;
return result;
}
-
修改
sub_10F10449C方法的实现,改为类似下面的伪代码objcreturn 4.0;-
对应的汇编指令就是
objcFMOV D0, #4.0 RET
-
-
我们修改
sub_10F10449C方法的前两条汇编指令,改为下面
objc
FMOV D0, #4.0
RET
-
示例
修改第一条汇编指令
-
鼠标点击
000000010F10449Cobjc__text:000000010F10449C SUB SP, SP, #0x50 -
右键选择
Assemble
-
改成
objcFMOV D0, #4.0

- 点击
enter
-
-
全部修改结果

-
保存到
Mach-O文件中IDA->Edit->Patch program->Apply patches to input file->Apply patches


-
保存后,底部会显示
log:
objc
Patch successful: /Users/touchworld/Documents/iOSDisassembler/hook/bilibili/IDA_max_0/bili-universal

代码
BiliBiliMApp-NJPlaybackRate.xm