音视频移动开发【免费下载链接】streamyfinA modern Jellyfin client built with Expo项目地址https://gitcode.com/gh_mirrors/st/streamyfin点击查看免费下载本篇技术指南以 modules/background-downloader/README.md 为骨架结合 Streamyfin 仓库中该模块的完整源码Swift / Kotlin / TypeScript与下载子系统providers/Downloads的调用链系统讲解这个基于 Expo Modules 的原生后台下载模块如何在 JS 侧发起下载、监听进度与完成事件、管理并发与队列、指定自定义存储路径以及 iOS 侧NSURLSession与 Android 侧前台服务各自的实现原理、权限配置与构建步骤。读完你既能直接上手调用该模块的 API也能理解其在 Jellyfin 客户端场景下应用退到后台甚至被杀死后下载依然继续的完整技术方案。模块概览一个跨平台的后台下载能力单元Background Downloader 是 Streamyfin一个基于 Expo 构建的现代 Jellyfin 客户端中负责大文件后台下载的原生模块位于仓库的 modules/background-downloader 目录。它通过 Expo Modules Core 向 JS 层暴露统一的BackgroundDownloader对象见 modules/index.ts 的导出声明平台差异被完全封装在原生侧iOS基于NSURLSession的 background sessionBackgroundDownloaderModule.swiftAndroid基于 OkHttp 流式下载 前台服务OkHttpDownloadManager.kt、DownloadService.kt。模块能力清单README Features包括应用进入后台甚至挂起后下载继续、通过事件实时回报进度、支持多任务并发下载、支持取消单个或全部任务、可指定自定义目标路径、完整的错误上报以及 iOS / Android 双平台支持。实现层面的一个事实校正README 声称 Android 使用系统的DownloadManagerAPI但仓库中的实际实现OkHttpDownloadManager.kt使用的是OkHttpClient 前台DownloadService。本文以源码实现为准展开讲解。TypeScript API 全解模块在 JS 侧的核心封装位于 index.ts事件类型定义位于 BackgroundDownloader.types.ts。除 README 文档列出的方法外源码还扩展了排队下载enqueueDownload、按 URL 取消排队任务cancelQueuedDownload与 iOS Live Activity 相关 API下面一并覆盖。基础用法启动下载与事件监听import { BackgroundDownloader } from /modules; // 启动一个下载第二个参数 destinationPath 可选 const taskId await BackgroundDownloader.startDownload( https://example.com/largefile.mp4 ); // 进度监听 const progressSub BackgroundDownloader.addProgressListener((event) { console.log(Progress: ${Math.floor(event.progress * 100)}%); console.log(Downloaded: ${event.bytesWritten} / ${event.totalBytes}); }); // 完成监听 const completeSub BackgroundDownloader.addCompleteListener((event) { console.log(Download complete!); console.log(File saved to:, event.filePath); console.log(Task ID:, event.taskId); }); // 错误监听 const errorSub BackgroundDownloader.addErrorListener((event) { console.error(Download failed:, event.error); }); // 取消某个下载 BackgroundDownloader.cancelDownload(taskId); // 获取所有活跃下载 const activeDownloads await BackgroundDownloader.getActiveDownloads(); // 不再需要时清理监听器 progressSub.remove(); completeSub.remove(); errorSub.remove();各监听器在 JS 封装中通过addListener订阅原生事件见 index.ts事件名分别为onDownloadProgress、onDownloadComplete、onDownloadError、onDownloadStarted与 iOSBackgroundDownloaderModule.swift和 AndroidBackgroundDownloaderModule.kt两侧ModuleDefinition中声明的Events一一对应。自定义目标路径默认情况下文件会保存到平台各自的默认目录iOS 的 Documents、Android 的外部文件目录。需要指定路径时传入第二个参数import { BackgroundDownloader } from /modules; import * as FileSystem from expo-file-system; const destinationPath ${FileSystem.documentDirectory}myfile.mp4; const taskId await BackgroundDownloader.startDownload( https://example.com/video.mp4, destinationPath );并发下载与任务管理模块支持多个下载同时进行README 给出了以Map维护任务进度的典型用法const downloads new Map(); async function startMultipleDownloads(urls: string[]) { for (const url of urls) { const taskId await BackgroundDownloader.startDownload(url); downloads.set(taskId, { url, progress: 0 }); } } const progressSub BackgroundDownloader.addProgressListener((event) { const download downloads.get(event.taskId); if (download) { download.progress event.progress; } }); // 取消全部下载 BackgroundDownloader.cancelAllDownloads();排队下载源码新增能力README 未提及但源码已实现的是串行下载队列enqueueDownload把任务加入队列同一时刻只处理一个队列非空时返回占位 taskId-1iOS 侧逻辑见 BackgroundDownloaderModule.swiftAndroid 侧见 BackgroundDownloaderModule.kt。对应的取消 API 为cancelQueuedDownload(url)按 URL 从队列中移除。iOS 专属 API// 开关 iOS 锁屏 Live ActivityAndroid 上为 no-op BackgroundDownloader.setLiveActivityEnabled(enabled: boolean); // 获取 App Group 共享目录路径Live Activity 扩展可读取不可用时返回 null const dir BackgroundDownloader.getLiveActivityDirectory(); // string | null方法签名汇总方法参数返回说明startDownloadurl: stringdestinationPath?: stringmetadata?: DownloadActivityMetadataheaders?: Recordstring,stringPromisenumber立即启动一个后台下载返回 taskIdenqueueDownload同上Promisenumber加入串行队列排队中返回-1cancelDownloadtaskId: numbervoid按任务 ID 取消cancelQueuedDownloadurl: stringvoid按 URL 移除排队任务cancelAllDownloads—void取消全部含清空队列getActiveDownloads—PromiseActiveDownload[]列出运行中与排队中的任务setLiveActivityEnabledenabled: booleanvoidiOS 专属关闭时同时结束已存在的 Live Activity 与系统状态 UIgetLiveActivityDirectory—string \| nulliOS 专属App Group 目录海报图需预置于此供扩展读取事件负载与类型定义README 给出了四类事件与ActiveDownload的基础类型仓库中 BackgroundDownloader.types.ts 在其基础上做了扩展——每个事件都增加了可选的itemId字段来自入队时传入的 metadataActiveDownload增加itemId、destinationPath状态从 README 的running | suspended | canceling | completed | unknown收敛为实际的running | queuedinterface DownloadProgressEvent { taskId: number; bytesWritten: number; totalBytes: number; progress: number; // 0.0 ~ 1.0 itemId?: string; // 入队 metadata 中回传的 Jellyfin item id } interface DownloadCompleteEvent { taskId: number; filePath: string; url: string; itemId?: string; } interface DownloadErrorEvent { taskId: number; error: string; itemId?: string; } interface DownloadStartedEvent { taskId: number; url: string; itemId?: string; } interface ActiveDownload { taskId: number; // 排队中的条目为 -1 url: string; itemId?: string; destinationPath?: string; state?: running | queued; }此外iOS Live Activity 需要一组完整元数据DownloadActivityMetadataitemId、title、subtitle、海报文件名posterFileName、转码下载无Content-Length时的兜底总量estimatedTotalBytes以及按状态downloading/completed/failed/queued键控的本地化文案labels——所有字符串在 JS 侧预翻译后下传避免在 Swift 中重复维护 i18n。iOS 实现原理基于 NSURLSession 的 background sessioniOS 侧使用带唯一标识的 background configuration 创建会话BackgroundDownloaderModule.swift会话标识符com.fredrikburmester.streamyfin.backgrounddownloaderallowsCellularAccess true允许蜂窝网络下载sessionSendsLaunchEvents true后台完成时唤醒应用isDiscretionary false不延迟调度所有网络传输由系统进程nsurlsessiond承接即使 App 被挂起甚至进程被杀下载仍持续推进任务完成后系统会重新拉起 App 并投递完成回调。代价是用户强制退出 App 时系统可能终止下载README Notes 中的限制。完整回调链进度、完成、错误模块通过独立的DownloadSessionDelegateBackgroundDownloaderModule.swift处理URLSessionDownloadDelegate回调进度didWriteData中把totalBytesWritten / totalBytesExpectedToWrite计算为进度原生侧每 500ms 节流一次事件handleProgressLocked避免高频冲刷 JS 侧完成didFinishDownloadingTo先检查 HTTP 状态码非 2xx 一律按失败处理——因为服务器返回的 401/404 错误页也会以下载完成形式落地若不拦截会把错误页当作媒体文件保存源码注释明确说明了这一点错误didCompleteWithError投递onDownloadError会话收尾urlSessionDidFinishEvents中调用通过 AppDelegate 存储的后台 completion handler通知系统本次后台唤醒已处理完毕。所有可变状态被收拢到一个串行stateQueuedispatch queue中URLSession 委托队列、Expo 函数体、getAllTasks完成回调等多个执行上下文对downloadTasks/downloadQueue的访问都被强制串行化避免并发数据竞争。任务持久化进程死亡后如何找回下载这是 iOS 侧最关键的可靠性设计。BackgroundURLSession在 App 死亡后仍会继续传输完成时系统重启 App 并投递didFinishDownloadingTo如果任务表只存在内存里重启后查找必然失败临时文件永远不会被移入正式目录——下载会无声丢失。解决方案是 DownloadTaskStore.swift把运行中任务与排队任务编码进 App Group 的UserDefaults存储键com.fredrikburmester.streamyfin.backgrounddownloader.tasks与.queue。模块OnCreate时先恢复任务表再重连会话BackgroundDownloaderModule.swift使进程死亡期间已完成的下载仍能被正确移动到位。背景会话的 taskIdentifier 在重启后保持稳定因此可以安全地作为键。任务条目带 7 天年龄上限避免存储无限增长。AppDelegate 桥接处理系统唤醒BackgroundDownloaderAppDelegate.swift 实现了handleEventsForBackgroundURLSession当会话标识符匹配时把系统传来的 completion handler 存入模块静态属性供urlSessionDidFinishEvents在事件全部投递后调用从而正确结束系统对 App 的后台唤醒。该 AppDelegate 通过 expo-module.config.json 的appDelegateSubscribers字段注册。Live Activity锁屏下载状态iOS 16.2DownloadLiveActivityController.swift 全程由原生URLSession委托驱动而非 JS——因为 App 退到后台后 React Native 运行时通常已被系统回收终态已下载的翻转只有原生侧可靠。几个值得注意的工程决策整个队列复用同一条 ActivityActivityKit 只允许前台发起Activity.request而排队任务往往从后台唤醒中启动因此第一条下载创建 Activity后续任务通过activity.update重新绑定rebind避免每个 episode 都要新建速度计算使用 EMA 平滑didWriteData每秒触发多次瞬时字节增量抖动太大控制器用 1 秒采样窗口 0.15 的平滑系数得出可读的下载速率完成时间投影基于平滑速度按0.85的悲观系数外推projectedEndDate与progressBarStartDate配合ProgressView(timerInterval:)/Text(timerInterval:)——这是 App 被挂起时 iOS 仍会自行推进的仅有两个原生动画原语使锁屏进度条在进程挂起期间也能自行走动同时刻意少承诺避免投影超前造成说谎观感staleDate 机制每次推送都带过期时间系统会视觉淡化过期的数据海报图预置扩展进程读不到 App 的 Documents 目录JS 侧通过getLiveActivityDirectory()获取 App Group 目录并预置海报见 liveActivity.ts扩展只按posterFileName读取共享容器进程重启后的 reconcilereconcile(persistedTasks:)按持久化任务中的itemId重新收养上一进程遗留的 Activity其余孤儿 Activity 被立即结束。iOS 26Continued ProcessingXcode 26 / iOS 26 SDK 下模块额外启用 DownloadContinuedProcessingKeeper.swift通过BGContinuedProcessingTask标识com.fredrikburmester.streamyfin.downloads保持进程持续执行URLSession 委托持续收到didWriteDataLive Activity 全程显示实测百分比而非投影值。单个任务横跨整个队列会话队列排空或取消时才finish()。在 iOS 26 之前的设备、提交被拒或系统到期时自动降级为挂起 投影的旧行为。Android 实现原理OkHttp 流式下载与 .part 暂存文件与 README 所述使用系统 DownloadManager不同实际实现OkHttpDownloadManager.kt使用 OkHttpClient连接超时 30s、读取超时 60s、调用超时 0长转码下载不设上限支持自定义请求头代理网关鉴权场景响应流被写入目标路径.part暂存文件全部写完并确认未取消后才重命名为正式文件——保证目标路径上永远只存在完成态文件进程中途被杀不会留下半成品冒充完成进程被杀时无法执行清理JS 侧 reconciliation 会把已存在的目标文件视为已完成下载所以这个顺序是正确性的关键取消通过call.isCanceled()在读取循环中检查取消时删除.part并退出进度同样按 500ms 节流回调非 2xx 响应直接报错不落盘。前台服务 WakeLock保证后台继续DownloadService.kt 是下载的前台服务通知 ID1001、渠道download_channelIMPORTANCE_LOWPRIORITY_LOW持续显示在通知栏下载进行中显示百分比启动阶段显示不确定进度条Android 14SDK 34通过ServiceCompat.startForeground声明FOREGROUND_SERVICE_TYPE_DATA_SYNC类型PARTIAL_WAKE_LOCK仅在下载进行中持有服务生命周期长于单个下载onDestroy释放为时过晚下载计数归零即释放syncActiveDownloads(count)由模块在任务列表变化时调用是唤醒锁与前台通知的唯一事实来源。ForegroundPromotionAndroid 15 的前台服务合规ForegroundPromotion.kt 处理了一个隐蔽的系统限制startForegroundService()是对系统会在几秒内调用 startForeground()的承诺服务若停止或超时未兑现进程会被ForegroundServiceDidNotStartInTimeException杀死。因此每个 start 命令都必须先回答、后判断是否有事可做。Android 15 还会在每次调用时重新审查 dataSync 服务能否从当前应用状态启动重复调用可能被拒——但首次的晋升仍然有效不能据此停止服务。该逻辑有完整的 JVM 单元测试覆盖ForegroundPromotionTest.kt8 个用例覆盖重复 start 命令、拒绝处理、多次下载只晋升一次、无下载时降级停止等场景。事件关联与取消的细节Android 侧同样以itemIdmetadata 中的 Jellyfin 条目 ID回传事件JS 侧无需维护 taskId 簿记即可关联任务。handleDownloadComplete中有一个边界处理已取消任务在取消后仍完成时最后字节已在取消前读完模块删除已落盘文件避免 JS 已丢弃记录后留下孤儿文件。文件存储位置与权限配置存储位置iOS默认保存到 App 的 Documents 目录文件名取response.suggestedFilename或 URL 最后一段路径组件失败时回退为download_taskIdBackgroundDownloaderModule.swiftAndroid默认使用 JS 传入的FileSystem.documentDirectory对应路径模块要求destinationPath为必填见 BackgroundDownloaderModule.kt。两个平台均可用destinationPath参数指定自定义路径目标目录不存在时原生侧会自动创建目标文件已存在时先删除旧文件再移入新文件。Android 权限自动合并AndroidManifest.xml 声明了模块所需的全部权限随 prebuild 自动合并进应用清单FOREGROUND_SERVICE与FOREGROUND_SERVICE_DATA_SYNC前台服务运行POST_NOTIFICATIONSAndroid 13 下载通知WAKE_LOCK后台保持下载另外INTERNET权限由 Expo 自动添加README 说明。模块还在application中注册了DownloadService声明foregroundServiceTypedataSync、exportedfalse。iOS 后台模式README 说明Info.plist需包含UIBackgroundModes为[audio, fetch]仓库中的 app.json 已通过 Expo 配置写入{ ios: { infoPlist: { UIBackgroundModes: [audio, fetch] } } }注意实际上NSURLSessionbackground session 本身即可在后台继续传输后台模式更多服务于音频播放等场景模块的运行不依赖 App 保持存活。在 Streamyfin 中的实际运用providers/Downloads 调用链模块在仓库中的真实消费者是providers/Downloads下载子系统其架构总览见 providers/Downloads/README.md。整个视频下载流程清晰地展示了上述 API 的实战组合启动流程useDownloadOperations.ts先下载附加资源Trickplay 缩略图、字幕、片头/片尾/广告分段、封面图再生成目标文件名与路径Paths.document下的文件名.mp4随后构建 Live Activity 元数据liveActivity.ts含预翻译的labels与海报文件先持久化 pending 记录再交给原生避免出现传输存在而后续会话无法归账的空窗最后以BackgroundDownloader.enqueueDownload(url, destinationPath, activityMetadata, getJellyfinHeadersForUrl(...))入队——Jellyfin 鉴权与自定义代理头通过第 4 个参数传入事件关联useDownloadEventHandlers.tsJS 侧只按事件中的itemId关联不维护 taskId 映射无itemId的事件如 AudioStorage 的下载直接忽略。完成事件从持久化的 pending 记录结算因此上一个 App 会话入队的下载完成后同样能正确收尾。进度事件中还处理了转码场景——服务器无Content-Length时用码率估算总量计算百分比见 downloadSize.ts 相关实现取消流程运行中任务按record.taskId调cancelDownload仍在原生队列中的按 URL 调cancelQueuedDownload队列串行整个 Jellyfin 媒体下载通过enqueueDownload排队同一时刻只有一个传输在飞iOS 侧队列在进程重启后也能从持久化存储恢复README 中并发下载能力在媒体下载场景下被刻意收敛为串行队列以保证顺序与资源占用可控。此外模块目录还自带一个独立示例 example.ts封装了一个带内部Map跟踪、事件订阅与清理的DownloadManager类适合作为接入参考。构建与集成作为 Expo 原生模块集成后需要重新构建原生工程README Building 章节# iOS npx expo prebuild -p ios npx expo run:ios # Android npx expo prebuild -p android npx expo run:android或手动安装# iOS cd ios pod install cd .. # Android - prebuild 会自动处理 npx expo prebuild -p android模块的 CocoaPods 规格见 BackgroundDownloader.podspeciOS/tvOS 15.0依赖ExpoModulesCore静态框架Android 侧则由 expo-module.config.json 声明 Kotlin 模块类并自动注册。注意事项与平台限制结合 README Notes 与源码实现使用该模块需要了解以下边界iOS 强制退出会中断下载用户从 App 切换器划掉应用时系统可能终止 background session 中的传输非强退Home 键、挂起不影响系统调度自主权iOS 由系统管理传输优先级可能为省电而暂停下载Android 由前台服务 WakeLock 保证持续传输通知表现Android 在通知栏常驻下载进度通知蜂窝网络下载默认允许非 2xx 响应被判定为失败两个平台都会把鉴权失败/404 的错误页拒之门外不会保存成媒体文件headers 不跨进程持久化iOS 侧出于安全考虑代理鉴权头不写入未加密的 App Group 存储见 DownloadTaskStore.swift因此排队任务若跨越进程存活重启后需由 App 重新入队iOS Live Activity 依赖系统状态需要 iOS 16.2且用户未在系统设置中关闭活动的实时活动权限ActivityKit 只在 App 处于前台时允许新建 Activity因此排队场景依赖单 Activity 复用 rebind策略转码下载无总量服务器不返回Content-Length时进度依赖estimatedTotalBytes估算DownloadActivityMetadata字段UI 百分比会随估算修正。赞分享音视频移动开发【免费下载链接】streamyfinA modern Jellyfin client built with Expo项目地址https://gitcode.com/gh_mirrors/st/streamyfin点击查看免费下载相关推荐Streamyfin 下载模块迁移指南从 react-native-background-downloader 到自研 BackgroundDownloader 原生模块Streamyfin 下载模块迁移指南从 react native background downloader 到自研 BackgroundDownloade音视频移动开发Expo Background Fetch 后台任务模块完全指南从安装、原生配置到 Android/iOS 双端实现原理Expo Background Fetch 后台任务模块完全指南从安装、原生配置到 Android/iOS 双端实现原理 本文以 Expo 官方仓库中的 ex移动开发前端跨平台原生移动Streamyfin 离线下载模块深度解析纯函数分层、原生后台下载与冷启动对账机制Streamyfin 离线下载模块深度解析纯函数分层、原生后台下载与冷启动对账机制 本文以 Streamyfin基于 Expo 构建的现代 Jellyfin音视频移动开发上一篇如何快速掌握VimscriptLearn Vimscript the Hard Way第一章实战教程下一篇ComfyUI-Custom-Scripts让AI绘画工作流效率提升3倍的20个实用插件创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考