Skip to content

Commit 68d6015

Browse files
author
arch
committed
add ffmpegstream class
1 parent bdcbcd5 commit 68d6015

File tree

1 file changed

+308
-0
lines changed

1 file changed

+308
-0
lines changed
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
""" FFmpeg Video Stream """
2+
3+
import cv2
4+
import os
5+
import logging
6+
import time
7+
8+
from dataclasses import dataclass
9+
from threading import Thread
10+
from queue import Queue
11+
12+
import subprocess as sp
13+
import numpy as np
14+
15+
16+
@dataclass
17+
class FFmpegStreamParameter:
18+
""" FFmpeg Stream Parameter Dataclass with default values """
19+
fov: int = 100
20+
theta: int = -90
21+
phi: int = -45
22+
height: int = 720
23+
width: int = 1240
24+
25+
26+
@dataclass
27+
class VideoInfo:
28+
""" Video Info Dataclass """
29+
fps :float
30+
height :int
31+
width :int
32+
length :int
33+
34+
35+
class FFmpegStream:
36+
""" FFmpeg Stream with frame buffer
37+
38+
Args:
39+
video_path (str): path to video file
40+
parameter (FFmpegStreamParameter): conversion parameter
41+
start_frame (int): start frame number
42+
queue_size (int): size of frame buffer
43+
"""
44+
45+
def __init__(self,
46+
video_path :str,
47+
parameter :FFmpegStreamParameter,
48+
start_frame :int = 0,
49+
queue_size :int = 256):
50+
51+
self.video_path = video_path
52+
self.parameter = parameter
53+
self.start_frame = start_frame
54+
self.queue_size = queue_size
55+
56+
self.stopped = False
57+
self.current_frame = 0
58+
self.sleep_time = 0.001
59+
60+
self.video_info = self.get_video_info(video_path)
61+
self.frame_buffer = Queue(maxsize=queue_size)
62+
63+
self.thread = Thread(target=self.run, args=())
64+
self.thread.daemon = True
65+
self.thread.start()
66+
67+
68+
logger = logging.getLogger(__name__)
69+
70+
71+
@staticmethod
72+
def get_video_info(
73+
video_path: str) -> VideoInfo:
74+
""" Get VideoInfo
75+
76+
Args:
77+
video_path (str): path to video
78+
79+
Returns:
80+
VideoInfo: video infos
81+
"""
82+
cap = cv2.VideoCapture(video_path)
83+
video_info = VideoInfo(
84+
fps = float(cap.get(cv2.CAP_PROP_FPS)),
85+
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
86+
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
87+
length = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
88+
)
89+
cap.release()
90+
return video_info
91+
92+
93+
@staticmethod
94+
def get_frame(
95+
video_path :str,
96+
frame_number :int) -> np.ndarray:
97+
""" Get Video frame
98+
99+
Args:
100+
video_path (str): path to video
101+
frame_number (int): frame number to extract from video
102+
103+
Returns:
104+
np.ndarray: opencv image
105+
"""
106+
cap = cv2.VideoCapture(video_path)
107+
if frame_number > 0:
108+
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
109+
success, frame = cap.read()
110+
cap.release()
111+
return frame
112+
113+
114+
@staticmethod
115+
def get_projection(
116+
frame :np.ndarray,
117+
parameter: FFmpegStreamParameter) -> np.ndarray:
118+
""" Get projection of frame
119+
120+
Args:
121+
frame (np.ndarray): opencv image
122+
parameter (FFmpegStreamParameter): conversion parameter
123+
124+
Returns:
125+
np.ndarray: projected opencv image
126+
"""
127+
dimension = '{}x{}'.format(frame.shape[1], frame.shape[0])
128+
129+
video_filter = 'v360=input=he' \
130+
+ ':in_stereo=sbs' \
131+
+ ':pitch=' + str(parameter.phi) \
132+
+ ':output=flat' \
133+
+ ':d_fov=' + str(parameter.fov) \
134+
+ ':w=' + str(parameter.width) \
135+
+ ':h=' + str(parameter.height)
136+
137+
command = [
138+
'ffmpeg',
139+
'-hide_banner',
140+
'-loglevel', 'warning',
141+
'-y',
142+
'-f', 'rawvideo',
143+
'-vcodec','rawvideo',
144+
'-s', dimension,
145+
'-pix_fmt', 'bgr24',
146+
'-i', '-',
147+
'-f', 'image2pipe',
148+
'-pix_fmt', 'bgr24',
149+
'-vsync', '0',
150+
'-vcodec', 'rawvideo',
151+
'-an',
152+
'-sn',
153+
'-vf', video_filter,
154+
'-'
155+
]
156+
print(command)
157+
pipe = sp.Popen(
158+
command,
159+
stdin = sp.PIPE,
160+
stdout = sp.PIPE,
161+
bufsize = 3 * parameter.width * parameter.height
162+
)
163+
164+
pipe.stdin.write(frame.tobytes())
165+
projection = np.frombuffer(
166+
pipe.stdout.read(parameter.width * parameter.height * 3),
167+
dtype='uint8'
168+
).reshape(
169+
(parameter.height, parameter.width, 3)
170+
)
171+
172+
pipe.stdin.close()
173+
pipe.stdout.close()
174+
pipe.terminate()
175+
176+
return projection
177+
178+
179+
def frame_to_millisec(self, frame_number) -> int:
180+
"""Get timestamp for given frame number
181+
182+
Args:
183+
frame_number (int): frame number
184+
185+
Returns:
186+
int: timestamp in video
187+
"""
188+
if frame_number <= 0: return 0
189+
return int(round(float(frame_number)*float(1000)/self.video_info.fps))
190+
191+
192+
def millisec_to_timestamp(self, millis :int)->str:
193+
""" Convert milliseconds to timestamp
194+
195+
Args:
196+
millis (int): position in video in milliseconds
197+
198+
Returns:
199+
str: position in video as timestamp with H:M:S.XXX
200+
"""
201+
millis = int(millis)
202+
seconds = int((millis / 1000) % 60)
203+
minutes = int((millis / (1000 * 60)) % 60)
204+
hours = int((millis / (1000 * 60 * 60)) % 24)
205+
millis = int(millis % 1000)
206+
207+
return str(hours).zfill(2) \
208+
+ ':' + str(minutes).zfill(2) \
209+
+ ':' + str(seconds).zfill(2) \
210+
+ '.' + str(millis).zfill(3)
211+
212+
213+
def stop(self) -> None:
214+
""" Stop FFmpeg video stream """
215+
self.stopped = True
216+
self.thread.join()
217+
218+
219+
def read(self) -> np.ndarray:
220+
""" Get next projected frame from stream
221+
222+
Returns:
223+
np.ndarray: opencv image data
224+
"""
225+
return self.frame_buffer.get()
226+
227+
228+
def isOpen(self) -> bool:
229+
""" Check if FFmpeg video stream is open or a frame is still available in the buffer
230+
231+
Returns:
232+
bool: True if video stream is open or a frame is still available in the buffer else False
233+
"""
234+
return self.more() or not self.stopped
235+
236+
237+
def more(self) -> bool:
238+
""" Check if frames in the frame bufer are available
239+
240+
Returns:
241+
bool: True if a frame is available else False
242+
"""
243+
tries = 0
244+
while self.frame_buffer.qsize() == 0 and not self.stopped and tries < 5:
245+
time.sleep(self.sleep_time)
246+
tries += 1
247+
248+
return self.frame_buffer.qsize() > 0
249+
250+
251+
def run(self) -> None:
252+
""" Function to read transformed frames from ffmpeg video stream into a queue """
253+
254+
video_filter = 'v360=input=he' \
255+
+ ':in_stereo=sbs' \
256+
+ ':pitch=' + str(self.parameter.phi) \
257+
+ ':output=flat' \
258+
+ ':d_fov=' + str(self.parameter.fov) \
259+
+ ':w=' + str(self.parameter.width) \
260+
+ ':h=' + str(self.parameter.height)
261+
262+
seek = self.millisec_to_timestamp(
263+
self.frame_to_millisec(self.start_frame)
264+
)
265+
266+
command = [
267+
'ffmpeg',
268+
'-hide_banner',
269+
'-loglevel', 'warning',
270+
'-ss', str(seek),
271+
'-i', self.video_path,
272+
'-f', 'image2pipe',
273+
'-pix_fmt', 'bgr24',
274+
'-vsync', '0',
275+
'-vcodec', 'rawvideo',
276+
'-an',
277+
'-sn',
278+
'-vf', video_filter,
279+
'-'
280+
]
281+
282+
pipe = sp.Popen(
283+
command,
284+
stdout = sp.PIPE,
285+
bufsize= 3 * self.parameter.height * self.parameter.width
286+
)
287+
288+
while not self.stopped:
289+
data = pipe.stdout.read(self.parameter.width * self.parameter.height * 3)
290+
if not data:
291+
break
292+
293+
frame = np.frombuffer(data, dtype='uint8').reshape(
294+
(self.parameter.height, self.parameter.width, 3)
295+
)
296+
if frame is None:
297+
break
298+
299+
while self.frame_buffer.full() and not self.stopped:
300+
time.sleep(self.sleep_time)
301+
302+
self.frame_buffer.put(frame)
303+
self.current_frame += 1
304+
305+
self.logger.info('Close FFmpeg Stream')
306+
pipe.stdout.close()
307+
pipe.terminate()
308+
self.stopped = True

0 commit comments

Comments
 (0)