更新记录
1.1.7(2025-03-27)
新增多任务下载的参数功能,支持改变重试次数,未来可能应需求继续增加
1.1.6.5(2025-03-26)
处理readme.md
说明
1.1.6(2025-03-26)
新增多任务下载功能(MulM3u82Mp4
),可以接受多个链接/本地文件和多个保存路径;修复progress获取一次后无法获取的bug;调整readme.md,新增测试页面。
平台兼容性
Vue2 | Vue3 |
---|---|
√ | √ |
App | 快应用 | 微信小程序 | 支付宝小程序 | 百度小程序 | 字节小程序 | QQ小程序 |
---|---|---|---|---|---|---|
HBuilderX 4.53,Android:5.0,iOS:不支持,HarmonyNext:不支持 | × | × | × | × | × | × |
钉钉小程序 | 快手小程序 | 飞书小程序 | 京东小程序 | 鸿蒙元服务 |
---|---|---|---|---|
× | × | × | × | × |
H5-Safari | Android Browser | 微信浏览器(Android) | QQ浏览器(Android) | Chrome | IE | Edge | Firefox | PC-Safari |
---|---|---|---|---|---|---|---|---|
× | × | × | × | × | × | × | × | × |
1.1.7版起多任务下载函数参数发生变化,更新时请注意!
使用前须知
本插件在uni-app x
和uni-app
原生项目上测试通过,请注意支持导入的HBuilder
版本。如存在使用插件出错情况,欢迎联系我反馈。
插件引入
import { MulM3u82Mp4,M3u82Mp4, CurrentProgress, clearCache, onCurrentProgressChange } from '@/uni_modules/cfit-m3u8'
插件使用
使用参数
M3u82Mp4(url:string,savepath:string,filePath:callback) //url:传入的链接;savepath:期望保存的文件/目录;filePath:最终保存的路径
MulM3u82Mp4(urls:array<string>, paths:array<string>,options:object,filePath:callback)//传入两个数组,确保数组长度相同,传入options为选项
CurrentProgress(progress:callback)
onCurrentProgressChange(progress:callback) //二者相同,保留CurrentProgress仅为向下兼容,后续版本可能废弃
clearCache()
savepath
(期望保存的文件/目录)说明:
支持多种模式。以下通过示例来演示。假设应用沙箱下载目录为/storage/emulated/0/Android/data/io.dcloud.uniappx/files/Download/
。
""
:传为空,保存为/storage/emulated/0/Android/data/io.dcloud.uniappx/files/Download/video_{timestamp}.mp4
,timestamp
为插件生成的时间戳。"video.mp4"
:只传入文件名(必须.mp4
结尾,否则将视为目录),保存为/storage/emulated/0/Android/data/io.dcloud.uniappx/files/Download/video.mp4
。"/storage/emulated/0/DCIM/Camera"
:只传为目录(须确保存在),保存为/storage/emulated/0/DCIM/Camera/video_{timestamp}.mp4
,timestamp
为插件生成的时间戳。"/storage/emulated/0/DCIM/Camera/video.mp4"
:传为目录+文件名(必须.mp4
结尾,否则将视为目录),保存为/storage/emulated/0/DCIM/Camera/video.mp4
。
远程m3u8合成
//下载m3u8文件合成mp4,参数二为保存路径
M3u82Mp4("https://cfitsec.cn/monologue/Monologue.m3u8","/storage/emulated/0/DCIM/Camera",(filePath) => {
console.log('下载完成:', filePath)
uni.showToast({ title: '保存至: ' + filePath, icon: 'none' })
})
//第二个参数不写则默认保存至应用沙箱内的下载目录
M3u82Mp4("https://cfitsec.cn/monologue/Monologue.m3u8","",(filePath) => {
console.log('下载完成:', filePath)
uni.showToast({ title: '保存至: ' + filePath, icon: 'none' })
})
//获取实时的下载进度
CurrentProgress((progress) => {
console.log("Progress:", progress)
})
//获取实时的下载进度
onCurrentProgressChange((progress) => {
console.log("Progress:", progress)
})
/**
* 1.1.7版目前支持这两个参数
* 传入的options若为空,则默认重试且重试3次
* 传入的time参数若无效则默认重试3次,超出范围按边界计
**/
var options={
retry:true, //是否失败重试
time:3 //重试次数,最多30次,若retry传入不是true则该参数无效
}
//多个m3u8下载
MulM3u82Mp4(["https://cfitsec.cn/monologue/Monologue.m3u8"],["/storage/emulated/0/DCIM/Camera"],options,(filePath) => {
console.log('下载完成:', filePath)
uni.showToast({ title: '保存至: ' + filePath, icon: 'none' })
})
下载成功会返回形如/storage/emulated/0/Android/data/io.dcloud.uniappx/files/Download/video_1742051459918.mp4
的文件路径,失败则统一返回null
,可手动判断成功与否。
本地m3u8合成
也可以传入本地m3u8文件,使用file://
开头即可。
示例:
//参数二为保存路径
M3u82Mp4('file:///storage/emulated/0/Download/monologue/monologue.m3u8',"",(filePath) => {
console.log('下载完成:', filePath)
uni.showToast({ title: '保存至: ' + filePath, icon: 'none' })
})
//MulM3u82Mp4同上,仅参数变化。
//获取实时的下载进度
CurrentProgress((progress) => {
console.log("Progress:", progress)
})
//获取实时的下载进度
onCurrentProgressChange((progress) => {
console.log("Progress:", progress)
})
其中/storage/emulated/0/Download/monologue/monologue.m3u8
为实际本地路径名。
清除下载缓存
示例:
clearCache(); //引入后可以直接调用
下载中不推荐启动本函数,可能导致下载出错(示例中下载时禁用了缓存清除按钮)
注意事项
- 升级到
1.1.2
以后原先代码出错:查看文档用法变化; - 运行时报错
No such file or directory
:确保传入的filepath
的目录部分存在且可写; - 确保下载过程中未运行
clearCache
函数,否则会影响下载的ts
文件; - 保存路径参数须保证保存路径存在且可读,使用插件前须开启权限;
- 本地
m3u8
的合并需确保读写权限开启; - 运行时报错
open failed: EACCES (Permission denied)
:读取外部路径文件权限未开启。 - 在本地合成带
AES128
加密的ts
文件时出现如上错误,一般是key
被拒绝读取,如果不想开权限,则可以修改key的拓展名例如ts等等(我要不知道为啥,但是这样可以过),以及修改m3u8
的指向key
路径。 uni-app x
在本地合成UC
浏览器等的m3u8
文件如果报错,一般是因为读取ts
文件没有拓展名导致的拒绝访问。此时可以手动修改ts
文件拓展名,或者开启更多文件权限,或者改成在uniapp
项目运行。- 保存到相册如果结尾带拓展名
.mp4.ts
,是uni.saveVideoToPhotosAlbum
保存时添加的,本插件保存的拓展名为正常的mp4
。实际生产环境可以换成其他保存文件的代码。该错误仅在uniapp x
出现,uniapp
正常。 - 示例代码为
uniappx
语言,uniapp
可以自行引用插件测试。 -
MulM3u82Mp4
在某下载任务下载失败后会进行重试,若依旧失败则返回null
,说明失败,下面的示例中任务二为失败示例示例代码
可部署插件后测试如下代码,注意需要在
pages.json
预先注册/pages/index/index
和/pages/index/mul
:<!--index.nvue--> <template> <view class="content"> <input class="input" v-model="url" placeholder="Enter M3u8 URL. . ." confirm-type="send" /> <input class="input" v-model="savepath" placeholder="Enter Save Path. . ." confirm-type="send" /> <button class="action-btn" @click="display">展示/隐藏视频</button> <view v-if="isshow" class="video-container"> <video class="video-player" :src="url"></video> <text class="url-text">{{url}}</text> </view> <button class="action-btn" @click="clear" :disabled="start">清除下载缓存</button> <button class="action-btn" @click="download" :disabled="start">下载文件</button> <navigator url="/pages/index/mul" hover-class="navigator-hover"> <button class="action-btn" style="width:270px">↗多m3u8文件下载</button> </navigator> </view> <view class="progress-box" v-if="start"> <progress :percent="progress" active-mode="forwards" :active="true" :border-radius="10" show-info :font-size="16" :stroke-width="3" active-color="#ff8877" class="progress" /> </view> <view class="content"> <text class="url-text" v-if="path!=''">下载的mp4路径:{{ path }}</text> <button class="action-btn" v-if="path!=''" @click="open">保存到相册</button> </view> </template> <script> // 引入插件 import { M3u82Mp4, CurrentProgress, clearCache, onCurrentProgressChange } from '@/uni_modules/cfit-m3u8' export default { data() { return { url: 'https://cfitsec.cn/monologue/Monologue.m3u8', isshow: false, path: "", interv: 0, start: false, progress: 0, savepath: "/storage/emulated/0/DCIM/Camera" } }, onLoad() { }, methods: { display() { this.isshow = !this.isshow; }, download() { this.progress = 0; var that = this; uni.showLoading({ title: '下载中. . .' }) this.start = true; M3u82Mp4( that.url, that.savepath, (filePath) => { this.start = false; uni.hideLoading(); try { clearInterval(that.interv) } catch (_) { } console.log('下载完成:', filePath) uni.showToast({ title: '保存至: ' + filePath, icon: 'none' }) that.path = filePath; } ) // CurrentProgress((progress) => { // console.log("Progress:", progress) // that.progress = progress; // }) onCurrentProgressChange((progress) => { console.log("Progress:", progress) that.progress = progress; }) }, clear() { clearCache(); uni.showToast({ position: "center", icon: "none", title: "缓存已清除" }); }, open() { uni.saveVideoToPhotosAlbum({ filePath: this.path,// success: (e) => { uni.showToast({ position: "center", icon: "none", title: "视频保存成功,请到手机相册查看" }); }, fail: (_) => { uni.showToast({ position: "center", icon: "none", title: "保存失败" }); } }); } } } </script> <style> .content { display: flex; flex-direction: column; align-items: center; padding: 40rpx 30rpx; background-color: #f8f8f8; } .input { padding: 20px; margin: 20px; height: 60px; border: 1px solid #e5e5e5; border-radius: 35rpx; background: #fff; } .progress{ width: 85%; left: 7.5%; } .action-btn { width: 80%; height: 80rpx; line-height: 80rpx; border-radius: 40rpx; background: #007AFF; color: white; font-size: 32rpx; margin-bottom: 30rpx; box-shadow: 0 4rpx 12rpx rgba(0, 122, 255, 0.3); } .download-btn { background: #34C759; box-shadow: 0 4rpx 12rpx rgba(52, 199, 89, 0.3); } .video-container { width: 100%; margin: 40rpx 0; background: white; border-radius: 16rpx; padding: 20rpx; box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.08); } .video-player { width: 100%; height: 400rpx; border-radius: 12rpx; overflow: hidden; background: black; } .url-text { margin-top: 30rpx; padding: 20rpx; background: #f4f4f4; border-radius: 8rpx; font-size: 26rpx; line-height: 1.6; color: #0066ff; } .progress-box { height: 25px; margin-bottom: 30px; } </style>
<!--mul.vue--> <template> <view class="container"> <button class="action-btn" @click="start">开始下载</button> <scroll-view class="task-list"> <view v-for="(task, index) in tasks" :key="index" class="task-card"> <text class="task-title">任务 {{ index + 1 }}</text> <view class="info-item"> <text class="label">URL:</text> <text class="value url">{{ task['url'] }}</text> </view> <view class="info-item"> <text class="label">保存路径:</text> <text class="value">{{ task['savePath'] }}</text> </view> <view class="status-line"> <text :class="['status', statusClass[task['status']!]]">{{ statusText[task['status']!] }}</text> </view> <progress v-if="task['status'] === 'downloading'" :percent="task['progress']" active-mode="forwards" :active="true" :border-radius="10" show-info :font-size="16" :stroke-width="3" active-color="#ff8877" class="progress" /> <view class="result-item" v-if="task['resultPath']"> <text class="label">输出路径:</text> <text class="value">{{ task['resultPath'] }}</text> </view> </view> </scroll-view> </view> </template> <script> import { MulM3u82Mp4, clearCache, onCurrentProgressChange } from '@/uni_modules/cfit-m3u8' export default { data() { return { tasks: [{ url: 'https://cfitsec.cn/monologue/Monologue.m3u8', savePath: '/sdcard/DCIM', status: 'pending', resultPath: '', progress: 0 }, { url: 'https://cfitsec.cn/monologue/Monologue.m3u8', savePath: '/sdcard/DCI/videos01.mp4', status: 'pending', resultPath: '', progress: 0 }, { url: 'https://cfitsec.cn/monologue/Monologue.m3u8', savePath: 'output.mp4', status: 'pending', resultPath: '', progress: 0 }], index: 0, statusText: { downloading: '下载中', success: '成功', failed: '失败', pending: '等待中' }, statusClass: { downloading: 'status-downloading', success: 'status-success', failed: 'status-failed', pending: 'status-pending' } } }, onLoad() { }, methods: { start() { this.index = 0 this.startDownload() const urls = this.tasks.map(t => t['url'].toString()) onCurrentProgressChange((progress) => { console.log(progress) if (this.index >= urls.length) { this.index = urls.length - 1; } const taskIndex = this.index this.tasks[taskIndex]['progress'] = Math.floor(progress) }) }, async startDownload() { const urls = this.tasks.map(t => t['url'].toString()) const paths = this.tasks.map(t => t['savePath'].toString()) this.tasks[this.index]['status'] = 'downloading' if (this.index >= urls.length) { this.index = urls.length - 1; } var options = { retry: true, //是否错误重试 time: 3 //错误重试次数,仅retry开启true后有效 }; MulM3u82Mp4(urls, paths, options, (path) => { if (this.index >= urls.length) { this.index = urls.length - 1; } const task = this.tasks[this.index]; if (path != 'null') { this.index++; if (this.index >= urls.length) { this.index = urls.length - 1; } this.tasks[this.index]['status'] = 'downloading' task['status'] = 'success' task['resultPath'] = path } else { this.index++; if (this.index >= urls.length) { this.index = urls.length - 1; } this.tasks[this.index]['status'] = 'downloading' task['status'] = 'failed' } }) } } } </script> <style lang="scss"> .container { padding: 20rpx; background-color: #f5f5f5; } .task-list {} .task-card { background: #ffffff; border-radius: 12rpx; padding: 24rpx; margin-bottom: 24rpx; box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); } .task-title { font-size: 22rpx; font-weight: bold; color: #333; margin-bottom: 16rpx; } .info-item, .status-line, .result-item { display: flex; flex-wrap: wrap; margin: 8rpx 0; } .label { color: #666; font-size: 28rpx; min-width: 120rpx; } .value { flex: 1; color: #333; font-size: 28rpx; } .url { color: #007AFF; } .status { font-size: 28rpx; margin-right: 20rpx; &-downloading { color: #007AFF; } &-success { color: #09BE4F; } &-failed { color: #FF2B2B; } &-pending { color: #666; } } .action-btn { width: 100%; height: 80rpx; line-height: 80rpx; border-radius: 40rpx; background: #007AFF; color: white; font-size: 32rpx; margin-bottom: 30rpx; box-shadow: 0 4rpx 12rpx rgba(0, 122, 255, 0.3); } .progress { color: #007AFF; font-size: 28rpx; } .progress-bar { margin-top: 16rpx; } .result-item { margin-top: 16rpx; padding-top: 16rpx; border-top: 1rpx solid #eee; } </style>