Conversation
Implement complete M3U8/HLS and direct video file offline download with batch episode selection, download management UI, and local playback support. New files: - Hive models: DownloadRecord (TypeID:7), DownloadEpisode (TypeID:8) - M3U8 parser: master/media playlist, encryption keys, URL resolution - Ad filter: discontinuity-group based ad detection and removal - Download engine: concurrent segment download, pause/resume, retry - Direct file download fallback with Range-based resume support - Download controller: WebView-based URL resolution, batch orchestration - Download management page with progress, pause/resume/retry/delete - Batch episode selection sheet from video page - Lightweight local video player page (media_kit) Modified files: - storage.dart: register Hive adapters, open downloads Box - index_module.dart: register DownloadController and IDownloadRepository - init_page.dart: initialize download controller on startup - settings_module.dart: add /download route - my_page.dart: add download management entry - video_page.dart: add download button and per-episode status icons - player_controller.dart: detect and use local cache for playback
…starting - Extract buildFullUrl() and buildHttpHeaders() into Plugin class to eliminate duplicated logic in DownloadController - Add IDownloadManager interface and DownloadRequest data class to replace 9+ parameter methods and enable proper DI - Remove DownloadManager factory singleton, register via Modular DI - Move forceAdBlocker setting access into IDownloadRepository - Fix bug where _processQueue() was a no-op that silently discarded queued downloads when parallel limit was reached
|
我们可以有限考虑上面提到的逻辑上的问题 UI/UX 问题可以之后解决,主要是现在此功能的 UX 不适合移动端,误触概率很高 |
将 Windows 平台的 WebviewController 替换为 HeadlessWebview, 避免在无 UI 的 M3U8 解析场景下产生内存泄漏。可见 Webview Widget 替换为占位 UI,WebView 完全在后台运行。
部分视频源将实际分片嵌套在 M3U8 引用中。新增 resolveNestedSegments() 递归展开嵌套引用,使下载管线能获取到真实的 TS 分片。
覆盖 master/media playlist 解析、VOD 检测、EVENT 流拒绝及嵌套 M3U8 分片展开,使用 Mux 和 Apple 的公开 HLS 测试流。
此前解析器仅靠 ENDLIST 和兜底逻辑推断 VOD,未显式识别 PLAYLIST-TYPE:VOD 标签,与注释描述不一致。新增 isExplicitVod 标志并补充对应测试用例。
|
对于此前提到的内容里目前只有ui没有做优化,我们可以在功能测试完毕后,我来提交ui的改动 对于Windows Webview控制器改动HeadlessWebview,我目前没有Windows设备进行测试,完整的测试我会在两天后进行测试. 之前确实遗漏了 #EXT-X-PLAYLIST-TYPE:VOD 已在 29766e6 中修复:解析器现在显式检测 PLAYLIST-TYPE:VOD 标签,加入了 另外测试中用了两个公开的 HLS 测试流可以做端到端验证:
|
lib/pages/webview/webview_controller_impel/webview_windows_controller_impel.dart
Outdated
Show resolved
Hide resolved
|
在使用操作符重载修复 m3u8Key 类的比较问题并解决嵌套 m3u8 后,在我看来这个 m3u8 解析实现已经没有问题,新增了一条关于 windows headless webview 的修改建议 关于后续的下载功能的 UX 问题,也就是移动端容易误触选集按钮本身和上面的下载按钮,我觉得我们可以进行分离 在在 video_page.dart 中使用一个 FloatingActionsButton 或是在播放控制面板上加一个按钮,点开之后使用 bottomSheet 展开一个新的分集列表,可以在那个分集列表里点击按钮进行下载,并且可以考虑多选 |
|
#1666 合并后存在合并冲突需要修复 |
disposeEnvironment() 内部已通过 headless_instances_.clear() 销毁 所有 headless 实例,移除额外的 headlessWebview.dispose() 调用, 避免两个异步方法通道调用产生竞争导致异常。 经原生 C++ 代码验证:disposeEnvironment 会依次清理所有实例和环境, 不存在双重释放问题;保留 disposeEnvironment 调用以支持代理配置热更新。
|
关于你提到的"修改前大片注释"和"双重释放崩溃"的问题,我对 webview_windows fork 原注释说的是 WebviewController.dispose() 和
由于 erase 会物理移除条目,后续 clear 最终方案是只调 disposeEnvironment(),不调 headlessWebview.dispose()。理由:
已在 8626ca4 中提交修复,代码中也加了注释说明原因 |
解决与 PR Predidit#1666 (定时关闭功能) 的 import 冲突
|
我们为什么不再进行 headlessWebview 对象是否为空的判断,而是在任何情况下都为其赋值为空并执行 disposeEnvironment |
不过为了代码一致性,可以改为 if (headlessWebview != null) {
headlessWebview = null;
}
WebviewController.disposeEnvironment(); |
|
你是对的,我们保留当前无判断的状态 这样逻辑层面就不再有任何问题,只剩下UI需要处理 关于UI方面,你有什么想法吗,重新实现一个完整的离线播放器似乎会产生大量重复代码 |
|
UI的构建我正在本地测试,我在思考怎么用一个比较优雅的方式来完成这个工作. |
|
我现在意识到一个烦人的架构问题 webview_controller 的本质是页面控制器,本身也是全局单例,在当前用例下,我们把这个全局单例控制器注入到了离线下载模块中并调用 这里可能导致严重的状态污染,也就是有下载任务在执行时,观看新的在线视频会导致 webview 状态混乱 考虑到播放器的困境,我在想我们能不能一劳永逸解决这一问题,由于当前 webview_controller 已经都是无头 webview 实现,不再需要页面上的小部件作为宿主,我在考虑能不能将其抽象为一个单一的 Provider ,这个 Provider 的数据可以来自由 webview 实现的解析器,也可以来自缓存 |
|
没有问题,我思考一下这个架构应该怎么做,正因为这个问题我才没有马上完善离线播放器,因为我认为这应该涉及的改动比较大,所以我不敢草率去做这个变动 |
|
我没记错的话还有一个阻碍是 ohos 无法使用无头 webview |
在播放器右上角三点菜单中添加"下载"子菜单,包含: - 下载当前集:一键下载正在播放的集数 - 批量下载:打开选集下载面板进行多选 此改动将下载功能与选集按钮分离,优化移动端用户体验, 避免误触选集按钮上方的下载按钮。
|
对于下载的入口,我进行了调整. 效果如下:
|
如果是这样的话,可能只能对鸿蒙的适配做一个降级处理. 我们可以在 ohos 的 VideoPage 或某个全局容器中保持一个隐藏WebView Widget,供下载模块使用。这样:
@ErBWs 你觉得这样的实现怎么样? |
|
我先将这个ohos的适配性改动上传,因为我没有任何鸿蒙相关的设备,所以无法调试. |
|
ohos 的 videopage 目前一直额外存在一个 webview 组件。 我已经很久没有碰过 kazumi 的代码了,有点忘记播放器的具体实现了。我这几天也会看一下代码看看有没有比较好的解决方案 |
好嘞! |
- 新增 lib/providers/ 模块: - IVideoSourceProvider 接口定义视频源解析抽象 - WebViewVideoSourceProvider 封装 headless WebView 解析 - CachedVideoSourceProvider 支持本地缓存视频读取 - CompositeVideoSourceProvider 组合策略(优先缓存) - VideoPageController 重构: - 原生播放器模式使用独立 Provider 实例 - WebView 渲染模式保持使用共享 Controller - 支持取消正在进行的解析操作 - DownloadController 重构: - 使用 WebViewVideoSourceProvider 替代直接操作 WebView - 解决与 VideoPage 共享 WebView 单例导致的状态冲突 此重构确保 VideoPage 和 DownloadController 的视频解析相互独立, 避免同时下载和播放时的状态污染问题。
|
这应该是我最后建议的变更了,这个实现现在实在是太棒了 我有两个问题
|
同时下载5集,完成了第一集,第二集,第三集,第五集。
|
…_inappwebview 将低版本 Android 的 WebView 回退实现从 webview_flutter 迁移到 flutter_inappwebview 的 PlatformHeadlessInAppWebView,使其兼容 WebViewVideoSourceProvider 的 headless 架构。使用 evaluateJavascript() 在 onLoadStart/onLoadStop 回调中注入脚本替代 UserScript AT_DOCUMENT_START, 同时移除 webview_flutter 依赖。
|
啊,这让我想到另一个问题,如果我们在正常在线播放流程中使用了缓存,我们是否应该显示一个 toast 提醒使用者,这样即使当他们遇到我们考虑不周全导致的缓存错位时,也可以删除缓存 而不是认为解析环节出了问题 |
可以,本pr中虽然 CachedVideoSourceProvider 和 CompositeVideoSourceProvider的代码已经写好了,但它们是死代码——在线播放路径(video_controller.dart:228)始终只实例化,WebViewVideoSourceProvider,从不检查本地是否有已下载的集数。 我接下来提交一个改动来应用这个功能,并且同时加入toast |
|
抱歉这个pr太庞大了,我有些看晕了,先不要启用这些死代码,暂时移除他们,作为后续pr添加,这个pr的规模已经太大了。 |
没问题,我也是觉得太庞大了所以我不敢一次性搞定 :) |
|
实际上我现在就想到一个位置索引错位导致的缺陷,并且不会因为抛出异常而进行回退 也就是我们同时下载1到5集,第2集未完成,播放器播放第2集会变成第3集 你对此问题有什么看法吗,还是我的理解出现了问题 |
|
我们移除上面提到的死代码和我最后的关于位置疑问后就可以结束这个pr 如果已经完成了 Android 低版本回退代码路径测试的话 |
不会,目前如果遇到这样的情况:第一集,第二集,第三集,第四集(未完成下载),第五集。在离线播放器的选集会显示第一集,第二集,第三集,第五集,不会显示第四集,这样也方便用户可以清楚地看到第四集并未完成下载。 是如实显示,并不会错误地将第五集显示为第四集。 |
移除未使用的 CachedVideoSourceProvider 和 CompositeVideoSourceProvider, 清理 providers.dart 的 export,减少 PR 中的死代码便于 review。
90bc687 to
0945243
Compare
|
稍等,我测试一下android 10版本的回退。 在android10的调试下我定位到一个问题,我明天会来尝试修复 |
在 onLoadStart 注入的 blob parser 和 iframe redirect 脚本中, 将顶层 callHandler 日志调用包裹 try-catch,防止 bridge 未初始化 时整个脚本崩溃导致 M3U8 拦截失效。
|
在实际测试中,我还发现一个问题:
本质问题:Request.get() 把网络错误包装成假的 200 Response,下游调用者不知道请求已失败,按正常数据处理就会类型错误。 这个改动并不在我们这个pr里,当时设计这个机制是为了什么?我们需要修复这个问题吗? 在体验上我看到客户端的状态是持续加载。 |
|
当前最新的改动下,我在pixel 4 + android10的环境下测试,所有功能均正常。 |
|
主要是在当前的网络请求架构下,参考 interceptor.dart 的实现 非200的请求会自动触发一个弹窗,我们有时不想要那个弹窗 当然网络请求架构现在有待商榷,如果你有兴趣的话,可以进行修改 |
|
我已经完成了这个PR后续的代码清理和部分错误修复,如果测试中出现了新的问题请及时告诉我 此外我注意到 android 后台下载的实现似乎相当麻烦,你对这个功能现在大概有什么方案和看法呢 |
Foreground Service + 通知这样的方案如何? |
|
这也是我在考虑的方案,我们不需要考虑 Android8 以下的兼容性,我们在引入 vulkan 之后已经不再能在 Android8 以下设备上运行 |
|
是的,我就是之前注意到你在其他issue提到的不再支持Android8以下,所以我觉得使用这个不错? |
|
如果你有兴趣和时间的话,欢迎提交PR,我现在需要处理一些这个PR的收尾问题,例如超时处理和日志处理 |
|
没问题 |
|
Hi, 只是想询问一个后续的问题 我们的为低版本 android 准备的回退 webview 实现的 JS 部分是怎么实现的,似乎和之前的实现不同,这部分代码似乎解析成功率不是特别高 |
我在标准的download以外做了一个回退机制 这个特性不取决于 Android 系统版本,而是取决于设备上安装的 WebView APK 版本。 Android 的 System WebView是可以独立于系统更新的(通过 Google Play / 应用商店升级)。DOCUMENT_START_SCRIPT 需要 Chromium WebView 91+(约 2021 5 月发布)。
简单说:不是 Android 系统版本低,而是 WebView 组件版本低,导致缺少 DOCUMENT_START_SCRIPT 能力,从而走到了只能用evaluateJavascript() 手动注入的回退实现,产生时机竞争和跨域 iframe 无法注入的问题。 |
* feat: add offline video download and caching Implement complete M3U8/HLS and direct video file offline download with batch episode selection, download management UI, and local playback support. New files: - Hive models: DownloadRecord (TypeID:7), DownloadEpisode (TypeID:8) - M3U8 parser: master/media playlist, encryption keys, URL resolution - Ad filter: discontinuity-group based ad detection and removal - Download engine: concurrent segment download, pause/resume, retry - Direct file download fallback with Range-based resume support - Download controller: WebView-based URL resolution, batch orchestration - Download management page with progress, pause/resume/retry/delete - Batch episode selection sheet from video page - Lightweight local video player page (media_kit) Modified files: - storage.dart: register Hive adapters, open downloads Box - index_module.dart: register DownloadController and IDownloadRepository - init_page.dart: initialize download controller on startup - settings_module.dart: add /download route - my_page.dart: add download management entry - video_page.dart: add download button and per-episode status icons - player_controller.dart: detect and use local cache for playback * refactor: decouple DownloadController and fix queued downloads never starting - Extract buildFullUrl() and buildHttpHeaders() into Plugin class to eliminate duplicated logic in DownloadController - Add IDownloadManager interface and DownloadRequest data class to replace 9+ parameter methods and enable proper DI - Remove DownloadManager factory singleton, register via Modular DI - Move forceAdBlocker setting access into IDownloadRepository - Fix bug where _processQueue() was a no-op that silently discarded queued downloads when parallel limit was reached * refactor: Windows WebView 控制器改用 HeadlessWebview 将 Windows 平台的 WebviewController 替换为 HeadlessWebview, 避免在无 UI 的 M3U8 解析场景下产生内存泄漏。可见 Webview Widget 替换为占位 UI,WebView 完全在后台运行。 * feat: 下载前展开嵌套 M3U8 播放列表 部分视频源将实际分片嵌套在 M3U8 引用中。新增 resolveNestedSegments() 递归展开嵌套引用,使下载管线能获取到真实的 TS 分片。 * test: 添加 M3U8 解析器验证测试 覆盖 master/media playlist 解析、VOD 检测、EVENT 流拒绝及嵌套 M3U8 分片展开,使用 Mux 和 Apple 的公开 HLS 测试流。 * fix: 显式处理 #EXT-X-PLAYLIST-TYPE:VOD 标签 此前解析器仅靠 ENDLIST 和兜底逻辑推断 VOD,未显式识别 PLAYLIST-TYPE:VOD 标签,与注释描述不一致。新增 isExplicitVod 标志并补充对应测试用例。 * fix: 修复 Windows WebView dispose 可能的竞争条件 disposeEnvironment() 内部已通过 headless_instances_.clear() 销毁 所有 headless 实例,移除额外的 headlessWebview.dispose() 调用, 避免两个异步方法通道调用产生竞争导致异常。 经原生 C++ 代码验证:disposeEnvironment 会依次清理所有实例和环境, 不存在双重释放问题;保留 disposeEnvironment 调用以支持代理配置热更新。 * feat: 播放器菜单新增下载入口 在播放器右上角三点菜单中添加"下载"子菜单,包含: - 下载当前集:一键下载正在播放的集数 - 批量下载:打开选集下载面板进行多选 此改动将下载功能与选集按钮分离,优化移动端用户体验, 避免误触选集按钮上方的下载按钮。 * refactor: 引入 VideoSourceProvider 抽象层解决 WebView 状态污染 - 新增 lib/providers/ 模块: - IVideoSourceProvider 接口定义视频源解析抽象 - WebViewVideoSourceProvider 封装 headless WebView 解析 - CachedVideoSourceProvider 支持本地缓存视频读取 - CompositeVideoSourceProvider 组合策略(优先缓存) - VideoPageController 重构: - 原生播放器模式使用独立 Provider 实例 - WebView 渲染模式保持使用共享 Controller - 支持取消正在进行的解析操作 - DownloadController 重构: - 使用 WebViewVideoSourceProvider 替代直接操作 WebView - 解决与 VideoPage 共享 WebView 单例导致的状态冲突 此重构确保 VideoPage 和 DownloadController 的视频解析相互独立, 避免同时下载和播放时的状态污染问题。 * feat: 离线播放复用主播放器架构 - 删除简化版 DownloadPlayerPage,统一使用 VideoPage - VideoPageController 新增离线模式支持 (isOfflineMode) - 离线模式下跳过 WebView 和弹幕请求,支持无网播放 - 离线集数列表使用下载时保存的剧集名称 - 修复历史记录使用实际集数编号 (actualEpisodeNumber) * refactor: 解耦 VideoPageController 对 DownloadController 的依赖 - IDownloadRepository 扩展接口: - getRecordByBangumiId() - 通过番剧ID获取下载记录 - getEpisode() - 获取指定集数的下载信息 - getCompletedEpisodes() - 获取已完成下载的集数列表 - VideoPageController 改用 Repository 层: - 依赖 IDownloadRepository + IDownloadManager - 移除对 DownloadController 的直接依赖 - 遵循 Controller 不应依赖 Controller 的分层原则 - DownloadController/Page 使用 Repository 新方法: - getRecord/getEpisode 委托给 Repository - getCompletedEpisodes 提供给 Page 层使用 - CachedVideoSourceProvider 简化: - 使用 Repository.getEpisode() 替代手动查询 * refactor: 移除 WebView Widget 层技术债务并改进 Hive 错误处理 - 移除 WebviewItem Widget 及其平台实现文件 - 移除 VideoPage 中的 WebView 流订阅,统一使用 Provider 模式 - 移除 VideoModule 中的 WebviewItemController 单例注册 - 为 downloadController.init() 添加 try-catch 防止数据损坏导致黑屏 - 改进 DownloadRepository.getAllRecords() 逐条读取,单条失败不影响整体 * feat: 离线弹幕缓存功能 * feat: 下载速率显示、并发配置与稳定性改进 - 新增下载速率实时显示(KB/s、MB/s) - 新增下载设置页面,支持配置集数并发(1-5)和分片并发(1-10) - 修复 Hive 数据库损坏导致的黑屏问题: - 启动时自动检测并恢复损坏的 Box - 下载进度仅在状态变化时持久化,大幅减少磁盘 I/O - 新增优先下载功能(插队),可跳过队列立即下载 - 新增"开始全部"功能,一键恢复剧集所有未完成的下载 - 修复重启后等待中的任务卡住的问题 - 修复 HTTP 416 断点续传错误 - 批量下载时显示提示信息 * refactor: 代码清理与健壮性改进 - 迁移 WebView 基础设施从 pages/webview/ 到 lib/webview/ - 清理 video_page.dart 中 useNativePlayer 死代码分支 - 分片下载使用 .tmp 临时文件防止崩溃导致不完整文件 - 优化 WebViewVideoSourceProvider 生命周期,复用 WebView 实例 * fix: 修复离线播放时 currentEpisode 越界导致崩溃 initForOfflinePlayback 中将实际集数编号直接赋给 currentEpisode, 但离线 roadList 仅包含已下载集数,导致索引越界。 改为在 roadList.data 中查找对应位置作为 currentEpisode。 * fix: 使用 URL 匹配替代位置匹配修复下载集数重排序问题 - 新增 getEpisodeByUrl 通过 episodePageUrl 查找下载记录,位置匹配作为回退 - startDownload 添加 URL 防重复检查,防止列表重排序后重复下载 - 下载状态图标、下载面板、单集下载均改用 URL 匹配 - 移除弹幕设置中重复的"下载时缓存弹幕"选项,保留下载设置中的 - int.parse 改为 int.tryParse 防御性编程 - 从 IVideoSourceProvider 接口层移除 useNativePlayer 参数 * refactor: 重写 WebviewItemControllerImpel 从 webview_flutter 迁移到 flutter_inappwebview 将低版本 Android 的 WebView 回退实现从 webview_flutter 迁移到 flutter_inappwebview 的 PlatformHeadlessInAppWebView,使其兼容 WebViewVideoSourceProvider 的 headless 架构。使用 evaluateJavascript() 在 onLoadStart/onLoadStop 回调中注入脚本替代 UserScript AT_DOCUMENT_START, 同时移除 webview_flutter 依赖。 * chore: 清理阶段4未使用的 VideoSourceProvider 实现 移除未使用的 CachedVideoSourceProvider 和 CompositeVideoSourceProvider, 清理 providers.dart 的 export,减少 PR 中的死代码便于 review。 * fix: 修复旧版 Android WebView onLoadStart 时 callHandler 未就绪导致脚本中止 在 onLoadStart 注入的 blob parser 和 iframe redirect 脚本中, 将顶层 callHandler 日志调用包裹 try-catch,防止 bridge 未初始化 时整个脚本崩溃导致 M3U8 拦截失效。 (cherry picked from commit 85dd497)
|
我在移植的时候遇到了一些问题,webview_item 会报错 |
当前主仓库中 WebviewItemController 已不再通过Modular DI注册,而是改用了 WebviewItemControllerFactory静态工厂方法创建实例。 ohos 移植者的代码 webview_ohos_item.dart:17 仍在用旧方式: // 旧方式 (已废弃) — 会报 UnregisteredInstance 解决方案 ohos 移植者需要做两件事:
参考 lib/webview/webview_controller.dart: class WebviewItemControllerFactory {
static WebviewItemController getController() {
// ... 其他平台 ...
if (Platform.isOhos) { // 或 ohos 的平台判断方式
return WebviewOhosItemController();
}
return WebviewItemControllerImpel();
}
}
// 不要这样做
final controller = Modular.get<WebviewItemController>();
// 改为直接通过工厂创建或从外部传入
final controller = WebviewItemControllerFactory.getController();由于 ohos 的 WebView 必须绑定可见 Widget(无法 headless),WebviewOhosItem 应该接收一个已创建的 controller实例作为参数,而不是从 DI 容器获取。这样与当前主仓库的架构(WebViewVideoSourceProvider 每次创建独立实例)保持一致。 |
基于此前我实现的对广告的过滤,我实现了完整的视频缓存功能.
在此版本下,我们可以在点击剧集后,选集对视频进行缓存.
在设置页面->下载管理访问后,可以查看下载进度,并直接播放.
同时在已经下载的内容中,可以在正常的播放窗口直接使用缓存.
这个版本的代码可能需要review,我觉得这个功能很有意思所以迫不及待提起pr,目前我已经在本地测试有效
摘要
实现完整的M3U8/HLS及直接视频文件离线下载功能,支持批量剧集选择、下载管理界面及本地播放支持。
新增文件:
修改文件: