-
Notifications
You must be signed in to change notification settings - Fork 0
/
transcode.py
240 lines (221 loc) · 9.33 KB
/
transcode.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
import os
import sys
import string
import re
import md5
import math
import shlex
import helper
from time import time, sleep
from urllib import unquote
import subprocess
#from subprocess import subprocess.Popen, subprocess.PIPE
WIN32 = True if sys.platform.startswith('win') else False
if WIN32:
import winhelper
DISABLE_LIVE_TRANSCODE = False
# Maximum length of time we'll keep video chunks on disk
MAX_CACHE_TIME = helper.hours_to_seconds(6)
# Maximum time we'll continue to transcode without having received a request
MAX_IDLE_TIME = helper.minutes_to_seconds(30)
class Segmenter(object):
def __init__(self, path=None):
if path:
self.path = os.path.abspath(path)
else:
name = 'live_segmenter.exe' if WIN32 else 'live_segmenter'
self.path = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), name)
self.cmd_tmpl = string.Template(helper.shellquote(self.path)+" 10 $tmpDir $segmentPrefix mpegts $startSegment")
if not helper.validate_exec(self.path):
DISABLE_LIVE_TRANSCODE = True
print "Live transcode disabled. Could not find segmenter at path: "+self.path
class Ffmpeg(object):
def __init__(self, path=None):
if path:
self.path = os.path.abspath(path)
else:
name = 'ffmpeg.exe' if WIN32 else 'ffmpeg'
self.path = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), name)
self.cmd_tmpl = string.Template(helper.shellquote(self.path)+" "
"-ss $startTime "
"-i \"$videoPath\" "
"-vcodec libx264 -r 23.976 "
"-b $bitrate -bt $bitrate -loglevel quiet "
"-vf \"crop=iw:ih:0:0,scale=$frameWidth:$frameHeight\" -aspect \"$frameWidth:$frameHeight\" "
"-acodec libmp3lame -ab 48k -ar 48000 -ac 2 -async 1 "
"-bufsize 1024k -threads 4 -preset fast -tune grain "
"-f mpegts - ")
if not helper.validate_exec(self.path):
DISABLE_LIVE_TRANSCODE = True
print "Live transcode disabled. Could not find ffmpeg at path: "+self.path
class TranscodeSession(object):
def __init__(self, transcoder, videoPath):
self.alive = True
self.idleTime = 0
self.lastRequest = time()
self.transcoder = transcoder
self.tmp_dir = transcoder.tmp_dir
self.videoPath = videoPath
self.inspection = self.inspect()
#self.fps = self.getFps()
self.duration = self.getDuration()
self.segment_duration = 10
self.frame_size = 640,360 #self.getFrameSize()
self.md5 = md5.new(videoPath).hexdigest()
self.ts_filename_tmpl = string.Template("$md5hash-$bitrate-$segment.ts")
self.current_ffmpeg_process = None
self.current_segmenter_process = None
self.win_batch_process = None
def idle_time(self):
self.idleTime = (time() - self.lastRequest)
return self.idleTime
def cleanup(self):
idle = self.idle_time()
if idle > MAX_IDLE_TIME:
if self.current_ffmpeg_process:
self.current_segmenter_process.kill()
self.current_ffmpeg_process.kill()
if idle > MAX_CACHE_TIME:
for ts in os.listdir(self.tmp_dir):
if ts.startswith(self.md5):
os.remove(os.path.join(self.tmp_dir, ts))
self.alive = False
def inspect(self):
proc = subprocess.Popen([self.transcoder.ffmpeg.path, '-i', self.videoPath], stderr=subprocess.PIPE, startupinfo=helper.noCmd())
proc.wait()
stderr = proc.stderr.read()
return stderr
def can_be_decoded(self):
failmsg = re.compile('Unable to find a suitable output format');
return not failmsg.search(self.inspection)
def getDuration(self):
pattern = re.compile('Duration:\s([0-9]{2}):([0-9]{2}):([0-9]{2}).([0-9]{2})');
durationMatch = pattern.search(self.inspection)
seconds = 0
seconds += 3600 * int(durationMatch.group(1))
seconds += 60 * int(durationMatch.group(2))
seconds += int(durationMatch.group(3))
seconds += 1#/self.fps * int(durationMatch.group(4))
return seconds
def getFrameSize(self):
pattern = re.compile('Video:\s(.*),(.*),\s(\d*)x(\d*),');
matches = pattern.search(self.inspection)
try:
width = matches.group(3)
height = matches.group(4)
return width, height
except Exception:
raise 'Problem capturing frame size'
def getFps(self):
pattern = re.compile('([0-9\.]{5})\sfps')
fpsString = pattern.search(self.inspection).group(1)
if fpsString:
return float(fpsString)
else:
return float(24)
raise 'Problem grabbing FPS'
def call_ffmpeg(self, **kwargs):
self.killProcessing()
cmd = self.transcoder.ffmpeg.cmd_tmpl.substitute(
startTime=int(kwargs['start_segment'])*self.segment_duration,
videoPath=self.videoPath, # FIXME last change monday 9:04 aM
#duration=kwargs['num_segments']*self.segment_duration,
frameWidth=self.frame_size[0],
frameHeight=self.frame_size[1],
bitrate=int(kwargs['bitrate'])*1000,
)
segmenter_cmd = self.transcoder.segmenter.cmd_tmpl.substitute(
tmpDir=helper.shellquote(self.transcoder.tmp_dir), # and this ughhh shellex? or what nib
startSegment=kwargs['start_segment'],
segmentPrefix=self.md5+"-"+kwargs['bitrate'],
)
with open(os.devnull, 'w') as fp:
self.current_ffmpeg_process = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stderr=fp, startupinfo=helper.noCmd())
self.current_segmenter_process = subprocess.Popen(shlex.split(segmenter_cmd), stdin=self.current_ffmpeg_process.stdout, stdout=fp, stderr=fp, startupinfo=helper.noCmd())
def killProcessing(self):
if WIN32 and self.win_batch_process:
self.win_batch_process.kill()
elif self.current_ffmpeg_process:
self.current_segmenter_process.kill()
self.current_ffmpeg_process.kill()
def transcode(self, seg, br, do_block=True):
self.lastRequest = time() # Reset the timer.
seg = int(seg)
self.ts_file = self.ts_filename_tmpl.substitute(md5hash=self.md5, bitrate=br, segment=seg)
self.ts_path = os.path.join(self.tmp_dir, self.ts_file)
ts_file_5 = self.ts_filename_tmpl.substitute(md5hash=self.md5, bitrate=br, segment=seg+5)
ts_path_5 = os.path.join(self.tmp_dir, ts_file_5)
if not os.path.exists(self.ts_path):
self.call_ffmpeg(start_segment = seg, bitrate=br)
while not os.path.exists(ts_path_5):
sleep(5)
continue
return self.ts_path
class Transcoder(object):
def __init__(self, config):
self.sessions = {} # We store our TranscodeSessions here
self.ffmpeg = Ffmpeg(config['ffmpeg_path'])
self.segmenter = Segmenter(config['segmenter_path'])
self.rootdir = config['rootdir']
if config['notes_dir']:
self.tmp_dir = os.path.join(config['notes_dir'], 'tmp')
else:
self.tmp_dir = os.path.join(self.rootdir, 'tmp')
if not os.path.exists(self.tmp_dir):
os.makedirs(self.tmp_dir)
def cleanup(self):
# This method will check self.sessions and ask if they are stale (lastRequest - now)
# They get "unstale" because we set the session's lastRequest to time() on every segment request
deadSessions = []
for md5, session in self.sessions.iteritems():
if session.alive:
session.cleanup()
else:
deadSessions.append(md5)
for md5 in deadSessions:
del self.sessions[md5]
def start_transcoding(self, videoPath):
if DISABLE_LIVE_TRANSCODE:
print "Live transcoding is currently disabled! There is a problem with your configuration."
return
print "Initiating transcode for asset at path: "+videoPath
videoPath = unquote(videoPath)
video_md5 = md5.new(videoPath).hexdigest()
if self.sessions.has_key(video_md5): # Session already exists?
return self.m3u8_bitrates_for(video_md5)
transcodingSession = TranscodeSession(self, videoPath)
if transcodingSession.can_be_decoded():
self.sessions[transcodingSession.md5] = transcodingSession
return self.m3u8_bitrates_for(transcodingSession.md5)
else:
return "Cannot decode this file."
def m3u8_segments_for(self, md5_hash, video_bitrate):
segment = string.Template("#EXTINF:$length,\n$md5hash-$bitrate-$segment.ts\n")
partCount = math.floor(self.sessions[md5_hash].duration / 10)
m3u8_segment_file = "#EXTM3U\n#EXT-X-TARGETDURATION:10\n"
for i in range(0, int(partCount)):
m3u8_segment_file += segment.substitute(length=10, md5hash=md5_hash, bitrate=video_bitrate, segment=i)
last_segment_length = math.ceil((self.sessions[md5_hash].duration - (partCount * 10)))
m3u8_segment_file += segment.substitute(length=last_segment_length, md5hash=md5_hash, bitrate=video_bitrate, segment=i)
m3u8_segment_file += "#EXT-X-ENDLIST"
return m3u8_segment_file
def m3u8_bitrates_for(self, md5_hash):
m3u8_fudge = string.Template(
"#EXTM3U\n"
# "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=384000\n"
# "$hash-384-segments.m3u8\n"
# "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=512000\n"
# "$hash-512-segments.m3u8\n"
"#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=768000\n"
"$hash-768-segments.m3u8\n"
# "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1024000\n"
# "$hash-1024-segments.m3u8\n"
)
return m3u8_fudge.substitute(hash=md5_hash)
def segment_path(self, md5_hash, the_bitrate, segment_number):
# A segment was requested.
path = self.sessions[md5_hash].transcode(segment_number, the_bitrate)
if path:
return path
else:
raise "Segment path not found"