From fc850dc4495509cc39bef572650283405bd14f98 Mon Sep 17 00:00:00 2001 From: josch Date: Tue, 26 Aug 2008 12:49:17 +0000 Subject: [PATCH] sanitized elixir usage and added huge gstreamer libs git-svn-id: http://yolanda.mister-muffin.de/svn@375 7eef14d0-6ed0-489d-bf55-20463b2d70db --- trunk/yolanda/config/environment.py | 4 +- trunk/yolanda/controllers/upload.py | 18 +- trunk/yolanda/lib/base.py | 3 +- trunk/yolanda/lib/gstreamer/__init__.py | 0 trunk/yolanda/lib/gstreamer/encode.py | 139 +++++++++++++++ trunk/yolanda/lib/gstreamer/info.py | 185 ++++++++++++++++++++ trunk/yolanda/lib/gstreamer/snapshot.py | 220 ++++++++++++++++++++++++ trunk/yolanda/model/__init__.py | 2 +- 8 files changed, 560 insertions(+), 11 deletions(-) create mode 100644 trunk/yolanda/lib/gstreamer/__init__.py create mode 100644 trunk/yolanda/lib/gstreamer/encode.py create mode 100644 trunk/yolanda/lib/gstreamer/info.py create mode 100644 trunk/yolanda/lib/gstreamer/snapshot.py diff --git a/trunk/yolanda/config/environment.py b/trunk/yolanda/config/environment.py index 71d2a70..de61ec2 100644 --- a/trunk/yolanda/config/environment.py +++ b/trunk/yolanda/config/environment.py @@ -35,7 +35,5 @@ def load_environment(global_conf, app_conf): # CONFIGURATION OPTIONS HERE (note: all config options will override # any Pylons config options) - engine = engine_from_config(config, 'sqlalchemy.') - model.metadata.bind = engine + model.metadata.bind = engine_from_config(config, 'sqlalchemy.') model.metadata.bind.echo = True - model.Session.bind = engine diff --git a/trunk/yolanda/controllers/upload.py b/trunk/yolanda/controllers/upload.py index 4f96adc..c7094cc 100644 --- a/trunk/yolanda/controllers/upload.py +++ b/trunk/yolanda/controllers/upload.py @@ -1,6 +1,8 @@ import logging from yolanda.lib.base import * +from yolanda.lib.gstreamer import info, snapshot +import os log = logging.getLogger(__name__) @@ -11,13 +13,19 @@ class UploadController(BaseController): def upload(self): myfile = request.params['file'] - permanent_file = open(os.path.join( - myfile.filename.lstrip(os.sep)), - 'w') + permanent_file = open(os.path.join(myfile.filename.lstrip(os.sep)),'w') - u.copyfileobj(myfile.file, permanent_file) + #u.copyfileobj(myfile.file, permanent_file) + + foo=model.Video(title=u"foooooo") + model.session.commit() + + videoinfo = info.Info(myfile.file) + videoinfo.get_info() + print videoinfo.print_info() + myfile.file.close() permanent_file.close() - return 'Successfully uploaded: %s'%myfile.filename + return 'Successfully uploaded: %s'%"" diff --git a/trunk/yolanda/lib/base.py b/trunk/yolanda/lib/base.py index 30d8654..52a8c28 100644 --- a/trunk/yolanda/lib/base.py +++ b/trunk/yolanda/lib/base.py @@ -13,7 +13,6 @@ from pylons.templating import render import yolanda.lib.helpers as h import yolanda.lib.utils as u import yolanda.model as model -import os class BaseController(WSGIController): @@ -22,7 +21,7 @@ class BaseController(WSGIController): # WSGIController.__call__ dispatches to the Controller method # the request is routed to. This routing information is # available in environ['pylons.routes_dict'] - response.headers['Content-type'] = "application/xml" + response.headers['Content-type'] = "application/xhtml+xml" return WSGIController.__call__(self, environ, start_response) # Include the '_' function in the public names diff --git a/trunk/yolanda/lib/gstreamer/__init__.py b/trunk/yolanda/lib/gstreamer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trunk/yolanda/lib/gstreamer/encode.py b/trunk/yolanda/lib/gstreamer/encode.py new file mode 100644 index 0000000..d08c2b5 --- /dev/null +++ b/trunk/yolanda/lib/gstreamer/encode.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python +""" + Encode - convert video to theora through gstreamer + + copyright 2008 - Johannes 'josch' Schauer + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" + +import sys, os +import gobject + +#DO NOT FORGET THIS OR A RAPTOR WILL COME AND GET YOU! +gobject.threads_init() + +import pygst +pygst.require("0.10") +import gst + +class Encode: + """ + Encode will allow you to convert all your media that gstreamer is + capable of playing into an ogg/vorbis/theora file + """ + def _build_pipeline(self): + self.player = gst.Pipeline("player") + source = gst.element_factory_make("filesrc", "file-source") + source.set_property("location", self.filename) + decodebin = gst.element_factory_make("decodebin", "decodebin") + decodebin.connect("pad-added", self.decodebin_callback) + audioconvert = gst.element_factory_make("audioconvert", "audioconvert") + rsmpl = gst.element_factory_make('audioresample') + vorbisenc = gst.element_factory_make("vorbisenc", "vorbisenc") + #set audio quality (max: 1.0, default: TODO) + vorbisenc.set_property("quality", self.audioquality) + ffmpeg = gst.element_factory_make("ffmpegcolorspace", "ffmpeg") + filter = gst.element_factory_make("capsfilter", "filter") + #filter.set_property("caps", gst.caps_from_string("width=64,height=64")) + videoscale = gst.element_factory_make("videoscale", "videoscale") + videoscale.set_property("method", 1) + videorate = gst.element_factory_make("videorate", "videorate") + theoraenc = gst.element_factory_make("theoraenc", "theoraenc") + #set video quality (max: 63, default: 16) + theoraenc.set_property("quality", self.videoquality) + #set quick property (default: True) + theoraenc.set_property("quick", self.quick) + #set sharpness property (default: TODO) + theoraenc.set_property("sharpness", self.sharpness) + queuea = gst.element_factory_make("queue", "queuea") + queuev = gst.element_factory_make("queue", "queuev") + muxer = gst.element_factory_make("oggmux", "muxer") + filesink = gst.element_factory_make("filesink", "filesink") + filesink.set_property("location", self.filename+".ogg") + + self.player.add(source, decodebin, ffmpeg, muxer, filesink, videorate, + videoscale, audioconvert, vorbisenc, theoraenc, queuea, queuev, + rsmpl, filter) + gst.element_link_many(source, decodebin) + if self.video: + gst.element_link_many(queuev, ffmpeg, filter, videoscale, + videorate, theoraenc, muxer) + if self.audio: + gst.element_link_many(queuea, audioconvert, rsmpl, vorbisenc, + muxer) + gst.element_link_many(muxer, filesink) + + bus = self.player.get_bus() + bus.add_signal_watch() + bus.connect("message", self.on_message) + self.player.set_state(gst.STATE_PLAYING) + + def __init__(self, filename, videoquality=16, quick=True, sharpness=2, + video=True, audio=True, audioquality=0.1): + if not os.path.isfile(filename): + raise IOError, "cannot read file" + self.filename = filename + if not video and not audio: + raise AttributeError, "must contain video or audio" + self.video = bool(video) + self.audio = bool(audio) + if not 1 <= int(videoquality) <= 63: + raise ValueError, "videoquality ranges from 1-63" + self.videoquality=int(videoquality) + self.quick = bool(quick) + if not 0 <= int(sharpness) <= 2: + raise ValueError, "sharpness ranges from 0-2" + self.sharpness = int(sharpness) + if not 0 <= float(audioquality) <= 1.0: + raise ValueError, "audioquality ranges from 0.0-1.0" + self.audioquality = float(audioquality) + + self.failed = False + gobject.idle_add(self._build_pipeline) + self.mainloop = gobject.MainLoop() + + def run(self): + self.mainloop.run() + return not self.failed + + def on_message(self, bus, message): + t = message.type + if t == gst.MESSAGE_EOS: + self.player.set_state(gst.STATE_NULL) + gobject.idle_add(self.mainloop.quit) + elif t == gst.MESSAGE_ERROR: + err, debug = message.parse_error() + print "Error: %s" % err, debug + self.failed = True + self.player.set_state(gst.STATE_NULL) + gobject.idle_add(self.mainloop.quit) + + def decodebin_callback(self, decodebin, pad): + if not pad.is_linked(): + if "video" in pad.get_caps()[0].get_name(): + pad.link(self.player.get_by_name("queuev").get_pad("sink")) + elif "audio" in pad.get_caps()[0].get_name(): + pad.link(self.player.get_by_name("queuea").get_pad("sink")) + +def main(args): + if len(args) != 2: + print 'usage: %s file' % args[0] + return 2 + + encode = Encode(args[1]) + encode.run() + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/trunk/yolanda/lib/gstreamer/info.py b/trunk/yolanda/lib/gstreamer/info.py new file mode 100644 index 0000000..2d98631 --- /dev/null +++ b/trunk/yolanda/lib/gstreamer/info.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python +""" + Info - get video information through gstreamer + + copyright 2008 - Johannes 'josch' Schauer + + derived from discoverer.py from the python gstreamer examples. + kudos to Edward Hervey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" + +import sys, os +import gobject + +#DO NOT FORGET THIS OR A RAPTOR WILL COME AND GET YOU! +gobject.threads_init() + +import pygst +pygst.require("0.10") +import gst +from gst.extend.discoverer import Discoverer + +class FDSource(gst.BaseSrc): + """ + borrowed from filesrc.py in the gstreamer examples + kudos to David I. Lehn and Johan Dahlin + only changed input from file location to file descriptor + """ + __gsttemplates__ = ( + gst.PadTemplate("src", + gst.PAD_SRC, + gst.PAD_ALWAYS, + gst.caps_new_any()), + ) + + blocksize = 4096 + fd = None + + def __init__(self, name): + self.__gobject_init__() + self.curoffset = 0 + self.set_name(name) + + def set_property(self, name, value): + if name == 'fd': + self.fd = value + + def do_create(self, offset, size): + if offset != self.curoffset: + self.fd.seek(offset, 0) + data = self.fd.read(self.blocksize) + if data: + self.curoffset += len(data) + return gst.FLOW_OK, gst.Buffer(data) + else: + return gst.FLOW_UNEXPECTED, None +gobject.type_register(FDSource) + +class Info(Discoverer): + """ + Because we derive from the Discoverer class itself here, all attributes + are directly accessible from outside as usual. + Added a synchronous interface with get_info() and filedescriptor input. + By implementing the MainLoop through subclassing we get maximum + flexibility and clean code. + """ + + def __init__(self, fd, max_interleave=1.0): + """ + fd: filedescriptor of the file to be discovered. + max_interleave: int or float; the maximum frame interleave in seconds. + The value must be greater than the input file frame interleave + or the discoverer may not find out all input file's streams. + The default value is 1 second and you shouldn't have to change it, + changing it mean larger discovering time and bigger memory usage. + this init is 90% copy of the original but with a filedescriptor instead + a filename and three lines to init the main event loop + """ + gobject.GObject.__init__(self) + + self.mimetype = None + + self.audiocaps = {} + self.videocaps = {} + + self.videowidth = 0 + self.videoheight = 0 + self.videorate = gst.Fraction(0,1) + + self.audiofloat = False + self.audiorate = 0 + self.audiodepth = 0 + self.audiowidth = 0 + self.audiochannels = 0 + + self.audiolength = 0L + self.videolength = 0L + + self.is_video = False + self.is_audio = False + + self.otherstreams = [] + + self.finished = False + self.tags = {} + self._success = False + self._nomorepads = False + + self._timeoutid = 0 + self._max_interleave = max_interleave + + # first mod of original __init__ to use a filedescriptor source + if type(fd) is not file: + raise TypeError, "expected file like input, got %s"%type(fd) + + # the initial elements of the pipeline + self.src = FDSource('filesrc') + self.src.set_property("fd", fd) + self.dbin = gst.element_factory_make("decodebin") + self.add(self.src, self.dbin) + self.src.link(self.dbin) + self.typefind = self.dbin.get_by_name("typefind") + + # callbacks + self.typefind.connect("have-type", self._have_type_cb) + self.dbin.connect("new-decoded-pad", self._new_decoded_pad_cb) + self.dbin.connect("no-more-pads", self._no_more_pads_cb) + self.dbin.connect("unknown-type", self._unknown_type_cb) + + # second mod to the discoverer __init__ to implement a main loop + self.connect('discovered', self._discovered) + gobject.idle_add(self._discover) + self.mainloop = gobject.MainLoop() + + def get_info(self): + """ + By running the main loop this will fire off the discover function. + The main loop will return when something was discovered. + The function returns wether or not the discovering was successful. + """ + self.mainloop.run() + return self.finished and self.mimetype and \ + (self.is_video or self.is_audio) + + def _discovered(self, discoverer, ismedia): + """When we discover something - quit main loop""" + gobject.idle_add(self.mainloop.quit) + + def _discover(self): + """ + when we are not finished (eg. because the file is invalid) then try + to discover video information (that will call the discovered function) + otherwise stop the main loop + """ + if self.finished: + gobject.idle_add(self.mainloop.quit) + else: + self.discover() + return False + +def main(args): + """here we add a nice cli interface and some example how to use the lib""" + if len(args) != 2: + print 'usage: %s file' % args[0] + return 2 + + input = open(args[1], "rb") + info = Info(input) + if info.get_info(): + info.print_info() + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/trunk/yolanda/lib/gstreamer/snapshot.py b/trunk/yolanda/lib/gstreamer/snapshot.py new file mode 100644 index 0000000..cece703 --- /dev/null +++ b/trunk/yolanda/lib/gstreamer/snapshot.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python +""" + Snapshot - get video thumbnail through gstreamer + + copyright 2008 - Johannes 'josch' Schauer + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +""" + +import sys, os +import gobject + +#DO NOT FORGET THIS OR A RAPTOR WILL COME AND GET YOU! +gobject.threads_init() + +import pygst +pygst.require("0.10") +import gst + +import Image +import ImageStat +import ImageOps + +import random + +#adjust this if necessary +BORING_IMAGE_VARIANCE = 500 +NUMBER_OF_TRIES = 10 + +class Snapshot: + """ + take interesting video snapshots. + """ + + def _capture_interesting_frame(self, pad, buffer): + """ + this is the buffer probe which processes every frame that is being + played by the capture pipeline. since the first frame is not random, we + skip this frame by setting the self.first_analyze flag. + if the current frame is found intersting we save it. if not we decrease + self.tries to limit the number of tries + """ + if not self.first_analyze: + #get current buffer's capabilities + caps = buffer.get_caps () + #we are interested in it's dimension + h, w = caps[0]['height'], caps[0]['width'] + #using PIL we grab the image in raw RGB mode from the buffer data + im = Image.frombuffer('RGB', (w, h), buffer.data, + 'raw', 'RGB', 0, 1) + #here we check the standard variance of a grayscale version of the + #current frame against the BORING_IMAGE_VARIANCE + if ImageStat.Stat(ImageOps.grayscale(im)).var[0] > \ + BORING_IMAGE_VARIANCE: + #success! save our interesting image + self.image = im + else: + #the image is just useless... retry... + self.tries -= 1 + else: + self.first_analyze = False + return True + + def _build_pipeline(self): + """ + here we init our capturing pipeline. + """ + self.player = gst.Pipeline("player") + source = gst.element_factory_make("filesrc", "file-source") + source.set_property("location", self.filename) + decodebin = gst.element_factory_make("decodebin", "decodebin") + #connect the pad-added signal to the apropriate callback + decodebin.connect("pad-added", self._decodebin_cb) + queuev = gst.element_factory_make("queue", "queuev") + ffmpeg = gst.element_factory_make("ffmpegcolorspace", "ffmpeg") + #we use the capsfilter to convert our videostream from YUV to RGB + filter = gst.element_factory_make("capsfilter", "filter") + filter.set_property("caps", gst.caps_from_string("video/x-raw-rgb")) + fakesink = gst.element_factory_make("fakesink", "fakesink") + + #add all elements to the capture pipeline + self.player.add(source, decodebin, ffmpeg, filter, fakesink, queuev) + #do some linking - only the link between decodebin and queuev is done + #later by the _decodebin_cb callback function + gst.element_link_many(source, decodebin) + gst.element_link_many(queuev, ffmpeg, filter, fakesink) + + #we add a buffer probe to the capsfilter output pad. every frame will + #get passed to the _capture_interesting_frame function + self.player.get_by_name('filter').get_pad('src').\ + add_buffer_probe(self._capture_interesting_frame) + + #watch for messages + bus = self.player.get_bus() + bus.add_signal_watch() + bus.connect("message", self._on_message) + + #start pipeline + self.player.set_state(gst.STATE_PLAYING) + + def __init__(self, filename): + """ + lets init some variables, check if the video file exists and add a + signal to the mainloop that executes _build_pipeline the instant the + mainloop is run + """ + if not os.path.isfile(filename): + raise IOError, "cannot read file" + self.filename = filename + self.time_format = gst.Format(gst.FORMAT_TIME) + #we add a signal to immediately execute the pipeline builder after + #the mainloop is actually run + gobject.idle_add(self._build_pipeline) + self.mainloop = gobject.MainLoop() + #this is set to False after the first run. this prevents the first + #frame processed by the buffer probe from not being random + self.first_analyze = True + #set the number of retries + self.tries = NUMBER_OF_TRIES + #if this is set the main loop is quit and the image returned + self.image = None + #we find this out before seeking + self.duration = None + + def get_snapshot(self): + """ + by starting the mainloop the capture pipeline will be build. this will + at some point issue an ASYNC_DONE message. this again will seek the + playing capture pipeline to a random position. a buffer probe will grab + the first frame played and test wether it is an intersting fellow. + if so, the random seeking will stop and we will quit the mainloop. this + will result in this function returning the captured image + """ + self.mainloop.run() + return self.image + + def _seek_and_play_random(self): + """ + this function gets repeatedly called because it is linked to the + ASYNC_DONE event which it itself also emits when seeking is finished. + here we only seek to a random position in the video stream + """ + #there are some cases in which this tries to seek after the maximum + #number of tries is reached and the player is already nullyfied + if self.tries > 0: + #if we didn't already set the duration - do it now + #meaybe this can be done earlier but i didnt figure out when + if self.duration is None: + self.duration = self.player.query_duration(self.time_format, + None)[0] + #seek to random position. this will again fire an ASYNC_DONE and + #let the frame we seeked to be rendered by the buffer probe + self.player.seek_simple(self.time_format, gst.SEEK_FLAG_FLUSH, + random.randint(1, self.duration)) + + def _on_message(self, bus, message): + """ + since seeking issues the ASYNC signal we create an infinite loop here. + on each frame being seeked to the buffer probe function + _capture_interesting_frame gets called. this will fill self.im with a + non-boring screenshot which will then cause this _seek_and_play_random + loop to be finished + """ + t = message.type + if t == gst.MESSAGE_ASYNC_DONE: + #only loop again if there is still no image and tries are left + if self.image is not None or self.tries < 1: + #success! clean everything up + self.player.set_state(gst.STATE_NULL) + gobject.idle_add(self.mainloop.quit) + else: + #issue new seeking that probably will bring us here again + gobject.idle_add(self._seek_and_play_random) + elif t == gst.MESSAGE_EOS: + #somehow we seeded into the end of file - lets seek to a new pos + gobject.idle_add(self._seek_and_play_random) + elif t == gst.MESSAGE_ERROR: + #oups error - this shouldn't happen' + err, debug = message.parse_error() + print "Error: %s" % err, debug + self.player.set_state(gst.STATE_NULL) + gobject.idle_add(self.mainloop.quit) + + def _decodebin_cb(self, decodebin, pad): + """ + when the decode bin is ready, a new pad is added for the video stream + we connect this pad to our video queue sink here + """ + #only do so if pad is not already linked to something + if not pad.is_linked(): + #only do this for the video stream - we are not interested in audio + if "video" in pad.get_caps()[0].get_name(): + #do the linking from the source pad to the queue's sink + pad.link(self.player.get_by_name("queuev").get_pad("sink")) + +def main(args): + """here we add a nice cli interface and some example how to use the lib""" + if len(args) != 2: + print 'usage: %s file' % args[0] + return 2 + + snapshot = Snapshot(args[1]) + im = snapshot.get_snapshot() + if im: + print "SUCCESS!" + im.save("%s.jpg" %args[1], "JPEG") + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/trunk/yolanda/model/__init__.py b/trunk/yolanda/model/__init__.py index b931e0d..92d14a3 100644 --- a/trunk/yolanda/model/__init__.py +++ b/trunk/yolanda/model/__init__.py @@ -1,7 +1,7 @@ import elixir # replace the elixir session with our own -Session = elixir.session(autoflush=True, transactional=True) +session = elixir.session # use the elixir metadata metadata = elixir.metadata