視頻播放、斷點續傳、多線程下載實現基礎:Range

實現一個視頻播放的功能,以及對大文件的下載操做等等都避不開一個點:獲取文件任意位置的數據,若是說咱們單純的經過 echo file-content 的方式只能用於文件下載,若是視頻文件用於播放中,則難以處理,具體表現則爲視頻播放的時候沒法調整進度條,並且若是是視頻網站,對於視頻只採用放在某個能夠直接訪問的目錄上,那麼這個視頻也就至關於公開了,對於什麼 VIP 什麼的也就無從提及,本篇文章將 Range,來提供視頻播放、斷點續傳、多線程下載的技術依賴實現php

Range

HTTP協議中,支持以 Range 的形式指定獲取資源的特定偏移的數據,語法格式以下,具體參考 Range: MDNhtml

Range: <unit>=<range-start>-
Range: <unit>=<range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>
  • <unit> 只能是 bytes (目前來講),指定單位
  • <range-start> 一個整數,表示在特定單位下,範圍的起始值。
  • <range-end> 一個整數,表示在特定單位下,範圍的結束值。這個值是可選的,若是不存在,表示此範圍一直延伸到文檔結束。

如: 獲取 0-100 字節的數據和120到結尾的數據git

Range: bytes=0-100,120-

Content-Range

該頭部指定了響應的數據的內容範圍,語法格式以下:github

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

說明:瀏覽器

  • <unit> 數據區間所採用的單位。一般是字節(bytes)。
  • <range-start> 一個整數,表示在給定單位下,區間的起始值。
  • <range-end> 一個整數,表示在給定單位下,區間的結束值。
  • <size> 整個文件的大小(若是大小未知則用 "*" 表示)

例如:服務器

Content-Range: bytes 200-1000/67589

多Range響應

目測在網絡上面的都沒有說到,可是HTTP協議支持多Range,具體返回內容信息格式以下:網絡

GET http://suda.dev.dx/file HTTP/1.1
Host: suda.dev.dx
Connection: keep-alive
Accept-Encoding: identity;q=1, *;q=0
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3679.0 Safari/537.36
Accept: */*
Referer: http://test.dev.dx/video.html
Accept-Language: zh-CN,zh;q=0.9
Cookie: php_session=8eec314af63d994c2eeb1baca7487332
Range: bytes=0-1,2-3


HTTP/1.1 206 Partial Content
Date: Sun, 10 Mar 2019 09:36:59 GMT
Server: Apache/2.4.23 (Win32) OpenSSL/1.0.2j mod_fcgid/2.3.9
X-Powered-By: PHP/7.2.1
Accept-Ranges: bytes
Content-Length: 220
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: multipart/byteranges; boundary=multiple_range_ss6bBSB6IlLi0YPpP8rK3g==

--multiple_range_ss6bBSB6IlLi0YPpP8rK3g==
Content-Type: video/mp4
Content-Range: bytes 0-1/132006090

<...somedata...>
--multiple_range_ss6bBSB6IlLi0YPpP8rK3g==
Content-Type: video/mp4
Content-Range: bytes 2-3/132006090

<...somedata...>

Accept-Rangs

服務器響應,告訴瀏覽器是否支持 Range,session

語法:多線程

Accept-Ranges: bytes
Accept-Ranges: none
  • none
    不支持任何範圍請求單位,因爲其等同於沒有返回此頭部,所以不多使用。不過一些瀏覽器,好比IE9,會依據該頭部去禁用或者移除下載管理器的暫停按鈕。
  • bytes
    範圍請求的單位是 bytes (字節)

實現代碼

本實現代碼能夠簡單理解爲僞代碼,部分依賴沒有給出,Swoole 環境下修改一下便可使用。app

使用代碼:

<?php

namespace suda\welcome\response;

use suda\framework\Request;
use suda\framework\Response;
use suda\application\processor\RequestProcessor;
use suda\application\processor\FileRangeProccessor;

class FileResponse implements RequestProcessor
{
    public function onRequest(Request $request, Response $response)
    {
        $filename = 'G:\視頻\刺客伍六七.2018\EP01.mp4';
        $processor = new FileRangeProccessor($filename);
        $processor->onRequest($request, $response);
    }
}

依賴代碼:

<?php
namespace suda\application\processor;

use SplFileObject;
use suda\framework\Request;
use suda\framework\Response;
use suda\framework\response\MimeType;
use suda\framework\http\stream\DataStream;
use suda\application\processor\RequestProcessor;

/**
 * 響應
 */
class FileRangeProccessor implements RequestProcessor
{
    /**
     * 文件路徑
     *
     * @var SplFileObject
     */
    protected $file;

    /**
     * MIME
     *
     * @var string
     */
    protected $mime;

    public function __construct($file)
    {
        $this->file = $file instanceof SplFileObject? $file : new SplFileObject($file);
        $this->mime = MimeType::getMimeType($this->file->getExtension());
    }

    /**
     * 處理文件請求
     *
     * @param \suda\framework\Request $request
     * @param \suda\framework\Response $response
     * @return void
     */
    public function onRequest(Request $request, Response $response)
    {
        $ranges = $this->getRanges($request);
        $response->setHeader('accept-ranges', 'bytes');
        if ($ranges === false || $request->getMethod() !== 'GET') {
            $response->status(400);
        } elseif ($ranges === null) {
            $response->sendFile($this->file->getRealPath());
        } elseif (count($ranges) === 1) {
            $response->status(206);
            $range = $ranges[0];
            $response->setHeader('content-type', $this->mime);
            $response->setHeader('content-range', $this->getRangeHeader($range));
            $this->sendFileByRange($response, $range);
        } else {
            $response->status(206);
            $this->sendMultipleFileByRange($response, $ranges);
        }
    }

    /**
     * 發送多Range
     *
     * @param \suda\framework\Response $response
     * @param array $ranges
     * @return void
     */
    protected function sendMultipleFileByRange(Response $response, array $ranges)
    {
        $separates = 'multiple_range_'.base64_encode(\md5(\uniqid(), true));
        $response->setHeader('content-type', 'multipart/byteranges; boundary='.$separates);
        foreach ($ranges as $range) {
            $response->write('--'.$separates."\r\n");
            $this->sendMultipleRangePart($response, $range);
            $this->sendFileByRange($response, $range);
            $response->write("\r\n");
        }
    }


    /**
     * 發送範圍數據
     *
     * @param \suda\framework\Response $response
     * @param array $range
     * @return void
     */
    protected function sendFileByRange(Response $response, array $range)
    {
        $response->write(new DataStream($this->file->getRealPath(), $range['start'], $range['end'] -  $range['start'] + 1));
    }

    /**
     * 獲取Range描述
     *
     * @param \suda\framework\Request $request
     * @return array|bool|null
     */
    protected function getRanges(Request $request)
    {
        $ranges = $this->parseRangeHeader($request);
        if (\is_array($ranges)) {
            return $this->parseRanges($ranges);
        } elseif ($ranges === false) {
            return false;
        }
        return null;
    }

    /**
     * 寫Range頭
     *
     * @param \suda\framework\Response $response
     * @param array $range
     * @return void
     */
    protected function sendMultipleRangePart(Response $response, array $range)
    {
        $response->write('Content-Type: '.$this->mime."\r\n");
        $response->write('Content-Range: '.$this->getRangeHeader($range) ."\r\n\r\n");
    }

    /**
     * 生成Range頭
     *
     * @param array $range
     * @return string
     */
    protected function getRangeHeader(array $range):string
    {
        return sprintf('bytes %d-%d/%d', $range['start'], $range['end'], $this->file->getSize());
    }

    /**
     * 獲取Range描述
     *
     * @param \suda\framework\Request $request
     * @return array|bool|null
     */
    protected function parseRangeHeader(Request $request)
    {
        $range = $request->getHeader('range', null);
        if (is_string($range)) {
            $range = trim($range);
            if (\strpos($range, 'bytes=') !== 0) {
                return false;
            }
            $rangesFrom = \substr($range, strlen('bytes='));
            return \explode(',', $rangesFrom);
        }
        return null;
    }
    
    /**
     * 處理範圍
     *
     * @param array $ranges
     * @return array|bool
     */
    protected function parseRanges(array $ranges)
    {
        $range = [];
        foreach ($ranges as  $value) {
            if (($r = $this->parseRange($value)) !== null) {
                $range[] = $r;
            } else {
                return false;
            }
        }
        return $range;
    }

    /**
     * 處理Range
     *
     * @param string $range
     * @return array
     */
    protected function parseRange(string $range):?array
    {
        $range = trim($range);
        if (strrpos($range, '-') === strlen($range) - 1) {
            return [
                'start' => intval(\rtrim($range, '-')),
                'end' => $this->file->getSize() - 1,
            ];
        } elseif (\strpos($range, '-') !== false) {
            list($start, $end) = \explode('-', $range, 2);
            return ['start' => intval($start) , 'end' => intval($end) ];
        }
        return null;
    }
}

參考文獻

  1. https://tools.ietf.org/html/r...
  2. https://tools.ietf.org/html/r...
  3. https://developer.mozilla.org...
  4. https://developer.mozilla.org...
  5. 完整代碼
相關文章
相關標籤/搜索