-
Notifications
You must be signed in to change notification settings - Fork 0
/
JpegRtpStillStream.py
253 lines (207 loc) · 7.71 KB
/
JpegRtpStillStream.py
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
from struct import pack_into, unpack_from, pack, pack_into
from sdp_utils import make_sdp2
from RtpFrameGenerator import RtpPacket, RtpFrameGenerator
from time import time
from JpegFile import JpegFile, serialize_scanlines, ReferenceJpeg, serialize
import logging
logger = logging.getLogger(__name__)
RTP_JPEG_RESTART = 0x40
RTP_PT_JPEG = 26
JPG_HDR_SIZE = 8 # Number of bytes for RTP-JPG header
DRI_SIZE = 4 # Number of bytes for DRI
"""
TODO: Check huffman table inside jpeg. We need to repack it
if it differs from the default one
"""
def load_jpeg_file_as_standard(image_path, quality):
try:
with open(image_path, 'rb') as image_file:
raw_data = image_file.read()
ref_image = ReferenceJpeg(raw_data)
pixels = ref_image.decompress_ref()
out_data = serialize(ref_image, quality)
return out_data
except IOError:
return None
def make_jpeg_data_standard(raw_data, quality):
ref_image = ReferenceJpeg(raw_data)
pixels = ref_image.decompress_ref()
out_data = serialize(ref_image, quality)
return out_data
class RtpJpegEncoder(RtpFrameGenerator):
"""
Encodes Jpeg file to RTP packets
"""
def __init__(self):
super(RtpJpegEncoder, self).__init__()
self.jpeg_TypeSpecific = 0
self.jpeg_Q = 255
self.jpeg_QT_MBZ = 0
self.jpeg_QT_Precision = 0
self.seq = 0
self.timestamp_start = time()
def get_timestamp_90khz(self, stamp=None):
step = 1.0 / 90000.0
if stamp is None:
stamp = time()
delta = int((stamp - self.timestamp_start)*90000)
return delta
def _create_rtp_packet(self):
"""
Constructs RTP packet. It should be filled later
:return:
"""
ssrc = 0
return RtpPacket(payload_type=RTP_PT_JPEG, ssrc=ssrc)
def calc_payload_size(self, jpeg, jpeg_offset, frame_length):
result = 8 # For RTP Jpeg header
if jpeg.dri:
result += 4
# Some space for QT
if self.jpeg_Q > 127 and jpeg_offset == 0:
result += 132
# And some scanline data to the end
result += frame_length
return result
# Makes another RTP frame
def make_rtp_frame_payload(self, jpeg, jpeg_offset, frame_length):
"""
:param jpeg:JpegFile
:param jpeg_offset:int
:param frame_length:int
:return:bytes RTP mjpeg payload
"""
offset = 0
hoffset = (jpeg_offset >> 16) & 0xff
loffset = jpeg_offset & 0xffff
if jpeg.width % 8 != 0 or jpeg.height % 8 != 0:
logger.error("Jpeg image size should be divisible by 8: %dx%d"%(jpeg.width, jpeg.height))
output = bytearray()
width_packed = jpeg.width >> 3
height_packed = jpeg.height >> 3
header = pack('!BBHBBBB',
self.jpeg_TypeSpecific,
hoffset, loffset, jpeg.type, self.jpeg_Q,
width_packed, height_packed)
output += header
offset += len(header)
if len(output) != offset:
raise RuntimeError("Miscalculated offset vs output: %d vs %d" % (offset, len(output)))
if jpeg.reset_interval:
l = 1
h = 1
flags = l & 0x1
flags |= (h << 1) & 0x2
flags |= (0x3fff << 2)
dri = pack('!HH', jpeg.reset_interval, flags)
output += dri
offset += 4
if len(output) != offset:
raise RuntimeError("Miscalculated offset vs output: %d vs %d" % (offset, len(output)))
if self.jpeg_Q > 127 and jpeg_offset == 0:
# Write table
# Write luma x64
# Write chroma x64
qt = bytearray(132)
qt_length = 128
pack_into('!BBH', qt, 0, self.jpeg_QT_MBZ, self.jpeg_QT_Precision, qt_length)
jpeg.write_luma(qt, 4)
jpeg.write_chroma(qt, 68)
output += qt
offset += 132
if len(output) != offset:
raise RuntimeError("Miscalculated offset vs output: %d vs %d" % (offset, len(output)))
# TODO: Should crimp it
max_jpeg_len = len(jpeg.image_data)
next_jpeg_pos = min(max_jpeg_len, jpeg_offset+(frame_length-offset))
output += jpeg.image_data[jpeg_offset:next_jpeg_pos]
jpeg_offset = next_jpeg_pos
return output, jpeg_offset
# Encode to RTP payload stream
def encode_rtp(self, timestamp, jpeg, max_datagram_size):
"""
:param timestamp:Time
:param jpeg:JpegFile parsed Jpeg object
:param max_datagram_size:
:return:
"""
jpeg_offset = 0
total_length = len(jpeg.image_data)
result = []
done = jpeg_offset >= total_length
first = True
while not done:
packet = self._create_rtp_packet()
packet.seqnum = self.seq
packet.timestamp = self.get_timestamp_90khz(timestamp)
frame_length = min(max_datagram_size, total_length-jpeg_offset)
data, jpeg_offset = self.make_rtp_frame_payload(jpeg, jpeg_offset, max_datagram_size)
done = jpeg_offset >= total_length
if done:
packet.marker = 1
# TODO: hide it inside RtpPacket
# We should implement copyless jpeg serialization as well
header_size = packet.calc_header_size()
packet.raw_packet = bytearray(header_size + len(data))
packet.encode_header(packet.raw_packet, 0)
packet.raw_packet[header_size:] = data
self.seq += 1
result.append(packet)
return result
def get_sdp(self, options):
return make_sdp2(options)
class RtpJpegFileStream(RtpJpegEncoder):
"""
RTP Stream that sends a single jpeg frame
"""
def __init__(self, path, packet_size=1000):
"""
:param path:string path to jpeg file
:param packet_size:int desired RTP packet size
"""
super(RtpJpegFileStream, self).__init__()
self._jpeg = JpegFile()
self._path = path
# Encoded RTP frames
self._frames = []
self._packet_size = packet_size
self._generator = None
self._quality=80
self.read_data()
def get_sdp(self, options):
options['width'] = self._jpeg.width
options['height'] = self._jpeg.height
return super(RtpJpegFileStream, self).get_sdp(options)
def read_data(self):
logger.info("Opening jpeg file %s"%self._path)
file = open(self._path, 'rb')
raw_data_base = file.read()
file.close()
logger.info("Starting JPEG decoding")
raw_data = make_jpeg_data_standard(raw_data_base, self._quality)
if raw_data is None:
raise IOError("Failed to open the file %s" % self._path)
logger.info("Done JPEG decoding")
#logger.debug("Loaded %d bytes" % len(raw_data))
self._jpeg.load_data(raw_data)
timestamp = time()
self._frames = self.encode_rtp(timestamp, self._jpeg, self._packet_size)
def restart_generator(self):
def frame_generator():
timestamp = time()
frames = self.encode_rtp(timestamp, self._jpeg, self._packet_size)
for frame in frames:
yield frame
if self._generator is None:
self._generator = frame_generator()
def next_packet(self):
if self._generator is None:
self.restart_generator()
while True:
try:
frame = next(self._generator)
return frame
except StopIteration:
self._generator = None
self.restart_generator()
return None