当前位置:   article > 正文

视频上传-分片上传那点事_视频分片上传

视频分片上传

在上一篇文章中,我们讲解了从视频上传到保存在服务端的整个过程,在这个过程中,我们又细分了前端上传视频的几种方式,前端处理视频的几种方式,在前后端通信过程中需要注意的哪些点等等。有不清楚的小伙伴可以看看 上篇文章

紧接上文,我们来讲下文件的分片上传

我们都知道分片上传是为了提升文件保存的速度。那它是如何实现的呢?下面的流程图会是一个很好的解释:

在这里插入图片描述

接下来,我们一步步的拆解。

前端如何进行分割操作

在上篇文章我们知道,通过 Element.files属性 拿到的是FileList集合。FileList集合由File对象组成。File对象又继承Blob对象。所以File对象可以使用slice方法来完成对文件对象的切割。

slice方法具体明细如下:

含义:Blob.slice() 方法用于创建一个包含源 Blob的指定字节范围内的数据的新 Blob 对象。
返回值:一个新的 Blob 对象,它包含了原始 Blob 对象的某一个段的数据。
参数:3个参数,分别如下:

  • start。第一个会被拷贝进新的 Blob 的字节的起始位置。
  • end。这个下标的对应的字节将会是被拷贝进新的Blob 的最后一个字节。
  • contentType。给新的 Blob 赋予一个新的文档类型。这将会把它的 type 属性设为被传入的值。它的默认值是一个空的字符串。

说了一大堆理论,该是实战了,先说一下思路:

  • 先通过input标签上传文件。
  • 上传文件后,会触发input标签的change事件,在这个事件里,通过event.target.files可以获取到上传的文件对象,并且将它保存在state里。
  • 定义每个文件块的大小,然后使用slice进行分割。

代码如下:

class Video extends React.Component {
    constructor(props){
        super(props);
        this.state = {
            fileObj: {}
        }
    }
    
    // 分片上传
    uploadChunkFile = async () => {
        // 定义每块体积大小为20MB
        let chunk_size = 20 * 1024 * 1024;
        // 获取上传的文件对象
        let fileObj = this.state.fileObj;
        // 获取上传的文件对象的体积
        let allSize = this.state.fileObj.size;
        // 获取文件对应的总的分片的数量
        let allChunkCount = Math.ceil(allSize / chunk_size);
        // chunk文件集合
        let chunkArr = [];
        for (let index = 0; index < allChunkCount; index++){
            let startIndex = index * chunk_size;
            let endIndex = Math.min(startIndex + chunk_size, allSize);
            chunkArr.push({
                data: fileObj.slice(
                    index * chunk_size,
                    endIndex
                ),
                filename: `chunk-${index}`,
                chunkIndex: index
            });
        }
    }
    
    // 上传文件触发
    inputChange = async (event) => {
        let self = this;
        let uploadFileObj = event.target.files[0] || {};
        this.setState(state => {
            return {
                ...state,
                fileObj: uploadFileObj
            }
        });
        return
    }
    
    render(){
        return <div>
            <button onClick={this.testConnect}>测试连接</button>
            <input
                type='file'
                onChange={(event) => this.inputChange(event)}
            />
            <button onClick={this.uploadChunkFile}>分片上传</button
        </div>
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

当我们上传一个66M的视频时,我们会发现,总的分片数量是4。符合预期。

发送chunk的几种方式

这块无非就2种,分别如下:

  • 将这些分片按照顺序发送给后端。
  • 将这些分片并发的方式发送给后端。这种方式下,需要考虑浏览器一次只能并发6个请求的情况,并且这种方式也是面试中高频考点(如何控制并发)。

在这个功能点里,我们采用按顺序的方式上传分片,因为这种方式是最直观的,会了这种方式,相信分片上传你就完全会了。

但是在实际的项目中,更多的还是并发的场景(我们下篇文章再讲)。

按照顺序发送

这个就是第一个请求成功后,再去发送第二个请求,以此类推…

它也是面试中的一个常考点:如何按照顺序发送请求?如何实现红绿灯效果?等等。

按顺序发送,2种思路,一种是循环,一种是递归

递归这里不用说,重点讲一下循环。

普通的for循环可以做到吗?

答案是可以的。

let arr = [
    {name: 1},
    {name: 2},
    {name: 3},
    {name: 4},
    {name: 5}
];

async function ax(){
    for (let index = 0; index < arr.length; index++){
        let result = await new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(arr[index].name);
            }, 1000);
        });
        console.log('result:', result);
    }
}

