mirror of
https://github.com/ossrs/srs.git
synced 2025-02-13 11:51:57 +00:00
support snapshot by http hooks for #152.
This commit is contained in:
parent
9eebb89432
commit
33a282e576
1 changed files with 152 additions and 2 deletions
|
@ -36,7 +36,8 @@ reload(sys)
|
||||||
exec("sys.setdefaultencoding('utf-8')")
|
exec("sys.setdefaultencoding('utf-8')")
|
||||||
assert sys.getdefaultencoding().lower() == "utf-8"
|
assert sys.getdefaultencoding().lower() == "utf-8"
|
||||||
|
|
||||||
import os, json, time, datetime, cherrypy, threading, urllib2
|
import os, json, time, datetime, cherrypy, threading, urllib2, shlex, subprocess
|
||||||
|
import cherrypy.process.plugins
|
||||||
|
|
||||||
# simple log functions.
|
# simple log functions.
|
||||||
def trace(msg):
|
def trace(msg):
|
||||||
|
@ -769,6 +770,46 @@ class RESTChats(object):
|
||||||
def OPTIONS(self, *args, **kwargs):
|
def OPTIONS(self, *args, **kwargs):
|
||||||
enable_crossdomain()
|
enable_crossdomain()
|
||||||
|
|
||||||
|
'''
|
||||||
|
the snapshot api,
|
||||||
|
to start a snapshot when encoder start publish stream,
|
||||||
|
stop the snapshot worker when stream finished.
|
||||||
|
'''
|
||||||
|
class RESTSnapshots(object):
|
||||||
|
exposed = True
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def POST(self):
|
||||||
|
enable_crossdomain()
|
||||||
|
|
||||||
|
# return the error code in str
|
||||||
|
code = Error.success
|
||||||
|
|
||||||
|
req = cherrypy.request.body.read()
|
||||||
|
trace("post to streams, req=%s"%(req))
|
||||||
|
try:
|
||||||
|
json_req = json.loads(req)
|
||||||
|
except Exception, ex:
|
||||||
|
code = Error.system_parse_json
|
||||||
|
trace("parse the request to json failed, req=%s, ex=%s, code=%s"%(req, ex, code))
|
||||||
|
return str(code)
|
||||||
|
|
||||||
|
action = json_req["action"]
|
||||||
|
if action == "on_publish":
|
||||||
|
code = worker.snapshot_create(json_req)
|
||||||
|
elif action == "on_unpublish":
|
||||||
|
code = worker.snapshot_destroy(json_req)
|
||||||
|
else:
|
||||||
|
trace("invalid request action: %s"%(json_req["action"]))
|
||||||
|
code = Error.request_invalid_action
|
||||||
|
|
||||||
|
return str(code)
|
||||||
|
|
||||||
|
def OPTIONS(self, *args, **kwargs):
|
||||||
|
enable_crossdomain()
|
||||||
|
|
||||||
# HTTP RESTful path.
|
# HTTP RESTful path.
|
||||||
class Root(object):
|
class Root(object):
|
||||||
exposed = True
|
exposed = True
|
||||||
|
@ -809,6 +850,7 @@ class V1(object):
|
||||||
self.proxy = RESTProxy()
|
self.proxy = RESTProxy()
|
||||||
self.chats = RESTChats()
|
self.chats = RESTChats()
|
||||||
self.servers = RESTServers()
|
self.servers = RESTServers()
|
||||||
|
self.snapshots = RESTSnapshots()
|
||||||
def GET(self):
|
def GET(self):
|
||||||
enable_crossdomain();
|
enable_crossdomain();
|
||||||
return json.dumps({"code":Error.success, "urls":{
|
return json.dumps({"code":Error.success, "urls":{
|
||||||
|
@ -849,10 +891,118 @@ port = int(sys.argv[1])
|
||||||
static_dir = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "static-dir"))
|
static_dir = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "static-dir"))
|
||||||
trace("api server listen at port: %s, static_dir: %s"%(port, static_dir))
|
trace("api server listen at port: %s, static_dir: %s"%(port, static_dir))
|
||||||
|
|
||||||
|
|
||||||
|
discard = open("/dev/null", "rw")
|
||||||
|
'''
|
||||||
|
create process by specifies command.
|
||||||
|
@param command the command str to start the process.
|
||||||
|
@param stdout_fd an int fd specifies the stdout fd.
|
||||||
|
@param stderr_fd an int fd specifies the stderr fd.
|
||||||
|
@param log_file a file object specifies the additional log to write to. ignore if None.
|
||||||
|
@return a Popen object created by subprocess.Popen().
|
||||||
|
'''
|
||||||
|
def create_process(command, stdout_fd, stderr_fd):
|
||||||
|
# log the original command
|
||||||
|
msg = "process start command: %s"%(command);
|
||||||
|
|
||||||
|
# to avoid shell injection, directly use the command, no need to filter.
|
||||||
|
args = shlex.split(str(command));
|
||||||
|
process = subprocess.Popen(args, stdout=stdout_fd, stderr=stderr_fd);
|
||||||
|
|
||||||
|
return process;
|
||||||
|
'''
|
||||||
|
isolate thread for srs worker, to do some job in background,
|
||||||
|
for example, to snapshot thumbnail of RTMP stream.
|
||||||
|
'''
|
||||||
|
class SrsWorker(cherrypy.process.plugins.SimplePlugin):
|
||||||
|
def __init__(self, bus):
|
||||||
|
cherrypy.process.plugins.SimplePlugin.__init__(self, bus);
|
||||||
|
self.__snapshots = {}
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
print "srs worker thread started"
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
print "srs worker thread stopped"
|
||||||
|
|
||||||
|
def main(self):
|
||||||
|
for url in self.__snapshots:
|
||||||
|
snapshot = self.__snapshots[url]
|
||||||
|
|
||||||
|
diff = time.time() - snapshot['timestamp']
|
||||||
|
process = snapshot['process']
|
||||||
|
|
||||||
|
# aborted.
|
||||||
|
if process is not None and snapshot['abort']:
|
||||||
|
process.kill()
|
||||||
|
process.poll()
|
||||||
|
del self.__snapshots[url]
|
||||||
|
print 'abort snapshot %s'%snapshot['cmd']
|
||||||
|
break
|
||||||
|
|
||||||
|
# already snapshoted and not expired.
|
||||||
|
if process is not None and diff < 10:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# terminate the active process
|
||||||
|
if process is not None:
|
||||||
|
# the poll will set the process.returncode
|
||||||
|
process.poll()
|
||||||
|
|
||||||
|
# None incidates the process hasn't terminate yet.
|
||||||
|
if process.returncode is not None:
|
||||||
|
# process terminated, check the returncode.
|
||||||
|
if process.returncode != 0:
|
||||||
|
print 'process terminated with error=%s, cmd=%s'%(process.returncode, snapshot['cmd'])
|
||||||
|
else:
|
||||||
|
# kill the process when user cancel.
|
||||||
|
process.kill()
|
||||||
|
|
||||||
|
# create new process to snapshot.
|
||||||
|
ffmpeg = "./objs/ffmpeg/bin/ffmpeg"
|
||||||
|
output = os.path.join(static_dir, "%s-%s-%%3d.png"%(snapshot['app'], snapshot['stream']))
|
||||||
|
cmd = '%s -i %s -vf fps=1/6 -vcodec png -f image2 -an -y -vframes 3 -y %s'%(ffmpeg, url, output)
|
||||||
|
print 'snapshot by: %s'%cmd
|
||||||
|
|
||||||
|
process = create_process(cmd, discard.fileno(), discard.fileno())
|
||||||
|
snapshot['process'] = process
|
||||||
|
snapshot['cmd'] = cmd
|
||||||
|
snapshot['timestamp'] = time.time()
|
||||||
|
pass;
|
||||||
|
|
||||||
|
# {"action":"on_publish","client_id":108,"ip":"127.0.0.1","vhost":"__defaultVhost__","app":"live","stream":"livestream"}
|
||||||
|
# ffmpeg -i rtmp://127.0.0.1:1935/live?vhost=dev/stream -vf fps=1/6 -vcodec png -f image2 -an -y -vframes 3 -y static-dir/live-livestream-%3d.png
|
||||||
|
def snapshot_create(self, req):
|
||||||
|
url = "rtmp://127.0.0.1/%s...vhost...%s/%s"%(req['app'], req['vhost'], req['stream'])
|
||||||
|
if url in self.__snapshots:
|
||||||
|
print 'ignore exists %s'%url
|
||||||
|
return Error.success
|
||||||
|
|
||||||
|
req['process'] = None
|
||||||
|
req['abort'] = False
|
||||||
|
req['timestamp'] = time.time()
|
||||||
|
self.__snapshots[url] = req
|
||||||
|
return Error.success
|
||||||
|
|
||||||
|
# {"action":"on_unpublish","client_id":108,"ip":"127.0.0.1","vhost":"__defaultVhost__","app":"live","stream":"livestream"}
|
||||||
|
def snapshot_destroy(self, req):
|
||||||
|
url = "rtmp://127.0.0.1/%s...vhost...%s/%s"%(req['app'], req['vhost'], req['stream'])
|
||||||
|
if url in self.__snapshots:
|
||||||
|
snapshot = self.__snapshots[url]
|
||||||
|
snapshot['abort'] = True
|
||||||
|
return Error.success
|
||||||
|
|
||||||
|
# subscribe the plugin to cherrypy.
|
||||||
|
worker = SrsWorker(cherrypy.engine)
|
||||||
|
worker.subscribe();
|
||||||
|
|
||||||
|
# disable the autoreloader to make it more simple.
|
||||||
|
cherrypy.engine.autoreload.unsubscribe();
|
||||||
|
|
||||||
# cherrypy config.
|
# cherrypy config.
|
||||||
conf = {
|
conf = {
|
||||||
'global': {
|
'global': {
|
||||||
'server.shutdown_timeout': 1,
|
'server.shutdown_timeout': 3,
|
||||||
'server.socket_host': '0.0.0.0',
|
'server.socket_host': '0.0.0.0',
|
||||||
'server.socket_port': port,
|
'server.socket_port': port,
|
||||||
'tools.encode.on': True,
|
'tools.encode.on': True,
|
||||||
|
|
Loading…
Reference in a new issue