编程语言
首页 > 编程语言> > 在 MacBook 上使用 Python 作实况视讯串流

在 MacBook 上使用 Python 作实况视讯串流

作者:互联网

本文是山姆锅在学习实况视讯串流 (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 作为基础的串流协定,主要的有:

本文选择使用 HLS 纯粹是因为山姆锅比较熟悉。

测试环境

运行流程

程序共分成发布端 (publisher)、串流端(streamer) 以及回放端 (player) 三个部分,回放端使用的是 OSX 内建的 Safari 浏览器,所以我们只需要有发布端跟串流端即可。

基本流程说明如下:

  1. 发布端即时从镜头撷取影像,转码 (encode) 成串流需要的编码与格式(MPEG2 TS)后通知串流端有新的区块(segment);
  2. 串流端根据收到的视讯区块动态产生串流中介数据档(metadata);
  3. 回放端则依照中介数据档来决定该回放的区块。

串流端

串流端在正式系统需要使用其它的伺服软件,如 Nginx。因为只是验证,这里山姆锅使用 Gevent + Bottle 来作为串流端的技术推叠(technology stack)。

为了要完成 HLS 串流工作,串流端需提供两种数据给回放端:

  1. 串流中介数据
    HLS 的中介数据以 m3u8 格式,content type 为: application/x-mpegURL
  2. 媒体区段数据
    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

其中,

关于 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()

其中,

发布端

从实践的角度,发布端其实比较麻烦,由于山姆锅希望使用实况的视讯来源, 自然把脑筋动到 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()

使用 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