InShot 视频编辑模块逆向分析报告

分析对象:反编译源码 /Users/ccc/Downloads/app/out/jadx_src/sources/,包 com.camerasideas.instashot(杭州影笑科技)。代码经过 ProGuard/R8 混淆,类名多为 Cxxxx,但 JADX INFO: compiled from: 标注保留了原始类名。所有证据均为实际读到的源码。


1. VideoEditActivity 的结构、状态管理与原生引擎交互

1.1 继承体系:纯 MVP 架构(不是 MVVM)

X3.A<V,P>  (BaseMvpActivity, X3/A.java)
 ├─ 抽象方法: N3()=布局id、I3()=FragmentManager回调、L3(V)=创建Presenter
 ├─ onCreate(): setContentView(N3()) → ButterKnife绑定 → ViewModel(p318q6.v) → P p10 = L3(this) → p10.m0(getIntent(),...)
 └─ onDestroy/onPause/onResume 全部转发给 presenter (j0()/p0()/q0()/r0())

VideoEditActivity extends X3.A<I0, F6> 并实现约 10 个接口(视图契约 I0、图形项监听 HOnClickListenerTimelineSeekBar.f 等):

// VideoEditActivity.java L199
public class VideoEditActivity extends X3.A<I0, F6> implements I0,
    com.camerasideas.graphicproc.graphicsitems.H, View.OnClickListener, T, V,
    TimelineSeekBar.f, p126g4.h, p249me.a { ... }

// L997:Presenter 工厂(模板方法)
@Override public final Y5.c L3(I0 i10) { return new F6(i10); }

Presenter 链:Q(BaseVideoPresenter)C1(MultipleClipEditPresenter)F6(VideoEditPresenter),BaseVideoPresenter(Q.java) 集中持有所有"剪辑管理器":

// Q.java(BaseVideoPresenter) L57-70
public final C1807p1 f34066q;   // PipClipManager
public final C1784i  f34067r;   // AudioClipManager
public final C1786i1 f34068s;   // MediaClipManager(单例,主轨)
public final C1779g0 f34069t;   // EffectClipManager
public final D4.F    f34070u;
public final A8      f34071v;   // VideoPlayer(单例,包 EditablePlayer)

1.2 UI 组织:ButterKnife 绑定 + Fragment 双容器

onCreate(L1718)中:@BindView 绑定 mVideoView(预览)、mItemView(贴纸/文字覆盖层)、mTimelineSeekBar(可缩放时间轴)、mMiddleLayout(DragFrameLayout)、mVideoToolsMenuLayout 等;注册 FragmentManager.m 回调 k(L527)统一管理 full_screen_fragment_containerbottom_layout 两个 Fragment 容器,按 Fragment 类型切换工具栏显隐(方法 D4()/t4()/k4()/z4()):

// L548:onFragmentCreated 中按 fragment 类型做状态机
if ((fragment instanceof VideoFilterFragment2) || (fragment instanceof VideoTrackFragment)
 || (fragment instanceof VideoTimelineFragment) || (fragment instanceof StickerFragment) ...) {
    videoEditActivity.D4(false);   // 隐藏操作栏
    videoEditActivity.J4();
}

状态管理:Activity 自身状态字段极少,状态几乎全部收敛到 Presenter(((F6) this.i).xxx())与各 ClipManager 单例;另有 EventBus(@nj.k public void onEvent(C4556e0),L1919)做跨页事件。

1.3 与原生引擎的交互

Activity → Presenter → A8(VideoPlayer,单例 A8.Q() com/camerasideas/mvp/presenter/A8.java)→ EditablePlayer(com/camerasideas/instashot/player/EditablePlayer.java,isplayer 的 JNI 壳)。预览/导出共用同一引擎:导出时 p048c5/b.java 再 new 一个 EditablePlayer(2, ...) 走离屏模式(见问题 4)。


2. videoengine 包结构与 JNI 封装

2.1 原生库加载(三个加载器,分层依赖)

// player/i.java = LibLoader(预览/编辑必需)
static { System.loadLibrary("isffmpeg"); System.loadLibrary("isvideoengine"); f30920a = j.f30921a; }

// player/j.java = PlayerLibLoader(播放必需)
static { System.loadLibrary("fmod"); System.loadLibrary("isffmpeg"); System.loadLibrary("isplayer"); EditablePlayer.i(); }

// p449x2/d.java = LibLoader(工具链:解封装/转码/缩略图)
static { System.loadLibrary("isffmpeg"); System.loadLibrary("isvideoutil"); }

2.2 isplayer 的 JNI 封装:EditablePlayer(核心)

com/camerasideas/instashot/player/EditablePlayer.java,@Keep private long mNativeContext + 一整套 native_*:

