🎬 第八章 FFmpeg命令行实现流媒体功能及直播应用
📄 一句话概括

拖进度条=发一个带 Range 的请求,服务器回 206 加一段字节。 服务器要会三件事,少一件就拖不动。

📝 本节和前面几节的关系

8.3.2 对比了两种服务器的整体行为,这一节把「拖进度条」这一件事拆到底。 它牵扯的是服务器那三个能力,不是播放器的问题。

一、拖一下,发生了什么

从文件中间开始要一段,实测结果:

项 值


文件总字节 48741 请求的起点 24370 响应码 206 Partial Content Content-Range bytes 24370-48740/48741 实际收到体 24371 字节

起点 24370 是「文件一半」,收到 24371 字节,两个数加起来正好 48741。 一个字节没多要,也一个字节没少给。

Content-Range 那一行的斜杠后面是总长,客户端靠它算进度条的比例。 没有这一行,它只知道「拿到了一段」,不知道「这一段在整体里的哪」。

二、服务器要会的三件事

这三件事缺一件,进度条就拖不动:

能力 不做的后果


认 Range 头 永远从头开始,拖了也白拖 回 206 与 Content-Range 客户端不知道这段是哪一段 回 Accept-Ranges: bytes 客户端不敢发 Range

第三件最容易被忽略:Accept-Ranges: bytes 是个「广告」。 它告诉客户端「我支持分段,你尽管要」。不发的话, 保守的客户端会默认「这服务器不支持」,于是老老实实从头下。

VLC 当 HTTP 服务器时会发这个头,所以浏览器与播放器都敢拖。 Python 自带的 http.server 从 3.7 起支持 Range, 但它不发 Accept-Ranges——这就是「能用」与「敢用」的差别。

顺带提醒:Range 还分单段与多段。 bytes=0-99,200-299 这种多段请求要回 multipart/byteranges, 手写服务器基本都不支持,实践中也很少有人用。

三、直播为什么拖不动

因为直播的「后半段」还没生成。 服务器手里没有那段字节,给不出来。

客户端要什么 服务器能怎么做


往后的部分 等它生成,现在没有 往前的部分 可能已经扔了(直播不留档) 当前这一段 给,这是唯一能给的

所以直播播放器的进度条都是灰的。 这不是产品设计,是协议层面就没有那段时间的数据。

想「回看」就必须有人把流录下来(也就是 DVR)—— 一旦录了,它就不是纯直播了,而是「直播加一段存储」, 那段存储可以按 Range 读,于是进度条又活了。

这就是为什么有些直播能拖回五分钟前:后端存了一份, 播放器拖的时候读的是存储,不是直播流。

四、VLC 当服务器的两条命令

VLC 当 HTTP 服务器有两条常用命令:

vlc s7.mp4 --sout="#std{access=http,mux=ts,dst=:8080}"

vlc s7.mp4 --sout="#http{mux=ffmpeg{mux=flv},dst=:8080/live.flv}"

命令 出来的东西 适合


access=http mux=ts 一路 TS 简单、能播 http mux=flv 一路 FLV 延迟更低

第一条最省事:浏览器与 ffplay 都能直接播这个地址。 第二条走 FLV,延迟能压到一两秒,代价是有些平台不认。

注意这两条都是「推」型:VLC 一直在发,观众连上来从头看还是从中间看, 取决于它是不是把已发出的数据留在缓冲里。 真直播一般不留,所以也是「从当下开始」。

五、动手 Demo:从中间要一段

下面这段代码手写一个带 Range 的请求,从文件一半开始要, 量响应码、Content-Range 与实际收到的字节数。

import http.server, os, socket, threading