ax();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
for…in… 能做到吗?

答案也是可以的

let arr = [
    {name: 1},
    {name: 2},
    {name: 3},
    {name: 4},
    {name: 5}
];

async function ax(){
    for (let index in arr){
        let result = await new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(arr[index].name);
            }, 1000);
        });
        console.log('result:', result);
    }

}

ax();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
for…of…能做到吗?

答案也是可以的

let arr = [
    {name: 1},
    {name: 2},
    {name: 3},
    {name: 4},
    {name: 5}
];

async function ax(){
    for (let index of arr){
        let result = await new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(index.name);
            }, 1000);
        });
        console.log('result:', result);
    }

}

ax();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
forEach可以做到吗?

不行,绝对不行

let arr = [
    {name: 1},
    {name: 2},
    {name: 3},
    {name: 4},
    {name: 5}
];

async function ax(){
    arr.forEach(async item => {
        let result = await new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(item.name);
            }, 1000);
        });
        console.log('result:', result);
    });
}

ax();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

在这里插入图片描述

MDN上也是这么说的,但是你要问具体原因,那就只能看forEach源码了。我感觉啊,forEach应该是个while循环实现的,外层的函数是个同步函数,所以导致forEach不能按照顺序发送Promise请求。

forEach伪代码如下:

Array.prototype.myForEach = function (cb){
    let originArr = this;
    let index = 0;
    while(index < originArr.length){
        cb(originArr[index], index);
    }
}

/**
    即使cb内部是异步操作,但是cb外面的调用方不是异步的,所以导致这种写法并不能按顺序发送Promise请求。
*/

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
while循环可以做到吗?

答案是可以的

let arr = [
    {name: 1},
    {name: 2},
    {name: 3},
    {name: 4},
    {name: 5}
];

async function ax(){
    let index = 0;
    while(index < arr.length){
        let result = await new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(arr[index].name);
            }, 1000);
        });
        index++;
        console.log('result:', result);
    }

}

ax();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
数组里哪些方法能做到?

这个就要看数组方法的源码了,但是分析过程跟forEach一样,这里就不一一例举了。

代码实践

在上面的分割章节里,我们讲解了File对象的分割,我们继续在原方法里进行改造,从而添加按顺序发送chunk块的需求。


// 分片上传请求
uploadChunkReq = async (fileBlob, chunkIndex, type) => {
    let formData = new FormData();

    let result = await axiosInstance.post(
        '/video/uploadChunk',
        {
            chunkIndex,
            type,
            videoDict: fileBlob,
        },
        {
            headers: {
                'Content-Type': 'multipart/form-data'
            }
        }
    );
    return result;
}


