Rewite code for music playing module

Registered by TLE on 2010-10-03

Create an abstract class for music playing backends. Change the gstreamer music playing module to implement this abstract class.

Check out this example code from Ubuntu application week about python and gst, in particular about signal handling:

#!/usr/bin/python

import sys
import os
from os import path
import time
import optparse
from subprocess import check_call

size_map = {
    180: (320, 180),
    315: (560, 315),
    360: (640, 360),
    450: (800, 450),
    540: (960, 540),
    720: (1280, 720),
    1080: (1920, 1080),
}
sizes = sorted(size_map)
default_size = 315
default_freq = 64

vorbis = {
    'enc': 'vorbisenc',
    'caps': 'audio/x-raw-float',
    'props': {
        'quality': 0.3,
    },
}
flac = {
    'enc': 'flacenc',
    'caps': 'audio/x-raw-int',
}
codec_map = {
    'theora': {
        'video': {
            'enc': 'theoraenc',
            'props': {
                'quality': 48,
                #'bitrate': 3000,
                'keyframe-force': 16,
                #'speed-level': 2,
            },
        },
        'audio': vorbis,
        'mux': 'oggmux',
        'ext': 'ogv',
        'keyframe_prop': 'keyframe-force',
    },

    'vp8': {
        'video': {
            'enc': 'vp8enc',
            'props': {
                'quality': 8,
                'max-keyframe-distance': 64,
                'threads': 2,
            },
        },
        'audio': vorbis,
        'mux': 'webmmux',
        'ext': 'webm',
    },

    'h264': {
        'video': {
            'enc': 'x264enc',
            # See http://www.mplayerhq.hu/DOCS/HTML/en/menc-feat-x264.html
            'props': {
                'threads': 4,
                'key-int-max': 128,
                'pass': 4, # quality-based encoding
                'me': 3, # exhaustive search (slow)
                'quantizer': 20, # 25 is reasonable without making files too big
                'subme': 6,
                'ref': 4,
            },
        },
        'audio': {
            'enc': 'lame',
            'caps': 'audio/x-raw-int',
            'props': {
                'vbr': 4,
                'vbr-mean-bitrate': 256,
                #'bitrate': 160,
            },
        },
        'mux': 'mp4mux',
        'ext': 'mp4',
    }
}
codecs = sorted(codec_map)
default_codec = 'theora'

def minsec(seconds):
    """
    Convert *seconds* to "minutes:seconds" string.

    For example:

    >>> minsec(194)
    '3:14'

    If *seconds* is a ``float``, it will be truncated. For example:

    >>> minsec(59.99)
    '0:59'
    """
    if not isinstance(seconds, (int, float)):
        raise TypeError (
            TYPE_ERROR % ('seconds', (int, float), type(seconds), seconds)
        )
    return '%d:%02d' % (seconds / 60, seconds % 60)

parser = optparse.OptionParser(
    usage='usage: %prog SOURCE [DESTINATION]',
)
parser.add_option('--codec',
    help='one of %r; default is %r' % (codecs, default_codec),
    metavar='NAME',
    default=default_codec,
)
parser.add_option('--size',
    help='one of %r; default is %r' % (sizes, default_size),
    metavar='INT',
    default=default_size,
    type='int',
)

parser.add_option('--key',
    help='keyframe max frequency; default is %r' % default_freq,
    metavar='INT',
    default=default_freq,
    type='int',
)
parser.add_option('--channels',
    help='audio channels (1 or 2); default is 2',
    metavar='INT',
    default=2,
    type='int',
)
#parser.add_option('--vq',
# help='video quality; default is %r' % default_quality,
# metavar='INT',
# default=default_quality,
# type='int',
#)
#parser.add_option('--aq',
# help='audio quality; default is 0.3',
# metavar='FLOAT',
# default=0.3,
# type='float',
#)
#parser.add_option('--webm',
# help='produce webm video',
# action='store_true',
# default=False,
#)

def error(msg):
    parser.print_help()
    print('\nERROR: ' + msg)
    sys.exit(1)

(options, args) = parser.parse_args()

if options.size not in sizes:
    error('SIZE must be on of %r; got %r' % (sizes, options.size))

if options.codec not in codecs:
    error('CODEC must be one of %r; got %r' % (codecs, options.codec))
c = codec_map[options.codec]

if len(args) < 1:
    error('must provide SOURCE')
src = path.abspath(args[0])
if not path.isfile(src):
    error('SOURCE is not file: %r' % src)

if len(args) == 2:
    dst = path.abspath(args[1])
else:
    (d, name) = path.split(src)
    base = path.splitext(name)[0]
    dname = '%s_%dp.%s' % (
        base,
        options.size,
        c['ext'],
    )
    dst = path.join(d, dname)
if path.exists(dst):
    error('DESTINATION exists: %r' % dst)

print 'SOURCE: %r' % src
print 'DESTINATION: %r' % dst

import gobject
import gst

gobject.threads_init()

#mp4mux

def set_props(element, kind):
    props = c[kind].get('props')
    if not props:
        return
    for key in sorted(props):
        value = props[key]
        print ' %s: %r' % (key, value)
        element.set_property(key, value)

def make_enc(kind):
    print 'creating %r encoder:' % kind
    enc = c[kind]['enc']
    print ' enc: %r' % enc
    element = gst.element_factory_make(enc)
    set_props(element, kind)
    return element

