前言Jaco 中大量用到了 pag 动画，今天我们就来盘一盘 pag 的源代码。libpag 地址在这里。目录结构大概是这样： libpag/ ├── src/ # 核心源代码 ├── include/ # 公共 API 头文件 ├── third_party/ # 第三方依赖 ├── test/ # 测试代码和资源 ├── resources/ # 测试资源文件 ├── web/ # Web 平台实现 ├── ios/ # iOS 平台项目 ├── android/ # Android 平台项目 ├── mac/ # macOS 平台项目 ├── ohos/ # OpenHarmony 平台项目 ├── linux/ # Linux 平台支持 ├── win/ # Windows 平台项目 ├── viewer/ # PAG 查看器应用 ├── exporter/ # After Effects 导出器 ├── vendor/ # 供应商特定代码 ├── tgfx/ # TGFX 图形库（符号链接） ├── assets/ # 项目资源文件 └── .github/ # GitHub 配置 具体功能：src/base/作用: 基础数据结构和动画元素定义keyframes/: 关键帧动画系统，处理各种属性的时间插值 shapes/: 矢量形状定义（椭圆、矩形、路径、星形等） text/: 文本渲染相关（字体、段落、文字样式） effects/: 视觉效果（模糊、发光、阴影等 AE 效果） layerStyles/: 图层样式（描边、填充、渐变等） utils/: 基础工具类（数学运算、矩阵变换、颜色处理）src/codec/作用: PAG 文件格式编解码tags/: PAG 文件标签解析器，每个标签对应一种数据类型 mp4/: MP4 容器格式支持，用于视频预合成 utils/: 编解码工具（数据流读写、压缩解压）src/rendering/作用: 渲染引擎核心 layers/: 各种图层类型的渲染实现 ShapeLayer: 矢量形状图层 TextLayer: 文本图层 ImageLayer: 图片图层 SolidLayer: 纯色图层 PreComposeLayer: 预合成图层 filters/: GPU 滤镜和特效处理 video/: 视频解码和播放 sequences/: 序列帧管理 graphics/: 图形绘制基础设施 caches/: 渲染缓存系统（纹理、路径、字形缓存） drawables/: 可绘制对象抽象 editing/: 运行时编辑功能 renderers/: 不同渲染器实现 utils/: 渲染工具类 src/platform/作用: 平台特定实现android/: Android NDK/JNI 绑定，硬件解码器接口 ios/: iOS 框架集成，Metal/OpenGL 支持 mac/: macOS 特定功能，视频工具箱集成 cocoa/: iOS/macOS 共享的 Objective-C++ 代码 web/: WebAssembly 绑定，Canvas/WebGL 接口 win/: Windows 平台支持，DirectX/ANGLE 集成 ohos/: OpenHarmony 平台适配 linux/: Linux 平台基础支持 qt/: Qt 框架集成（跨平台 GUI） swiftshader/: 软件渲染器支持 源代码很难跑起来，我们要想使用他的话，还是下一个针对 iOS 的 demo。地址在这里：https://github.com/libpag/pag-ios开始iOS 项目一览运行 Demo 后，我们可以看到如下页面：可以看到他的功能比较强大，我们只需要一个 pag 文件，然后代码中加载即可。参考pag 官网
前言从今天开始，我们出一个系列叫做拆包达人：通过对 IPA 包的拆解，逆向的方式来学习，废话不多说，现在开始。Jaco中东直播应用，对标 TikTok 或者抖音，其 APP 长这样：拆完 IPA 包后，我们看到目录如下：. ├── _CodeSignature │ └── CodeResources ├── ai_generating.pag ├── ai_preparing.pag ├── AliVideoCert-com_weo_projectz-20240418175247.crt ├── AppAuthCore_Privacy.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── AppsFlyerLib_Privacy.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── ar.lproj │ ├── AppLocalizable.strings │ ├── InfoPlist.strings │ ├── JacoLocalizable.strings │ ├── LaunchScreen.strings │ ├── LiveLocalizable.strings │ ├── Localizable.strings │ ├── Main.strings │ ├── MessageLocalizable.strings │ ├── RongCloudKit.strings │ └── RongSticker.strings ├── Assets.car ├── AVPlayerPage.txt ├── Base.lproj │ ├── LaunchScreen.storyboardc │ └── Main.storyboardc ├── batter_full_like.json ├── batter_like_avatar.json ├── batter_like_heart.json ├── batter_like_soccer.json ├── BeetoADBundle.bundle │ ├── Assets.car │ ├── Info.plist │ └── zh-cn.lproj ├── big_playback_lottie.json ├── big_record_lottie.json ├── BMeiSheSDK.bundle │ ├── Info.plist │ └── meishe.lic ├── box_open.json ├── box_unclick.json ├── box_win.json ├── Bubble Tea.png ├── caption.bundle │ ├── 0C1A0ABB-3ECA-46B3-95C4-07555BBA3A5F.1.captionstyle │ ├── 0C1A0ABB-3ECA-46B3-95C4-07555BBA3A5F.lic │ ├── 0D4D1A68-5CE2-46E8-8635-7672EBA35FA2.1.captionstyle │ ├── 0D4D1A68-5CE2-46E8-8635-7672EBA35FA2.lic │ ├── 1C7EE477-20E9-494F-BF48-4D37E834A725.1.captionstyle │ ├── 1C7EE477-20E9-494F-BF48-4D37E834A725.lic │ ├── 1E61C62D-1317-4A5C-8773-830A19514735.1.captionstyle │ ├── 1E61C62D-1317-4A5C-8773-830A19514735.lic │ ├── 2044EE5C-D0B6-4E76-B6D5-391F0C4AD05E.1.captionstyle │ ├── 2044EE5C-D0B6-4E76-B6D5-391F0C4AD05E.lic │ ├── 24206D2B-F06E-4AA5-B4DF-5E103DF9DAB7.1.captionstyle │ ├── 24206D2B-F06E-4AA5-B4DF-5E103DF9DAB7.lic │ ├── 2C7609BE-7563-4E79-853D-B5540F593A11.1.captionstyle │ ├── 2C7609BE-7563-4E79-853D-B5540F593A11.lic │ ├── 2FE34C0F-F984-456F-8E2F-60EB1FEFEB90.9.captionstyle │ ├── 2FE34C0F-F984-456F-8E2F-60EB1FEFEB90.lic │ ├── 315FEA0F-A05B-4396-9C8D-2D3EDA001C81.1.captionstyle │ ├── 315FEA0F-A05B-4396-9C8D-2D3EDA001C81.lic │ ├── 39B0B7E1-D914-4AA6-BFBB-53123FA6607C.1.captionstyle │ ├── 39B0B7E1-D914-4AA6-BFBB-53123FA6607C.lic │ ├── 3A23D76E-4D68-4842-AE7E-DDC1CDE2B36C.1.captionstyle │ ├── 3A23D76E-4D68-4842-AE7E-DDC1CDE2B36C.lic │ ├── 3F23B70F-05B4-4108-B001-C3FB866DF72A.1.captionstyle │ ├── 3F23B70F-05B4-4108-B001-C3FB866DF72A.lic │ ├── 43B73C4F-0652-4EA4-952A-4204F678A47E.1.captionstyle │ ├── 43B73C4F-0652-4EA4-952A-4204F678A47E.lic │ ├── 49566C3C-D1E3-46C0-BFEC-BDB785530BAF.1.captionstyle │ ├── 49566C3C-D1E3-46C0-BFEC-BDB785530BAF.lic │ ├── 4A625662-8FC9-4033-A661-FBD3D2CF1FEA.1.captionstyle │ ├── 4A625662-8FC9-4033-A661-FBD3D2CF1FEA.lic │ ├── 4C76AACA-19AC-481F-AE1E-0F32625B2EC3.4.captionstyle │ ├── 4C76AACA-19AC-481F-AE1E-0F32625B2EC3.lic │ ├── 5726F064-1EB5-4D5A-8192-8B3949C0E4EA.1.captionstyle │ ├── 5726F064-1EB5-4D5A-8192-8B3949C0E4EA.lic │ ├── 5A0EFE09-AD94-4C01-9CC8-610D5873DEAE.1.captionstyle │ ├── 5A0EFE09-AD94-4C01-9CC8-610D5873DEAE.lic │ ├── 5BDEA66F-2326-4DFD-A7C5-75C75655CCC6.1.captionstyle │ ├── 5BDEA66F-2326-4DFD-A7C5-75C75655CCC6.lic │ ├── 656B18C0-8C65-4286-93C8-1690E5EE9E88.1.captionstyle │ ├── 656B18C0-8C65-4286-93C8-1690E5EE9E88.lic │ ├── 65A85DA8-E2A6-4408-9699-05AB791AD6B4.1.captionstyle │ ├── 65A85DA8-E2A6-4408-9699-05AB791AD6B4.lic │ ├── 66CBBEB8-FB1D-4DA3-B6E2-48DECEF95A07.1.captionstyle │ ├── 66CBBEB8-FB1D-4DA3-B6E2-48DECEF95A07.lic │ ├── 690C25D3-56FA-43F5-B7DD-5230EECF81DD.2.captionstyle │ ├── 690C25D3-56FA-43F5-B7DD-5230EECF81DD.lic │ ├── 73B20460-8228-4101-BE96-BCABE60F794D.1.captionstyle │ ├── 73B20460-8228-4101-BE96-BCABE60F794D.lic │ ├── 8027447F-73A1-4A64-8B13-4876E8F1567E.1.captionstyle │ ├── 8027447F-73A1-4A64-8B13-4876E8F1567E.lic │ ├── 82EFFD41-E686-4BA4-9BF6-74F648E0E3FB.1.captionstyle │ ├── 82EFFD41-E686-4BA4-9BF6-74F648E0E3FB.lic │ ├── 867C5CE6-849C-496F-9150-90E8B3B7D0AF.1.captionstyle │ ├── 867C5CE6-849C-496F-9150-90E8B3B7D0AF.lic │ ├── 916609BB-1B9A-40AA-9C87-596998DD2D0F.1.captionstyle │ ├── 916609BB-1B9A-40AA-9C87-596998DD2D0F.lic │ ├── 93338969-F0BE-457F-9E31-CFCB95F1F937.5.captionstyle │ ├── 93338969-F0BE-457F-9E31-CFCB95F1F937.lic │ ├── 96CE3810-928D-426D-84D8-1791641D5930.1.captionstyle │ ├── 96CE3810-928D-426D-84D8-1791641D5930.lic │ ├── 9C0ABC4C-6AA7-4394-95E8-50CC2DEF7191.1.captionstyle │ ├── 9C0ABC4C-6AA7-4394-95E8-50CC2DEF7191.lic │ ├── 9D252FC7-8587-418F-9BF8-974FF8593171.4.captionstyle │ ├── 9D252FC7-8587-418F-9BF8-974FF8593171.lic │ ├── A21DA7C1-4CA8-4A60-A764-78D036CAA18E.1.captionstyle │ ├── A21DA7C1-4CA8-4A60-A764-78D036CAA18E.lic │ ├── A983E9E2-CA00-45C1-9DB0-6204EABE2361.1.captionstyle │ ├── A983E9E2-CA00-45C1-9DB0-6204EABE2361.lic │ ├── B1CE8932-092A-421A-AF34-B43C61AB08EE.1.captionstyle │ ├── B1CE8932-092A-421A-AF34-B43C61AB08EE.lic │ ├── B3967436-19CF-4521-82E5-B607FDE87F68.1.captionstyle │ ├── B3967436-19CF-4521-82E5-B607FDE87F68.lic │ ├── B9B1C935-04F3-4CA0-B512-AB1450BC462F.1.captionstyle │ ├── B9B1C935-04F3-4CA0-B512-AB1450BC462F.lic │ ├── BB2B9917-C49E-4A4E-9722-C6ED1B35489A.1.captionstyle │ ├── BB2B9917-C49E-4A4E-9722-C6ED1B35489A.lic │ ├── BF688B3C-42C2-41A6-9EDB-DFB0A8D9393D.1.captionstyle │ ├── BF688B3C-42C2-41A6-9EDB-DFB0A8D9393D.lic │ ├── C0B02545-0233-4916-AB60-8A079373E9D8.1.captionstyle │ ├── C0B02545-0233-4916-AB60-8A079373E9D8.lic │ ├── C48AA6C4-486A-4101-A269-9689EFB529E7.1.captionstyle │ ├── C48AA6C4-486A-4101-A269-9689EFB529E7.lic │ ├── caption_style1.png │ ├── caption_style2.png │ ├── caption_style3.png │ ├── caption_style4.png │ ├── caption_style5.png │ ├── caption.json │ ├── D085861B-0A01-4D2B-A061-4DF4D8BBDDB2.2.captionstyle │ ├── D085861B-0A01-4D2B-A061-4DF4D8BBDDB2.lic │ ├── D3B7527A-811B-4098-AFBE-208F034C394D.1.captionstyle │ ├── D3B7527A-811B-4098-AFBE-208F034C394D.lic │ ├── D929CC52-C463-4042-89BD-F7686CD9851A.1.captionstyle │ ├── D929CC52-C463-4042-89BD-F7686CD9851A.lic │ ├── E06AC0E0-8C8E-4A81-A30C-C65839CEED91.1.captionstyle │ ├── E06AC0E0-8C8E-4A81-A30C-C65839CEED91.lic │ ├── EA8CB895-9EFC-48AB-9CFC-035A93CBF0FD.2.captionstyle │ ├── EA8CB895-9EFC-48AB-9CFC-035A93CBF0FD.lic │ ├── EADEB54E-411C-46AC-A88B-547D80DDDDBF.1.captionstyle │ ├── EADEB54E-411C-46AC-A88B-547D80DDDDBF.lic │ ├── EC2BBA40-981B-450F-A582-104187FAEC17.2.captionstyle │ └── EC2BBA40-981B-450F-A582-104187FAEC17.lic ├── capture_tip.gif ├── category.json ├── ComposeMakeup.bundle │ └── ComposeMakeup ├── cross_room.json ├── dark.json ├── Deep.png ├── DGCharts.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── dm_livecard_live.json ├── dm_voice_delete.json ├── dm_voice_lock_img.png ├── dm_voice_lock.json ├── doubleLike.json ├── doubleLikeSoccer.json ├── Emoji.plist ├── EmojiList.plist ├── en.lproj │ ├── AppLocalizable.strings │ ├── InfoPlist.strings │ ├── JacoLocalizable.strings │ ├── LaunchScreen.strings │ ├── LiveLocalizable.strings │ ├── Localizable.strings │ ├── Main.strings │ ├── MessageLocalizable.strings │ ├── RongCloudKit.strings │ └── RongSticker.strings ├── engaging.json ├── enter_live_arrow.pag ├── FBLPromises_Privacy.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── feed_voice_wave.json ├── filter.bundle │ ├── filter.json │ ├── mopian_6945244021867921415 │ ├── qingkong_6945244054134538270 │ ├── tongnian_6945244030104649736 │ └── Warmfood_7165158837891104769 ├── filters.json ├── FMDB_Privacy.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── Frameworks │ ├── AgoraAiEchoCancellationExtension.framework │ ├── AgoraAiNoiseSuppressionExtension.framework │ ├── Agorafdkaac.framework │ ├── Agoraffmpeg.framework │ ├── AgoraReplayKitExtension.framework │ ├── AgoraRtcKit.framework │ ├── AgoraSoundTouch.framework │ ├── AgoraVideoDecoderExtension.framework │ ├── AgoraVideoEncoderExtension.framework │ ├── alivcffmpeg.framework │ ├── AliyunMediaDownloader.framework │ ├── AliyunPlayer.framework │ ├── aosl.framework │ ├── IQKeyboardManagerSwift.framework │ ├── libpag.framework │ ├── NveEffectKit.framework │ ├── NvStreamingSdkCore.framework │ ├── RongChatRoom.framework │ ├── RongCustomerService.framework │ ├── RongDiscussion.framework │ ├── RongIMKit.framework │ ├── RongIMLib.framework │ ├── RongIMLibCore.framework │ ├── RongPublicService.framework │ ├── RongSight.framework │ ├── SCSDKCameraKit.framework │ ├── SCSDKCoreKit.framework │ ├── SCSDKCreativeKit.framework │ ├── Sentry.framework │ ├── tsr_client.framework │ ├── video_dec.framework │ ├── video_enc.framework │ └── ZegoExpressEngine.framework ├── GCDWebUploader.bundle │ └── Contents ├── gift_guidance_border.pag ├── gift_sound.mp3 ├── gold_coins_animation.json ├── GoogleCast.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── GoogleCastCoreResources.bundle │ ├── af.lproj │ ├── ar.lproj │ ├── bg.lproj │ ├── bn.lproj │ ├── ca.lproj │ ├── CastFrameworkDB.momd │ ├── cs.lproj │ ├── da.lproj │ ├── de.lproj │ ├── el.lproj │ ├── en_GB.lproj │ ├── en.lproj │ ├── es_419.lproj │ ├── es.lproj │ ├── et.lproj │ ├── fa.lproj │ ├── fi.lproj │ ├── fil.lproj │ ├── fr.lproj │ ├── hi.lproj │ ├── hr.lproj │ ├── hu.lproj │ ├── hy.lproj │ ├── Icons │ ├── id.lproj │ ├── is.lproj │ ├── it.lproj │ ├── iw.lproj │ ├── ja.lproj │ ├── ka.lproj │ ├── km.lproj │ ├── ko.lproj │ ├── lo.lproj │ ├── lt.lproj │ ├── lv.lproj │ ├── mk.lproj │ ├── mn.lproj │ ├── mr.lproj │ ├── ms.lproj │ ├── my.lproj │ ├── ne.lproj │ ├── nl.lproj │ ├── no.lproj │ ├── pl.lproj │ ├── pt_PT.lproj │ ├── pt.lproj │ ├── ro.lproj │ ├── ru.lproj │ ├── si.lproj │ ├── sk.lproj │ ├── sl.lproj │ ├── sr.lproj │ ├── sv.lproj │ ├── sw.lproj │ ├── ta.lproj │ ├── th.lproj │ ├── tr.lproj │ ├── uk.lproj │ ├── vi.lproj │ ├── zh_CN.lproj │ ├── zh_TW.lproj │ └── zh.lproj ├── GoogleCastUIResources.bundle │ ├── af.lproj │ ├── ar.lproj │ ├── Base.lproj │ ├── bg.lproj │ ├── bn.lproj │ ├── ca.lproj │ ├── cs.lproj │ ├── da.lproj │ ├── de.lproj │ ├── el.lproj │ ├── en_GB.lproj │ ├── en.lproj │ ├── es_419.lproj │ ├── es.lproj │ ├── et.lproj │ ├── fa.lproj │ ├── fi.lproj │ ├── fil.lproj │ ├── fr.lproj │ ├── hi.lproj │ ├── hr.lproj │ ├── hu.lproj │ ├── hy.lproj │ ├── Icons │ ├── id.lproj │ ├── is.lproj │ ├── it.lproj │ ├── iw.lproj │ ├── ja.lproj │ ├── ka.lproj │ ├── km.lproj │ ├── ko.lproj │ ├── lo.lproj │ ├── lt.lproj │ ├── lv.lproj │ ├── mk.lproj │ ├── mn.lproj │ ├── mr.lproj │ ├── ms.lproj │ ├── my.lproj │ ├── ne.lproj │ ├── nl.lproj │ ├── no.lproj │ ├── pl.lproj │ ├── pt_PT.lproj │ ├── pt.lproj │ ├── ro.lproj │ ├── ru.lproj │ ├── si.lproj │ ├── sk.lproj │ ├── sl.lproj │ ├── sr.lproj │ ├── sv.lproj │ ├── sw.lproj │ ├── ta.lproj │ ├── th.lproj │ ├── tr.lproj │ ├── uk.lproj │ ├── vi.lproj │ ├── zh_CN.lproj │ ├── zh_TW.lproj │ └── zh.lproj ├── GoogleService-Info.plist ├── GoogleSignIn.bundle │ ├── ar.lproj │ ├── ca.lproj │ ├── cs.lproj │ ├── da.lproj │ ├── de.lproj │ ├── el.lproj │ ├── en_GB.lproj │ ├── en.lproj │ ├── es_MX.lproj │ ├── es.lproj │ ├── fi.lproj │ ├── fr_CA.lproj │ ├── fr.lproj │ ├── google.png │ ├── google@2x.png │ ├── google@3x.png │ ├── he.lproj │ ├── hi.lproj │ ├── hr.lproj │ ├── hu.lproj │ ├── id.lproj │ ├── Info.plist │ ├── it.lproj │ ├── ja.lproj │ ├── ko.lproj │ ├── ms.lproj │ ├── nb.lproj │ ├── nl.lproj │ ├── pl.lproj │ ├── PrivacyInfo.xcprivacy │ ├── pt_BR.lproj │ ├── pt_PT.lproj │ ├── pt.lproj │ ├── ro.lproj │ ├── Roboto-Bold.ttf │ ├── ru.lproj │ ├── sk.lproj │ ├── sv.lproj │ ├── th.lproj │ ├── tr.lproj │ ├── uk.lproj │ ├── vi.lproj │ ├── zh_CN.lproj │ └── zh_TW.lproj ├── GoogleUtilities_Privacy.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── GTMAppAuth_Privacy.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── GTMSessionFetcher_Core_Privacy.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── home_interest_selection_guide.pag ├── icon_logo_new.png ├── Info.plist ├── InfoDebug.plist ├── JACO ├── Jaco Style.png ├── jaco_3_0_home_heart.pag ├── jaco_3_0_home_viewers.pag ├── jaco_3.0_support_coin.pag ├── jaco_3.0_support_flower.pag ├── jaco_profile_guide_side.pag ├── jaco_profile_guide_swip.pag ├── JacoCategoryJson.json ├── JacoIcon60x60@2x.png ├── JacoIcon76x76@2x~ipad.png ├── JXCategoryView.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── JXPagingView.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── Kingfisher.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── ko_lightning.json ├── ko_red_warn.json ├── ko_sector_countdown.json ├── LicenseBag.bundle │ └── huoshan.licbag ├── light.json ├── live_bottom_barItem_newGift.pag ├── live_combo_send_x100.pag ├── live_combo_send_x15.pag ├── live_combo_send_x30.pag ├── live_combo_send_x50.pag ├── live_face.json ├── live_gift_combo_level1.pag ├── live_gift_combo_level2.pag ├── live_gift_combo_level3.pag ├── live_gift_combo_level4.pag ├── live_gift_combo_level5.pag ├── live_gift_combo_level6.pag ├── live_gift_track_burst.pag ├── live_gift_track_donotstop_ar.pag ├── live_gift_track_donotstop.pag ├── live_gift_track_fire_ar.pag ├── live_gift_track_fire.pag ├── live_gift_track_flash.pag ├── live_gift_track_good_ar.pag ├── live_gift_track_good.pag ├── live_gift_track_greate_ar.pag ├── live_gift_track_greate.pag ├── live_gift_track_legend_ar.pag ├── live_gift_track_legend.pag ├── live_im_zoom_in.json ├── live_im_zoom_out.json ├── live_ko_end.json ├── live_link_voice_wave_blue.json ├── live_link_voice_wave_orange.json ├── live_link_voice_wave_red.json ├── live_link_voice_wave.json ├── live_multi_ink.json ├── live_pk_add_arrow.json ├── live_pk_likeOnly_ar.json ├── live_pk_likeOnly_en.json ├── live_pk_progress_catch_up_ko.pag ├── live_pk_progress_catch_up.pag ├── live_pk_progress_emoji_lose.pag ├── live_pk_progress_emoji_win.pag ├── live_pk_progress_normal.pag ├── live_pk_singleGift_ar.json ├── live_pk_singleGift_en.json ├── live_popup_draw.json ├── live_popup_lose.json ├── live_popup_start.json ├── live_popup_win.json ├── live_rom_box_jump.json ├── live_room_box_enter.json ├── live_room_lucky_box_enter.json ├── live_room_lucky_box_rock.json ├── live_room_swip.json ├── live_sei_carrier.jpg ├── live_share_screen.mp4 ├── live_single_link.json ├── live_single_voice_or_live.json ├── liveroom_casting.pag ├── liveroom_cion_jump.json ├── liveroom_heart_jump.json ├── livevideo_scatter_flowers.json ├── living_bmp.pag ├── loading.json ├── logo_top_image_web1.webp ├── LottiePrivacyInfo.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── lv_goal_progress_35.json ├── lv_goal_progress_50.json ├── lv_goal_progress_65.json ├── lv_goal_progress_80.json ├── lv_goal_progress_99.json ├── MaterialDialogs.bundle │ └── Resources ├── MJRefresh.bundle │ ├── arrow@2x.png │ ├── en.lproj │ ├── ko.lproj │ ├── ru.lproj │ ├── trail_arrow@2x.png │ ├── uk.lproj │ ├── zh-Hans.lproj │ └── zh-Hant.lproj ├── MJRefresh.Privacy.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── ModelResource.bundle │ ├── facefitting │ ├── hairparser │ ├── handmodel │ ├── idream │ ├── mattingmodel │ ├── skeleton_model │ ├── skin_seg │ ├── ttfaceattrmodel │ └── ttfacemodel ├── mopian_6945244021867921415.png ├── Peach Pinky.png ├── PhoneNumberKitPrivacy.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── PhoneNumberMetadata.json ├── pk_end_tips.pag ├── pk_props.mp4 ├── PkgInfo ├── PlugIns │ ├── BroadcastUpload.appex │ ├── BroadcastUploadSetupUI.appex │ └── NotificationService.appex ├── polish_data.json ├── PrivacyInfo.xcprivacy ├── profile_living.json ├── Protobuf_Privacy.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── qingkong_6945244054134538270.png ├── Quality.png ├── RangersAppLog.bundle │ ├── h5bridge-wkwebview.txt │ └── Info.plist ├── RCColor.plist ├── RCConfig.plist ├── README.md ├── RecaptchaEnterpriseSDKResources.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── received_rank_badge.json ├── recharge_discount_bg.json ├── reference_loading.pag ├── refresh_yellow.json ├── RongCloud.bundle │ ├── add@2x.png │ ├── add@3x.png │ ├── arrow@2x.png │ ├── arrow@3x.png │ ├── audio_press_short@2x.png │ ├── audio_press_short@3x.png │ ├── block_notification@2x.png │ ├── block_notification@3x.png │ ├── blue.png │ ├── blue－hover.png │ ├── broken@2x.png │ ├── bubble.png │ ├── burn_video_picture@2x.png │ ├── burn_video_picture@3x.png │ ├── burnPicture@2x.png │ ├── burnPicture@3x.png │ ├── cancelButton@2x.png │ ├── chat_from_bg_normal_dark@2x.png │ ├── chat_from_bg_normal@2x.png │ ├── chat_to_bg_normal_dark@2x.png │ ├── chat_to_bg_normal@2x.png │ ├── chat_to_bg_white_dark@2x.png │ ├── chat_to_bg_white@2x.png │ ├── check.png │ ├── close_burn@2x.png │ ├── close_burn@3x.png │ ├── close.png │ ├── combine_failed@2x.png │ ├── combine_failed@3x.png │ ├── combine_loading@2x.png │ ├── combine_loading@3x.png │ ├── Comment.png │ ├── cs_eva_complete_hover@2x.png │ ├── cs_eva_complete@2x.png │ ├── cs_no@2x.png │ ├── cs_yes_hover@2x.png │ ├── cs_yes@2x.png │ ├── custom_service_evaluation_no_hover.png │ ├── custom_service_evaluation_no.png │ ├── custom_service_evaluation_star_hover.png │ ├── custom_service_evaluation_star.png │ ├── custom_service_evaluation_yes_hover.png │ ├── custom_service_evaluation_yes.png │ ├── custom_service_switch_to_admin_dark@2x.png │ ├── custom_service_switch_to_admin_dark@3x.png │ ├── custom_service_switch_to_admin@2x.png │ ├── custom_service_switch_to_admin@3x.png │ ├── default_collection_portrait@2x.png │ ├── default_collection_portrait@3x.png │ ├── default_discussion_collection_portrait@2x.png │ ├── default_discussion_collection_portrait@3x.png │ ├── default_discussion_portrait@2x.png │ ├── default_discussion_portrait@3x.png │ ├── default_group_portrait@2x.png │ ├── default_group_portrait@3x.png │ ├── default_portrait_msg@2x.png │ ├── default_portrait_msg@3x.png │ ├── default_portrait@2x.png │ ├── default_portrait@3x.png │ ├── delete_message_dark@2x.png │ ├── delete_message_dark@3x.png │ ├── delete_message@2x.png │ ├── delete_message@3x.png │ ├── emoji_btn_delete@2x.png │ ├── emoji_btn_normal.png │ ├── ExcelFile@2x.png │ ├── ExcelFile@3x.png │ ├── exclamation_dark@2x.png │ ├── exclamation@2x.png │ ├── fileicon_video_wall@2x.png │ ├── fileicon_video_wall@3x.png │ ├── files_hover.png │ ├── fire_identify_dark@2x.png │ ├── fire_identify_dark@3x.png │ ├── fire_identify@2x.png │ ├── fire_identify@3x.png │ ├── folder.png │ ├── follow-hover.png │ ├── follow.png │ ├── forward_message_dark@2x.png │ ├── forward_message_dark@3x.png │ ├── forward_message@2x.png │ ├── forward_message@3x.png │ ├── forwardIcon_dark@2x.png │ ├── forwardIcon@2x.png │ ├── from_burn_picture_dark@2x.png │ ├── from_burn_picture_dark@3x.png │ ├── from_burn_picture@2x .png │ ├── from_burn_picture@3x.png │ ├── from_burn_video_picture_dark@2x.png │ ├── from_burn_video_picture_dark@3x.png │ ├── from_burn_video_picture@2x.png │ ├── from_burn_video_picture@3x.png │ ├── from_thumb_image_broken_dark@2x.png │ ├── from_thumb_image_broken_dark@3x.png │ ├── from_thumb_image_broken@2x.png │ ├── from_thumb_image_broken@3x.png │ ├── from_voice_1_dark@2x.png │ ├── from_voice_1_dark@3x.png │ ├── from_voice_1@2x.png │ ├── from_voice_1@3x.png │ ├── from_voice_2_dark@2x.png │ ├── from_voice_2_dark@3x.png │ ├── from_voice_2@2x.png │ ├── from_voice_2@3x.png │ ├── from_voice_3_dark@2x.png │ ├── from_voice_3_dark@3x.png │ ├── from_voice_3@2x.png │ ├── from_voice_3@3x.png │ ├── gif_loadfailed@2x.png │ ├── gif_loadfailed@3x.png │ ├── gif_loading@2x.png │ ├── gif_loading@3x.png │ ├── gif_loadprogress@2x.png │ ├── gif_loadprogress@3x.png │ ├── gif_needload@2x.png │ ├── gif_needload@3x.png │ ├── HQVoiceMsg_failed_tip@2x.png │ ├── HQVoiceMsg_failed_tip@3x.png │ ├── icon_sight_close@2x.png │ ├── inputbar_add_dark@2x.png │ ├── inputbar_add_dark@3x.png │ ├── inputbar_add@2x.png │ ├── inputbar_add@3x.png │ ├── inputbar_emoji_dark@2x.png │ ├── inputbar_emoji_dark@3x.png │ ├── inputbar_emoji@2x.png │ ├── inputbar_emoji@3x.png │ ├── inputbar_keyboard_dark@2x.png │ ├── inputbar_keyboard_dark@3x.png │ ├── inputbar_keyboard@2x.png │ ├── inputbar_keyboard@3x.png │ ├── inputbar_voice_dark@2x.png │ ├── inputbar_voice_dark@3x.png │ ├── inputbar_voice@2x.png │ ├── inputbar_voice@3x.png │ ├── keyboard_burn@2x.png │ ├── keyboard_burn@3x.png │ ├── Keynote@2x.png │ ├── Keynote@3x.png │ ├── map_annotation@2x.png │ ├── map_annotation@3x.png │ ├── message_cell_destruct@2x.png │ ├── message_cell_destruct@3x.png │ ├── message_cell_select_dark@2x.png │ ├── message_cell_select_dark@3x.png │ ├── message_cell_select@2x.png │ ├── message_cell_select@3x.png │ ├── message_cell_unselect_dark@2x.png │ ├── message_cell_unselect_dark@3x.png │ ├── message_cell_unselect@2x.png │ ├── message_cell_unselect@3x.png │ ├── message_fail_dark@2x.png │ ├── message_fail_dark@3x.png │ ├── message_fail@2x.png │ ├── message_fail@3x.png │ ├── message_read_status@2x.png │ ├── message_read_status@3x.png │ ├── Mp3File@2x.png │ ├── Mp3File@3x.png │ ├── navigator_btn_back_dark@2x.png │ ├── navigator_btn_back_dark@3x.png │ ├── navigator_btn_back@2x.png │ ├── navigator_btn_back@3x.png │ ├── navigator_white_back@2x.png │ ├── network_fail_dark@2x.png │ ├── network_fail_dark@3x.png │ ├── network_fail@2x.png │ ├── network_fail@3x.png │ ├── no_message_img_dark@2x.png │ ├── no_message_img@2x.png │ ├── noSolve-hover.png │ ├── noSolve.png │ ├── Numbers@2x.png │ ├── Numbers@3x.png │ ├── OtherFile@2x.png │ ├── OtherFile@3x.png │ ├── Pages@2x.png │ ├── Pages@3x.png │ ├── PdfFile@2x.png │ ├── PdfFile@3x.png │ ├── photo_burn@2x.png │ ├── photo_burn@3x.png │ ├── photo_preview_selected.png │ ├── photo_preview_unselected.png │ ├── photopicker_state_normal.png │ ├── photopicker_state_selected.png │ ├── PictureFile@2x.png │ ├── PictureFile@3x.png │ ├── play_btn_normal@2x.png │ ├── play_btn_normal@3x.png │ ├── play_video@2x.png │ ├── player_shadow_bottom@2x.png │ ├── player_shadow_top@2x.png │ ├── player_slider_pan@2x.png │ ├── player_start_button@2x.png │ ├── player_suspend_button@2x.png │ ├── plugin_item_burn_dark@2x.png │ ├── plugin_item_burn_dark@3x.png │ ├── plugin_item_burn_highlighted_dark@2x.png │ ├── plugin_item_burn_highlighted_dark@3x.png │ ├── plugin_item_burn_highlighted@2x.png │ ├── plugin_item_burn_highlighted@3x.png │ ├── plugin_item_burn@2x.png │ ├── plugin_item_burn@3x.png │ ├── plugin_item_camera_dark@2x.png │ ├── plugin_item_camera_dark@3x.png │ ├── plugin_item_camera_highlighted_dark@2x.png │ ├── plugin_item_camera_highlighted_dark@3x.png │ ├── plugin_item_camera_highlighted@2x.png │ ├── plugin_item_camera_highlighted@3x.png │ ├── plugin_item_camera@2x.png │ ├── plugin_item_camera@3x.png │ ├── plugin_item_card_dark@2x.png │ ├── plugin_item_card_dark@3x.png │ ├── plugin_item_card_highlighted_dark@2x.png │ ├── plugin_item_card_highlighted_dark@3x.png │ ├── plugin_item_card_highlighted@2x.png │ ├── plugin_item_card_highlighted@3x.png │ ├── plugin_item_card@2x.png │ ├── plugin_item_card@3x.png │ ├── plugin_item_file_dark@2x.png │ ├── plugin_item_file_dark@3x.png │ ├── plugin_item_file_highlighted_dark@2x.png │ ├── plugin_item_file_highlighted_dark@3x.png │ ├── plugin_item_file_highlighted@2x.png │ ├── plugin_item_file_highlighted@3x.png │ ├── plugin_item_file@2x.png │ ├── plugin_item_file@3x.png │ ├── plugin_item_location_dark@2x.png │ ├── plugin_item_location_dark@3x.png │ ├── plugin_item_location_highlighted_dark@2x.png │ ├── plugin_item_location_highlighted_dark@3x.png │ ├── plugin_item_location_highlighted@2x.png │ ├── plugin_item_location_highlighted@3x.png │ ├── plugin_item_location@2x.png │ ├── plugin_item_location@3x.png │ ├── plugin_item_picture_dark@2x.png │ ├── plugin_item_picture_dark@3x.png │ ├── plugin_item_picture_highlighted_dark@2x.png │ ├── plugin_item_picture_highlighted_dark@3x.png │ ├── plugin_item_picture_highlighted@2x.png │ ├── plugin_item_picture_highlighted@3x.png │ ├── plugin_item_picture@2x.png │ ├── plugin_item_picture@3x.png │ ├── portrait_kefu@2x.png │ ├── pptFile@2x.png │ ├── pptFile@3x.png │ ├── pub_menu_dark@2x.png │ ├── pub_menu_dark@3x.png │ ├── pub_menu@2x.png │ ├── pub_menu@3x.png │ ├── public_serive_menu_icon@2x.png │ ├── public_serive_menu_icon@3x.png │ ├── public_service_submenu_bg@2x.png │ ├── public_service_submenu_bg@3x.png │ ├── rc_conversation_list_msg_sending@2x.png │ ├── rc_conversation_list_msg_sending@3x.png │ ├── rc_richcontentmsg_placeholder@2x.png │ ├── rc_setting@2x.png │ ├── rc_setting@3x.png │ ├── receipt_hover.png │ ├── receipt.png │ ├── referencing_view_dismiss_icon@2x.png │ ├── referencing_view_dismiss_icon@3x.png │ ├── Resolved-hover.png │ ├── Resolved.png │ ├── return@2x.png │ ├── return@3x.png │ ├── right_arrow@2x.png │ ├── right_arrow@3x.png │ ├── selected_full.png │ ├── sendMsg_failed_tip_dark@2x.png │ ├── sendMsg_failed_tip_dark@3x.png │ ├── sendMsg_failed_tip@2x.png │ ├── sendMsg_failed_tip@3x.png │ ├── sight_camera_switch@2x.png │ ├── sight_download_failed@2x.png │ ├── sight_download_failed@3x.png │ ├── sight_file_icon@2x.png │ ├── sight_file_icon@3x.png │ ├── sight_label_shadow@2x.png │ ├── sight_list_button@2x.png │ ├── sight_message_icon@2x.png │ ├── sight_message_icon@3x.png │ ├── sight_pause_btn@2x.png │ ├── sight_pause_btn@3x.png │ ├── sight_play_btn@2x.png │ ├── sight_play_btn@3x.png │ ├── sight_preview_cancel@2x.png │ ├── sight_preview_cancel@3x.png │ ├── sight_preview_done@2x.png │ ├── sight_preview_done@3x.png │ ├── sight_top_toolbar_close@2x.png │ ├── sight_top_toolbar_close@3x.png │ ├── sms-received.caf │ ├── template.json │ ├── text_burn_img@2x.png │ ├── text_burn_img@3x.png │ ├── TextFile@2x.png │ ├── TextFile@3x.png │ ├── to_thumb_image_broken_dark@2x.png │ ├── to_thumb_image_broken_dark@3x.png │ ├── to_thumb_image_broken@2x.png │ ├── to_thumb_image_broken@3x.png │ ├── to_voice_1_dark@2x.png │ ├── to_voice_1_dark@3x.png │ ├── to_voice_1@2x.png │ ├── to_voice_1@3x.png │ ├── to_voice_2_dark@2x.png │ ├── to_voice_2_dark@3x.png │ ├── to_voice_2@2x.png │ ├── to_voice_2@3x.png │ ├── to_voice_3_dark@2x.png │ ├── to_voice_3_dark@3x.png │ ├── to_voice_3@2x.png │ ├── to_voice_3@3x.png │ ├── translation_from_bg_normal_dark@2x.png │ ├── translation_from_bg_normal@2x.png │ ├── unselected_full.png │ ├── up_dark@2x.png │ ├── up_dark@3x.png │ ├── up@2x.png │ ├── up@3x.png │ ├── video_icon_play@2x.png │ ├── VideoFile@2x.png │ ├── VideoFile@3x.png │ ├── voice_1.png │ ├── voice_2.png │ ├── voice_3.png │ ├── voice_4.png │ ├── voice_5.png │ ├── voice_6.png │ ├── voice_7.png │ ├── voice_8.png │ ├── voice_burn@2x.png │ ├── voice_burn@3x.png │ ├── voice_unread@2x.png │ ├── voice_unread@3x.png │ ├── voice_volume0@2x.png │ ├── voip │ ├── WordFile@2x.png │ └── WordFile@3x.png ├── sayhi.json ├── SC_Info │ ├── JACO.sinf │ ├── JACO.supf │ ├── JACO.supp │ ├── JACO.supx │ └── Manifest.plist ├── selective.json ├── share_screen_audio_loading.json ├── shareView_casting.pag ├── shime.json ├── shortVideoPauseRecord.json ├── shortVideoRecording.json ├── shortVideoStartRecord.json ├── Silence.wav ├── small_playback_lottie.json ├── small_record_lottie.json ├── splash_loading_pag_bmp.pag ├── splash_toHome_pag.pag ├── StickerResource.bundle │ └── stickers ├── supporter_rank_badge.json ├── SwiftProtobuf.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── textPacketColor.json ├── textTF.bundle │ ├── Almas.png │ ├── Almas.ttf │ ├── ArefRuqaa.png │ ├── ArefRuqaa.ttf │ ├── CallOfOpsDuty.otf │ ├── CallOfOpsDuty.png │ ├── Classic.png │ ├── Classic.ttf │ ├── Dortmund.otf │ ├── Dortmund.png │ ├── Kayana.otf │ ├── Kayana.png │ ├── Kufam.png │ ├── Kufam.ttf │ ├── LamaSans-ExtraBold.otf │ ├── LamaSans.otf │ ├── LamaSans.png │ ├── Mailand.otf │ ├── Mailand.png │ ├── NotoNastaliqUrdu.png │ ├── NotoNastaliqUrdu.ttf │ ├── RedditSans.png │ ├── RedditSans.ttf │ ├── Sarmady.otf │ ├── Sarmady.png │ ├── Scheherazade.png │ ├── Scheherazade.ttf │ ├── SFArabic.ttf │ ├── SpicyWasabi.png │ ├── SpicyWasabi.ttf │ ├── SuperSedan.png │ ├── SuperSedan.ttf │ ├── textTF.json │ ├── Thestral.otf │ ├── Thestral.png │ ├── Vazirmatn.png │ └── Vazirmatn.ttf ├── thin ├── three_dot.json ├── Toast-Swift.bundle │ ├── Info.plist │ └── PrivacyInfo.xcprivacy ├── tongnian_6945244030104649736.png ├── tree.txt ├── TuringShield.bundle │ └── PrivacyInfo.xcprivacy ├── version.info ├── video_double_like.json ├── video_edit_watermark.png ├── video_favorite.json ├── video_follow.json ├── video_live_now.json ├── video_music_play.json ├── video_survey_great.pag ├── video_survey_not_bad.pag ├── video_survey_poor.pag ├── Warm Man.png ├── Warm.png ├── Warmfood_7165158837891104769.png ├── Water Mist.png └── ZLPhotoBrowser.bundle ├── ar.lproj ├── de.lproj ├── en.lproj ├── es-419.lproj ├── fr.lproj ├── id.lproj ├── it.lproj ├── ja-US.lproj ├── ko.lproj ├── ms.lproj ├── pt-BR.lproj ├── ru.lproj ├── tr.lproj ├── vi.lproj ├── zh-Hans.lproj ├── zh-Hant.lproj ├── zl_addPhoto@2x.png ├── zl_addPhoto@3x.png ├── zl_adjust_selected@2x.png ├── zl_adjust_selected@3x.png ├── zl_adjust@2x.png ├── zl_adjust@3x.png ├── zl_albumSelect@2x.png ├── zl_albumSelect@3x.png ├── zl_arrow_down_dark@3x.png ├── zl_arrow_down_light@3x.png ├── zl_arrow_down@2x.png ├── zl_arrow_down@3x.png ├── zl_ashbin_open@2x.png ├── zl_ashbin_open@3x.png ├── zl_ashbin@2x.png ├── zl_ashbin@3x.png ├── zl_brightness_selected@2x.png ├── zl_brightness_selected@3x.png ├── zl_brightness@2x.png ├── zl_brightness@3x.png ├── zl_btn_circle@2x.png ├── zl_btn_circle@3x.png ├── zl_btn_original_circle@2x.png ├── zl_btn_original_circle@3x.png ├── zl_btn_original_selected@2x.png ├── zl_btn_original_selected@3x.png ├── zl_btn_selected_icon@2x.png ├── zl_btn_selected_icon@3x.png ├── zl_btn_selected@2x.png ├── zl_btn_selected@3x.png ├── zl_btn_unselected@2x.png ├── zl_btn_unselected@3x.png ├── zl_camera_close@2x.png ├── zl_camera_close@3x.png ├── zl_clip@2x.png ├── zl_clip@3x.png ├── zl_close@2x.png ├── zl_close@3x.png ├── zl_contrast_selected@2x.png ├── zl_contrast_selected@3x.png ├── zl_contrast@2x.png ├── zl_contrast@3x.png ├── zl_defaultphoto.png ├── zl_download_close@3x.png ├── zl_drawLine_selected@2x.png ├── zl_drawLine_selected@3x.png ├── zl_drawLine@2x.png ├── zl_drawLine@3x.png ├── zl_editImage_tag@2x.png ├── zl_editImage_tag@3x.png ├── zl_filter_selected@2x.png ├── zl_filter_selected@3x.png ├── zl_filter@2x.png ├── zl_filter@3x.png ├── zl_flash_off@2x.png ├── zl_flash_off@3x.png ├── zl_flash_on@2x.png ├── zl_flash_on@3x.png ├── zl_focus@2x.png ├── zl_focus@3x.png ├── zl_ic_left@2x.png ├── zl_ic_left@3x.png ├── zl_ic_right@2x.png ├── zl_ic_right@3x.png ├── zl_icon_icloud@2x.png ├── zl_icon_icloud@3x.png ├── zl_imageSticker@2x.png ├── zl_imageSticker@3x.png ├── zl_livePhoto@2x.png ├── zl_livePhoto@3x.png ├── zl_loading_dark@2x.png ├── zl_loading_dark@3x.png ├── zl_loading_light@2x.png ├── zl_loading_light@3x.png ├── zl_mix_delete@2x.png ├── zl_mix_delete@3x.png ├── zl_mosaic_selected@2x.png ├── zl_mosaic_selected@3x.png ├── zl_mosaic@2x.png ├── zl_mosaic@3x.png ├── zl_mute_off.png ├── zl_mute_on.png ├── zl_navBack@2x.png ├── zl_navBack@3x.png ├── zl_navClose_dark@3x.png ├── zl_navClose@3x.png ├── zl_pauseButtonWhite@2x.png ├── zl_pauseButtonWhite@3x.png ├── zl_play_icon.png ├── zl_playButtonWhite@2x.png ├── zl_playButtonWhite@3x.png ├── zl_playVideo@2x.png ├── zl_playVideo@3x.png ├── zl_preview_nav_bg@3x.png ├── zl_redo_disable@2x.png ├── zl_redo_disable@3x.png ├── zl_redo@2x.png ├── zl_redo@3x.png ├── zl_retake@2x.png ├── zl_retake@3x.png ├── zl_revoke_disable@2x.png ├── zl_revoke_disable@3x.png ├── zl_revoke@2x.png ├── zl_revoke@3x.png ├── zl_right_arrow@2x.png ├── zl_right_arrow@3x.png ├── zl_right@2x.png ├── zl_right@3x.png ├── zl_rotateimage@2x.png ├── zl_rotateimage@3x.png ├── zl_saturation_selected@2x.png ├── zl_saturation_selected@3x.png ├── zl_saturation@2x.png ├── zl_saturation@3x.png ├── zl_shadow@3x.png ├── zl_takePhoto@2x.png ├── zl_takePhoto@3x.png ├── zl_textSticker@2x.png ├── zl_textSticker@3x.png ├── zl_toggle_camera@2x.png ├── zl_toggle_camera@3x.png ├── zl_video@2x.png ├── zl_video@3x.png ├── zl_videoLoadFailed@2x.png ├── zl_videoLoadFailed@3x.png ├── zl_warning@2x.png └── zl_warning@3x.png 287 directories, 862 files
前言SRS 不用多介绍了，开干就行。一、安装 Docker# 安装 Docker sudo dnf install -y docker # 启动 Docker 服务 sudo systemctl start docker sudo systemctl enable docker # 验证安装 docker --version二、部署 SRS 流媒体服务器1. 拉取 SRS Docker 镜像docker pull ossrs/srs:52. 创建 SRS 配置文件mkdir -p /etc/srs cat > /etc/srs/srs.conf << 'EOF' listen 1935; max_connections 1000; daemon off; srs_log_tank console; http_server { enabled on; listen 8080; dir ./objs/nginx/html; } http_api { enabled on; listen 1985; } stats { network 0; } vhost __defaultVhost__ { # 直播优化配置 tcp_nodelay on; min_latency on; # 播放配置 play { gop_cache off; # 关闭 GOP 缓存，降低延迟 queue_length 10; # 播放队列长度 mw_latency 100; # 最小等待延迟 100ms } # HTTP-FLV 配置 http_remux { enabled on; mount [vhost]/[app]/[stream].flv; } # HLS 配置（可选） hls { enabled on; hls_path ./objs/nginx/html; hls_fragment 2; # 每个 TS 切片 2 秒 hls_window 10; # 保留 10 个切片 } } EOF3. 启动 SRS 容器docker run -d \ --name srs \ --restart always \ -p 1935:1935 \ -p 8080:8080 \ -p 1985:1985 \ -v /etc/srs/srs.conf:/usr/local/srs/conf/srs.conf \ ossrs/srs:54. 验证 SRS 运行状态# 查看容器日志 docker logs srs # 查看容器状态 docker ps | grep srs # 访问 Web 管理界面 # http://服务器IP:8080三、测试推流需要先安装 ffmpeg，没有的，来我这里看一下。1. 推流测试（彩条 + 实时时间）ffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 \ -f lavfi -i sine=frequency=1000:sample_rate=44100 \ -vf "drawtext=fontsize=60:fontcolor=white:box=1:boxcolor=black@0.7:x=(w-text_w)/2:y=(h-text_h)/2:text='%{localtime}'" \ -c:v libx264 -pix_fmt yuv420p -preset ultrafast -tune zerolatency \ -profile:v baseline -g 60 -b:v 800k \ -c:a aac -b:a 128k \ -f flv rtmp://127.0.0.1:1935/live/test2. 验证流状态# 检查流参数 ffprobe -v error -show_streams http://127.0.0.1:8080/live/test.flv # 应该看到： # codec_name=h264 # width=1280 # height=720 # pix_fmt=yuv420p3. 播放测试VLC 播放器：打开网络流：http://服务器IP:8080/live/test.flv
出来鞭尸了！ijkplayer iOS 编译完全指南（Xcode 26.0.1 下，修复部分问题）
前言“GitHub 上的 ijkplayer 是‘尸体’，B站内部的 ijkplayer 是‘活体’。”那么，鞭尸有何帮助？老项目应该还有一些在维护，需要做一些适配的时候，我能提供一些帮助新人想上手直播拉流的，可以参考一下本文档。概述macOS 上为 iOS 平台如何编译 ijkplayer ？此文为您解答ijkplayer 是基于 FFmpeg 的轻量级 Android/iOS 视频播放器，具有以下特点：支持 iOS/Android 平台基于 FFmpeg n4.0 版本支持硬件加速解码支持直播流播放 (RTMP, HLS)可定制化的编解码器支持编译结果总览成功编译的架构 | 架构 | 目标平台 | SDK | 库文件大小 | 编译时间 | |-----|---------|-----|-----------|---------| | **arm64** | iOS 真机 | iPhoneOS | ~25 MB | ~8分钟 | | **x86_64** | iOS 模拟器 | iPhoneSimulator | ~31 MB | ~8分钟 | | **Universal** | 通用库 | 合并 | ~56 MB | ~1分钟 | 生成的库文件build/universal/lib/ ├── libavcodec.a (37.6 MB) - 编解码器核心库 ├── libavfilter.a (1.4 MB) - 音视频过滤器库 ├── libavformat.a (10.9 MB) - 封装格式处理库 ├── libavutil.a (3.0 MB) - 工具函数库 ├── libswresample.a (696 KB) - 音频重采样库 └── libswscale.a (4.3 MB) - 视频缩放库环境要求与配置系统要求macOS版本: 10.15+ (Catalina 或更高)Xcode版本: 14.0+ (本次使用 26.0.1)磁盘空间: 至少 5GB 可用空间内存: 建议 8GB+必需工具# 安装 Homebrew (如果未安装) /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" # 安装必需工具 brew install yasm # 汇编器 brew install pkg-config # 包配置工具 (可选) brew install nasm # 另一个汇编器 (可选) # 验证 Xcode 命令行工具 xcode-select --install xcode-select -p环境变量配置# 添加到 ~/.zshrc 或 ~/.bash_profile export PATH="/usr/local/bin:$PATH" export DEVELOPER_DIR=$(xcode-select -p)项目初始化与配置修改1. 克隆项目git clone https://github.com/bilibili/ijkplayer.git ijkplayer-ios cd ijkplayer-ios git checkout -B latest k0.8.82. 初始化 iOS 构建环境./init-ios.sh这个脚本会：下载 gas-preprocessor (ARM 汇编预处理器)拉取 FFmpeg 源代码为每个架构创建 FFmpeg 分支设置配置文件链接3. 关键配置修改3.1 架构配置 (compile-ffmpeg.sh)# 只使用现代架构 FF_ALL_ARCHS_MODERN="arm64 x86_64" FF_ALL_ARCHS=$FF_ALL_ARCHS_MODERN3.2 Xcode 26+ 兼容性修复 (tools/do-compile-ffmpeg.sh)移除废弃的 Bitcode 标志:# arm64 架构 elif [ "$FF_ARCH" = "arm64" ]; then FF_XCODE_BITCODE="" # 原来是 "-fembed-bitcode" # x86_64 架构 elif [ "$FF_ARCH" = "x86_64" ]; then FF_XCODE_BITCODE="" # 原来是 "-fembed-bitcode"更新最低 iOS 版本:# arm64 FF_XCRUN_OSVERSION="-miphoneos-version-min=12.0" # x86_64 FF_XCRUN_OSVERSION="-mios-simulator-version-min=12.0"添加编译器兼容性标志:FFMPEG_CFLAGS="$FFMPEG_CFLAGS -Wno-error=incompatible-function-pointer-types" FFMPEG_CFLAGS="$FFMPEG_CFLAGS -Wno-error=int-conversion"4. 选择编解码器配置cd ios ln -s module-lite.sh config/module.sh # 精简版 (推荐) # 或 ln -s module-default.sh config/module.sh # 完整版编译步骤详解完整编译流程cd ios # 1. 清理之前的编译文件 ./compile-ffmpeg.sh clean # 2. 编译所有架构 (约15-20分钟) ./compile-ffmpeg.sh all # 或分步编译： ./compile-ffmpeg.sh arm64 # 编译真机版本 ./compile-ffmpeg.sh x86_64 # 编译模拟器版本 ./compile-ffmpeg.sh lipo # 合并为通用库编译过程说明阶段 1: 配置 (Configure)检测编译环境生成 Makefile配置编解码器选项设置编译标志阶段 2: 编译 (Compile)编译 libavutil (基础工具)编译 libavcodec (编解码器)编译 libavformat (格式处理)编译 libavfilter (过滤器)编译 libswscale (缩放)编译 libswresample (重采样)阶段 3: 安装 (Install)复制库文件到输出目录安装头文件生成 pkg-config 文件阶段 4: 合并 (Lipo)使用 lipo 工具合并多架构生成通用库文件合并头文件编译输出结构ios/build/ ├── ffmpeg-arm64/ # ARM64 架构构建 │ └── output/ │ ├── include/ # 头文件 │ │ ├── libavcodec/ │ │ ├── libavformat/ │ │ ├── libavutil/ │ │ └── ... │ └── lib/ # 静态库 │ ├── libavcodec.a │ ├── libavformat.a │ └── ... ├── ffmpeg-x86_64/ # x86_64 架构构建 │ └── output/ │ ├── include/ │ └── lib/ └── universal/ # 通用库 (Fat Binary) ├── include/ # 合并的头文件 └── lib/ # 合并的库文件 ├── libavcodec.a # 包含 arm64 + x86_64 ├── libavformat.a └── ...验证编译结果检查库架构# 验证通用库包含的架构 lipo -info build/universal/lib/libavcodec.a # 输出: Architectures in the fat file: libavcodec.a are: x86_64 arm64 # 查看库详细信息 lipo -detailed_info build/universal/lib/libavcodec.a # 检查符号表 nm build/universal/lib/libavcodec.a | grep avcodec_version验证头文件# 确认头文件存在 ls -la build/universal/include/libavcodec/avcodec.h ls -la build/universal/include/libavformat/avformat.h集成到 iOS 项目方法一：使用 Demo 项目# 打开 Demo 项目 open IJKMediaDemo/IJKMediaDemo.xcodeproj # 在 Xcode 中： # 1. 选择目标设备 (模拟器或真机) # 2. 点击 Run 按钮 # 3. 测试视频播放功能方法二：集成到自己的项目1. 添加库文件将以下文件添加到项目：build/universal/lib/*.a # 所有静态库2. 配置 Build SettingsHeader Search Paths: $(PROJECT_DIR)/path/to/build/universal/include Library Search Paths: $(PROJECT_DIR)/path/to/build/universal/lib Other Linker Flags: -lz -lbz2 -liconv3. 添加系统框架在 Build Phases → Link Binary With Libraries 添加：VideoToolbox.frameworkCoreMedia.frameworkCoreVideo.frameworkCoreGraphics.frameworkAVFoundation.frameworkAudioToolbox.frameworkUIKit.frameworkFoundation.frameworkQuartzCore.frameworkOpenGLES.frameworkCFNetwork.frameworkSecurity.frameworkMobileCoreServices.framework4. 示例代码#import <IJKMediaFramework/IJKMediaFramework.h> // 创建播放器 IJKFFMoviePlayerController *player = [[IJKFFMoviePlayerController alloc] initWithContentURL:[NSURL URLWithString:@"http://example.com/video.mp4"] withOptions:nil]; // 准备播放 [player prepareToPlay]; // 添加播放视图 player.view.frame = self.view.bounds; [self.view addSubview:player.view]; // 开始播放 [player play];支持的功能配置编解码器支持 (module-lite.sh)解码器 (Decoders)视频: H.264, HEVC/H.265, VP8, VP9, FLV, H.263音频: AAC, AAC-LATM, MP3, FLAC编码器 (Encoders)图像: PNG (用于截图)封装格式 (Formats)输入: FLV, MP4/MOV, HLS, MPEG-TS, MKV/WebM输出: MP4网络协议 (Protocols)HTTP/HTTPSRTMP/RTMPTHLSFTPTCP/UDP文件系统编译优化选项--enable-small: 减小库文件大小--enable-optimizations: 启用优化--enable-neon: ARM NEON 优化 (仅 ARM64)--enable-debug: 包含调试符号--enable-pic: 位置无关代码常见问题与解决方案问题 1: yasm 未找到错误: yasm not found 解决: brew install yasm问题 2: 编译器类型检查错误错误: incompatible function pointer types 解决: 已通过添加 -Wno-error=incompatible-function-pointer-types 解决问题 3: iOS 版本 API 不兼容错误: 'SecIdentityCreate' is only available on iOS 11.2 or newer 解决: 更新最低 iOS 版本到 12.0问题 4: Bitcode 相关错误错误: -mllvm and -bitcode_bundle cannot be used together 解决: 移除所有 -fembed-bitcode 标志问题 5: 架构不匹配错误: Undefined symbols for architecture x86_64 解决: 确保编译了所需架构并正确链接性能指标库文件大小对比配置总大小说明module-lite.sh~56 MB精简版，基本编解码器module-default.sh~120 MB完整版，所有编解码器module-lite-hevc.sh~65 MB精简版 + HEVC编译时间操作时间CPU 使用率清理1-2 分钟低编译 arm647-10 分钟高 (100%)编译 x86_647-10 分钟高 (100%)合并库<1 分钟低总计15-22 分钟-编译日志与警告说明预期警告 (可忽略)废弃 API 警告warning: 'codec' is deprecated warning: 'refcounted_frames' is deprecated这些是 FFmpeg 内部的兼容性代码。函数指针类型警告warning: incompatible function pointer types已通过编译标志抑制为警告。NEON 指令警告warning: instruction movi.2d with immediate #0ARM 汇编优化相关，不影响功能。更新与维护更新 FFmpeg 版本cd ijkplayer-ios git pull origin master ./init-ios.sh # 重新应用配置修改 # 重新编译清理编译缓存./compile-ffmpeg.sh clean rm -rf build/ rm -rf ffmpeg-*/切换编解码器配置cd config rm module.sh ln -s module-default.sh module.sh # 或其他配置 cd .. ./compile-ffmpeg.sh clean ./compile-ffmpeg.sh all参考资源官方文档ijkplayer GitHubFFmpeg 官网Apple Developer - Xcode相关项目VLC for iOSMobileFFmpegFFmpeg-iOS-build技术规范H.264 规范HEVC/H.265 规范HLS 规范许可证信息ijkplayer许可证: LGPL v2.1+版权所有: BilibiliFFmpeg许可证: LGPL v2.1+ (本配置)可选 GPL 许可 (需要重新配置)使用注意LGPL 要求动态链接或提供目标文件如果修改了 FFmpeg 源码，需要公开修改部分商业使用请仔细阅读许可证条款总结成功适配 Xcode 26.0.1移除废弃的 Bitcode 支持更新最低 iOS 版本到 12.0生成 arm64 和 x86_64 架构库创建通用库 (Universal Binary)验证所有库文件正确生成编译产物可直接用于 iOS 应用开发，支持真机和模拟器调试。
关于 SRSSRS(Simple Realtime Server)是一个简单高效的实时视频服务器，支持RTMP、WebRTC、HLS、HTTP-FLV、SRT等多种实时流媒体协议。Oryx是一个一体化、开箱即用、开源的视频解决方案，可部署在云上或自建机房，以直播和WebRTC等能力赋能你的业务。B站视频教程SRS开源服务器CentOS 安装下载虚拟机上安装，CentOS 系统下载地址：https://www.centos.org/download/安装按提示安装即可，需要注意的是，不要选最小化，而是选择包含图形界面的开启网络右上角开启网络安装/运行 Docker根据文档 docker 官方文档 安装即可。主要是以下几个命令：#来自 kyson.cn ，转载请注明出处 sudo yum install -y yum-utils sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo yum install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo systemctl start docker安装 SRS根据文档 https://github.com/ossrs/srs/tree/5.0release 安装即可。本人截止目前是 release5.0，因此下了这个版本的。主要是执行以下命令：#来自 kyson.cn ，转载请注明出处 docker run --rm -it -p 1935:1935 -p 1985:1985 -p 8080:8080 \ -p 8000:8000/udp -p 10080:10080/udp ossrs/srs:5安装成功后，我们打开网页浏览器，输入 localhost:8080 确认一下：推流可以用 ffmpeg 或者 obs 推流。使用 ffmpeg 推流我找了个文档https://linuxize.com/post/how-to-install-ffmpeg-on-centos-7/这个可行性比较好，官网的让人摸不着头脑。大概命令是以下几个：#来自 kyson.cn ，转载请注明出处 sudo yum install epel-release sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm sudo yum install ffmpeg ffmpeg-devel使用 obs 推流先去 obs官网下载 obs，下载完成后运行可见如下界面点击来源下面的加号（➕），添加录屏或者媒体源，再点击右边的开始直播即可。查看效果
在之前的文章入门视频看这一篇足够 中我们提到了 FFmpeg，也使用了 FFmpeg 最基本的命令：ffmpeg -i somemoive.mp4。今天我们就通过源码的方式来剖析 FFmpeg 的运行机制。概览ffmpeg总共有 7 大库，分别是avdeviceavcodecavformatswresampleswscalepostprocavfilter加上他们的共同依赖avutil，就算是八大库吧。这几大库按需配置，为了减少体积，建议将不用的关闭掉，关闭的时候还要根据他们之间的依赖关系，这个依赖关系可以在源代码ffbuild/config.mak 中看出。下载/编译/运行（VSCode）git clonegit clone https://git.ffmpeg.org/ffmpeg.git ffmpeg 配置./configure --enable-debug=3 --disable-optimizations比如我只想编译 h264 的解码器和支持硬解，那么只需要如下即可,没有配置的都会关闭，只打开你所配置的。./configure --disable-everything --enable-decoder=h264 --enable-hwaccel=h264_dxva2其他参数–enable-shared 默认情况下编译的库是静态的，如果我们想编译动态的，那么就可以加上这个选项。makemake -j2执行这一步后会发现多了很多文件。其中，以 _g 结尾的就是可以调试的程序ffmpeg_gffplay_gffprobe_g编辑 launch.json 文件 { "version": "0.2.0", "configurations": [ { "type": "lldb", "request": "launch", "name": "Debug", "program": "${workspaceFolder}/ffmpeg_g", "args": [ "-i", "/Users/kyson.cn/Downloads/kyson.cn.mp4" ], "cwd": "${workspaceFolder}" } ] } 运行运行后打印出了如下信息：ffmpeg version N-115575-g4e120fbbbd Copyright (c) 2000-2024 the FFmpeg developers built with Apple clang version 15.0.0 (clang-1500.3.9.4) configuration: --enable-debug=3 --disable-optimizations libavutil 59. 21.100 / 59. 21.100 libavcodec 61. 6.100 / 61. 6.100 libavformat 61. 3.104 / 61. 3.104 libavdevice 61. 2.100 / 61. 2.100 libavfilter 10. 2.102 / 10. 2.102 libswscale 8. 2.100 / 8. 2.100 libswresample 5. 2.100 / 5. 2.100 Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/Users/kyson/Downloads/7365320336742599978.mp4': Metadata: major_brand : isom minor_version : 512 compatible_brands: isomiso2avc1mp41 comment : vid:v12044gd0000cordva7og65qe9e89sqg aigc_info : {"aigc_label_type": 0} encoder : Lavf58.76.100 Duration: 00:01:12.91, start: 0.000000, bitrate: 948 kb/s Stream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709, progressive), 576x768 [SAR 1:1 DAR 3:4], 909 kb/s, 30 fps, 30 tbr, 15360 tbn (default) Metadata: handler_name : VideoHandler vendor_id : [0][0][0][0] Stream #0:1[0x2](und): Audio: aac (HE-AACv2) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 32 kb/s (default) Metadata: handler_name : SoundHandler vendor_id : [0][0][0][0] At least one output file must be specified这是因为我们在 launch.json中指定的参数是 -i，也就相当于执行我们之前提到的 ffmpeg -i yourmovie.mp4 命令。下载/编译/运行（Xcode）相信大部分人在 Mac 开发使用的还是 Xcode，因此我这边顺带把 Xcode 的配置也说了，懒得操作的，可以到 GitHub 里找一个别人现成编译好的，比如这个：FFmpeg-in-Xcode。大概思路就是：新建一个 Mac 下的命令行工程，暂且命名为 ffmpegDemo；拿到 make 产物，然后拖到 Xcode 项目中，等待其 index 完成（过程漫长）新建一个 target，暂且命名为 ffmpeg_cmd ，设置其 directory 以及 scheme搞定最终版本给大家截个图：小试身手在 ffmpeg.c 中打个断点，可以看到成功了。调用栈：我们看一下堆栈，分别在如下文件中：ffmpeg_opt.c：负责解析命令行输入的参数更进一步格式转换ffmpeg 的另外一个功能是转换文件类型，比如将 mp4 文件转换为 avi，对应的命令为: ffmpeg -i yourvideo.mp4 target.avi如果要逐步调试，查看调用堆栈的话，我们只需要在 launch.json 中稍加改动即可。 "args": [ "-i", "/Users/kyson.cn/Downloads/kyson.cn.mp4" "/Users/kyson.cn/Downloads/target.avi" ],改完后，点击编译运行，就能在 /Users/kyson.cn/Downloads 文件夹下找到 target.avi 文件；另一方面，在 vscode 中也会打印出相应的数据，来展示 avi 文件的信息：Output #0, avi, to '/Users/kyson.cn/Downloads/target.avi': Metadata: major_brand : isom minor_version : 512 compatible_brands: isomiso2avc1mp41 ICMT : vid:v12044gd0000cordva7og65qe9e89sqg aigc_info : {"aigc_label_type": 0} ISFT : Lavf61.3.104 Stream #0:0(und): Video: mpeg4 (FMP4 / 0x34504D46), yuv420p(tv, bt709, progressive), 576x768 [SAR 1:1 DAR 3:4], q=2-31, 200 kb/s, 30 fps, 30 tbn (default) Metadata: handler_name : VideoHandler vendor_id : [0][0][0][0] encoder : Lavc61.6.100 mpeg4 Side data: cpb: bitrate max/min/avg: 0/0/200000 buffer size: 0 vbv_delay: N/A Stream #0:1(und): Audio: ac3 ([0] [0][0] / 0x2000), 44100 Hz, stereo, fltp, 192 kb/s (default) Metadata: handler_name : SoundHandler vendor_id : [0][0][0][0] encoder : Lavc61.6.100 ac3 [out#0/avi @ 0x7f9288707140] video:6605KiB audio:1710KiB subtitle:0KiB other streams:0KiB global headers:0KiB muxing overhead: 1.337688% frame= 2187 fps=1126 q=31.0 Lsize= 8427KiB time=00:01:12.90 bitrate= 946.9kbits/s speed=37.5x 视频截取命令为: ffmpeg -ss 00:00:00 -t 00:00:30 -i yourvideo.mp4 -vcodec copy -acodec copy target.mp4，含义是截取视频 00:00:00 到 00:00:30 的视频，不改变音视频编码方式，目标视频文件名为 target.mp4提取音频对应的命令为：ffmpeg -i yourvideo.mp4 -vn getAudio.mp3。提取图片每隔 5s 提取视频中的一张图片：ffmpeg -i yourvideo.mp4 -r 5 -f image2 image_%2d.png。转 m3u8 文件将视频转成 m3u8 格式，方便网络传输：ffmpeg -i yourvideo.mp4 -vcodec copy -acodec copy -hls_list_size 0 -f hls/index.m3u8需要注意的是，要提前建一个 hls 文件夹。设置 IDR 帧间隔可以通过 -g（GOP size）参数控制 IDR 帧的间隔。GOP 是指 "Group of Pictures"，设置 GOP size 也意味着设置了插入 IDR 帧的频率。ffmpeg -i input.mp4 -g 60 output.mp4这意味着每隔 60 帧插入一个 IDR 帧（在 30fps 的视频中相当于每隔 2 秒插入一个 IDR 帧）。其他其他还有转码之类。总结参考ffmpeg CompilationGuideFFmpeg播放HLS流程分析FFmpeg视频切片.m3u8文件的流程与播放m3u8和mp4格式有什么区别ijkplayer视频秒开优化指北Mac下调试FFmpeg的两种方式(支持10.15.4及以上)FFmpeg原理介绍
封装视频是什么？你可能知道 MP4，他是视频常用模式，你可能也听说过 AVI，FLV 等等很多视频格式，但她们内部是怎么构成的，你又不完全清楚。这里先和大家分享下，我们知道的这些视频文件格式其实只是封装方式。视频文件内部包含了视频数据和音频数据，把他们通过某种方式封装起来，使得视频和音频数据同步就是封装要做的事情。工具视频播放涉及太多知识，泛泛而谈毫无意义，比如上一节，我说了封装，但你听了以后肯定还是摸不着头脑。但如果有个工具可以边学习边操作理解起来就容易的多，这里我选择 FFmpeg。当然，OpenCV，VLC 原理也都差不多，学习其中的任何一种即可举一反三。下载 FFmpeg直接官网下载即可：https://ffmpeg.org/，在 download 选项下，找到您对应平台的二进制文件即可。代码时间有了FFmpeg，我这边直接通过如下命令来表明，视频文件是通过封装视频数据和音频数据来实现的：./ffmpeg -i LRV_20230927_182949_01_029.mp4 ffmetadata FFMETADATAFILE Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'LRV_20230927_182949_01_029.mp4': Metadata: major_brand : avc1 minor_version : 0 compatible_brands: avc1isom creation_time : 2023-09-27T18:29:39.000000Z cycle : 16 gop : 16 Duration: 00:00:12.88, start: 0.000000, bitrate: 13071 kb/s Stream #0:0[0x1](eng): Video: h264 (Main) (avc1 / 0x31637661), yuvj420p(pc, bt709, progressive), 368x640 [SAR 1:1 DAR 23:40], 3948 kb/s, 24 fps, 24 tbr, 24k tbn (default) Metadata: creation_time : 2023-09-27T18:29:39.000000Z handler_name : Ambarella AVC vendor_id : [0][0][0][0] encoder : Ambarella AVC encoder Side data: displaymatrix: rotation of 90.00 degrees Stream #0:1[0x2](eng): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 192 kb/s (default) Metadata: creation_time : 2023-09-27T18:29:39.000000Z handler_name : Ambarella AAC vendor_id : [0][0][0][0]其中 Stream #0:0[0x1](eng): Video: 表示的就是视频流；Stream #0:1[0x2](eng): Audio:表示的就是音频流。小结FFmpeg 看起来很强大，能直接知道读出视频文件中的视频部分和音频部分，虽然 Stream #0:0[0x1](eng): Video: 和 Stream #0:1[0x2](eng): Audio: 这两行后面的内容我们暂时还不能读懂，但猜测是对视频数据和音频数据的参数的一些详细解读。帧帧（Frame）是视频的基本组成单元。视频是由一系列连续播放的静态图像（帧）组成的，通过快速连续播放这些帧，我们可以看到平滑的运动效果。通常，帧以每秒显示的帧数（FPS, Frames Per Second）来衡量。在视频编码中，帧不仅仅是简单的图像，还可以分为不同类型，它们在视频压缩和解码过程中起到不同的作用。视频编码中的帧通常分为三种主要类型：I帧（Intra-frame）、P帧（Predictive frame） 和 B帧（Bidirectional frame）。它们在编码时的压缩效率和解码复杂度不同。帧类型定义特点优缺点I 帧自我包含的完整帧独立解码，不依赖其他帧占用空间大，解码简单，通常用作关键帧P 帧预测帧，依赖前面的 I 帧或 P 帧只存储差异，压缩效率较高解码依赖于参考帧，压缩效率中等B 帧双向预测帧，依赖前后帧双向参考，压缩效率最高解码依赖前后帧，压缩效率最高，复杂度高关于 FFmpegFFmpeg 并不能直接操作音视频，而是操作 PCM 或者 YUV随之而来的问题：YUV 是什么无论是 RGB 还是 YUV ，他们都是 表达 色彩信息的一种方式。首先，讲一下为什么 YUV 色彩空间 在音视频开发，编码压缩领域更加常用，这个涉及到 HVS （Human Visual System）人类视觉系统 对 色彩空间的感知能力。视觉心理学研究表明，人的视觉系统对光的感知程度可以用两个属性来描述：亮度（luminance）跟 色度（chrominance），这里的色度也叫做 饱和度或彩度，总之 色度的叫法有很多，要注意上下文来区分语义。然后 色度感知 包含两个维度：色调（Hue）和 色饱和度（saturation）。色调是由光波的峰值定义的，描述的是光的颜色。色饱和度是由光波的谱宽定义的，描述的是光的纯度。因此 HVS 对色彩的感知主要有 3个属性：亮度（luminance），色调（Hue）和 色饱和度（saturation）。也就是 YUV 色彩空间，Y 代表 亮度，U代表色调，V代表色饱和度。经过大量研究实验表明，视觉系统 对 色度 的敏感度 是远小于 亮度的。所以可以对 色度 采用更小的采样率来压缩数据，对亮度采用正常的采样率即可，这样压缩数据不会对视觉体验产生太大的影响。简单来说就是用更少的数据/信息来表达 色度（chroma），用更多的数据/信息来表达 亮度（luminance）。YUV 分类YUV 其实目前有 三种分类：YIQ 适用于 NTSC 彩色电视制式YUV 适用于 PAL 和 SECAM 彩色电视制式YCbCr 适用于计算机用的显示器我们做互联网音视频开发， 一般说的 YUV 是 指 YCbCr ，U 就是 Cb，V 就是 Cr。大家经常在一些音视频书籍看到 YCbCr ，把它当成是 YUV 就行。实际上 YCbCr 才是比较准确的术语，JPEG、MPEG 标准 用的也是 YCbCr 。本文后面说讲的 YUV 也是指 YCbCr ，不是指用于 PAL 和 SECAM 彩色电视的 YUV。YUV 读取YUV 可以通过软件 7YUV 打开，如图是一个 YUV 图片：需要注意的是，YUV 格式的图片需要指定宽高，否则会展示异常。YUV VS RGBRGB， YUV 都是用于记录视频/图片数据的。YUV 中最重要的是 Y，因为人眼对亮度比较敏感，UV 就是色彩信息。两个例子证明：在黑暗中，颜色没有意义，因为看不到。早年时候存在的黑白电视机就没有 UV，只有 Y为什么不使用 RGB 而是 YUV？ 原因：YUV的优点是可以对其中两个分量CbCr进行采样而不太破坏图像的显示， rgb就不行会导致图像严重失真。RGB 转 YUV将一个宽高像素分别为 864x549 的 RGB 图片转成 YUV 命令如下：ffmpeg -i mov_01.jpg -s 864x549 -pix_fmt yuvj444p mov_yuv_444.yuv我们可以看到如下输出：ffmpeg version N-115585-g7d46ab9e12 Copyright (c) 2000-2024 the FFmpeg developers built with Apple clang version 15.0.0 (clang-1500.3.9.4) configuration: --disable-optimizations --disable-stripping --enable-debug=3 --disable-doc libavutil 59. 21.100 / 59. 21.100 libavcodec 61. 6.101 / 61. 6.101 libavformat 61. 3.104 / 61. 3.104 libavdevice 61. 2.100 / 61. 2.100 libavfilter 10. 2.102 / 10. 2.102 libswscale 8. 2.100 / 8. 2.100 libswresample 5. 2.100 / 5. 2.100 Input #0, png_pipe, from 'mov_01.jpg': Duration: N/A, bitrate: N/A Stream #0:0: Video: png, rgba(pc, gbr/unknown/unknown), 864x549, 25 fps, 25 tbr, 25 tbn Stream mapping: Stream #0:0 -> #0:0 (png (native) -> rawvideo (native)) Press [q] to stop, [?] for help [swscaler @ 0x7f8246d49000] deprecated pixel format used, make sure you did set range correctly Output #0, rawvideo, to 'mov_yuv_444.yuv': Metadata: encoder : Lavf61.3.104 Stream #0:0: Video: rawvideo (444P / 0x50343434), yuvj444p(pc, progressive), 864x549, q=2-31, 284601 kb/s, 25 fps, 25 tbn Metadata: encoder : Lavc61.6.101 rawvideo Side data: ICC Profile [out#0/rawvideo @ 0x7f8246806800] video:1390KiB audio:0KiB subtitle:0KiB other streams:0KiB global headers:0KiB muxing overhead: 0.000000% frame= 1 fps=0.0 q=-0.0 Lsize= 1390KiB time=00:00:00.04 bitrate=284601.6kbits/s speed=2.06x说明转换成功了MP4 格式视频可以看作是一系列图片快速播放形成的，前面我们已经了解过图片格式，现在我们开始讲视频。视频格式有很多种，我们先讲比较常见的 MP4 格式，这里先发挥读者的主观能动性：如果让你设计视频格式，你会怎么设计呢？我们随便找个 MP4 文件，然后通过 MP4Box 工具或者 MP4 Explorer 工具查看 MP4 文件的构成。开始行动前在设计或者使用一个播放器之前，我们有必要了解一些基本概念。播放器内核层：抽象概念作用与说明DataSource负责原始数据读取（网络/本地/加密流），屏蔽底层 I/O 差异 例：HttpDataSource、FileDataSource、CacheDataSourceDemuxer / Extractor解封装（Demuxing）：从容器格式（MP4/TS/HLS）中分离音视频流 例：MP4Extractor、HlsMediaSourceDecoder音视频解码器，将压缩数据（H.264/AAC）转为原始帧（YUV/PCM） 可软解（FFmpeg）或硬解（VideoToolbox/MediaCodec）Renderer渲染器：将解码后的帧送至输出设备（画面到屏幕，音频到扬声器） 通常分为 VideoRenderer 和 AudioRendererTimeline描述媒体时间轴结构（含多轨道、广告插播点、章节等） 支持动态拼接（如 DASH 多 Period）LoadControl控制缓冲策略：何时加载、加载多少、丢弃旧数据 避免内存爆炸或频繁卡顿TrackSelector轨道选择器：根据设备能力/用户偏好选择音轨、字幕、分辨率 例：自动选 1080p 或 720pMediaSession与系统媒体中心集成（锁屏控制、通知栏、耳机按键） Android 的 MediaSessionCompat / iOS 的 MPNowPlayingInfoCenterAnalytics / Logger播放埋点模块：记录起播时间、卡顿次数、码率切换等指标 用于 QoS 监控和体验优化DRM Session数字版权管理：处理 Widevine/FairPlay 解密密钥 与 License Server 交互，保护内容安全Cache Manager离线缓存管理：预下载、断点续播、LRU 清理 常配合 DataSource 使用Event Bus / Listener事件通知机制：播放状态变更、错误、缓冲更新等 解耦核心引擎与上层业务播放器应用层：抽象概念AVFoundation 中的对应类/机制ijkplayer (iOS) 中的对应概念媒体资源描述AVAsset / AVURLAssetIJKFFMovieDefinition / 直接传 URL单个可播放项AVPlayerItem无显式对象，通过 setUrl: 切换播放引擎AVPlayerIJKFFMediaPlayer视频画面渲染层AVPlayerLayer（CALayer 子类）IJKFFMovieView（UIView 子类，内部含 MTKView）播放控制与状态监听KVO + 通知（如 timeControlStatus、AVPlayerItem.status）delegate 回调（如 mediaPlayer:didPreload:）音频会话管理AVAudioSession内部自动处理，可配置后台播放支持需手动配置 AVAudioSession + 后台模式需设置 allowsBackgroundPlayback = YES字幕/多音轨AVMediaSelectionGroup / AVPlayerItem.selectableMediaOptions基础支持，需手动解析外挂字幕缓冲与加载策略隐藏（不可控），可通过 preferredForwardBufferDuration 微调可通过 options 调整（如 max-buffer-size）自定义数据源AVAssetResourceLoaderDelegate（用于 HTTPS 解密、自定义协议等）支持 setStreamOpenReadSeekCloseCallbacksPCM 是什么IJKPlayerIJKPlayer 简介ijkplayer是一个基于 FFmpeg 和 SDL 的开源 Android/iOS 视频播放器，由 bilibili 开发。它支持几乎所有视频格式，具有高效的硬件解码和渲染能力，可以实现流畅的视频播放体验。ijkplayer 还提供了丰富的 API 接口，支持多种播放控制方法和事件监听，可以让开发者灵活地集成和定制播放器。此外，ijkplayer 还提供了一些强大的剪辑和水印功能，可以实现视频编辑和个性化定制。ijkplayer 和 FFMpeg、SDL 的关系是如何的呢？简单来说 ffmpeg 提供最基础的解码能力，将视频文件解析成特定的二进制流数据（比如我们常见的RGB、YUV），而SDL负责将特定的二进制流文件渲染出来。IJKPlayer 在 iOS 和 Android 平台上实现的主要差异表现在视频硬解码方案和音视频渲染方案上：iOS视频硬解码：VideoToolbox音频渲染：AudioQueue视频渲染：OpenGL ESAndroid视频硬解码：MediaCodec音频渲染：OpenSL ES、AudioTrack视频渲染：OpenGL ES、MediaCodecIJKPlayer 项目的目录结构tool：初始化项目工程脚本。config：编译 FFmpeg 使用的配置文件。extra：存放编译 IJKPlayer 所需的依赖源文件, 如 FFmpeg、OpenSSL 等。ijkmedia：核心代码。ijkplayer：播放器数据下载及解码相关。ijksdl：音视频数据渲染相关。ios：iOS平台上的上层接口封装以及平台相关方法。android：android平台上的上层接口封装以及平台相关方法。IJKPlayer 解码（硬解/软解）IJKPlayer 在视频解码上支持软解和硬解两种方式，可在起播前配置优先使用的解码方式，播放过程中不可切换。IJKPlayer 中的音频解码只支持软解，暂不支持硬解。问题一：bilibili 国际版硬解转软解问题二：卡顿率（Buffer队列）问题三：seek操作问题四：各平台渲染引擎Android ： ANativeWindow参考解析 IJKPlayer音视频 ijkplayer 源码解析系列1--播放器介绍我们为什么使用DASHFFmpeg播放视频流程详解视频和视频帧：视频和帧基础知识整理
进入B站后笔者首先着手的就是哔哩哔哩国际版的弹幕重构。哔哩哔哩国际版内部简称蓝版，这是因为其主题色位蓝色，这么做的目的是为了和国内的粉版区分。其能在海外的 App Store上下载到，以下是 APP 的介绍页面：蓝版的代码最初是从粉版 fork 出来的，并且阉割了一些比较重的、不常用的功能。考虑到海外的网络情况和国内差的不是一点半点，因此本人(kyson.cn)在重构弹幕时还做了很大幅度的优化。这会在文章中慢慢分享给大家。引言首先给大家提个问题：如果让您设计一个弹幕，你会怎么着手去做这件事。这里我相信大家不约而同肯定会想到：github。我们先罗列一下 github 上 star 数较多的几个弹幕库：弹幕库名star 数地址BarrageRenderer2.2khttps://github.com/unash/BarrageRendererDanmakuFlameMaster9.5khttps://github.com/bilibili/DanmakuFlameMasterBarrageRendererBarrageRenderer 是个人开发者编写的，完成度还算挺高，可惜目前已经不维护了。查看 BarrageRenderer 代码可知，其关键逻辑还是更改 UIView 的 Frame：#pragma mark - update /// 每个刷新周期执行一次 - (void)update { [_dispatcher dispatchSprites]; // 分发精灵 for (BarrageSprite * sprite in _dispatcher.activeSprites) { [sprite updateWithTime:self.time]; } } #pragma mark - update - (void)updateWithTime:(NSTimeInterval)time { _valid = !self.forcedInvalid && [self validWithTime:time]; _view.frame = [self rectWithTime:time]; if ([_view respondsToSelector:@selector(updateWithTime:)]) { [_view updateWithTime:time]; } } DanmakuFlameMasterDanmakuFlameMaster 简称 DFM，开源的只有 Android 版本，是哔哩哔哩早期使用的弹幕引擎。由于本人不是专业的 Android 开发，因此我不做过多分析。本人(kyson.cn)在B站第一个任务就是负责将 DFM 下架，转而使用字幕/弹幕统一框架 Chronos。 关于 Chronos 我后面做详细介绍，这里先和大家报告一下 DFM 在 iOS 版本下的一些实现原理。由于 iOS 版本的 DFM 没有开源，我这里不能将具体代码贴出，但我会将其实现原理大致告诉大家。和 BarrageRenderer 有一些相似之处我怀疑 BarrageRenderer 作者是否在B站工作过，他的设计理念，甚至一些方法名都和 DFM 一模一样。不信你看：- (void)updateWithTime:(DFMTime)time { //此处省略一部分代码 }这是核心的更新弹幕位置的函数。其他的部分相似的还有命名：DFMClock，设计思想也有一部分类似，因此看懂 BarrageRenderer 代码对理解 DFM 有一定帮助DFM 不是基于 UIKit这是和 BarrageRenderer 差异最明显的地方。DFM 基于更底层的 Metal/OpenGL，使用哪个 Render 组件取决于手机型号是否支持。Chronos不管是 BarrageRenderer 还是 DFM 都有一定的局限性：他们很难在 iOS 和 Android 实现统一，最多保证理念一致，但实现需要依靠各自平台库支持。这个时候一个大一统的方案呼之欲出，他就说 Chronos。Chronos 脱胎于谷歌的 Khronos ， 在 B 站内部被封装成了 Framework，接受 TS 注入，所以内部应该是有个 JSC 容器。如此一来，我们只需要把 js 逻辑写好，其他部分交给 chronos 来绘制即可。Android 和 iOS 各自安装一份 Chronos 库文件相对来说就容易的多。架构chronos 有多层构成，业务层无需关注底层引擎，只需要写对应业务，实现业务和底层隔离，并且能跨平台。底层实现既然能解释运行 ts，那么肯定里面有 jsc。大概的底层实现如下：具体 API 有如下这些：我们来总结一下这份 API：+------------------+------------------------------------------+--------------------------------------------------+ | 模块类别 | 关键类（Class / Interface） | 职责说明 | +------------------+------------------------------------------+--------------------------------------------------+ | 核心渲染 | Scene, Node, LabelNode, SpriteNode, | 构建 2D 场景图，管理视觉元素层级、变换、绘制 | | | ShapeNode, EffectNode | | +------------------+------------------------------------------+--------------------------------------------------+ | 动画系统 | Action, MoveBy, RotateTo, Sequence, | 提供可组合的动画指令，支持时间轴控制与缓动 | | | BezierPath, EmitterNode | | +------------------+------------------------------------------+--------------------------------------------------+ | 物理系统 | PhysicsWorld, PhysicsBody, | 模拟刚体动力学，支持重力、碰撞、关节等行为 | | | PhysicsJoint | | +------------------+------------------------------------------+--------------------------------------------------+ | 资源管理 | Image, Font, Texture, Resources | 加载、缓存、释放图片/字体等资源，避免重复加载 | +------------------+------------------------------------------+--------------------------------------------------+ | 交互与输入 | TouchEvent, Sensor, Window | 处理触摸、加速度计、陀螺仪等用户输入事件 | +------------------+------------------------------------------+--------------------------------------------------+ | 高级图形能力 | Shader, RenderTexture, CustomCommand | 支持自定义着色器、离屏渲染、GPU 特效 | +------------------+------------------------------------------+--------------------------------------------------+ | 并行计算 | Worker | 在独立线程执行 JS 逻辑，避免阻塞主线程 | +------------------+------------------------------------------+--------------------------------------------------+ | 工具与辅助 | Vec2, Color, Rect, Math | 提供数学运算、颜色处理、几何结构等基础工具 | +------------------+------------------------------------------+--------------------------------------------------+手搓一个为了模仿他的实现，我们可以手搓一个。目录结构：ChronosMiniDemo/ ├── ChronosMiniDemo.xcodeproj/ ← Xcode 自动生成 └── ChronosMiniDemo/ ├── AppDelegate.swift ├── ViewController.swift ├── SceneDelegate.swift ← 若用 SceneDelegate ├── DanmakuBridge.h ├── DanmakuBridge.m └── main.bundle.js文件 1：DanmakuBridge.h// ChronosMiniDemo/DanmakuBridge.h #import <Foundation/Foundation.h> @interface DanmakuBridge : NSObject - (void)setupInContext:(JSGlobalContextRef)ctx; @end文件 2：DanmakuBridge.m // ChronosMiniDemo/DanmakuBridge.m #import "DanmakuBridge.h" #import <JavaScriptCore/JavaScriptCore.h> // 全局保存 JS 回调（简化版） static JSValueRef g_clickCallback = NULL; static JSGlobalContextRef g_context = NULL; static void addDanmaku(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { if (argumentCount < 2) return; JSStringRef textJS = JSValueToStringCopy(ctx, arguments[0], NULL); JSStringRef colorJS = JSValueToStringCopy(ctx, arguments[1], NULL); char textBuf[256], colorBuf[256]; JSStringGetUTF8CString(textJS, textBuf, sizeof(textBuf)); JSStringGetUTF8CString(colorJS, colorBuf, sizeof(colorBuf)); NSString *text = [NSString stringWithUTF8String:textBuf]; NSString *color = [NSString stringWithUTF8String:colorBuf]; NSLog(@"✅ [Native] 收到弹幕: %@, 颜色: %@", text, color); // 模拟 1.5 秒后点击 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (g_clickCallback) { JSStringRef clickText = JSStringCreateWithUTF8CString(textBuf); JSValueRef args[] = { JSValueMakeString(ctx, clickText) }; JSObjectCallAsFunction(ctx, (JSObjectRef)g_clickCallback, NULL, 1, args, NULL); JSStringRelease(clickText); } }); JSStringRelease(textJS); JSStringRelease(colorJS); } static void onDanmakuClick(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) { if (argumentCount > 0) { if (g_clickCallback) { JSValueUnprotect(ctx, g_clickCallback); } g_clickCallback = arguments[0]; JSValueProtect(ctx, g_clickCallback); g_context = ctx; NSLog(@"✅ [Native] 已注册弹幕点击回调"); } } @implementation DanmakuBridge - (void)setupInContext:(JSGlobalContextRef)ctx { JSObjectRef bridgeObj = JSObjectMake(ctx, NULL, NULL); JSObjectSetProperty(ctx, bridgeObj, JSStringCreateWithUTF8CString("addDanmaku"), JSObjectMakeFunctionWithCallback(ctx, NULL, addDanmaku), kJSPropertyAttributeReadOnly, NULL); JSObjectSetProperty(ctx, bridgeObj, JSStringCreateWithUTF8CString("onDanmakuClick"), JSObjectMakeFunctionWithCallback(ctx, NULL, onDanmakuClick), kJSPropertyAttributeReadOnly, NULL); JSObjectSetProperty(ctx, JSContextGetGlobalObject(ctx), JSStringCreateWithUTF8CString("nativeBridge"), bridgeObj, kJSPropertyAttributeNone, NULL); } @end 文件 3：main.bundle.js（TS 编译结果）// ChronosMiniDemo/main.bundle.js (function() { 'use strict'; // 添加弹幕 nativeBridge.addDanmaku("前方高能！", "#FF6600"); // 监听点击 nativeBridge.onDanmakuClick(function(text) { console.log("[JS] 用户点击了弹幕: " + text); }); })();文件 4：ViewController.swift（替换原有）// ChronosMiniDemo/ViewController.swift import UIKit import JavaScriptCore class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = .systemBackground let label = UILabel(frame: CGRect(x: 20, y: 100, width: 300, height: 50)) label.text = "查看控制台输出！" label.textAlignment = .center self.view.addSubview(label) // 启动 JS 引擎 runDanmakuEngine() } private func runDanmakuEngine() { let context = JSGlobalContextCreate(nil) let bridge = DanmakuBridge() bridge.setupInContext(context!) // 加载 JS if let jsPath = Bundle.main.path(forResource: "main.bundle", ofType: "js"), let jsCode = try? String(contentsOfFile: jsPath, encoding: .utf8) { let jsStr = JSStringCreateWithCFString(jsCode as CFString) JSEvaluateScript(context, jsStr, nil, nil, 0, nil) JSStringRelease(jsStr) } JSGlobalContextRelease(context) } }最后看效果：其他方案腾讯的 PAG 可以实现酷炫的动画效果，简单的弹幕当然不在话下gltf引用awesome-danmakuB站弹幕库DanmakuFlameMaster源码浅析