用koa2处理音视频文件

当你使用手机畅快的看着视频、听着音乐,这时,你有想过这些东西是怎么传输到你的手机上的么?html

此次,就让咱们以nodejskoa2为例,来一个大揭秘!node

咱们以mp3类型的音频为例子: 下图就是一个http请求mp3文件,git

  1. Request Headers中有个Range: bytes=0-,Range表明指示服务器应该返回文件的哪一或哪几部分。end是一个整数(如:Range: bytes=0-136868),表示在特定单位下,范围的结束值。这个值是可选的,若是不存在,表示此范围一直延伸到文档结束。
  2. 假如在响应中存在 Accept-Ranges 首部(而且它的值不为 “none”),那么表示该服务器支持范围请求。 Accept-Ranges: bytes 表示界定范围的单位是 bytes 。这里 Content-Length它提供了要检索的文件的完整大小。
  3. Response Headers中的,Content-Length 首部如今用来表示先前请求范围的大小(而不是整个文件的大小)。Content-Range 响应首部则表示这一部份内容在整个资源中所处的位置。 对于以上的解释能够参考:HTTP请求范围

1. 了解完基础知识,就到了nodejs登场的时候。

首先介绍两个咱们最经常使用的两个模块fs【文件系统】path【模块提供用于处理文件路径和目录路径的实用工具】 ,咱们以koa为例进行介绍github

引用的写法以下:web

const fs = require('fs')
const path = require('path')
复制代码

2.音视频文件的类型

从上图中能够看出,在Response HeadersContent-Type: audio/mpeg,而经常使用的音视频格式有mp三、mp四、webm、ogg、ogv、flv、wav等,在HTTP中返回的Content-Type各不相同,整理以下:api

const mime = {
    'mp4': 'video/mp4',
    'webm': 'video/webm',
    'ogg': 'application/ogg',
    'ogv': 'video/ogg',
    'mpg': 'video/mepg',
    'flv': 'flv-application/octet-stream',
    'mp3': 'audio/mpeg',
    'wav': 'audio/x-wav'
}
复制代码

3.判断请求文件类型

每次在客户端进行访问的时候,咱们首先须要肯定请求文件的类型,所以,咱们还须要以下的一个纯函数:数组

let getContentType = (type) => {
    if (mine[type]) {
        return mine[type]
    } else {
        reutrn null
    }
}
复制代码

4.读取文件

有了上面的准备咱们就能够开始读取相应的文件,并返回给客户端了。bash

let readFile = async(ctx, options) => {
    // 咱们先确认客户端请求的文件的长度范围
    let match = ctx.request.header['range']
    // 获取文件的后缀名
    let ext = path.extname(ctx.path).toLocaleLowerCase()
    // 获取文件在磁盘上的路径
    let diskPath = decodeURI(path.resolve(options.root + ctx.path))
    // 获取文件的开始位置和结束位置
    let bytes = match.split('=')[1]
    // 有了文件路径以后,咱们就能够来读取文件啦
    let stats = fs.statSync(diskPath)
    // 在返回文件以前,咱们还要知道获取文件的范围(获取读取文件的开始位置和开始位置)
    let start = Number.parseInt(bytes.split('-')[0]) // 开始位置
    let end   = Number.parseInt(bytes.split('-')[1]) || (stats.size - 1) // 结束位置
    // 若是是文件类型
    if (stats.isFile()) {
        reture new Promise((resolve, reject) => {
            // 读取所须要的文件
            let stream = fs.createReadStream(diskPath, {start: start, end: end})
            // 监听 ‘close’当读取完成时,将stream销毁
            ctx.res.on('close', function () {
                stream.distory()
            })
            // 设置 Response Headers
            ctx.set('Content-Range': `bytes ${start}-${end}/${stats.size}`)
            ctx.set('Accept-Range', `bytes`)
            // 返回状态码
            ctx.status = 206
            // getContentType上场了,设置返回的Content-Type
            ctx.type = getContentType(ext.replace('.','')
            stream.on('open', function(length) {
                if (ctx.res.socket.writeable) {
                    try {
                        stream.pipe(ctx.res)
                    } catch (e) {
                        stream.destroy()
                    }
                } else {
                    stream.destroy()
                }
            })
            stream.on('error', function(err) {
                 if (ctx.res.socket.writable) {
                    try {
                        ctx.body = err
                    } catch (e) {
                        stream.destroy()
                    }
                }
                reject()
            })
            // 传输完成
            stream.on('end', function () {
                resolve()
            })
        })
    }
}
复制代码

5.导出文件

此时咱们还须要将方法导出去,方便使用服务器

module.exports = function (opts) {
    // 设置默认值
    let options = Object.assign({}, {
        extMatch: ['.mp4', '.flv', '.webm', '.ogv', '.mpg', '.wav', '.ogg'],
        root: process.cwd()
    }, opts)
    
    return async (ctx, next) => {
        // 获取文件的后缀名
        let ext = path.extname(ctx.path).toLocaleLowerCase()
        // 判断用户传入的extMath是否为数组类型,且访问的文件是否在此数组之中
        let isMatchArr = options.extMatch instanceof Array && options.extMatch.indexOf(ext) > -1
        // 判断用户传输的extMath是否为正则类型,且请求的文件路径包含相应的关键字
        let isMatchReg = options.extMatch instanceof RegExp && options.extMatch.test(ctx.path)
        if (isMatchArr || isMatchReg) {
            if (ctx.request.header && ctx.request.header['range']) {
                // readFile 上场
                return await readFile(ctx, options)
            }
        }
        await next()
    }
}
复制代码

6.在app.js中使用

终于来到了咱们在项目中使用的关键时刻app

const Koa = require('koa')
const app = new Koa()
app.use(koaMedia({
  extMatch: /\.mp[3-4]$/i
}))
复制代码

这样咱们就完成了从客户端请求到服务端返回的所有过程。

关于中间件原理能够看个人这篇文章nodejs中koa2中间件原理分析

注:使用到的API

1. Content-Range

Content-Range: <unit> <range-start>-<range-end>/<size>

  1. <unit> 数据区间所采用的单位。一般是字节(byte)。
  2. <range-start> 一个整数,表示在给定单位下,区间的起始值。
  3. <range-end> 一个整数,表示在给定单位下,区间的结束值。
  4. <size> 整个文件的大小(若是大小未知则用"*"表示)。

2. fs.stat

fs.stat用于检查文件是否存在,读取文件状态

3. fs.statSync

fs.statSync同步的stat,返回stats类

4. stats.isFile()

stats.isFile()判断获取的对象是否为常规文件,是则返回true

5. stats.size

stats.size获取文件大小(以字节为单位)

6. path.extname

path.extname方法返回 path 的扩展名,从最后一次出现 .(句点)字符到 path最后一部分的字符串结束。 若是在 path 的最后一部分中没有 . ,或者若是 path 的基本名称(参阅 path.basename())除了第一个字符之外没有 .,则返回空字符串。

7. fs.createReadStream

fs.createReadStream,参数option能够包括 startend 值,以从文件中读取必定范围的字节而不是整个文件。start 和 end 都包含在内并从 0 开始计数,容许的值在 [0, Number.MAX_SAFE_INTEGER] 的范围内。若是指定了 fd 而且省略 start 或为 undefined,则 fs.createReadStream() 从当前的文件位置开始顺序地读取。 encoding 能够是 Buffer 接受的任何一种字符编码。

特别鸣谢:koa-video

相关文章
相关标签/搜索