class H(http.server.BaseHTTPRequestHandler):
    u"""最小 HTTP 服务:只认 Range,回 206 加 Content-Range。"""

    def log_message(self, *a):
        pass

    def do_GET(self):
        p = os.path.join(os.getcwd(), "s7.mp4")
        n = os.path.getsize(p)
        rng = self.headers.get("Range")
        start, code, cnt = 0, 200, n
        if rng:
            start = int(rng.split("=")[1].split("-")[0] or 0)
            code, cnt = 206, n - start
        self.send_response(code)
        self.send_header("Content-Length", str(cnt))
        if code == 206:
            self.send_header("Content-Range",
                             "bytes %d-%d/%d" % (start, n - 1, n))
        self.end_headers()
        with open(p, "rb") as f:
            f.seek(start)
            self.wfile.write(f.read(cnt))

srv = http.server.ThreadingHTTPServer(("127.0.0.1", 8000), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()

size = 48741
start = size // 2
req = (u"GET /s7.mp4 HTTP/1.1\r\n"
       u"Host: x\r\n"
       u"Range: bytes=%d-\r\n\r\n" % start)

s = socket.create_connection(("127.0.0.1", 8000), 3.0)
s.sendall(req.encode())
buf = b""
while True:
    d = s.recv(65535)
    if not d:
        break
    buf += d
s.close()

head, body = buf.split(b"\r\n\r\n", 1)
print(head.decode("utf-8", "replace").split("\r\n")[0])
print(u"收到体 %d 字节,起点 %d,总长 %d" % (len(body), start, size))
print(u"加起来 = %d" % (len(body) + start))

srv.shutdown()
srv.server_close()

实跑输出:

--------------------------------------------------------------------
8.3.3 VLC 作为 HTTP 流媒体服务器
--------------------------------------------------------------------

**① 拖进度条 = 发一个带 Range 的请求**

  项                值
  ---------------------------------------------
  文件总字节        48741
  请求的起点        24370
  响应码            206 Partial Content
  Content-Range     bytes 24370-48740/48741
  实际收到体        24371 字节

**② 服务器要会三件事才叫「能拖进度条」**

  能力                        不会的后果
  --------------------------------------------------------
  认 Range 头                 永远从头开始,拖了也白拖
  回 206 与 Content-Range     客户端不知道这段是哪一段
  回 Accept-Ranges: bytes     客户端不敢发 Range

**VLC 当 HTTP 服务器时会回 `Accept-Ranges: bytes`,所以浏览器/播放器才敢拖。**
**Python 那个 `http.server` 从 3.7 起也支持 Range,但它不发 `Accept-Ranges`。**

**③ 直播流为什么不能拖**

**因为直播的「后半段」还没生成,服务器没有那段字节可以给。**
**硬要拖,只能给一个 416(范围不满足),或者干脆忽略。**

**所以所有直播播放器的进度条都是「不可拖」的——**
**这不是产品设计,是协议层面就没有那段时间的数据。**

起点 24370、收到 24371 字节,加起来正好 48741——一个字节不差。 这就是 Range 的语义:给你一段,长度与起止都由你指定。

如果服务器不支持 Range,这条请求也会得到 200 加整个文件。 那时收到的体是 48741 字节而不是 24371——客户端要能分辨这两种。

判断方法就是看状态码:206 是分段,200 是整份。 播放器看到 200 就知道「拖不了」,于是把进度条变灰。

30 秒实验

  1. 只要一个字节:Range: bytes=100-100,看是不是回 206 加 1 字节。
  2. 越界请求:Range: bytes=99999-,看回 416 还是 200。
  3. 多段请求:试 bytes=0-9,20-29,看服务器怎么处理。

常见误区

  1. 以为支持 Range 就够了。还要发 Accept-Ranges,客户端才敢用。
  2. 以为直播能拖回去。没存储就没那段数据。
  3. 把多段 Range 当常规操作。手写服务器基本都不支持。
自测三问(点开对答案) 1. 拖进度条在协议上是什么? —— 发一个带 Range 头的 GET,服务器回 206 加那一段字节。 2. Accept-Ranges 有什么用? —— 它是广告,告诉客户端「我支持分段」,客户端才敢发 Range。 3. 为什么有些直播能拖回五分钟? —— 后端录了一份,拖的时候读的是存储而不是直播流。

相关笔记