更新记录

1.0.1(2026-06-30)

新增ios android 获取设备存储空间大小的功能

1.0.0(2026-06-30)

新增ios android m3u8视频下载,http 服务方便下载后播放,支持key 在http服务器里边解密进行播放


平台兼容性

uni-app(5.14)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
- - - - - - -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - - -

uni-app x(5.14)

Chrome Safari Android iOS 鸿蒙 微信小程序
- - 5.0 13 - -

M3U8 视频下载与本地播放(puke-m3u8-downloader)

UTS 插件,App 端(Android / iOS)下载 M3U8 视频(key + ts 分片),并通过本地 HTTP 服务离线播放,支持加密 key 在服务端解密、防盗播。

适用于在线教育、网课、点播等需要在 App 内「下载到本地、无网络也能看」的场景。双端原生实现,性能与稳定性均经过线上项目验证。


✨ 特性

  • 双端原生:Android(NanoHTTPD)+ iOS(GCDWebServer)两套原生实现,体验一致。
  • 完整下载链路:解析 M3U8 → 下载 key + ts 分片 → 重写 playlist → 本地播放,一条龙。
  • 本地 HTTP 服务离线播放:下载完成后启动本地 HTTP 服务,把本地 m3u8 喂给系统播放器即可播放。
  • 加密 key 服务端解密(防盗播):对 32 字节的加密 key,在 HTTP 服务返回时取奇数索引位还原成 16 字节 AES key,磁盘上的原始 key 文件不外泄,防止用户拷贝 ts 直接播放。
  • 断点续传:已下载的 ts 分片自动跳过;App 被杀掉重启后,用原 m3u8 地址再次 startDownload 即可续传,不会重复下载。
  • 完整传输控制:开始 / 批量开始 / 暂停 / 继续 / 取消 / 删除。
  • 全局进度监听onProgress 回调返回状态、百分比、已下载 / 总字节数、错误信息,内置节流,避免高频刷 JS 桥导致卡顿。
  • 并发控制:最多 2 个视频并行下载,单视频内顺序下载分片,对移动端网络友好。
  • 协议兼容:兼容 master playlist(自动选最高码率变体)、手动跟随 HTTP→HTTPS 重定向。
  • 安全降级:非 App 平台通过 js_sdk 包装层安全降级,isSupport() 返回 false,调用不崩溃,方便同一套代码多端编译。

📱 支持平台

本插件为 uni-app-x 专用 UTS 插件(旧版 uni-app / 5+App 不支持)。

平台 是否支持 说明
uni-app-x · App-Android minVersion 21
uni-app-x · App-iOS minVersion 13
H5 / 各家小程序 / 快应用 受限于本地文件与本地 HTTP 服务能力,仅 App 端可用

引擎要求:HBuilderX ^3.6.8、uni-app-x ^5.14

小程序 / H5 端调用时会安全降级,不会报错崩溃,可在业务层用 isSupport() 判断后再展示「下载」入口。

🚀 快速开始

插件是 UTS 插件,不要用 uni.requireNativePlugin(那是 5+App 纯原生插件的加载方式,会报「当前运行的基座不包含原生插件」)。直接 import js_sdk 即可,HBuilderX 编译 App 时会按平台解析 utssdk 实现,随标准基座 / 正式包一起编译,无需制作自定义基座。

import m3u8Downloader from '@/uni_modules/puke-m3u8-downloader/js_sdk'

最小可用三步:

  1. App 启动时启动本地 HTTP 服务(推荐在 App.vueonLaunch 调用,幂等);
  2. 注册一次全局进度监听 onProgress(贯穿 App 生命周期);
  3. 调用 startDownload 开始下载。

📖 完整使用示例

<script setup>
// #ifdef APP-PLUS
import m3u8Downloader from '@/uni_modules/puke-m3u8-downloader/js_sdk'
// #endif

