在 MacBook 上使用 Python 作实况视讯串流
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了在 MacBook 上使用 Python 作实况视讯串流,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含10425字,纯文字阅读大概需要15分钟。
内容图文
本文是山姆锅在学习实况视讯串流 (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/
内容总结
以上是互联网集市为您收集整理的在 MacBook 上使用 Python 作实况视讯串流全部内容,希望文章能够帮你解决在 MacBook 上使用 Python 作实况视讯串流所遇到的程序开发问题。
如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
来源:【匿名】