private native int native_addVideoClip(int i, String str, SurfaceHolder surfaceHolder, VideoClipProperty videoClipProperty);
private native int native_addPipClip(int i, String str, SurfaceHolder surfaceHolder, VideoClipProperty videoClipProperty);
private native int native_addAudioClip(int i, String str, AudioClipProperty audioClipProperty);
private native int native_moveVideoClip(int i, int i10);           // 轨道内移动
private native int native_movePipClip(int i, int i10, int i11, long j10);
private native int native_setCompositor(ICompositor iCompositor);  // 合成回调
private native int native_setImageLoader(IImageLoader iImageLoader);
private native int native_sendCommand(int i, long j10, long j11);  // 命令通道

两个 @Keep 静态回调是原生→Java 的桥(WeakReference 防泄漏 + 主线程 Handler):

@Keep private static void postEventFromNative(Object obj, int i, final int i10, final int i11, Object obj2) {
    final EditablePlayer p = ((WeakReference) obj).get();
    if (i == 1) p.f30877d.post(() -> p.f30876c.d(i10, i11));        // 状态事件
    else if (i == 0) p.f30874a.f(obj2);                              // 帧/合成数据
}
@Keep private static String selectMediaCodec(Object obj, String str, int i, int i10) { ... }

selectMediaCodec 由原生端回调,委托 DefaultMediaCodecSelector(player/c.java,基于 MediaCodecList 打分)选择硬解解码器——即原生播放器回调 Java 选解码器。player/SurfaceHolder.javaSurfaceTexture 包装,实现 ISurfaceCreator,作为视频表面句柄传入 native。

2.3 isvideoutil 的 JNI 封装:VideoEditorUtil(静态门面)

com/android/video/util/VideoEditorUtil.java —— 解封装/转码工具全部集中在这里,返回 "key=value#..." 字符串由 Java 解析(如 iformat/duration/videocodec):

public static native int nativeConcatFiles(String[] strArr, String str);
public static native int nativeCopyAudio(String str, String str2);
public static native long nativeGetNearKeyFrame(String str, long j10);
public static native int nativeOpenVideoFile(String str, int i);
public static native int nativeGetNextFrame(Bitmap bitmap);          // 抽帧到 Bitmap
public static native byte[] nativeGenerateWaveformData(String str, long j10, long j11, int i);
public static native int nativeConvert2(String str, char c10);        // 导出(旧管线)

2.4 引擎数据模型(全部是 Gson 可序列化 DTO,@Jb.c("PI_xx") 存字段名)

混淆名 原始类 职责
videoengine/A.java ParamInfo 整个"工程文档":f31528a List<x> 视频轨、f31529b List<C2467f> 音频轨、f31546t List<C2481u> 特效、f31547u List<B> 画中画、f31549w List<X> 文字项、分辨率/码率/帧率
videoengine/x.java MediaClipInfo 主轨片段:速度、裁剪、关键帧列表 List<Q> f31693Uf31714h0 VideoKeyframeAnimator
videoengine/C2467f.java AudioClipInfo 音频片段(音量/淡入淡出/均衡器/变速)
videoengine/B.java PipClipInfo 画中画片段(继承 BorderItem)
videoengine/C2481u.java EffectClipInfo 特效片段(滤镜 Nb.e、动画 Nb.h)
videoengine/M.java TransitionInfo 转场
videoengine/C2462a.java AssetVideo 素材视频
videoengine/N.java VideoEngineGsonFactory Gson 工厂(草稿持久化)

关键设计:ParamInfo + @Jb.c 注解 = 通过 Gson 把整个编辑工程序列化存草稿、跨 Activity 传递(N0.b(context).toJson(c1856s)),这是"文档模型"式编辑器的核心。


3. FFmpeg 的 fork 用法(libisffmpeg.so)


4. 视频导出/编码流程

4.1 编码器抽象:策略模式

// encoder/c.java = IEncoder
public interface c {
    interface a { void b(ByteBuffer byteBuffer, MediaCodec.BufferInfo bufferInfo); }
    boolean b();                  // 是否异步
    void c();                     // makeCurrent(EGL)
    void d(int i, long j10);      // 喂一帧(i含EOS标志)
    void e(i iVar);               // 结果回调(接口 i = p008a5 包)
    void release();
}

三个实现:

encoder/a.java = AsyncHWEncoder(硬编,MediaCodec 异步回调) —— extends MediaCodec.Callback:

MediaCodec.createEncoderByType(cVar.f31473a);            // "video/avc"
... bitrate-mode 按能力降级:CQ(0)→VBR(1)→CBR(2)
mediaFormat.setInteger("color-format", 2130708361);      // COLOR_FormatSurface
... mediaCodec.createInputSurface() + EGL14 建 window surface
d() 喂帧:EGLExt.eglPresentationTimeANDROID(...) + EGL14.eglSwapBuffers(...)
// 看门狗:喂了 40 帧仍无输出 → SaveErrorCode.ERR_ENCODER_STOP_OUTPUT / ERR_ENCODER_NO_OUTPUT

encoder/FfmpegEncoder.java(软编/GLES 拉帧) —— nativeInit/nativeEncodeCurrentFrame/nativeRelease,输出经 @Keep onOutputBufferAvailable(ByteBuffer,int,int,long,int) 以 MediaCodec.BufferInfo 形态回吐(即原生 FFmpeg 编码器伪装成 MediaCodec 输出,复用同一套 mux 逻辑)。

encoder/b.java = GifEncoder —— GPUImageNativeLibrary.copyToBitmap 读帧 → GifSki.addFrameRgba(skigifcore)写 GIF89a。

4.2 导出管线(Mp4VideoSaver 选编)

// X4/b.java = Mp4VideoSaver, f() 中:
if (c.a(context, fVar).getBoolean("enablehwencoder", true)
 && c.a(context, fVar2).getBoolean("hw_encoder_support", true)
 && !c.a(context, fVar2).getBoolean("IsSoftwareEncoderUsed", false)) {
    this.f15470h = new com.camerasideas.instashot.encoder.a();   // 先试硬编
    if (!aVar.a(cVar)) { release(); this.f15470h = null; }
}
if (this.f15470h == null) { this.f15470h = new FfmpegEncoder(); } // 回退 FFmpeg
// 输出 .h264 裸流(com/camerasideas/instashot/util/k.java 写文件)

4.3 渲染→封装

// videoengine/MediaMuxer.java(非 android.media.MediaMuxer!)
private native int native_addTrack(MediaFormat mediaFormat);
private native int native_copyTrack(String str, boolean z10, long j10, long j11);
private native int native_writeSampleData(int i, long j10, byte[] bArr, int i10, int i11, int i12);
private native int native_setDar(int i, double d10);          // 设置 DAR(显示宽高比)

5. 时间线/多轨道数据模型

5.1 数据层(单例管理器 + 观察者)

// common/C1786i1.java = MediaClipManager(volatile 双重检查单例)
public static volatile C1786i1 i;
public static C1786i1 u(Context context) { ... synchronized ... }
public long f25935b;                 // 总时长(μs)
public int  f25936c;                 // 当前选中片段 index
public final List<C1777f1> f25940g;  // 主轨片段列表(同步锁保护)
public final AbstractC1829x0.b f25939f;  // 观察者注册表(委托列表)

构造时注册 4 类"行委托":C1815s1AudioFollowFrameX0C1800n0(ItemClipTimeProvider)——每当剪辑增删改即回调 Presenter 刷新。每个 C1777f1(MediaClip)继承 videoengine/x(MediaClipInfo),带完整属性(变速 CurveSpeedUtil、关键帧动画器 videoengine/S f31714h0、滤镜 Nb.h、旋转矩阵)。

另有 C1784i=AudioClipManager、C1807p1=PipClipManager、C1779g0=EffectClipManager,F6 中通过 m/n/o 三个 AbstractC1828x(ListObserver)把各管理器变更桥接到原生播放器(A8)。

5.2 UI 层(com/camerasideas/track/ 包,RecyclerView 多行时间轴)


6. 转场 / 特效 / 动画(libanim_engine_lib.so)

6.1 转场数据与 UI

6.2 关键帧动画(纯 Java 插值)

videoengine/S.java = VideoKeyframeAnimator(持有 16 位 float 矩阵 f31600c,按时间戳 Comparator 排序关键帧 List<Q>,配套 T=VideoKeyframeCalculator、Q=VideoKeyframe);同类还有 E=PipKeyframeAnimator、C2473l=AudioKeyframeAnimator、C2471j=AudioKeyframe、C2474m=AudioKeyframeCalculator。UI 侧 widget/KeyframeIcon.java(Activity 的 mBtnKeyframe)负责在时间轴上打关键帧。

6.3 anim_engine_lib 封装(Lottie 系动画引擎)

// org/instory/suit/LottieLibLoader.java
static { System.loadLibrary("anim_engine_lib"); mIsLibLoaded = true; }

// Tb/l.java = ISTextLayoutMeasurer(文本布局测量也走 anim_engine_lib)
public final class l<T extends n> { static { System.loadLibrary("anim_engine_lib"); ... } }

7. 音频处理(FMOD + 自动卡点 + 波形)

7.1 FMOD 的商业封装(org/fmod)