// 进度回调(全局只注册一次,建议放在 App.vue)
const onDownloadProgress = (res) => {
    // res: { courseVideoId, status, progress, downloadedBytes, totalBytes, error? }
    console.log(res.courseVideoId, res.status, res.progress + '%')
}

onLoad(() => {
    // #ifdef APP-PLUS
    // 1. 启动本地 HTTP 服务(默认端口 19685,可省略 port)
    if (m3u8Downloader.isSupport()) {
        m3u8Downloader.startLocalServer({ port: 19685 })
    }
    // 2. 注册进度监听
    m3u8Downloader.onProgress(onDownloadProgress)
    // #endif
})

// 开始下载单个视频
const startDownload = () => {
    // #ifdef APP-PLUS
    m3u8Downloader.startDownload({
        courseVideoId: 'video_001',          // 视频唯一 ID,作为本地缓存目录名
        m3u8Url: 'https://example.com/index.m3u8', // M3U8 播放地址
        title: '第一课 · 课程导学'             // 可选,仅用于日志/状态展示
    })
    // #endif
}

// 批量下载
const startBatchDownload = () => {
    // #ifdef APP-PLUS
    m3u8Downloader.startBatchDownload({
        videos: [
            { courseVideoId: 'video_001', m3u8Url: 'https://example.com/1/index.m3u8', title: '第一课' },
            { courseVideoId: 'video_002', m3u8Url: 'https://example.com/2/index.m3u8', title: '第二课' }
        ]
    })
    // #endif
}

// 下载完成后,取本地播放地址喂给 <video> 播放
const playLocal = (courseVideoId) => {
    // #ifdef APP-PLUS
    m3u8Downloader.getLocalPlayUrl({
        courseVideoId,
        success: (res) => {
            // res.url 形如:http://127.0.0.1:19685/video_001/playlist.m3u8
            // 直接把 res.url 赋值给 video 的 src 即可离线播放
            console.log('本地播放地址:', res.url)
        },
        fail: (err) => {
            console.error(err.errMsg) // 例:本地视频不存在
        }
    })
    // #endif
}

onUnload(() => {
    // #ifdef APP-PLUS
    m3u8Downloader.offProgress(onDownloadProgress) // 移除指定回调;传 null 移除全部
    // 如不再需要本地播放,可停掉服务(一般 App 退出由系统回收,无需手动停)
    // m3u8Downloader.stopLocalServer()
    // #endif
})
</script>

📘 API 文档

所有方法在非 App 端均为安全降级(空操作或回调默认值),不会抛错。

isSupport()

当前平台是否支持下载 / 本地播放。App 端返回 true,其他平台返回 false

startLocalServer(options)

启动本地 HTTP 服务(用于本地 M3U8 播放)。

  • options.port?:服务端口,默认 19685。已启动则幂等返回。

必须先启动本地服务,下载与播放才可用。推荐在 App.vueonLaunch 调用。

stopLocalServer()

停止本地 HTTP 服务。

startDownload(options)

开始下载单个视频。

  • options.courseVideoId:视频唯一 ID,作为本地缓存目录名(String)。
  • options.m3u8Url:完整的 M3U8 播放地址(String)。
  • options.title?:视频标题,仅用于日志 / 状态展示。

startBatchDownload(options)

开始批量下载,内部逐个调用 startDownload,受全局并发控制(最多 2 个并行)。

  • options.videosStartDownloadOptions[] 列表。

pauseDownload(options) / resumeDownload(options)

  • options.courseVideoId:暂停 / 继续指定视频下载。

cancelDownload(options)

  • options.courseVideoId:取消下载(不删本地文件,已下载分片保留)。

deleteDownload(options)

  • options.courseVideoId:删除该视频的本地缓存目录与任务记录。

getDownloadStatus(options)

查询下载状态。

  • options.courseVideoId
  • options.success(res)res = { status, progress, downloadedBytes?, totalBytes? }
  • options.fail(err) / options.complete(res)

