更新记录

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 xuni-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}.mp4timestamp为插件生成的时间戳。
  • "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}.mp4timestamp为插件生成的时间戳。
  • "/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>

隐私、权限声明

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

android.permission.READ_EXTERNAL_STORAGE android.permission.WRITE_EXTERNAL_STORAGE android.permission.INTERNET android.permission.ACCESS_NETWORK_STATE

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

插件不采集任何数据

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

使用中有什么不明白的地方,就向插件作者提问吧~ 我要提问