加载:player/j.java(PlayerLibLoader)System.loadLibrary("fmod")。Java 侧三件套:

// X3/h2.java = VideoEngineSdk
public static h2 a(Context context) {
    ... FMOD.init(context); Cc.c.b(context); return h2Var;
}

7.2 自动卡点(libauto_beat.so)

// com/android/autobeat/algo/AutoBeatAlgo.java(继承 com/android/algo/base/b)
private static final native float[] nativeBeat(long j10, float[] fArr);
private static final native float[] nativeBeatFile(long j10, String str, int i);
private static final native float[] nativeBeatsWithProb(long j10, float[] fArr, float f10, boolean z10);
private static final native boolean nativeIsSilent(long j10, float[] fArr, float f10, float f11);
private static final native long[] nativeInit(String str, String[] strArr, ...);
public final List<String> getLibraries() { ... "auto_beat", "YXAIEngine", "YXAImg" }

流程:以 22050 Hz 读 PCM 分块(每块 ≥44100 采样)→ nativeBeat 返回节拍(错误码 5676~5681,如 5677=模型文件缺失、5678=音频过短、5679=音量过低);外层 p065d2 包是 Kotlin 协程封装(AutoBeatLib.kt),p084e2 为状态/概率配置模型。节拍点落库为 videoengine/C2479s(AudioRhythmInfo),由 C2476o(AudioRhythmAnimator)驱动贴纸/文字"卡点跳动"。

7.3 音频可视化


8. 贴纸 / 文字 / 字幕系统

核心在 com/camerasideas/graphicproc/graphicsitems/(画布覆盖层模型,全部 Gson 序列化):

混淆名 原始类 字段前缀
V.java StickerItem SI_1..17(图片路径、Matrix、旋转/缩放、描边 OutlineProperty、GIF)
X.java TextItem TI_1..15(文本、TextAttributes、TtsInfo、CaptionInfo、AnimationProperty)
C1725d.java CaptionInfo 字幕信息
C1722a.java AnimationItem 动画贴纸
b0.java WatermarkItem 水印
K.java PipItem 画中画
C.java MosaicItem 马赛克
// graphicsitems/X.java = TextItem 关键字段
@Jb.c("TI_1")  private String f24254e0;          // 文本内容
@Jb.c("TI_11") private C1756j f24255f0;          // 文本布局
@Jb.c(alternate = {"SI_17"}, value = "TI_13") private a0 f24256g0;  // TtsInfo(语音转文字)
@Jb.c("TI_14") private C1725d f24257h0;          // CaptionInfo(字幕)
@Jb.c("TI_15") private com.camerasideas.graphics.text.p f24258i0;   // TextAttributes

关键设计模式与架构决策总结

  1. MVP + 模板方法:BaseMvpActivity 定义 L3()/N3()/I3() 骨架;Presenter 三层继承(Q 基座 → C1 多轨 → F6 具体页),View 契约接口 I0 十几个回调,Activity 只是"接线板"。
  2. 文档模型(Project-as-DTO):ParamInfo + @Jb.c 注解驱动 Gson 序列化,编辑状态天然可存草稿/跨页传递;MediaClipInfo/AudioClipInfo/PipClipInfo/EffectClipInfo/TransitionInfo 同构。
  3. 单例管理器集群:MediaClipManager、VideoPlayer(A8)、GraphicItemManager(C1730i)、VideoEngineSdk(h2)均为双重检查单例;数据变更通过委托列表(Observer)广播,再桥接给原生引擎。
  4. 原生引擎 = 单一事实源:预览与导出共用 isplayer(EditablePlayer),导出仅换 mode=2 + 挂编码器;JNI 边界用 @Keep 静态回调 + WeakReference + 主线程 Handler,原生回调 Java 选解码器(selectMediaCodec 反向依赖)。
  5. 编码器策略模式:IEncoder 三实现(AsyncHWEncoder/FfmpegEncoder/GifEncoder),Mp4VideoSaver 按远程配置(enablehwencoder/hw_encoder_support/IsSoftwareEncoderUsed)回退选择,FFmpeg 编码器伪装成 MediaCodec.BufferInfo 输出以复用封装逻辑。
  6. 自研封装器替代系统 API:videoengine/MediaMuxer(非 android.media 的)、FMOD Java 设备桥(org/fmod)、FFmpeg 全部藏在 isvideoutil 后面——商业引擎(影笑自研 is 系列 + FMOD + Lottie fork)把系统 MediaCodec/MediaMuxer 只当底层原语用。
  7. 关键帧动画 Java 化:VideoKeyframeAnimator/PipKeyframeAnimator/AudioKeyframeAnimator + Calculator,配 AnimationProperty(入场/出场),与 native 渲染松耦合。