Capture network streams using vlc.py on a schedule.
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 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 | """
A script to capture network streams using VLC, based on a scheduler. It was originally designed for mpeg transport streams on the local network, but can be modified for other stream types. It runs as an infinite loop and can be terminated by pressing "Q" (see below for key commands).
Last updated 19th August 2015.
Dependencies:
- My fork of Danny Yoo's getch()-like function: https://code.activestate.com/recipes/579095
- vlc.py http://wiki.videolan.org/Python_bindings
The end user will need to create two JSON files in the same directory as the script:
- capture.json to store the network stream/channel information
- schedule.json to store the timer recordings
Example format of capture.json:
{
"stream name one":"stream one address",
"stream name two":"stream two address"
}
Example format of schedule.json:
[
{ "start":"2015-08-18 18:08:00", "duration":1, "channel":"stream name one", "programme":"Test Recording"},
{ "start":"2015-08-18 19:20:00", "end":"2015-08-18 20:20:00", "channel":"stream name two", "programme":"Second Recording"}
]
You can specify either the length of the recording or an end datetime. The programme field is simply a descriptor (the recording will be named YYYYMMDD_HHMMSS_channel_programme.ts).
Commands:
Once the script is running, it will automatically parse the channel list and schedule. There are three basic commands, trigger by key presses:
- pressing "R" reloads the schedule and will update any upcoming scheduled recordings
- pressing "C" reloads the channel list
- pressing "Q" exits the script
"""
import os
import sys
import json
import datetime
import time
import getch # External py file
import vlc # External py file
# Static global variables
PATH = os.path.abspath(os.path.dirname(__file__))
SCRIPT = os.path.basename(__file__)
# FUTURE IMPROVEMENT
# Add optionParser to allow user to specify config file
config_file = os.path.join(PATH, 'capture.json')
timer_file = os.path.join(PATH, 'schedule.json')
log_file = os.path.join(PATH, "%s.log" % os.path.splitext(SCRIPT)[0])
def writePrint(text):
'''Write to the log and print to the screen.'''
f = open(log_file, 'a')
print text
f.write(text)
f.close()
def timePrint(text, dt=None):
'''Print to STDOUT with a datetime prefix. If no timestamp is provided,
the current date and time will be used.'''
if dt is None:
now = datetime.datetime.now()
dt = now.strftime('%H:%M:%S')
writePrint("%s %s" % (dt, text))
def indentPrint(text):
'''Print to STDOUT with an indent matching the timestamp printout in
timePrint().'''
writePrint("\t %s" % (text))
def loadChannelConfig(silent=False):
'''Load the stream configuration file.'''
f = open(config_file, 'r')
cjson = json.load(f)
f.close()
channels = len(cjson.keys())
if not silent:
writePrint("%d channels available." % channels)
return cjson
def loadSchedule(silent=False):
'''Load the scheduled recordings file.'''
# Read the schedule file
f = open(timer_file, 'r')
rjson = json.load(f)
f.close()
recordings = len(rjson)
if not silent:
writePrint("%d recordings scheduled." % recordings)
return rjson
def parseSchedule(schedule, channels):
'''Parse the schedule and return a list of timings to check against.'''
recordings = {}
schedules = len(schedule)
for x in xrange(0, schedules):
entry = schedule[x] # should be a JSON object
# Recording start time
start = entry['start']
dt = datetime.datetime.strptime(start, '%Y-%m-%d %H:%M:%S')
channel = entry['channel'] # Extract the channel name
# Check for an endtime or a duration
endtime = None
offset = None
if 'end' in entry:
endtime = datetime.datetime.strptime(entry['end'], '%Y-%m-%d %H:%M:%S')
if 'duration' in entry:
duration = entry['duration'] # Get the timer duration (minutes)
offset = dt + datetime.timedelta(minutes=duration)
# Check to see which gives the longer recording - the duration or end timestamp
if offset is not None and endtime is not None:
if offset > endtime:
endtime = offset
elif offset is not None:
endtime = offset
elif endtime is None and offset is None:
# No valid duration/end time
writePrint('End or duration missing for scheduled recording %s (%s).' % (dt, channel))
continue
elif endtime is not None and endtime < dt:
# End is earlier than the start!
writePrint('End timestamp earlier than start! Cannot record %s (%s).' % (dt, channel))
programme = None
if 'programme' in entry:
programme = entry['programme']
addr = channels[channel] # Get the channel URL
pid = '%s %s' % (start, channel)
recordings[pid] = {
'url': addr,
'channel': channel,
'start':dt,
'end': endtime,
'programme': programme,
'sid': x # Basic schedule id - this can be improved upon later to make it a unique identifier that is read/written by the schedule editor (or a database primary key)
}
return recordings
def initialiseTS(channel, tstamp=datetime.datetime.now(), programme=None, ext='.ts'):
'''Check for a free filename.'''
# Get list of existing files
d = set(x for x in os.listdir(PATH) if (x.endswith(ext)))
# Filename template
fn = [tstamp.strftime('%Y%m%d_%H%M%S'), channel]
# If we have a programme name, add it to the filename
if programme is not None:
programme.replace(' ', '_') # Replace whitespace with underscores
fn.append(programme)
fn_str = '_'.join(fn)
name = '%s%s' % (fn_str, ext)
n = 0
# While a filename matches the standard naming pattern, increment the
# counter until we find a spare filename
while name in d:
name = '%s_%d%s' % (fn_str, n, ext)
n += 1
return os.path.join(PATH, name)
def recordStream(instream, outfile):
'''Record the network stream to the output file.'''
inst = vlc.Instance() # Create a VLC instance
p = inst.media_player_new() # Create a player instance
cmd1 = "sout=file/ts:%s" % outfile
media = inst.media_new(instream, cmd1)
media.get_mrl()
p.set_media(media)
return (inst, p, media)
def initialise(silent=False):
'''Load the channel list and scheduled recordings.'''
# Initial startup
channels = loadChannelConfig(silent) # Get the available channels
schedule = loadSchedule(silent) # Get the schedule
recordings = parseSchedule(schedule, channels) # Parse the schedule information
return recordings
def reloadSchedule(existing, running):
'''Reload the list of scheduled recordings.'''
now = datetime.datetime.now() # Get the current timestamp
revised = initialise(True) # Get the revised schedule
# Get the schedule id for each of the running recordings
running_ids = {}
for r in running:
sid = running[r]['sid']
running_ids[sid] = r
# Get the schedule id for each of the upcoming recordings
upcoming_ids = {}
for e in existing:
sid = existing[e]['sid']
upcoming_ids[sid] = e
# Number of new entries
new_rec = 0
# Compare the revised schedule against the existing
for r in revised:
data = revised[r]
sched_id = data['sid']
endtime = data['end']
# If this recording is already running
if sched_id in running_ids:
h = running_ids[sched_id]
# TO DO: When the SID is implemented properly with the
# schedule editor, there will be no need to check any of these
# fields apart from the end time
# Check if it's the same channel and programme
ch = (data['channel'] == running[h]['channel'])
pr = (data['programme'] == running[h]['programme'])
# If it's the same channel and programme, check if we need to revise the end time
if pr and ch and endtime != running[h]['end']:
timePrint('Changed end time for running recording:')
if data['programme'] is not None:
indentPrint('%(programme)s (%(channel)s)' % data)
else:
indentPrint('%s' % h)
indentPrint('%s to %s' % (running[h]['end'].strftime('%Y-%m-%d %H:%M:%S'), endtime.strftime('%Y-%m-%d %H:%M:%S')))
running[h]['end'] = endtime
# Otherwise, it's not a currently-running recording
# We only want to consider programmes that haven't finished yet
elif endtime > now:
if sched_id in upcoming_ids:
s = upcoming_ids[sched_id]
# Remove the old data so that it can be replaced with the new
temp = existing.pop(s, None)
# Check if it's the same channel and programme
ch = (data['channel'] == temp['channel'])
pr = (data['programme'] == temp['programme'])
# Only notify a change if it's the same programme
if temp != data and ch and (pr or temp['programme'] is None):
timePrint('Changes made to scheduled recording:')
if data['programme'] is not None:
indentPrint('%(programme)s (%(channel)s)' % data)
else:
indentPrint('%s' % s)
else:
new_rec += 1
existing[r] = data
if new_rec > 0:
timePrint('Added %d new scheduled recordings.' % new_rec)
return (existing, running)
def main():
recordings = initialise() # Load the channels and schedule
handles = {} # Create storage for the recording handles
busy = True
while busy:
now = datetime.datetime.now() # Get the current timestamp
# Check existing recordings
hs = handles.keys()
for h in hs:
data = handles[h]
end = data['end']
channel = data['channel']
programme = data['programme']
if now > end:
timePrint("Finished recording %s (%s)." % (programme, channel))
try:
data['player'].stop() # Stop playback
data['player'].release() # Close the player
data['inst'].release() # Destroy the instance
except Exception, err:
timePrint("Unable to destroy player reference due to error:")
writePrint(str(err))
handles.pop(h) # Remove the handle to the player
# Loop through the schedule
rs = recordings.keys()
for r in rs:
data = recordings[r] # Schedule entry details
start = data['start']
end = data['end']
channel = data['channel']
programme = data['programme']
# If we're not recording the stream but we're between the
# start and end times for the programme, record it
if r not in handles and (now > start):
if (now < end):
# Determine a suitable output filename
fn = initialiseTS(channel, start, programme)
# Create the VLC instance and player
(inst, player, media) = recordStream(data['url'], fn)
# Store the handle to the VLC instance and relevant data
handles[r] = {
'inst': inst,
'player': player,
'media': media,
'end': end,
'programme': programme,
'channel': channel,
'sid': data['sid']
}
# Start the stream and hence the recording
player.play()
timePrint("Started recording:")
indentPrint("%s (%s)" % (programme, channel))
indentPrint("%s to %s" % (start.strftime('%Y-%m-%d %H:%M:%S'), end.strftime('%Y-%m-%d %H:%M:%S')))
else:
timePrint("Missed scheduled recording:")
indentPrint("%s (%s)" % (programme, channel))
indentPrint("%s to %s" % (start.strftime('%Y-%m-%d %H:%M:%S'), end.strftime('%Y-%m-%d %H:%M:%S')))
# Remove the item from the schedule to prevent it being
# processed again
recordings.pop(r)
k = len(handles.keys()) + len(recordings.keys())
#busy = k > 0
# Loop for 10 seconds, checking for a keyhit
n = 10
while n > 0:
keyhit = getch.getch()
n -= 1
# Check if we have a keyhit
if keyhit is not None:
kl = keyhit.lower()
# Reload schedule
if 'r' in kl:
timePrint('Reloading schedule...')
(recordings, handles) = reloadSchedule(recordings, handles)
# Reload channel config
if 'c' in kl:
pass
# Quit
if 'q' in kl:
# Add request for confirmation here
busy = False
if not busy:
timePrint("Exiting...\n")
if __name__ == '__main__':
main()
|
I wrote this in order to be able to record network streams at a set time using VLC. The closest example I had found was http://code.activestate.com/recipes/577802/ and so I thought I would share this.
It depends on my forked version of Danny Yoo's getch()-like function ( https://code.activestate.com/recipes/579095 ) and vlc.py ( http://wiki.videolan.org/Python_bindings )