// 分片上传动作
uploadChunkFile = async () => {
    // 定义每块体积大小为20MB
    let chunk_size = 20 * 1024 * 1024;
    // 获取上传的文件对象
    let fileObj = this.state.fileObj;
    // 获取上传的文件对象的体积
    let allSize = this.state.fileObj.size;
    // 获取文件对应的总的分片的数量
    let allChunkCount = Math.ceil(allSize / chunk_size);
    // chunk文件集合
    let chunkArr = [];
    for (let index = 0; index < allChunkCount; index++){
        let startIndex = index * chunk_size;
        let endIndex = Math.min(startIndex + chunk_size, allSize);
        /**
            每个分片信息都包含:分片的数据、分片的编号、分片的名称
        */
        chunkArr.push({
            data: fileObj.slice(
                index * chunk_size,
                endIndex
            ),
            filename: `chunk-${index}`,
            chunkIndex: index
        });
    }
    
    // 按顺序发送chunk分片
    for (let item of chunkArr){
        let result = await this.uploadChunkReq(item.data, item.chunkIndex, 'chunk');
        console.log('分片上传的结果:', result);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

后端如何合并chunk

主要是3件事,

首先要新增一个接口,用来保存分片数据;

其次当所有的分片都保存成功了,应该去合并分片最终形成文件;合并chunk的时机可以是后端自己判断,也可以是前端触发,具体要看场景

最后,删除分片数据。

单独保存chunk

这里我们需要改造原有的方法,看过上一篇的小伙伴都知道,如果express是通过multer第三方库来解析的form-data数据,那它就一定会经过multer里定义的中间件,我们要在这里去将不同类型的文件存放到不同的文件夹里。

// 定义chunk的临时存放路径
var tempChunkPosition = multer({
  // dest: 'tempChunk'
  storage: multer.diskStorage({
    destination: function (req, file, cb) {
      if (req.body.type == 'chunk'){
        // 说明是分片上传
        cb(null, path.join(__dirname, '../tempChunk'));
      } else {
        // 说明上传的是小文件
        cb(null, path.join(__dirname, '../videoDest'));
      }
    },
    filename: function (req, file, cb) {
      if (req.body.type == 'chunk'){
        // 如果是分片上传
        cb(null, file.fieldname + '-' + `${req.body.chunkIndex}` + '-' + Date.now());
      } else {
        // 说明上传的是小文件
        cb(null, file.fieldname + '-' + Date.now() + '.mp4');
      }
    }
  })
});

/ 上传切片
router.post('/uploadChunk', tempChunkPosition.single("videoDict"),(req, res, next) => {

  return res.send({
    success: true,
    msg: '上传成功'
  });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

合并chunk

这一步就是将临时的分片数据全都读取出来,然后依次将他们写入到文件中。

因为我们在上传分片的过程中,已经将分片的标识索引传给了后端,所以后端无需再对读出来的chunk集合进行顺序排序。

router.post('/mergeChunk',(req, res, next) => {

  // 获取文件切片的路径
  let chunkPath = path.join(__dirname, '../tempChunk');

  // 开始读取切片
  const chunkArr = fs.readdirSync(chunkPath);

  chunkArr.forEach(file => {
    fs.appendFileSync(
      path.join(__dirname, `../videoDest/${req.body.originFileName}.mp4`),
      fs.readFileSync(`${chunkPath}/${file}`)
    );
  });
 
  return res.send({
    success: true,
    msg: '文件合并成功'
  });
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

此时我们再对前端的上传分片的函数进行改造,主要就是新增 “合并分片”的动作触发。


// 合并分片请求
mergeChunk = async () => {
    let result = await axiosInstance.post(
        '/video/mergeChunk',
        {
            originFileName: '11'
        },
        {
            headers: {
                'Content-Type': 'application/json'
            }
        }
    );
    return result;
}

//分片上传
uploadChunkFile = async () => {
    // 前面的都不变......
    for (let item of chunkArr){
        let result = await this.uploadChunkReq(item.data, item.chunkIndex, 'chunk');
        console.log('分片上传的结果:', result);
    }
    // 前面的都不变......

    // 合并请求(新增的++++++++++++++)
    this.mergeChunk();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

到这一步,我们的分片上传的流程就已经全部打通了,此时大家上传文件后,就会看到后端的目录里不仅有分片数据,而且还有完整的视频文件。

敬请期待

我们这次讲解了文件的分片上传,但是整体跑下来你会发现,有点太顺风顺水,没有包含错误机制,所以下篇文章,我们不仅会将如何并发控制分片的上传,还会有上传过程中的错误控制。

最后

好啦,本篇文章到这里就结束啦,如果上述过程中有错误的地方,欢迎各位大神指出。希望我的文章对你有帮助,我们下期再见啦~~

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号