class AudioTranscoder(gst.Bin):
    def __init__(self):
        gst.Bin.__init__(self)

        # Create elements
        self._inq = gst.element_factory_make('queue')
        self._conv = gst.element_factory_make('audioconvert')
        self._rate = gst.element_factory_make('audiorate')
        self._enc = make_enc('audio')
        self._outq = gst.element_factory_make('queue')
        elements = (self._inq, self._conv, self._rate, self._enc, self._outq)

        # Add to bin and link:
        self.add(*elements)
        self._inq.link(self._conv)
        caps = gst.caps_from_string(c['audio']['caps'])
        self._conv.link(self._rate, caps)
        gst.element_link_many(self._rate, self._enc, self._outq)

        # Add ghostpads
        self.add_pad(gst.GhostPad('sink', self._inq.get_pad('sink')))
        self.add_pad(gst.GhostPad('src', self._outq.get_pad('src')))

class VideoTranscoder(gst.Bin):
    def __init__(self):
        gst.Bin.__init__(self)

        # Create elements
        self._inq = gst.element_factory_make('queue')
        #self._scale = gst.element_factory_make('cogscale')
        self._scale = gst.element_factory_make('ffvideoscale')
        self._enc = make_enc('video')
        self._q = gst.element_factory_make('queue')
        self._outq = gst.element_factory_make('queue')

        # Set properties
        #self._scale.set_property('quality', 10)
        self._scale.set_property('method', 9)

        # Add to bin and link:
        self.add(
            self._inq, self._scale,
            self._q, self._enc,
            self._outq,
        )
        self._inq.link(self._scale)
        gst.element_link_many(self._q, self._enc, self._outq)

        size = size_map.get(options.size)
        if size:
            caps = gst.caps_from_string(
                'video/x-raw-yuv, width=%d, height=%d' % size
            )
            self._scale.link(self._q, caps)
        else:
            self._scale.link(self._q)

        # Add ghostpads
        self.add_pad(gst.GhostPad('sink', self._inq.get_pad('sink')))
        self.add_pad(gst.GhostPad('src', self._outq.get_pad('src')))

class Transcoder(object):
    def __init__(self, src, dst):
        self.error = None
        self.mainloop = gobject.MainLoop()
        self.pipeline = gst.Pipeline()
        self._start = None
        self._end = None

        # Create bus and connect several handlers
        self.bus = self.pipeline.get_bus()
        self.bus.add_signal_watch()
        self.bus.connect('message::eos', self.on_eos)
        self.bus.connect('message::tag', self.on_tag)
        self.bus.connect('message::error', self.on_error)

        # Create elements
        self.src = gst.element_factory_make('filesrc')
        self.dec = gst.element_factory_make('decodebin2')
        self.audio = AudioTranscoder()
        self.video = VideoTranscoder()

        self.mux = gst.element_factory_make(c['mux'])
        self.sink = gst.element_factory_make('filesink')

        # Set properties
        self.src.set_property('location', src)
        self.sink.set_property('location', dst)

        # Connect handler for 'new-decoded-pad' signal
        self.dec.connect('new-decoded-pad', self.on_new_decoded_pad)

        # Add elements to pipeline
        self.pipeline.add(
            self.src, self.dec,
            self.audio, self.video,
            self.mux, self.sink,
        )

        # Link *some* elements
        # This is completed in self.on_new_decoded_pad()
        self.src.link(self.dec)
        self.audio.link(self.mux)
        self.video.link(self.mux)
        self.mux.link(self.sink)

        # Reference used in self.on_new_decoded_pad()
        self.apad = self.audio.get_pad('sink')
        self.vpad = self.video.get_pad('sink')

    def run(self):
        print 'Starting pipeline...'
        self._start = time.time()
        self.pipeline.set_state(gst.STATE_PLAYING)
        self.mainloop.run()

    def elpased(self):
        if None in (self._start, self._end):
            return
        return minsec(self._end - self._start)

    def kill(self):
        print 'Killing pipeline...'
        self.pipeline.set_state(gst.STATE_NULL)
        self.pipeline.get_state()
        self._end = time.time()
        self.mainloop.quit()

    def on_new_decoded_pad(self, element, pad, last):
        caps = pad.get_caps()
        name = caps[0].get_name()
        print 'on_decoded_pad:', name
        if name.startswith('audio/'):
            if not self.apad.is_linked(): # Only link once
                print ' Linking audio'
                pad.link(self.apad)
        elif name.startswith('video/'):
            if not self.vpad.is_linked(): # Only link once
                print ' Linking video'
                pad.link(self.vpad)

    def on_eos(self, bus, msg):
        print 'on_eos'
        self.kill()

    def on_tag(self, bus, msg):
        taglist = msg.parse_tag()
        print 'on_tag:'
        for key in taglist.keys():
            print ' %s = %s' % (key, taglist[key])

    def on_error(self, bus, msg):
        self.error = msg.parse_error()[1]
        self.kill()

t = Transcoder(src, dst)
t.run()

if t.error:
    error(t.error)

#check_call(['ogginfo', dst])

print """
Source:
  %r
Destination:
  %r
Transcoded in %s
""" % (src, dst, t.elpased())

Blueprint information

Status:
Not started
Approver:
None
Priority:
Medium
Drafter:
TLE
Direction:
Approved
Assignee:
TLE
Definition:
Approved
Series goal:
None
Implementation:
Not started
Milestone target:
None

Related branches

Sprints

Whiteboard

The details are in the description. In the long run, this change will allow for other music playing modules to be made, e.g. using libvlc, which in turn will make windows support easier to accomplish.

(?)

Work Items

This blueprint contains Public information 
Everyone can see this information.

Subscribers

No subscribers.