本文是山姆锅在学习实况视讯串流 (live video streaming) 过程,用来验证概念(proof of concept) 的纪录。透过 MacBook 内建的镜头作为视讯源,并借由 HTTP Live Streaming (HLS) 协定作实况串流。 虽说是实况,但因为采用 HLS 协定,先天上就会有延迟的。实验的结果不算太成功,本来只能使用桌面环境的 Safari 浏览器来观看视讯,经过高手指正后,现在手机版的也可以了。
何谓 HTTP Live Streaming (HLS)?
HLS 是苹果公司制定,以 HTTP
协定为基础的媒体串流协定,可以支持随选 (Video-on-Demand; VOD) 以及
实况 (live) 模式。其它同样使用 HTTP 作为基础的串流协定,主要的有:
- Adobe HTTP Dynamic Streaming (HDS)
- Microsoft Smooth Streaming (MSS)
- MPEG-DASH
本文选择使用 HLS 纯粹是因为山姆锅比较熟悉。
测试环境
- 主机: MacBook Pro
- OS: OSX 10.10
- CPU: X86-64
- Python: 2.7.10
运行流程
程序共分成发布端 (publisher)、串流端(streamer) 以及回放端 (player) 三个部分,回放端使用的是
OSX 内建的 Safari 浏览器,所以我们只需要有发布端跟串流端即可。
基本流程说明如下:
- 发布端即时从镜头撷取影像,转码 (encode) 成串流需要的编码与格式(MPEG2
TS)后通知串流端有新的区块(segment);
- 串流端根据收到的视讯区块动态产生串流中介数据档(metadata);
- 回放端则依照中介数据档来决定该回放的区块。
串流端
串流端在正式系统需要使用其它的伺服软件,如
Nginx。因为只是验证,这里山姆锅使用 Gevent + Bottle
来作为串流端的技术推叠(technology stack)。
为了要完成 HLS 串流工作,串流端需提供两种数据给回放端:
-
- 串流中介数据
- HLS 的中介数据以 m3u8 格式,content type 为:
application/x-mpegURL
-
- 媒体区段数据
- HLS 的区段须以 MPEG2 TS 格式存放,每个区段一个文件,通常副文件名为
.ts, content type: video/mp2t
底下简单说明串流中介数据,首先看一段实际的内容:
1 2 3 4 5 6 7 8
|
#EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:3 #EXT-X-MEDIA-SEQUENCE:28 #EXTINF:2.250000, http://127.0.0.1:8080/live/out028.ts #EXTINF:1.500000, http://127.0.0.1:8080/live/out029.ts
|
其中,
-
- #EXTM3U
- 让回放端知道中介数据是以扩充版的 M3U 格式撰写。
-
- #EXT-X-VERSION:3
- 指定此中介数据格式的版本,不支持此版本的回放端无法解读。
-
- #EXT-X-TARGETDURATION:3
- 指定串流中,此叙述之后的视讯区段最长的秒数。本文每个区段接近 2
秒,所以这里指定 3 秒。
-
- #EXT-X-MEDIA-SEQUENCE:58
- 指定中介数据中的第一个区块在整个串流中的序号,没有这个叙述则默认为
0。
因为是实况串流,区块会不断持续产生,如果保留所有过往的区块数据,除了浪费带宽跟性能外,
最终也会导致程序挂点。所以,需要以滚动窗口(rolling
window)的方式,只保留最近的区块。
-
- #EXTINF:1.500000
- 每个区块之前都需要有这个声明,其中
1.50000 是此区块的时间长度(以秒为单位)。
这个声明之后的下一行必须是区块文件的
URL 位址,让回放端知道要如何以及去何处撷取区块数据。
-
- #EXT-X-ENDLIST
- 如果是实况串流,了解以上的声明就足够,但对于随选视讯,需要这个声明让回放端知道中介数据结束。
也就是说,只要这个声明没有出现,回放端会假设是实况串流。
关于 HLS 的近一步资讯可以参考
规格文档
。
底下是串流端主要的程序内容(已删减):
streamer.pyview raw
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
|
from __future__ import absolute_import, print_function
import os from collections import deque
from gevent import monkey; monkey.patch_all() from bottle import route, run, static_file, request, response, hook
WEBROOT = os.path.abspath('./webroot')
LIVE_MEDIA_FOLDER = os.path.join(WEBROOT, 'live')
ROLLING_WINDOW = 10 playlist = deque(maxlen=ROLLING_WINDOW)
published_segments = 0
def (): response.set_header('Cache-Control', 'no-cache, no-store, must-revalidate') response.set_header('Pragma', 'no-cache') response.set_header('Expires', '0')
@route('/stream.m3u8') def live_stream_meta(): global playlist global published_segments
print("Serve playlist") response.content_type = 'application/x-mpegURL'
result = list() result.append('#EXTM3Un') result.append('#EXT-X-VERSION:3n') result.append('#EXT-X-TARGETDURATION:3n')
if len(playlist) == 0: result.append('#EXT-X-MEDIA-SEQUENCE:0n') else: sequence = playlist[0][2] result.append('#EXT-X-MEDIA-SEQUENCE:%dn' % sequence)
for name, duration, sequence in playlist: result.append('#EXTINF:%s,n' % duration) result.append('/live/%sn' % name) # result.append('#EXT-X-ENDLIST') print(result) return result
@route('/live/<filename>') def live_stream_data(filename): print("Serve stream data:", filename) response.content_type = 'video/mp2t' in_file = os.path.join(LIVE_MEDIA_FOLDER, filename) with open(in_file) as f: return f.read()
@route('/publish/<filename:path>/<duration>') def publish(filename, duration): global playlist global published_segments playlist.append((filename, duration, published_segments)) print("Published segment:(%s, %s)" % (filename, duration)) published_segments += 1
def main(): run(host='0.0.0.0', port=8080, server='gevent')
if __name__ == '__main__': main()
|
其中,
-
- live_stream_meta
- 用来提供回放端需要的串流中介数据。
-
- live_stream_data
- 用来提供媒体区块数据给回放端。
-
- publish
- 让发布端通知有新的区块产生,发布端须提供文件名以及区块时间长度。
发布端
从实践的角度,发布端其实比较麻烦,由于山姆锅希望使用实况的视讯来源,
自然把脑筋动到 MacBook 内建的镜头身上;另外需要将影像转码成 HLS
串流可以接受的格式 (MPEG2 TS),一开始还真的不知道如何着手。
针对转码的部分有评估过 GStreamer(因为 Kivy 好像有使用),但对于要如何组合
pipeline 还真的没有概念,跳过。说到视讯转码,另外的候选当然是鼎鼎大名的
ffmpeg 了!但问题是要使用 哪个 Python
的绑定(binding)?过程就省略,反正最后选择 PyAV
这个程序库,如果您有其它更好的选择,请不吝指教。
再来就是影像撷取的问题:一开始还在想 GStreamer, OpenCV 怎么作?后来发现
ffmpeg 就有支持,幸运的是 PyAV 也有提供相关范例:
1
|
source = av.open(format='avfoundation', file='0')
|
其中,`av` 是 PyAV 的套件名称。当然这个只适用在 OSX 环境。
底下是发布端的程序:
publisher.pyview raw
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
|
# -*- coding: utf-8 -*- from __future__ import absolute_import, print_function
import os import time import urllib2 import av import threading from Queue import Queue
OUTPUT_FOLDER = os.path.abspath('./webroot/live')
interrupted = False
class SegmentSubmitter(threading.Thread):
def __init__(self): super(SegmentSubmitter, self).__init__() self.queue = Queue() self.setDaemon(True)
def put_item(self, item): self.queue.put_nowait(item)
def run(self): print("Submitter started.") while True: item = self.queue.get() if len(item) == 0: break
print("Submitting %s" % item[0]) url = 'http://127.0.0.1:8080/publish/%s/%f' % item try: content = urllib2.urlopen(url=url).read() except urllib2.URLError: pass
def gen_segment(filename, source, bit_rate=1000000, vcodec='h264', pix_fmt='yuv420p', frame_rate=20, duration=2): global interrupted
out_filename = os.path.join(OUTPUT_FOLDER, filename) output = av.open(out_filename, 'w')
outs = output.add_stream(vcodec, str(frame_rate)) outs.bit_rate = bit_rate outs.pix_fmt = pix_fmt outs.width = 640 outs.height = 480 secs_per_frame = 1.0 / frame_rate frame_count = 0 segment_start_time = time.time()
while True: start_time = time.time() packet = source.next()
for frame in packet.decode(): frame.pts = None out_packet = outs.encode(frame) frame_count += 1 if out_packet: output.mux(out_packet)
if (time.time() - segment_start_time) > duration: break
time_to_wait = start_time + secs_per_frame - time.time() if time_to_wait > 0: try: time.sleep(time_to_wait) except KeyboardInterrupt: interrupted = True break
while True: out_packet = outs.encode() if out_packet: frame_count += 1 output.mux(out_packet) else: break
output.close()
segment_duration = time.time() - segment_start_time return segment_duration, frame_count
def publish(source): global interrupted
num_segments = 0 submitter = SegmentSubmitter() submitter.start()
stream = next(s for s in source.streams if s.type == 'video') it = source.demux(stream)
while not interrupted: filename = 'seg-%d.ts' % num_segments print("Generating segment: %s" % filename) num_segments += 1 duration, frame_count = gen_segment(filename, it) print("Segment generated: (%s, %f, %d)" % (filename, duration, frame_count)) submitter.put_item((filename, duration))
def main(): source = av.open(format='avfoundation', file='0') #source = av.open(file='movie.mp4', 'r')
print("Number of streams in source: %d" % len(source.streams))
publish(source)
if __name__ == '__main__': main()
|
- 共有两个线程在运行,其中一个负责影像撷取并产生区块文件,另一个负责通知串流端有新区块产生。
- 不知道是程序写得没有效率还是怎样,source 的 frame rate
最多只能到每秒 20 帧左右。
- 虽然有根据 frame rate,
来调整撷取的时间间隔以避免影像快转,结果有改善,但似乎还要加强。
使用 Flowplayer 让其它浏览器也可以观看 HLS 串流
除了 Apple 自家的 Safari 外,其它浏览器对于 HLS
的支持上不完整,在这些浏览器需要特别处理。 底下是使用
Flowplayer 的范例:
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
|
<!doctype html>
<head>
<link rel="stylesheet" href="player/skin/functional.css">
<!-- site specific styling --> <style> body { font: 12px "Myriad Pro", "Lucida Grande", sans-serif; text-align: center; padding-top: 5%; } .flowplayer { width: 80%; } </style>
<!-- for video tag based installs flowplayer depends on jQuery 1.7.2+ --> <script src="https://code.jquery.com/jquery-1.11.2.min.js"></script>
<!-- include flowplayer --> <script src="player/flowplayer.min.js"></script>
</head>
<body>
<!-- the player --> <div class="flowplayer" data-swf="/player/flowplayer.swf" data-ratio="0.4167"> <video> <source type="application/x-mpegurl" src="http://127.0.0.1:8080/stream.m3u8"> </video> </div>
</body>
|
实际使用会很卡,由于使用 Safari 也会稍微卡卡的,应该是我的程序问题。
结语
本文提供的范例还有不少坑,真的希望有哪位高人能够指导一下。在过程中,
最大的收获竟然是发现 Nginx (透过插件) 已经可以支持多种串流协定!
参考数据
_`Bottle`: http://bottlepy.org/docs/dev/index.html
_`Gevent`: http://www.gevent.org/
_`PyAV`: https://github.com/mikeboers/PyAV
原文引用 大专栏 https://www.dazhuanlan.com/2019/08/27/5d64b831bf08a/
标签:__,Python,frame,filename,串流,time,区块,MacBook
来源: https://www.cnblogs.com/petewell/p/11418273.html