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
|
import json
import re
import codecs
import logging
from subprocess import Popen, PIPE, DEVNULL
import shlex
utf8reader = codecs.getreader('utf-8')
def LoggedPopen(command, *args, **kwargs):
logging.info("Popen(command={} args={}) with kwargs={}".format(
" ".join(map(repr, command)),
" ".join(map(repr, args)),
" ".join(map(repr, kwargs))))
return Popen(command, *args, **kwargs)
def ffprobe_data(ospath):
logging.info('ffprobe %s', ospath)
process = LoggedPopen(['ffprobe', '-v', 'fatal', '-print_format', 'json',
'-show_format', '-show_streams', ospath], stdout=PIPE, stderr=DEVNULL)
data = json.load(utf8reader(process.stdout))
assert process.wait() == 0, "ffprobe failed"
process.stdout.close()
return data
def stream(ospath, ss, t):
logging.info('start ffmpeg stream h264 480p on path=%s ss=%s t=%s', ospath, ss, t)
t_2 = t + 2.0
output_ts_offset = ss
if ss != 0.0:
ss_string = "-ss {:0.6f}".format(ss)
else:
ss_string = ""
cutter = LoggedPopen(
shlex.split("ffmpeg -v fatal {ss_string} -i ".format(**locals())) +
[ospath] +
shlex.split("-c:a aac -strict experimental -ac 2 -b:a 64k"
" -c:v libx264 -pix_fmt yuv420p -profile:v high -level 4.0 -preset ultrafast -trellis 0"
" -crf 31 -vf scale=w=trunc(oh*a/2)*2:h=480"
" -shortest -f mpegts"
" -output_ts_offset {output_ts_offset:.6f} -t {t:.6f} pipe:%d.ts".format(**locals())),
stdout=PIPE)
return cutter
def find_next_keyframe(ospath, start, max_offset):
"""
:param: start: start search pts as float '123.123'
:param: max_offset: max offset after start as float
:return: (prev_duration, pts):
prev_duration: duration of the frame previous to the found key-frame
pts: PTS of next iframe but not search longer than max_offset
:raise: Exception if no keyframe found
"""
logging.info("start ffprobe to find next i-frame from {}".format(start))
if start == 0.0:
logging.info("return (0.0, 0.0) for start == 0.0")
return 0.0, 0.0
process = LoggedPopen(
shlex.split("ffprobe -read_intervals {start:.6f}%+{max_offset:.6f} -show_frames "
"-select_streams v -print_format flat".format(**locals())) + [ospath],
stdout=PIPE, stderr=DEVNULL)
data = {'frame': None}
prev_duration = None
try:
line = process.stdout.readline()
while line:
frame, name, value = re.match('frames\\.frame\\.(\d+)\\.([^=]*)=(.*)', line.decode('ascii')).groups()
if data['frame'] != frame:
data.clear()
prev_duration = None
data['frame'] = frame
data[name] = value
if 'pkt_duration_time' in data and data['pkt_duration_time'][1:-1] != "N/A":
prev_duration = float(data['pkt_duration_time'][1:-1])
if 'key_frame' in data and data['key_frame'] == '1' and prev_duration is not None:
if 'pkt_pts_time' in data and data['pkt_pts_time'][1:-1] != 'N/A' and float(
data['pkt_pts_time'][1:-1]) > start:
logging.info("Found pkt_pts_time={} prev__duration={}".format(data['pkt_pts_time'], prev_duration))
return prev_duration, float(data['pkt_pts_time'][1:-1])
elif 'pkt_dts_time' in data and data['pkt_dts_time'][1:-1] != 'N/A' and float(
data['pkt_dts_time'][1:-1]) > start:
logging.info("Found pkt_dts_time={} prev_duration={}".format(data['pkt_dts_time'], prev_duration))
return prev_duration, float(data['pkt_dts_time'][1:-1])
line = process.stdout.readline()
raise Exception("Failed to find next i-frame in {} .. {} of {}".format(start, start + max_offset, ospath))
finally:
process.stdout.close()
process.terminate()
def calculate_splittimes(ospath, chunk_duration):
"""
:param ospath: path to media file
:return: list of PTS times to split the media, in the form of
((start1, duration1), (start2, duration2), ...)
(('24.949000', '19.500000'), ('44.449000', ...), ...)
Note: - start2 is equal to start1 + duration1
- sum(durationX) is equal to media duration
"""
def calculate_points(media_duration):
pos = min(60, media_duration)
while pos < media_duration:
yield pos
pos += chunk_duration
duration = float(ffprobe_data(ospath)['format']['duration'])
points = list(calculate_points(duration))
for (point, nextPoint) in zip([0.0] + points, points + [duration]):
yield ("{:0.6f}".format(point), "{:0.6f}".format(nextPoint - point))
def poster(ospath):
process = LoggedPopen(shlex.split("ffmpeg -v fatal -noaccurate_seek -ss 25.0 -i") + [ospath] +
shlex.split("-frames:v 1 -map 0:v"
" -filter:v \"scale='w=trunc(oh*a/2)*2:h=480'\""
" -f singlejpeg pipe:"),
stdout=PIPE)
return process
def thumbnail(ospath, width, height):
process = LoggedPopen(shlex.split("ffmpeg -v fatal -noaccurate_seek -ss 25.0 -i") + [ospath] +
shlex.split("-frames:v 1 -map 0:v"
" -filter:v \"scale='w=trunc(oh*a/2)*2:h={}', crop='min({},iw):min({},ih)'\""
" -f singlejpeg pipe:".format(height+(height/10), width, height)),
stdout=PIPE)
return process
def thumbnail_video(ospath, width, height):
duration = float(ffprobe_data(ospath)['format']['duration'])
command = shlex.split("ffmpeg -v fatal")
chunk_startpos = range(min(int(duration)-1,30), int(duration), 500)
for pos in chunk_startpos:
command += ["-ss", "{:.6f}".format(pos), "-t", "2", "-i", ospath]
filter = " ".join(map(lambda i: "[{}:v]".format(i), range(len(chunk_startpos))))
filter += " concat=n={}:v=1:a=0 [v1]".format(len(chunk_startpos))
filter += "; [v1] fps=14 [v2]"
filter += "; [v2] scale='w=trunc(oh*a/2)*2:h={}' [v3]".format(height + 6)
filter += "; [v3] crop='min({},iw):min({},ih)' [v4]".format(width, height)
command += ['-filter_complex', filter, '-map', '[v4]']
command += shlex.split("-c:v libvpx -deadline realtime -f webm pipe:")
encoder = LoggedPopen(command, stdout=PIPE)
return encoder
|