getLocalPlayUrl(options)

获取本地播放地址(下载完成后调用)。

  • options.courseVideoId
  • options.success(res)res = { url },形如 http://127.0.0.1:19685/<courseVideoId>/playlist.m3u8
  • options.fail(err):本地视频不存在等错误,err.errMsg
  • options.complete(res)

getStorageInfo(options)

获取离线缓存所在卷的可用 / 总空间,便于下载前做容量判断。

  • options.success(res)res = { availableBytes, totalBytes }
  • options.fail(err) / options.complete(res)

getCachedSize(options)

获取单个课程视频缓存目录的实际占用大小。

  • options.courseVideoId
  • options.success(res)res = { courseVideoId, bytes }
  • options.fail(err) / options.complete(res)

onProgress(callback) / offProgress(callback)

注册 / 移除全局下载进度监听。

  • offProgressnull 移除全部监听。

status 状态枚举

含义
none 无任务 / 已取消
pending 已加入队列,等待下载
downloading 下载中
paused 已暂停
completed 下载完成
failed 下载失败

DownloadProgress 回调字段

字段 类型 说明
courseVideoId string 视频唯一 ID
status string 见上表状态枚举
progress number 进度 0–100
downloadedBytes number 已下载字节数
totalBytes number 总字节数(注意:为避免阻塞,未做全分片预扫,下载过程中该值可能为 0,仅用于进度展示)
error? string 失败时的错误信息

📁 本地文件目录结构

下载的文件统一存放在应用沙盒内(Android 为外部存储,iOS 为 Documents),根目录名为 school_videos

<应用存储>/school_videos/<courseVideoId>/
├── playlist.m3u8     # 重写后的本地 playlist,ts 行已替换成本地文件名,key 的 URI 替换为 "key"
├── segment_000.ts    # ts 分片,序号固定 3 位补零(padIndex)
├── segment_001.ts
├── ...
└── key               # 加密 key 原始文件(32 或 16 字节,磁盘上保持原样不外泄)

本地播放 URL 形如:http://127.0.0.1:19685/<courseVideoId>/playlist.m3u8

本地 HTTP 服务在返回 key 时会按规则解密:若磁盘 key 为 32 字节,取奇数索引位 raw[1,3,...,31] 还原成 16 字节 AES key 返回给播放器;16 字节则原样返回。磁盘文件始终不变,防盗播。

⚠️ 注意事项 / FAQ

  • 仅支持 App 端:小程序 / H5 不支持本地文件与本地 HTTP 服务,请用 isSupport() 判断后再展示下载入口。
  • 必须先 startLocalServer:下载与本地播放都依赖该服务,推荐在 App.vueonLaunch 启动(幂等,可重复调用)。
  • onProgress 全局只注册一次:建议放在 App 生命周期内,避免页面切换时重复注册导致回调多次触发。
  • App 被杀后任务表丢失:原生任务表是内存态,App 被系统杀死后丢失。重启后用原 m3u8Url 再次 startDownload 即可续传(已下载的 ts 分片会自动跳过)。
  • 自定义端口startLocalServer({ port })getLocalPlayUrl 内部默认使用同一端口(19685),自定义端口请保持一致,否则取到的播放地址会无法访问。
  • 字段类型courseVideoId / m3u8Url / title 必须是字符串。js_sdk 已做一层 String() 防护,但直接调用 UTS 时请确保类型正确,避免 as! 强转导致的 native crash。
  • key 解密规则需与服务端对齐:默认按 32 字节取奇数索引位得 16 字节 AES key。若你的加密方案不同,需修改两端 readKeyBytesForPlayer / 等价实现。

🔗 开发参考

📝 更新日志

详见 changelog.md

  • 1.0.0(2026-06-30):新增 iOS / Android M3U8 视频下载,内置 HTTP 服务方便下载后播放,支持 key 在 HTTP 服务里解密进行播放。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。