用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

相關文章
相關標籤/搜索