{"owner":"CarGuo","repo":"GSYVideoPlayer","hasSkills":true,"totalSkillsCount":15,"totalTokensCount":26544,"categories":["anthropic-skill","project-spec"],"hasMcp":false,"mcpConfig":null,"found":["skills/06-gsy-cache-and-download/SKILL.md","skills/04-gsy-kernel-switch/SKILL.md","skills/05-gsy-list-playback/SKILL.md","skills/08-gsy-subtitle-and-preview/SKILL.md","skills/02-gsy-option-builder/SKILL.md","skills/03-gsy-fullscreen-orientation/SKILL.md","skills/01-gsy-quickstart/SKILL.md","skills/11-gsy-compose-integration/SKILL.md","skills/10-gsy-cast-and-loopback/SKILL.md","skills/07-gsy-render-and-effects/SKILL.md","skills/12-gsy-custom-view/SKILL.md","skills/09-gsy-live-and-ads/SKILL.md","skills/13-gsy-proguard-r8/SKILL.md","doc/ARCHITECTURE.md","skills/README.md"],"skills":{"skills/06-gsy-cache-and-download/SKILL.md":"---\nname: gsy-cache-and-download\ndescription: 边播边缓（AndroidVideoCache 代理）、自定义缓存目录、清理缓存、离线下载至完整文件。\nwhen_to_use: 需要\"边看边攒\"离线可播；或空间敏感需要限量清理；或做真下载入库（不是缓存代理）。\ninputs:\n  - name: url\n    description: 网络视频地址（http/https）\n    required: true\n  - name: cache_dir\n    description: 缓存目录（默认 context.cacheDir/videoCache）\n    required: false\noutputs:\n  - 播放同时把 mp4 缓存到本地；命中缓存后重播 0 网络。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheManager.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/CacheFactory.java\n  - file: gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/cache/ICacheManager.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailDownloadPlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailDownloadExoPlayer.java\n---\n\n# gsy-cache-and-download\n\n## 概念\n\n- **缓存**（cache）：`ProxyCacheManager` 内部起本地 HTTP 代理，拦截 `player` 的请求，把响应流一边喂给 player 一边写到本地；文件名是 URL MD5。\n- **下载**（download）：与缓存共享目录，但一定要走\"从头到尾\"完整播完才能保证文件可复用为独立 mp4；提前 seek 会造成 range 分片，回放时代理会重新下载缺失段。\n\n## 开启缓存\n\n方式 1：`setUp(url, true, title)` 第二参数 `cacheWithPlay=true`。\n\n方式 2：`GSYVideoOptionBuilder`：\n- `setCacheWithPlay(true)`\n- `setCachePath(File)`（可选）\n\n## 自定义缓存目录\n\n```java\nFile dir = new File(getExternalFilesDir(null), \"videoCache\");\nbuilder.setCachePath(dir);\n```\n\n若使用不同 `CacheManager`（例如 Exo），传给 `CacheFactory.setCacheManager(...)` 前记得也走 `setCachePath` 或自定义 `ICacheManager` 实现。\n\n## `ICacheManager` API\n\n见 [ICacheManager](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/cache/ICacheManager.java)。核心方法：\n\n| 方法 | 说明 |\n|---|---|\n| `doCacheLogic(Context, IMediaPlayer, String url, Map<String,String> headers, File cachePath)` | 播放前介入，把 url 换为代理 url |\n| `clearCache(Context, File cachePath, String url)` | 单条清除；`url = null` 清空整目录 |\n| `hadCached()` | 当前播放是否已\"命中\"缓存（100% 完成） |\n| `setCacheAvailableListener(CacheAvailableListener)` | 缓存进度回调 |\n| `release()` | 释放代理 |\n\n## `ProxyCacheManager` 关键静态方法\n\n在 [ProxyCacheManager](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheManager.java) 里：\n\n- `clearAllCache(Context, File)` —— 清空整个缓存目录。\n- 内部 `HttpProxyCacheServer` 单例 lazy 初始化，第一次 `doCacheLogic` 时才起服务；Application 就绪即可。\n\n## `ProxyCacheUserAgentHeadersInjector`\n\n自定义 UA / Cookie / Referer 注入到代理层：见 [ProxyCacheUserAgentHeadersInjector](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheUserAgentHeadersInjector.java)。挂到 `HttpProxyCacheServer.Builder.headerInjector()` 上，或直接用 `Map` 传给 `setUp`。\n\n## 离线下载\n\n三步流程：\n\n1. 用 `HttpProxyCacheServer` 单独跑一次预下载（不 attach 到 Player），或直接开一条播放并 `setStartAfterPrepared(false)` 静默走全长；\n2. 事件 `VideoAllCallBack#onAutoComplete` 触发 → 缓存文件已完整；\n3. 用 [FileUtils](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/FileUtils.java) 拷贝 / 重命名到业务目录。\n\nDemo：[DetailDownloadPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailDownloadPlayer.java)（IJK）与 [DetailDownloadExoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailDownloadExoPlayer.java)（Exo）。\n\n## 不缓存的场景\n\n以下情况 `cacheWithPlay` 应设 `false`：\n- 直播 / RTMP / RTSP（连续无尽流）；\n- HLS m3u8（代理逻辑对分片不友好，建议 Exo 内核 + Exo cache）；\n- DASH / SmoothStreaming；\n- DRM 或私有加密。\n\n## 常见坑\n\n- **代理端口被占**：`HttpProxyCacheServer` 首次启动失败会 fallback 到原始 url，缓存不生效。多进程时可能出现，建议仅主进程用。\n- **缓存目录跨用户**：`context.cacheDir` 是 per-user；用外部存储需要动态权限。\n- **同 URL 缓存名冲突**：MD5(url) 相同即命中；如果 URL 带一次性签名，每次不同 → 缓存穿透。可以先规范化 URL 再传给 `setUp`。\n- **HTTP header 变更失效**：headers 参与代理请求生成，但缓存文件名只按 URL；同一 URL 换 UA 不会重新下载。\n","skills/04-gsy-kernel-switch/SKILL.md":"---\nname: gsy-kernel-switch\ndescription: 在 IJK / System / ExoPlayer2(Media3) / Aliyun 四个媒体内核之间自由切换，以及自定义 Manager 的方法。\nwhen_to_use: 需要支持 DASH/SmoothStreaming、需要 Media3 track selection、想减 so 体积只留系统解码、要用 Aliyun 内核、或者要多实例并行独立 Manager。\ninputs:\n  - name: kernel\n    description: ijk / system / exo / ali / custom\n    required: true\noutputs:\n  - PlayerFactory 与（可选）CacheFactory 已切换到目标 Manager\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/PlayerFactory.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/IjkPlayerManager.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/SystemPlayerManager.java\n  - file: gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/IPlayerManager.java\n  - file: gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/BasePlayerManager.java\n  - file: gsyVideoPlayer-exo_player2\n  - file: gsyVideoPlayer-aliplay/src/main/java/com/shuyu/aliplay/AliPlayerManager.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/exo/GSYExoPlayerManager.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/manager/CustomManager.java\n---\n\n# gsy-kernel-switch\n\n## 内核矩阵\n\n| Kernel | Class | 来源模块 | 特点 |\n|---|---|---|---|\n| **IJK**（默认） | [IjkPlayerManager](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/IjkPlayerManager.java) | `gsyVideoPlayer-java` + 升级后的独立 ABI / `ex_so` | FFmpeg n5.1.10 的固定配置；软/硬解能力按格式与设备确认 |\n| **System** | [SystemPlayerManager](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/SystemPlayerManager.java) | `gsyVideoPlayer-java` | 无 so；靠 `android.media.MediaPlayer`；直播/复杂容器兼容差 |\n| **Exo/Media3** | `GSYExoPlayerManager`（Demo 内）／`GSYExo2MediaPlayer` | `gsyVideoPlayer-exo_player2` | DASH / HLS / SmoothStreaming / TrackSelection |\n| **Aliyun** | [AliPlayerManager](../../gsyVideoPlayer-aliplay/src/main/java/com/shuyu/aliplay/AliPlayerManager.java) | `gsyVideoPlayer-aliplay` | 阿里云 vid/authInfo；HLS 加密 |\n\n## 切换 API\n\n**全局切换**：`PlayerFactory.setPlayManager(Class<? extends IPlayerManager>)`（内部 `getPlayManager` 用 `sPlayerManager.newInstance()` 反射构造，故被切入的类**必须**有 `public` 无参构造）。见 [PlayerFactory#L15-L27](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/PlayerFactory.java) 与本仓库 R8 保留规则 [proguard-rules.pro](../../app/proguard-rules.pro)。\n\n```java\n// Exo\nPlayerFactory.setPlayManager(Exo2PlayerManager.class);\nCacheFactory.setCacheManager(ExoPlayerCacheManager.class);\n\n// System\nPlayerFactory.setPlayManager(SystemPlayerManager.class);\n\n// Aliyun\nPlayerFactory.setPlayManager(AliPlayerManager.class);\n```\n\n必须在 `player.setUp(...)` **之前**调用；否则本次实例仍用旧内核。\n\n## `IPlayerManager` 抽象\n\n统一接口见 [IPlayerManager](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/IPlayerManager.java)。核心方法（子类必须实现）：\n\n| 方法 | 说明 |\n|---|---|\n| `initVideoPlayer(Context, Message, List<VideoOptionModel>, ICacheManager)` | 初始化内核；`Message.obj` 是 `GSYModel` |\n| `showDisplay(Message)` | 绑定 Surface / SurfaceHolder |\n| `setSpeed(float, boolean soundTouch)` | 速率 + 变速不变调 |\n| `setNeedMute(boolean)` | 静音 |\n| `setVolume(float, float)` | 双通道音量 |\n| `getMediaPlayer()` | 底层 player 实例 |\n| `isSurfaceSupportLockCanvas()` | 是否支持 `lockCanvas`（TextureView 补帧） |\n| `start / pause / stop / seekTo / release / getCurrentPosition / getDuration / getVideoSarNum / getVideoSarDen / getVideoWidth / getVideoHeight` | 基础控制 |\n\n推荐继承 [BasePlayerManager](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/BasePlayerManager.java) 以少写模板。\n\n## `IPlayerInitSuccessListener` hook\n\n[IPlayerInitSuccessListener](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/IPlayerInitSuccessListener.java) 允许在 `initVideoPlayer` 完成后对底层 Player 拿手（比如设置 Exo 的 TrackSelector）：\n\n```java\nIjkPlayerManager.setIjkLibLoader(...);\n// 或\nGSYExoVideoManager.setPlayerInitSuccessListener(new IPlayerInitSuccessListener() {\n    @Override public void onPlayerInitSuccess(IMediaPlayer mp, GSYModel model) { ... }\n});\n```\n\n## `CacheFactory` 同步切换\n\n不同内核有各自缓存方案；切内核记得配套：\n\n| Player Manager | 对应 CacheManager |\n|---|---|\n| `IjkPlayerManager` / `SystemPlayerManager` | [ProxyCacheManager](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheManager.java)（默认） |\n| Exo/Media3 | `ExoPlayerCacheManager`（`gsyvideoplayer-exo_player2` 内） |\n| Aliyun | 由 Aliyun SDK 自身管理，一般 `CacheFactory` 保持 `ProxyCacheManager` 或自定义空实现 |\n\n## 多实例并行\n\n用 [CustomManager](../../app/src/main/java/com/example/gsyvideoplayer/video/manager/CustomManager.java) 模式：拷贝 `GSYVideoManager` 单例套壳，在自定义 `GSYVideoView` 里覆盖 `getGSYVideoManager()`；不同页面互不影响，参考 [MultiSampleVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/MultiSampleVideo.java) + [ListMultiVideoActivity](../../app/src/main/java/com/example/gsyvideoplayer/ListMultiVideoActivity.java)。\n\n## Demo 对照\n\n- Exo 切源 / 无缝切换：[DetailExoListPlayer](../../app/src/main/java/com/example/gsyvideoplayer/exo/DetailExoListPlayer.java)、[ExoAdaptiveTrackActivity](../../app/src/main/java/com/example/gsyvideoplayer/exo/ExoAdaptiveTrackActivity.java)\n- MediaCodec 智能回退：[SmartMediaCodecFallbackActivity](../../app/src/main/java/com/example/gsyvideoplayer/SmartMediaCodecFallbackActivity.java) + [doc/SMART_MEDIACODEC_FALLBACK_RESEARCH.md](../../doc/SMART_MEDIACODEC_FALLBACK_RESEARCH.md)\n\n## v14.0.0 原生选择\n\nIJK 当前三 ABI 成套库为 FFmpeg n5.1.10 / OpenSSL 3.5.9。`GSYIjkMediaCodecSelector` 先保留原 IJK 选择，再在 API 29+ 选择平台报告的普通 Codec2 硬件解码器，排除纯软件及安全/隧道必需解码器。这是初始化阶段的选择补充，与确认运行中硬解失败后重建软解的策略分开；见 [发布说明](../../doc/UPDATE_VERSION.md#v1400-2026-10-10)。\n\n## 常见坑\n\n- `PlayerFactory.setPlayManager` 传入的类**必须**有 `public` 无参构造，且被 ProGuard 保留。\n- Exo 切源建议用 `Exo2PlayerManager` + `GSYExo2MediaPlayer`；直接换 Manager 但用 Ijk 的 URL 会走缓存代理导致 DASH 解析失败——记得 `setOverrideExtension(\"mpd\")` 或关闭缓存。\n- 系统内核 `SystemPlayerManager` 不支持 `soundTouch`、多数直播格式；生产环境仅作 fallback。\n","skills/05-gsy-list-playback/SKILL.md":"---\nname: gsy-list-playback\ndescription: RecyclerView / ViewPager2 / ViewPager 列表滑动自动播放，滚动切换、复用绑定、进入详情页时\"漂移\"到全屏。\nwhen_to_use: 短视频信息流、朋友圈式内嵌播放、抖音/快手类 ViewPager2 竖播、列表进入详情页的无缝续播。\ninputs:\n  - name: adapter\n    description: RecyclerView.Adapter / FragmentStateAdapter\n    required: true\noutputs:\n  - 列表条目具备\"进入可视区自动播放，离开释放\"的能力，且切换详情页可续播。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/ListVideoUtil.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoHelper.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/utils/ScrollCalculatorHelper.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/AutoPlayRecyclerViewActivity.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/ViewPager2Activity.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/RecyclerViewActivity.java\n---\n\n# gsy-list-playback\n\n## 三条实现路线\n\n| 路线 | 特点 | 适用 |\n|---|---|---|\n| **A. 每 item 内嵌 `StandardGSYVideoPlayer`** | 每条 item 有独立 player 实例，滑到即播 | 大多数场景（含无限缩略页面） |\n| **B. `ListVideoUtil` 单例复用** | 全列表共享一个 player 实例，动态 `attachToParent` | 内存敏感、条目多；不能同屏两个视频 |\n| **C. `GSYVideoHelper` 悬浮小窗** | 用于详情页返回列表时\"漂浮\"续播 | 抖音式详情返回 |\n\n## 路线 A：每 item 一个 player\n\n关键点：**在 `onBindViewHolder` 里给每个 player `setPlayPosition(position)` + `setPlayTag(String)`**，防止滚动复用导致 URL 命中错人。\n\n`GSYVideoOptionBuilder`：\n- `setPlayTag(String)` [#L347](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java)\n- `setPlayPosition(int)` [#L356](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java)\n\n滚动出屏自动释放：`RecyclerView.OnScrollListener` → 当前 firstVisible / lastVisible 之外的 holder 调 `GSYVideoManager.releaseAllVideos()`。\n\n见 [RecyclerViewActivity](../../app/src/main/java/com/example/gsyvideoplayer/RecyclerViewActivity.java)。\n\n## 路线 B：`ListVideoUtil`（跨 item 复用一个 player）\n\n工具类：[ListVideoUtil](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/ListVideoUtil.java)。\n\n| API | 说明 |\n|---|---|\n| `ListVideoUtil(Context)` | 单例创建（在 Activity/Fragment 里持一份） |\n| `addVideoPlayer(int position, ImageView thumb, String tag, ViewGroup container, View playBtn)` | 把 player 移到 `container`；`tag` 与 `position` 用于识别是否是新目标 |\n| `setPlayPositionAndTag(int, String)` | 手动更新目标 |\n| `startPlay(String url)` | 开始播放 |\n| `isCurrentViewPlaying(int pos, String tag)` | 判定当前播放是否在这个 item |\n| `releaseVideoPlayer()` | 释放，通常在离开页面时调 |\n\n**自动可视区触发**用 [ScrollCalculatorHelper](../../app/src/main/java/com/example/gsyvideoplayer/utils/ScrollCalculatorHelper.java) 的 `onScrollReleaseAllVideos(...) / onScrollPlayVideo(...)`。见 [AutoPlayRecyclerViewActivity](../../app/src/main/java/com/example/gsyvideoplayer/AutoPlayRecyclerViewActivity.java)。\n\n## 路线 C：`GSYVideoHelper` 小窗漂移\n\n工具类：[GSYVideoHelper](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoHelper.java)。核心思路：**列表 item / 详情页共用同一个 player 实例**，在离开列表时把 player parent 换到浮层容器，进入详情再换回去。\n\n| API | 说明 |\n|---|---|\n| `GSYVideoHelper(Context, GSYBaseVideoPlayer)` | 携一份 player |\n| `setSmall(int width, int height, int gravity, int marginX, int marginY)` | 小窗尺寸 |\n| `showSmallVideo(...)` / `smallVideoToNormal()` | 小窗 ↔ 正常 |\n| `releaseVideoPlayer()` | 释放 |\n\nDemo：[SmallVideoHelper](../../app/src/main/java/com/example/gsyvideoplayer/utils/SmallVideoHelper.java)（app 层包装） + [ListVideoActivity](../../app/src/main/java/com/example/gsyvideoplayer/ListVideoActivity.java)。\n\n## ViewPager2 竖播\n\n- 用 `FragmentStateAdapter`，每个 Fragment 内嵌 `StandardGSYVideoPlayer` 或 `NormalGSYVideoPlayer`；\n- `ViewPager2.registerOnPageChangeCallback` 里 `onPageSelected` → `previousPlayer.onVideoPause(); currentPlayer.startPlayLogic()`；\n- 记得 `setUserInputEnabled` 竖向。\n\nDemo：[ViewPager2Activity](../../app/src/main/java/com/example/gsyvideoplayer/ViewPager2Activity.java)（RecyclerView 变体） + Compose 版 [VerticalShortVideoComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/VerticalShortVideoComposeActivity.kt)。\n\n## 常见坑\n\n- **相同 URL 不重播**：忘了 `setPlayTag/setPlayPosition`，`setUp` 内部去重返回 false。\n- **切换过快出黑帧**：`setUp` 后立即 `startPlayLogic` 前先隐藏 `thumb`；或用 `KeepLastFrameVideo` 保留上一帧（详见 [doc/KEEP_LAST_FRAME.md](../../doc/KEEP_LAST_FRAME.md)）。\n- **同屏多个 item 各自播放**：单例 `GSYVideoManager` 只能有一个 listener；同屏并行需要参考 [CustomManager](../../app/src/main/java/com/example/gsyvideoplayer/video/manager/CustomManager.java)。\n","skills/08-gsy-subtitle-and-preview/SKILL.md":"---\nname: gsy-subtitle-and-preview\ndescription: 外挂/内嵌字幕（SRT/VTT）、Exo 内嵌字幕轨、Seek 悬浮预览缩略图（VTT thumbnails）。\nwhen_to_use: 需要显示 srt/vtt/ass 字幕、切换字幕轨、seek 时鼠标/手指浮层展示预览帧。\ninputs:\n  - name: subtitle_source\n    description: 字幕文件路径或 URL、MIME\n    required: false\n  - name: preview_source\n    description: VTT 预览描述文件\n    required: false\noutputs:\n  - player 上叠加 subtitle view；seekbar 拖动出现预览帧。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview\n  - file: app/src/main/java/com/example/gsyvideoplayer/SubtitleDetailPlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitlePlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/PreViewGSYVideoPlayer.java\n---\n\n# gsy-subtitle-and-preview\n\n## 字幕 API\n\n顶层控制器：[GSYSubtitleController](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleController.java)。视图：[GSYSubtitleView](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleView.java)。\n\n| 方法 | 说明 |\n|---|---|\n| `new GSYSubtitleController(context, subtitleView)` | 绑定 UI |\n| `setSources(List<GSYSubtitleSource>)` | 一次注入多轨字幕 |\n| `setSource(GSYSubtitleSource)` | 只有一条 |\n| `getSources()` | 只读 List |\n| `selectSubtitle(String id)` | 按 id 切换字幕，\"\" 或 null 关闭 |\n| `setStyle(GSYSubtitleStyle)` | 字体/描边/背景/颜色 |\n| `setOffsetMs(long)` | 音画/字幕 offset |\n| `setEnabled(boolean)` | 开关 |\n| `updatePosition(long positionMs)` | 由 player 进度驱动 |\n| `release()` | 释放 |\n\n## `GSYSubtitleSource` 字段\n\n见 [GSYSubtitleSource](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleSource.java)：\n\n| 字段 | 说明 |\n|---|---|\n| `id` | 唯一 id |\n| `label` | 显示名 |\n| `language` | ISO code |\n| `mime` | 见 [GSYSubtitleMime](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleMime.java)：`text/vtt`、`application/x-subrip`（srt）、`text/plain` |\n| `uri` | http 或 file uri |\n| `embedded` | 是否内嵌轨（Exo 场景） |\n\n## 内置解析器\n\n- SRT：[GSYSrtSubtitleParser](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSrtSubtitleParser.java)\n- WebVTT：[GSYWebVttSubtitleParser](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYWebVttSubtitleParser.java)\n- 自定义 MIME：注册到 [GSYSubtitleParserFactory](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleParserFactory.java)\n\n## Exo 内嵌字幕\n\n`gsyvideoplayer-exo_player2` 的 Manager 会把 Exo 的 subtitle track 通过 `GSYSubtitleController` 上抛。Demo：[GSYExoSubTitlePlayer](../../app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitlePlayer.java) + [GSYExoSubTitleVideoManager](../../app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitleVideoManager.java)。\n\n## Seek 预览 API\n\n顶层：[GSYVideoPreviewProvider](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview/GSYVideoPreviewProvider.java) 与列表实现 [GSYVideoPreviewListProvider](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview/GSYVideoPreviewListProvider.java)。\n\n| 方法 | 说明 |\n|---|---|\n| `GSYVideoPreviewProvider#getPreviewFrame(long positionMs)` | 返 `GSYVideoPreviewFrame`，含 image url + crop 坐标 |\n| `GSYVideoPreviewListProvider(List<GSYVideoPreviewFrame>)` | 直接注入帧列表（自己解析或用 VTT） |\n| `GSYVideoPreviewVttParser#parse(String vttContent, String baseUrl)` | 解析 WebVTT thumbnails（雪碧图 + 时间段） |\n| `GSYVideoPreviewVttParser#parseFrames(String vttContent, String baseUrl)` | 只要帧列表 |\n\n## `GSYVideoPreviewFrame` 字段\n\n见 [GSYVideoPreviewFrame](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview/GSYVideoPreviewFrame.java)：`startTimeMs`、`endTimeMs`、`imageUrl`、`cropX/Y/Width/Height`、`hasCrop()`。\n\n## 关键调用序（VTT 预览 + SRT 字幕）\n\n1. 加载 subtitle：`controller.setSource(new GSYSubtitleSource(\"cn\", \"中文\", \"zh\", GSYSubtitleMime.SRT, uri, false))`;\n2. 播放进度回调（`GSYVideoProgressListener`）中 `controller.updatePosition(current)`；\n3. 预览：`GSYVideoPreviewVttParser.parse(vttText, base)` → 得到 Provider → 注入到 `PreViewGSYVideoPlayer#setPreviewProvider(provider)`；\n4. seekbar 拖动时 SDK 会自动调 `provider.getPreviewFrame(target)` 并做 crop。\n\n## Demo 对照\n\n- SRT 字幕：[SubtitleDetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/SubtitleDetailPlayer.java)（对应 `raw/demo_subtitle.srt` / `demo_subtitle_vtt.vtt`）\n- Exo 内嵌字幕：[GSYExoSubTitleDetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitleDetailPlayer.java)\n- 预览缩略图：[PreViewGSYVideoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/PreViewGSYVideoPlayer.java) + [PlayPickActivity](../../app/src/main/java/com/example/gsyvideoplayer/PlayPickActivity.java)\n\n## 常见坑\n\n- SRT 时间轴带 BOM 或换行不规范会直接被 parser 丢弃；先用 UTF-8 无 BOM 保存。\n- VTT 缩略图 URL 相对路径必须传 `baseUrl` 才能拼绝对路径。\n- `updatePosition` 必须在 UI 线程或至少与 `subtitleView` 同线程；`GSYSubtitleController` 内部无线程切换。\n","skills/02-gsy-option-builder/SKILL.md":"---\nname: gsy-option-builder\ndescription: 用 GSYVideoOptionBuilder 一次性把播放器所有可调项配好；等价于对 StandardGSYVideoPlayer 挨个 setter 但更紧凑。\nwhen_to_use: 需要开启循环 / 变速 / 边播边缓 / 触摸手势 / 自动旋转 / 封面 / 底部进度条样式 / 全屏动画 等多个开关时；或在多处播放页复用同一份配置。\ninputs:\n  - name: player\n    description: StandardGSYVideoPlayer / GSYBaseVideoPlayer 实例\n    required: true\noutputs:\n  - 已按 builder 状态 build 完成的 player，可直接 startPlayLogic\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseActivityDetail.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java\n---\n\n# gsy-option-builder\n\n## API 总览\n\n入口：[GSYVideoOptionBuilder](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java)。\n\n链式调用，最后 `build(player)` 把状态灌到具体 `GSYBaseVideoPlayer`。\n\n```java\nnew GSYVideoOptionBuilder()\n    .setUrl(url)\n    .setCacheWithPlay(true)\n    .setVideoTitle(title)\n    .setIsTouchWiget(true)\n    .setRotateViewAuto(false)\n    .setLockLand(false)\n    .setLooping(false)\n    .setSpeed(1.0f)\n    .setSeekOnStart(0)\n    .setVideoAllCallBack(callback)\n    .build(player);\n```\n\n## 完整参数表（按 [源文件行号](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) 对齐）\n\n| Setter | 类型 | 默认 | 用途 |\n|---|---|---|---|\n| `setUrl(String)` [#L376](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | String | null | 播放地址 |\n| `setVideoTitle(String)` [#L386](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | String | null | 顶部标题 |\n| `setMapHeadData(Map)` [#L437](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | `Map<String,String>` | null | HTTP header |\n| `setOverrideExtension(String)` [#L561](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | String | null | 强制 Exo 用哪种解封装器（`m3u8`/`mpd`/`ism`） |\n| `setCacheWithPlay(boolean)` [#L396](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 边播边缓 |\n| `setCachePath(File)` [#L427](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | File | null | 缓存目录 |\n| `setLooping(boolean)` [#L202](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 循环 |\n| `setSpeed(float)` [#L237](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | float | 1.0 | 播放速率，0.25~3.0 常用 |\n| `setSoundTouch(boolean)` [#L246](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 变速不变调 |\n| `setSeekOnStart(long)` [#L366](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | long ms | -1 | 起播 seek 位置 |\n| `setSeekRatio(float)` [#L324](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | float | 1.0 | 触摸横滑与进度的比例 |\n| `setStartAfterPrepared(boolean)` [#L406](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 首帧就绪后自动播；`false` 用于\"手动 start\" |\n| `setReleaseWhenLossAudio(boolean)` [#L417](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 丢失音频焦点是否 release |\n| `setIsTouchWiget(boolean)` [#L263](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 非全屏是否响应触摸手势 |\n| `setIsTouchWigetFull(boolean)` [#L272](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 全屏是否响应触摸手势 |\n| `setDismissControlTime(int ms)` [#L543](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | int | 2500 | 控制条自动隐藏时间 |\n| `setNeedShowWifiTip(boolean)` [#L281](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 移动网络提示 |\n| `setRotateViewAuto(boolean)` [#L221](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 自动旋转全屏；关闭后配合 `OrientationUtils` |\n| `setRotateWithSystem(boolean)` [#L337](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 是否跟随系统旋转设置 |\n| `setLockLand(boolean)` [#L229](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 全屏是否锁横屏 |\n| `setAutoFullWithSize(boolean)` [#L184](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 竖屏视频自动竖屏全屏 |\n| `setOnlyRotateLand(boolean)` [#L567](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 只允许横屏方向变化 |\n| `setShowFullAnimation(boolean)` [#L194](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 全屏过渡动画 |\n| `setHideKey(boolean)` [#L254](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 全屏隐藏虚拟按键 |\n| `setFullHideActionBar(boolean)` [#L587](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 全屏隐藏 ActionBar |\n| `setFullHideStatusBar(boolean)` [#L592](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 全屏隐藏 StatusBar |\n| `setNeedLockFull(boolean)` [#L525](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 全屏显示屏幕锁按钮 |\n| `setLockClickListener(LockClickListener)` [#L533](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 接口 | null | 屏幕锁点击回调 |\n| `setEnlargeImageRes(int)` [#L291](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | drawableRes | -1 | 全屏按钮图标 |\n| `setShrinkImageRes(int)` [#L301](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | drawableRes | -1 | 退出全屏按钮图标 |\n| `setThumbImageView(View)` [#L464](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | View | null | 封面容器（`ImageView` 或子布局） |\n| `setThumbPlay(boolean)` [#L516](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 点击封面即播 |\n| `setShowPauseCover(boolean)` [#L314](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 暂停时显示最后一帧 |\n| `setBottomProgressBarDrawable(Drawable)` [#L482](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | Drawable | null | 底部长条进度条颜色 |\n| `setBottomShowProgressBarDrawable(Drawable, Drawable)` [#L473](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 2×Drawable | null | 交互进度条 & thumb |\n| `setDialogProgressBar(Drawable)` [#L499](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | Drawable | null | 触摸拖动进度条样式 |\n| `setDialogVolumeProgressBar(Drawable)` [#L490](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | Drawable | null | 触摸音量进度条 |\n| `setDialogProgressColor(int, int)` [#L507](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | int color | -11 | 触摸进度条高亮/普通颜色 |\n| `setShowDragProgressTextOnSeekBar(boolean)` [#L572](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 拖动时 seekbar 上气泡 |\n| `setEffectFilter(GSYVideoGLView.ShaderInterface)` [#L551](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | Shader | `NoEffect` | GL 滤镜（渲染必须 `GLSURFACE`） |\n| `setPlayTag(String)` [#L347](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | String | \"\" | 列表播放去重用 |\n| `setPlayPosition(int)` [#L356](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | int | -22 | 列表位置索引 |\n| `setSetUpLazy(boolean)` [#L582](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 延迟 setUp，`build` 后手动 `startPlayLogic` |\n| `setVideoAllCallBack(VideoAllCallBack)` [#L213](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 接口 | null | 全事件回调（prepared/complete/error…） |\n| `setGSYVideoProgressListener(GSYVideoProgressListener)` [#L455](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 接口 | null | 高频进度回调 |\n| `setGSYStateUiListener(GSYStateUiListener)` [#L600](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 接口 | null | UI 状态变化回调 |\n| `setSurfaceErrorPlay(boolean)` [#L446](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | Surface 异常时点击继续 |\n| `setNeedOrientationUtils(boolean)` [#L611](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 全屏是否内部构造 `OrientationUtils` |\n\n## 关键调用序\n\n1. 构造 `GSYVideoOptionBuilder` → 链式 setter；\n2. `builder.build(player)` 会自动 setUp（除非 `setSetUpLazy(true)`）；\n3. 需要在页面里同时接收回调 → 让 Activity `implements VideoAllCallBack`，直接 `setVideoAllCallBack(this)`。\n\n## Demo 对照\n\n- 详情页 + Builder 全套：[DetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java)\n- 结合 `GSYBaseActivityDetail` 的最短接入：[DetailControlActivity](../../app/src/main/java/com/example/gsyvideoplayer/DetailControlActivity.java) 与 [GSYBaseActivityDetail#L56-L61](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseActivityDetail.java)\n\n## 常见坑\n\n- `build(player)` 内部会调 `setUp`；若 `setSetUpLazy(true)` 需要自己再调 `startPlayLogic()`。\n- Effect 滤镜需要先 `GSYVideoType.setRenderType(GLSURFACE)`，否则 `setEffectFilter` 无效。\n- `setPlayTag` 与 `setPlayPosition` 是列表页避免\"上下滑滚 setUp 命中同 URL 不重播\"的关键。\n","skills/03-gsy-fullscreen-orientation/SKILL.md":"---\nname: gsy-fullscreen-orientation\ndescription: 处理进入/退出全屏、屏幕旋转、竖屏视频自适应、系统旋转设置联动、反向横屏。\nwhen_to_use: 出现\"全屏没进/没退\"、\"横屏方向反了\"、\"旋转跟随系统坏了\"、\"竖屏视频被横过来\"、\"抖动无限循环\"这类问题；或首次在详情页接入全屏。\ninputs:\n  - name: activity\n    description: 承载 player 的 Activity\n    required: true\n  - name: player\n    description: GSYBaseVideoPlayer 或其子类\n    required: true\noutputs:\n  - OrientationUtils 已绑定；点击\"全屏\"按钮可以正确进入 window fullscreen 副本；返回键与旋转都能正确复位。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationUtils.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationOption.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseActivityDetail.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java\n---\n\n# gsy-fullscreen-orientation\n\n## 概念\n\n- **视觉全屏**：`player.startWindowFullscreen(context, hideActionBar, hideStatusBar)`，会用反射克隆一个同类 player 塞进 `android.R.id.content` 顶部，返回原实例。\n- **屏幕方向**：`OrientationUtils` 监听 `OrientationEventListener` + `Settings.System.ACCELEROMETER_ROTATION`，负责旋转 Activity 方向。\n- **两者可解耦**：只想旋转不切副本 → `setNeedOrientationUtils(false)` + 自建 `OrientationUtils`；只想切副本不旋转 → 不构造 `OrientationUtils`，`player` 保持默认 `setRotateViewAuto(false)`。\n\n## 核心 API\n\n| 类 / 方法 | 位置 | 说明 |\n|---|---|---|\n| `OrientationUtils(Activity, GSYBaseVideoPlayer)` | [OrientationUtils#L50-L52](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationUtils.java) | 主构造 |\n| `OrientationUtils(Activity, player, OrientationOption)` | [OrientationUtils#L54-L64](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationUtils.java) | 支持自定义旋转角度阈值 |\n| `setEnable(boolean)` | `OrientationUtils` | 使能/失能自动旋转 |\n| `setRotateWithSystem(boolean)` | `OrientationUtils` | 是否跟随系统\"旋转开关\" |\n| `setIsOnlyRotateLand(boolean)` | `OrientationUtils` | true 时仅在横屏方向变化时响应 |\n| `resolveByClick()` | `OrientationUtils` | 用户点全屏按钮时手动触发一次旋转 |\n| `backToProtVideo()` | `OrientationUtils` | 返回键触发时把方向复位竖屏 |\n| `setIsPause(boolean)` | `OrientationUtils` | 生命周期 pause/resume 桥接 |\n| `releaseListener()` | `OrientationUtils` | `onDestroy` 释放监听 |\n| `startWindowFullscreen(Context, boolean, boolean)` | [GSYBaseVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java) | 进入 window 全屏，返回克隆体 |\n| `GSYVideoManager.backFromWindowFull(Context)` | [GSYVideoManager#L82-L94](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java) | 返回键统一处理 |\n| `GSYVideoManager.isFullState(Activity)` | [GSYVideoManager#L143-L151](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java) | 当前是否全屏 |\n\n## `OrientationOption` 参数\n\n| 字段 | 默认 | 作用 |\n|---|---|---|\n| `normalPortraitAngleStart / End` | 5 / 355 | 判定竖屏区间 |\n| `normalLandAngleStart / End` | 85 / 95 | 判定正向横屏区间 |\n| `reverseLandAngleStart / End` | 265 / 275 | 判定反向横屏区间 |\n\n区间越大越\"灵敏\"，太大会抖动。\n\n## 关键调用序（详情页最佳实践）\n\n1. `onCreate`：`orientationUtils = new OrientationUtils(this, player)`; `orientationUtils.setEnable(false)`（进页面不自动旋转）\n2. 全屏按钮点击：`orientationUtils.resolveByClick()` → `player.startWindowFullscreen(this, true, true)`\n3. `onBackPressed`：`orientationUtils.backToProtVideo()` → `GSYVideoManager.backFromWindowFull(this)`\n4. `onPause`：`orientationUtils.setIsPause(true)`\n5. `onResume`：`orientationUtils.setIsPause(false)`\n6. `onDestroy`：`orientationUtils.releaseListener()`\n\n`GSYBaseActivityDetail` 已把 1/2/3/4/5/6 全套代码写好，直接继承最省事。\n\n## Demo 对照\n\n- 最标准的全屏 + 旋转：[DetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java)\n- 只旋转不切副本：[LandLayoutVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/LandLayoutVideo.java) + [DetailNormalActivityPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailNormalActivityPlayer.java)\n- 反射克隆入口：[GSYBaseVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java)（`startWindowFullscreen` → `getConstructor(Context.class, Boolean.class)`）\n\n## v14.0.0 折叠屏示例\n\nXML [FoldDetailActivity](../../app/src/main/java/com/example/gsyvideoplayer/FoldDetailActivity.java) 与 Compose [FoldComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/FoldComposeActivity.kt) 按 FoldingFeature 实现 BOOK/TABLETOP 分屏、铰链分隔与全屏。XML 全屏克隆布局通过 `LandLayoutVideo` 传递姿态；普通全屏 API 不会自动给任意业务页面添加折叠布局。用法及测试注入边界见 [RECENT_FEATURES.md](../../doc/RECENT_FEATURES.md)。\n\n## 常见坑\n\n- **黑屏一瞬**：`startWindowFullscreen` 用反射构造新 player，若你的子类只暴露 `(Context)` 构造且未加 `(Context, Boolean)`，R8 混淆后必炸。ProGuard 里必须保留：`-keep class * extends GSYBaseVideoPlayer { public <init>(android.content.Context); public <init>(android.content.Context, java.lang.Boolean); }`\n- **旋转停不下来**：忘了 `orientationUtils.setIsPause(true)` 或 `releaseListener()`。\n- **系统关了自动旋转但还想手动横屏**：`setRotateWithSystem(false)`。\n- **竖屏视频被强制横屏**：`setAutoFullWithSize(true)` 让 SDK 自适应。\n","skills/01-gsy-quickstart/SKILL.md":"---\nname: gsy-quickstart\ndescription: 从零把 GSYVideoPlayer 塞进一个 Activity 或 Fragment，跑通「setUp → startPlayLogic → release」最小闭环。\nwhen_to_use: 用户第一次在项目里集成 GSYVideoPlayer；只要能播一个 mp4 就行；或需要确认基础生命周期是否接对。\ninputs:\n  - name: play_url\n    description: 视频地址（http/https/file/asset/rtmp/rtsp）\n    required: true\n  - name: container\n    description: 承载 View 的容器（一般是 FrameLayout）\n    required: true\noutputs:\n  - StandardGSYVideoPlayer 实例已 setUp 并可控（play/pause/release）\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/PlayActivity.java\n---\n\n# gsy-quickstart\n\n## 依赖\n\n- 推荐整包：`io.github.carguo:gsyvideoplayer:14.0.0`（已含 Java、Exo2、三 ABI ex_so）。\n- 分模块主库：`io.github.carguo:gsyvideoplayer-java:14.0.0`。\n- 分模块 IJK so：选 `io.github.carguo:gsyvideoplayer-ex_so:14.0.0`，或所需独立 artifact `gsyvideoplayer-arm64` / `gsyvideoplayer-armv7a` / `gsyvideoplayer-x64`，不重复引入同 ABI 两套库。\n- 默认 ABI 为 `arm64-v8a` / `armeabi-v7a` / `x86_64`（FFmpeg n5.1.10 / OpenSSL 3.5.9）；旧 armv5/x86 未升级，仅旧架构项目显式使用。\n- 权限：`INTERNET`、`WAKE_LOCK`、`ACCESS_NETWORK_STATE`（AndroidManifest）\n\n明细见 [doc/DEPENDENCIES.md](../../doc/DEPENDENCIES.md)。\n\n## 核心 API\n\n| 类 / 方法 | 位置 | 作用 |\n|---|---|---|\n| `StandardGSYVideoPlayer(Context)` | [StandardGSYVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java) | 主用 UI 组件，默认布局在 `R.layout.video_layout_standard` |\n| `setUp(url, cacheWithPlay, title)` | [GSYVideoView#L438-L451](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 最短形式的地址绑定 |\n| `setUp(url, cacheWithPlay, cachePath, title)` | [GSYVideoView#L477-L488](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 支持自定义缓存目录 |\n| `setUp(url, cacheWithPlay, cachePath, headers, title)` | [GSYVideoView#L453-L475](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 附带 HTTP header |\n| `startPlayLogic()` | `StandardGSYVideoPlayer`（继承自 `GSYVideoControlView`） | 触发 prepare→start 全流程 |\n| `getCurrentPlayer()` | `GSYBaseVideoPlayer` | 获取\"当前活跃 player\"（可能是全屏克隆体） |\n| `onVideoPause()` / `onVideoResume()` / `onVideoResume(boolean seek)` | [GSYVideoView#L518-L577](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 对应 Activity `onPause / onResume`；直播场景传 `seek=false` |\n| `release()` | [GSYVideoView#L818](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 释放当前 Player 与 Surface |\n| `GSYVideoManager.releaseAllVideos()` | [GSYVideoManager#L99-L104](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java) | 全局收尾（切页面时兜底） |\n| `GSYVideoManager.onPause() / onResume()` | [GSYVideoManager#L110-L135](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java) | 全局暂停/恢复所有 GSY player |\n\n## 参数表：`setUp(...)`\n\n| 参数 | 类型 | 说明 |\n|---|---|---|\n| `url` | `String` | 支持 http/https/file/asset/rtmp/rtsp/自定义 scheme；`ProxyCacheManager` 会自动拦截可缓存的普通 http |\n| `cacheWithPlay` | `boolean` | true → 边播边缓；对直播 / m3u8 / DRM 建议 false |\n| `cachePath` | `File?` | 自定义缓存目录，默认 `context.cacheDir/videoCache` |\n| `mapHeadData` | `Map<String,String>?` | HTTP 请求头（Referer、Cookie、Auth 等） |\n| `title` | `String` | 顶部标题栏文案，会显示在 `R.id.title` |\n\n返回值：`boolean`，false 表示 URL 为空或与当前 tag 相同（防重复 setup）。\n\n## 最小调用序（Activity 生命周期）\n\n1. `onCreate` → `player.setUp(...)` → 可选 `player.setThumb...`、`setPlayTag()` → `player.startPlayLogic()`\n2. `onPause` → `player.getCurrentPlayer().onVideoPause()`\n3. `onResume` → `player.getCurrentPlayer().onVideoResume()`（直播传 `false`）\n4. `onBackPressed` → 若在全屏：`GSYVideoManager.backFromWindowFull(this)` 返回 true 就吞掉事件\n5. `onDestroy` → `player.getCurrentPlayer().release()` + `GSYVideoManager.releaseAllVideos()`\n\n## Demo 对照\n\n- 最短一屏：[PlayActivity](../../app/src/main/java/com/example/gsyvideoplayer/PlayActivity.java)\n- Fragment：[VideoFragment](../../app/src/main/java/com/example/gsyvideoplayer/fragment/VideoFragment.java) + [FragmentVideoActivity](../../app/src/main/java/com/example/gsyvideoplayer/FragmentVideoActivity.java)\n- 空控件模式（自绘 UI）：[PlayEmptyControlActivity](../../app/src/main/java/com/example/gsyvideoplayer/PlayEmptyControlActivity.java) + [EmptyControlVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/EmptyControlVideo.java)\n\n## 常见坑\n\n- 忘记 `release()` → dex 泄漏 + surface 崩溃：一定在 `onDestroy` 兜底。\n- 不覆盖 `getCurrentPlayer()` 直接调 `player.onVideoPause()`：全屏切回后指令发到\"影子\"实例，导致状态错乱。\n- `setUp` 传空 title 走 `null`，会隐藏顶部栏（`View.GONE`），不会崩。\n","skills/11-gsy-compose-integration/SKILL.md":"---\nname: gsy-compose-integration\ndescription: 在 Jetpack Compose 中通过 AndroidView 桥接 GSYVideoPlayer，覆盖列表、竖直短视频、生命周期管控。\nwhen_to_use: 项目已经或正在迁移到 Compose；需要在可组合项里嵌入播放器；需要复用列表滑动/自动播放逻辑但 UI 用 Compose。\ninputs:\n  - name: url\n    description: 视频 URL\n    required: true\noutputs:\n  - Composable 内嵌 GSYVideoPlayer，生命周期与 Composable 一致。\nreferences:\n  - file: gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose\n  - file: app/src/main/java/com/example/gsyvideoplayer/compose\n---\n\n# gsy-compose-integration\n\n## 模块\n\n`gsyVideoPlayer-compose` 是 Compose 桥接层，两种模式并存：\n\n| 模式 | 入口 | 说明 |\n|---|---|---|\n| **Wrapper**（推荐上手） | [GSYVideoPlayerView](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/wrapper/GSYVideoPlayerView.kt) / [GSYAnyVideoPlayerView](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/wrapper/GSYAnyVideoPlayerView.kt) | `AndroidView` 直接承载 `StandardGSYVideoPlayer` / 任意 `NormalGSYVideoPlayer` 子类，保留全部内置能力 |\n| **Native**（纯 Compose 控件） | [rememberGSYPlayerController](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerSurface.kt) + [GSYPlayerSurface](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerSurface.kt) + [GSYDefaultControls](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYDefaultControls.kt) | 只承载画面，控制层完全用 Compose 自绘 |\n| **通用工具** | [LifecycleBridge](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/common/LifecycleBridge.kt) | `LifecycleEffect { event -> ... }` 用于订阅宿主 Lifecycle |\n\n## Wrapper API：`GSYVideoPlayerView`\n\n见 [GSYVideoPlayerView#L28-L36](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/wrapper/GSYVideoPlayerView.kt)：\n\n| 参数 | 类型 | 说明 |\n|---|---|---|\n| `modifier` | `Modifier` | 通常 `Modifier.fillMaxWidth().aspectRatio(16f/9)` |\n| `setUp` | `(StandardGSYVideoPlayer) -> Unit` | 等价 `GSYVideoOptionBuilder.build(player)` |\n| `setUpKey` | `Any?` = null | 非空时其变化会重新调用 `setUp`（典型：传 url） |\n| `autoReleaseOnDispose` | Boolean = true | 离屏自动 `player.release()` |\n| `autoPauseResume` | Boolean = true | 自动桥 Lifecycle：`ON_PAUSE→GSYVideoManager.onPause()`、`ON_RESUME→onResume()` |\n| `onPlayerCreated` | `(StandardGSYVideoPlayer) -> Unit` | 拿到 player 实例的回调 |\n\n## Wrapper API：`GSYAnyVideoPlayerView<T : NormalGSYVideoPlayer>`\n\n见 [GSYAnyVideoPlayerView#L26-L35](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/wrapper/GSYAnyVideoPlayerView.kt)。相比 `GSYVideoPlayerView` 多出：\n\n| 参数 | 类型 | 说明 |\n|---|---|---|\n| `factory` | `(Context) -> T` | 传自定义子类（如 `ListGSYVideoPlayer`、`SampleControlVideo`、`DanmakuVideoPlayer`） |\n\n## Native API：`rememberGSYPlayerController`\n\n见 [rememberGSYPlayerController#L26-L32](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerSurface.kt)：\n\n| 参数 | 默认 | 说明 |\n|---|---|---|\n| `url` | null | 非空时内部 `LaunchedEffect` 触发 `controller.setUp(...)` |\n| `cacheWithPlay` | false | 边播边缓 |\n| `title` | \"\" | 标题（透传给底层 player） |\n| `autoPlay` | false | attach 完成后自动 `startPlayLogic` |\n| `autoPauseResume` | true | 自动订阅 Lifecycle（同 wrapper） |\n\n返回 [GSYPlayerController](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerController.kt)，可以再调 `.play() / .pause() / .seekTo(ms) / .setSpeed(...) / .changeTextureViewShowType() / .setUp(url, cache, title, autoPlay) / .dispose()`；状态订阅走 [GSYPlayerState](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerState.kt)（`StateFlow<GSYPlayerState>`），事件走 [GSYPlayerEvent](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerEvent.kt)。\n\n## 最小可运行示例\n\n**Wrapper 模式**：\n\n```kotlin\nGSYVideoPlayerView(\n    modifier = Modifier.fillMaxWidth().aspectRatio(16f/9),\n    setUpKey = url,\n    setUp = { player ->\n        GSYVideoOptionBuilder()\n            .setUrl(url)\n            .setCacheWithPlay(true)\n            .setVideoTitle(\"demo\")\n            .setIsTouchWiget(true)\n            .setAutoFullWithSize(true)\n            .build(player)\n        player.startPlayLogic()\n    }\n)\n```\n\n**Native 模式**：\n\n```kotlin\nval controller = rememberGSYPlayerController(url = url, autoPlay = true)\nBox(Modifier.fillMaxWidth().aspectRatio(16f/9)) {\n    GSYPlayerSurface(controller = controller, modifier = Modifier.matchParentSize())\n    GSYDefaultControls(controller = controller)\n}\n```\n\n## 生命周期最佳实践\n\n```kotlin\nval lifecycle = LocalLifecycleOwner.current.lifecycle\nDisposableEffect(lifecycle) {\n    val obs = LifecycleEventObserver { _, e ->\n        when (e) {\n            Lifecycle.Event.ON_PAUSE  -> state.pause()\n            Lifecycle.Event.ON_RESUME -> state.resumeIfNeeded()\n            Lifecycle.Event.ON_DESTROY-> state.release()\n            else -> Unit\n        }\n    }\n    lifecycle.addObserver(obs)\n    onDispose { lifecycle.removeObserver(obs) }\n}\n```\n\n## Demo 对照\n\n| 场景 | Activity |\n|---|---|\n| 原生桥接示例 | [DetailNativeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/DetailNativeActivity.kt) / [BasicWrapperActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/BasicWrapperActivity.kt) / [FullFeatureNativeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/FullFeatureNativeActivity.kt) |\n| 广告 + 主片（列表内） | [AdInListComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/AdInListComposeActivity.kt) |\n| 列表 + 全屏 | [ListWithFullscreenActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/ListWithFullscreenActivity.kt) / [ListPlayNativeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/ListPlayNativeActivity.kt) / [AutoPlayListActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/AutoPlayListActivity.kt) |\n| 抖音式竖屏 | [VerticalShortVideoComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/VerticalShortVideoComposeActivity.kt) |\n| 滤镜 | [DetailFilterComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/DetailFilterComposeActivity.kt) |\n| 无缝切源 / Exo 切源 | [SwitchSeamlessComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/SwitchSeamlessComposeActivity.kt) / [ExoSwitchSourceComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/ExoSwitchSourceComposeActivity.kt) / [SwitchUrlActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/SwitchUrlActivity.kt) |\n| 字幕 | [SubtitleComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/SubtitleComposeActivity.kt) |\n| 弹幕 | [DanmakuComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/DanmakuComposeActivity.kt) |\n| 缓存 / 下载 | [CacheDownloadComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/CacheDownloadComposeActivity.kt) |\n| 硬解 / MediaCodec | [MediaCodecComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/MediaCodecComposeActivity.kt) |\n| 悬浮小窗 | [FloatingWindowComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/FloatingWindowComposeActivity.kt) |\n| 音频独立 | [AudioOnlyComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/AudioOnlyComposeActivity.kt) |\n| 多类型混排 | [MoreTypeComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/MoreTypeComposeActivity.kt) |\n| 多窗口并行 | [MultiWindowActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/MultiWindowActivity.kt) / [MultiWindowParallelComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/MultiWindowParallelComposeActivity.kt) |\n| 本地文件 | [LocalFileComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/LocalFileComposeActivity.kt) |\n| 自定义主题 | [CustomControlsThemeComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/CustomControlsThemeComposeActivity.kt) |\n| WebView 详情 | [WebDetailComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/WebDetailComposeActivity.kt) |\n\n样例 hosts 全在 [compose/host/](../../app/src/main/java/com/example/gsyvideoplayer/compose/host)。\n\n## v14.0.0 状态与接入\n\n外部坐标为 `io.github.carguo:gsyvideoplayer-compose:14.0.0`（GitHub Packages 使用 `com.shuyu`）。IJK 使用方还需选择 `ex_so` 或独立 ABI 原生模块；完整组合见 [DEPENDENCIES.md](../../doc/DEPENDENCIES.md)。\n\n`detachHost` 只解绑 View 侧回调/tick，`dispose` 统一释放保留的 host；`snapshot.bufferPercent` 合并内核轮询与回调值。`controller.changeTextureViewShowType()` 在主线程更新当前内嵌/全屏比例。Compose Demo 共 25 项，新增折叠屏 BOOK/TABLETOP。详见 [COMPOSE_USE.md](../../doc/COMPOSE_USE.md)。\n\n## 常见坑\n\n- Compose 频繁重组 → `AndroidView` 的 `factory` 只跑一次；不要把 `setUp` 放在 `factory` 里，放到 `update` block，并用 `remember(url)` 做去重。\n- `AndroidView` 内部布局要指定固定高度或 `aspectRatio`，否则 SurfaceView 会 0 高度导致黑屏。\n- 竖直短视频 + `Pager`：切页时用 `pagerState.currentPage` 触发 `state.play(url) / previousState.pause()`；释放放在 `DisposableEffect(pagerState)` 里。\n- 与 Compose 主题深浅色切换：`GSYVideoPlayerCompose` 是原生 View 层，配色需要单独 `setBottomProgressBarDrawable` 等 API。\n","skills/10-gsy-cast-and-loopback/SKILL.md":"---\nname: gsy-cast-and-loopback\ndescription: 使用内核层投屏 SPI（CastCapability + CastProvider）接入 DLNA，或做本地 loopback 回环模拟被投屏端。\nwhen_to_use: 需要接入 DLNA/AirPlay/Chromecast 到已有播放页；做投屏发起端 UI；做集成测试的本地 loopback 接收端；或做多协议聚合发现。\ninputs:\n  - name: providers\n    description: 一个或多个 CastProvider 实例（DLNA 默认由 jUPnP 提供）\n    required: true\noutputs:\n  - CastCapability 单例已注册 provider，可发现设备、连接、控制播放/停止。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastProvider.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastSession.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastListener.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastMediaInfo.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastState.java\n  - file: gsyVideoPlayer-cast/src/main/java/com/shuyu/gsyvideoplayer/cast/dlna\n  - file: app/src/main/java/com/example/gsyvideoplayer/CastDemoActivity.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/cast\n  - file: doc/CAST_FEATURE_PLAN.md\n---\n\n# gsy-cast-and-loopback\n\n## 顶层架构\n\n```\nCastCapability (单例)  ← 主线程 API 入口\n  ├─ registerProvider(CastProvider)\n  ├─ startDiscovery(Context) / stopDiscovery()\n  ├─ getAvailableDevices(): List<CastDevice>\n  ├─ connect(device, ConnectCallback) : CastSession（异步）\n  ├─ addListener(CastListener)\n  └─ disconnect()\n```\n\n对齐 Media3 `Player` 语义：`CastCapability ≈ Player 池`、`activeSession ≈ 当前 Player`、`CastProvider ≈ 渲染后端`、`CastListener/SessionListener ≈ Player.Listener`。见 [CastCapability](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) 与 [CastProvider](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastProvider.java) 的头 JavaDoc。\n\n## 核心 API\n\n### CastCapability\n\n| 方法 | 位置 | 说明 |\n|---|---|---|\n| `getInstance()` | [#L38-L47](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | DCL 单例 |\n| `registerProvider(CastProvider)` | [#L85-L90](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 注册；`protocol` 相同则覆盖 |\n| `unregisterProvider(String)` | [#L93-L103](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 反注册并停止其发现 |\n| `startDiscovery(Context)` | [#L106-L125](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 所有已注册 provider 同时开始发现 |\n| `stopDiscovery()` | [#L128-L135](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 全部停止 |\n| `getAvailableDevices()` | [#L138-L146](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 快照，跨 provider 聚合 |\n| `connect(CastDevice, ConnectCallback)` | [#L154-L183](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 会自动 disconnect 旧会话 |\n| `disconnect()` | [#L186-L196](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | release 当前 session |\n| `getActiveSession()` | [#L199-L201](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 拿当前会话（可能 null） |\n| `addListener / removeListener` | [#L203-L209](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 主线程 listener |\n\n### CastProvider（SPI 契约）\n\n| 方法 | 说明 |\n|---|---|\n| `getProtocol(): String` | 全小写协议 id（`dlna` / `chromecast` / `airplay`） |\n| `startDiscovery(Context, DiscoveryListener)` | 后台线程订阅，回调 postMain |\n| `stopDiscovery()` | 释放订阅、解绑 Service |\n| `connect(CastDevice, ConnectCallback): CastSession` | 建立会话；同步返回 session，异步回调结果 |\n\n线程约定：所有 network IO 由 provider 自己后台跑，回调都 postMain。\n\n### CastSession（媒体控制）\n\n见 [CastSession](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastSession.java)：\n\n| 方法 | 说明 |\n|---|---|\n| `load(CastMediaInfo)` | 推送要投的 URL |\n| `play() / pause() / stop()` | 播控 |\n| `seekTo(long ms)` | 定位 |\n| `setVolume(int 0-100)` / `setMute(boolean)` | 音量 |\n| `getState(): CastState` | IDLE / CONNECTING / CONNECTED / LOADING / PLAYING / PAUSED / STOPPED / ERROR，见 [CastState](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastState.java) |\n| `addListener(SessionListener)` | 位置/状态回调 |\n| `release()` | 归还 |\n\n### CastMediaInfo（要投的媒体）\n\n见 [CastMediaInfo](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastMediaInfo.java)。字段：`url`、`title`、`durationMs`、`mimeType`、`headers`、`metadata`（`Map`）。\n\n## DLNA 参考实现\n\n模块 `gsyvideoplayer-cast` 提供 [JupnpDlnaProvider](../../gsyVideoPlayer-cast/src/main/java/com/shuyu/gsyvideoplayer/cast/dlna/JupnpDlnaProvider.java) 与 [JupnpDlnaSession](../../gsyVideoPlayer-cast/src/main/java/com/shuyu/gsyvideoplayer/cast/dlna/JupnpDlnaSession.java)。业务代码调用：\n\n```java\nCastCapability cap = CastCapability.getInstance();\ncap.registerProvider(new JupnpDlnaProvider());\ncap.addListener(myListener);\ncap.startDiscovery(context);\n```\n\n发现事件：`CastListener#onDeviceListChanged(List<CastDevice>)`；连接：`cap.connect(devices.get(0), connectCallback)`。\n\n## 本地 Loopback（Demo 测试用）\n\napp 模块下的 [cast/](../../app/src/main/java/com/example/gsyvideoplayer/cast) 提供**可发现的接收端**，方便在同一台设备上完成集成测试：\n\n- [DevReceiverService](../../app/src/main/java/com/example/gsyvideoplayer/cast/DevReceiverService.java) —— foreground service，通过 jUPnP 注册\"回环设备\"。\n- [LoopbackDeviceFactory](../../app/src/main/java/com/example/gsyvideoplayer/cast/LoopbackDeviceFactory.java) —— 构造 UPnP MediaRenderer。\n- [LoopbackAvTransportService](../../app/src/main/java/com/example/gsyvideoplayer/cast/LoopbackAvTransportService.java) / [LoopbackRenderingControlService](../../app/src/main/java/com/example/gsyvideoplayer/cast/LoopbackRenderingControlService.java) —— AVTransport / RenderingControl 实现。\n- [CastReceiverFloatingWindow](../../app/src/main/java/com/example/gsyvideoplayer/cast/CastReceiverFloatingWindow.java) + [CastReceiverPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/CastReceiverPlayer.java) —— 可视化播放。\n\n## Demo 对照\n\n- 一体化：[CastDemoActivity](../../app/src/main/java/com/example/gsyvideoplayer/CastDemoActivity.java)\n- Cast 相关控制页：[SampleCastControlVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/SampleCastControlVideo.java)\n- 设计与测试：[doc/CAST_FEATURE_PLAN.md](../../doc/CAST_FEATURE_PLAN.md)、[doc/CAST_RECEIVER_DESIGN.md](../../doc/CAST_RECEIVER_DESIGN.md)、[doc/CAST_TEST_PLAYBOOK.md](../../doc/CAST_TEST_PLAYBOOK.md)\n\n## 常见坑\n\n- 不同 provider 上报的 `CastDevice` 需要用 `getProtocol()` 分组；直接把陌生 protocol 传给 `connect` 会拿到 `IllegalStateException`。\n- Provider 网络 IO 必须在自己线程，`CastCapability` 不做 main 切换。\n- Wi-Fi 多播锁没拿 → DLNA 发现不到设备：Provider 需要 `WifiManager.MulticastLock`。\n- Loopback 与真实设备同网段时会同时被发现，Demo 里有过滤示例。\n","skills/07-gsy-render-and-effects/SKILL.md":"---\nname: gsy-render-and-effects\ndescription: 三种渲染载体（TextureView / SurfaceView / GLSurfaceView）、显示比例（16:9/4:3/full/custom）、GL 滤镜与自定义 Shader。\nwhen_to_use: 想放竖屏视频、需要 GL 滤镜、要做全屏裁减/拉伸、SurfaceView 与动画冲突、想抓截图或 gif。\ninputs:\n  - name: render_type\n    description: TEXTURE / SURFACE / GLSURFACE\n    required: true\n  - name: show_type\n    description: 显示比例枚举\n    required: false\noutputs:\n  - 渲染载体 + 显示比例 + 滤镜均按需生效。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/GSYRenderView.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/view/GSYVideoGLView.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/effect\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailFilterActivity.java\n---\n\n# gsy-render-and-effects\n\n## 渲染类型 `GSYVideoType.setRenderType(int)`\n\n| 常量 | 值 | 说明 |\n|---|---|---|\n| `TEXTURE` | 0 | 默认，与动画/圆角/缩放兼容最好 |\n| `SURFACE` | 1 | 性能最好，但和 View 动画冲突 |\n| `GLSURFACE` | 2 | 必需，若要用 `setEffectFilter` |\n\n见 [GSYVideoType#L39-L49](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java)。设置时机：`setUp` 之前，全局生效。\n\n## 显示比例 `GSYVideoType.setShowType(int)`\n\n| 常量 | 值 | 说明 |\n|---|---|---|\n| `SCREEN_TYPE_DEFAULT` | 0 | 按视频原比例居中 |\n| `SCREEN_TYPE_16_9` | 1 | 强制 16:9 |\n| `SCREEN_TYPE_4_3` | 2 | 强制 4:3 |\n| `SCREEN_TYPE_18_9` | 6 | 全面屏 |\n| `SCREEN_TYPE_FULL` | 4 | 全屏裁减；`surface_container` 建议 `FrameLayout` |\n| `SCREEN_MATCH_FULL` | -4 | 全屏拉伸；`surface_container` 建议 `FrameLayout` |\n| `SCREEN_TYPE_CUSTOM` | -5 | 需先 `setScreenScaleRatio(float)` |\n\n见 [GSYVideoType#L10-L29](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java) 与 [GSYVideoType#L165-L167](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java)。\n\n## 硬解码开关\n\n| 静态方法 | 说明 |\n|---|---|\n| `enableMediaCodec()` / `disableMediaCodec()` | IJK 硬解码总开关 [#L71-L79](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java) |\n| `enableMediaCodecTexture()` / `disableMediaCodecTexture()` | 硬解 + Texture 直渲染优化 [#L100-L108](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java) |\n| `enableSmartMediaCodec()` / `disableSmartMediaCodec()` | 硬解失败自动软解回退 [#L86-L94](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java) |\n| `isMediaCodec()` / `isMediaCodecTexture()` / `isSmartMediaCodec()` | 查询 |\n\n设置时机：**`setUp` 之前**，且**部分机型**需要在 `Application.onCreate` 就配好。\n\n## GL 滤镜\n\n需要满足两个条件：\n1. `GSYVideoType.setRenderType(GSYVideoType.GLSURFACE)`\n2. `builder.setEffectFilter(new XxxEffect())` 或 `player.setEffectFilter(...)`\n\nSDK 内置滤镜（在 `com.shuyu.gsyvideoplayer.render.effect.*`）：`AutoFixEffect`、`BarrelBlurEffect`、`BlackAndWhiteEffect`、`BrightnessEffect`、`ContrastEffect`、`CrossProcessEffect`、`DocumentaryEffect`、`DuotoneEffect`、`FillLightEffect`、`GammaEffect`、`GaussianBlurEffect`、`GrainEffect`、`GreyScaleEffect`、`HueEffect`、`InvertColorsEffect`、`LamoishEffect`、`NoEffect`、`OverlayEffect`、`PosterizeEffect`、`SampleBlurEffect`、`SaturationEffect`、`SepiaEffect`、`SharpnessEffect`、`TemperatureEffect`、`TintEffect`、`VignetteEffect`。目录：[render/effect](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/effect)。\n\n自定义 Shader：实现 [`GSYVideoGLView.ShaderInterface`](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/view/GSYVideoGLView.java)。Demo 里 4 个复杂样例：[GSYVideoGLViewCustomRender](../../app/src/main/java/com/example/gsyvideoplayer/effect/GSYVideoGLViewCustomRender.java) / 2 / 3 / 4，以及像素化 [PixelationEffect](../../app/src/main/java/com/example/gsyvideoplayer/effect/PixelationEffect.java)、BitmapOverlay [BitmapEffect](../../app/src/main/java/com/example/gsyvideoplayer/effect/BitmapEffect.java) / [BitmapIconEffect](../../app/src/main/java/com/example/gsyvideoplayer/effect/BitmapIconEffect.java)。\n\n## v14.0.0 多 pass 与新效果\n\n- `GSYVideoGLViewMultiPassRender` + `setMultiPassEffect` + `player.setCustomGLRenderer` 接入 `GaussianBlurMultiPassEffect` / `IterativeBlurPyramidEffect` / `BloomEffect`。这些不是单 pass `ShaderInterface` 滤镜，不能直接传给 `setEffectFilter`。\n- `LookupEffect(assetPath, intensity)` / `BeautyEffect(smoothLevel, whiteLevel)` / `GlitchEffect` / `CrtEffect` / `OldTvSignalEffect` 使用 `setEffectFilter`。LUT 必须由使用方放入自己的 assets；Demo 的三张图不在播放器 AAR 中。\n- `TextureShaderInterface` 在 GL 线程管理额外纹理；动态效果使用渲染器按秒绑定的 `uTime`。\n- 显示比例变化后，传统 View 调 `player.getCurrentPlayer().changeTextureViewShowType()`；Compose 调 `controller.changeTextureViewShowType()`。\n- 完整代码入口和实现文档见 [RECENT_FEATURES.md](../../doc/RECENT_FEATURES.md#v1400-gl-管线与新滤镜)。\n\n## 抓帧 / gif / 截图\n\n在 [GSYRenderView](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/GSYRenderView.java)：\n\n| 方法 | 回调 | 说明 |\n|---|---|---|\n| `taskShotPic(GSYVideoShotListener)` | 返 Bitmap | 单帧截图 |\n| `saveFrame(File, GSYVideoShotSaveListener)` | 保存到磁盘 | 落盘截图 |\n| `taskGifPic(...)` + `GifCreateHelper` | 生成 gif | 见 [GifCreateHelper](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GifCreateHelper.java) |\n\n## Demo 对照\n\n- 滤镜集合：[DetailFilterActivity](../../app/src/main/java/com/example/gsyvideoplayer/DetailFilterActivity.java) + [DetailFilterComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/DetailFilterComposeActivity.kt)\n- 透明视频：[SampleTransparentVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/SampleTransparentVideo.java) + [DetailTransparentActivity](../../app/src/main/java/com/example/gsyvideoplayer/DetailTransparentActivity.java)\n- 自定义 Render：[CustomRenderVideoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/CustomRenderVideoPlayer.java)\n\n## 常见坑\n\n- `SurfaceView` 在 `RecyclerView` 里滑动会撕裂 / 覆盖 → 列表用 `TEXTURE`。\n- GL 效果的性能取决于分辨率、pass 数和设备；先评估 pass/金字塔层数与实际帧率，不能通过静默换成 TextureView 宣称效果已生效。\n- 竖屏视频强制 `SCREEN_TYPE_FULL` 会大幅裁减；建议 `SCREEN_TYPE_DEFAULT` + `setAutoFullWithSize(true)`。\n","skills/12-gsy-custom-view/SKILL.md":"---\nname: gsy-custom-view\ndescription: 自定义播放器 UI（继承 StandardGSYVideoPlayer）与自定义控制层：换 seekbar、加弹幕层、加清晰度切换、加投屏按钮等。\nwhen_to_use: SDK 默认 UI 不符合需求；需要在标准控制层上加自定义控件；需要多状态视图（loading/error/paywall）。\ninputs:\n  - name: layout_id\n    description: 自定义 XML 布局资源 id\n    required: true\noutputs:\n  - 一个继承 GSYBaseVideoPlayer 子类的自定义播放器组件。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/NormalGSYVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/SampleCoverVideo.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/SampleControlVideo.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/LandLayoutVideo.java\n---\n\n# gsy-custom-view\n\n## 继承层次\n\n```\nGSYTextureRenderView（渲染管理）\n  └── GSYVideoView（状态机 / 生命周期）\n        └── GSYVideoControlView（默认控制层：进度条/按钮）\n              └── GSYVideoPlayer（触摸手势）\n                    └── GSYBaseVideoPlayer（全屏/小屏切换）\n                          ├── StandardGSYVideoPlayer（默认 UI）\n                          └── NormalGSYVideoPlayer（无返回/无标题的简版）\n```\n\n自定义组件通常继承 [StandardGSYVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java) 或 [NormalGSYVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/NormalGSYVideoPlayer.java)。\n\n## 必须实现的三件事\n\n### 1) 自定义布局 & id\n\n覆写：\n```java\n@Override public int getLayoutId() { return R.layout.my_video_player; }\n```\n布局里必须包含（id 不能改）：\n- `@id/surface_container` —— 放渲染 View（FrameLayout）\n- 需要用到的默认控件（如 `@id/start`、`@id/progress`、`@id/current`、`@id/total`、`@id/bottom_progressbar`、`@id/thumb`、`@id/small_close`、`@id/back`、`@id/fullscreen`、`@id/loading`）—— 缺哪个哪个功能失效，编译不报错。\n\n### 2) 覆写全屏 & 小屏组件类\n\n关键：`getConstructor(Context, Boolean)` 是 SDK 内部反射拷贝出的全屏实例。**必须提供两个构造器**：\n\n```java\npublic MyVideoPlayer(Context c) { super(c); }\npublic MyVideoPlayer(Context c, Boolean fullFlag) { super(c, fullFlag); }\npublic MyVideoPlayer(Context c, AttributeSet a) { super(c, a); }\n```\n\nR8 侧已在 [proguard-rules.pro](../../app/proguard-rules.pro) 保留：\n```\n-keep class * extends com.shuyu.gsyvideoplayer.video.base.GSYBaseVideoPlayer {\n    public <init>(android.content.Context);\n    public <init>(android.content.Context, java.lang.Boolean);\n}\n```\n\n### 3) 覆写 `showSmallVideo() / hideSmallVideo()`（如需要小窗）以及 `startWindowFullscreen()`（如要全屏样式差异）\n\n## 常用重写点\n\n| 方法 | 作用 |\n|---|---|\n| `changeUiToNormal()` / `changeUiToPreparingShow()` / `changeUiToPlayingShow()` / `changeUiToPauseShow()` / `changeUiToCompleteShow()` / `changeUiToError()` / `changeUiToPlayingBufferingShow()` | 不同状态下的 UI 切换钩子 |\n| `onClickUiToggle()` | 单击 UI 显隐 |\n| `setStateAndUi(int state)` | 由 SDK 内部调，可 super 后加自己的逻辑 |\n| `updateStartImage()` | 播放/暂停按钮 icon 切换 |\n| `resolveTypeUI()` | 首帧比例决定后的自定义处理 |\n| `dismissBrightnessDialog / dismissVolumeDialog / dismissProgressDialog` | 手势弹窗 UI |\n| `hideAllWidget / setViewShowState` | 显隐所有控件 |\n\n## 手势自定义\n\n`GSYVideoControlView` 里通过如下开关切换：\n- `setIsTouchWiget(boolean)` —— 手势总开关（默认 true）\n- `setIsTouchWigetFull(boolean)` —— 只在全屏开\n- `setNeedLockFull(boolean)` —— 全屏锁按钮\n- `setSeekRatio(float)` —— 手势 seek 灵敏度\n- `setThreshold(int)` —— 触发手势阈值\n\n手势弹窗替换：覆写 `showBrightnessDialog / showVolumeDialog / showProgressDialog` 用自己的 Dialog。\n\n## 标准封面 / 清晰度 / 弹幕\n\n- 封面：Demo 组件 [SampleCoverVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/SampleCoverVideo.java) 演示用 Glide 加载封面。\n- 清晰度切换：Demo 组件 [SwitchVideo](../../app/src/main/java/com/example/gsyvideoplayer/switchplay/SwitchVideo.java) + [DetailListPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailListPlayer.java)（多列源，`setUp(List<GSYVideoModel>, ...)`）。\n- 弹幕层：Demo 组件 [DanmakuVideoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/DanmakuVideoPlayer.java) —— 在布局叠一层 `DanmakuView`，在 `onProgress` 里 `dispatch(long time)`。\n- 控制条自定义：Demo 组件 [SampleControlVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/SampleControlVideo.java)（毛玻璃底栏 + 播放按钮）。\n- 横屏专用布局：Demo 组件 [LandLayoutVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/LandLayoutVideo.java)（`getFullId()` 返回不同 layout）。\n\n## 多状态视图（loading / error / paywall）\n\n在 `changeUiToError()` 与 `changeUiToPreparingShow()` 里 `setVisibility` 自定义 View；也可以在 `onVideoAllCallBack.onPlayError` 中显式 `showErrorLayer()`。\n\n## 常见坑\n\n- 忘写 `Boolean` 构造器 → 全屏切换 `IllegalArgumentException` 或直接 crash（反射失败）。\n- 自定义 `layout` 缺 id → 默认功能沉默失效，问题不易定位。建议基于 `layout_standard_video_player.xml` 复制修改。\n- 全屏组件和非全屏组件类型不同 → 用 `getCurrentPlayer()` 获取\"当前活跃 player\"，别缓存 `this`。\n- 释放时机：`onDestroy` 里既要 `super.onBackPressed` 判断，也要 `GSYVideoManager.releaseAllVideos()`。\n","skills/09-gsy-live-and-ads/SKILL.md":"---\nname: gsy-live-and-ads\ndescription: 直播（RTMP/RTSP/HLS）、贴片广告 (PreRoll AD)、静音、切码率、无缝切源、切换过程保留最后一帧。\nwhen_to_use: 做直播；短视频/长视频前贴片广告；PV/UV 埋点后要 A/B 分发；快节奏内容切换。\ninputs:\n  - name: stream_url\n    description: 直播地址（rtmp://.../ rtsp://.../ .m3u8 等）\n    required: false\n  - name: ad_url\n    description: 前贴广告地址\n    required: false\noutputs:\n  - 直播不使用缓存；广告播放完切主片；无缝续播不闪黑。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYADVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYSampleADVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoADManager.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseADActivityDetail.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer2.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/KeepLastFrameDemoActivity.java\n---\n\n# gsy-live-and-ads\n\n## 直播接入要点\n\n- `setUp(url, false, title)` —— `cacheWithPlay` 必须 `false`。\n- `onResume` / `player.onVideoResume(false)`：**`seek=false` 避免拉流跳变**（见 [GSYVideoView#L549](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java)）。\n- 内核选择：\n  - RTMP / RTSP：**必须 IJK**；System / Exo / Aliyun 各有兼容性差异。\n  - HLS：Exo 更稳；Ijk 需要开启硬解 `GSYVideoType.enableMediaCodec()` 才顺。\n- 关键参数：\n  - `builder.setNeedShowWifiTip(false)`（后台 App 不打断）\n  - `builder.setReleaseWhenLossAudio(false)`（丢音频焦点不释放）\n  - `builder.setSurfaceErrorPlay(true)`\n\n对 IJK 加 low-latency：通过 `VideoOptionModel` 注入 ffmpeg option，见 [BasePlayerManager](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/BasePlayerManager.java) 与 [GSYVideoManager.instance().setOptionModelList(list)](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoBaseManager.java)。常用 option：`analyzemaxduration=100`、`probesize=10240`、`flush_packets=1`、`fflags=nobuffer`、`packet-buffering=0`、`framedrop=1`。\n\n## 广告 AD\n\n- 双 Manager 双 Player：`GSYVideoADManager.instance()` 负责广告 player，`GSYVideoManager.instance()` 负责主片；两者共存不打架。\n- 组件 [GSYADVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYADVideoPlayer.java) 与\"广告示例\"[GSYSampleADVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYSampleADVideoPlayer.java) 都继承 `StandardGSYVideoPlayer`。\n- Activity 基类：[GSYBaseADActivityDetail](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseADActivityDetail.java)，提供 `getGSYVideoADPlayer()` / `initADVideo()` / `startAD(String adUrl, String url)` / `onADEnd`。\n\n### `startAD(...)` 关键调用序\n\n1. Activity 里持有主 player + AD player 两个 View；\n2. `initADVideo()` 装配（内部会连锁 `OrientationUtils`）；\n3. `startAD(adUrl, mainUrl)` 播广告；\n4. AD complete → SDK 内部隐藏 AD player、start 主 player；\n5. 用户点\"跳过\"→ `getGSYVideoADPlayer().onAutoCompletion()`。\n\n## 无缝切源 / 切码率\n\nExo 内核用 `Exo2PlayerManager.setCurrentMediaSource(...)` 或 Demo 里的 `GSYExo2MediaPlayer.setMediaSources(...)`；Ijk 则重新 `setUp` + `startPlayLogic`。切换的过程中要求\"不闪黑\"，用：\n\n- [KeepLastFrameVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/KeepLastFrameVideo.java)（Demo 组件）+ [KeepLastFrameDemoActivity](../../app/src/main/java/com/example/gsyvideoplayer/KeepLastFrameDemoActivity.java)；\n- 原理见 [doc/KEEP_LAST_FRAME.md](../../doc/KEEP_LAST_FRAME.md)：新 player 拿到首帧前，把旧 SurfaceTexture 内容以 Bitmap 覆盖在上层。\n\n## 广告 / 主片全屏切换的两个陷阱\n\n- 主片全屏 → 广告插入：`GSYVideoADManager` 无内部 `OrientationUtils`；`GSYBaseADActivityDetail` 已处理，但**自定义子类需重写 `getGSYVideoADPlayer()` 返回 AD player 才能被基类的旋转/回退纳管**。\n- 广告全屏返回 → 直接销毁 AD player：`onBackPressed` 需要判断 `GSYVideoManager.backFromWindowFull(this) || GSYVideoADManager.backFromWindowFull(this)`。\n\n## Demo 对照\n\n- 单主片：[DetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java)\n- 前贴片 + 主片：[DetailADPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer.java) / [DetailADPlayer2](../../app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer2.java)\n- 列表条目内广告：[ListADVideoActivity](../../app/src/main/java/com/example/gsyvideoplayer/ListADVideoActivity.java) / [ListADVideoActivity2](../../app/src/main/java/com/example/gsyvideoplayer/ListADVideoActivity2.java) / [RequestListADVideoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/RequestListADVideoPlayer.java)\n- 智能硬解回退：[SmartMediaCodecFallbackActivity](../../app/src/main/java/com/example/gsyvideoplayer/SmartMediaCodecFallbackActivity.java)\n\n## v14.0.0 RTSP 与倍速\n\n- FORMAT `timeout` 为微秒（`20_000_000L` = 20 秒），明确配置的 `rtsp_transport` 会透传。\n- PLAYER `rtsp-live-max-buffer-ms` 在 prepare 前设置，`0` 关闭，`500..60000` 毫秒用于正常 1× 直播队列恢复。恢复会丢弃旧媒体并产生跳跃/音频间隙，不是自动重连。\n- 初始化重定向覆盖 OPTIONS/DESCRIBE/SETUP/首次 PLAY；不支持播放中 REDIRECT 或任意跨端口控制会话。\n- `soundtouch=1` 为 0.25×–4× 软件变速；`soundtouch=0` 为设备相关平台范围。引擎切换和 EOF 按真实音频排空处理，终止错误后需要 reset 或重建。\n- 详见 [QUESTION.md](../../doc/QUESTION.md) 与 [v14 发布说明](../../doc/UPDATE_VERSION.md#v1400-2026-10-10)。\n\n## 常见坑\n\n- 直播卡顿升级 → 检查是否开启 `cacheWithPlay`（对直播来说必须关）。\n- 广告 `onAutoComplete` 后主片没自动 setUp → 检查是不是把 `setStartAfterPrepared(false)` 传给了主 builder。\n- RTMP 断流回收后重连白屏 → `onVideoResume(false)` + `player.getCurrentPlayer().onVideoReset()` 再 `startPlayLogic()`。\n","skills/13-gsy-proguard-r8/SKILL.md":"# 13-gsy-proguard-r8\n\nPath: `skills/13-gsy-proguard-r8/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/13-gsy-proguard-r8/SKILL.md)","doc/ARCHITECTURE.md":"# doc - System Architecture & Module Boundaries\n\nPath: `doc/ARCHITECTURE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/doc/ARCHITECTURE.md)","skills/README.md":"# Skills Skill\n\nPath: `skills/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/README.md)"},"files":{"skills/06-gsy-cache-and-download/SKILL.md":"---\nname: gsy-cache-and-download\ndescription: 边播边缓（AndroidVideoCache 代理）、自定义缓存目录、清理缓存、离线下载至完整文件。\nwhen_to_use: 需要\"边看边攒\"离线可播；或空间敏感需要限量清理；或做真下载入库（不是缓存代理）。\ninputs:\n  - name: url\n    description: 网络视频地址（http/https）\n    required: true\n  - name: cache_dir\n    description: 缓存目录（默认 context.cacheDir/videoCache）\n    required: false\noutputs:\n  - 播放同时把 mp4 缓存到本地；命中缓存后重播 0 网络。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheManager.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/CacheFactory.java\n  - file: gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/cache/ICacheManager.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailDownloadPlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailDownloadExoPlayer.java\n---\n\n# gsy-cache-and-download\n\n## 概念\n\n- **缓存**（cache）：`ProxyCacheManager` 内部起本地 HTTP 代理，拦截 `player` 的请求，把响应流一边喂给 player 一边写到本地；文件名是 URL MD5。\n- **下载**（download）：与缓存共享目录，但一定要走\"从头到尾\"完整播完才能保证文件可复用为独立 mp4；提前 seek 会造成 range 分片，回放时代理会重新下载缺失段。\n\n## 开启缓存\n\n方式 1：`setUp(url, true, title)` 第二参数 `cacheWithPlay=true`。\n\n方式 2：`GSYVideoOptionBuilder`：\n- `setCacheWithPlay(true)`\n- `setCachePath(File)`（可选）\n\n## 自定义缓存目录\n\n```java\nFile dir = new File(getExternalFilesDir(null), \"videoCache\");\nbuilder.setCachePath(dir);\n```\n\n若使用不同 `CacheManager`（例如 Exo），传给 `CacheFactory.setCacheManager(...)` 前记得也走 `setCachePath` 或自定义 `ICacheManager` 实现。\n\n## `ICacheManager` API\n\n见 [ICacheManager](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/cache/ICacheManager.java)。核心方法：\n\n| 方法 | 说明 |\n|---|---|\n| `doCacheLogic(Context, IMediaPlayer, String url, Map<String,String> headers, File cachePath)` | 播放前介入，把 url 换为代理 url |\n| `clearCache(Context, File cachePath, String url)` | 单条清除；`url = null` 清空整目录 |\n| `hadCached()` | 当前播放是否已\"命中\"缓存（100% 完成） |\n| `setCacheAvailableListener(CacheAvailableListener)` | 缓存进度回调 |\n| `release()` | 释放代理 |\n\n## `ProxyCacheManager` 关键静态方法\n\n在 [ProxyCacheManager](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheManager.java) 里：\n\n- `clearAllCache(Context, File)` —— 清空整个缓存目录。\n- 内部 `HttpProxyCacheServer` 单例 lazy 初始化，第一次 `doCacheLogic` 时才起服务；Application 就绪即可。\n\n## `ProxyCacheUserAgentHeadersInjector`\n\n自定义 UA / Cookie / Referer 注入到代理层：见 [ProxyCacheUserAgentHeadersInjector](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheUserAgentHeadersInjector.java)。挂到 `HttpProxyCacheServer.Builder.headerInjector()` 上，或直接用 `Map` 传给 `setUp`。\n\n## 离线下载\n\n三步流程：\n\n1. 用 `HttpProxyCacheServer` 单独跑一次预下载（不 attach 到 Player），或直接开一条播放并 `setStartAfterPrepared(false)` 静默走全长；\n2. 事件 `VideoAllCallBack#onAutoComplete` 触发 → 缓存文件已完整；\n3. 用 [FileUtils](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/FileUtils.java) 拷贝 / 重命名到业务目录。\n\nDemo：[DetailDownloadPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailDownloadPlayer.java)（IJK）与 [DetailDownloadExoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailDownloadExoPlayer.java)（Exo）。\n\n## 不缓存的场景\n\n以下情况 `cacheWithPlay` 应设 `false`：\n- 直播 / RTMP / RTSP（连续无尽流）；\n- HLS m3u8（代理逻辑对分片不友好，建议 Exo 内核 + Exo cache）；\n- DASH / SmoothStreaming；\n- DRM 或私有加密。\n\n## 常见坑\n\n- **代理端口被占**：`HttpProxyCacheServer` 首次启动失败会 fallback 到原始 url，缓存不生效。多进程时可能出现，建议仅主进程用。\n- **缓存目录跨用户**：`context.cacheDir` 是 per-user；用外部存储需要动态权限。\n- **同 URL 缓存名冲突**：MD5(url) 相同即命中；如果 URL 带一次性签名，每次不同 → 缓存穿透。可以先规范化 URL 再传给 `setUp`。\n- **HTTP header 变更失效**：headers 参与代理请求生成，但缓存文件名只按 URL；同一 URL 换 UA 不会重新下载。\n","skills/04-gsy-kernel-switch/SKILL.md":"---\nname: gsy-kernel-switch\ndescription: 在 IJK / System / ExoPlayer2(Media3) / Aliyun 四个媒体内核之间自由切换，以及自定义 Manager 的方法。\nwhen_to_use: 需要支持 DASH/SmoothStreaming、需要 Media3 track selection、想减 so 体积只留系统解码、要用 Aliyun 内核、或者要多实例并行独立 Manager。\ninputs:\n  - name: kernel\n    description: ijk / system / exo / ali / custom\n    required: true\noutputs:\n  - PlayerFactory 与（可选）CacheFactory 已切换到目标 Manager\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/PlayerFactory.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/IjkPlayerManager.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/SystemPlayerManager.java\n  - file: gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/IPlayerManager.java\n  - file: gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/BasePlayerManager.java\n  - file: gsyVideoPlayer-exo_player2\n  - file: gsyVideoPlayer-aliplay/src/main/java/com/shuyu/aliplay/AliPlayerManager.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/exo/GSYExoPlayerManager.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/manager/CustomManager.java\n---\n\n# gsy-kernel-switch\n\n## 内核矩阵\n\n| Kernel | Class | 来源模块 | 特点 |\n|---|---|---|---|\n| **IJK**（默认） | [IjkPlayerManager](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/IjkPlayerManager.java) | `gsyVideoPlayer-java` + 升级后的独立 ABI / `ex_so` | FFmpeg n5.1.10 的固定配置；软/硬解能力按格式与设备确认 |\n| **System** | [SystemPlayerManager](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/SystemPlayerManager.java) | `gsyVideoPlayer-java` | 无 so；靠 `android.media.MediaPlayer`；直播/复杂容器兼容差 |\n| **Exo/Media3** | `GSYExoPlayerManager`（Demo 内）／`GSYExo2MediaPlayer` | `gsyVideoPlayer-exo_player2` | DASH / HLS / SmoothStreaming / TrackSelection |\n| **Aliyun** | [AliPlayerManager](../../gsyVideoPlayer-aliplay/src/main/java/com/shuyu/aliplay/AliPlayerManager.java) | `gsyVideoPlayer-aliplay` | 阿里云 vid/authInfo；HLS 加密 |\n\n## 切换 API\n\n**全局切换**：`PlayerFactory.setPlayManager(Class<? extends IPlayerManager>)`（内部 `getPlayManager` 用 `sPlayerManager.newInstance()` 反射构造，故被切入的类**必须**有 `public` 无参构造）。见 [PlayerFactory#L15-L27](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/PlayerFactory.java) 与本仓库 R8 保留规则 [proguard-rules.pro](../../app/proguard-rules.pro)。\n\n```java\n// Exo\nPlayerFactory.setPlayManager(Exo2PlayerManager.class);\nCacheFactory.setCacheManager(ExoPlayerCacheManager.class);\n\n// System\nPlayerFactory.setPlayManager(SystemPlayerManager.class);\n\n// Aliyun\nPlayerFactory.setPlayManager(AliPlayerManager.class);\n```\n\n必须在 `player.setUp(...)` **之前**调用；否则本次实例仍用旧内核。\n\n## `IPlayerManager` 抽象\n\n统一接口见 [IPlayerManager](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/IPlayerManager.java)。核心方法（子类必须实现）：\n\n| 方法 | 说明 |\n|---|---|\n| `initVideoPlayer(Context, Message, List<VideoOptionModel>, ICacheManager)` | 初始化内核；`Message.obj` 是 `GSYModel` |\n| `showDisplay(Message)` | 绑定 Surface / SurfaceHolder |\n| `setSpeed(float, boolean soundTouch)` | 速率 + 变速不变调 |\n| `setNeedMute(boolean)` | 静音 |\n| `setVolume(float, float)` | 双通道音量 |\n| `getMediaPlayer()` | 底层 player 实例 |\n| `isSurfaceSupportLockCanvas()` | 是否支持 `lockCanvas`（TextureView 补帧） |\n| `start / pause / stop / seekTo / release / getCurrentPosition / getDuration / getVideoSarNum / getVideoSarDen / getVideoWidth / getVideoHeight` | 基础控制 |\n\n推荐继承 [BasePlayerManager](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/BasePlayerManager.java) 以少写模板。\n\n## `IPlayerInitSuccessListener` hook\n\n[IPlayerInitSuccessListener](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/IPlayerInitSuccessListener.java) 允许在 `initVideoPlayer` 完成后对底层 Player 拿手（比如设置 Exo 的 TrackSelector）：\n\n```java\nIjkPlayerManager.setIjkLibLoader(...);\n// 或\nGSYExoVideoManager.setPlayerInitSuccessListener(new IPlayerInitSuccessListener() {\n    @Override public void onPlayerInitSuccess(IMediaPlayer mp, GSYModel model) { ... }\n});\n```\n\n## `CacheFactory` 同步切换\n\n不同内核有各自缓存方案；切内核记得配套：\n\n| Player Manager | 对应 CacheManager |\n|---|---|\n| `IjkPlayerManager` / `SystemPlayerManager` | [ProxyCacheManager](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheManager.java)（默认） |\n| Exo/Media3 | `ExoPlayerCacheManager`（`gsyvideoplayer-exo_player2` 内） |\n| Aliyun | 由 Aliyun SDK 自身管理，一般 `CacheFactory` 保持 `ProxyCacheManager` 或自定义空实现 |\n\n## 多实例并行\n\n用 [CustomManager](../../app/src/main/java/com/example/gsyvideoplayer/video/manager/CustomManager.java) 模式：拷贝 `GSYVideoManager` 单例套壳，在自定义 `GSYVideoView` 里覆盖 `getGSYVideoManager()`；不同页面互不影响，参考 [MultiSampleVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/MultiSampleVideo.java) + [ListMultiVideoActivity](../../app/src/main/java/com/example/gsyvideoplayer/ListMultiVideoActivity.java)。\n\n## Demo 对照\n\n- Exo 切源 / 无缝切换：[DetailExoListPlayer](../../app/src/main/java/com/example/gsyvideoplayer/exo/DetailExoListPlayer.java)、[ExoAdaptiveTrackActivity](../../app/src/main/java/com/example/gsyvideoplayer/exo/ExoAdaptiveTrackActivity.java)\n- MediaCodec 智能回退：[SmartMediaCodecFallbackActivity](../../app/src/main/java/com/example/gsyvideoplayer/SmartMediaCodecFallbackActivity.java) + [doc/SMART_MEDIACODEC_FALLBACK_RESEARCH.md](../../doc/SMART_MEDIACODEC_FALLBACK_RESEARCH.md)\n\n## v14.0.0 原生选择\n\nIJK 当前三 ABI 成套库为 FFmpeg n5.1.10 / OpenSSL 3.5.9。`GSYIjkMediaCodecSelector` 先保留原 IJK 选择，再在 API 29+ 选择平台报告的普通 Codec2 硬件解码器，排除纯软件及安全/隧道必需解码器。这是初始化阶段的选择补充，与确认运行中硬解失败后重建软解的策略分开；见 [发布说明](../../doc/UPDATE_VERSION.md#v1400-2026-10-10)。\n\n## 常见坑\n\n- `PlayerFactory.setPlayManager` 传入的类**必须**有 `public` 无参构造，且被 ProGuard 保留。\n- Exo 切源建议用 `Exo2PlayerManager` + `GSYExo2MediaPlayer`；直接换 Manager 但用 Ijk 的 URL 会走缓存代理导致 DASH 解析失败——记得 `setOverrideExtension(\"mpd\")` 或关闭缓存。\n- 系统内核 `SystemPlayerManager` 不支持 `soundTouch`、多数直播格式；生产环境仅作 fallback。\n","skills/05-gsy-list-playback/SKILL.md":"---\nname: gsy-list-playback\ndescription: RecyclerView / ViewPager2 / ViewPager 列表滑动自动播放，滚动切换、复用绑定、进入详情页时\"漂移\"到全屏。\nwhen_to_use: 短视频信息流、朋友圈式内嵌播放、抖音/快手类 ViewPager2 竖播、列表进入详情页的无缝续播。\ninputs:\n  - name: adapter\n    description: RecyclerView.Adapter / FragmentStateAdapter\n    required: true\noutputs:\n  - 列表条目具备\"进入可视区自动播放，离开释放\"的能力，且切换详情页可续播。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/ListVideoUtil.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoHelper.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/utils/ScrollCalculatorHelper.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/AutoPlayRecyclerViewActivity.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/ViewPager2Activity.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/RecyclerViewActivity.java\n---\n\n# gsy-list-playback\n\n## 三条实现路线\n\n| 路线 | 特点 | 适用 |\n|---|---|---|\n| **A. 每 item 内嵌 `StandardGSYVideoPlayer`** | 每条 item 有独立 player 实例，滑到即播 | 大多数场景（含无限缩略页面） |\n| **B. `ListVideoUtil` 单例复用** | 全列表共享一个 player 实例，动态 `attachToParent` | 内存敏感、条目多；不能同屏两个视频 |\n| **C. `GSYVideoHelper` 悬浮小窗** | 用于详情页返回列表时\"漂浮\"续播 | 抖音式详情返回 |\n\n## 路线 A：每 item 一个 player\n\n关键点：**在 `onBindViewHolder` 里给每个 player `setPlayPosition(position)` + `setPlayTag(String)`**，防止滚动复用导致 URL 命中错人。\n\n`GSYVideoOptionBuilder`：\n- `setPlayTag(String)` [#L347](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java)\n- `setPlayPosition(int)` [#L356](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java)\n\n滚动出屏自动释放：`RecyclerView.OnScrollListener` → 当前 firstVisible / lastVisible 之外的 holder 调 `GSYVideoManager.releaseAllVideos()`。\n\n见 [RecyclerViewActivity](../../app/src/main/java/com/example/gsyvideoplayer/RecyclerViewActivity.java)。\n\n## 路线 B：`ListVideoUtil`（跨 item 复用一个 player）\n\n工具类：[ListVideoUtil](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/ListVideoUtil.java)。\n\n| API | 说明 |\n|---|---|\n| `ListVideoUtil(Context)` | 单例创建（在 Activity/Fragment 里持一份） |\n| `addVideoPlayer(int position, ImageView thumb, String tag, ViewGroup container, View playBtn)` | 把 player 移到 `container`；`tag` 与 `position` 用于识别是否是新目标 |\n| `setPlayPositionAndTag(int, String)` | 手动更新目标 |\n| `startPlay(String url)` | 开始播放 |\n| `isCurrentViewPlaying(int pos, String tag)` | 判定当前播放是否在这个 item |\n| `releaseVideoPlayer()` | 释放，通常在离开页面时调 |\n\n**自动可视区触发**用 [ScrollCalculatorHelper](../../app/src/main/java/com/example/gsyvideoplayer/utils/ScrollCalculatorHelper.java) 的 `onScrollReleaseAllVideos(...) / onScrollPlayVideo(...)`。见 [AutoPlayRecyclerViewActivity](../../app/src/main/java/com/example/gsyvideoplayer/AutoPlayRecyclerViewActivity.java)。\n\n## 路线 C：`GSYVideoHelper` 小窗漂移\n\n工具类：[GSYVideoHelper](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoHelper.java)。核心思路：**列表 item / 详情页共用同一个 player 实例**，在离开列表时把 player parent 换到浮层容器，进入详情再换回去。\n\n| API | 说明 |\n|---|---|\n| `GSYVideoHelper(Context, GSYBaseVideoPlayer)` | 携一份 player |\n| `setSmall(int width, int height, int gravity, int marginX, int marginY)` | 小窗尺寸 |\n| `showSmallVideo(...)` / `smallVideoToNormal()` | 小窗 ↔ 正常 |\n| `releaseVideoPlayer()` | 释放 |\n\nDemo：[SmallVideoHelper](../../app/src/main/java/com/example/gsyvideoplayer/utils/SmallVideoHelper.java)（app 层包装） + [ListVideoActivity](../../app/src/main/java/com/example/gsyvideoplayer/ListVideoActivity.java)。\n\n## ViewPager2 竖播\n\n- 用 `FragmentStateAdapter`，每个 Fragment 内嵌 `StandardGSYVideoPlayer` 或 `NormalGSYVideoPlayer`；\n- `ViewPager2.registerOnPageChangeCallback` 里 `onPageSelected` → `previousPlayer.onVideoPause(); currentPlayer.startPlayLogic()`；\n- 记得 `setUserInputEnabled` 竖向。\n\nDemo：[ViewPager2Activity](../../app/src/main/java/com/example/gsyvideoplayer/ViewPager2Activity.java)（RecyclerView 变体） + Compose 版 [VerticalShortVideoComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/VerticalShortVideoComposeActivity.kt)。\n\n## 常见坑\n\n- **相同 URL 不重播**：忘了 `setPlayTag/setPlayPosition`，`setUp` 内部去重返回 false。\n- **切换过快出黑帧**：`setUp` 后立即 `startPlayLogic` 前先隐藏 `thumb`；或用 `KeepLastFrameVideo` 保留上一帧（详见 [doc/KEEP_LAST_FRAME.md](../../doc/KEEP_LAST_FRAME.md)）。\n- **同屏多个 item 各自播放**：单例 `GSYVideoManager` 只能有一个 listener；同屏并行需要参考 [CustomManager](../../app/src/main/java/com/example/gsyvideoplayer/video/manager/CustomManager.java)。\n","skills/08-gsy-subtitle-and-preview/SKILL.md":"---\nname: gsy-subtitle-and-preview\ndescription: 外挂/内嵌字幕（SRT/VTT）、Exo 内嵌字幕轨、Seek 悬浮预览缩略图（VTT thumbnails）。\nwhen_to_use: 需要显示 srt/vtt/ass 字幕、切换字幕轨、seek 时鼠标/手指浮层展示预览帧。\ninputs:\n  - name: subtitle_source\n    description: 字幕文件路径或 URL、MIME\n    required: false\n  - name: preview_source\n    description: VTT 预览描述文件\n    required: false\noutputs:\n  - player 上叠加 subtitle view；seekbar 拖动出现预览帧。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview\n  - file: app/src/main/java/com/example/gsyvideoplayer/SubtitleDetailPlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitlePlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/PreViewGSYVideoPlayer.java\n---\n\n# gsy-subtitle-and-preview\n\n## 字幕 API\n\n顶层控制器：[GSYSubtitleController](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleController.java)。视图：[GSYSubtitleView](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleView.java)。\n\n| 方法 | 说明 |\n|---|---|\n| `new GSYSubtitleController(context, subtitleView)` | 绑定 UI |\n| `setSources(List<GSYSubtitleSource>)` | 一次注入多轨字幕 |\n| `setSource(GSYSubtitleSource)` | 只有一条 |\n| `getSources()` | 只读 List |\n| `selectSubtitle(String id)` | 按 id 切换字幕，\"\" 或 null 关闭 |\n| `setStyle(GSYSubtitleStyle)` | 字体/描边/背景/颜色 |\n| `setOffsetMs(long)` | 音画/字幕 offset |\n| `setEnabled(boolean)` | 开关 |\n| `updatePosition(long positionMs)` | 由 player 进度驱动 |\n| `release()` | 释放 |\n\n## `GSYSubtitleSource` 字段\n\n见 [GSYSubtitleSource](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleSource.java)：\n\n| 字段 | 说明 |\n|---|---|\n| `id` | 唯一 id |\n| `label` | 显示名 |\n| `language` | ISO code |\n| `mime` | 见 [GSYSubtitleMime](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleMime.java)：`text/vtt`、`application/x-subrip`（srt）、`text/plain` |\n| `uri` | http 或 file uri |\n| `embedded` | 是否内嵌轨（Exo 场景） |\n\n## 内置解析器\n\n- SRT：[GSYSrtSubtitleParser](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSrtSubtitleParser.java)\n- WebVTT：[GSYWebVttSubtitleParser](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYWebVttSubtitleParser.java)\n- 自定义 MIME：注册到 [GSYSubtitleParserFactory](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleParserFactory.java)\n\n## Exo 内嵌字幕\n\n`gsyvideoplayer-exo_player2` 的 Manager 会把 Exo 的 subtitle track 通过 `GSYSubtitleController` 上抛。Demo：[GSYExoSubTitlePlayer](../../app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitlePlayer.java) + [GSYExoSubTitleVideoManager](../../app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitleVideoManager.java)。\n\n## Seek 预览 API\n\n顶层：[GSYVideoPreviewProvider](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview/GSYVideoPreviewProvider.java) 与列表实现 [GSYVideoPreviewListProvider](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview/GSYVideoPreviewListProvider.java)。\n\n| 方法 | 说明 |\n|---|---|\n| `GSYVideoPreviewProvider#getPreviewFrame(long positionMs)` | 返 `GSYVideoPreviewFrame`，含 image url + crop 坐标 |\n| `GSYVideoPreviewListProvider(List<GSYVideoPreviewFrame>)` | 直接注入帧列表（自己解析或用 VTT） |\n| `GSYVideoPreviewVttParser#parse(String vttContent, String baseUrl)` | 解析 WebVTT thumbnails（雪碧图 + 时间段） |\n| `GSYVideoPreviewVttParser#parseFrames(String vttContent, String baseUrl)` | 只要帧列表 |\n\n## `GSYVideoPreviewFrame` 字段\n\n见 [GSYVideoPreviewFrame](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview/GSYVideoPreviewFrame.java)：`startTimeMs`、`endTimeMs`、`imageUrl`、`cropX/Y/Width/Height`、`hasCrop()`。\n\n## 关键调用序（VTT 预览 + SRT 字幕）\n\n1. 加载 subtitle：`controller.setSource(new GSYSubtitleSource(\"cn\", \"中文\", \"zh\", GSYSubtitleMime.SRT, uri, false))`;\n2. 播放进度回调（`GSYVideoProgressListener`）中 `controller.updatePosition(current)`；\n3. 预览：`GSYVideoPreviewVttParser.parse(vttText, base)` → 得到 Provider → 注入到 `PreViewGSYVideoPlayer#setPreviewProvider(provider)`；\n4. seekbar 拖动时 SDK 会自动调 `provider.getPreviewFrame(target)` 并做 crop。\n\n## Demo 对照\n\n- SRT 字幕：[SubtitleDetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/SubtitleDetailPlayer.java)（对应 `raw/demo_subtitle.srt` / `demo_subtitle_vtt.vtt`）\n- Exo 内嵌字幕：[GSYExoSubTitleDetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitleDetailPlayer.java)\n- 预览缩略图：[PreViewGSYVideoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/PreViewGSYVideoPlayer.java) + [PlayPickActivity](../../app/src/main/java/com/example/gsyvideoplayer/PlayPickActivity.java)\n\n## 常见坑\n\n- SRT 时间轴带 BOM 或换行不规范会直接被 parser 丢弃；先用 UTF-8 无 BOM 保存。\n- VTT 缩略图 URL 相对路径必须传 `baseUrl` 才能拼绝对路径。\n- `updatePosition` 必须在 UI 线程或至少与 `subtitleView` 同线程；`GSYSubtitleController` 内部无线程切换。\n","skills/02-gsy-option-builder/SKILL.md":"---\nname: gsy-option-builder\ndescription: 用 GSYVideoOptionBuilder 一次性把播放器所有可调项配好；等价于对 StandardGSYVideoPlayer 挨个 setter 但更紧凑。\nwhen_to_use: 需要开启循环 / 变速 / 边播边缓 / 触摸手势 / 自动旋转 / 封面 / 底部进度条样式 / 全屏动画 等多个开关时；或在多处播放页复用同一份配置。\ninputs:\n  - name: player\n    description: StandardGSYVideoPlayer / GSYBaseVideoPlayer 实例\n    required: true\noutputs:\n  - 已按 builder 状态 build 完成的 player，可直接 startPlayLogic\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseActivityDetail.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java\n---\n\n# gsy-option-builder\n\n## API 总览\n\n入口：[GSYVideoOptionBuilder](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java)。\n\n链式调用，最后 `build(player)` 把状态灌到具体 `GSYBaseVideoPlayer`。\n\n```java\nnew GSYVideoOptionBuilder()\n    .setUrl(url)\n    .setCacheWithPlay(true)\n    .setVideoTitle(title)\n    .setIsTouchWiget(true)\n    .setRotateViewAuto(false)\n    .setLockLand(false)\n    .setLooping(false)\n    .setSpeed(1.0f)\n    .setSeekOnStart(0)\n    .setVideoAllCallBack(callback)\n    .build(player);\n```\n\n## 完整参数表（按 [源文件行号](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) 对齐）\n\n| Setter | 类型 | 默认 | 用途 |\n|---|---|---|---|\n| `setUrl(String)` [#L376](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | String | null | 播放地址 |\n| `setVideoTitle(String)` [#L386](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | String | null | 顶部标题 |\n| `setMapHeadData(Map)` [#L437](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | `Map<String,String>` | null | HTTP header |\n| `setOverrideExtension(String)` [#L561](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | String | null | 强制 Exo 用哪种解封装器（`m3u8`/`mpd`/`ism`） |\n| `setCacheWithPlay(boolean)` [#L396](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 边播边缓 |\n| `setCachePath(File)` [#L427](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | File | null | 缓存目录 |\n| `setLooping(boolean)` [#L202](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 循环 |\n| `setSpeed(float)` [#L237](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | float | 1.0 | 播放速率，0.25~3.0 常用 |\n| `setSoundTouch(boolean)` [#L246](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 变速不变调 |\n| `setSeekOnStart(long)` [#L366](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | long ms | -1 | 起播 seek 位置 |\n| `setSeekRatio(float)` [#L324](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | float | 1.0 | 触摸横滑与进度的比例 |\n| `setStartAfterPrepared(boolean)` [#L406](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 首帧就绪后自动播；`false` 用于\"手动 start\" |\n| `setReleaseWhenLossAudio(boolean)` [#L417](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 丢失音频焦点是否 release |\n| `setIsTouchWiget(boolean)` [#L263](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 非全屏是否响应触摸手势 |\n| `setIsTouchWigetFull(boolean)` [#L272](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 全屏是否响应触摸手势 |\n| `setDismissControlTime(int ms)` [#L543](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | int | 2500 | 控制条自动隐藏时间 |\n| `setNeedShowWifiTip(boolean)` [#L281](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 移动网络提示 |\n| `setRotateViewAuto(boolean)` [#L221](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 自动旋转全屏；关闭后配合 `OrientationUtils` |\n| `setRotateWithSystem(boolean)` [#L337](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 是否跟随系统旋转设置 |\n| `setLockLand(boolean)` [#L229](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 全屏是否锁横屏 |\n| `setAutoFullWithSize(boolean)` [#L184](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 竖屏视频自动竖屏全屏 |\n| `setOnlyRotateLand(boolean)` [#L567](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 只允许横屏方向变化 |\n| `setShowFullAnimation(boolean)` [#L194](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 全屏过渡动画 |\n| `setHideKey(boolean)` [#L254](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 全屏隐藏虚拟按键 |\n| `setFullHideActionBar(boolean)` [#L587](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 全屏隐藏 ActionBar |\n| `setFullHideStatusBar(boolean)` [#L592](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 全屏隐藏 StatusBar |\n| `setNeedLockFull(boolean)` [#L525](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 全屏显示屏幕锁按钮 |\n| `setLockClickListener(LockClickListener)` [#L533](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 接口 | null | 屏幕锁点击回调 |\n| `setEnlargeImageRes(int)` [#L291](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | drawableRes | -1 | 全屏按钮图标 |\n| `setShrinkImageRes(int)` [#L301](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | drawableRes | -1 | 退出全屏按钮图标 |\n| `setThumbImageView(View)` [#L464](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | View | null | 封面容器（`ImageView` 或子布局） |\n| `setThumbPlay(boolean)` [#L516](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 点击封面即播 |\n| `setShowPauseCover(boolean)` [#L314](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 暂停时显示最后一帧 |\n| `setBottomProgressBarDrawable(Drawable)` [#L482](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | Drawable | null | 底部长条进度条颜色 |\n| `setBottomShowProgressBarDrawable(Drawable, Drawable)` [#L473](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 2×Drawable | null | 交互进度条 & thumb |\n| `setDialogProgressBar(Drawable)` [#L499](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | Drawable | null | 触摸拖动进度条样式 |\n| `setDialogVolumeProgressBar(Drawable)` [#L490](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | Drawable | null | 触摸音量进度条 |\n| `setDialogProgressColor(int, int)` [#L507](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | int color | -11 | 触摸进度条高亮/普通颜色 |\n| `setShowDragProgressTextOnSeekBar(boolean)` [#L572](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 拖动时 seekbar 上气泡 |\n| `setEffectFilter(GSYVideoGLView.ShaderInterface)` [#L551](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | Shader | `NoEffect` | GL 滤镜（渲染必须 `GLSURFACE`） |\n| `setPlayTag(String)` [#L347](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | String | \"\" | 列表播放去重用 |\n| `setPlayPosition(int)` [#L356](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | int | -22 | 列表位置索引 |\n| `setSetUpLazy(boolean)` [#L582](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 延迟 setUp，`build` 后手动 `startPlayLogic` |\n| `setVideoAllCallBack(VideoAllCallBack)` [#L213](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 接口 | null | 全事件回调（prepared/complete/error…） |\n| `setGSYVideoProgressListener(GSYVideoProgressListener)` [#L455](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 接口 | null | 高频进度回调 |\n| `setGSYStateUiListener(GSYStateUiListener)` [#L600](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 接口 | null | UI 状态变化回调 |\n| `setSurfaceErrorPlay(boolean)` [#L446](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | Surface 异常时点击继续 |\n| `setNeedOrientationUtils(boolean)` [#L611](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 全屏是否内部构造 `OrientationUtils` |\n\n## 关键调用序\n\n1. 构造 `GSYVideoOptionBuilder` → 链式 setter；\n2. `builder.build(player)` 会自动 setUp（除非 `setSetUpLazy(true)`）；\n3. 需要在页面里同时接收回调 → 让 Activity `implements VideoAllCallBack`，直接 `setVideoAllCallBack(this)`。\n\n## Demo 对照\n\n- 详情页 + Builder 全套：[DetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java)\n- 结合 `GSYBaseActivityDetail` 的最短接入：[DetailControlActivity](../../app/src/main/java/com/example/gsyvideoplayer/DetailControlActivity.java) 与 [GSYBaseActivityDetail#L56-L61](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseActivityDetail.java)\n\n## 常见坑\n\n- `build(player)` 内部会调 `setUp`；若 `setSetUpLazy(true)` 需要自己再调 `startPlayLogic()`。\n- Effect 滤镜需要先 `GSYVideoType.setRenderType(GLSURFACE)`，否则 `setEffectFilter` 无效。\n- `setPlayTag` 与 `setPlayPosition` 是列表页避免\"上下滑滚 setUp 命中同 URL 不重播\"的关键。\n","skills/03-gsy-fullscreen-orientation/SKILL.md":"---\nname: gsy-fullscreen-orientation\ndescription: 处理进入/退出全屏、屏幕旋转、竖屏视频自适应、系统旋转设置联动、反向横屏。\nwhen_to_use: 出现\"全屏没进/没退\"、\"横屏方向反了\"、\"旋转跟随系统坏了\"、\"竖屏视频被横过来\"、\"抖动无限循环\"这类问题；或首次在详情页接入全屏。\ninputs:\n  - name: activity\n    description: 承载 player 的 Activity\n    required: true\n  - name: player\n    description: GSYBaseVideoPlayer 或其子类\n    required: true\noutputs:\n  - OrientationUtils 已绑定；点击\"全屏\"按钮可以正确进入 window fullscreen 副本；返回键与旋转都能正确复位。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationUtils.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationOption.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseActivityDetail.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java\n---\n\n# gsy-fullscreen-orientation\n\n## 概念\n\n- **视觉全屏**：`player.startWindowFullscreen(context, hideActionBar, hideStatusBar)`，会用反射克隆一个同类 player 塞进 `android.R.id.content` 顶部，返回原实例。\n- **屏幕方向**：`OrientationUtils` 监听 `OrientationEventListener` + `Settings.System.ACCELEROMETER_ROTATION`，负责旋转 Activity 方向。\n- **两者可解耦**：只想旋转不切副本 → `setNeedOrientationUtils(false)` + 自建 `OrientationUtils`；只想切副本不旋转 → 不构造 `OrientationUtils`，`player` 保持默认 `setRotateViewAuto(false)`。\n\n## 核心 API\n\n| 类 / 方法 | 位置 | 说明 |\n|---|---|---|\n| `OrientationUtils(Activity, GSYBaseVideoPlayer)` | [OrientationUtils#L50-L52](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationUtils.java) | 主构造 |\n| `OrientationUtils(Activity, player, OrientationOption)` | [OrientationUtils#L54-L64](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationUtils.java) | 支持自定义旋转角度阈值 |\n| `setEnable(boolean)` | `OrientationUtils` | 使能/失能自动旋转 |\n| `setRotateWithSystem(boolean)` | `OrientationUtils` | 是否跟随系统\"旋转开关\" |\n| `setIsOnlyRotateLand(boolean)` | `OrientationUtils` | true 时仅在横屏方向变化时响应 |\n| `resolveByClick()` | `OrientationUtils` | 用户点全屏按钮时手动触发一次旋转 |\n| `backToProtVideo()` | `OrientationUtils` | 返回键触发时把方向复位竖屏 |\n| `setIsPause(boolean)` | `OrientationUtils` | 生命周期 pause/resume 桥接 |\n| `releaseListener()` | `OrientationUtils` | `onDestroy` 释放监听 |\n| `startWindowFullscreen(Context, boolean, boolean)` | [GSYBaseVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java) | 进入 window 全屏，返回克隆体 |\n| `GSYVideoManager.backFromWindowFull(Context)` | [GSYVideoManager#L82-L94](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java) | 返回键统一处理 |\n| `GSYVideoManager.isFullState(Activity)` | [GSYVideoManager#L143-L151](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java) | 当前是否全屏 |\n\n## `OrientationOption` 参数\n\n| 字段 | 默认 | 作用 |\n|---|---|---|\n| `normalPortraitAngleStart / End` | 5 / 355 | 判定竖屏区间 |\n| `normalLandAngleStart / End` | 85 / 95 | 判定正向横屏区间 |\n| `reverseLandAngleStart / End` | 265 / 275 | 判定反向横屏区间 |\n\n区间越大越\"灵敏\"，太大会抖动。\n\n## 关键调用序（详情页最佳实践）\n\n1. `onCreate`：`orientationUtils = new OrientationUtils(this, player)`; `orientationUtils.setEnable(false)`（进页面不自动旋转）\n2. 全屏按钮点击：`orientationUtils.resolveByClick()` → `player.startWindowFullscreen(this, true, true)`\n3. `onBackPressed`：`orientationUtils.backToProtVideo()` → `GSYVideoManager.backFromWindowFull(this)`\n4. `onPause`：`orientationUtils.setIsPause(true)`\n5. `onResume`：`orientationUtils.setIsPause(false)`\n6. `onDestroy`：`orientationUtils.releaseListener()`\n\n`GSYBaseActivityDetail` 已把 1/2/3/4/5/6 全套代码写好，直接继承最省事。\n\n## Demo 对照\n\n- 最标准的全屏 + 旋转：[DetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java)\n- 只旋转不切副本：[LandLayoutVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/LandLayoutVideo.java) + [DetailNormalActivityPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailNormalActivityPlayer.java)\n- 反射克隆入口：[GSYBaseVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java)（`startWindowFullscreen` → `getConstructor(Context.class, Boolean.class)`）\n\n## v14.0.0 折叠屏示例\n\nXML [FoldDetailActivity](../../app/src/main/java/com/example/gsyvideoplayer/FoldDetailActivity.java) 与 Compose [FoldComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/FoldComposeActivity.kt) 按 FoldingFeature 实现 BOOK/TABLETOP 分屏、铰链分隔与全屏。XML 全屏克隆布局通过 `LandLayoutVideo` 传递姿态；普通全屏 API 不会自动给任意业务页面添加折叠布局。用法及测试注入边界见 [RECENT_FEATURES.md](../../doc/RECENT_FEATURES.md)。\n\n## 常见坑\n\n- **黑屏一瞬**：`startWindowFullscreen` 用反射构造新 player，若你的子类只暴露 `(Context)` 构造且未加 `(Context, Boolean)`，R8 混淆后必炸。ProGuard 里必须保留：`-keep class * extends GSYBaseVideoPlayer { public <init>(android.content.Context); public <init>(android.content.Context, java.lang.Boolean); }`\n- **旋转停不下来**：忘了 `orientationUtils.setIsPause(true)` 或 `releaseListener()`。\n- **系统关了自动旋转但还想手动横屏**：`setRotateWithSystem(false)`。\n- **竖屏视频被强制横屏**：`setAutoFullWithSize(true)` 让 SDK 自适应。\n","skills/01-gsy-quickstart/SKILL.md":"---\nname: gsy-quickstart\ndescription: 从零把 GSYVideoPlayer 塞进一个 Activity 或 Fragment，跑通「setUp → startPlayLogic → release」最小闭环。\nwhen_to_use: 用户第一次在项目里集成 GSYVideoPlayer；只要能播一个 mp4 就行；或需要确认基础生命周期是否接对。\ninputs:\n  - name: play_url\n    description: 视频地址（http/https/file/asset/rtmp/rtsp）\n    required: true\n  - name: container\n    description: 承载 View 的容器（一般是 FrameLayout）\n    required: true\noutputs:\n  - StandardGSYVideoPlayer 实例已 setUp 并可控（play/pause/release）\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/PlayActivity.java\n---\n\n# gsy-quickstart\n\n## 依赖\n\n- 推荐整包：`io.github.carguo:gsyvideoplayer:14.0.0`（已含 Java、Exo2、三 ABI ex_so）。\n- 分模块主库：`io.github.carguo:gsyvideoplayer-java:14.0.0`。\n- 分模块 IJK so：选 `io.github.carguo:gsyvideoplayer-ex_so:14.0.0`，或所需独立 artifact `gsyvideoplayer-arm64` / `gsyvideoplayer-armv7a` / `gsyvideoplayer-x64`，不重复引入同 ABI 两套库。\n- 默认 ABI 为 `arm64-v8a` / `armeabi-v7a` / `x86_64`（FFmpeg n5.1.10 / OpenSSL 3.5.9）；旧 armv5/x86 未升级，仅旧架构项目显式使用。\n- 权限：`INTERNET`、`WAKE_LOCK`、`ACCESS_NETWORK_STATE`（AndroidManifest）\n\n明细见 [doc/DEPENDENCIES.md](../../doc/DEPENDENCIES.md)。\n\n## 核心 API\n\n| 类 / 方法 | 位置 | 作用 |\n|---|---|---|\n| `StandardGSYVideoPlayer(Context)` | [StandardGSYVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java) | 主用 UI 组件，默认布局在 `R.layout.video_layout_standard` |\n| `setUp(url, cacheWithPlay, title)` | [GSYVideoView#L438-L451](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 最短形式的地址绑定 |\n| `setUp(url, cacheWithPlay, cachePath, title)` | [GSYVideoView#L477-L488](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 支持自定义缓存目录 |\n| `setUp(url, cacheWithPlay, cachePath, headers, title)` | [GSYVideoView#L453-L475](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 附带 HTTP header |\n| `startPlayLogic()` | `StandardGSYVideoPlayer`（继承自 `GSYVideoControlView`） | 触发 prepare→start 全流程 |\n| `getCurrentPlayer()` | `GSYBaseVideoPlayer` | 获取\"当前活跃 player\"（可能是全屏克隆体） |\n| `onVideoPause()` / `onVideoResume()` / `onVideoResume(boolean seek)` | [GSYVideoView#L518-L577](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 对应 Activity `onPause / onResume`；直播场景传 `seek=false` |\n| `release()` | [GSYVideoView#L818](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 释放当前 Player 与 Surface |\n| `GSYVideoManager.releaseAllVideos()` | [GSYVideoManager#L99-L104](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java) | 全局收尾（切页面时兜底） |\n| `GSYVideoManager.onPause() / onResume()` | [GSYVideoManager#L110-L135](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java) | 全局暂停/恢复所有 GSY player |\n\n## 参数表：`setUp(...)`\n\n| 参数 | 类型 | 说明 |\n|---|---|---|\n| `url` | `String` | 支持 http/https/file/asset/rtmp/rtsp/自定义 scheme；`ProxyCacheManager` 会自动拦截可缓存的普通 http |\n| `cacheWithPlay` | `boolean` | true → 边播边缓；对直播 / m3u8 / DRM 建议 false |\n| `cachePath` | `File?` | 自定义缓存目录，默认 `context.cacheDir/videoCache` |\n| `mapHeadData` | `Map<String,String>?` | HTTP 请求头（Referer、Cookie、Auth 等） |\n| `title` | `String` | 顶部标题栏文案，会显示在 `R.id.title` |\n\n返回值：`boolean`，false 表示 URL 为空或与当前 tag 相同（防重复 setup）。\n\n## 最小调用序（Activity 生命周期）\n\n1. `onCreate` → `player.setUp(...)` → 可选 `player.setThumb...`、`setPlayTag()` → `player.startPlayLogic()`\n2. `onPause` → `player.getCurrentPlayer().onVideoPause()`\n3. `onResume` → `player.getCurrentPlayer().onVideoResume()`（直播传 `false`）\n4. `onBackPressed` → 若在全屏：`GSYVideoManager.backFromWindowFull(this)` 返回 true 就吞掉事件\n5. `onDestroy` → `player.getCurrentPlayer().release()` + `GSYVideoManager.releaseAllVideos()`\n\n## Demo 对照\n\n- 最短一屏：[PlayActivity](../../app/src/main/java/com/example/gsyvideoplayer/PlayActivity.java)\n- Fragment：[VideoFragment](../../app/src/main/java/com/example/gsyvideoplayer/fragment/VideoFragment.java) + [FragmentVideoActivity](../../app/src/main/java/com/example/gsyvideoplayer/FragmentVideoActivity.java)\n- 空控件模式（自绘 UI）：[PlayEmptyControlActivity](../../app/src/main/java/com/example/gsyvideoplayer/PlayEmptyControlActivity.java) + [EmptyControlVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/EmptyControlVideo.java)\n\n## 常见坑\n\n- 忘记 `release()` → dex 泄漏 + surface 崩溃：一定在 `onDestroy` 兜底。\n- 不覆盖 `getCurrentPlayer()` 直接调 `player.onVideoPause()`：全屏切回后指令发到\"影子\"实例，导致状态错乱。\n- `setUp` 传空 title 走 `null`，会隐藏顶部栏（`View.GONE`），不会崩。\n","skills/11-gsy-compose-integration/SKILL.md":"---\nname: gsy-compose-integration\ndescription: 在 Jetpack Compose 中通过 AndroidView 桥接 GSYVideoPlayer，覆盖列表、竖直短视频、生命周期管控。\nwhen_to_use: 项目已经或正在迁移到 Compose；需要在可组合项里嵌入播放器；需要复用列表滑动/自动播放逻辑但 UI 用 Compose。\ninputs:\n  - name: url\n    description: 视频 URL\n    required: true\noutputs:\n  - Composable 内嵌 GSYVideoPlayer，生命周期与 Composable 一致。\nreferences:\n  - file: gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose\n  - file: app/src/main/java/com/example/gsyvideoplayer/compose\n---\n\n# gsy-compose-integration\n\n## 模块\n\n`gsyVideoPlayer-compose` 是 Compose 桥接层，两种模式并存：\n\n| 模式 | 入口 | 说明 |\n|---|---|---|\n| **Wrapper**（推荐上手） | [GSYVideoPlayerView](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/wrapper/GSYVideoPlayerView.kt) / [GSYAnyVideoPlayerView](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/wrapper/GSYAnyVideoPlayerView.kt) | `AndroidView` 直接承载 `StandardGSYVideoPlayer` / 任意 `NormalGSYVideoPlayer` 子类，保留全部内置能力 |\n| **Native**（纯 Compose 控件） | [rememberGSYPlayerController](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerSurface.kt) + [GSYPlayerSurface](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerSurface.kt) + [GSYDefaultControls](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYDefaultControls.kt) | 只承载画面，控制层完全用 Compose 自绘 |\n| **通用工具** | [LifecycleBridge](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/common/LifecycleBridge.kt) | `LifecycleEffect { event -> ... }` 用于订阅宿主 Lifecycle |\n\n## Wrapper API：`GSYVideoPlayerView`\n\n见 [GSYVideoPlayerView#L28-L36](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/wrapper/GSYVideoPlayerView.kt)：\n\n| 参数 | 类型 | 说明 |\n|---|---|---|\n| `modifier` | `Modifier` | 通常 `Modifier.fillMaxWidth().aspectRatio(16f/9)` |\n| `setUp` | `(StandardGSYVideoPlayer) -> Unit` | 等价 `GSYVideoOptionBuilder.build(player)` |\n| `setUpKey` | `Any?` = null | 非空时其变化会重新调用 `setUp`（典型：传 url） |\n| `autoReleaseOnDispose` | Boolean = true | 离屏自动 `player.release()` |\n| `autoPauseResume` | Boolean = true | 自动桥 Lifecycle：`ON_PAUSE→GSYVideoManager.onPause()`、`ON_RESUME→onResume()` |\n| `onPlayerCreated` | `(StandardGSYVideoPlayer) -> Unit` | 拿到 player 实例的回调 |\n\n## Wrapper API：`GSYAnyVideoPlayerView<T : NormalGSYVideoPlayer>`\n\n见 [GSYAnyVideoPlayerView#L26-L35](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/wrapper/GSYAnyVideoPlayerView.kt)。相比 `GSYVideoPlayerView` 多出：\n\n| 参数 | 类型 | 说明 |\n|---|---|---|\n| `factory` | `(Context) -> T` | 传自定义子类（如 `ListGSYVideoPlayer`、`SampleControlVideo`、`DanmakuVideoPlayer`） |\n\n## Native API：`rememberGSYPlayerController`\n\n见 [rememberGSYPlayerController#L26-L32](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerSurface.kt)：\n\n| 参数 | 默认 | 说明 |\n|---|---|---|\n| `url` | null | 非空时内部 `LaunchedEffect` 触发 `controller.setUp(...)` |\n| `cacheWithPlay` | false | 边播边缓 |\n| `title` | \"\" | 标题（透传给底层 player） |\n| `autoPlay` | false | attach 完成后自动 `startPlayLogic` |\n| `autoPauseResume` | true | 自动订阅 Lifecycle（同 wrapper） |\n\n返回 [GSYPlayerController](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerController.kt)，可以再调 `.play() / .pause() / .seekTo(ms) / .setSpeed(...) / .changeTextureViewShowType() / .setUp(url, cache, title, autoPlay) / .dispose()`；状态订阅走 [GSYPlayerState](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerState.kt)（`StateFlow<GSYPlayerState>`），事件走 [GSYPlayerEvent](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerEvent.kt)。\n\n## 最小可运行示例\n\n**Wrapper 模式**：\n\n```kotlin\nGSYVideoPlayerView(\n    modifier = Modifier.fillMaxWidth().aspectRatio(16f/9),\n    setUpKey = url,\n    setUp = { player ->\n        GSYVideoOptionBuilder()\n            .setUrl(url)\n            .setCacheWithPlay(true)\n            .setVideoTitle(\"demo\")\n            .setIsTouchWiget(true)\n            .setAutoFullWithSize(true)\n            .build(player)\n        player.startPlayLogic()\n    }\n)\n```\n\n**Native 模式**：\n\n```kotlin\nval controller = rememberGSYPlayerController(url = url, autoPlay = true)\nBox(Modifier.fillMaxWidth().aspectRatio(16f/9)) {\n    GSYPlayerSurface(controller = controller, modifier = Modifier.matchParentSize())\n    GSYDefaultControls(controller = controller)\n}\n```\n\n## 生命周期最佳实践\n\n```kotlin\nval lifecycle = LocalLifecycleOwner.current.lifecycle\nDisposableEffect(lifecycle) {\n    val obs = LifecycleEventObserver { _, e ->\n        when (e) {\n            Lifecycle.Event.ON_PAUSE  -> state.pause()\n            Lifecycle.Event.ON_RESUME -> state.resumeIfNeeded()\n            Lifecycle.Event.ON_DESTROY-> state.release()\n            else -> Unit\n        }\n    }\n    lifecycle.addObserver(obs)\n    onDispose { lifecycle.removeObserver(obs) }\n}\n```\n\n## Demo 对照\n\n| 场景 | Activity |\n|---|---|\n| 原生桥接示例 | [DetailNativeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/DetailNativeActivity.kt) / [BasicWrapperActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/BasicWrapperActivity.kt) / [FullFeatureNativeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/FullFeatureNativeActivity.kt) |\n| 广告 + 主片（列表内） | [AdInListComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/AdInListComposeActivity.kt) |\n| 列表 + 全屏 | [ListWithFullscreenActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/ListWithFullscreenActivity.kt) / [ListPlayNativeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/ListPlayNativeActivity.kt) / [AutoPlayListActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/AutoPlayListActivity.kt) |\n| 抖音式竖屏 | [VerticalShortVideoComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/VerticalShortVideoComposeActivity.kt) |\n| 滤镜 | [DetailFilterComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/DetailFilterComposeActivity.kt) |\n| 无缝切源 / Exo 切源 | [SwitchSeamlessComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/SwitchSeamlessComposeActivity.kt) / [ExoSwitchSourceComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/ExoSwitchSourceComposeActivity.kt) / [SwitchUrlActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/SwitchUrlActivity.kt) |\n| 字幕 | [SubtitleComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/SubtitleComposeActivity.kt) |\n| 弹幕 | [DanmakuComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/DanmakuComposeActivity.kt) |\n| 缓存 / 下载 | [CacheDownloadComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/CacheDownloadComposeActivity.kt) |\n| 硬解 / MediaCodec | [MediaCodecComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/MediaCodecComposeActivity.kt) |\n| 悬浮小窗 | [FloatingWindowComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/FloatingWindowComposeActivity.kt) |\n| 音频独立 | [AudioOnlyComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/AudioOnlyComposeActivity.kt) |\n| 多类型混排 | [MoreTypeComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/MoreTypeComposeActivity.kt) |\n| 多窗口并行 | [MultiWindowActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/MultiWindowActivity.kt) / [MultiWindowParallelComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/MultiWindowParallelComposeActivity.kt) |\n| 本地文件 | [LocalFileComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/LocalFileComposeActivity.kt) |\n| 自定义主题 | [CustomControlsThemeComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/CustomControlsThemeComposeActivity.kt) |\n| WebView 详情 | [WebDetailComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/WebDetailComposeActivity.kt) |\n\n样例 hosts 全在 [compose/host/](../../app/src/main/java/com/example/gsyvideoplayer/compose/host)。\n\n## v14.0.0 状态与接入\n\n外部坐标为 `io.github.carguo:gsyvideoplayer-compose:14.0.0`（GitHub Packages 使用 `com.shuyu`）。IJK 使用方还需选择 `ex_so` 或独立 ABI 原生模块；完整组合见 [DEPENDENCIES.md](../../doc/DEPENDENCIES.md)。\n\n`detachHost` 只解绑 View 侧回调/tick，`dispose` 统一释放保留的 host；`snapshot.bufferPercent` 合并内核轮询与回调值。`controller.changeTextureViewShowType()` 在主线程更新当前内嵌/全屏比例。Compose Demo 共 25 项，新增折叠屏 BOOK/TABLETOP。详见 [COMPOSE_USE.md](../../doc/COMPOSE_USE.md)。\n\n## 常见坑\n\n- Compose 频繁重组 → `AndroidView` 的 `factory` 只跑一次；不要把 `setUp` 放在 `factory` 里，放到 `update` block，并用 `remember(url)` 做去重。\n- `AndroidView` 内部布局要指定固定高度或 `aspectRatio`，否则 SurfaceView 会 0 高度导致黑屏。\n- 竖直短视频 + `Pager`：切页时用 `pagerState.currentPage` 触发 `state.play(url) / previousState.pause()`；释放放在 `DisposableEffect(pagerState)` 里。\n- 与 Compose 主题深浅色切换：`GSYVideoPlayerCompose` 是原生 View 层，配色需要单独 `setBottomProgressBarDrawable` 等 API。\n","skills/10-gsy-cast-and-loopback/SKILL.md":"---\nname: gsy-cast-and-loopback\ndescription: 使用内核层投屏 SPI（CastCapability + CastProvider）接入 DLNA，或做本地 loopback 回环模拟被投屏端。\nwhen_to_use: 需要接入 DLNA/AirPlay/Chromecast 到已有播放页；做投屏发起端 UI；做集成测试的本地 loopback 接收端；或做多协议聚合发现。\ninputs:\n  - name: providers\n    description: 一个或多个 CastProvider 实例（DLNA 默认由 jUPnP 提供）\n    required: true\noutputs:\n  - CastCapability 单例已注册 provider，可发现设备、连接、控制播放/停止。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastProvider.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastSession.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastListener.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastMediaInfo.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastState.java\n  - file: gsyVideoPlayer-cast/src/main/java/com/shuyu/gsyvideoplayer/cast/dlna\n  - file: app/src/main/java/com/example/gsyvideoplayer/CastDemoActivity.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/cast\n  - file: doc/CAST_FEATURE_PLAN.md\n---\n\n# gsy-cast-and-loopback\n\n## 顶层架构\n\n```\nCastCapability (单例)  ← 主线程 API 入口\n  ├─ registerProvider(CastProvider)\n  ├─ startDiscovery(Context) / stopDiscovery()\n  ├─ getAvailableDevices(): List<CastDevice>\n  ├─ connect(device, ConnectCallback) : CastSession（异步）\n  ├─ addListener(CastListener)\n  └─ disconnect()\n```\n\n对齐 Media3 `Player` 语义：`CastCapability ≈ Player 池`、`activeSession ≈ 当前 Player`、`CastProvider ≈ 渲染后端`、`CastListener/SessionListener ≈ Player.Listener`。见 [CastCapability](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) 与 [CastProvider](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastProvider.java) 的头 JavaDoc。\n\n## 核心 API\n\n### CastCapability\n\n| 方法 | 位置 | 说明 |\n|---|---|---|\n| `getInstance()` | [#L38-L47](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | DCL 单例 |\n| `registerProvider(CastProvider)` | [#L85-L90](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 注册；`protocol` 相同则覆盖 |\n| `unregisterProvider(String)` | [#L93-L103](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 反注册并停止其发现 |\n| `startDiscovery(Context)` | [#L106-L125](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 所有已注册 provider 同时开始发现 |\n| `stopDiscovery()` | [#L128-L135](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 全部停止 |\n| `getAvailableDevices()` | [#L138-L146](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 快照，跨 provider 聚合 |\n| `connect(CastDevice, ConnectCallback)` | [#L154-L183](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 会自动 disconnect 旧会话 |\n| `disconnect()` | [#L186-L196](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | release 当前 session |\n| `getActiveSession()` | [#L199-L201](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 拿当前会话（可能 null） |\n| `addListener / removeListener` | [#L203-L209](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 主线程 listener |\n\n### CastProvider（SPI 契约）\n\n| 方法 | 说明 |\n|---|---|\n| `getProtocol(): String` | 全小写协议 id（`dlna` / `chromecast` / `airplay`） |\n| `startDiscovery(Context, DiscoveryListener)` | 后台线程订阅，回调 postMain |\n| `stopDiscovery()` | 释放订阅、解绑 Service |\n| `connect(CastDevice, ConnectCallback): CastSession` | 建立会话；同步返回 session，异步回调结果 |\n\n线程约定：所有 network IO 由 provider 自己后台跑，回调都 postMain。\n\n### CastSession（媒体控制）\n\n见 [CastSession](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastSession.java)：\n\n| 方法 | 说明 |\n|---|---|\n| `load(CastMediaInfo)` | 推送要投的 URL |\n| `play() / pause() / stop()` | 播控 |\n| `seekTo(long ms)` | 定位 |\n| `setVolume(int 0-100)` / `setMute(boolean)` | 音量 |\n| `getState(): CastState` | IDLE / CONNECTING / CONNECTED / LOADING / PLAYING / PAUSED / STOPPED / ERROR，见 [CastState](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastState.java) |\n| `addListener(SessionListener)` | 位置/状态回调 |\n| `release()` | 归还 |\n\n### CastMediaInfo（要投的媒体）\n\n见 [CastMediaInfo](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastMediaInfo.java)。字段：`url`、`title`、`durationMs`、`mimeType`、`headers`、`metadata`（`Map`）。\n\n## DLNA 参考实现\n\n模块 `gsyvideoplayer-cast` 提供 [JupnpDlnaProvider](../../gsyVideoPlayer-cast/src/main/java/com/shuyu/gsyvideoplayer/cast/dlna/JupnpDlnaProvider.java) 与 [JupnpDlnaSession](../../gsyVideoPlayer-cast/src/main/java/com/shuyu/gsyvideoplayer/cast/dlna/JupnpDlnaSession.java)。业务代码调用：\n\n```java\nCastCapability cap = CastCapability.getInstance();\ncap.registerProvider(new JupnpDlnaProvider());\ncap.addListener(myListener);\ncap.startDiscovery(context);\n```\n\n发现事件：`CastListener#onDeviceListChanged(List<CastDevice>)`；连接：`cap.connect(devices.get(0), connectCallback)`。\n\n## 本地 Loopback（Demo 测试用）\n\napp 模块下的 [cast/](../../app/src/main/java/com/example/gsyvideoplayer/cast) 提供**可发现的接收端**，方便在同一台设备上完成集成测试：\n\n- [DevReceiverService](../../app/src/main/java/com/example/gsyvideoplayer/cast/DevReceiverService.java) —— foreground service，通过 jUPnP 注册\"回环设备\"。\n- [LoopbackDeviceFactory](../../app/src/main/java/com/example/gsyvideoplayer/cast/LoopbackDeviceFactory.java) —— 构造 UPnP MediaRenderer。\n- [LoopbackAvTransportService](../../app/src/main/java/com/example/gsyvideoplayer/cast/LoopbackAvTransportService.java) / [LoopbackRenderingControlService](../../app/src/main/java/com/example/gsyvideoplayer/cast/LoopbackRenderingControlService.java) —— AVTransport / RenderingControl 实现。\n- [CastReceiverFloatingWindow](../../app/src/main/java/com/example/gsyvideoplayer/cast/CastReceiverFloatingWindow.java) + [CastReceiverPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/CastReceiverPlayer.java) —— 可视化播放。\n\n## Demo 对照\n\n- 一体化：[CastDemoActivity](../../app/src/main/java/com/example/gsyvideoplayer/CastDemoActivity.java)\n- Cast 相关控制页：[SampleCastControlVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/SampleCastControlVideo.java)\n- 设计与测试：[doc/CAST_FEATURE_PLAN.md](../../doc/CAST_FEATURE_PLAN.md)、[doc/CAST_RECEIVER_DESIGN.md](../../doc/CAST_RECEIVER_DESIGN.md)、[doc/CAST_TEST_PLAYBOOK.md](../../doc/CAST_TEST_PLAYBOOK.md)\n\n## 常见坑\n\n- 不同 provider 上报的 `CastDevice` 需要用 `getProtocol()` 分组；直接把陌生 protocol 传给 `connect` 会拿到 `IllegalStateException`。\n- Provider 网络 IO 必须在自己线程，`CastCapability` 不做 main 切换。\n- Wi-Fi 多播锁没拿 → DLNA 发现不到设备：Provider 需要 `WifiManager.MulticastLock`。\n- Loopback 与真实设备同网段时会同时被发现，Demo 里有过滤示例。\n","skills/07-gsy-render-and-effects/SKILL.md":"---\nname: gsy-render-and-effects\ndescription: 三种渲染载体（TextureView / SurfaceView / GLSurfaceView）、显示比例（16:9/4:3/full/custom）、GL 滤镜与自定义 Shader。\nwhen_to_use: 想放竖屏视频、需要 GL 滤镜、要做全屏裁减/拉伸、SurfaceView 与动画冲突、想抓截图或 gif。\ninputs:\n  - name: render_type\n    description: TEXTURE / SURFACE / GLSURFACE\n    required: true\n  - name: show_type\n    description: 显示比例枚举\n    required: false\noutputs:\n  - 渲染载体 + 显示比例 + 滤镜均按需生效。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/GSYRenderView.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/view/GSYVideoGLView.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/effect\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailFilterActivity.java\n---\n\n# gsy-render-and-effects\n\n## 渲染类型 `GSYVideoType.setRenderType(int)`\n\n| 常量 | 值 | 说明 |\n|---|---|---|\n| `TEXTURE` | 0 | 默认，与动画/圆角/缩放兼容最好 |\n| `SURFACE` | 1 | 性能最好，但和 View 动画冲突 |\n| `GLSURFACE` | 2 | 必需，若要用 `setEffectFilter` |\n\n见 [GSYVideoType#L39-L49](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java)。设置时机：`setUp` 之前，全局生效。\n\n## 显示比例 `GSYVideoType.setShowType(int)`\n\n| 常量 | 值 | 说明 |\n|---|---|---|\n| `SCREEN_TYPE_DEFAULT` | 0 | 按视频原比例居中 |\n| `SCREEN_TYPE_16_9` | 1 | 强制 16:9 |\n| `SCREEN_TYPE_4_3` | 2 | 强制 4:3 |\n| `SCREEN_TYPE_18_9` | 6 | 全面屏 |\n| `SCREEN_TYPE_FULL` | 4 | 全屏裁减；`surface_container` 建议 `FrameLayout` |\n| `SCREEN_MATCH_FULL` | -4 | 全屏拉伸；`surface_container` 建议 `FrameLayout` |\n| `SCREEN_TYPE_CUSTOM` | -5 | 需先 `setScreenScaleRatio(float)` |\n\n见 [GSYVideoType#L10-L29](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java) 与 [GSYVideoType#L165-L167](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java)。\n\n## 硬解码开关\n\n| 静态方法 | 说明 |\n|---|---|\n| `enableMediaCodec()` / `disableMediaCodec()` | IJK 硬解码总开关 [#L71-L79](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java) |\n| `enableMediaCodecTexture()` / `disableMediaCodecTexture()` | 硬解 + Texture 直渲染优化 [#L100-L108](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java) |\n| `enableSmartMediaCodec()` / `disableSmartMediaCodec()` | 硬解失败自动软解回退 [#L86-L94](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java) |\n| `isMediaCodec()` / `isMediaCodecTexture()` / `isSmartMediaCodec()` | 查询 |\n\n设置时机：**`setUp` 之前**，且**部分机型**需要在 `Application.onCreate` 就配好。\n\n## GL 滤镜\n\n需要满足两个条件：\n1. `GSYVideoType.setRenderType(GSYVideoType.GLSURFACE)`\n2. `builder.setEffectFilter(new XxxEffect())` 或 `player.setEffectFilter(...)`\n\nSDK 内置滤镜（在 `com.shuyu.gsyvideoplayer.render.effect.*`）：`AutoFixEffect`、`BarrelBlurEffect`、`BlackAndWhiteEffect`、`BrightnessEffect`、`ContrastEffect`、`CrossProcessEffect`、`DocumentaryEffect`、`DuotoneEffect`、`FillLightEffect`、`GammaEffect`、`GaussianBlurEffect`、`GrainEffect`、`GreyScaleEffect`、`HueEffect`、`InvertColorsEffect`、`LamoishEffect`、`NoEffect`、`OverlayEffect`、`PosterizeEffect`、`SampleBlurEffect`、`SaturationEffect`、`SepiaEffect`、`SharpnessEffect`、`TemperatureEffect`、`TintEffect`、`VignetteEffect`。目录：[render/effect](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/effect)。\n\n自定义 Shader：实现 [`GSYVideoGLView.ShaderInterface`](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/view/GSYVideoGLView.java)。Demo 里 4 个复杂样例：[GSYVideoGLViewCustomRender](../../app/src/main/java/com/example/gsyvideoplayer/effect/GSYVideoGLViewCustomRender.java) / 2 / 3 / 4，以及像素化 [PixelationEffect](../../app/src/main/java/com/example/gsyvideoplayer/effect/PixelationEffect.java)、BitmapOverlay [BitmapEffect](../../app/src/main/java/com/example/gsyvideoplayer/effect/BitmapEffect.java) / [BitmapIconEffect](../../app/src/main/java/com/example/gsyvideoplayer/effect/BitmapIconEffect.java)。\n\n## v14.0.0 多 pass 与新效果\n\n- `GSYVideoGLViewMultiPassRender` + `setMultiPassEffect` + `player.setCustomGLRenderer` 接入 `GaussianBlurMultiPassEffect` / `IterativeBlurPyramidEffect` / `BloomEffect`。这些不是单 pass `ShaderInterface` 滤镜，不能直接传给 `setEffectFilter`。\n- `LookupEffect(assetPath, intensity)` / `BeautyEffect(smoothLevel, whiteLevel)` / `GlitchEffect` / `CrtEffect` / `OldTvSignalEffect` 使用 `setEffectFilter`。LUT 必须由使用方放入自己的 assets；Demo 的三张图不在播放器 AAR 中。\n- `TextureShaderInterface` 在 GL 线程管理额外纹理；动态效果使用渲染器按秒绑定的 `uTime`。\n- 显示比例变化后，传统 View 调 `player.getCurrentPlayer().changeTextureViewShowType()`；Compose 调 `controller.changeTextureViewShowType()`。\n- 完整代码入口和实现文档见 [RECENT_FEATURES.md](../../doc/RECENT_FEATURES.md#v1400-gl-管线与新滤镜)。\n\n## 抓帧 / gif / 截图\n\n在 [GSYRenderView](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/GSYRenderView.java)：\n\n| 方法 | 回调 | 说明 |\n|---|---|---|\n| `taskShotPic(GSYVideoShotListener)` | 返 Bitmap | 单帧截图 |\n| `saveFrame(File, GSYVideoShotSaveListener)` | 保存到磁盘 | 落盘截图 |\n| `taskGifPic(...)` + `GifCreateHelper` | 生成 gif | 见 [GifCreateHelper](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GifCreateHelper.java) |\n\n## Demo 对照\n\n- 滤镜集合：[DetailFilterActivity](../../app/src/main/java/com/example/gsyvideoplayer/DetailFilterActivity.java) + [DetailFilterComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/DetailFilterComposeActivity.kt)\n- 透明视频：[SampleTransparentVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/SampleTransparentVideo.java) + [DetailTransparentActivity](../../app/src/main/java/com/example/gsyvideoplayer/DetailTransparentActivity.java)\n- 自定义 Render：[CustomRenderVideoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/CustomRenderVideoPlayer.java)\n\n## 常见坑\n\n- `SurfaceView` 在 `RecyclerView` 里滑动会撕裂 / 覆盖 → 列表用 `TEXTURE`。\n- GL 效果的性能取决于分辨率、pass 数和设备；先评估 pass/金字塔层数与实际帧率，不能通过静默换成 TextureView 宣称效果已生效。\n- 竖屏视频强制 `SCREEN_TYPE_FULL` 会大幅裁减；建议 `SCREEN_TYPE_DEFAULT` + `setAutoFullWithSize(true)`。\n","skills/12-gsy-custom-view/SKILL.md":"---\nname: gsy-custom-view\ndescription: 自定义播放器 UI（继承 StandardGSYVideoPlayer）与自定义控制层：换 seekbar、加弹幕层、加清晰度切换、加投屏按钮等。\nwhen_to_use: SDK 默认 UI 不符合需求；需要在标准控制层上加自定义控件；需要多状态视图（loading/error/paywall）。\ninputs:\n  - name: layout_id\n    description: 自定义 XML 布局资源 id\n    required: true\noutputs:\n  - 一个继承 GSYBaseVideoPlayer 子类的自定义播放器组件。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/NormalGSYVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/SampleCoverVideo.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/SampleControlVideo.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/LandLayoutVideo.java\n---\n\n# gsy-custom-view\n\n## 继承层次\n\n```\nGSYTextureRenderView（渲染管理）\n  └── GSYVideoView（状态机 / 生命周期）\n        └── GSYVideoControlView（默认控制层：进度条/按钮）\n              └── GSYVideoPlayer（触摸手势）\n                    └── GSYBaseVideoPlayer（全屏/小屏切换）\n                          ├── StandardGSYVideoPlayer（默认 UI）\n                          └── NormalGSYVideoPlayer（无返回/无标题的简版）\n```\n\n自定义组件通常继承 [StandardGSYVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java) 或 [NormalGSYVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/NormalGSYVideoPlayer.java)。\n\n## 必须实现的三件事\n\n### 1) 自定义布局 & id\n\n覆写：\n```java\n@Override public int getLayoutId() { return R.layout.my_video_player; }\n```\n布局里必须包含（id 不能改）：\n- `@id/surface_container` —— 放渲染 View（FrameLayout）\n- 需要用到的默认控件（如 `@id/start`、`@id/progress`、`@id/current`、`@id/total`、`@id/bottom_progressbar`、`@id/thumb`、`@id/small_close`、`@id/back`、`@id/fullscreen`、`@id/loading`）—— 缺哪个哪个功能失效，编译不报错。\n\n### 2) 覆写全屏 & 小屏组件类\n\n关键：`getConstructor(Context, Boolean)` 是 SDK 内部反射拷贝出的全屏实例。**必须提供两个构造器**：\n\n```java\npublic MyVideoPlayer(Context c) { super(c); }\npublic MyVideoPlayer(Context c, Boolean fullFlag) { super(c, fullFlag); }\npublic MyVideoPlayer(Context c, AttributeSet a) { super(c, a); }\n```\n\nR8 侧已在 [proguard-rules.pro](../../app/proguard-rules.pro) 保留：\n```\n-keep class * extends com.shuyu.gsyvideoplayer.video.base.GSYBaseVideoPlayer {\n    public <init>(android.content.Context);\n    public <init>(android.content.Context, java.lang.Boolean);\n}\n```\n\n### 3) 覆写 `showSmallVideo() / hideSmallVideo()`（如需要小窗）以及 `startWindowFullscreen()`（如要全屏样式差异）\n\n## 常用重写点\n\n| 方法 | 作用 |\n|---|---|\n| `changeUiToNormal()` / `changeUiToPreparingShow()` / `changeUiToPlayingShow()` / `changeUiToPauseShow()` / `changeUiToCompleteShow()` / `changeUiToError()` / `changeUiToPlayingBufferingShow()` | 不同状态下的 UI 切换钩子 |\n| `onClickUiToggle()` | 单击 UI 显隐 |\n| `setStateAndUi(int state)` | 由 SDK 内部调，可 super 后加自己的逻辑 |\n| `updateStartImage()` | 播放/暂停按钮 icon 切换 |\n| `resolveTypeUI()` | 首帧比例决定后的自定义处理 |\n| `dismissBrightnessDialog / dismissVolumeDialog / dismissProgressDialog` | 手势弹窗 UI |\n| `hideAllWidget / setViewShowState` | 显隐所有控件 |\n\n## 手势自定义\n\n`GSYVideoControlView` 里通过如下开关切换：\n- `setIsTouchWiget(boolean)` —— 手势总开关（默认 true）\n- `setIsTouchWigetFull(boolean)` —— 只在全屏开\n- `setNeedLockFull(boolean)` —— 全屏锁按钮\n- `setSeekRatio(float)` —— 手势 seek 灵敏度\n- `setThreshold(int)` —— 触发手势阈值\n\n手势弹窗替换：覆写 `showBrightnessDialog / showVolumeDialog / showProgressDialog` 用自己的 Dialog。\n\n## 标准封面 / 清晰度 / 弹幕\n\n- 封面：Demo 组件 [SampleCoverVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/SampleCoverVideo.java) 演示用 Glide 加载封面。\n- 清晰度切换：Demo 组件 [SwitchVideo](../../app/src/main/java/com/example/gsyvideoplayer/switchplay/SwitchVideo.java) + [DetailListPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailListPlayer.java)（多列源，`setUp(List<GSYVideoModel>, ...)`）。\n- 弹幕层：Demo 组件 [DanmakuVideoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/DanmakuVideoPlayer.java) —— 在布局叠一层 `DanmakuView`，在 `onProgress` 里 `dispatch(long time)`。\n- 控制条自定义：Demo 组件 [SampleControlVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/SampleControlVideo.java)（毛玻璃底栏 + 播放按钮）。\n- 横屏专用布局：Demo 组件 [LandLayoutVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/LandLayoutVideo.java)（`getFullId()` 返回不同 layout）。\n\n## 多状态视图（loading / error / paywall）\n\n在 `changeUiToError()` 与 `changeUiToPreparingShow()` 里 `setVisibility` 自定义 View；也可以在 `onVideoAllCallBack.onPlayError` 中显式 `showErrorLayer()`。\n\n## 常见坑\n\n- 忘写 `Boolean` 构造器 → 全屏切换 `IllegalArgumentException` 或直接 crash（反射失败）。\n- 自定义 `layout` 缺 id → 默认功能沉默失效，问题不易定位。建议基于 `layout_standard_video_player.xml` 复制修改。\n- 全屏组件和非全屏组件类型不同 → 用 `getCurrentPlayer()` 获取\"当前活跃 player\"，别缓存 `this`。\n- 释放时机：`onDestroy` 里既要 `super.onBackPressed` 判断，也要 `GSYVideoManager.releaseAllVideos()`。\n","skills/09-gsy-live-and-ads/SKILL.md":"---\nname: gsy-live-and-ads\ndescription: 直播（RTMP/RTSP/HLS）、贴片广告 (PreRoll AD)、静音、切码率、无缝切源、切换过程保留最后一帧。\nwhen_to_use: 做直播；短视频/长视频前贴片广告；PV/UV 埋点后要 A/B 分发；快节奏内容切换。\ninputs:\n  - name: stream_url\n    description: 直播地址（rtmp://.../ rtsp://.../ .m3u8 等）\n    required: false\n  - name: ad_url\n    description: 前贴广告地址\n    required: false\noutputs:\n  - 直播不使用缓存；广告播放完切主片；无缝续播不闪黑。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYADVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYSampleADVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoADManager.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseADActivityDetail.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer2.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/KeepLastFrameDemoActivity.java\n---\n\n# gsy-live-and-ads\n\n## 直播接入要点\n\n- `setUp(url, false, title)` —— `cacheWithPlay` 必须 `false`。\n- `onResume` / `player.onVideoResume(false)`：**`seek=false` 避免拉流跳变**（见 [GSYVideoView#L549](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java)）。\n- 内核选择：\n  - RTMP / RTSP：**必须 IJK**；System / Exo / Aliyun 各有兼容性差异。\n  - HLS：Exo 更稳；Ijk 需要开启硬解 `GSYVideoType.enableMediaCodec()` 才顺。\n- 关键参数：\n  - `builder.setNeedShowWifiTip(false)`（后台 App 不打断）\n  - `builder.setReleaseWhenLossAudio(false)`（丢音频焦点不释放）\n  - `builder.setSurfaceErrorPlay(true)`\n\n对 IJK 加 low-latency：通过 `VideoOptionModel` 注入 ffmpeg option，见 [BasePlayerManager](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/BasePlayerManager.java) 与 [GSYVideoManager.instance().setOptionModelList(list)](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoBaseManager.java)。常用 option：`analyzemaxduration=100`、`probesize=10240`、`flush_packets=1`、`fflags=nobuffer`、`packet-buffering=0`、`framedrop=1`。\n\n## 广告 AD\n\n- 双 Manager 双 Player：`GSYVideoADManager.instance()` 负责广告 player，`GSYVideoManager.instance()` 负责主片；两者共存不打架。\n- 组件 [GSYADVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYADVideoPlayer.java) 与\"广告示例\"[GSYSampleADVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYSampleADVideoPlayer.java) 都继承 `StandardGSYVideoPlayer`。\n- Activity 基类：[GSYBaseADActivityDetail](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseADActivityDetail.java)，提供 `getGSYVideoADPlayer()` / `initADVideo()` / `startAD(String adUrl, String url)` / `onADEnd`。\n\n### `startAD(...)` 关键调用序\n\n1. Activity 里持有主 player + AD player 两个 View；\n2. `initADVideo()` 装配（内部会连锁 `OrientationUtils`）；\n3. `startAD(adUrl, mainUrl)` 播广告；\n4. AD complete → SDK 内部隐藏 AD player、start 主 player；\n5. 用户点\"跳过\"→ `getGSYVideoADPlayer().onAutoCompletion()`。\n\n## 无缝切源 / 切码率\n\nExo 内核用 `Exo2PlayerManager.setCurrentMediaSource(...)` 或 Demo 里的 `GSYExo2MediaPlayer.setMediaSources(...)`；Ijk 则重新 `setUp` + `startPlayLogic`。切换的过程中要求\"不闪黑\"，用：\n\n- [KeepLastFrameVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/KeepLastFrameVideo.java)（Demo 组件）+ [KeepLastFrameDemoActivity](../../app/src/main/java/com/example/gsyvideoplayer/KeepLastFrameDemoActivity.java)；\n- 原理见 [doc/KEEP_LAST_FRAME.md](../../doc/KEEP_LAST_FRAME.md)：新 player 拿到首帧前，把旧 SurfaceTexture 内容以 Bitmap 覆盖在上层。\n\n## 广告 / 主片全屏切换的两个陷阱\n\n- 主片全屏 → 广告插入：`GSYVideoADManager` 无内部 `OrientationUtils`；`GSYBaseADActivityDetail` 已处理，但**自定义子类需重写 `getGSYVideoADPlayer()` 返回 AD player 才能被基类的旋转/回退纳管**。\n- 广告全屏返回 → 直接销毁 AD player：`onBackPressed` 需要判断 `GSYVideoManager.backFromWindowFull(this) || GSYVideoADManager.backFromWindowFull(this)`。\n\n## Demo 对照\n\n- 单主片：[DetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java)\n- 前贴片 + 主片：[DetailADPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer.java) / [DetailADPlayer2](../../app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer2.java)\n- 列表条目内广告：[ListADVideoActivity](../../app/src/main/java/com/example/gsyvideoplayer/ListADVideoActivity.java) / [ListADVideoActivity2](../../app/src/main/java/com/example/gsyvideoplayer/ListADVideoActivity2.java) / [RequestListADVideoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/RequestListADVideoPlayer.java)\n- 智能硬解回退：[SmartMediaCodecFallbackActivity](../../app/src/main/java/com/example/gsyvideoplayer/SmartMediaCodecFallbackActivity.java)\n\n## v14.0.0 RTSP 与倍速\n\n- FORMAT `timeout` 为微秒（`20_000_000L` = 20 秒），明确配置的 `rtsp_transport` 会透传。\n- PLAYER `rtsp-live-max-buffer-ms` 在 prepare 前设置，`0` 关闭，`500..60000` 毫秒用于正常 1× 直播队列恢复。恢复会丢弃旧媒体并产生跳跃/音频间隙，不是自动重连。\n- 初始化重定向覆盖 OPTIONS/DESCRIBE/SETUP/首次 PLAY；不支持播放中 REDIRECT 或任意跨端口控制会话。\n- `soundtouch=1` 为 0.25×–4× 软件变速；`soundtouch=0` 为设备相关平台范围。引擎切换和 EOF 按真实音频排空处理，终止错误后需要 reset 或重建。\n- 详见 [QUESTION.md](../../doc/QUESTION.md) 与 [v14 发布说明](../../doc/UPDATE_VERSION.md#v1400-2026-10-10)。\n\n## 常见坑\n\n- 直播卡顿升级 → 检查是否开启 `cacheWithPlay`（对直播来说必须关）。\n- 广告 `onAutoComplete` 后主片没自动 setUp → 检查是不是把 `setStartAfterPrepared(false)` 传给了主 builder。\n- RTMP 断流回收后重连白屏 → `onVideoResume(false)` + `player.getCurrentPlayer().onVideoReset()` 再 `startPlayLogic()`。\n","skills/13-gsy-proguard-r8/SKILL.md":"# 13-gsy-proguard-r8\n\nPath: `skills/13-gsy-proguard-r8/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/13-gsy-proguard-r8/SKILL.md)","doc/ARCHITECTURE.md":"# doc - System Architecture & Module Boundaries\n\nPath: `doc/ARCHITECTURE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/doc/ARCHITECTURE.md)","skills/README.md":"# Skills Skill\n\nPath: `skills/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/README.md)"},"items":[{"name":"ARCHITECTURE.md","path":"doc/ARCHITECTURE.md","rawUrl":"https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/doc/ARCHITECTURE.md","title":"doc - System Architecture & Module Boundaries","category":"project-spec","format":"markdown","content":"# doc - System Architecture & Module Boundaries\n\nPath: `doc/ARCHITECTURE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/doc/ARCHITECTURE.md)","tokens":300,"sizeBytes":188},{"name":"SKILL.md","path":"skills/01-gsy-quickstart/SKILL.md","rawUrl":"https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/01-gsy-quickstart/SKILL.md","title":"gsy-quickstart","category":"anthropic-skill","format":"markdown","content":"---\nname: gsy-quickstart\ndescription: 从零把 GSYVideoPlayer 塞进一个 Activity 或 Fragment，跑通「setUp → startPlayLogic → release」最小闭环。\nwhen_to_use: 用户第一次在项目里集成 GSYVideoPlayer；只要能播一个 mp4 就行；或需要确认基础生命周期是否接对。\ninputs:\n  - name: play_url\n    description: 视频地址（http/https/file/asset/rtmp/rtsp）\n    required: true\n  - name: container\n    description: 承载 View 的容器（一般是 FrameLayout）\n    required: true\noutputs:\n  - StandardGSYVideoPlayer 实例已 setUp 并可控（play/pause/release）\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/PlayActivity.java\n---\n\n# gsy-quickstart\n\n## 依赖\n\n- 推荐整包：`io.github.carguo:gsyvideoplayer:14.0.0`（已含 Java、Exo2、三 ABI ex_so）。\n- 分模块主库：`io.github.carguo:gsyvideoplayer-java:14.0.0`。\n- 分模块 IJK so：选 `io.github.carguo:gsyvideoplayer-ex_so:14.0.0`，或所需独立 artifact `gsyvideoplayer-arm64` / `gsyvideoplayer-armv7a` / `gsyvideoplayer-x64`，不重复引入同 ABI 两套库。\n- 默认 ABI 为 `arm64-v8a` / `armeabi-v7a` / `x86_64`（FFmpeg n5.1.10 / OpenSSL 3.5.9）；旧 armv5/x86 未升级，仅旧架构项目显式使用。\n- 权限：`INTERNET`、`WAKE_LOCK`、`ACCESS_NETWORK_STATE`（AndroidManifest）\n\n明细见 [doc/DEPENDENCIES.md](../../doc/DEPENDENCIES.md)。\n\n## 核心 API\n\n| 类 / 方法 | 位置 | 作用 |\n|---|---|---|\n| `StandardGSYVideoPlayer(Context)` | [StandardGSYVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java) | 主用 UI 组件，默认布局在 `R.layout.video_layout_standard` |\n| `setUp(url, cacheWithPlay, title)` | [GSYVideoView#L438-L451](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 最短形式的地址绑定 |\n| `setUp(url, cacheWithPlay, cachePath, title)` | [GSYVideoView#L477-L488](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 支持自定义缓存目录 |\n| `setUp(url, cacheWithPlay, cachePath, headers, title)` | [GSYVideoView#L453-L475](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 附带 HTTP header |\n| `startPlayLogic()` | `StandardGSYVideoPlayer`（继承自 `GSYVideoControlView`） | 触发 prepare→start 全流程 |\n| `getCurrentPlayer()` | `GSYBaseVideoPlayer` | 获取\"当前活跃 player\"（可能是全屏克隆体） |\n| `onVideoPause()` / `onVideoResume()` / `onVideoResume(boolean seek)` | [GSYVideoView#L518-L577](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 对应 Activity `onPause / onResume`；直播场景传 `seek=false` |\n| `release()` | [GSYVideoView#L818](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 释放当前 Player 与 Surface |\n| `GSYVideoManager.releaseAllVideos()` | [GSYVideoManager#L99-L104](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java) | 全局收尾（切页面时兜底） |\n| `GSYVideoManager.onPause() / onResume()` | [GSYVideoManager#L110-L135](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java) | 全局暂停/恢复所有 GSY player |\n\n## 参数表：`setUp(...)`\n\n| 参数 | 类型 | 说明 |\n|---|---|---|\n| `url` | `String` | 支持 http/https/file/asset/rtmp/rtsp/自定义 scheme；`ProxyCacheManager` 会自动拦截可缓存的普通 http |\n| `cacheWithPlay` | `boolean` | true → 边播边缓；对直播 / m3u8 / DRM 建议 false |\n| `cachePath` | `File?` | 自定义缓存目录，默认 `context.cacheDir/videoCache` |\n| `mapHeadData` | `Map<String,String>?` | HTTP 请求头（Referer、Cookie、Auth 等） |\n| `title` | `String` | 顶部标题栏文案，会显示在 `R.id.title` |\n\n返回值：`boolean`，false 表示 URL 为空或与当前 tag 相同（防重复 setup）。\n\n## 最小调用序（Activity 生命周期）\n\n1. `onCreate` → `player.setUp(...)` → 可选 `player.setThumb...`、`setPlayTag()` → `player.startPlayLogic()`\n2. `onPause` → `player.getCurrentPlayer().onVideoPause()`\n3. `onResume` → `player.getCurrentPlayer().onVideoResume()`（直播传 `false`）\n4. `onBackPressed` → 若在全屏：`GSYVideoManager.backFromWindowFull(this)` 返回 true 就吞掉事件\n5. `onDestroy` → `player.getCurrentPlayer().release()` + `GSYVideoManager.releaseAllVideos()`\n\n## Demo 对照\n\n- 最短一屏：[PlayActivity](../../app/src/main/java/com/example/gsyvideoplayer/PlayActivity.java)\n- Fragment：[VideoFragment](../../app/src/main/java/com/example/gsyvideoplayer/fragment/VideoFragment.java) + [FragmentVideoActivity](../../app/src/main/java/com/example/gsyvideoplayer/FragmentVideoActivity.java)\n- 空控件模式（自绘 UI）：[PlayEmptyControlActivity](../../app/src/main/java/com/example/gsyvideoplayer/PlayEmptyControlActivity.java) + [EmptyControlVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/EmptyControlVideo.java)\n\n## 常见坑\n\n- 忘记 `release()` → dex 泄漏 + surface 崩溃：一定在 `onDestroy` 兜底。\n- 不覆盖 `getCurrentPlayer()` 直接调 `player.onVideoPause()`：全屏切回后指令发到\"影子\"实例，导致状态错乱。\n- `setUp` 传空 title 走 `null`，会隐藏顶部栏（`View.GONE`），不会崩。\n","frontmatter":{"name":"gsy-quickstart","description":"从零把 GSYVideoPlayer 塞进一个 Activity 或 Fragment，跑通「setUp → startPlayLogic → release」最小闭环。","when_to_use":"用户第一次在项目里集成 GSYVideoPlayer；只要能播一个 mp4 就行；或需要确认基础生命周期是否接对。","inputs":[{"name":"play_url","description":"视频地址（http/https/file/asset/rtmp/rtsp）","required":true},{"name":"container","description":"承载 View 的容器（一般是 FrameLayout）","required":true}],"outputs":["StandardGSYVideoPlayer 实例已 setUp 并可控（play/pause/release）"],"references":[{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/PlayActivity.java"}]},"isInternal":false,"tokens":1802,"sizeBytes":5733},{"name":"SKILL.md","path":"skills/02-gsy-option-builder/SKILL.md","rawUrl":"https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/02-gsy-option-builder/SKILL.md","title":"gsy-option-builder","category":"anthropic-skill","format":"markdown","content":"---\nname: gsy-option-builder\ndescription: 用 GSYVideoOptionBuilder 一次性把播放器所有可调项配好；等价于对 StandardGSYVideoPlayer 挨个 setter 但更紧凑。\nwhen_to_use: 需要开启循环 / 变速 / 边播边缓 / 触摸手势 / 自动旋转 / 封面 / 底部进度条样式 / 全屏动画 等多个开关时；或在多处播放页复用同一份配置。\ninputs:\n  - name: player\n    description: StandardGSYVideoPlayer / GSYBaseVideoPlayer 实例\n    required: true\noutputs:\n  - 已按 builder 状态 build 完成的 player，可直接 startPlayLogic\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseActivityDetail.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java\n---\n\n# gsy-option-builder\n\n## API 总览\n\n入口：[GSYVideoOptionBuilder](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java)。\n\n链式调用，最后 `build(player)` 把状态灌到具体 `GSYBaseVideoPlayer`。\n\n```java\nnew GSYVideoOptionBuilder()\n    .setUrl(url)\n    .setCacheWithPlay(true)\n    .setVideoTitle(title)\n    .setIsTouchWiget(true)\n    .setRotateViewAuto(false)\n    .setLockLand(false)\n    .setLooping(false)\n    .setSpeed(1.0f)\n    .setSeekOnStart(0)\n    .setVideoAllCallBack(callback)\n    .build(player);\n```\n\n## 完整参数表（按 [源文件行号](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) 对齐）\n\n| Setter | 类型 | 默认 | 用途 |\n|---|---|---|---|\n| `setUrl(String)` [#L376](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | String | null | 播放地址 |\n| `setVideoTitle(String)` [#L386](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | String | null | 顶部标题 |\n| `setMapHeadData(Map)` [#L437](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | `Map<String,String>` | null | HTTP header |\n| `setOverrideExtension(String)` [#L561](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | String | null | 强制 Exo 用哪种解封装器（`m3u8`/`mpd`/`ism`） |\n| `setCacheWithPlay(boolean)` [#L396](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 边播边缓 |\n| `setCachePath(File)` [#L427](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | File | null | 缓存目录 |\n| `setLooping(boolean)` [#L202](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 循环 |\n| `setSpeed(float)` [#L237](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | float | 1.0 | 播放速率，0.25~3.0 常用 |\n| `setSoundTouch(boolean)` [#L246](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 变速不变调 |\n| `setSeekOnStart(long)` [#L366](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | long ms | -1 | 起播 seek 位置 |\n| `setSeekRatio(float)` [#L324](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | float | 1.0 | 触摸横滑与进度的比例 |\n| `setStartAfterPrepared(boolean)` [#L406](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 首帧就绪后自动播；`false` 用于\"手动 start\" |\n| `setReleaseWhenLossAudio(boolean)` [#L417](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 丢失音频焦点是否 release |\n| `setIsTouchWiget(boolean)` [#L263](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 非全屏是否响应触摸手势 |\n| `setIsTouchWigetFull(boolean)` [#L272](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 全屏是否响应触摸手势 |\n| `setDismissControlTime(int ms)` [#L543](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | int | 2500 | 控制条自动隐藏时间 |\n| `setNeedShowWifiTip(boolean)` [#L281](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 移动网络提示 |\n| `setRotateViewAuto(boolean)` [#L221](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 自动旋转全屏；关闭后配合 `OrientationUtils` |\n| `setRotateWithSystem(boolean)` [#L337](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 是否跟随系统旋转设置 |\n| `setLockLand(boolean)` [#L229](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 全屏是否锁横屏 |\n| `setAutoFullWithSize(boolean)` [#L184](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 竖屏视频自动竖屏全屏 |\n| `setOnlyRotateLand(boolean)` [#L567](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 只允许横屏方向变化 |\n| `setShowFullAnimation(boolean)` [#L194](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 全屏过渡动画 |\n| `setHideKey(boolean)` [#L254](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 全屏隐藏虚拟按键 |\n| `setFullHideActionBar(boolean)` [#L587](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 全屏隐藏 ActionBar |\n| `setFullHideStatusBar(boolean)` [#L592](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 全屏隐藏 StatusBar |\n| `setNeedLockFull(boolean)` [#L525](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 全屏显示屏幕锁按钮 |\n| `setLockClickListener(LockClickListener)` [#L533](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 接口 | null | 屏幕锁点击回调 |\n| `setEnlargeImageRes(int)` [#L291](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | drawableRes | -1 | 全屏按钮图标 |\n| `setShrinkImageRes(int)` [#L301](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | drawableRes | -1 | 退出全屏按钮图标 |\n| `setThumbImageView(View)` [#L464](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | View | null | 封面容器（`ImageView` 或子布局） |\n| `setThumbPlay(boolean)` [#L516](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 点击封面即播 |\n| `setShowPauseCover(boolean)` [#L314](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 暂停时显示最后一帧 |\n| `setBottomProgressBarDrawable(Drawable)` [#L482](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | Drawable | null | 底部长条进度条颜色 |\n| `setBottomShowProgressBarDrawable(Drawable, Drawable)` [#L473](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 2×Drawable | null | 交互进度条 & thumb |\n| `setDialogProgressBar(Drawable)` [#L499](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | Drawable | null | 触摸拖动进度条样式 |\n| `setDialogVolumeProgressBar(Drawable)` [#L490](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | Drawable | null | 触摸音量进度条 |\n| `setDialogProgressColor(int, int)` [#L507](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | int color | -11 | 触摸进度条高亮/普通颜色 |\n| `setShowDragProgressTextOnSeekBar(boolean)` [#L572](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 拖动时 seekbar 上气泡 |\n| `setEffectFilter(GSYVideoGLView.ShaderInterface)` [#L551](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | Shader | `NoEffect` | GL 滤镜（渲染必须 `GLSURFACE`） |\n| `setPlayTag(String)` [#L347](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | String | \"\" | 列表播放去重用 |\n| `setPlayPosition(int)` [#L356](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | int | -22 | 列表位置索引 |\n| `setSetUpLazy(boolean)` [#L582](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 延迟 setUp，`build` 后手动 `startPlayLogic` |\n| `setVideoAllCallBack(VideoAllCallBack)` [#L213](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 接口 | null | 全事件回调（prepared/complete/error…） |\n| `setGSYVideoProgressListener(GSYVideoProgressListener)` [#L455](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 接口 | null | 高频进度回调 |\n| `setGSYStateUiListener(GSYStateUiListener)` [#L600](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 接口 | null | UI 状态变化回调 |\n| `setSurfaceErrorPlay(boolean)` [#L446](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | Surface 异常时点击继续 |\n| `setNeedOrientationUtils(boolean)` [#L611](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 全屏是否内部构造 `OrientationUtils` |\n\n## 关键调用序\n\n1. 构造 `GSYVideoOptionBuilder` → 链式 setter；\n2. `builder.build(player)` 会自动 setUp（除非 `setSetUpLazy(true)`）；\n3. 需要在页面里同时接收回调 → 让 Activity `implements VideoAllCallBack`，直接 `setVideoAllCallBack(this)`。\n\n## Demo 对照\n\n- 详情页 + Builder 全套：[DetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java)\n- 结合 `GSYBaseActivityDetail` 的最短接入：[DetailControlActivity](../../app/src/main/java/com/example/gsyvideoplayer/DetailControlActivity.java) 与 [GSYBaseActivityDetail#L56-L61](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseActivityDetail.java)\n\n## 常见坑\n\n- `build(player)` 内部会调 `setUp`；若 `setSetUpLazy(true)` 需要自己再调 `startPlayLogic()`。\n- Effect 滤镜需要先 `GSYVideoType.setRenderType(GLSURFACE)`，否则 `setEffectFilter` 无效。\n- `setPlayTag` 与 `setPlayPosition` 是列表页避免\"上下滑滚 setUp 命中同 URL 不重播\"的关键。\n","frontmatter":{"name":"gsy-option-builder","description":"用 GSYVideoOptionBuilder 一次性把播放器所有可调项配好；等价于对 StandardGSYVideoPlayer 挨个 setter 但更紧凑。","when_to_use":"需要开启循环 / 变速 / 边播边缓 / 触摸手势 / 自动旋转 / 封面 / 底部进度条样式 / 全屏动画 等多个开关时；或在多处播放页复用同一份配置。","inputs":[{"name":"player","description":"StandardGSYVideoPlayer / GSYBaseVideoPlayer 实例","required":true}],"outputs":["已按 builder 状态 build 完成的 player，可直接 startPlayLogic"],"references":[{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseActivityDetail.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java"}]},"isInternal":false,"tokens":3532,"sizeBytes":11830},{"name":"SKILL.md","path":"skills/03-gsy-fullscreen-orientation/SKILL.md","rawUrl":"https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/03-gsy-fullscreen-orientation/SKILL.md","title":"gsy-fullscreen-orientation","category":"anthropic-skill","format":"markdown","content":"---\nname: gsy-fullscreen-orientation\ndescription: 处理进入/退出全屏、屏幕旋转、竖屏视频自适应、系统旋转设置联动、反向横屏。\nwhen_to_use: 出现\"全屏没进/没退\"、\"横屏方向反了\"、\"旋转跟随系统坏了\"、\"竖屏视频被横过来\"、\"抖动无限循环\"这类问题；或首次在详情页接入全屏。\ninputs:\n  - name: activity\n    description: 承载 player 的 Activity\n    required: true\n  - name: player\n    description: GSYBaseVideoPlayer 或其子类\n    required: true\noutputs:\n  - OrientationUtils 已绑定；点击\"全屏\"按钮可以正确进入 window fullscreen 副本；返回键与旋转都能正确复位。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationUtils.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationOption.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseActivityDetail.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java\n---\n\n# gsy-fullscreen-orientation\n\n## 概念\n\n- **视觉全屏**：`player.startWindowFullscreen(context, hideActionBar, hideStatusBar)`，会用反射克隆一个同类 player 塞进 `android.R.id.content` 顶部，返回原实例。\n- **屏幕方向**：`OrientationUtils` 监听 `OrientationEventListener` + `Settings.System.ACCELEROMETER_ROTATION`，负责旋转 Activity 方向。\n- **两者可解耦**：只想旋转不切副本 → `setNeedOrientationUtils(false)` + 自建 `OrientationUtils`；只想切副本不旋转 → 不构造 `OrientationUtils`，`player` 保持默认 `setRotateViewAuto(false)`。\n\n## 核心 API\n\n| 类 / 方法 | 位置 | 说明 |\n|---|---|---|\n| `OrientationUtils(Activity, GSYBaseVideoPlayer)` | [OrientationUtils#L50-L52](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationUtils.java) | 主构造 |\n| `OrientationUtils(Activity, player, OrientationOption)` | [OrientationUtils#L54-L64](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationUtils.java) | 支持自定义旋转角度阈值 |\n| `setEnable(boolean)` | `OrientationUtils` | 使能/失能自动旋转 |\n| `setRotateWithSystem(boolean)` | `OrientationUtils` | 是否跟随系统\"旋转开关\" |\n| `setIsOnlyRotateLand(boolean)` | `OrientationUtils` | true 时仅在横屏方向变化时响应 |\n| `resolveByClick()` | `OrientationUtils` | 用户点全屏按钮时手动触发一次旋转 |\n| `backToProtVideo()` | `OrientationUtils` | 返回键触发时把方向复位竖屏 |\n| `setIsPause(boolean)` | `OrientationUtils` | 生命周期 pause/resume 桥接 |\n| `releaseListener()` | `OrientationUtils` | `onDestroy` 释放监听 |\n| `startWindowFullscreen(Context, boolean, boolean)` | [GSYBaseVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java) | 进入 window 全屏，返回克隆体 |\n| `GSYVideoManager.backFromWindowFull(Context)` | [GSYVideoManager#L82-L94](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java) | 返回键统一处理 |\n| `GSYVideoManager.isFullState(Activity)` | [GSYVideoManager#L143-L151](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java) | 当前是否全屏 |\n\n## `OrientationOption` 参数\n\n| 字段 | 默认 | 作用 |\n|---|---|---|\n| `normalPortraitAngleStart / End` | 5 / 355 | 判定竖屏区间 |\n| `normalLandAngleStart / End` | 85 / 95 | 判定正向横屏区间 |\n| `reverseLandAngleStart / End` | 265 / 275 | 判定反向横屏区间 |\n\n区间越大越\"灵敏\"，太大会抖动。\n\n## 关键调用序（详情页最佳实践）\n\n1. `onCreate`：`orientationUtils = new OrientationUtils(this, player)`; `orientationUtils.setEnable(false)`（进页面不自动旋转）\n2. 全屏按钮点击：`orientationUtils.resolveByClick()` → `player.startWindowFullscreen(this, true, true)`\n3. `onBackPressed`：`orientationUtils.backToProtVideo()` → `GSYVideoManager.backFromWindowFull(this)`\n4. `onPause`：`orientationUtils.setIsPause(true)`\n5. `onResume`：`orientationUtils.setIsPause(false)`\n6. `onDestroy`：`orientationUtils.releaseListener()`\n\n`GSYBaseActivityDetail` 已把 1/2/3/4/5/6 全套代码写好，直接继承最省事。\n\n## Demo 对照\n\n- 最标准的全屏 + 旋转：[DetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java)\n- 只旋转不切副本：[LandLayoutVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/LandLayoutVideo.java) + [DetailNormalActivityPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailNormalActivityPlayer.java)\n- 反射克隆入口：[GSYBaseVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java)（`startWindowFullscreen` → `getConstructor(Context.class, Boolean.class)`）\n\n## v14.0.0 折叠屏示例\n\nXML [FoldDetailActivity](../../app/src/main/java/com/example/gsyvideoplayer/FoldDetailActivity.java) 与 Compose [FoldComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/FoldComposeActivity.kt) 按 FoldingFeature 实现 BOOK/TABLETOP 分屏、铰链分隔与全屏。XML 全屏克隆布局通过 `LandLayoutVideo` 传递姿态；普通全屏 API 不会自动给任意业务页面添加折叠布局。用法及测试注入边界见 [RECENT_FEATURES.md](../../doc/RECENT_FEATURES.md)。\n\n## 常见坑\n\n- **黑屏一瞬**：`startWindowFullscreen` 用反射构造新 player，若你的子类只暴露 `(Context)` 构造且未加 `(Context, Boolean)`，R8 混淆后必炸。ProGuard 里必须保留：`-keep class * extends GSYBaseVideoPlayer { public <init>(android.content.Context); public <init>(android.content.Context, java.lang.Boolean); }`\n- **旋转停不下来**：忘了 `orientationUtils.setIsPause(true)` 或 `releaseListener()`。\n- **系统关了自动旋转但还想手动横屏**：`setRotateWithSystem(false)`。\n- **竖屏视频被强制横屏**：`setAutoFullWithSize(true)` 让 SDK 自适应。\n","frontmatter":{"name":"gsy-fullscreen-orientation","description":"处理进入/退出全屏、屏幕旋转、竖屏视频自适应、系统旋转设置联动、反向横屏。","when_to_use":"出现\"全屏没进/没退\"、\"横屏方向反了\"、\"旋转跟随系统坏了\"、\"竖屏视频被横过来\"、\"抖动无限循环\"这类问题；或首次在详情页接入全屏。","inputs":[{"name":"activity","description":"承载 player 的 Activity","required":true},{"name":"player","description":"GSYBaseVideoPlayer 或其子类","required":true}],"outputs":["OrientationUtils 已绑定；点击\"全屏\"按钮可以正确进入 window fullscreen 副本；返回键与旋转都能正确复位。"],"references":[{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationUtils.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationOption.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseActivityDetail.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java"}]},"isInternal":false,"tokens":1893,"sizeBytes":6115},{"name":"SKILL.md","path":"skills/04-gsy-kernel-switch/SKILL.md","rawUrl":"https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/04-gsy-kernel-switch/SKILL.md","title":"gsy-kernel-switch","category":"anthropic-skill","format":"markdown","content":"---\nname: gsy-kernel-switch\ndescription: 在 IJK / System / ExoPlayer2(Media3) / Aliyun 四个媒体内核之间自由切换，以及自定义 Manager 的方法。\nwhen_to_use: 需要支持 DASH/SmoothStreaming、需要 Media3 track selection、想减 so 体积只留系统解码、要用 Aliyun 内核、或者要多实例并行独立 Manager。\ninputs:\n  - name: kernel\n    description: ijk / system / exo / ali / custom\n    required: true\noutputs:\n  - PlayerFactory 与（可选）CacheFactory 已切换到目标 Manager\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/PlayerFactory.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/IjkPlayerManager.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/SystemPlayerManager.java\n  - file: gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/IPlayerManager.java\n  - file: gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/BasePlayerManager.java\n  - file: gsyVideoPlayer-exo_player2\n  - file: gsyVideoPlayer-aliplay/src/main/java/com/shuyu/aliplay/AliPlayerManager.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/exo/GSYExoPlayerManager.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/manager/CustomManager.java\n---\n\n# gsy-kernel-switch\n\n## 内核矩阵\n\n| Kernel | Class | 来源模块 | 特点 |\n|---|---|---|---|\n| **IJK**（默认） | [IjkPlayerManager](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/IjkPlayerManager.java) | `gsyVideoPlayer-java` + 升级后的独立 ABI / `ex_so` | FFmpeg n5.1.10 的固定配置；软/硬解能力按格式与设备确认 |\n| **System** | [SystemPlayerManager](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/SystemPlayerManager.java) | `gsyVideoPlayer-java` | 无 so；靠 `android.media.MediaPlayer`；直播/复杂容器兼容差 |\n| **Exo/Media3** | `GSYExoPlayerManager`（Demo 内）／`GSYExo2MediaPlayer` | `gsyVideoPlayer-exo_player2` | DASH / HLS / SmoothStreaming / TrackSelection |\n| **Aliyun** | [AliPlayerManager](../../gsyVideoPlayer-aliplay/src/main/java/com/shuyu/aliplay/AliPlayerManager.java) | `gsyVideoPlayer-aliplay` | 阿里云 vid/authInfo；HLS 加密 |\n\n## 切换 API\n\n**全局切换**：`PlayerFactory.setPlayManager(Class<? extends IPlayerManager>)`（内部 `getPlayManager` 用 `sPlayerManager.newInstance()` 反射构造，故被切入的类**必须**有 `public` 无参构造）。见 [PlayerFactory#L15-L27](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/PlayerFactory.java) 与本仓库 R8 保留规则 [proguard-rules.pro](../../app/proguard-rules.pro)。\n\n```java\n// Exo\nPlayerFactory.setPlayManager(Exo2PlayerManager.class);\nCacheFactory.setCacheManager(ExoPlayerCacheManager.class);\n\n// System\nPlayerFactory.setPlayManager(SystemPlayerManager.class);\n\n// Aliyun\nPlayerFactory.setPlayManager(AliPlayerManager.class);\n```\n\n必须在 `player.setUp(...)` **之前**调用；否则本次实例仍用旧内核。\n\n## `IPlayerManager` 抽象\n\n统一接口见 [IPlayerManager](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/IPlayerManager.java)。核心方法（子类必须实现）：\n\n| 方法 | 说明 |\n|---|---|\n| `initVideoPlayer(Context, Message, List<VideoOptionModel>, ICacheManager)` | 初始化内核；`Message.obj` 是 `GSYModel` |\n| `showDisplay(Message)` | 绑定 Surface / SurfaceHolder |\n| `setSpeed(float, boolean soundTouch)` | 速率 + 变速不变调 |\n| `setNeedMute(boolean)` | 静音 |\n| `setVolume(float, float)` | 双通道音量 |\n| `getMediaPlayer()` | 底层 player 实例 |\n| `isSurfaceSupportLockCanvas()` | 是否支持 `lockCanvas`（TextureView 补帧） |\n| `start / pause / stop / seekTo / release / getCurrentPosition / getDuration / getVideoSarNum / getVideoSarDen / getVideoWidth / getVideoHeight` | 基础控制 |\n\n推荐继承 [BasePlayerManager](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/BasePlayerManager.java) 以少写模板。\n\n## `IPlayerInitSuccessListener` hook\n\n[IPlayerInitSuccessListener](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/IPlayerInitSuccessListener.java) 允许在 `initVideoPlayer` 完成后对底层 Player 拿手（比如设置 Exo 的 TrackSelector）：\n\n```java\nIjkPlayerManager.setIjkLibLoader(...);\n// 或\nGSYExoVideoManager.setPlayerInitSuccessListener(new IPlayerInitSuccessListener() {\n    @Override public void onPlayerInitSuccess(IMediaPlayer mp, GSYModel model) { ... }\n});\n```\n\n## `CacheFactory` 同步切换\n\n不同内核有各自缓存方案；切内核记得配套：\n\n| Player Manager | 对应 CacheManager |\n|---|---|\n| `IjkPlayerManager` / `SystemPlayerManager` | [ProxyCacheManager](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheManager.java)（默认） |\n| Exo/Media3 | `ExoPlayerCacheManager`（`gsyvideoplayer-exo_player2` 内） |\n| Aliyun | 由 Aliyun SDK 自身管理，一般 `CacheFactory` 保持 `ProxyCacheManager` 或自定义空实现 |\n\n## 多实例并行\n\n用 [CustomManager](../../app/src/main/java/com/example/gsyvideoplayer/video/manager/CustomManager.java) 模式：拷贝 `GSYVideoManager` 单例套壳，在自定义 `GSYVideoView` 里覆盖 `getGSYVideoManager()`；不同页面互不影响，参考 [MultiSampleVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/MultiSampleVideo.java) + [ListMultiVideoActivity](../../app/src/main/java/com/example/gsyvideoplayer/ListMultiVideoActivity.java)。\n\n## Demo 对照\n\n- Exo 切源 / 无缝切换：[DetailExoListPlayer](../../app/src/main/java/com/example/gsyvideoplayer/exo/DetailExoListPlayer.java)、[ExoAdaptiveTrackActivity](../../app/src/main/java/com/example/gsyvideoplayer/exo/ExoAdaptiveTrackActivity.java)\n- MediaCodec 智能回退：[SmartMediaCodecFallbackActivity](../../app/src/main/java/com/example/gsyvideoplayer/SmartMediaCodecFallbackActivity.java) + [doc/SMART_MEDIACODEC_FALLBACK_RESEARCH.md](../../doc/SMART_MEDIACODEC_FALLBACK_RESEARCH.md)\n\n## v14.0.0 原生选择\n\nIJK 当前三 ABI 成套库为 FFmpeg n5.1.10 / OpenSSL 3.5.9。`GSYIjkMediaCodecSelector` 先保留原 IJK 选择，再在 API 29+ 选择平台报告的普通 Codec2 硬件解码器，排除纯软件及安全/隧道必需解码器。这是初始化阶段的选择补充，与确认运行中硬解失败后重建软解的策略分开；见 [发布说明](../../doc/UPDATE_VERSION.md#v1400-2026-10-10)。\n\n## 常见坑\n\n- `PlayerFactory.setPlayManager` 传入的类**必须**有 `public` 无参构造，且被 ProGuard 保留。\n- Exo 切源建议用 `Exo2PlayerManager` + `GSYExo2MediaPlayer`；直接换 Manager 但用 Ijk 的 URL 会走缓存代理导致 DASH 解析失败——记得 `setOverrideExtension(\"mpd\")` 或关闭缓存。\n- 系统内核 `SystemPlayerManager` 不支持 `soundTouch`、多数直播格式；生产环境仅作 fallback。\n","frontmatter":{"name":"gsy-kernel-switch","description":"在 IJK / System / ExoPlayer2(Media3) / Aliyun 四个媒体内核之间自由切换，以及自定义 Manager 的方法。","when_to_use":"需要支持 DASH/SmoothStreaming、需要 Media3 track selection、想减 so 体积只留系统解码、要用 Aliyun 内核、或者要多实例并行独立 Manager。","inputs":[{"name":"kernel","description":"ijk / system / exo / ali / custom","required":true}],"outputs":["PlayerFactory 与（可选）CacheFactory 已切换到目标 Manager"],"references":[{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/PlayerFactory.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/IjkPlayerManager.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/SystemPlayerManager.java"},{"file":"gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/IPlayerManager.java"},{"file":"gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/BasePlayerManager.java"},{"file":"gsyVideoPlayer-exo_player2"},{"file":"gsyVideoPlayer-aliplay/src/main/java/com/shuyu/aliplay/AliPlayerManager.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/exo/GSYExoPlayerManager.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/video/manager/CustomManager.java"}]},"isInternal":false,"tokens":2128,"sizeBytes":6977},{"name":"SKILL.md","path":"skills/05-gsy-list-playback/SKILL.md","rawUrl":"https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/05-gsy-list-playback/SKILL.md","title":"gsy-list-playback","category":"anthropic-skill","format":"markdown","content":"---\nname: gsy-list-playback\ndescription: RecyclerView / ViewPager2 / ViewPager 列表滑动自动播放，滚动切换、复用绑定、进入详情页时\"漂移\"到全屏。\nwhen_to_use: 短视频信息流、朋友圈式内嵌播放、抖音/快手类 ViewPager2 竖播、列表进入详情页的无缝续播。\ninputs:\n  - name: adapter\n    description: RecyclerView.Adapter / FragmentStateAdapter\n    required: true\noutputs:\n  - 列表条目具备\"进入可视区自动播放，离开释放\"的能力，且切换详情页可续播。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/ListVideoUtil.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoHelper.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/utils/ScrollCalculatorHelper.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/AutoPlayRecyclerViewActivity.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/ViewPager2Activity.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/RecyclerViewActivity.java\n---\n\n# gsy-list-playback\n\n## 三条实现路线\n\n| 路线 | 特点 | 适用 |\n|---|---|---|\n| **A. 每 item 内嵌 `StandardGSYVideoPlayer`** | 每条 item 有独立 player 实例，滑到即播 | 大多数场景（含无限缩略页面） |\n| **B. `ListVideoUtil` 单例复用** | 全列表共享一个 player 实例，动态 `attachToParent` | 内存敏感、条目多；不能同屏两个视频 |\n| **C. `GSYVideoHelper` 悬浮小窗** | 用于详情页返回列表时\"漂浮\"续播 | 抖音式详情返回 |\n\n## 路线 A：每 item 一个 player\n\n关键点：**在 `onBindViewHolder` 里给每个 player `setPlayPosition(position)` + `setPlayTag(String)`**，防止滚动复用导致 URL 命中错人。\n\n`GSYVideoOptionBuilder`：\n- `setPlayTag(String)` [#L347](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java)\n- `setPlayPosition(int)` [#L356](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java)\n\n滚动出屏自动释放：`RecyclerView.OnScrollListener` → 当前 firstVisible / lastVisible 之外的 holder 调 `GSYVideoManager.releaseAllVideos()`。\n\n见 [RecyclerViewActivity](../../app/src/main/java/com/example/gsyvideoplayer/RecyclerViewActivity.java)。\n\n## 路线 B：`ListVideoUtil`（跨 item 复用一个 player）\n\n工具类：[ListVideoUtil](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/ListVideoUtil.java)。\n\n| API | 说明 |\n|---|---|\n| `ListVideoUtil(Context)` | 单例创建（在 Activity/Fragment 里持一份） |\n| `addVideoPlayer(int position, ImageView thumb, String tag, ViewGroup container, View playBtn)` | 把 player 移到 `container`；`tag` 与 `position` 用于识别是否是新目标 |\n| `setPlayPositionAndTag(int, String)` | 手动更新目标 |\n| `startPlay(String url)` | 开始播放 |\n| `isCurrentViewPlaying(int pos, String tag)` | 判定当前播放是否在这个 item |\n| `releaseVideoPlayer()` | 释放，通常在离开页面时调 |\n\n**自动可视区触发**用 [ScrollCalculatorHelper](../../app/src/main/java/com/example/gsyvideoplayer/utils/ScrollCalculatorHelper.java) 的 `onScrollReleaseAllVideos(...) / onScrollPlayVideo(...)`。见 [AutoPlayRecyclerViewActivity](../../app/src/main/java/com/example/gsyvideoplayer/AutoPlayRecyclerViewActivity.java)。\n\n## 路线 C：`GSYVideoHelper` 小窗漂移\n\n工具类：[GSYVideoHelper](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoHelper.java)。核心思路：**列表 item / 详情页共用同一个 player 实例**，在离开列表时把 player parent 换到浮层容器，进入详情再换回去。\n\n| API | 说明 |\n|---|---|\n| `GSYVideoHelper(Context, GSYBaseVideoPlayer)` | 携一份 player |\n| `setSmall(int width, int height, int gravity, int marginX, int marginY)` | 小窗尺寸 |\n| `showSmallVideo(...)` / `smallVideoToNormal()` | 小窗 ↔ 正常 |\n| `releaseVideoPlayer()` | 释放 |\n\nDemo：[SmallVideoHelper](../../app/src/main/java/com/example/gsyvideoplayer/utils/SmallVideoHelper.java)（app 层包装） + [ListVideoActivity](../../app/src/main/java/com/example/gsyvideoplayer/ListVideoActivity.java)。\n\n## ViewPager2 竖播\n\n- 用 `FragmentStateAdapter`，每个 Fragment 内嵌 `StandardGSYVideoPlayer` 或 `NormalGSYVideoPlayer`；\n- `ViewPager2.registerOnPageChangeCallback` 里 `onPageSelected` → `previousPlayer.onVideoPause(); currentPlayer.startPlayLogic()`；\n- 记得 `setUserInputEnabled` 竖向。\n\nDemo：[ViewPager2Activity](../../app/src/main/java/com/example/gsyvideoplayer/ViewPager2Activity.java)（RecyclerView 变体） + Compose 版 [VerticalShortVideoComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/VerticalShortVideoComposeActivity.kt)。\n\n## 常见坑\n\n- **相同 URL 不重播**：忘了 `setPlayTag/setPlayPosition`，`setUp` 内部去重返回 false。\n- **切换过快出黑帧**：`setUp` 后立即 `startPlayLogic` 前先隐藏 `thumb`；或用 `KeepLastFrameVideo` 保留上一帧（详见 [doc/KEEP_LAST_FRAME.md](../../doc/KEEP_LAST_FRAME.md)）。\n- **同屏多个 item 各自播放**：单例 `GSYVideoManager` 只能有一个 listener；同屏并行需要参考 [CustomManager](../../app/src/main/java/com/example/gsyvideoplayer/video/manager/CustomManager.java)。\n","frontmatter":{"name":"gsy-list-playback","description":"RecyclerView / ViewPager2 / ViewPager 列表滑动自动播放，滚动切换、复用绑定、进入详情页时\"漂移\"到全屏。","when_to_use":"短视频信息流、朋友圈式内嵌播放、抖音/快手类 ViewPager2 竖播、列表进入详情页的无缝续播。","inputs":[{"name":"adapter","description":"RecyclerView.Adapter / FragmentStateAdapter","required":true}],"outputs":["列表条目具备\"进入可视区自动播放，离开释放\"的能力，且切换详情页可续播。"],"references":[{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/ListVideoUtil.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoHelper.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/utils/ScrollCalculatorHelper.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/AutoPlayRecyclerViewActivity.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/ViewPager2Activity.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/RecyclerViewActivity.java"}]},"isInternal":false,"tokens":1602,"sizeBytes":5403},{"name":"SKILL.md","path":"skills/06-gsy-cache-and-download/SKILL.md","rawUrl":"https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/06-gsy-cache-and-download/SKILL.md","title":"gsy-cache-and-download","category":"anthropic-skill","format":"markdown","content":"---\nname: gsy-cache-and-download\ndescription: 边播边缓（AndroidVideoCache 代理）、自定义缓存目录、清理缓存、离线下载至完整文件。\nwhen_to_use: 需要\"边看边攒\"离线可播；或空间敏感需要限量清理；或做真下载入库（不是缓存代理）。\ninputs:\n  - name: url\n    description: 网络视频地址（http/https）\n    required: true\n  - name: cache_dir\n    description: 缓存目录（默认 context.cacheDir/videoCache）\n    required: false\noutputs:\n  - 播放同时把 mp4 缓存到本地；命中缓存后重播 0 网络。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheManager.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/CacheFactory.java\n  - file: gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/cache/ICacheManager.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailDownloadPlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailDownloadExoPlayer.java\n---\n\n# gsy-cache-and-download\n\n## 概念\n\n- **缓存**（cache）：`ProxyCacheManager` 内部起本地 HTTP 代理，拦截 `player` 的请求，把响应流一边喂给 player 一边写到本地；文件名是 URL MD5。\n- **下载**（download）：与缓存共享目录，但一定要走\"从头到尾\"完整播完才能保证文件可复用为独立 mp4；提前 seek 会造成 range 分片，回放时代理会重新下载缺失段。\n\n## 开启缓存\n\n方式 1：`setUp(url, true, title)` 第二参数 `cacheWithPlay=true`。\n\n方式 2：`GSYVideoOptionBuilder`：\n- `setCacheWithPlay(true)`\n- `setCachePath(File)`（可选）\n\n## 自定义缓存目录\n\n```java\nFile dir = new File(getExternalFilesDir(null), \"videoCache\");\nbuilder.setCachePath(dir);\n```\n\n若使用不同 `CacheManager`（例如 Exo），传给 `CacheFactory.setCacheManager(...)` 前记得也走 `setCachePath` 或自定义 `ICacheManager` 实现。\n\n## `ICacheManager` API\n\n见 [ICacheManager](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/cache/ICacheManager.java)。核心方法：\n\n| 方法 | 说明 |\n|---|---|\n| `doCacheLogic(Context, IMediaPlayer, String url, Map<String,String> headers, File cachePath)` | 播放前介入，把 url 换为代理 url |\n| `clearCache(Context, File cachePath, String url)` | 单条清除；`url = null` 清空整目录 |\n| `hadCached()` | 当前播放是否已\"命中\"缓存（100% 完成） |\n| `setCacheAvailableListener(CacheAvailableListener)` | 缓存进度回调 |\n| `release()` | 释放代理 |\n\n## `ProxyCacheManager` 关键静态方法\n\n在 [ProxyCacheManager](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheManager.java) 里：\n\n- `clearAllCache(Context, File)` —— 清空整个缓存目录。\n- 内部 `HttpProxyCacheServer` 单例 lazy 初始化，第一次 `doCacheLogic` 时才起服务；Application 就绪即可。\n\n## `ProxyCacheUserAgentHeadersInjector`\n\n自定义 UA / Cookie / Referer 注入到代理层：见 [ProxyCacheUserAgentHeadersInjector](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheUserAgentHeadersInjector.java)。挂到 `HttpProxyCacheServer.Builder.headerInjector()` 上，或直接用 `Map` 传给 `setUp`。\n\n## 离线下载\n\n三步流程：\n\n1. 用 `HttpProxyCacheServer` 单独跑一次预下载（不 attach 到 Player），或直接开一条播放并 `setStartAfterPrepared(false)` 静默走全长；\n2. 事件 `VideoAllCallBack#onAutoComplete` 触发 → 缓存文件已完整；\n3. 用 [FileUtils](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/FileUtils.java) 拷贝 / 重命名到业务目录。\n\nDemo：[DetailDownloadPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailDownloadPlayer.java)（IJK）与 [DetailDownloadExoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailDownloadExoPlayer.java)（Exo）。\n\n## 不缓存的场景\n\n以下情况 `cacheWithPlay` 应设 `false`：\n- 直播 / RTMP / RTSP（连续无尽流）；\n- HLS m3u8（代理逻辑对分片不友好，建议 Exo 内核 + Exo cache）；\n- DASH / SmoothStreaming；\n- DRM 或私有加密。\n\n## 常见坑\n\n- **代理端口被占**：`HttpProxyCacheServer` 首次启动失败会 fallback 到原始 url，缓存不生效。多进程时可能出现，建议仅主进程用。\n- **缓存目录跨用户**：`context.cacheDir` 是 per-user；用外部存储需要动态权限。\n- **同 URL 缓存名冲突**：MD5(url) 相同即命中；如果 URL 带一次性签名，每次不同 → 缓存穿透。可以先规范化 URL 再传给 `setUp`。\n- **HTTP header 变更失效**：headers 参与代理请求生成，但缓存文件名只按 URL；同一 URL 换 UA 不会重新下载。\n","frontmatter":{"name":"gsy-cache-and-download","description":"边播边缓（AndroidVideoCache 代理）、自定义缓存目录、清理缓存、离线下载至完整文件。","when_to_use":"需要\"边看边攒\"离线可播；或空间敏感需要限量清理；或做真下载入库（不是缓存代理）。","inputs":[{"name":"url","description":"网络视频地址（http/https）","required":true},{"name":"cache_dir","description":"缓存目录（默认 context.cacheDir/videoCache）","required":false}],"outputs":["播放同时把 mp4 缓存到本地；命中缓存后重播 0 网络。"],"references":[{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheManager.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/CacheFactory.java"},{"file":"gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/cache/ICacheManager.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/DetailDownloadPlayer.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/DetailDownloadExoPlayer.java"}]},"isInternal":false,"tokens":1490,"sizeBytes":4793},{"name":"SKILL.md","path":"skills/07-gsy-render-and-effects/SKILL.md","rawUrl":"https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/07-gsy-render-and-effects/SKILL.md","title":"gsy-render-and-effects","category":"anthropic-skill","format":"markdown","content":"---\nname: gsy-render-and-effects\ndescription: 三种渲染载体（TextureView / SurfaceView / GLSurfaceView）、显示比例（16:9/4:3/full/custom）、GL 滤镜与自定义 Shader。\nwhen_to_use: 想放竖屏视频、需要 GL 滤镜、要做全屏裁减/拉伸、SurfaceView 与动画冲突、想抓截图或 gif。\ninputs:\n  - name: render_type\n    description: TEXTURE / SURFACE / GLSURFACE\n    required: true\n  - name: show_type\n    description: 显示比例枚举\n    required: false\noutputs:\n  - 渲染载体 + 显示比例 + 滤镜均按需生效。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/GSYRenderView.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/view/GSYVideoGLView.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/effect\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailFilterActivity.java\n---\n\n# gsy-render-and-effects\n\n## 渲染类型 `GSYVideoType.setRenderType(int)`\n\n| 常量 | 值 | 说明 |\n|---|---|---|\n| `TEXTURE` | 0 | 默认，与动画/圆角/缩放兼容最好 |\n| `SURFACE` | 1 | 性能最好，但和 View 动画冲突 |\n| `GLSURFACE` | 2 | 必需，若要用 `setEffectFilter` |\n\n见 [GSYVideoType#L39-L49](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java)。设置时机：`setUp` 之前，全局生效。\n\n## 显示比例 `GSYVideoType.setShowType(int)`\n\n| 常量 | 值 | 说明 |\n|---|---|---|\n| `SCREEN_TYPE_DEFAULT` | 0 | 按视频原比例居中 |\n| `SCREEN_TYPE_16_9` | 1 | 强制 16:9 |\n| `SCREEN_TYPE_4_3` | 2 | 强制 4:3 |\n| `SCREEN_TYPE_18_9` | 6 | 全面屏 |\n| `SCREEN_TYPE_FULL` | 4 | 全屏裁减；`surface_container` 建议 `FrameLayout` |\n| `SCREEN_MATCH_FULL` | -4 | 全屏拉伸；`surface_container` 建议 `FrameLayout` |\n| `SCREEN_TYPE_CUSTOM` | -5 | 需先 `setScreenScaleRatio(float)` |\n\n见 [GSYVideoType#L10-L29](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java) 与 [GSYVideoType#L165-L167](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java)。\n\n## 硬解码开关\n\n| 静态方法 | 说明 |\n|---|---|\n| `enableMediaCodec()` / `disableMediaCodec()` | IJK 硬解码总开关 [#L71-L79](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java) |\n| `enableMediaCodecTexture()` / `disableMediaCodecTexture()` | 硬解 + Texture 直渲染优化 [#L100-L108](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java) |\n| `enableSmartMediaCodec()` / `disableSmartMediaCodec()` | 硬解失败自动软解回退 [#L86-L94](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java) |\n| `isMediaCodec()` / `isMediaCodecTexture()` / `isSmartMediaCodec()` | 查询 |\n\n设置时机：**`setUp` 之前**，且**部分机型**需要在 `Application.onCreate` 就配好。\n\n## GL 滤镜\n\n需要满足两个条件：\n1. `GSYVideoType.setRenderType(GSYVideoType.GLSURFACE)`\n2. `builder.setEffectFilter(new XxxEffect())` 或 `player.setEffectFilter(...)`\n\nSDK 内置滤镜（在 `com.shuyu.gsyvideoplayer.render.effect.*`）：`AutoFixEffect`、`BarrelBlurEffect`、`BlackAndWhiteEffect`、`BrightnessEffect`、`ContrastEffect`、`CrossProcessEffect`、`DocumentaryEffect`、`DuotoneEffect`、`FillLightEffect`、`GammaEffect`、`GaussianBlurEffect`、`GrainEffect`、`GreyScaleEffect`、`HueEffect`、`InvertColorsEffect`、`LamoishEffect`、`NoEffect`、`OverlayEffect`、`PosterizeEffect`、`SampleBlurEffect`、`SaturationEffect`、`SepiaEffect`、`SharpnessEffect`、`TemperatureEffect`、`TintEffect`、`VignetteEffect`。目录：[render/effect](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/effect)。\n\n自定义 Shader：实现 [`GSYVideoGLView.ShaderInterface`](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/view/GSYVideoGLView.java)。Demo 里 4 个复杂样例：[GSYVideoGLViewCustomRender](../../app/src/main/java/com/example/gsyvideoplayer/effect/GSYVideoGLViewCustomRender.java) / 2 / 3 / 4，以及像素化 [PixelationEffect](../../app/src/main/java/com/example/gsyvideoplayer/effect/PixelationEffect.java)、BitmapOverlay [BitmapEffect](../../app/src/main/java/com/example/gsyvideoplayer/effect/BitmapEffect.java) / [BitmapIconEffect](../../app/src/main/java/com/example/gsyvideoplayer/effect/BitmapIconEffect.java)。\n\n## v14.0.0 多 pass 与新效果\n\n- `GSYVideoGLViewMultiPassRender` + `setMultiPassEffect` + `player.setCustomGLRenderer` 接入 `GaussianBlurMultiPassEffect` / `IterativeBlurPyramidEffect` / `BloomEffect`。这些不是单 pass `ShaderInterface` 滤镜，不能直接传给 `setEffectFilter`。\n- `LookupEffect(assetPath, intensity)` / `BeautyEffect(smoothLevel, whiteLevel)` / `GlitchEffect` / `CrtEffect` / `OldTvSignalEffect` 使用 `setEffectFilter`。LUT 必须由使用方放入自己的 assets；Demo 的三张图不在播放器 AAR 中。\n- `TextureShaderInterface` 在 GL 线程管理额外纹理；动态效果使用渲染器按秒绑定的 `uTime`。\n- 显示比例变化后，传统 View 调 `player.getCurrentPlayer().changeTextureViewShowType()`；Compose 调 `controller.changeTextureViewShowType()`。\n- 完整代码入口和实现文档见 [RECENT_FEATURES.md](../../doc/RECENT_FEATURES.md#v1400-gl-管线与新滤镜)。\n\n## 抓帧 / gif / 截图\n\n在 [GSYRenderView](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/GSYRenderView.java)：\n\n| 方法 | 回调 | 说明 |\n|---|---|---|\n| `taskShotPic(GSYVideoShotListener)` | 返 Bitmap | 单帧截图 |\n| `saveFrame(File, GSYVideoShotSaveListener)` | 保存到磁盘 | 落盘截图 |\n| `taskGifPic(...)` + `GifCreateHelper` | 生成 gif | 见 [GifCreateHelper](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GifCreateHelper.java) |\n\n## Demo 对照\n\n- 滤镜集合：[DetailFilterActivity](../../app/src/main/java/com/example/gsyvideoplayer/DetailFilterActivity.java) + [DetailFilterComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/DetailFilterComposeActivity.kt)\n- 透明视频：[SampleTransparentVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/SampleTransparentVideo.java) + [DetailTransparentActivity](../../app/src/main/java/com/example/gsyvideoplayer/DetailTransparentActivity.java)\n- 自定义 Render：[CustomRenderVideoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/CustomRenderVideoPlayer.java)\n\n## 常见坑\n\n- `SurfaceView` 在 `RecyclerView` 里滑动会撕裂 / 覆盖 → 列表用 `TEXTURE`。\n- GL 效果的性能取决于分辨率、pass 数和设备；先评估 pass/金字塔层数与实际帧率，不能通过静默换成 TextureView 宣称效果已生效。\n- 竖屏视频强制 `SCREEN_TYPE_FULL` 会大幅裁减；建议 `SCREEN_TYPE_DEFAULT` + `setAutoFullWithSize(true)`。\n","frontmatter":{"name":"gsy-render-and-effects","description":"三种渲染载体（TextureView / SurfaceView / GLSurfaceView）、显示比例（16:9/4:3/full/custom）、GL 滤镜与自定义 Shader。","when_to_use":"想放竖屏视频、需要 GL 滤镜、要做全屏裁减/拉伸、SurfaceView 与动画冲突、想抓截图或 gif。","inputs":[{"name":"render_type","description":"TEXTURE / SURFACE / GLSURFACE","required":true},{"name":"show_type","description":"显示比例枚举","required":false}],"outputs":["渲染载体 + 显示比例 + 滤镜均按需生效。"],"references":[{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/GSYRenderView.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/view/GSYVideoGLView.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/effect"},{"file":"app/src/main/java/com/example/gsyvideoplayer/DetailFilterActivity.java"}]},"isInternal":false,"tokens":2235,"sizeBytes":7050},{"name":"SKILL.md","path":"skills/08-gsy-subtitle-and-preview/SKILL.md","rawUrl":"https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/08-gsy-subtitle-and-preview/SKILL.md","title":"gsy-subtitle-and-preview","category":"anthropic-skill","format":"markdown","content":"---\nname: gsy-subtitle-and-preview\ndescription: 外挂/内嵌字幕（SRT/VTT）、Exo 内嵌字幕轨、Seek 悬浮预览缩略图（VTT thumbnails）。\nwhen_to_use: 需要显示 srt/vtt/ass 字幕、切换字幕轨、seek 时鼠标/手指浮层展示预览帧。\ninputs:\n  - name: subtitle_source\n    description: 字幕文件路径或 URL、MIME\n    required: false\n  - name: preview_source\n    description: VTT 预览描述文件\n    required: false\noutputs:\n  - player 上叠加 subtitle view；seekbar 拖动出现预览帧。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview\n  - file: app/src/main/java/com/example/gsyvideoplayer/SubtitleDetailPlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitlePlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/PreViewGSYVideoPlayer.java\n---\n\n# gsy-subtitle-and-preview\n\n## 字幕 API\n\n顶层控制器：[GSYSubtitleController](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleController.java)。视图：[GSYSubtitleView](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleView.java)。\n\n| 方法 | 说明 |\n|---|---|\n| `new GSYSubtitleController(context, subtitleView)` | 绑定 UI |\n| `setSources(List<GSYSubtitleSource>)` | 一次注入多轨字幕 |\n| `setSource(GSYSubtitleSource)` | 只有一条 |\n| `getSources()` | 只读 List |\n| `selectSubtitle(String id)` | 按 id 切换字幕，\"\" 或 null 关闭 |\n| `setStyle(GSYSubtitleStyle)` | 字体/描边/背景/颜色 |\n| `setOffsetMs(long)` | 音画/字幕 offset |\n| `setEnabled(boolean)` | 开关 |\n| `updatePosition(long positionMs)` | 由 player 进度驱动 |\n| `release()` | 释放 |\n\n## `GSYSubtitleSource` 字段\n\n见 [GSYSubtitleSource](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleSource.java)：\n\n| 字段 | 说明 |\n|---|---|\n| `id` | 唯一 id |\n| `label` | 显示名 |\n| `language` | ISO code |\n| `mime` | 见 [GSYSubtitleMime](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleMime.java)：`text/vtt`、`application/x-subrip`（srt）、`text/plain` |\n| `uri` | http 或 file uri |\n| `embedded` | 是否内嵌轨（Exo 场景） |\n\n## 内置解析器\n\n- SRT：[GSYSrtSubtitleParser](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSrtSubtitleParser.java)\n- WebVTT：[GSYWebVttSubtitleParser](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYWebVttSubtitleParser.java)\n- 自定义 MIME：注册到 [GSYSubtitleParserFactory](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleParserFactory.java)\n\n## Exo 内嵌字幕\n\n`gsyvideoplayer-exo_player2` 的 Manager 会把 Exo 的 subtitle track 通过 `GSYSubtitleController` 上抛。Demo：[GSYExoSubTitlePlayer](../../app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitlePlayer.java) + [GSYExoSubTitleVideoManager](../../app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitleVideoManager.java)。\n\n## Seek 预览 API\n\n顶层：[GSYVideoPreviewProvider](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview/GSYVideoPreviewProvider.java) 与列表实现 [GSYVideoPreviewListProvider](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview/GSYVideoPreviewListProvider.java)。\n\n| 方法 | 说明 |\n|---|---|\n| `GSYVideoPreviewProvider#getPreviewFrame(long positionMs)` | 返 `GSYVideoPreviewFrame`，含 image url + crop 坐标 |\n| `GSYVideoPreviewListProvider(List<GSYVideoPreviewFrame>)` | 直接注入帧列表（自己解析或用 VTT） |\n| `GSYVideoPreviewVttParser#parse(String vttContent, String baseUrl)` | 解析 WebVTT thumbnails（雪碧图 + 时间段） |\n| `GSYVideoPreviewVttParser#parseFrames(String vttContent, String baseUrl)` | 只要帧列表 |\n\n## `GSYVideoPreviewFrame` 字段\n\n见 [GSYVideoPreviewFrame](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview/GSYVideoPreviewFrame.java)：`startTimeMs`、`endTimeMs`、`imageUrl`、`cropX/Y/Width/Height`、`hasCrop()`。\n\n## 关键调用序（VTT 预览 + SRT 字幕）\n\n1. 加载 subtitle：`controller.setSource(new GSYSubtitleSource(\"cn\", \"中文\", \"zh\", GSYSubtitleMime.SRT, uri, false))`;\n2. 播放进度回调（`GSYVideoProgressListener`）中 `controller.updatePosition(current)`；\n3. 预览：`GSYVideoPreviewVttParser.parse(vttText, base)` → 得到 Provider → 注入到 `PreViewGSYVideoPlayer#setPreviewProvider(provider)`；\n4. seekbar 拖动时 SDK 会自动调 `provider.getPreviewFrame(target)` 并做 crop。\n\n## Demo 对照\n\n- SRT 字幕：[SubtitleDetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/SubtitleDetailPlayer.java)（对应 `raw/demo_subtitle.srt` / `demo_subtitle_vtt.vtt`）\n- Exo 内嵌字幕：[GSYExoSubTitleDetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitleDetailPlayer.java)\n- 预览缩略图：[PreViewGSYVideoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/PreViewGSYVideoPlayer.java) + [PlayPickActivity](../../app/src/main/java/com/example/gsyvideoplayer/PlayPickActivity.java)\n\n## 常见坑\n\n- SRT 时间轴带 BOM 或换行不规范会直接被 parser 丢弃；先用 UTF-8 无 BOM 保存。\n- VTT 缩略图 URL 相对路径必须传 `baseUrl` 才能拼绝对路径。\n- `updatePosition` 必须在 UI 线程或至少与 `subtitleView` 同线程；`GSYSubtitleController` 内部无线程切换。\n","frontmatter":{"name":"gsy-subtitle-and-preview","description":"外挂/内嵌字幕（SRT/VTT）、Exo 内嵌字幕轨、Seek 悬浮预览缩略图（VTT thumbnails）。","when_to_use":"需要显示 srt/vtt/ass 字幕、切换字幕轨、seek 时鼠标/手指浮层展示预览帧。","inputs":[{"name":"subtitle_source","description":"字幕文件路径或 URL、MIME","required":false},{"name":"preview_source","description":"VTT 预览描述文件","required":false}],"outputs":["player 上叠加 subtitle view；seekbar 拖动出现预览帧。"],"references":[{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview"},{"file":"app/src/main/java/com/example/gsyvideoplayer/SubtitleDetailPlayer.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitlePlayer.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/video/PreViewGSYVideoPlayer.java"}]},"isInternal":false,"tokens":1767,"sizeBytes":5655},{"name":"SKILL.md","path":"skills/09-gsy-live-and-ads/SKILL.md","rawUrl":"https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/09-gsy-live-and-ads/SKILL.md","title":"gsy-live-and-ads","category":"anthropic-skill","format":"markdown","content":"---\nname: gsy-live-and-ads\ndescription: 直播（RTMP/RTSP/HLS）、贴片广告 (PreRoll AD)、静音、切码率、无缝切源、切换过程保留最后一帧。\nwhen_to_use: 做直播；短视频/长视频前贴片广告；PV/UV 埋点后要 A/B 分发；快节奏内容切换。\ninputs:\n  - name: stream_url\n    description: 直播地址（rtmp://.../ rtsp://.../ .m3u8 等）\n    required: false\n  - name: ad_url\n    description: 前贴广告地址\n    required: false\noutputs:\n  - 直播不使用缓存；广告播放完切主片；无缝续播不闪黑。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYADVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYSampleADVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoADManager.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseADActivityDetail.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer2.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/KeepLastFrameDemoActivity.java\n---\n\n# gsy-live-and-ads\n\n## 直播接入要点\n\n- `setUp(url, false, title)` —— `cacheWithPlay` 必须 `false`。\n- `onResume` / `player.onVideoResume(false)`：**`seek=false` 避免拉流跳变**（见 [GSYVideoView#L549](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java)）。\n- 内核选择：\n  - RTMP / RTSP：**必须 IJK**；System / Exo / Aliyun 各有兼容性差异。\n  - HLS：Exo 更稳；Ijk 需要开启硬解 `GSYVideoType.enableMediaCodec()` 才顺。\n- 关键参数：\n  - `builder.setNeedShowWifiTip(false)`（后台 App 不打断）\n  - `builder.setReleaseWhenLossAudio(false)`（丢音频焦点不释放）\n  - `builder.setSurfaceErrorPlay(true)`\n\n对 IJK 加 low-latency：通过 `VideoOptionModel` 注入 ffmpeg option，见 [BasePlayerManager](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/BasePlayerManager.java) 与 [GSYVideoManager.instance().setOptionModelList(list)](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoBaseManager.java)。常用 option：`analyzemaxduration=100`、`probesize=10240`、`flush_packets=1`、`fflags=nobuffer`、`packet-buffering=0`、`framedrop=1`。\n\n## 广告 AD\n\n- 双 Manager 双 Player：`GSYVideoADManager.instance()` 负责广告 player，`GSYVideoManager.instance()` 负责主片；两者共存不打架。\n- 组件 [GSYADVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYADVideoPlayer.java) 与\"广告示例\"[GSYSampleADVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYSampleADVideoPlayer.java) 都继承 `StandardGSYVideoPlayer`。\n- Activity 基类：[GSYBaseADActivityDetail](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseADActivityDetail.java)，提供 `getGSYVideoADPlayer()` / `initADVideo()` / `startAD(String adUrl, String url)` / `onADEnd`。\n\n### `startAD(...)` 关键调用序\n\n1. Activity 里持有主 player + AD player 两个 View；\n2. `initADVideo()` 装配（内部会连锁 `OrientationUtils`）；\n3. `startAD(adUrl, mainUrl)` 播广告；\n4. AD complete → SDK 内部隐藏 AD player、start 主 player；\n5. 用户点\"跳过\"→ `getGSYVideoADPlayer().onAutoCompletion()`。\n\n## 无缝切源 / 切码率\n\nExo 内核用 `Exo2PlayerManager.setCurrentMediaSource(...)` 或 Demo 里的 `GSYExo2MediaPlayer.setMediaSources(...)`；Ijk 则重新 `setUp` + `startPlayLogic`。切换的过程中要求\"不闪黑\"，用：\n\n- [KeepLastFrameVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/KeepLastFrameVideo.java)（Demo 组件）+ [KeepLastFrameDemoActivity](../../app/src/main/java/com/example/gsyvideoplayer/KeepLastFrameDemoActivity.java)；\n- 原理见 [doc/KEEP_LAST_FRAME.md](../../doc/KEEP_LAST_FRAME.md)：新 player 拿到首帧前，把旧 SurfaceTexture 内容以 Bitmap 覆盖在上层。\n\n## 广告 / 主片全屏切换的两个陷阱\n\n- 主片全屏 → 广告插入：`GSYVideoADManager` 无内部 `OrientationUtils`；`GSYBaseADActivityDetail` 已处理，但**自定义子类需重写 `getGSYVideoADPlayer()` 返回 AD player 才能被基类的旋转/回退纳管**。\n- 广告全屏返回 → 直接销毁 AD player：`onBackPressed` 需要判断 `GSYVideoManager.backFromWindowFull(this) || GSYVideoADManager.backFromWindowFull(this)`。\n\n## Demo 对照\n\n- 单主片：[DetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java)\n- 前贴片 + 主片：[DetailADPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer.java) / [DetailADPlayer2](../../app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer2.java)\n- 列表条目内广告：[ListADVideoActivity](../../app/src/main/java/com/example/gsyvideoplayer/ListADVideoActivity.java) / [ListADVideoActivity2](../../app/src/main/java/com/example/gsyvideoplayer/ListADVideoActivity2.java) / [RequestListADVideoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/RequestListADVideoPlayer.java)\n- 智能硬解回退：[SmartMediaCodecFallbackActivity](../../app/src/main/java/com/example/gsyvideoplayer/SmartMediaCodecFallbackActivity.java)\n\n## v14.0.0 RTSP 与倍速\n\n- FORMAT `timeout` 为微秒（`20_000_000L` = 20 秒），明确配置的 `rtsp_transport` 会透传。\n- PLAYER `rtsp-live-max-buffer-ms` 在 prepare 前设置，`0` 关闭，`500..60000` 毫秒用于正常 1× 直播队列恢复。恢复会丢弃旧媒体并产生跳跃/音频间隙，不是自动重连。\n- 初始化重定向覆盖 OPTIONS/DESCRIBE/SETUP/首次 PLAY；不支持播放中 REDIRECT 或任意跨端口控制会话。\n- `soundtouch=1` 为 0.25×–4× 软件变速；`soundtouch=0` 为设备相关平台范围。引擎切换和 EOF 按真实音频排空处理，终止错误后需要 reset 或重建。\n- 详见 [QUESTION.md](../../doc/QUESTION.md) 与 [v14 发布说明](../../doc/UPDATE_VERSION.md#v1400-2026-10-10)。\n\n## 常见坑\n\n- 直播卡顿升级 → 检查是否开启 `cacheWithPlay`（对直播来说必须关）。\n- 广告 `onAutoComplete` 后主片没自动 setUp → 检查是不是把 `setStartAfterPrepared(false)` 传给了主 builder。\n- RTMP 断流回收后重连白屏 → `onVideoResume(false)` + `player.getCurrentPlayer().onVideoReset()` 再 `startPlayLogic()`。\n","frontmatter":{"name":"gsy-live-and-ads","description":"直播（RTMP/RTSP/HLS）、贴片广告 (PreRoll AD)、静音、切码率、无缝切源、切换过程保留最后一帧。","when_to_use":"做直播；短视频/长视频前贴片广告；PV/UV 埋点后要 A/B 分发；快节奏内容切换。","inputs":[{"name":"stream_url","description":"直播地址（rtmp://.../ rtsp://.../ .m3u8 等）","required":false},{"name":"ad_url","description":"前贴广告地址","required":false}],"outputs":["直播不使用缓存；广告播放完切主片；无缝续播不闪黑。"],"references":[{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYADVideoPlayer.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYSampleADVideoPlayer.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoADManager.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseADActivityDetail.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer2.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/KeepLastFrameDemoActivity.java"}]},"isInternal":false,"tokens":2086,"sizeBytes":6506},{"name":"SKILL.md","path":"skills/10-gsy-cast-and-loopback/SKILL.md","rawUrl":"https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/10-gsy-cast-and-loopback/SKILL.md","title":"gsy-cast-and-loopback","category":"anthropic-skill","format":"markdown","content":"---\nname: gsy-cast-and-loopback\ndescription: 使用内核层投屏 SPI（CastCapability + CastProvider）接入 DLNA，或做本地 loopback 回环模拟被投屏端。\nwhen_to_use: 需要接入 DLNA/AirPlay/Chromecast 到已有播放页；做投屏发起端 UI；做集成测试的本地 loopback 接收端；或做多协议聚合发现。\ninputs:\n  - name: providers\n    description: 一个或多个 CastProvider 实例（DLNA 默认由 jUPnP 提供）\n    required: true\noutputs:\n  - CastCapability 单例已注册 provider，可发现设备、连接、控制播放/停止。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastProvider.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastSession.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastListener.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastMediaInfo.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastState.java\n  - file: gsyVideoPlayer-cast/src/main/java/com/shuyu/gsyvideoplayer/cast/dlna\n  - file: app/src/main/java/com/example/gsyvideoplayer/CastDemoActivity.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/cast\n  - file: doc/CAST_FEATURE_PLAN.md\n---\n\n# gsy-cast-and-loopback\n\n## 顶层架构\n\n```\nCastCapability (单例)  ← 主线程 API 入口\n  ├─ registerProvider(CastProvider)\n  ├─ startDiscovery(Context) / stopDiscovery()\n  ├─ getAvailableDevices(): List<CastDevice>\n  ├─ connect(device, ConnectCallback) : CastSession（异步）\n  ├─ addListener(CastListener)\n  └─ disconnect()\n```\n\n对齐 Media3 `Player` 语义：`CastCapability ≈ Player 池`、`activeSession ≈ 当前 Player`、`CastProvider ≈ 渲染后端`、`CastListener/SessionListener ≈ Player.Listener`。见 [CastCapability](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) 与 [CastProvider](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastProvider.java) 的头 JavaDoc。\n\n## 核心 API\n\n### CastCapability\n\n| 方法 | 位置 | 说明 |\n|---|---|---|\n| `getInstance()` | [#L38-L47](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | DCL 单例 |\n| `registerProvider(CastProvider)` | [#L85-L90](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 注册；`protocol` 相同则覆盖 |\n| `unregisterProvider(String)` | [#L93-L103](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 反注册并停止其发现 |\n| `startDiscovery(Context)` | [#L106-L125](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 所有已注册 provider 同时开始发现 |\n| `stopDiscovery()` | [#L128-L135](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 全部停止 |\n| `getAvailableDevices()` | [#L138-L146](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 快照，跨 provider 聚合 |\n| `connect(CastDevice, ConnectCallback)` | [#L154-L183](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 会自动 disconnect 旧会话 |\n| `disconnect()` | [#L186-L196](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | release 当前 session |\n| `getActiveSession()` | [#L199-L201](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 拿当前会话（可能 null） |\n| `addListener / removeListener` | [#L203-L209](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 主线程 listener |\n\n### CastProvider（SPI 契约）\n\n| 方法 | 说明 |\n|---|---|\n| `getProtocol(): String` | 全小写协议 id（`dlna` / `chromecast` / `airplay`） |\n| `startDiscovery(Context, DiscoveryListener)` | 后台线程订阅，回调 postMain |\n| `stopDiscovery()` | 释放订阅、解绑 Service |\n| `connect(CastDevice, ConnectCallback): CastSession` | 建立会话；同步返回 session，异步回调结果 |\n\n线程约定：所有 network IO 由 provider 自己后台跑，回调都 postMain。\n\n### CastSession（媒体控制）\n\n见 [CastSession](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastSession.java)：\n\n| 方法 | 说明 |\n|---|---|\n| `load(CastMediaInfo)` | 推送要投的 URL |\n| `play() / pause() / stop()` | 播控 |\n| `seekTo(long ms)` | 定位 |\n| `setVolume(int 0-100)` / `setMute(boolean)` | 音量 |\n| `getState(): CastState` | IDLE / CONNECTING / CONNECTED / LOADING / PLAYING / PAUSED / STOPPED / ERROR，见 [CastState](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastState.java) |\n| `addListener(SessionListener)` | 位置/状态回调 |\n| `release()` | 归还 |\n\n### CastMediaInfo（要投的媒体）\n\n见 [CastMediaInfo](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastMediaInfo.java)。字段：`url`、`title`、`durationMs`、`mimeType`、`headers`、`metadata`（`Map`）。\n\n## DLNA 参考实现\n\n模块 `gsyvideoplayer-cast` 提供 [JupnpDlnaProvider](../../gsyVideoPlayer-cast/src/main/java/com/shuyu/gsyvideoplayer/cast/dlna/JupnpDlnaProvider.java) 与 [JupnpDlnaSession](../../gsyVideoPlayer-cast/src/main/java/com/shuyu/gsyvideoplayer/cast/dlna/JupnpDlnaSession.java)。业务代码调用：\n\n```java\nCastCapability cap = CastCapability.getInstance();\ncap.registerProvider(new JupnpDlnaProvider());\ncap.addListener(myListener);\ncap.startDiscovery(context);\n```\n\n发现事件：`CastListener#onDeviceListChanged(List<CastDevice>)`；连接：`cap.connect(devices.get(0), connectCallback)`。\n\n## 本地 Loopback（Demo 测试用）\n\napp 模块下的 [cast/](../../app/src/main/java/com/example/gsyvideoplayer/cast) 提供**可发现的接收端**，方便在同一台设备上完成集成测试：\n\n- [DevReceiverService](../../app/src/main/java/com/example/gsyvideoplayer/cast/DevReceiverService.java) —— foreground service，通过 jUPnP 注册\"回环设备\"。\n- [LoopbackDeviceFactory](../../app/src/main/java/com/example/gsyvideoplayer/cast/LoopbackDeviceFactory.java) —— 构造 UPnP MediaRenderer。\n- [LoopbackAvTransportService](../../app/src/main/java/com/example/gsyvideoplayer/cast/LoopbackAvTransportService.java) / [LoopbackRenderingControlService](../../app/src/main/java/com/example/gsyvideoplayer/cast/LoopbackRenderingControlService.java) —— AVTransport / RenderingControl 实现。\n- [CastReceiverFloatingWindow](../../app/src/main/java/com/example/gsyvideoplayer/cast/CastReceiverFloatingWindow.java) + [CastReceiverPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/CastReceiverPlayer.java) —— 可视化播放。\n\n## Demo 对照\n\n- 一体化：[CastDemoActivity](../../app/src/main/java/com/example/gsyvideoplayer/CastDemoActivity.java)\n- Cast 相关控制页：[SampleCastControlVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/SampleCastControlVideo.java)\n- 设计与测试：[doc/CAST_FEATURE_PLAN.md](../../doc/CAST_FEATURE_PLAN.md)、[doc/CAST_RECEIVER_DESIGN.md](../../doc/CAST_RECEIVER_DESIGN.md)、[doc/CAST_TEST_PLAYBOOK.md](../../doc/CAST_TEST_PLAYBOOK.md)\n\n## 常见坑\n\n- 不同 provider 上报的 `CastDevice` 需要用 `getProtocol()` 分组；直接把陌生 protocol 传给 `connect` 会拿到 `IllegalStateException`。\n- Provider 网络 IO 必须在自己线程，`CastCapability` 不做 main 切换。\n- Wi-Fi 多播锁没拿 → DLNA 发现不到设备：Provider 需要 `WifiManager.MulticastLock`。\n- Loopback 与真实设备同网段时会同时被发现，Demo 里有过滤示例。\n","frontmatter":{"name":"gsy-cast-and-loopback","description":"使用内核层投屏 SPI（CastCapability + CastProvider）接入 DLNA，或做本地 loopback 回环模拟被投屏端。","when_to_use":"需要接入 DLNA/AirPlay/Chromecast 到已有播放页；做投屏发起端 UI；做集成测试的本地 loopback 接收端；或做多协议聚合发现。","inputs":[{"name":"providers","description":"一个或多个 CastProvider 实例（DLNA 默认由 jUPnP 提供）","required":true}],"outputs":["CastCapability 单例已注册 provider，可发现设备、连接、控制播放/停止。"],"references":[{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastProvider.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastSession.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastListener.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastMediaInfo.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastState.java"},{"file":"gsyVideoPlayer-cast/src/main/java/com/shuyu/gsyvideoplayer/cast/dlna"},{"file":"app/src/main/java/com/example/gsyvideoplayer/CastDemoActivity.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/cast"},{"file":"doc/CAST_FEATURE_PLAN.md"}]},"isInternal":false,"tokens":2328,"sizeBytes":7895},{"name":"SKILL.md","path":"skills/11-gsy-compose-integration/SKILL.md","rawUrl":"https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/11-gsy-compose-integration/SKILL.md","title":"gsy-compose-integration","category":"anthropic-skill","format":"markdown","content":"---\nname: gsy-compose-integration\ndescription: 在 Jetpack Compose 中通过 AndroidView 桥接 GSYVideoPlayer，覆盖列表、竖直短视频、生命周期管控。\nwhen_to_use: 项目已经或正在迁移到 Compose；需要在可组合项里嵌入播放器；需要复用列表滑动/自动播放逻辑但 UI 用 Compose。\ninputs:\n  - name: url\n    description: 视频 URL\n    required: true\noutputs:\n  - Composable 内嵌 GSYVideoPlayer，生命周期与 Composable 一致。\nreferences:\n  - file: gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose\n  - file: app/src/main/java/com/example/gsyvideoplayer/compose\n---\n\n# gsy-compose-integration\n\n## 模块\n\n`gsyVideoPlayer-compose` 是 Compose 桥接层，两种模式并存：\n\n| 模式 | 入口 | 说明 |\n|---|---|---|\n| **Wrapper**（推荐上手） | [GSYVideoPlayerView](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/wrapper/GSYVideoPlayerView.kt) / [GSYAnyVideoPlayerView](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/wrapper/GSYAnyVideoPlayerView.kt) | `AndroidView` 直接承载 `StandardGSYVideoPlayer` / 任意 `NormalGSYVideoPlayer` 子类，保留全部内置能力 |\n| **Native**（纯 Compose 控件） | [rememberGSYPlayerController](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerSurface.kt) + [GSYPlayerSurface](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerSurface.kt) + [GSYDefaultControls](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYDefaultControls.kt) | 只承载画面，控制层完全用 Compose 自绘 |\n| **通用工具** | [LifecycleBridge](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/common/LifecycleBridge.kt) | `LifecycleEffect { event -> ... }` 用于订阅宿主 Lifecycle |\n\n## Wrapper API：`GSYVideoPlayerView`\n\n见 [GSYVideoPlayerView#L28-L36](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/wrapper/GSYVideoPlayerView.kt)：\n\n| 参数 | 类型 | 说明 |\n|---|---|---|\n| `modifier` | `Modifier` | 通常 `Modifier.fillMaxWidth().aspectRatio(16f/9)` |\n| `setUp` | `(StandardGSYVideoPlayer) -> Unit` | 等价 `GSYVideoOptionBuilder.build(player)` |\n| `setUpKey` | `Any?` = null | 非空时其变化会重新调用 `setUp`（典型：传 url） |\n| `autoReleaseOnDispose` | Boolean = true | 离屏自动 `player.release()` |\n| `autoPauseResume` | Boolean = true | 自动桥 Lifecycle：`ON_PAUSE→GSYVideoManager.onPause()`、`ON_RESUME→onResume()` |\n| `onPlayerCreated` | `(StandardGSYVideoPlayer) -> Unit` | 拿到 player 实例的回调 |\n\n## Wrapper API：`GSYAnyVideoPlayerView<T : NormalGSYVideoPlayer>`\n\n见 [GSYAnyVideoPlayerView#L26-L35](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/wrapper/GSYAnyVideoPlayerView.kt)。相比 `GSYVideoPlayerView` 多出：\n\n| 参数 | 类型 | 说明 |\n|---|---|---|\n| `factory` | `(Context) -> T` | 传自定义子类（如 `ListGSYVideoPlayer`、`SampleControlVideo`、`DanmakuVideoPlayer`） |\n\n## Native API：`rememberGSYPlayerController`\n\n见 [rememberGSYPlayerController#L26-L32](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerSurface.kt)：\n\n| 参数 | 默认 | 说明 |\n|---|---|---|\n| `url` | null | 非空时内部 `LaunchedEffect` 触发 `controller.setUp(...)` |\n| `cacheWithPlay` | false | 边播边缓 |\n| `title` | \"\" | 标题（透传给底层 player） |\n| `autoPlay` | false | attach 完成后自动 `startPlayLogic` |\n| `autoPauseResume` | true | 自动订阅 Lifecycle（同 wrapper） |\n\n返回 [GSYPlayerController](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerController.kt)，可以再调 `.play() / .pause() / .seekTo(ms) / .setSpeed(...) / .changeTextureViewShowType() / .setUp(url, cache, title, autoPlay) / .dispose()`；状态订阅走 [GSYPlayerState](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerState.kt)（`StateFlow<GSYPlayerState>`），事件走 [GSYPlayerEvent](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerEvent.kt)。\n\n## 最小可运行示例\n\n**Wrapper 模式**：\n\n```kotlin\nGSYVideoPlayerView(\n    modifier = Modifier.fillMaxWidth().aspectRatio(16f/9),\n    setUpKey = url,\n    setUp = { player ->\n        GSYVideoOptionBuilder()\n            .setUrl(url)\n            .setCacheWithPlay(true)\n            .setVideoTitle(\"demo\")\n            .setIsTouchWiget(true)\n            .setAutoFullWithSize(true)\n            .build(player)\n        player.startPlayLogic()\n    }\n)\n```\n\n**Native 模式**：\n\n```kotlin\nval controller = rememberGSYPlayerController(url = url, autoPlay = true)\nBox(Modifier.fillMaxWidth().aspectRatio(16f/9)) {\n    GSYPlayerSurface(controller = controller, modifier = Modifier.matchParentSize())\n    GSYDefaultControls(controller = controller)\n}\n```\n\n## 生命周期最佳实践\n\n```kotlin\nval lifecycle = LocalLifecycleOwner.current.lifecycle\nDisposableEffect(lifecycle) {\n    val obs = LifecycleEventObserver { _, e ->\n        when (e) {\n            Lifecycle.Event.ON_PAUSE  -> state.pause()\n            Lifecycle.Event.ON_RESUME -> state.resumeIfNeeded()\n            Lifecycle.Event.ON_DESTROY-> state.release()\n            else -> Unit\n        }\n    }\n    lifecycle.addObserver(obs)\n    onDispose { lifecycle.removeObserver(obs) }\n}\n```\n\n## Demo 对照\n\n| 场景 | Activity |\n|---|---|\n| 原生桥接示例 | [DetailNativeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/DetailNativeActivity.kt) / [BasicWrapperActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/BasicWrapperActivity.kt) / [FullFeatureNativeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/FullFeatureNativeActivity.kt) |\n| 广告 + 主片（列表内） | [AdInListComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/AdInListComposeActivity.kt) |\n| 列表 + 全屏 | [ListWithFullscreenActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/ListWithFullscreenActivity.kt) / [ListPlayNativeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/ListPlayNativeActivity.kt) / [AutoPlayListActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/AutoPlayListActivity.kt) |\n| 抖音式竖屏 | [VerticalShortVideoComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/VerticalShortVideoComposeActivity.kt) |\n| 滤镜 | [DetailFilterComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/DetailFilterComposeActivity.kt) |\n| 无缝切源 / Exo 切源 | [SwitchSeamlessComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/SwitchSeamlessComposeActivity.kt) / [ExoSwitchSourceComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/ExoSwitchSourceComposeActivity.kt) / [SwitchUrlActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/SwitchUrlActivity.kt) |\n| 字幕 | [SubtitleComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/SubtitleComposeActivity.kt) |\n| 弹幕 | [DanmakuComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/DanmakuComposeActivity.kt) |\n| 缓存 / 下载 | [CacheDownloadComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/CacheDownloadComposeActivity.kt) |\n| 硬解 / MediaCodec | [MediaCodecComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/MediaCodecComposeActivity.kt) |\n| 悬浮小窗 | [FloatingWindowComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/FloatingWindowComposeActivity.kt) |\n| 音频独立 | [AudioOnlyComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/AudioOnlyComposeActivity.kt) |\n| 多类型混排 | [MoreTypeComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/MoreTypeComposeActivity.kt) |\n| 多窗口并行 | [MultiWindowActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/MultiWindowActivity.kt) / [MultiWindowParallelComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/MultiWindowParallelComposeActivity.kt) |\n| 本地文件 | [LocalFileComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/LocalFileComposeActivity.kt) |\n| 自定义主题 | [CustomControlsThemeComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/CustomControlsThemeComposeActivity.kt) |\n| WebView 详情 | [WebDetailComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/WebDetailComposeActivity.kt) |\n\n样例 hosts 全在 [compose/host/](../../app/src/main/java/com/example/gsyvideoplayer/compose/host)。\n\n## v14.0.0 状态与接入\n\n外部坐标为 `io.github.carguo:gsyvideoplayer-compose:14.0.0`（GitHub Packages 使用 `com.shuyu`）。IJK 使用方还需选择 `ex_so` 或独立 ABI 原生模块；完整组合见 [DEPENDENCIES.md](../../doc/DEPENDENCIES.md)。\n\n`detachHost` 只解绑 View 侧回调/tick，`dispose` 统一释放保留的 host；`snapshot.bufferPercent` 合并内核轮询与回调值。`controller.changeTextureViewShowType()` 在主线程更新当前内嵌/全屏比例。Compose Demo 共 25 项，新增折叠屏 BOOK/TABLETOP。详见 [COMPOSE_USE.md](../../doc/COMPOSE_USE.md)。\n\n## 常见坑\n\n- Compose 频繁重组 → `AndroidView` 的 `factory` 只跑一次；不要把 `setUp` 放在 `factory` 里，放到 `update` block，并用 `remember(url)` 做去重。\n- `AndroidView` 内部布局要指定固定高度或 `aspectRatio`，否则 SurfaceView 会 0 高度导致黑屏。\n- 竖直短视频 + `Pager`：切页时用 `pagerState.currentPage` 触发 `state.play(url) / previousState.pause()`；释放放在 `DisposableEffect(pagerState)` 里。\n- 与 Compose 主题深浅色切换：`GSYVideoPlayerCompose` 是原生 View 层，配色需要单独 `setBottomProgressBarDrawable` 等 API。\n","frontmatter":{"name":"gsy-compose-integration","description":"在 Jetpack Compose 中通过 AndroidView 桥接 GSYVideoPlayer，覆盖列表、竖直短视频、生命周期管控。","when_to_use":"项目已经或正在迁移到 Compose；需要在可组合项里嵌入播放器；需要复用列表滑动/自动播放逻辑但 UI 用 Compose。","inputs":[{"name":"url","description":"视频 URL","required":true}],"outputs":["Composable 内嵌 GSYVideoPlayer，生命周期与 Composable 一致。"],"references":[{"file":"gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose"},{"file":"app/src/main/java/com/example/gsyvideoplayer/compose"}]},"isInternal":false,"tokens":2916,"sizeBytes":10222},{"name":"SKILL.md","path":"skills/12-gsy-custom-view/SKILL.md","rawUrl":"https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/12-gsy-custom-view/SKILL.md","title":"gsy-custom-view","category":"anthropic-skill","format":"markdown","content":"---\nname: gsy-custom-view\ndescription: 自定义播放器 UI（继承 StandardGSYVideoPlayer）与自定义控制层：换 seekbar、加弹幕层、加清晰度切换、加投屏按钮等。\nwhen_to_use: SDK 默认 UI 不符合需求；需要在标准控制层上加自定义控件；需要多状态视图（loading/error/paywall）。\ninputs:\n  - name: layout_id\n    description: 自定义 XML 布局资源 id\n    required: true\noutputs:\n  - 一个继承 GSYBaseVideoPlayer 子类的自定义播放器组件。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/NormalGSYVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/SampleCoverVideo.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/SampleControlVideo.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/LandLayoutVideo.java\n---\n\n# gsy-custom-view\n\n## 继承层次\n\n```\nGSYTextureRenderView（渲染管理）\n  └── GSYVideoView（状态机 / 生命周期）\n        └── GSYVideoControlView（默认控制层：进度条/按钮）\n              └── GSYVideoPlayer（触摸手势）\n                    └── GSYBaseVideoPlayer（全屏/小屏切换）\n                          ├── StandardGSYVideoPlayer（默认 UI）\n                          └── NormalGSYVideoPlayer（无返回/无标题的简版）\n```\n\n自定义组件通常继承 [StandardGSYVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java) 或 [NormalGSYVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/NormalGSYVideoPlayer.java)。\n\n## 必须实现的三件事\n\n### 1) 自定义布局 & id\n\n覆写：\n```java\n@Override public int getLayoutId() { return R.layout.my_video_player; }\n```\n布局里必须包含（id 不能改）：\n- `@id/surface_container` —— 放渲染 View（FrameLayout）\n- 需要用到的默认控件（如 `@id/start`、`@id/progress`、`@id/current`、`@id/total`、`@id/bottom_progressbar`、`@id/thumb`、`@id/small_close`、`@id/back`、`@id/fullscreen`、`@id/loading`）—— 缺哪个哪个功能失效，编译不报错。\n\n### 2) 覆写全屏 & 小屏组件类\n\n关键：`getConstructor(Context, Boolean)` 是 SDK 内部反射拷贝出的全屏实例。**必须提供两个构造器**：\n\n```java\npublic MyVideoPlayer(Context c) { super(c); }\npublic MyVideoPlayer(Context c, Boolean fullFlag) { super(c, fullFlag); }\npublic MyVideoPlayer(Context c, AttributeSet a) { super(c, a); }\n```\n\nR8 侧已在 [proguard-rules.pro](../../app/proguard-rules.pro) 保留：\n```\n-keep class * extends com.shuyu.gsyvideoplayer.video.base.GSYBaseVideoPlayer {\n    public <init>(android.content.Context);\n    public <init>(android.content.Context, java.lang.Boolean);\n}\n```\n\n### 3) 覆写 `showSmallVideo() / hideSmallVideo()`（如需要小窗）以及 `startWindowFullscreen()`（如要全屏样式差异）\n\n## 常用重写点\n\n| 方法 | 作用 |\n|---|---|\n| `changeUiToNormal()` / `changeUiToPreparingShow()` / `changeUiToPlayingShow()` / `changeUiToPauseShow()` / `changeUiToCompleteShow()` / `changeUiToError()` / `changeUiToPlayingBufferingShow()` | 不同状态下的 UI 切换钩子 |\n| `onClickUiToggle()` | 单击 UI 显隐 |\n| `setStateAndUi(int state)` | 由 SDK 内部调，可 super 后加自己的逻辑 |\n| `updateStartImage()` | 播放/暂停按钮 icon 切换 |\n| `resolveTypeUI()` | 首帧比例决定后的自定义处理 |\n| `dismissBrightnessDialog / dismissVolumeDialog / dismissProgressDialog` | 手势弹窗 UI |\n| `hideAllWidget / setViewShowState` | 显隐所有控件 |\n\n## 手势自定义\n\n`GSYVideoControlView` 里通过如下开关切换：\n- `setIsTouchWiget(boolean)` —— 手势总开关（默认 true）\n- `setIsTouchWigetFull(boolean)` —— 只在全屏开\n- `setNeedLockFull(boolean)` —— 全屏锁按钮\n- `setSeekRatio(float)` —— 手势 seek 灵敏度\n- `setThreshold(int)` —— 触发手势阈值\n\n手势弹窗替换：覆写 `showBrightnessDialog / showVolumeDialog / showProgressDialog` 用自己的 Dialog。\n\n## 标准封面 / 清晰度 / 弹幕\n\n- 封面：Demo 组件 [SampleCoverVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/SampleCoverVideo.java) 演示用 Glide 加载封面。\n- 清晰度切换：Demo 组件 [SwitchVideo](../../app/src/main/java/com/example/gsyvideoplayer/switchplay/SwitchVideo.java) + [DetailListPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailListPlayer.java)（多列源，`setUp(List<GSYVideoModel>, ...)`）。\n- 弹幕层：Demo 组件 [DanmakuVideoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/DanmakuVideoPlayer.java) —— 在布局叠一层 `DanmakuView`，在 `onProgress` 里 `dispatch(long time)`。\n- 控制条自定义：Demo 组件 [SampleControlVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/SampleControlVideo.java)（毛玻璃底栏 + 播放按钮）。\n- 横屏专用布局：Demo 组件 [LandLayoutVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/LandLayoutVideo.java)（`getFullId()` 返回不同 layout）。\n\n## 多状态视图（loading / error / paywall）\n\n在 `changeUiToError()` 与 `changeUiToPreparingShow()` 里 `setVisibility` 自定义 View；也可以在 `onVideoAllCallBack.onPlayError` 中显式 `showErrorLayer()`。\n\n## 常见坑\n\n- 忘写 `Boolean` 构造器 → 全屏切换 `IllegalArgumentException` 或直接 crash（反射失败）。\n- 自定义 `layout` 缺 id → 默认功能沉默失效，问题不易定位。建议基于 `layout_standard_video_player.xml` 复制修改。\n- 全屏组件和非全屏组件类型不同 → 用 `getCurrentPlayer()` 获取\"当前活跃 player\"，别缓存 `this`。\n- 释放时机：`onDestroy` 里既要 `super.onBackPressed` 判断，也要 `GSYVideoManager.releaseAllVideos()`。\n","frontmatter":{"name":"gsy-custom-view","description":"自定义播放器 UI（继承 StandardGSYVideoPlayer）与自定义控制层：换 seekbar、加弹幕层、加清晰度切换、加投屏按钮等。","when_to_use":"SDK 默认 UI 不符合需求；需要在标准控制层上加自定义控件；需要多状态视图（loading/error/paywall）。","inputs":[{"name":"layout_id","description":"自定义 XML 布局资源 id","required":true}],"outputs":["一个继承 GSYBaseVideoPlayer 子类的自定义播放器组件。"],"references":[{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/NormalGSYVideoPlayer.java"},{"file":"gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/video/SampleCoverVideo.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/video/SampleControlVideo.java"},{"file":"app/src/main/java/com/example/gsyvideoplayer/video/LandLayoutVideo.java"}]},"isInternal":false,"tokens":1865,"sizeBytes":6101},{"name":"SKILL.md","path":"skills/13-gsy-proguard-r8/SKILL.md","rawUrl":"https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/13-gsy-proguard-r8/SKILL.md","title":"13-gsy-proguard-r8","category":"anthropic-skill","format":"markdown","content":"# 13-gsy-proguard-r8\n\nPath: `skills/13-gsy-proguard-r8/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/13-gsy-proguard-r8/SKILL.md)","tokens":300,"sizeBytes":191},{"name":"README.md","path":"skills/README.md","rawUrl":"https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/README.md","title":"Skills Skill","category":"anthropic-skill","format":"markdown","content":"# Skills Skill\n\nPath: `skills/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/README.md)","tokens":300,"sizeBytes":149}],"systemPromptSnippet":"<yakaai_skills repo=\"CarGuo/GSYVideoPlayer\">\n<!-- File: doc/ARCHITECTURE.md (Tokens: ~300 | Category: project-spec) -->\n# doc - System Architecture & Module Boundaries\n\nPath: `doc/ARCHITECTURE.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/doc/ARCHITECTURE.md)\n\n<!-- File: skills/01-gsy-quickstart/SKILL.md (Tokens: ~1802 | Category: anthropic-skill) -->\n---\nname: gsy-quickstart\ndescription: 从零把 GSYVideoPlayer 塞进一个 Activity 或 Fragment，跑通「setUp → startPlayLogic → release」最小闭环。\nwhen_to_use: 用户第一次在项目里集成 GSYVideoPlayer；只要能播一个 mp4 就行；或需要确认基础生命周期是否接对。\ninputs:\n  - name: play_url\n    description: 视频地址（http/https/file/asset/rtmp/rtsp）\n    required: true\n  - name: container\n    description: 承载 View 的容器（一般是 FrameLayout）\n    required: true\noutputs:\n  - StandardGSYVideoPlayer 实例已 setUp 并可控（play/pause/release）\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/PlayActivity.java\n---\n\n# gsy-quickstart\n\n## 依赖\n\n- 推荐整包：`io.github.carguo:gsyvideoplayer:14.0.0`（已含 Java、Exo2、三 ABI ex_so）。\n- 分模块主库：`io.github.carguo:gsyvideoplayer-java:14.0.0`。\n- 分模块 IJK so：选 `io.github.carguo:gsyvideoplayer-ex_so:14.0.0`，或所需独立 artifact `gsyvideoplayer-arm64` / `gsyvideoplayer-armv7a` / `gsyvideoplayer-x64`，不重复引入同 ABI 两套库。\n- 默认 ABI 为 `arm64-v8a` / `armeabi-v7a` / `x86_64`（FFmpeg n5.1.10 / OpenSSL 3.5.9）；旧 armv5/x86 未升级，仅旧架构项目显式使用。\n- 权限：`INTERNET`、`WAKE_LOCK`、`ACCESS_NETWORK_STATE`（AndroidManifest）\n\n明细见 [doc/DEPENDENCIES.md](../../doc/DEPENDENCIES.md)。\n\n## 核心 API\n\n| 类 / 方法 | 位置 | 作用 |\n|---|---|---|\n| `StandardGSYVideoPlayer(Context)` | [StandardGSYVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java) | 主用 UI 组件，默认布局在 `R.layout.video_layout_standard` |\n| `setUp(url, cacheWithPlay, title)` | [GSYVideoView#L438-L451](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 最短形式的地址绑定 |\n| `setUp(url, cacheWithPlay, cachePath, title)` | [GSYVideoView#L477-L488](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 支持自定义缓存目录 |\n| `setUp(url, cacheWithPlay, cachePath, headers, title)` | [GSYVideoView#L453-L475](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 附带 HTTP header |\n| `startPlayLogic()` | `StandardGSYVideoPlayer`（继承自 `GSYVideoControlView`） | 触发 prepare→start 全流程 |\n| `getCurrentPlayer()` | `GSYBaseVideoPlayer` | 获取\"当前活跃 player\"（可能是全屏克隆体） |\n| `onVideoPause()` / `onVideoResume()` / `onVideoResume(boolean seek)` | [GSYVideoView#L518-L577](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 对应 Activity `onPause / onResume`；直播场景传 `seek=false` |\n| `release()` | [GSYVideoView#L818](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java) | 释放当前 Player 与 Surface |\n| `GSYVideoManager.releaseAllVideos()` | [GSYVideoManager#L99-L104](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java) | 全局收尾（切页面时兜底） |\n| `GSYVideoManager.onPause() / onResume()` | [GSYVideoManager#L110-L135](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java) | 全局暂停/恢复所有 GSY player |\n\n## 参数表：`setUp(...)`\n\n| 参数 | 类型 | 说明 |\n|---|---|---|\n| `url` | `String` | 支持 http/https/file/asset/rtmp/rtsp/自定义 scheme；`ProxyCacheManager` 会自动拦截可缓存的普通 http |\n| `cacheWithPlay` | `boolean` | true → 边播边缓；对直播 / m3u8 / DRM 建议 false |\n| `cachePath` | `File?` | 自定义缓存目录，默认 `context.cacheDir/videoCache` |\n| `mapHeadData` | `Map<String,String>?` | HTTP 请求头（Referer、Cookie、Auth 等） |\n| `title` | `String` | 顶部标题栏文案，会显示在 `R.id.title` |\n\n返回值：`boolean`，false 表示 URL 为空或与当前 tag 相同（防重复 setup）。\n\n## 最小调用序（Activity 生命周期）\n\n1. `onCreate` → `player.setUp(...)` → 可选 `player.setThumb...`、`setPlayTag()` → `player.startPlayLogic()`\n2. `onPause` → `player.getCurrentPlayer().onVideoPause()`\n3. `onResume` → `player.getCurrentPlayer().onVideoResume()`（直播传 `false`）\n4. `onBackPressed` → 若在全屏：`GSYVideoManager.backFromWindowFull(this)` 返回 true 就吞掉事件\n5. `onDestroy` → `player.getCurrentPlayer().release()` + `GSYVideoManager.releaseAllVideos()`\n\n## Demo 对照\n\n- 最短一屏：[PlayActivity](../../app/src/main/java/com/example/gsyvideoplayer/PlayActivity.java)\n- Fragment：[VideoFragment](../../app/src/main/java/com/example/gsyvideoplayer/fragment/VideoFragment.java) + [FragmentVideoActivity](../../app/src/main/java/com/example/gsyvideoplayer/FragmentVideoActivity.java)\n- 空控件模式（自绘 UI）：[PlayEmptyControlActivity](../../app/src/main/java/com/example/gsyvideoplayer/PlayEmptyControlActivity.java) + [EmptyControlVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/EmptyControlVideo.java)\n\n## 常见坑\n\n- 忘记 `release()` → dex 泄漏 + surface 崩溃：一定在 `onDestroy` 兜底。\n- 不覆盖 `getCurrentPlayer()` 直接调 `player.onVideoPause()`：全屏切回后指令发到\"影子\"实例，导致状态错乱。\n- `setUp` 传空 title 走 `null`，会隐藏顶部栏（`View.GONE`），不会崩。\n\n<!-- File: skills/02-gsy-option-builder/SKILL.md (Tokens: ~3532 | Category: anthropic-skill) -->\n---\nname: gsy-option-builder\ndescription: 用 GSYVideoOptionBuilder 一次性把播放器所有可调项配好；等价于对 StandardGSYVideoPlayer 挨个 setter 但更紧凑。\nwhen_to_use: 需要开启循环 / 变速 / 边播边缓 / 触摸手势 / 自动旋转 / 封面 / 底部进度条样式 / 全屏动画 等多个开关时；或在多处播放页复用同一份配置。\ninputs:\n  - name: player\n    description: StandardGSYVideoPlayer / GSYBaseVideoPlayer 实例\n    required: true\noutputs:\n  - 已按 builder 状态 build 完成的 player，可直接 startPlayLogic\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseActivityDetail.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java\n---\n\n# gsy-option-builder\n\n## API 总览\n\n入口：[GSYVideoOptionBuilder](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java)。\n\n链式调用，最后 `build(player)` 把状态灌到具体 `GSYBaseVideoPlayer`。\n\n```java\nnew GSYVideoOptionBuilder()\n    .setUrl(url)\n    .setCacheWithPlay(true)\n    .setVideoTitle(title)\n    .setIsTouchWiget(true)\n    .setRotateViewAuto(false)\n    .setLockLand(false)\n    .setLooping(false)\n    .setSpeed(1.0f)\n    .setSeekOnStart(0)\n    .setVideoAllCallBack(callback)\n    .build(player);\n```\n\n## 完整参数表（按 [源文件行号](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) 对齐）\n\n| Setter | 类型 | 默认 | 用途 |\n|---|---|---|---|\n| `setUrl(String)` [#L376](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | String | null | 播放地址 |\n| `setVideoTitle(String)` [#L386](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | String | null | 顶部标题 |\n| `setMapHeadData(Map)` [#L437](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | `Map<String,String>` | null | HTTP header |\n| `setOverrideExtension(String)` [#L561](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | String | null | 强制 Exo 用哪种解封装器（`m3u8`/`mpd`/`ism`） |\n| `setCacheWithPlay(boolean)` [#L396](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 边播边缓 |\n| `setCachePath(File)` [#L427](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | File | null | 缓存目录 |\n| `setLooping(boolean)` [#L202](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 循环 |\n| `setSpeed(float)` [#L237](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | float | 1.0 | 播放速率，0.25~3.0 常用 |\n| `setSoundTouch(boolean)` [#L246](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 变速不变调 |\n| `setSeekOnStart(long)` [#L366](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | long ms | -1 | 起播 seek 位置 |\n| `setSeekRatio(float)` [#L324](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | float | 1.0 | 触摸横滑与进度的比例 |\n| `setStartAfterPrepared(boolean)` [#L406](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 首帧就绪后自动播；`false` 用于\"手动 start\" |\n| `setReleaseWhenLossAudio(boolean)` [#L417](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 丢失音频焦点是否 release |\n| `setIsTouchWiget(boolean)` [#L263](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 非全屏是否响应触摸手势 |\n| `setIsTouchWigetFull(boolean)` [#L272](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 全屏是否响应触摸手势 |\n| `setDismissControlTime(int ms)` [#L543](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | int | 2500 | 控制条自动隐藏时间 |\n| `setNeedShowWifiTip(boolean)` [#L281](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 移动网络提示 |\n| `setRotateViewAuto(boolean)` [#L221](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 自动旋转全屏；关闭后配合 `OrientationUtils` |\n| `setRotateWithSystem(boolean)` [#L337](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 是否跟随系统旋转设置 |\n| `setLockLand(boolean)` [#L229](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 全屏是否锁横屏 |\n| `setAutoFullWithSize(boolean)` [#L184](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 竖屏视频自动竖屏全屏 |\n| `setOnlyRotateLand(boolean)` [#L567](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 只允许横屏方向变化 |\n| `setShowFullAnimation(boolean)` [#L194](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 全屏过渡动画 |\n| `setHideKey(boolean)` [#L254](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 全屏隐藏虚拟按键 |\n| `setFullHideActionBar(boolean)` [#L587](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 全屏隐藏 ActionBar |\n| `setFullHideStatusBar(boolean)` [#L592](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 全屏隐藏 StatusBar |\n| `setNeedLockFull(boolean)` [#L525](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 全屏显示屏幕锁按钮 |\n| `setLockClickListener(LockClickListener)` [#L533](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 接口 | null | 屏幕锁点击回调 |\n| `setEnlargeImageRes(int)` [#L291](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | drawableRes | -1 | 全屏按钮图标 |\n| `setShrinkImageRes(int)` [#L301](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | drawableRes | -1 | 退出全屏按钮图标 |\n| `setThumbImageView(View)` [#L464](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | View | null | 封面容器（`ImageView` 或子布局） |\n| `setThumbPlay(boolean)` [#L516](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 点击封面即播 |\n| `setShowPauseCover(boolean)` [#L314](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 暂停时显示最后一帧 |\n| `setBottomProgressBarDrawable(Drawable)` [#L482](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | Drawable | null | 底部长条进度条颜色 |\n| `setBottomShowProgressBarDrawable(Drawable, Drawable)` [#L473](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 2×Drawable | null | 交互进度条 & thumb |\n| `setDialogProgressBar(Drawable)` [#L499](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | Drawable | null | 触摸拖动进度条样式 |\n| `setDialogVolumeProgressBar(Drawable)` [#L490](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | Drawable | null | 触摸音量进度条 |\n| `setDialogProgressColor(int, int)` [#L507](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | int color | -11 | 触摸进度条高亮/普通颜色 |\n| `setShowDragProgressTextOnSeekBar(boolean)` [#L572](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 拖动时 seekbar 上气泡 |\n| `setEffectFilter(GSYVideoGLView.ShaderInterface)` [#L551](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | Shader | `NoEffect` | GL 滤镜（渲染必须 `GLSURFACE`） |\n| `setPlayTag(String)` [#L347](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | String | \"\" | 列表播放去重用 |\n| `setPlayPosition(int)` [#L356](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | int | -22 | 列表位置索引 |\n| `setSetUpLazy(boolean)` [#L582](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | false | 延迟 setUp，`build` 后手动 `startPlayLogic` |\n| `setVideoAllCallBack(VideoAllCallBack)` [#L213](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 接口 | null | 全事件回调（prepared/complete/error…） |\n| `setGSYVideoProgressListener(GSYVideoProgressListener)` [#L455](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 接口 | null | 高频进度回调 |\n| `setGSYStateUiListener(GSYStateUiListener)` [#L600](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | 接口 | null | UI 状态变化回调 |\n| `setSurfaceErrorPlay(boolean)` [#L446](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | Surface 异常时点击继续 |\n| `setNeedOrientationUtils(boolean)` [#L611](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java) | boolean | true | 全屏是否内部构造 `OrientationUtils` |\n\n## 关键调用序\n\n1. 构造 `GSYVideoOptionBuilder` → 链式 setter；\n2. `builder.build(player)` 会自动 setUp（除非 `setSetUpLazy(true)`）；\n3. 需要在页面里同时接收回调 → 让 Activity `implements VideoAllCallBack`，直接 `setVideoAllCallBack(this)`。\n\n## Demo 对照\n\n- 详情页 + Builder 全套：[DetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java)\n- 结合 `GSYBaseActivityDetail` 的最短接入：[DetailControlActivity](../../app/src/main/java/com/example/gsyvideoplayer/DetailControlActivity.java) 与 [GSYBaseActivityDetail#L56-L61](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseActivityDetail.java)\n\n## 常见坑\n\n- `build(player)` 内部会调 `setUp`；若 `setSetUpLazy(true)` 需要自己再调 `startPlayLogic()`。\n- Effect 滤镜需要先 `GSYVideoType.setRenderType(GLSURFACE)`，否则 `setEffectFilter` 无效。\n- `setPlayTag` 与 `setPlayPosition` 是列表页避免\"上下滑滚 setUp 命中同 URL 不重播\"的关键。\n\n<!-- File: skills/03-gsy-fullscreen-orientation/SKILL.md (Tokens: ~1893 | Category: anthropic-skill) -->\n---\nname: gsy-fullscreen-orientation\ndescription: 处理进入/退出全屏、屏幕旋转、竖屏视频自适应、系统旋转设置联动、反向横屏。\nwhen_to_use: 出现\"全屏没进/没退\"、\"横屏方向反了\"、\"旋转跟随系统坏了\"、\"竖屏视频被横过来\"、\"抖动无限循环\"这类问题；或首次在详情页接入全屏。\ninputs:\n  - name: activity\n    description: 承载 player 的 Activity\n    required: true\n  - name: player\n    description: GSYBaseVideoPlayer 或其子类\n    required: true\noutputs:\n  - OrientationUtils 已绑定；点击\"全屏\"按钮可以正确进入 window fullscreen 副本；返回键与旋转都能正确复位。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationUtils.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationOption.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseActivityDetail.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java\n---\n\n# gsy-fullscreen-orientation\n\n## 概念\n\n- **视觉全屏**：`player.startWindowFullscreen(context, hideActionBar, hideStatusBar)`，会用反射克隆一个同类 player 塞进 `android.R.id.content` 顶部，返回原实例。\n- **屏幕方向**：`OrientationUtils` 监听 `OrientationEventListener` + `Settings.System.ACCELEROMETER_ROTATION`，负责旋转 Activity 方向。\n- **两者可解耦**：只想旋转不切副本 → `setNeedOrientationUtils(false)` + 自建 `OrientationUtils`；只想切副本不旋转 → 不构造 `OrientationUtils`，`player` 保持默认 `setRotateViewAuto(false)`。\n\n## 核心 API\n\n| 类 / 方法 | 位置 | 说明 |\n|---|---|---|\n| `OrientationUtils(Activity, GSYBaseVideoPlayer)` | [OrientationUtils#L50-L52](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationUtils.java) | 主构造 |\n| `OrientationUtils(Activity, player, OrientationOption)` | [OrientationUtils#L54-L64](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/OrientationUtils.java) | 支持自定义旋转角度阈值 |\n| `setEnable(boolean)` | `OrientationUtils` | 使能/失能自动旋转 |\n| `setRotateWithSystem(boolean)` | `OrientationUtils` | 是否跟随系统\"旋转开关\" |\n| `setIsOnlyRotateLand(boolean)` | `OrientationUtils` | true 时仅在横屏方向变化时响应 |\n| `resolveByClick()` | `OrientationUtils` | 用户点全屏按钮时手动触发一次旋转 |\n| `backToProtVideo()` | `OrientationUtils` | 返回键触发时把方向复位竖屏 |\n| `setIsPause(boolean)` | `OrientationUtils` | 生命周期 pause/resume 桥接 |\n| `releaseListener()` | `OrientationUtils` | `onDestroy` 释放监听 |\n| `startWindowFullscreen(Context, boolean, boolean)` | [GSYBaseVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java) | 进入 window 全屏，返回克隆体 |\n| `GSYVideoManager.backFromWindowFull(Context)` | [GSYVideoManager#L82-L94](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java) | 返回键统一处理 |\n| `GSYVideoManager.isFullState(Activity)` | [GSYVideoManager#L143-L151](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoManager.java) | 当前是否全屏 |\n\n## `OrientationOption` 参数\n\n| 字段 | 默认 | 作用 |\n|---|---|---|\n| `normalPortraitAngleStart / End` | 5 / 355 | 判定竖屏区间 |\n| `normalLandAngleStart / End` | 85 / 95 | 判定正向横屏区间 |\n| `reverseLandAngleStart / End` | 265 / 275 | 判定反向横屏区间 |\n\n区间越大越\"灵敏\"，太大会抖动。\n\n## 关键调用序（详情页最佳实践）\n\n1. `onCreate`：`orientationUtils = new OrientationUtils(this, player)`; `orientationUtils.setEnable(false)`（进页面不自动旋转）\n2. 全屏按钮点击：`orientationUtils.resolveByClick()` → `player.startWindowFullscreen(this, true, true)`\n3. `onBackPressed`：`orientationUtils.backToProtVideo()` → `GSYVideoManager.backFromWindowFull(this)`\n4. `onPause`：`orientationUtils.setIsPause(true)`\n5. `onResume`：`orientationUtils.setIsPause(false)`\n6. `onDestroy`：`orientationUtils.releaseListener()`\n\n`GSYBaseActivityDetail` 已把 1/2/3/4/5/6 全套代码写好，直接继承最省事。\n\n## Demo 对照\n\n- 最标准的全屏 + 旋转：[DetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java)\n- 只旋转不切副本：[LandLayoutVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/LandLayoutVideo.java) + [DetailNormalActivityPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailNormalActivityPlayer.java)\n- 反射克隆入口：[GSYBaseVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java)（`startWindowFullscreen` → `getConstructor(Context.class, Boolean.class)`）\n\n## v14.0.0 折叠屏示例\n\nXML [FoldDetailActivity](../../app/src/main/java/com/example/gsyvideoplayer/FoldDetailActivity.java) 与 Compose [FoldComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/FoldComposeActivity.kt) 按 FoldingFeature 实现 BOOK/TABLETOP 分屏、铰链分隔与全屏。XML 全屏克隆布局通过 `LandLayoutVideo` 传递姿态；普通全屏 API 不会自动给任意业务页面添加折叠布局。用法及测试注入边界见 [RECENT_FEATURES.md](../../doc/RECENT_FEATURES.md)。\n\n## 常见坑\n\n- **黑屏一瞬**：`startWindowFullscreen` 用反射构造新 player，若你的子类只暴露 `(Context)` 构造且未加 `(Context, Boolean)`，R8 混淆后必炸。ProGuard 里必须保留：`-keep class * extends GSYBaseVideoPlayer { public <init>(android.content.Context); public <init>(android.content.Context, java.lang.Boolean); }`\n- **旋转停不下来**：忘了 `orientationUtils.setIsPause(true)` 或 `releaseListener()`。\n- **系统关了自动旋转但还想手动横屏**：`setRotateWithSystem(false)`。\n- **竖屏视频被强制横屏**：`setAutoFullWithSize(true)` 让 SDK 自适应。\n\n<!-- File: skills/04-gsy-kernel-switch/SKILL.md (Tokens: ~2128 | Category: anthropic-skill) -->\n---\nname: gsy-kernel-switch\ndescription: 在 IJK / System / ExoPlayer2(Media3) / Aliyun 四个媒体内核之间自由切换，以及自定义 Manager 的方法。\nwhen_to_use: 需要支持 DASH/SmoothStreaming、需要 Media3 track selection、想减 so 体积只留系统解码、要用 Aliyun 内核、或者要多实例并行独立 Manager。\ninputs:\n  - name: kernel\n    description: ijk / system / exo / ali / custom\n    required: true\noutputs:\n  - PlayerFactory 与（可选）CacheFactory 已切换到目标 Manager\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/PlayerFactory.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/IjkPlayerManager.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/SystemPlayerManager.java\n  - file: gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/IPlayerManager.java\n  - file: gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/BasePlayerManager.java\n  - file: gsyVideoPlayer-exo_player2\n  - file: gsyVideoPlayer-aliplay/src/main/java/com/shuyu/aliplay/AliPlayerManager.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/exo/GSYExoPlayerManager.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/manager/CustomManager.java\n---\n\n# gsy-kernel-switch\n\n## 内核矩阵\n\n| Kernel | Class | 来源模块 | 特点 |\n|---|---|---|---|\n| **IJK**（默认） | [IjkPlayerManager](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/IjkPlayerManager.java) | `gsyVideoPlayer-java` + 升级后的独立 ABI / `ex_so` | FFmpeg n5.1.10 的固定配置；软/硬解能力按格式与设备确认 |\n| **System** | [SystemPlayerManager](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/SystemPlayerManager.java) | `gsyVideoPlayer-java` | 无 so；靠 `android.media.MediaPlayer`；直播/复杂容器兼容差 |\n| **Exo/Media3** | `GSYExoPlayerManager`（Demo 内）／`GSYExo2MediaPlayer` | `gsyVideoPlayer-exo_player2` | DASH / HLS / SmoothStreaming / TrackSelection |\n| **Aliyun** | [AliPlayerManager](../../gsyVideoPlayer-aliplay/src/main/java/com/shuyu/aliplay/AliPlayerManager.java) | `gsyVideoPlayer-aliplay` | 阿里云 vid/authInfo；HLS 加密 |\n\n## 切换 API\n\n**全局切换**：`PlayerFactory.setPlayManager(Class<? extends IPlayerManager>)`（内部 `getPlayManager` 用 `sPlayerManager.newInstance()` 反射构造，故被切入的类**必须**有 `public` 无参构造）。见 [PlayerFactory#L15-L27](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/player/PlayerFactory.java) 与本仓库 R8 保留规则 [proguard-rules.pro](../../app/proguard-rules.pro)。\n\n```java\n// Exo\nPlayerFactory.setPlayManager(Exo2PlayerManager.class);\nCacheFactory.setCacheManager(ExoPlayerCacheManager.class);\n\n// System\nPlayerFactory.setPlayManager(SystemPlayerManager.class);\n\n// Aliyun\nPlayerFactory.setPlayManager(AliPlayerManager.class);\n```\n\n必须在 `player.setUp(...)` **之前**调用；否则本次实例仍用旧内核。\n\n## `IPlayerManager` 抽象\n\n统一接口见 [IPlayerManager](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/IPlayerManager.java)。核心方法（子类必须实现）：\n\n| 方法 | 说明 |\n|---|---|\n| `initVideoPlayer(Context, Message, List<VideoOptionModel>, ICacheManager)` | 初始化内核；`Message.obj` 是 `GSYModel` |\n| `showDisplay(Message)` | 绑定 Surface / SurfaceHolder |\n| `setSpeed(float, boolean soundTouch)` | 速率 + 变速不变调 |\n| `setNeedMute(boolean)` | 静音 |\n| `setVolume(float, float)` | 双通道音量 |\n| `getMediaPlayer()` | 底层 player 实例 |\n| `isSurfaceSupportLockCanvas()` | 是否支持 `lockCanvas`（TextureView 补帧） |\n| `start / pause / stop / seekTo / release / getCurrentPosition / getDuration / getVideoSarNum / getVideoSarDen / getVideoWidth / getVideoHeight` | 基础控制 |\n\n推荐继承 [BasePlayerManager](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/BasePlayerManager.java) 以少写模板。\n\n## `IPlayerInitSuccessListener` hook\n\n[IPlayerInitSuccessListener](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/IPlayerInitSuccessListener.java) 允许在 `initVideoPlayer` 完成后对底层 Player 拿手（比如设置 Exo 的 TrackSelector）：\n\n```java\nIjkPlayerManager.setIjkLibLoader(...);\n// 或\nGSYExoVideoManager.setPlayerInitSuccessListener(new IPlayerInitSuccessListener() {\n    @Override public void onPlayerInitSuccess(IMediaPlayer mp, GSYModel model) { ... }\n});\n```\n\n## `CacheFactory` 同步切换\n\n不同内核有各自缓存方案；切内核记得配套：\n\n| Player Manager | 对应 CacheManager |\n|---|---|\n| `IjkPlayerManager` / `SystemPlayerManager` | [ProxyCacheManager](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheManager.java)（默认） |\n| Exo/Media3 | `ExoPlayerCacheManager`（`gsyvideoplayer-exo_player2` 内） |\n| Aliyun | 由 Aliyun SDK 自身管理，一般 `CacheFactory` 保持 `ProxyCacheManager` 或自定义空实现 |\n\n## 多实例并行\n\n用 [CustomManager](../../app/src/main/java/com/example/gsyvideoplayer/video/manager/CustomManager.java) 模式：拷贝 `GSYVideoManager` 单例套壳，在自定义 `GSYVideoView` 里覆盖 `getGSYVideoManager()`；不同页面互不影响，参考 [MultiSampleVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/MultiSampleVideo.java) + [ListMultiVideoActivity](../../app/src/main/java/com/example/gsyvideoplayer/ListMultiVideoActivity.java)。\n\n## Demo 对照\n\n- Exo 切源 / 无缝切换：[DetailExoListPlayer](../../app/src/main/java/com/example/gsyvideoplayer/exo/DetailExoListPlayer.java)、[ExoAdaptiveTrackActivity](../../app/src/main/java/com/example/gsyvideoplayer/exo/ExoAdaptiveTrackActivity.java)\n- MediaCodec 智能回退：[SmartMediaCodecFallbackActivity](../../app/src/main/java/com/example/gsyvideoplayer/SmartMediaCodecFallbackActivity.java) + [doc/SMART_MEDIACODEC_FALLBACK_RESEARCH.md](../../doc/SMART_MEDIACODEC_FALLBACK_RESEARCH.md)\n\n## v14.0.0 原生选择\n\nIJK 当前三 ABI 成套库为 FFmpeg n5.1.10 / OpenSSL 3.5.9。`GSYIjkMediaCodecSelector` 先保留原 IJK 选择，再在 API 29+ 选择平台报告的普通 Codec2 硬件解码器，排除纯软件及安全/隧道必需解码器。这是初始化阶段的选择补充，与确认运行中硬解失败后重建软解的策略分开；见 [发布说明](../../doc/UPDATE_VERSION.md#v1400-2026-10-10)。\n\n## 常见坑\n\n- `PlayerFactory.setPlayManager` 传入的类**必须**有 `public` 无参构造，且被 ProGuard 保留。\n- Exo 切源建议用 `Exo2PlayerManager` + `GSYExo2MediaPlayer`；直接换 Manager 但用 Ijk 的 URL 会走缓存代理导致 DASH 解析失败——记得 `setOverrideExtension(\"mpd\")` 或关闭缓存。\n- 系统内核 `SystemPlayerManager` 不支持 `soundTouch`、多数直播格式；生产环境仅作 fallback。\n\n<!-- File: skills/05-gsy-list-playback/SKILL.md (Tokens: ~1602 | Category: anthropic-skill) -->\n---\nname: gsy-list-playback\ndescription: RecyclerView / ViewPager2 / ViewPager 列表滑动自动播放，滚动切换、复用绑定、进入详情页时\"漂移\"到全屏。\nwhen_to_use: 短视频信息流、朋友圈式内嵌播放、抖音/快手类 ViewPager2 竖播、列表进入详情页的无缝续播。\ninputs:\n  - name: adapter\n    description: RecyclerView.Adapter / FragmentStateAdapter\n    required: true\noutputs:\n  - 列表条目具备\"进入可视区自动播放，离开释放\"的能力，且切换详情页可续播。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/ListVideoUtil.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoHelper.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/utils/ScrollCalculatorHelper.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/AutoPlayRecyclerViewActivity.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/ViewPager2Activity.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/RecyclerViewActivity.java\n---\n\n# gsy-list-playback\n\n## 三条实现路线\n\n| 路线 | 特点 | 适用 |\n|---|---|---|\n| **A. 每 item 内嵌 `StandardGSYVideoPlayer`** | 每条 item 有独立 player 实例，滑到即播 | 大多数场景（含无限缩略页面） |\n| **B. `ListVideoUtil` 单例复用** | 全列表共享一个 player 实例，动态 `attachToParent` | 内存敏感、条目多；不能同屏两个视频 |\n| **C. `GSYVideoHelper` 悬浮小窗** | 用于详情页返回列表时\"漂浮\"续播 | 抖音式详情返回 |\n\n## 路线 A：每 item 一个 player\n\n关键点：**在 `onBindViewHolder` 里给每个 player `setPlayPosition(position)` + `setPlayTag(String)`**，防止滚动复用导致 URL 命中错人。\n\n`GSYVideoOptionBuilder`：\n- `setPlayTag(String)` [#L347](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java)\n- `setPlayPosition(int)` [#L356](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/builder/GSYVideoOptionBuilder.java)\n\n滚动出屏自动释放：`RecyclerView.OnScrollListener` → 当前 firstVisible / lastVisible 之外的 holder 调 `GSYVideoManager.releaseAllVideos()`。\n\n见 [RecyclerViewActivity](../../app/src/main/java/com/example/gsyvideoplayer/RecyclerViewActivity.java)。\n\n## 路线 B：`ListVideoUtil`（跨 item 复用一个 player）\n\n工具类：[ListVideoUtil](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/ListVideoUtil.java)。\n\n| API | 说明 |\n|---|---|\n| `ListVideoUtil(Context)` | 单例创建（在 Activity/Fragment 里持一份） |\n| `addVideoPlayer(int position, ImageView thumb, String tag, ViewGroup container, View playBtn)` | 把 player 移到 `container`；`tag` 与 `position` 用于识别是否是新目标 |\n| `setPlayPositionAndTag(int, String)` | 手动更新目标 |\n| `startPlay(String url)` | 开始播放 |\n| `isCurrentViewPlaying(int pos, String tag)` | 判定当前播放是否在这个 item |\n| `releaseVideoPlayer()` | 释放，通常在离开页面时调 |\n\n**自动可视区触发**用 [ScrollCalculatorHelper](../../app/src/main/java/com/example/gsyvideoplayer/utils/ScrollCalculatorHelper.java) 的 `onScrollReleaseAllVideos(...) / onScrollPlayVideo(...)`。见 [AutoPlayRecyclerViewActivity](../../app/src/main/java/com/example/gsyvideoplayer/AutoPlayRecyclerViewActivity.java)。\n\n## 路线 C：`GSYVideoHelper` 小窗漂移\n\n工具类：[GSYVideoHelper](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoHelper.java)。核心思路：**列表 item / 详情页共用同一个 player 实例**，在离开列表时把 player parent 换到浮层容器，进入详情再换回去。\n\n| API | 说明 |\n|---|---|\n| `GSYVideoHelper(Context, GSYBaseVideoPlayer)` | 携一份 player |\n| `setSmall(int width, int height, int gravity, int marginX, int marginY)` | 小窗尺寸 |\n| `showSmallVideo(...)` / `smallVideoToNormal()` | 小窗 ↔ 正常 |\n| `releaseVideoPlayer()` | 释放 |\n\nDemo：[SmallVideoHelper](../../app/src/main/java/com/example/gsyvideoplayer/utils/SmallVideoHelper.java)（app 层包装） + [ListVideoActivity](../../app/src/main/java/com/example/gsyvideoplayer/ListVideoActivity.java)。\n\n## ViewPager2 竖播\n\n- 用 `FragmentStateAdapter`，每个 Fragment 内嵌 `StandardGSYVideoPlayer` 或 `NormalGSYVideoPlayer`；\n- `ViewPager2.registerOnPageChangeCallback` 里 `onPageSelected` → `previousPlayer.onVideoPause(); currentPlayer.startPlayLogic()`；\n- 记得 `setUserInputEnabled` 竖向。\n\nDemo：[ViewPager2Activity](../../app/src/main/java/com/example/gsyvideoplayer/ViewPager2Activity.java)（RecyclerView 变体） + Compose 版 [VerticalShortVideoComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/VerticalShortVideoComposeActivity.kt)。\n\n## 常见坑\n\n- **相同 URL 不重播**：忘了 `setPlayTag/setPlayPosition`，`setUp` 内部去重返回 false。\n- **切换过快出黑帧**：`setUp` 后立即 `startPlayLogic` 前先隐藏 `thumb`；或用 `KeepLastFrameVideo` 保留上一帧（详见 [doc/KEEP_LAST_FRAME.md](../../doc/KEEP_LAST_FRAME.md)）。\n- **同屏多个 item 各自播放**：单例 `GSYVideoManager` 只能有一个 listener；同屏并行需要参考 [CustomManager](../../app/src/main/java/com/example/gsyvideoplayer/video/manager/CustomManager.java)。\n\n<!-- File: skills/06-gsy-cache-and-download/SKILL.md (Tokens: ~1490 | Category: anthropic-skill) -->\n---\nname: gsy-cache-and-download\ndescription: 边播边缓（AndroidVideoCache 代理）、自定义缓存目录、清理缓存、离线下载至完整文件。\nwhen_to_use: 需要\"边看边攒\"离线可播；或空间敏感需要限量清理；或做真下载入库（不是缓存代理）。\ninputs:\n  - name: url\n    description: 网络视频地址（http/https）\n    required: true\n  - name: cache_dir\n    description: 缓存目录（默认 context.cacheDir/videoCache）\n    required: false\noutputs:\n  - 播放同时把 mp4 缓存到本地；命中缓存后重播 0 网络。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheManager.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/CacheFactory.java\n  - file: gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/cache/ICacheManager.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailDownloadPlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailDownloadExoPlayer.java\n---\n\n# gsy-cache-and-download\n\n## 概念\n\n- **缓存**（cache）：`ProxyCacheManager` 内部起本地 HTTP 代理，拦截 `player` 的请求，把响应流一边喂给 player 一边写到本地；文件名是 URL MD5。\n- **下载**（download）：与缓存共享目录，但一定要走\"从头到尾\"完整播完才能保证文件可复用为独立 mp4；提前 seek 会造成 range 分片，回放时代理会重新下载缺失段。\n\n## 开启缓存\n\n方式 1：`setUp(url, true, title)` 第二参数 `cacheWithPlay=true`。\n\n方式 2：`GSYVideoOptionBuilder`：\n- `setCacheWithPlay(true)`\n- `setCachePath(File)`（可选）\n\n## 自定义缓存目录\n\n```java\nFile dir = new File(getExternalFilesDir(null), \"videoCache\");\nbuilder.setCachePath(dir);\n```\n\n若使用不同 `CacheManager`（例如 Exo），传给 `CacheFactory.setCacheManager(...)` 前记得也走 `setCachePath` 或自定义 `ICacheManager` 实现。\n\n## `ICacheManager` API\n\n见 [ICacheManager](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/cache/ICacheManager.java)。核心方法：\n\n| 方法 | 说明 |\n|---|---|\n| `doCacheLogic(Context, IMediaPlayer, String url, Map<String,String> headers, File cachePath)` | 播放前介入，把 url 换为代理 url |\n| `clearCache(Context, File cachePath, String url)` | 单条清除；`url = null` 清空整目录 |\n| `hadCached()` | 当前播放是否已\"命中\"缓存（100% 完成） |\n| `setCacheAvailableListener(CacheAvailableListener)` | 缓存进度回调 |\n| `release()` | 释放代理 |\n\n## `ProxyCacheManager` 关键静态方法\n\n在 [ProxyCacheManager](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheManager.java) 里：\n\n- `clearAllCache(Context, File)` —— 清空整个缓存目录。\n- 内部 `HttpProxyCacheServer` 单例 lazy 初始化，第一次 `doCacheLogic` 时才起服务；Application 就绪即可。\n\n## `ProxyCacheUserAgentHeadersInjector`\n\n自定义 UA / Cookie / Referer 注入到代理层：见 [ProxyCacheUserAgentHeadersInjector](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cache/ProxyCacheUserAgentHeadersInjector.java)。挂到 `HttpProxyCacheServer.Builder.headerInjector()` 上，或直接用 `Map` 传给 `setUp`。\n\n## 离线下载\n\n三步流程：\n\n1. 用 `HttpProxyCacheServer` 单独跑一次预下载（不 attach 到 Player），或直接开一条播放并 `setStartAfterPrepared(false)` 静默走全长；\n2. 事件 `VideoAllCallBack#onAutoComplete` 触发 → 缓存文件已完整；\n3. 用 [FileUtils](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/FileUtils.java) 拷贝 / 重命名到业务目录。\n\nDemo：[DetailDownloadPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailDownloadPlayer.java)（IJK）与 [DetailDownloadExoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailDownloadExoPlayer.java)（Exo）。\n\n## 不缓存的场景\n\n以下情况 `cacheWithPlay` 应设 `false`：\n- 直播 / RTMP / RTSP（连续无尽流）；\n- HLS m3u8（代理逻辑对分片不友好，建议 Exo 内核 + Exo cache）；\n- DASH / SmoothStreaming；\n- DRM 或私有加密。\n\n## 常见坑\n\n- **代理端口被占**：`HttpProxyCacheServer` 首次启动失败会 fallback 到原始 url，缓存不生效。多进程时可能出现，建议仅主进程用。\n- **缓存目录跨用户**：`context.cacheDir` 是 per-user；用外部存储需要动态权限。\n- **同 URL 缓存名冲突**：MD5(url) 相同即命中；如果 URL 带一次性签名，每次不同 → 缓存穿透。可以先规范化 URL 再传给 `setUp`。\n- **HTTP header 变更失效**：headers 参与代理请求生成，但缓存文件名只按 URL；同一 URL 换 UA 不会重新下载。\n\n<!-- File: skills/07-gsy-render-and-effects/SKILL.md (Tokens: ~2235 | Category: anthropic-skill) -->\n---\nname: gsy-render-and-effects\ndescription: 三种渲染载体（TextureView / SurfaceView / GLSurfaceView）、显示比例（16:9/4:3/full/custom）、GL 滤镜与自定义 Shader。\nwhen_to_use: 想放竖屏视频、需要 GL 滤镜、要做全屏裁减/拉伸、SurfaceView 与动画冲突、想抓截图或 gif。\ninputs:\n  - name: render_type\n    description: TEXTURE / SURFACE / GLSURFACE\n    required: true\n  - name: show_type\n    description: 显示比例枚举\n    required: false\noutputs:\n  - 渲染载体 + 显示比例 + 滤镜均按需生效。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/GSYRenderView.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/view/GSYVideoGLView.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/effect\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailFilterActivity.java\n---\n\n# gsy-render-and-effects\n\n## 渲染类型 `GSYVideoType.setRenderType(int)`\n\n| 常量 | 值 | 说明 |\n|---|---|---|\n| `TEXTURE` | 0 | 默认，与动画/圆角/缩放兼容最好 |\n| `SURFACE` | 1 | 性能最好，但和 View 动画冲突 |\n| `GLSURFACE` | 2 | 必需，若要用 `setEffectFilter` |\n\n见 [GSYVideoType#L39-L49](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java)。设置时机：`setUp` 之前，全局生效。\n\n## 显示比例 `GSYVideoType.setShowType(int)`\n\n| 常量 | 值 | 说明 |\n|---|---|---|\n| `SCREEN_TYPE_DEFAULT` | 0 | 按视频原比例居中 |\n| `SCREEN_TYPE_16_9` | 1 | 强制 16:9 |\n| `SCREEN_TYPE_4_3` | 2 | 强制 4:3 |\n| `SCREEN_TYPE_18_9` | 6 | 全面屏 |\n| `SCREEN_TYPE_FULL` | 4 | 全屏裁减；`surface_container` 建议 `FrameLayout` |\n| `SCREEN_MATCH_FULL` | -4 | 全屏拉伸；`surface_container` 建议 `FrameLayout` |\n| `SCREEN_TYPE_CUSTOM` | -5 | 需先 `setScreenScaleRatio(float)` |\n\n见 [GSYVideoType#L10-L29](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java) 与 [GSYVideoType#L165-L167](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java)。\n\n## 硬解码开关\n\n| 静态方法 | 说明 |\n|---|---|\n| `enableMediaCodec()` / `disableMediaCodec()` | IJK 硬解码总开关 [#L71-L79](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java) |\n| `enableMediaCodecTexture()` / `disableMediaCodecTexture()` | 硬解 + Texture 直渲染优化 [#L100-L108](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java) |\n| `enableSmartMediaCodec()` / `disableSmartMediaCodec()` | 硬解失败自动软解回退 [#L86-L94](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GSYVideoType.java) |\n| `isMediaCodec()` / `isMediaCodecTexture()` / `isSmartMediaCodec()` | 查询 |\n\n设置时机：**`setUp` 之前**，且**部分机型**需要在 `Application.onCreate` 就配好。\n\n## GL 滤镜\n\n需要满足两个条件：\n1. `GSYVideoType.setRenderType(GSYVideoType.GLSURFACE)`\n2. `builder.setEffectFilter(new XxxEffect())` 或 `player.setEffectFilter(...)`\n\nSDK 内置滤镜（在 `com.shuyu.gsyvideoplayer.render.effect.*`）：`AutoFixEffect`、`BarrelBlurEffect`、`BlackAndWhiteEffect`、`BrightnessEffect`、`ContrastEffect`、`CrossProcessEffect`、`DocumentaryEffect`、`DuotoneEffect`、`FillLightEffect`、`GammaEffect`、`GaussianBlurEffect`、`GrainEffect`、`GreyScaleEffect`、`HueEffect`、`InvertColorsEffect`、`LamoishEffect`、`NoEffect`、`OverlayEffect`、`PosterizeEffect`、`SampleBlurEffect`、`SaturationEffect`、`SepiaEffect`、`SharpnessEffect`、`TemperatureEffect`、`TintEffect`、`VignetteEffect`。目录：[render/effect](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/effect)。\n\n自定义 Shader：实现 [`GSYVideoGLView.ShaderInterface`](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/view/GSYVideoGLView.java)。Demo 里 4 个复杂样例：[GSYVideoGLViewCustomRender](../../app/src/main/java/com/example/gsyvideoplayer/effect/GSYVideoGLViewCustomRender.java) / 2 / 3 / 4，以及像素化 [PixelationEffect](../../app/src/main/java/com/example/gsyvideoplayer/effect/PixelationEffect.java)、BitmapOverlay [BitmapEffect](../../app/src/main/java/com/example/gsyvideoplayer/effect/BitmapEffect.java) / [BitmapIconEffect](../../app/src/main/java/com/example/gsyvideoplayer/effect/BitmapIconEffect.java)。\n\n## v14.0.0 多 pass 与新效果\n\n- `GSYVideoGLViewMultiPassRender` + `setMultiPassEffect` + `player.setCustomGLRenderer` 接入 `GaussianBlurMultiPassEffect` / `IterativeBlurPyramidEffect` / `BloomEffect`。这些不是单 pass `ShaderInterface` 滤镜，不能直接传给 `setEffectFilter`。\n- `LookupEffect(assetPath, intensity)` / `BeautyEffect(smoothLevel, whiteLevel)` / `GlitchEffect` / `CrtEffect` / `OldTvSignalEffect` 使用 `setEffectFilter`。LUT 必须由使用方放入自己的 assets；Demo 的三张图不在播放器 AAR 中。\n- `TextureShaderInterface` 在 GL 线程管理额外纹理；动态效果使用渲染器按秒绑定的 `uTime`。\n- 显示比例变化后，传统 View 调 `player.getCurrentPlayer().changeTextureViewShowType()`；Compose 调 `controller.changeTextureViewShowType()`。\n- 完整代码入口和实现文档见 [RECENT_FEATURES.md](../../doc/RECENT_FEATURES.md#v1400-gl-管线与新滤镜)。\n\n## 抓帧 / gif / 截图\n\n在 [GSYRenderView](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/render/GSYRenderView.java)：\n\n| 方法 | 回调 | 说明 |\n|---|---|---|\n| `taskShotPic(GSYVideoShotListener)` | 返 Bitmap | 单帧截图 |\n| `saveFrame(File, GSYVideoShotSaveListener)` | 保存到磁盘 | 落盘截图 |\n| `taskGifPic(...)` + `GifCreateHelper` | 生成 gif | 见 [GifCreateHelper](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/utils/GifCreateHelper.java) |\n\n## Demo 对照\n\n- 滤镜集合：[DetailFilterActivity](../../app/src/main/java/com/example/gsyvideoplayer/DetailFilterActivity.java) + [DetailFilterComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/DetailFilterComposeActivity.kt)\n- 透明视频：[SampleTransparentVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/SampleTransparentVideo.java) + [DetailTransparentActivity](../../app/src/main/java/com/example/gsyvideoplayer/DetailTransparentActivity.java)\n- 自定义 Render：[CustomRenderVideoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/CustomRenderVideoPlayer.java)\n\n## 常见坑\n\n- `SurfaceView` 在 `RecyclerView` 里滑动会撕裂 / 覆盖 → 列表用 `TEXTURE`。\n- GL 效果的性能取决于分辨率、pass 数和设备；先评估 pass/金字塔层数与实际帧率，不能通过静默换成 TextureView 宣称效果已生效。\n- 竖屏视频强制 `SCREEN_TYPE_FULL` 会大幅裁减；建议 `SCREEN_TYPE_DEFAULT` + `setAutoFullWithSize(true)`。\n\n<!-- File: skills/08-gsy-subtitle-and-preview/SKILL.md (Tokens: ~1767 | Category: anthropic-skill) -->\n---\nname: gsy-subtitle-and-preview\ndescription: 外挂/内嵌字幕（SRT/VTT）、Exo 内嵌字幕轨、Seek 悬浮预览缩略图（VTT thumbnails）。\nwhen_to_use: 需要显示 srt/vtt/ass 字幕、切换字幕轨、seek 时鼠标/手指浮层展示预览帧。\ninputs:\n  - name: subtitle_source\n    description: 字幕文件路径或 URL、MIME\n    required: false\n  - name: preview_source\n    description: VTT 预览描述文件\n    required: false\noutputs:\n  - player 上叠加 subtitle view；seekbar 拖动出现预览帧。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview\n  - file: app/src/main/java/com/example/gsyvideoplayer/SubtitleDetailPlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitlePlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/PreViewGSYVideoPlayer.java\n---\n\n# gsy-subtitle-and-preview\n\n## 字幕 API\n\n顶层控制器：[GSYSubtitleController](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleController.java)。视图：[GSYSubtitleView](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleView.java)。\n\n| 方法 | 说明 |\n|---|---|\n| `new GSYSubtitleController(context, subtitleView)` | 绑定 UI |\n| `setSources(List<GSYSubtitleSource>)` | 一次注入多轨字幕 |\n| `setSource(GSYSubtitleSource)` | 只有一条 |\n| `getSources()` | 只读 List |\n| `selectSubtitle(String id)` | 按 id 切换字幕，\"\" 或 null 关闭 |\n| `setStyle(GSYSubtitleStyle)` | 字体/描边/背景/颜色 |\n| `setOffsetMs(long)` | 音画/字幕 offset |\n| `setEnabled(boolean)` | 开关 |\n| `updatePosition(long positionMs)` | 由 player 进度驱动 |\n| `release()` | 释放 |\n\n## `GSYSubtitleSource` 字段\n\n见 [GSYSubtitleSource](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleSource.java)：\n\n| 字段 | 说明 |\n|---|---|\n| `id` | 唯一 id |\n| `label` | 显示名 |\n| `language` | ISO code |\n| `mime` | 见 [GSYSubtitleMime](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleMime.java)：`text/vtt`、`application/x-subrip`（srt）、`text/plain` |\n| `uri` | http 或 file uri |\n| `embedded` | 是否内嵌轨（Exo 场景） |\n\n## 内置解析器\n\n- SRT：[GSYSrtSubtitleParser](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSrtSubtitleParser.java)\n- WebVTT：[GSYWebVttSubtitleParser](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYWebVttSubtitleParser.java)\n- 自定义 MIME：注册到 [GSYSubtitleParserFactory](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/subtitle/GSYSubtitleParserFactory.java)\n\n## Exo 内嵌字幕\n\n`gsyvideoplayer-exo_player2` 的 Manager 会把 Exo 的 subtitle track 通过 `GSYSubtitleController` 上抛。Demo：[GSYExoSubTitlePlayer](../../app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitlePlayer.java) + [GSYExoSubTitleVideoManager](../../app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitleVideoManager.java)。\n\n## Seek 预览 API\n\n顶层：[GSYVideoPreviewProvider](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview/GSYVideoPreviewProvider.java) 与列表实现 [GSYVideoPreviewListProvider](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview/GSYVideoPreviewListProvider.java)。\n\n| 方法 | 说明 |\n|---|---|\n| `GSYVideoPreviewProvider#getPreviewFrame(long positionMs)` | 返 `GSYVideoPreviewFrame`，含 image url + crop 坐标 |\n| `GSYVideoPreviewListProvider(List<GSYVideoPreviewFrame>)` | 直接注入帧列表（自己解析或用 VTT） |\n| `GSYVideoPreviewVttParser#parse(String vttContent, String baseUrl)` | 解析 WebVTT thumbnails（雪碧图 + 时间段） |\n| `GSYVideoPreviewVttParser#parseFrames(String vttContent, String baseUrl)` | 只要帧列表 |\n\n## `GSYVideoPreviewFrame` 字段\n\n见 [GSYVideoPreviewFrame](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/preview/GSYVideoPreviewFrame.java)：`startTimeMs`、`endTimeMs`、`imageUrl`、`cropX/Y/Width/Height`、`hasCrop()`。\n\n## 关键调用序（VTT 预览 + SRT 字幕）\n\n1. 加载 subtitle：`controller.setSource(new GSYSubtitleSource(\"cn\", \"中文\", \"zh\", GSYSubtitleMime.SRT, uri, false))`;\n2. 播放进度回调（`GSYVideoProgressListener`）中 `controller.updatePosition(current)`；\n3. 预览：`GSYVideoPreviewVttParser.parse(vttText, base)` → 得到 Provider → 注入到 `PreViewGSYVideoPlayer#setPreviewProvider(provider)`；\n4. seekbar 拖动时 SDK 会自动调 `provider.getPreviewFrame(target)` 并做 crop。\n\n## Demo 对照\n\n- SRT 字幕：[SubtitleDetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/SubtitleDetailPlayer.java)（对应 `raw/demo_subtitle.srt` / `demo_subtitle_vtt.vtt`）\n- Exo 内嵌字幕：[GSYExoSubTitleDetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/exosubtitle/GSYExoSubTitleDetailPlayer.java)\n- 预览缩略图：[PreViewGSYVideoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/PreViewGSYVideoPlayer.java) + [PlayPickActivity](../../app/src/main/java/com/example/gsyvideoplayer/PlayPickActivity.java)\n\n## 常见坑\n\n- SRT 时间轴带 BOM 或换行不规范会直接被 parser 丢弃；先用 UTF-8 无 BOM 保存。\n- VTT 缩略图 URL 相对路径必须传 `baseUrl` 才能拼绝对路径。\n- `updatePosition` 必须在 UI 线程或至少与 `subtitleView` 同线程；`GSYSubtitleController` 内部无线程切换。\n\n<!-- File: skills/09-gsy-live-and-ads/SKILL.md (Tokens: ~2086 | Category: anthropic-skill) -->\n---\nname: gsy-live-and-ads\ndescription: 直播（RTMP/RTSP/HLS）、贴片广告 (PreRoll AD)、静音、切码率、无缝切源、切换过程保留最后一帧。\nwhen_to_use: 做直播；短视频/长视频前贴片广告；PV/UV 埋点后要 A/B 分发；快节奏内容切换。\ninputs:\n  - name: stream_url\n    description: 直播地址（rtmp://.../ rtsp://.../ .m3u8 等）\n    required: false\n  - name: ad_url\n    description: 前贴广告地址\n    required: false\noutputs:\n  - 直播不使用缓存；广告播放完切主片；无缝续播不闪黑。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYADVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYSampleADVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoADManager.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseADActivityDetail.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer2.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/KeepLastFrameDemoActivity.java\n---\n\n# gsy-live-and-ads\n\n## 直播接入要点\n\n- `setUp(url, false, title)` —— `cacheWithPlay` 必须 `false`。\n- `onResume` / `player.onVideoResume(false)`：**`seek=false` 避免拉流跳变**（见 [GSYVideoView#L549](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYVideoView.java)）。\n- 内核选择：\n  - RTMP / RTSP：**必须 IJK**；System / Exo / Aliyun 各有兼容性差异。\n  - HLS：Exo 更稳；Ijk 需要开启硬解 `GSYVideoType.enableMediaCodec()` 才顺。\n- 关键参数：\n  - `builder.setNeedShowWifiTip(false)`（后台 App 不打断）\n  - `builder.setReleaseWhenLossAudio(false)`（丢音频焦点不释放）\n  - `builder.setSurfaceErrorPlay(true)`\n\n对 IJK 加 low-latency：通过 `VideoOptionModel` 注入 ffmpeg option，见 [BasePlayerManager](../../gsyVideoPlayer-base/src/main/java/com/shuyu/gsyvideoplayer/player/BasePlayerManager.java) 与 [GSYVideoManager.instance().setOptionModelList(list)](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYVideoBaseManager.java)。常用 option：`analyzemaxduration=100`、`probesize=10240`、`flush_packets=1`、`fflags=nobuffer`、`packet-buffering=0`、`framedrop=1`。\n\n## 广告 AD\n\n- 双 Manager 双 Player：`GSYVideoADManager.instance()` 负责广告 player，`GSYVideoManager.instance()` 负责主片；两者共存不打架。\n- 组件 [GSYADVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYADVideoPlayer.java) 与\"广告示例\"[GSYSampleADVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/GSYSampleADVideoPlayer.java) 都继承 `StandardGSYVideoPlayer`。\n- Activity 基类：[GSYBaseADActivityDetail](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/GSYBaseADActivityDetail.java)，提供 `getGSYVideoADPlayer()` / `initADVideo()` / `startAD(String adUrl, String url)` / `onADEnd`。\n\n### `startAD(...)` 关键调用序\n\n1. Activity 里持有主 player + AD player 两个 View；\n2. `initADVideo()` 装配（内部会连锁 `OrientationUtils`）；\n3. `startAD(adUrl, mainUrl)` 播广告；\n4. AD complete → SDK 内部隐藏 AD player、start 主 player；\n5. 用户点\"跳过\"→ `getGSYVideoADPlayer().onAutoCompletion()`。\n\n## 无缝切源 / 切码率\n\nExo 内核用 `Exo2PlayerManager.setCurrentMediaSource(...)` 或 Demo 里的 `GSYExo2MediaPlayer.setMediaSources(...)`；Ijk 则重新 `setUp` + `startPlayLogic`。切换的过程中要求\"不闪黑\"，用：\n\n- [KeepLastFrameVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/KeepLastFrameVideo.java)（Demo 组件）+ [KeepLastFrameDemoActivity](../../app/src/main/java/com/example/gsyvideoplayer/KeepLastFrameDemoActivity.java)；\n- 原理见 [doc/KEEP_LAST_FRAME.md](../../doc/KEEP_LAST_FRAME.md)：新 player 拿到首帧前，把旧 SurfaceTexture 内容以 Bitmap 覆盖在上层。\n\n## 广告 / 主片全屏切换的两个陷阱\n\n- 主片全屏 → 广告插入：`GSYVideoADManager` 无内部 `OrientationUtils`；`GSYBaseADActivityDetail` 已处理，但**自定义子类需重写 `getGSYVideoADPlayer()` 返回 AD player 才能被基类的旋转/回退纳管**。\n- 广告全屏返回 → 直接销毁 AD player：`onBackPressed` 需要判断 `GSYVideoManager.backFromWindowFull(this) || GSYVideoADManager.backFromWindowFull(this)`。\n\n## Demo 对照\n\n- 单主片：[DetailPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailPlayer.java)\n- 前贴片 + 主片：[DetailADPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer.java) / [DetailADPlayer2](../../app/src/main/java/com/example/gsyvideoplayer/DetailADPlayer2.java)\n- 列表条目内广告：[ListADVideoActivity](../../app/src/main/java/com/example/gsyvideoplayer/ListADVideoActivity.java) / [ListADVideoActivity2](../../app/src/main/java/com/example/gsyvideoplayer/ListADVideoActivity2.java) / [RequestListADVideoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/RequestListADVideoPlayer.java)\n- 智能硬解回退：[SmartMediaCodecFallbackActivity](../../app/src/main/java/com/example/gsyvideoplayer/SmartMediaCodecFallbackActivity.java)\n\n## v14.0.0 RTSP 与倍速\n\n- FORMAT `timeout` 为微秒（`20_000_000L` = 20 秒），明确配置的 `rtsp_transport` 会透传。\n- PLAYER `rtsp-live-max-buffer-ms` 在 prepare 前设置，`0` 关闭，`500..60000` 毫秒用于正常 1× 直播队列恢复。恢复会丢弃旧媒体并产生跳跃/音频间隙，不是自动重连。\n- 初始化重定向覆盖 OPTIONS/DESCRIBE/SETUP/首次 PLAY；不支持播放中 REDIRECT 或任意跨端口控制会话。\n- `soundtouch=1` 为 0.25×–4× 软件变速；`soundtouch=0` 为设备相关平台范围。引擎切换和 EOF 按真实音频排空处理，终止错误后需要 reset 或重建。\n- 详见 [QUESTION.md](../../doc/QUESTION.md) 与 [v14 发布说明](../../doc/UPDATE_VERSION.md#v1400-2026-10-10)。\n\n## 常见坑\n\n- 直播卡顿升级 → 检查是否开启 `cacheWithPlay`（对直播来说必须关）。\n- 广告 `onAutoComplete` 后主片没自动 setUp → 检查是不是把 `setStartAfterPrepared(false)` 传给了主 builder。\n- RTMP 断流回收后重连白屏 → `onVideoResume(false)` + `player.getCurrentPlayer().onVideoReset()` 再 `startPlayLogic()`。\n\n<!-- File: skills/10-gsy-cast-and-loopback/SKILL.md (Tokens: ~2328 | Category: anthropic-skill) -->\n---\nname: gsy-cast-and-loopback\ndescription: 使用内核层投屏 SPI（CastCapability + CastProvider）接入 DLNA，或做本地 loopback 回环模拟被投屏端。\nwhen_to_use: 需要接入 DLNA/AirPlay/Chromecast 到已有播放页；做投屏发起端 UI；做集成测试的本地 loopback 接收端；或做多协议聚合发现。\ninputs:\n  - name: providers\n    description: 一个或多个 CastProvider 实例（DLNA 默认由 jUPnP 提供）\n    required: true\noutputs:\n  - CastCapability 单例已注册 provider，可发现设备、连接、控制播放/停止。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastProvider.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastSession.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastListener.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastMediaInfo.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastState.java\n  - file: gsyVideoPlayer-cast/src/main/java/com/shuyu/gsyvideoplayer/cast/dlna\n  - file: app/src/main/java/com/example/gsyvideoplayer/CastDemoActivity.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/cast\n  - file: doc/CAST_FEATURE_PLAN.md\n---\n\n# gsy-cast-and-loopback\n\n## 顶层架构\n\n```\nCastCapability (单例)  ← 主线程 API 入口\n  ├─ registerProvider(CastProvider)\n  ├─ startDiscovery(Context) / stopDiscovery()\n  ├─ getAvailableDevices(): List<CastDevice>\n  ├─ connect(device, ConnectCallback) : CastSession（异步）\n  ├─ addListener(CastListener)\n  └─ disconnect()\n```\n\n对齐 Media3 `Player` 语义：`CastCapability ≈ Player 池`、`activeSession ≈ 当前 Player`、`CastProvider ≈ 渲染后端`、`CastListener/SessionListener ≈ Player.Listener`。见 [CastCapability](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) 与 [CastProvider](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastProvider.java) 的头 JavaDoc。\n\n## 核心 API\n\n### CastCapability\n\n| 方法 | 位置 | 说明 |\n|---|---|---|\n| `getInstance()` | [#L38-L47](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | DCL 单例 |\n| `registerProvider(CastProvider)` | [#L85-L90](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 注册；`protocol` 相同则覆盖 |\n| `unregisterProvider(String)` | [#L93-L103](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 反注册并停止其发现 |\n| `startDiscovery(Context)` | [#L106-L125](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 所有已注册 provider 同时开始发现 |\n| `stopDiscovery()` | [#L128-L135](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 全部停止 |\n| `getAvailableDevices()` | [#L138-L146](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 快照，跨 provider 聚合 |\n| `connect(CastDevice, ConnectCallback)` | [#L154-L183](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 会自动 disconnect 旧会话 |\n| `disconnect()` | [#L186-L196](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | release 当前 session |\n| `getActiveSession()` | [#L199-L201](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 拿当前会话（可能 null） |\n| `addListener / removeListener` | [#L203-L209](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastCapability.java) | 主线程 listener |\n\n### CastProvider（SPI 契约）\n\n| 方法 | 说明 |\n|---|---|\n| `getProtocol(): String` | 全小写协议 id（`dlna` / `chromecast` / `airplay`） |\n| `startDiscovery(Context, DiscoveryListener)` | 后台线程订阅，回调 postMain |\n| `stopDiscovery()` | 释放订阅、解绑 Service |\n| `connect(CastDevice, ConnectCallback): CastSession` | 建立会话；同步返回 session，异步回调结果 |\n\n线程约定：所有 network IO 由 provider 自己后台跑，回调都 postMain。\n\n### CastSession（媒体控制）\n\n见 [CastSession](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastSession.java)：\n\n| 方法 | 说明 |\n|---|---|\n| `load(CastMediaInfo)` | 推送要投的 URL |\n| `play() / pause() / stop()` | 播控 |\n| `seekTo(long ms)` | 定位 |\n| `setVolume(int 0-100)` / `setMute(boolean)` | 音量 |\n| `getState(): CastState` | IDLE / CONNECTING / CONNECTED / LOADING / PLAYING / PAUSED / STOPPED / ERROR，见 [CastState](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastState.java) |\n| `addListener(SessionListener)` | 位置/状态回调 |\n| `release()` | 归还 |\n\n### CastMediaInfo（要投的媒体）\n\n见 [CastMediaInfo](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/cast/CastMediaInfo.java)。字段：`url`、`title`、`durationMs`、`mimeType`、`headers`、`metadata`（`Map`）。\n\n## DLNA 参考实现\n\n模块 `gsyvideoplayer-cast` 提供 [JupnpDlnaProvider](../../gsyVideoPlayer-cast/src/main/java/com/shuyu/gsyvideoplayer/cast/dlna/JupnpDlnaProvider.java) 与 [JupnpDlnaSession](../../gsyVideoPlayer-cast/src/main/java/com/shuyu/gsyvideoplayer/cast/dlna/JupnpDlnaSession.java)。业务代码调用：\n\n```java\nCastCapability cap = CastCapability.getInstance();\ncap.registerProvider(new JupnpDlnaProvider());\ncap.addListener(myListener);\ncap.startDiscovery(context);\n```\n\n发现事件：`CastListener#onDeviceListChanged(List<CastDevice>)`；连接：`cap.connect(devices.get(0), connectCallback)`。\n\n## 本地 Loopback（Demo 测试用）\n\napp 模块下的 [cast/](../../app/src/main/java/com/example/gsyvideoplayer/cast) 提供**可发现的接收端**，方便在同一台设备上完成集成测试：\n\n- [DevReceiverService](../../app/src/main/java/com/example/gsyvideoplayer/cast/DevReceiverService.java) —— foreground service，通过 jUPnP 注册\"回环设备\"。\n- [LoopbackDeviceFactory](../../app/src/main/java/com/example/gsyvideoplayer/cast/LoopbackDeviceFactory.java) —— 构造 UPnP MediaRenderer。\n- [LoopbackAvTransportService](../../app/src/main/java/com/example/gsyvideoplayer/cast/LoopbackAvTransportService.java) / [LoopbackRenderingControlService](../../app/src/main/java/com/example/gsyvideoplayer/cast/LoopbackRenderingControlService.java) —— AVTransport / RenderingControl 实现。\n- [CastReceiverFloatingWindow](../../app/src/main/java/com/example/gsyvideoplayer/cast/CastReceiverFloatingWindow.java) + [CastReceiverPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/CastReceiverPlayer.java) —— 可视化播放。\n\n## Demo 对照\n\n- 一体化：[CastDemoActivity](../../app/src/main/java/com/example/gsyvideoplayer/CastDemoActivity.java)\n- Cast 相关控制页：[SampleCastControlVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/SampleCastControlVideo.java)\n- 设计与测试：[doc/CAST_FEATURE_PLAN.md](../../doc/CAST_FEATURE_PLAN.md)、[doc/CAST_RECEIVER_DESIGN.md](../../doc/CAST_RECEIVER_DESIGN.md)、[doc/CAST_TEST_PLAYBOOK.md](../../doc/CAST_TEST_PLAYBOOK.md)\n\n## 常见坑\n\n- 不同 provider 上报的 `CastDevice` 需要用 `getProtocol()` 分组；直接把陌生 protocol 传给 `connect` 会拿到 `IllegalStateException`。\n- Provider 网络 IO 必须在自己线程，`CastCapability` 不做 main 切换。\n- Wi-Fi 多播锁没拿 → DLNA 发现不到设备：Provider 需要 `WifiManager.MulticastLock`。\n- Loopback 与真实设备同网段时会同时被发现，Demo 里有过滤示例。\n\n<!-- File: skills/11-gsy-compose-integration/SKILL.md (Tokens: ~2916 | Category: anthropic-skill) -->\n---\nname: gsy-compose-integration\ndescription: 在 Jetpack Compose 中通过 AndroidView 桥接 GSYVideoPlayer，覆盖列表、竖直短视频、生命周期管控。\nwhen_to_use: 项目已经或正在迁移到 Compose；需要在可组合项里嵌入播放器；需要复用列表滑动/自动播放逻辑但 UI 用 Compose。\ninputs:\n  - name: url\n    description: 视频 URL\n    required: true\noutputs:\n  - Composable 内嵌 GSYVideoPlayer，生命周期与 Composable 一致。\nreferences:\n  - file: gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose\n  - file: app/src/main/java/com/example/gsyvideoplayer/compose\n---\n\n# gsy-compose-integration\n\n## 模块\n\n`gsyVideoPlayer-compose` 是 Compose 桥接层，两种模式并存：\n\n| 模式 | 入口 | 说明 |\n|---|---|---|\n| **Wrapper**（推荐上手） | [GSYVideoPlayerView](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/wrapper/GSYVideoPlayerView.kt) / [GSYAnyVideoPlayerView](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/wrapper/GSYAnyVideoPlayerView.kt) | `AndroidView` 直接承载 `StandardGSYVideoPlayer` / 任意 `NormalGSYVideoPlayer` 子类，保留全部内置能力 |\n| **Native**（纯 Compose 控件） | [rememberGSYPlayerController](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerSurface.kt) + [GSYPlayerSurface](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerSurface.kt) + [GSYDefaultControls](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYDefaultControls.kt) | 只承载画面，控制层完全用 Compose 自绘 |\n| **通用工具** | [LifecycleBridge](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/common/LifecycleBridge.kt) | `LifecycleEffect { event -> ... }` 用于订阅宿主 Lifecycle |\n\n## Wrapper API：`GSYVideoPlayerView`\n\n见 [GSYVideoPlayerView#L28-L36](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/wrapper/GSYVideoPlayerView.kt)：\n\n| 参数 | 类型 | 说明 |\n|---|---|---|\n| `modifier` | `Modifier` | 通常 `Modifier.fillMaxWidth().aspectRatio(16f/9)` |\n| `setUp` | `(StandardGSYVideoPlayer) -> Unit` | 等价 `GSYVideoOptionBuilder.build(player)` |\n| `setUpKey` | `Any?` = null | 非空时其变化会重新调用 `setUp`（典型：传 url） |\n| `autoReleaseOnDispose` | Boolean = true | 离屏自动 `player.release()` |\n| `autoPauseResume` | Boolean = true | 自动桥 Lifecycle：`ON_PAUSE→GSYVideoManager.onPause()`、`ON_RESUME→onResume()` |\n| `onPlayerCreated` | `(StandardGSYVideoPlayer) -> Unit` | 拿到 player 实例的回调 |\n\n## Wrapper API：`GSYAnyVideoPlayerView<T : NormalGSYVideoPlayer>`\n\n见 [GSYAnyVideoPlayerView#L26-L35](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/wrapper/GSYAnyVideoPlayerView.kt)。相比 `GSYVideoPlayerView` 多出：\n\n| 参数 | 类型 | 说明 |\n|---|---|---|\n| `factory` | `(Context) -> T` | 传自定义子类（如 `ListGSYVideoPlayer`、`SampleControlVideo`、`DanmakuVideoPlayer`） |\n\n## Native API：`rememberGSYPlayerController`\n\n见 [rememberGSYPlayerController#L26-L32](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerSurface.kt)：\n\n| 参数 | 默认 | 说明 |\n|---|---|---|\n| `url` | null | 非空时内部 `LaunchedEffect` 触发 `controller.setUp(...)` |\n| `cacheWithPlay` | false | 边播边缓 |\n| `title` | \"\" | 标题（透传给底层 player） |\n| `autoPlay` | false | attach 完成后自动 `startPlayLogic` |\n| `autoPauseResume` | true | 自动订阅 Lifecycle（同 wrapper） |\n\n返回 [GSYPlayerController](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerController.kt)，可以再调 `.play() / .pause() / .seekTo(ms) / .setSpeed(...) / .changeTextureViewShowType() / .setUp(url, cache, title, autoPlay) / .dispose()`；状态订阅走 [GSYPlayerState](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerState.kt)（`StateFlow<GSYPlayerState>`），事件走 [GSYPlayerEvent](../../gsyVideoPlayer-compose/src/main/java/com/shuyu/gsyvideoplayer/compose/native_/GSYPlayerEvent.kt)。\n\n## 最小可运行示例\n\n**Wrapper 模式**：\n\n```kotlin\nGSYVideoPlayerView(\n    modifier = Modifier.fillMaxWidth().aspectRatio(16f/9),\n    setUpKey = url,\n    setUp = { player ->\n        GSYVideoOptionBuilder()\n            .setUrl(url)\n            .setCacheWithPlay(true)\n            .setVideoTitle(\"demo\")\n            .setIsTouchWiget(true)\n            .setAutoFullWithSize(true)\n            .build(player)\n        player.startPlayLogic()\n    }\n)\n```\n\n**Native 模式**：\n\n```kotlin\nval controller = rememberGSYPlayerController(url = url, autoPlay = true)\nBox(Modifier.fillMaxWidth().aspectRatio(16f/9)) {\n    GSYPlayerSurface(controller = controller, modifier = Modifier.matchParentSize())\n    GSYDefaultControls(controller = controller)\n}\n```\n\n## 生命周期最佳实践\n\n```kotlin\nval lifecycle = LocalLifecycleOwner.current.lifecycle\nDisposableEffect(lifecycle) {\n    val obs = LifecycleEventObserver { _, e ->\n        when (e) {\n            Lifecycle.Event.ON_PAUSE  -> state.pause()\n            Lifecycle.Event.ON_RESUME -> state.resumeIfNeeded()\n            Lifecycle.Event.ON_DESTROY-> state.release()\n            else -> Unit\n        }\n    }\n    lifecycle.addObserver(obs)\n    onDispose { lifecycle.removeObserver(obs) }\n}\n```\n\n## Demo 对照\n\n| 场景 | Activity |\n|---|---|\n| 原生桥接示例 | [DetailNativeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/DetailNativeActivity.kt) / [BasicWrapperActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/BasicWrapperActivity.kt) / [FullFeatureNativeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/FullFeatureNativeActivity.kt) |\n| 广告 + 主片（列表内） | [AdInListComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/AdInListComposeActivity.kt) |\n| 列表 + 全屏 | [ListWithFullscreenActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/ListWithFullscreenActivity.kt) / [ListPlayNativeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/ListPlayNativeActivity.kt) / [AutoPlayListActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/AutoPlayListActivity.kt) |\n| 抖音式竖屏 | [VerticalShortVideoComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/VerticalShortVideoComposeActivity.kt) |\n| 滤镜 | [DetailFilterComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/DetailFilterComposeActivity.kt) |\n| 无缝切源 / Exo 切源 | [SwitchSeamlessComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/SwitchSeamlessComposeActivity.kt) / [ExoSwitchSourceComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/ExoSwitchSourceComposeActivity.kt) / [SwitchUrlActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/SwitchUrlActivity.kt) |\n| 字幕 | [SubtitleComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/SubtitleComposeActivity.kt) |\n| 弹幕 | [DanmakuComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/DanmakuComposeActivity.kt) |\n| 缓存 / 下载 | [CacheDownloadComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/CacheDownloadComposeActivity.kt) |\n| 硬解 / MediaCodec | [MediaCodecComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/MediaCodecComposeActivity.kt) |\n| 悬浮小窗 | [FloatingWindowComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/FloatingWindowComposeActivity.kt) |\n| 音频独立 | [AudioOnlyComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/AudioOnlyComposeActivity.kt) |\n| 多类型混排 | [MoreTypeComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/MoreTypeComposeActivity.kt) |\n| 多窗口并行 | [MultiWindowActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/MultiWindowActivity.kt) / [MultiWindowParallelComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/MultiWindowParallelComposeActivity.kt) |\n| 本地文件 | [LocalFileComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/LocalFileComposeActivity.kt) |\n| 自定义主题 | [CustomControlsThemeComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/CustomControlsThemeComposeActivity.kt) |\n| WebView 详情 | [WebDetailComposeActivity](../../app/src/main/java/com/example/gsyvideoplayer/compose/host/WebDetailComposeActivity.kt) |\n\n样例 hosts 全在 [compose/host/](../../app/src/main/java/com/example/gsyvideoplayer/compose/host)。\n\n## v14.0.0 状态与接入\n\n外部坐标为 `io.github.carguo:gsyvideoplayer-compose:14.0.0`（GitHub Packages 使用 `com.shuyu`）。IJK 使用方还需选择 `ex_so` 或独立 ABI 原生模块；完整组合见 [DEPENDENCIES.md](../../doc/DEPENDENCIES.md)。\n\n`detachHost` 只解绑 View 侧回调/tick，`dispose` 统一释放保留的 host；`snapshot.bufferPercent` 合并内核轮询与回调值。`controller.changeTextureViewShowType()` 在主线程更新当前内嵌/全屏比例。Compose Demo 共 25 项，新增折叠屏 BOOK/TABLETOP。详见 [COMPOSE_USE.md](../../doc/COMPOSE_USE.md)。\n\n## 常见坑\n\n- Compose 频繁重组 → `AndroidView` 的 `factory` 只跑一次；不要把 `setUp` 放在 `factory` 里，放到 `update` block，并用 `remember(url)` 做去重。\n- `AndroidView` 内部布局要指定固定高度或 `aspectRatio`，否则 SurfaceView 会 0 高度导致黑屏。\n- 竖直短视频 + `Pager`：切页时用 `pagerState.currentPage` 触发 `state.play(url) / previousState.pause()`；释放放在 `DisposableEffect(pagerState)` 里。\n- 与 Compose 主题深浅色切换：`GSYVideoPlayerCompose` 是原生 View 层，配色需要单独 `setBottomProgressBarDrawable` 等 API。\n\n<!-- File: skills/12-gsy-custom-view/SKILL.md (Tokens: ~1865 | Category: anthropic-skill) -->\n---\nname: gsy-custom-view\ndescription: 自定义播放器 UI（继承 StandardGSYVideoPlayer）与自定义控制层：换 seekbar、加弹幕层、加清晰度切换、加投屏按钮等。\nwhen_to_use: SDK 默认 UI 不符合需求；需要在标准控制层上加自定义控件；需要多状态视图（loading/error/paywall）。\ninputs:\n  - name: layout_id\n    description: 自定义 XML 布局资源 id\n    required: true\noutputs:\n  - 一个继承 GSYBaseVideoPlayer 子类的自定义播放器组件。\nreferences:\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/NormalGSYVideoPlayer.java\n  - file: gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/base/GSYBaseVideoPlayer.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/SampleCoverVideo.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/SampleControlVideo.java\n  - file: app/src/main/java/com/example/gsyvideoplayer/video/LandLayoutVideo.java\n---\n\n# gsy-custom-view\n\n## 继承层次\n\n```\nGSYTextureRenderView（渲染管理）\n  └── GSYVideoView（状态机 / 生命周期）\n        └── GSYVideoControlView（默认控制层：进度条/按钮）\n              └── GSYVideoPlayer（触摸手势）\n                    └── GSYBaseVideoPlayer（全屏/小屏切换）\n                          ├── StandardGSYVideoPlayer（默认 UI）\n                          └── NormalGSYVideoPlayer（无返回/无标题的简版）\n```\n\n自定义组件通常继承 [StandardGSYVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/StandardGSYVideoPlayer.java) 或 [NormalGSYVideoPlayer](../../gsyVideoPlayer-java/src/main/java/com/shuyu/gsyvideoplayer/video/NormalGSYVideoPlayer.java)。\n\n## 必须实现的三件事\n\n### 1) 自定义布局 & id\n\n覆写：\n```java\n@Override public int getLayoutId() { return R.layout.my_video_player; }\n```\n布局里必须包含（id 不能改）：\n- `@id/surface_container` —— 放渲染 View（FrameLayout）\n- 需要用到的默认控件（如 `@id/start`、`@id/progress`、`@id/current`、`@id/total`、`@id/bottom_progressbar`、`@id/thumb`、`@id/small_close`、`@id/back`、`@id/fullscreen`、`@id/loading`）—— 缺哪个哪个功能失效，编译不报错。\n\n### 2) 覆写全屏 & 小屏组件类\n\n关键：`getConstructor(Context, Boolean)` 是 SDK 内部反射拷贝出的全屏实例。**必须提供两个构造器**：\n\n```java\npublic MyVideoPlayer(Context c) { super(c); }\npublic MyVideoPlayer(Context c, Boolean fullFlag) { super(c, fullFlag); }\npublic MyVideoPlayer(Context c, AttributeSet a) { super(c, a); }\n```\n\nR8 侧已在 [proguard-rules.pro](../../app/proguard-rules.pro) 保留：\n```\n-keep class * extends com.shuyu.gsyvideoplayer.video.base.GSYBaseVideoPlayer {\n    public <init>(android.content.Context);\n    public <init>(android.content.Context, java.lang.Boolean);\n}\n```\n\n### 3) 覆写 `showSmallVideo() / hideSmallVideo()`（如需要小窗）以及 `startWindowFullscreen()`（如要全屏样式差异）\n\n## 常用重写点\n\n| 方法 | 作用 |\n|---|---|\n| `changeUiToNormal()` / `changeUiToPreparingShow()` / `changeUiToPlayingShow()` / `changeUiToPauseShow()` / `changeUiToCompleteShow()` / `changeUiToError()` / `changeUiToPlayingBufferingShow()` | 不同状态下的 UI 切换钩子 |\n| `onClickUiToggle()` | 单击 UI 显隐 |\n| `setStateAndUi(int state)` | 由 SDK 内部调，可 super 后加自己的逻辑 |\n| `updateStartImage()` | 播放/暂停按钮 icon 切换 |\n| `resolveTypeUI()` | 首帧比例决定后的自定义处理 |\n| `dismissBrightnessDialog / dismissVolumeDialog / dismissProgressDialog` | 手势弹窗 UI |\n| `hideAllWidget / setViewShowState` | 显隐所有控件 |\n\n## 手势自定义\n\n`GSYVideoControlView` 里通过如下开关切换：\n- `setIsTouchWiget(boolean)` —— 手势总开关（默认 true）\n- `setIsTouchWigetFull(boolean)` —— 只在全屏开\n- `setNeedLockFull(boolean)` —— 全屏锁按钮\n- `setSeekRatio(float)` —— 手势 seek 灵敏度\n- `setThreshold(int)` —— 触发手势阈值\n\n手势弹窗替换：覆写 `showBrightnessDialog / showVolumeDialog / showProgressDialog` 用自己的 Dialog。\n\n## 标准封面 / 清晰度 / 弹幕\n\n- 封面：Demo 组件 [SampleCoverVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/SampleCoverVideo.java) 演示用 Glide 加载封面。\n- 清晰度切换：Demo 组件 [SwitchVideo](../../app/src/main/java/com/example/gsyvideoplayer/switchplay/SwitchVideo.java) + [DetailListPlayer](../../app/src/main/java/com/example/gsyvideoplayer/DetailListPlayer.java)（多列源，`setUp(List<GSYVideoModel>, ...)`）。\n- 弹幕层：Demo 组件 [DanmakuVideoPlayer](../../app/src/main/java/com/example/gsyvideoplayer/video/DanmakuVideoPlayer.java) —— 在布局叠一层 `DanmakuView`，在 `onProgress` 里 `dispatch(long time)`。\n- 控制条自定义：Demo 组件 [SampleControlVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/SampleControlVideo.java)（毛玻璃底栏 + 播放按钮）。\n- 横屏专用布局：Demo 组件 [LandLayoutVideo](../../app/src/main/java/com/example/gsyvideoplayer/video/LandLayoutVideo.java)（`getFullId()` 返回不同 layout）。\n\n## 多状态视图（loading / error / paywall）\n\n在 `changeUiToError()` 与 `changeUiToPreparingShow()` 里 `setVisibility` 自定义 View；也可以在 `onVideoAllCallBack.onPlayError` 中显式 `showErrorLayer()`。\n\n## 常见坑\n\n- 忘写 `Boolean` 构造器 → 全屏切换 `IllegalArgumentException` 或直接 crash（反射失败）。\n- 自定义 `layout` 缺 id → 默认功能沉默失效，问题不易定位。建议基于 `layout_standard_video_player.xml` 复制修改。\n- 全屏组件和非全屏组件类型不同 → 用 `getCurrentPlayer()` 获取\"当前活跃 player\"，别缓存 `this`。\n- 释放时机：`onDestroy` 里既要 `super.onBackPressed` 判断，也要 `GSYVideoManager.releaseAllVideos()`。\n\n<!-- File: skills/13-gsy-proguard-r8/SKILL.md (Tokens: ~300 | Category: anthropic-skill) -->\n# 13-gsy-proguard-r8\n\nPath: `skills/13-gsy-proguard-r8/SKILL.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/13-gsy-proguard-r8/SKILL.md)\n\n<!-- File: skills/README.md (Tokens: ~300 | Category: anthropic-skill) -->\n# Skills Skill\n\nPath: `skills/README.md`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/CarGuo/GSYVideoPlayer/HEAD/skills/README.md)\n</yakaai_skills>"}