#!/usr/bin/env python
#
# entrans.py
# Copyright (C) 2006 Mark Nauwelaerts <mnauw@users.sourceforge.net>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library 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
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
# Boston, MA 02110-1307, USA.


import sys, traceback, time, signal, os, stat, copy
import optparse, ConfigParser, re

import gobject
gobject.threads_init()

import pygst
pygst.require('0.10')
import gst
import gst.extend.pygobject

# add_buffer_probe ignores user_data

# -- Auxiliary Functions

# prevent deprecation warnings while remaining backwards compatible
if not 'parse_bin_from_description' in dir(gst):
  gst.parse_bin_from_description = gst.gst_parse_bin_from_description

# converts given value (typically Python string) to the given gtype (e.g. INT)
# (may throw exception if conversion fails)
def gobject_convert_value(value, gtype):
  if gtype in (gobject.TYPE_INT, gobject.TYPE_UINT,
               gobject.TYPE_LONG, gobject.TYPE_ULONG,
               gobject.TYPE_INT64, gobject.TYPE_UINT64):
    value = int(value)
  elif gtype == gobject.TYPE_BOOLEAN:
    if value == 'False':
      value = False
    elif value == 'True':
      value = True
    elif isinstance(value, str):
      if value.lower() in ['f', 'false', 'no', '0', 'off']:
        value = False
    else:
      value = bool(value)
  elif gtype in (gobject.TYPE_DOUBLE, gobject.TYPE_FLOAT):
    value = float(value)
  elif gtype == gobject.TYPE_STRING:
    value = str(value)
  elif gtype == gst.Caps.__gtype__:
    value = gst.caps_from_string(value)
  # leave untouched for other cases, e.g. enums
  return value

# based on gst.extend.pygobject
def gobject_convert_property(object, property, value):
  """
  Convert the given value for the given property to the proper type
  in a sensible manner.

  @type object:   L{gobject.GObject}
  @type property: string
  @param value:   value intended for the property
  """
  for pspec in gobject.list_properties(object):
    if pspec.name == property:
        break
  else:
    raise TypeError(
      "Property '%s' in element '%s' does not exist" % (
          property, object.get_property('name')))

  try:
    value = gobject_convert_value(value, pspec.value_type)
  except ValueError:
    msg = "Invalid value given for property '%s' in element '%s'" % (
        property, object.get_property('name'))
    raise ValueError(msg)
  return value

def gobject_set_property(object, property, value):
  object.set_property(property, gobject_convert_property(object, property, value))

# string representation for a property-value, particularly enum
def prop_to_str(value):
  if hasattr(value, 'value_name'):
    return value.value_nick + ' - ' + value.value_name
  else:
    return str(value)

def caps_to_short_str(caps):
  ncaps = gst.Caps()
  for s in caps:
    s = s.copy()
    for f in s.keys():
      if gobject.type_name(s.get_field_type(f)) in ['GstValueArray', 'GstBuffer']:
        s[f] = str(s.get_field_type(f))
    ncaps.append(s)
  return ncaps

def element_is_src(element):
  return not list(element.sink_pads())

def element_is_sink(element):
  return not list(element.src_pads())

# auxiliary for below, normalize a (towards downstream ordered) list of pads:
# - remove proxy pads
# - ghostpad is replaced by ghostpad and its target (in the proper order)
def pad_list_normalize(padlist):
  res = []
  for pad in padlist:
    if gobject.type_name(pad) == 'GstProxyPad':
      continue
    elif isinstance(pad, gst.GhostPad):
      if pad.get_direction() == gst.PAD_SRC:
        res.extend([pad, pad.get_target()])
      else:
        res.extend([pad.get_target(), pad])
    else:
      res.append(pad)
  return res

# returns list of pads (upstream or downstream) of given pad (including)
# - descends into bins
# - ignores proxy pads
# - also includes targets of ghostbin
# - list begins at pad (and so upstream or downstream)
# - at junctions, only 1 path is traced (upstream or downstream)
# - up to count pads (not counting pad) are traced

def pad_get_pred(pad, count = 100):
  def pad_get_pred_rec(pad, count):
    if count < 0:
      return []
    if pad.get_direction() == gst.PAD_SRC:
      try:
        peer = pad.iterate_internal_links().next()
      except (StopIteration, TypeError):
        peer = None
    else:
      peer = pad.get_peer()
    if not peer:
      return [pad]
    else:
      if gobject.type_name(pad) != 'GstProxyPad':
        res = pad_get_pred_rec(peer, count - 1)
        res.insert(0, pad)
      else:
        res = pad_get_pred_rec(peer, count)
      return res
  return pad_list_normalize(pad_get_pred_rec(pad, count))

def pad_get_succ(pad, count = 100):
  def pad_get_succ_rec(pad, count):
    if count < 0:
      return []
    if pad.get_direction() == gst.PAD_SINK:
      try:
        peer = pad.iterate_internal_links().next()
      except (StopIteration, TypeError):
        peer = None
    else:
      peer = pad.get_peer()
    if not peer:
        return [pad]
    else:
      if gobject.type_name(pad) != 'GstProxyPad':
        res = pad_get_succ_rec(peer, count - 1)
        res.insert(0, pad)
      else:
        res = pad_get_succ_rec(peer, count)
      return res
  res = []
  return list(reversed(pad_list_normalize(reversed(pad_get_succ_rec(pad, count)))))

def pad_is_caps(pad, caps_fragment, klass_fragment):
  pads = pad_get_pred(pad)
  pads.extend(pad_get_succ(pad))
  gst.debug('Checking %s against %s, %s' % (pad, caps_fragment, klass_fragment))
  for p in pads:
    factory = p.get_parent_element().get_factory()
    caps = p.get_negotiated_caps()
    gst.log('Negotiated caps on ' + str(p) + ': ' + str(caps))
    if not caps:  # may not yet be negotiated
      caps = p.get_pad_template_caps()
    gst.log(str(p) + ': class ' + factory.get_klass() + ', caps ' + str(caps))
    # note that some app types also look like video/...
    if factory.get_klass().find(klass_fragment) >= 0 or \
      re.search(klass_fragment.lower(), p.get_name()) or \
      re.search(caps_fragment, str(caps)): # and caps[0].n_fields() > 0):
      return True
  return False

def pad_is_sub(pad):
  subcaps = [ 'text/plain', 'video/x-dvd-subpicture', 'application/x-ssa',
      'application/x-ass', 'application/x-usf', 'application/x-subtitle-unknown']
  return pad_is_caps(pad, '^(' + '|'.join(subcaps) + ')', 'nomatch')

def pad_is_video(pad):
  return not pad_is_sub(pad) and pad_is_caps(pad, 'video/x-raw', 'Video')

def pad_is_audio(pad):
  return pad_is_caps(pad, 'audio/x-raw', 'Audio')

def pad_is_current(pad):
  pads = pad_get_pred(pad)
  for p in pads:
    if p.get_name()[0:8] == 'current_':
      return True
  return False

def pad_stream_type(pad):
  if pad_is_sub(pad):
    return 'sub'
  elif pad_is_video(pad):
    return 'video'
  elif pad_is_audio(pad):
    return 'audio'
  else:
    return 'unknown'

# set of stream types of sink pads
def element_stream_types(element):
  return set([pad_stream_type(pad) for pad in element.sink_pads()])

def element_is_sub(element):
  return 'sub' in element_stream_types(element)

def element_is_video(element):
  return 'video' in element_stream_types(element)

def element_is_audio(element):
  return 'audio' in element_stream_types(element)

# returns immediate upstream neighbour(s)
def element_pred(element):
  res = []
  for x in element.sink_pads():
    p = pad_get_pred(x, 1)
    if len(p) > 1 and p[1].get_parent_element():
      res.append(p[1].get_parent_element())
  return res

# returns immediate downstream neighbour(s)
def element_succ(element):
  res = []
  for x in element.src_pads():
    p = pad_get_succ(x, 1)
    if len(p) > 1 and p[1].get_parent_element():
      res.append(p[1].get_parent_element())
  return res

def to_str(element):
  return element.get_path_string() + ' [' + gobject.type_name(element) + ']'

# property matching
# patterns - list of re (strings)
# element - GstObject
# prop - name of property
#
# forms the strings:
#  <element factory name>.prop  (if GstElement)
#  <element name>.prop
#  <element_path>.prop
# and checks if it matches any of the re's given
# (if prop is not given, .prop is omitted above)
def object_match_prop(object, patterns, prop = None):
  checks = []
  if not prop:
    prop = ''
  else:
    prop = '.' + prop
  if isinstance(object, gst.Element):
    factory = object.get_factory()
    name = factory.get_name()
    checks.append(name + prop)
  checks.append(object.get_name() + prop)
  checks.append(object.get_path_string() + prop)
  for pat in patterns:
    for check in checks:
      if re.match(pat, check):
        return True
  return False

# message matching
# patterns - list of re (strings)
# message - a message (from element)
#
# forms the strings:
# element = sending element
# type = printable name of message type
# name = name of structure (if any)
#  <element factory name>.type[.name]  (if GstElement)
#  <element name>.type[.name]
#  <element_path>.type[.name]
# and checks if it matches any of the re's given
def message_match(message, patterns):
  checks = []
  object = message.src
  postfix = '.' + message.type.first_value_nick
  if message.structure:
    postfix += '.' + message.structure.get_name()
  if isinstance(object, gst.Element):
    factory = object.get_factory()
    name = factory.get_name()
    checks.append(name + postfix)
  checks.append(object.get_name() + postfix)
  checks.append(object.get_path_string() + postfix)
  for pat in patterns:
    for check in checks:
      if re.match(pat, check):
        return True
  return False

# perform wildcard expansion on capsfilter in launch line
def expand_caps(launch):
  mimetypes = ['video/x-raw-yuv', 'video/x-raw-rgb',
               'audio/x-raw-int', 'audio/x-raw-float']
  expr = re.compile(r'^(.*)!.*?((?:(?:video)|(?:audio))/.*?[*].*?),(.*?)!(.*)$')
  m = expr.search(launch)
  while m:
    prefix = m.group(1)
    mime = m.group(2)
    props = m.group(3)
    postfix = m.group(4)
    caps = []
    for m in mimetypes:
      if re.search(mime.replace('*', '.*'), m):
        caps.append(m + ',' + props)
    if not caps:
      raise Exception, 'Failed to expand mimetype ' + mime
    launch = " ! ".join([prefix, " ; ".join(caps), postfix])
    m = expr.search(launch)
  gst.debug("Returning launch line " + launch)
  return launch

# format given time to something user friendly
# pretty much inspired by other code ...
# time - in gst units
# msecond - number of milli-second digits (may be 0)
def get_time_as_str(time, msecond = 3):
  ret = ''
  if not msecond:
    msecond = -1
  for div, sep, mod, pad in ((gst.SECOND*3600, '', 0, 0),
                              (gst.SECOND*60, ':', 60, 2),
                              (gst.SECOND, ':', 60, 2),
                              (gst.MSECOND, '.', 1000, msecond)):
    n = time // div
    if mod:
        n %= mod
    if pad != -1:
      ret += sep + ('%%0%dd' % pad) % n
  return ret

# parse time given in H:M:S.MS format
# - leading parts are optional, so is MS
# - MS can have up to gst unit precision
# time - string to parse
# raises exception if format error
def parse_time_from_str(time):
  try:
    l = time.split('.')
    if len(l) <= 2:
      if len(l) == 1:
        l.append('0')
      m = l[0].split(':')
      if len(m) <= 3:
        l[0:1] = m
        l.reverse()
        if not l[1]:
          l[1] = '0'
        t = 0
        for v, w in zip(l, (gst.SECOND//(10**len(l[0])), gst.SECOND,
                            gst.SECOND*60, gst.SECOND*3600)):
          t += int(v) * w
        return t
  except:
    pass
  # something was not valid
  raise ValueError, "Invalid time format in " + time

# returns all elements in the bin (recursively)
# (topologically) sorted from src to sink
def bin_sorted_recursive(bin, include_bins = False):
  result = []
  tmp = list(bin.sorted())
  tmp.reverse()
  # TODO bin.sorted may return duplicates (in case of multiple sinks)
  have_seen = []
  for element in tmp:
    if element in have_seen:
      continue
    have_seen.append(element)
    if isinstance(element, gst.Bin):
      result.extend(bin_sorted_recursive(element, include_bins))
    else:
      result.append(element)
  if include_bins:
    result.insert(0, bin)
  return result

def clone_element(element, name = 0):
  return element.get_factory().create('temp' + str(name))

# -- END

# --
# --
# configuration class, responsible for
# - option parsing
# - option/configuration store
class Configuration:

  def __init__(self):
    # -- PUBLIC
    # non-option leftover arguments
    self.args = None
    self.optparser = None
    # also see below for configuration attributes
    # -- PRIVATE
    self.parser = None
    self.options = None
    # properties: 3-level dict: (element, (prop, (time, val)))
    # time is -1 if it is not a controlled property setting
    self.props = { }
    # keep refs to controllers
    self.controllers = []

    usage = 'usage: %prog [options] -- [--raw PIPELINE] ' + \
          '[--video PIPELINE] [--audio PIPELINE] [--other PIPELINE]'
    version = '%prog 0.10.3'
    optparser = optparse.OptionParser(usage=usage, version=version)
    self.optparser = optparser
    optparser.add_option('-i', '--inputfile', help='Input file or URI')
    optparser.add_option('-o', '--outputfile', help='Output file')
    optparser.add_option('--decoder', default='decodebin',
        help='Decoder element')
    optparser.add_option('--muxer',
        help='Muxer element; overrides default choice from -o')
    optparser.add_option('--vn', action='append', metavar='#no,#no,...',
        help='Video stream numbers')
    optparser.add_option('--an', action='append', metavar='#no,#no,...',
        help='Audio stream numbers')
    optparser.add_option('--on', action='append', metavar='#no,#no,...',
        help='Other stream numbers')
    optparser.add_option('--sync-link', action='store_true',
        help='Synchronous linking [false]')
    optparser.add_option('--no-sync-link', dest='sync-link', action='store_false')
    optparser.add_option('--at', action='append', metavar='tag,...',
        help='Audio stream language tag pattern')
    optparser.add_option('--stamp', action='store_true',
        help='Re-sequence timestamps [true]')
    optparser.add_option('--no-stamp', dest='stamp', action='store_false')
    optparser.add_option('-c', '--cut', action='append', metavar='T1-T2,T3-T4,...',
        help='Only process this part of the stream')
    optparser.add_option('-s', '--section', type='choice', metavar='METHOD',
        choices=['seek', 'seek-key', 'cut', 'cut-time'], default='seek',
        help='Section selection mode [%default]')
    optparser.add_option('-a', '--accurate', action='store_true',
        help='Use sample accurate cutting (if not segment/seeking)')
    optparser.add_option('--no-accurate', dest='accurate', action='store_false')
    optparser.add_option('--dam', action='store_true',
        help='Dams are used (in raw pipeline)')
    optparser.add_option('--no-dam', dest='dam', action='store_false')
    optparser.add_option('-f', '--framerate', metavar='NUM/DENOM', default='25/1',
        help='Fallback framerate if none automagically discovered [%default]')
    optparser.add_option('-b', '--block-overrun', action='store_true',
        help='Prevent queue size adjustments')
    optparser.add_option('--no-block-overrun', dest='block_overrun', action='store_false')
    optparser.add_option('--set-prop', action='append', metavar='ELEMENT:PROP:VALUE',
        help='Set property PROP')
    optparser.add_option('--vb', type='int', metavar='kbitrate', default='0',
        help='Target video bitrate')
    optparser.add_option('--ab', type='int', metavar='kbitrate', default='0',
        help='Target audio bitrate')
    optparser.add_option('--vq', type='int', metavar='quantizer/quality', default='0',
        help='Constant video quantizer or quality')
    optparser.add_option('--aq', metavar='quality', default='0',
        help='Audio encoding quality')
    optparser.add_option('--pass', type='int', metavar='0|1|2', default='0',
        help='Pass 1/2 of 2-pass encoding')
    optparser.add_option('-t', '--tag', action='append', metavar='TAG:VALUE',
        help='Set tag')
    optparser.add_option('-d', '--delay', default=2, type='int', metavar='SECONDS',
        help='Delay between progress updates [%default]')
    optparser.add_option('--timeout', default=4, type='int', metavar='SECONDS',
        help='Timeout between successive stages [%default]')
    optparser.add_option('--progress-fps', action='store_true',
        help='Also provide proc speed in fps')
    optparser.add_option('--no-progress-fps', dest='progress_fps', action='store_false')
    optparser.add_option('--progress-real', action='store_true',
        help='Calculate speed based on real-time, not cpu time')
    optparser.add_option('--no-progress-real', dest='progress_real', action='store_false')
    optparser.add_option('-m', '--messages', action='store_true',
        help='Output messages')
    optparser.add_option('--no-messages', dest='messages', action='store_false')
    optparser.add_option('--display-msg', action='append', metavar='MSGPATTERN,...',
        help='Only inform about matching messages')
    optparser.add_option('--ignore-msg', action='append', metavar='MSGPATTERN,...',
        help='Ignore matching messages' )
    optparser.add_option('-v', '--verbose', action='store_true',
        help='Output property notifications')
    optparser.add_option('--no-verbose', dest='verbose', action='store_false')
    optparser.add_option('--short-caps', action='store_true',
        help='Output short versions of caps, e.g. buffer dumps')
    optparser.add_option('--no-short-caps', action='store_false')
    optparser.add_option('-x', '--exclude', action='append', metavar='PROPPATTERN,...',
        help='Do not output status information of matching properties')
    optparser.add_option('--include', action='append', metavar='PROPPATTERN,...',
        help='Ignore status information of matching properties')
    optparser.add_option('--display-prop', action='append', metavar='PROPPATTERN,...',
        help='Provide info on matching properties')
    optparser.add_option('--ignore-prop', action='append', metavar='PROPPATTERN,...',
        help='Ignore matching properties' )
    optparser.add_option('--config', help='Configuration file',
        metavar='CONFIGFILE')
    optparser.add_option('--profile', metavar='PROFILE',
        help='Use settings from profile file')
    optparser.add_option('--save', action='append', metavar='MESSAGE:FILE:APPEND,...',
        help='Save messages')

    (self.options, self.args) = optparser.parse_args()

    # get configuration from file
    if self.options.config:
      file = [self.options.config]
    else:
      file = ['.gst-entrans', os.path.expanduser('~/.gst-entrans')]
    if self.options.profile:
      file.extend([self.options.profile,
        os.path.expanduser(os.path.join('~', self.options.profile))])
    self.parser = ConfigParser.SafeConfigParser()
    self.parser.read(file)

    # initialize config store
    self.__dict__.update({ 'sections': [], # list of (start: , end: )
                           'tag': { },     # dict of (tag, value)
                           'save': { },    # dict of (message, (file: , append: ))
                           'exclude': [], 'include': [],            # list of regexps
                           'ignore_prop': [], 'display_prop': [],   # list of regexps
                           'ignore_msg': [], 'display_msg': [],     # list of regexps
                           'at': [],             # list of regexps
                           'an': [], 'vn': [], 'on': [],  # list of numbers
                           'fps': None
                           })

    # convert properties to internal dict storage
    for section in self.parser.sections():
      if section != 'options':
        self.props[section] = { }
        for option in self.parser.options(section):
          self.props[section][option] = { -1: self.parser.get(section, option) }

    # -- tiny util for opt processing
    # returns a list of which the items no longer contain separator sep
    def flatten(opt, sep = ','):
      if isinstance(opt, str):
        opt = [opt]
      elif isinstance(opt, list):
        pass
      else: # should not be
        return opt
      if sep:
        nv = []
        for t in opt:
          nv.extend(t.split(','))
        return nv
      else:
        return opt

    sources = []
    if self.parser.has_section('options'):
      sources.append(self.parser.items('options'))
    sources.append(self.options.__dict__.iteritems())
    for s in sources:
      for k, v in s:
        k = k.replace('-', '_')
        if not k in self.options.__dict__:
          optparser.error('Invalid option ' + k + ' in configuration file.')
        else:
          if k in ['inputfile', 'outputfile', 'muxer', 'decoder']:
            self.__dict__[k] = v
          elif k in ['verbose', 'messages', 'block_overrun', 'dam', 'accurate',
                     'progress_real', 'progress_fps', 'short_caps', 'stamp',
                     'sync_link']:
            # booleans
            # if not mentioned at all anywhere, must be False
            if v == None:
              if not self.__dict__.has_key(k):
                if k == 'stamp': # exception: default True
                  v = True
                else:
                  v = False
              else:
                continue
            if v == True or v == False:
              v = bool(v)
            elif v.upper() in ['1', 'ON', 'TRUE', 'YES']:
              v = True
            elif v.upper() in ['0', 'OFF', 'FALSE', 'NO']:
              v = False
            else:
              optparser.error('Invalid value ' + v + ' for ' + k)
            self.__dict__[k] = v
          elif k == 'vb':
            self.vkbitrate = int(v)
          elif k == 'ab':
            self.akbitrate = int(v)
          elif k == 'vq':
            self.vquantizer = int(v)
          elif k == 'aq':
            self.aquantizer = float(v)
          elif k == 'pass':
            v = int(v)
            self.encpass = v
            if v < 0 or v > 2:
              optparser.error('Invalid value ' + v + ' for ' + k)
          elif k == 'delay':
            self.progress = int(v) * 1000
          elif k == 'timeout':
            self.timeout = int(v) * 1000
          elif k in ['vn', 'an', 'on']:
            if v:
              v = flatten(v)
              for t in v:
                if not t.isdigit():
                  optparser.error('Invalid value ' + t + ' for ' + k)
              self.__dict__[k].extend([int(t) for t in v])
          elif k == 'framerate':
            n = v.split('/')
            num = 'wrong'
            denom = 'wrong'
            if len(n) == 2:
              num = n[0]
              denom = n[1]
            if len(n) == 1:
              num = n[0]
              denom = '1'
            if num.isdigit() and denom.isdigit():
              self.fps = gst.Fraction(int(num), int(denom))
              continue
            optparser.error('Invalid framerate in ' + v)
          elif k == 'save' and v:
            v = flatten(v)
            for message in v:
              data = message.split(':')
              if len(data) < 2 or len(data) > 3:
                optparser.error('invalid spec for -s')
              # check that we can create the file and empty it now for later use
              filename = data[1].replace('${n}', 'dummy')
              try:
                f = open(filename, 'w')
              except:
                optparser.error('could not create ' + filename + ' for writing')
              f.close()
              os.remove(filename)       # clean up test
              self.save[data[0]] = { 'file': data[1] }
              if len(data) == 3:
                self.save[data[0]]['append'] \
                   = data[2].lower() in ['1','t', 'true', 'yes']
              else:
                self.save[data[0]]['append'] = False
          elif k == 'tag' and v:
            for message in flatten(v, None):
              data = message.split(':')
              if len(data) != 2:
                optparser.error('Invalid spec for -t')
              if not gst.tag_exists(data[0]):
                optparser.error('Invalid tag ' + data[0])
              self.tag[data[0]] = data[1]
          elif k in ['include', 'exclude', 'display_prop', 'ignore_prop',
                     'display_msg', 'ignore_msg'] and v:
            self.__dict__[k].extend(flatten(v))
            for exp in self.__dict__[k]:
              try:
                re.compile(exp)
              except:
                optparser.error('Invalid regexp ' + exp + ' for ' + k)
          elif k == 'set_prop' and v:
            for d in flatten(v, None):
              data = d.split(':')
              if len(data) < 3:
                optparser.error('Invalid spec for --set-prop')
              if len(data) > 3:
                try:
                  time = parse_time_from_str(':'.join(data[3:]))
                except Exception, e:
                  optparser.error('Invalid spec for %s: %s' % (k, str(e)))
              else:
                time = -1
              section = data[0]
              opt = data[1]
              if not section in self.props:
                self.props[section] = { }
              if not opt in self.props[section]:
                self.props[section][opt] = { }
              self.props[section][opt][time] = data[2]
          elif k == 'at' and v:
            v = flatten(v)
            for exp in v:
              try:
                re.compile(exp)
              except:
                optparser.error('Invalid regexp ' + exp + ' for ' + k)
            self.at.extend(v)
          elif k == 'section':
            if v == 'seek':
              self.seek = NonLinPipeline.SECTIONS_SEEK
            elif v == 'seek-key':
              self.seek = NonLinPipeline.SECTIONS_SEEK_KEY
            elif v == 'cut':
              self.seek = NonLinPipeline.SECTIONS_CUT
            elif v == 'cut-time':
              self.seek = NonLinPipeline.SECTIONS_CUT_TIME
          elif k == 'cut' and v:
            cut = flatten(v)
            no_more = False
            for section in cut:
              if no_more:
                optparser.error('No section allowed after open-ended section.')
              info = section.strip()
              format = ''
              convert = False
              m = re.match('^(\(?[a-zA-Z]+\)?):(.*)$', info)
              if m:
                format = m.group(1)
                info = m.group(2)
                convert = False
                if (format[0] == '(' and format[-1] != ')') or \
                   (format[0] != '(' and format[-1] == ')'):
                   optparser.error('Invalid format specification in ' + section)
                else:
                  if format[0] == '(':
                    format = format[1:-1]
                    convert = True
                  if format == 'time':
                    format = ''
              info = info.split('-')
              if len(info) == 2 and info[0]:
                for i, v in enumerate(info):
                  v = v.strip()
                  if not v:
                    info[i] = None
                    no_more = True
                  else:
                    if format:
                      if v.isdigit():
                        info[i] = int(v)
                      else:
                        optparser.error('Invalid frame specification ' + v)
                    else:
                      if v[0] in ['f', 'F']:
                        v = v[1:]
                        if v.isdigit():
                          info[i] = -int(v)
                        else:
                          optparser.error('Invalid frame specification ' + v)
                      else:
                        try:
                          info[i] = parse_time_from_str(v)
                        except Exception, e:
                          optparser.error(str(e))
                self.sections.append({ 'start': info[0], 'end': info[1],
                                       'format': format, 'convert': convert })
              else:
                optparser.error('Invalid specification in ' + section)

    if self.vquantizer and self.vkbitrate:
      optparser.error('Only one of --vb and --vq may be given')
    if self.vquantizer and self.encpass:
      optparser.error('Only one of --vb and --pass may be given')
    if self.aquantizer and self.akbitrate:
      optparser.error('Only one of --ab and --aq may be given')
    if self.at and (self.vn or self.an or self.on):
      optparser.error('--at cannot be used with --vn, --an or --on')
    for x in self.sections:
      if x['format'] and not x['convert'] and \
         self.seek in [NonLinPipeline.SECTIONS_CUT,
                           NonLinPipeline.SECTIONS_CUT_TIME]:
        optparser.error('Custom seek format not possible with given cut method')

  def set_plugins(self):
    for section in self.props:
      if 'pf_rank' in self.props[section]:
        factory = gst.element_factory_find(section)
        if factory:
          rank = self.props[section]['pf_rank'][-1]
          if rank.isdigit():
            rank = max(0, min(int(gst.RANK_PRIMARY), int(rank)))
            gst.debug('Setting rank of plugin %s to %d' % (factory.get_name(), rank))
            factory.set_rank(rank)

  def set(self, elements, subst = { }, force = False):
    for element in elements:
      gst.log('Setting properties on ' + element.get_name())
      factory = element.get_factory()
      # FIXME this should somehow be patched in a better way into core,
      #    for the conf benefit of al gst
      # make a pristine copy so we can see if some property has already been set
      newel = factory.create('temporary')
      props = { }
      # most specific setting survives
      for name in [factory.get_name(), element.get_name(), element.get_path_string()]:
        if name in self.props:
          props.update(self.props[name])
      control = { }
      for prop, value in props.iteritems():
        # last check, if value given in pipeline description, that one must win
        # also disregard possible junk that is not really a property
        # NOTE: this is expected to fail hard if user gives non-existing property
        if prop[0:3] != 'pf_':
          if prop not in [p.name for p in element.props]:
            raise TypeError, 'no such property "%s" in element "%s"' \
                    % (prop, element.get_name())
          if force or element.get_property(prop) == newel.get_property(prop):
            for t, v in value.iteritems():
              if t < 0:
                gst.debug('Setting %s.%s to %s' % (element, prop, v))
                gobject_set_property(element, prop, v)
              else: # controlled time-value list
                if control.has_key(prop):
                  control[prop].append((v,t))
                else:
                  control[prop] = [(v,t)]
      # handle controlled props
      if control:
        controller = gst.Controller(element, *control.keys())
        for prop in control:
          controller.set_interpolation_mode(prop, gst.INTERPOLATE_NONE)
          for v, t in control[prop]:
            controller.set(prop, t, gobject_convert_property(element, prop, v))
          # HACK would keep it quiet in the beginning, but not afterwards :-(
          # element.set_property(prop, element.get_property(prop))
        # HACK keep a ref around to the controller, otherwise it is GC
        # and this seems to be the only ref to it ?? bug in core ?
        self.controllers.append(controller)
      # and check for custom stuff
      self.set_special(element, subst)

  # check for and set some special properties; bitrate, quantizer, ...
  def set_special(self, element, subst):
    gst.log('Setting special properties on ' + element.get_name())
    factory = element.get_factory()
    newel = factory.create('temporary')
    if factory.get_klass().find('Audio') >= 0:
      audio = True
    else:
      audio = False
    for pspec in gobject.list_properties(element):
      if not pspec.flags & gobject.PARAM_WRITABLE:
        continue
      if not pspec.flags & gobject.PARAM_READABLE:
        continue
      prop = pspec.name
      if element.get_property(prop) != newel.get_property(prop):
        continue
      # property might not be as we expect
      try:
        if prop == 'bitrate':
          value = element.get_property(prop)
          # is it really in bit or rather in kbit ?
          if value > 10000 or value < 0:
            factor = 1000
          else:
            factor = 1
          if self.vkbitrate and not audio:
            element.set_property(prop, factor * self.vkbitrate)
          if self.akbitrate and audio:
            element.set_property(prop, factor * self.akbitrate)
        elif prop == 'pass':
          # hopefully enum, try int as fall-back
          if self.vquantizer:
            if gobject.type_is_a (pspec.value_type, gobject.TYPE_ENUM):
              gobject_set_property(element, prop, 'quant')
          if self.encpass:
            if gobject.type_is_a (pspec.value_type, gobject.TYPE_ENUM):
              gobject_set_property(element, prop, 'pass' + str(self.encpass))
            else:
              gobject.set_property(element, prop, self.encpass)
        elif prop == 'quantizer' or prop == 'quality':
          if self.vquantizer and not audio:
            gobject_set_property(element, prop, self.vquantizer)
          if self.aquantizer and audio:
            gobject_set_property(element, prop, self.aquantizer)
        # substitutions
        # HACK: only on strings
        elif repr(pspec).find('GParamString') >= 0 \
            and pspec.flags & gobject.PARAM_WRITABLE \
            and pspec.flags & gobject.PARAM_READABLE:
          value = element.get_property(prop)
          if not value:
            value = ""
          newvalue = value
          for s, v in subst.iteritems():
            newvalue = newvalue.replace('${' + s + '}', v)
          if value != newvalue:
            gst.debug('Performed substitutions for prop ' + prop + ' on ' + to_str(element))
            element.set_property(prop, newvalue)
      except TypeError:
        gst.warning('Failed to set special property ' + prop)
# -- END Configuration

# --
# Manages startup of and cutting in a non-linear pipeline.
class NonLinPipeline(gobject.GObject):

  SECTIONS_CUT = 0
  SECTIONS_CUT_TIME = 1
  SECTIONS_SEEK = 2
  SECTIONS_SEEK_KEY = 3

  # !! NOTE !!
  # pads that are blocked will also block a thread when it perform a pad_alloc,
  # and queue is *not* a thread boundary for such a call
  # so a demux loop thread *might* be blocked by this, if it does not handle things right
  # Right typically means it should send new-segments (e.g. from the seeking thread) in one loop,
  # so that all parts can block before anything tries pad_alloc.
  # Some elements send new-segment right after adding a pad (dvd-mpeg), which blocks a pad,
  # so care must be taken when to consider all pads blocked.

  # Possible alternative: use dam with cond-based blocking (replace pad blocking),
  # not blocking on pad-alloc (on event?);
  # should probably hold and queue events for later sending
  # (can take care of new segments, tags and so sent *before* no-more-pads signalled)
  # (see also note in TODO about no-more-pads)

  # * updated: emitted (by client) when a new part has been added to (dynamic) pipeline
  #            (that should be inspected for dam-like candidates)
  # * complete: emitted (by client) when pipeline has been fully constructed
  # * blocked: emitted when a pad reaches blocked state
  # * started: emitted when pipeline is fully constructed/connected
  #             *and* data flow is now unobstructed (after unblocking) on its way
  #      (note !! that buffers and caps may just not yet have reached dams
  #               when emitting or receiving this)
  # (in seek-mode; when it reaches PAUSED; in cut-mode; when all pads have been unblocked)
  # * playing: emitted when pipeline set to PLAYING
  #
  # Both are emitted only once (in either case), and the former prior to the latter.
  __gsignals__ = {'updated': (gobject.SIGNAL_RUN_LAST, None, []),
                  'complete': (gobject.SIGNAL_RUN_LAST, None,[]),
                  'blocked': (gobject.SIGNAL_RUN_LAST, None, [gst.Pad]),
                  'started': (gobject.SIGNAL_RUN_LAST, None, []),
                  'playing': (gobject.SIGNAL_RUN_LAST, None, [])}

  def __init__(self, pipeline, damming, complete, sections, seek,
        precision = False, framerate = None):
    self.__gobject_init__()
    # -- PUBLIC -- only guaranteed valid after *started*
    # the pipeline to manage, it should contain dam element to assist in this
    self.pipeline = pipeline
    # the sections allowed to get past the dams:
    # list of dicts with keys: start, end, format, convert
    # if no format given:
    # pos value: time in gst sense
    # neg value: frame number
    # if (nick name of) format given:
    # pos values for start and end that indicate range in that format
    # if convert, then convert to time and seek in time, otherwise seek in format
    self.sections = sections
    # use precision slicing
    self.precision = precision
    # collects the dams that are found
    # slightly ordered; video first, then audio
    self.dams = []
    # the fps of the stream; will be searched for ...
    self.fps = None
    # .. and use this one if all that fails
    self.default_fps = framerate
    # the method to use to only pass desired sections;
    # one of the values above
    self.seek = seek
    # whether to also perform pad blocking if no sections to seek
    self.do_block = False
    # --
    # -- PRIVATE
    # whether or not to use dams
    self.use_dams = damming
    # (dynamic) pipeline is completed
    self.complete = complete
    # we got NO_PREROLL upon changing state; probably live pipeline
    self.no_preroll = False
    # we are started
    self.started = False
    # probes that are attached
    self.probes = dict()
    # pads that should be blocked and have been blocked
    self.to_block_pads = []
    self.blocked_pads = []
    # dams that are about to see data
    self.data_count = 0
    # private 'mirror' of sections used for *seek*ing
    # (setup as a transformation later on)
    self.seek_sections = None
    # next_segment to seek to
    self.next_segment = 0
    # target element to use for seek, query, etc
    self.target = None
    # -- default signal handlers
    self.connect('complete', self.cb_complete)
    self.connect('updated', self.cb_updated)
    self.connect('started', self.cb_started)
    # -- normalization
    if not self.sections:
      self.seek = self.SECTIONS_SEEK
    if not self.default_fps:
      self.default_fps = gst.Fraction(25, 1)

  def cb_complete(self, object):
    gst.log('Pipeline complete')
    if not self.complete:
      self.complete = True
      self.check_pads_blocked()

  def cb_updated(self, object):
    gst.log('Pipeline updated')
    if not self.use_dams:
      self.find_dams()

  def cb_started(self, object):
    self.started = True
    # make sure things are ok for clients
    self.find_fps()
    self.sort_dams()

  def sort_dams(self):
    gst.debug("Sorting dams")
    # don't do this over and over again
    if self.started:
      return
    gst.debug("Actually sorting dams")
    result = []
    # super-simplistic sort
    for dam in self.dams:
      if element_is_video(dam):
        result.append(dam)
    for dam in self.dams:
      if dam not in result:
        result.append(dam)
    self.dams = result

  def find_fps(self):
    # don't overwrite or search for it if already have fps
    if self.fps:
      return
    # if more than one framerate out there, the one most upstream wins
    for element in bin_sorted_recursive(self.pipeline):
      for pad in element.pads():
        caps = pad.get_negotiated_caps()
        if caps and caps[0].has_key('framerate') and caps[0]['framerate'].num != 0:
          gst.debug('Found framerate on ' + str(pad) + ' in caps ' + str(caps))
          self.fps = caps[0]['framerate']
          break
    if not self.fps:
      gst.warning("No framerate has been found. Defaulting")
      self.fps = self.default_fps
    return self.fps

  def convert_to_time(self, frame):
    if frame and frame < 0:
      return -frame * gst.SECOND * self.fps.denom / self.fps.num
    else:
      return frame

  def seek_next_segment(self):
    if self.next_segment >= len(self.sections):
      return
    start = self.seek_sections[self.next_segment]['start']
    end = self.seek_sections[self.next_segment]['end']
    format = self.seek_sections[self.next_segment]['format']
    self.next_segment = self.next_segment + 1

    if not format:
      format = gst.FORMAT_TIME

    flags = 0
    if self.next_segment == 1:
      flags = gst.SEEK_FLAG_FLUSH
    if self.next_segment < len(self.sections):
      flags = flags | gst.SEEK_FLAG_SEGMENT
    if self.seek == self.SECTIONS_SEEK_KEY:
      flags = flags | gst.SEEK_FLAG_KEY_UNIT

    gst.info("Seeking from " + str(start) + " to " + str(end))
    if not end:
      endtype = gst.SEEK_TYPE_NONE
      end = 0
    else:
      endtype = gst.SEEK_TYPE_SET
    if self.target.seek(1.0, format, flags, gst.SEEK_TYPE_SET, start, endtype, end):
      gst.info("Seek succeeded!")
    else:
      gst.error("Seek failed!")

  def cut_next_segment(self):
    gst.log("Providing section information.")
    for dam in self.dams:
      gst.debug("Providing section information to " + str(dam))
      for section in self.sections:
        dam.set_property('begin-time', section['start'])
        end = section['end']
        if not end:
          end = gst.CLOCK_TIME_NONE
        dam.set_property('end-time', end)
        dam.set_property('save-section', True)

  def cb_on_pad_blocked_sync(self, pad, is_blocked):
    # cb can be called again; unblocking, after seeking, before unblocking
    if is_blocked:
      state = "blocked"
    else:
      state = "unblocked"
    gst.debug("Pad " + str(pad) + " has " + state + ".")
    if pad not in self.blocked_pads:
      self.blocked_pads.append(pad)
      self.check_pads_blocked()
      self.emit('blocked', pad)

  def cb_notify_start(self):
    self.emit('started')
    # remove from main loop
    return False

  def check_pads_blocked(self):
    # pads may be incrementally added (dynamic case)
    gst.log("Checking block_pads " + str(self.blocked_pads) + " == " + str(self.to_block_pads))
    if not (self.blocked_pads
        and len(self.blocked_pads) == len(self.to_block_pads) and self.complete):
      return
    gst.info("All pads have blocked, scheduling seek")
    gobject.idle_add(self.handle_pads_blocked)

  def handle_pads_blocked(self):
    # should be some framerate info somewhere by now
    self.find_fps()
    # put dams right order, so we focus on a video one if needed
    self.sort_dams()
    if self.dams:
      self.target = self.dams[0]
    else:
      self.target = self.pipeline
    # normalize sections
    self.seek_sections = []
    for s in self.sections:
      ss = {}
      format = ss['format'] = s['format']
      if format:
        format = gst.format_get_by_nick(format)
        if not format:
          gst.error("Failed to determine custom format; ignoring section!")
          s['start'] = s['end'] = 0
          continue
        ss['start'], ss['end'] = s['start'], s['end']
        ss['format'] = format
        s['start'] = s['end'] = None
        try:
          if ss['start']:
            qformat, s['start'] = \
                self.target.query_convert(format, ss['start'], gst.FORMAT_TIME)
          if ss['end']:
            qformat, s['end'] = \
                self.target.query_convert(format, ss['end'], gst.FORMAT_TIME)
          if (ss['start'] and not s['start']) or (ss['end'] and not s['start']):
            raise
        except:
          gst.error("Failed to convert custom position; no time info!")
          # FIXME (above as well) cutting dam might not like this ...
          s['start'] = s['end'] = 0
        if s['convert']:
          ss['format'] = ''
          ss['start'], ss['end'] = s['start'], s['end']
      else:
        s['start'] = self.convert_to_time(s['start'])
        s['end'] = self.convert_to_time(s['end'])
        ss['start'], ss['end'] = s['start'], s['end']
      self.seek_sections.append(ss)
    # hm, only now we can fully check whether given sections were valid
    valid = True
    last = 0
    for section in self.sections:
      if section['end'] and section['start'] > section['end']:
        valid = False
      if section['start'] < last \
         and self.seek in [self.SECTIONS_CUT, self.SECTIONS_CUT_TIME]:
        valid = False
      last = section['end']
    if not valid:
      # FIXME brute way out
      raise Exception('Invalid section specification: impossible order')
    # get data flow going
    if self.seek in [self.SECTIONS_CUT, self.SECTIONS_CUT_TIME]:
      self.cut_next_segment()
      self.cb_notify_start()
    else:
      self.seek_next_segment()
    # deblock pads
    gst.info('Unblocking pads')
    for pad in self.blocked_pads:
      pad.set_blocked_async(False, lambda *x: None)
    # remove this from main loop
    gst.debug('Unblocked pads')
    return False

  def cb_linked(self, pad, peer):
    gst.debug("Pad " + str(pad) + " now linked to " + str(peer))
    self.setup_pad_block(pad.get_parent_element())

  # unused
  def cb_unlinked(self, pad, peer):
    if peer in self.pads:
      self.pads.remove(peer)
    # unblock
    peer.set_blocked_async(False, lambda *x: None)

  def setup_pad_block(self, dam):
    # only do blocking if there are any sections
    if self.sections or self.do_block:
      pad = dam.sink_pads().next()
      peer = pad.get_peer()
      if peer:
        # need to mark this here to keep proper count of what has to block
        self.to_block_pads.append(peer)
        peer.set_blocked_async(True, self.cb_on_pad_blocked_sync)
        gst.debug("Pad " + str(peer) + " setup for blocking.")
      else:
        pad.connect('linked', self.cb_linked)

  def find_dams(self):
    gst.log('Looking for dams')
    for element in self.pipeline.recurse():
      if re.match('dam\d*', element.get_name()):
        if element not in self.dams:
          gst.debug("Using " + to_str(element) + " as dam")
          self.dams.append(element)
          self.setup_pad_block(element)

  def cb_message(self, bus, message):
    # if pipeline goes to paused state, set it to playing
    if message.type == gst.MESSAGE_STATE_CHANGED and message.src == self.pipeline:
      gst.debug("Checking state change")
      oldstate, newstate, pending = gst.Message.parse_state_changed(message)
      if newstate == gst.STATE_PAUSED and oldstate == gst.STATE_READY:
        gst.info("Setting pipeline to PLAYING ...")
        res = self.pipeline.set_state(gst.STATE_PLAYING)
        if res == gst.STATE_CHANGE_FAILURE:
          sys.stderr.write("ERROR: pipeline does not want to play\n")
        else:
          if not self.started:
            if self.seek in [self.SECTIONS_CUT, self.SECTIONS_CUT_TIME]:
              message = 'Unexpected state for requested mode; strange things may happen ...'
              if self.no_preroll:
                gst.info(message)
                gst.info('... but probably OK for a live pipeline')
              else:
                gst.error(message)
            self.emit('started')
          # HACK !! now we go and find queues to set min level threshold
          # in order to allow for graceful EOS termination when not using dams
          # (see explanation elsewhere)
          # Note this may lead to a race condition and blocking
          # in case of a very short pipeline
          # (since queue hacks these to 0 when it receives eos ...)
          # Hm, also need to make sure decodebin has not put max-size too low
          # --
          # Even when using dams, it must be assured that a thread passes
          # by a dam, so that this one can send his EOS
          # (keeping a min threshold could help with this)
          gst.debug("HACK: fixing up queues")
          for element in self.pipeline.recurse():
            if gobject.type_name(element) in ['GstQueue', 'GstQueue2', 'GstMultiQueue']:
              if not self.use_dams and gobject.type_name(element) == 'GstQueue':
                # size linked to "collectpads balancing"
                element.set_property('min-threshold-buffers', 10)
              else:
                # disable to avoid short pipeline races
                # and just assume there's enough activity,
                # so that a thread will still pass by each dam
                # (to avoid problem indicated above)
                #element.set_property('min-threshold-buffers', 2)
                pass
              if element.get_property('max-size-bytes') < 50*(2**20):
                element.set_property('max-size-bytes', 50*2**20)
              # queues don't need to buffer wildly
              if element_is_video(element):
                element.set_property('max-size-buffers', 50)
              # a small amount of audio may consist of
              # lots of small (compressed) fragments
              else:
                element.set_property('max-size-buffers', 500)
          # now we have a good state to show
          self.emit('playing')
    elif message.type == gst.MESSAGE_SEGMENT_DONE:
      if self.sections:
        self.seek_next_segment()
    return True

  def cb_sync_message(self, bus, message):
    if message.structure.has_name('dam'):
      if message.structure:
        struct = message.structure.to_string()
      else:
        struct = ""
      gst.debug("Handled Sync Message from element " + message.src.get_name() + "(" \
          + message.type.first_value_nick + "): " + str(struct))
      if message.structure.has_field('announce'):
        # inform of proc mode
        message.src.set_property('segment-mode',
            self.seek not in [self.SECTIONS_CUT, self.SECTIONS_CUT_TIME])
        message.src.set_property('use-count', self.seek == self.SECTIONS_CUT)
        message.src.set_property('precision', self.precision)
        # record it
        self.dams.append(message.src)
        # and start the machinery
        self.setup_pad_block(message.src)

  def start(self):

    bus = self.pipeline.get_bus()
    bus.enable_sync_message_emission()

    # to go from PAUSED to PLAYING
    bus.add_signal_watch()
    bus.connect('message', self.cb_message)

    if self.use_dams:
      # this will set it all in motions when dams are discovered
      bus.connect("sync-message::element", self.cb_sync_message)
    else:
      self.find_dams()

    # FIXME post these messages on the bus ?
    gst.info("Setting pipeline to PAUSED ...")
    res = self.pipeline.set_state(gst.STATE_PAUSED);

    # FIXME error not recognized as GError, maybe subclass from **gst.GError**!!??
    #error = gst.GError(gst.STREAM_ERROR, gst.STREAM_ERROR_FAILED, "Pipeline can't PREROLL")
    #bus.post(gst.message_new_error(self.pipeline, error, "Pipeline can't PREROLL"))

    if res == gst.STATE_CHANGE_FAILURE:
      gst.error("Pipeline doesn't want to pause")
      return
    elif res == gst.STATE_CHANGE_NO_PREROLL:
      gst.info("Pipeline is live and does not need PREROLL ...")
      self.no_preroll = True
      return
    elif res == gst.STATE_CHANGE_ASYNC:
      gst.info("Pipeline is PREROLLING ...")

    # finishing up state change happens in other threads *and* main loop
    return

  # NOTE !!
  # It is possible that the main demux thread is 'caught' here by
  # pad-alloc.  Another thread may have the stream lock, or may be
  # temporarily held waiting in collectpads.
  # To make it through all this waiting and have a chance to actually
  # send the EOS, the feeds into collectpads need to be able to keep going
  # without the demuxer for a while.
  # As such, the queues should always have a certain level of buffers available
  # (e.g. using min-threshold-buffers.
  # Note that this is in effect implicitly the case for a live recording pipeline,
  # which is typically driven by various src pushing elements.
  def cb_do_eos(self, pad, is_blocked, dam):
    if dam not in self.blocked_pads:
      self.blocked_pads.append(dam)
      gst.debug("Sending EOS from " + to_str(dam))
      dam.send_event(gst.event_new_eos())
    else:
      gst.debug("Already sent eos from " + to_str(dam))

  def stop(self, force):
    # live pipeline may hang for obscure reason:
    # state changing thread fails to acquire LIVE_LOCK from v4l2src,
    # even though it is being released by the (pushsrc) thread ??
    if force and not self.no_preroll:
      gst.debug("Setting pipeline to NULL ...")
      self.pipeline.set_state(gst.STATE_NULL)
      self.pipeline.get_state(timeout=0)
    else:
      if self.use_dams:
        for dam in self.dams:
          gst.debug("Setting eos on " + to_str(dam))
          dam.set_property('force-eos', True)
      else:
        self.blocked_pads = []
        for dam in self.dams:
          peer = dam.sink_pads().next().get_peer()
          gst.debug("Blocking on pred of " + to_str(dam) + ", " + str(peer))
          peer.set_blocked_async(True, self.cb_do_eos, dam)
      # now we force queues to discharge and keep things going
      gst.debug("HACK: discharging queues")
      for element in self.pipeline.recurse():
        if gobject.type_name(element) == 'GstQueue':
          element.set_property('min-threshold-buffers', 0)
# -- END NonLinPipeline


# --
# a bit of ugly code in here, but it is at least contained ...
class Progress:

  # duration according to external sources (= pipeline) is provided
  def __init__(self, sections, duration):
    # -- PUBLIC:
    # NONE as python None
    # times in gst clocktime (nanosec)
    self.duration = None
    # current position
    self.position = None
    # info on proc or drop
    # dict with keys: type, clock, pos, initial, speed, cpu, cpuspeed
    self.proc = None
    self.drop = None
    # info on current section
    self.current = None
    # --
    # -- PRIVATE
    # total duration by external sources
    if not duration or duration == gst.CLOCK_TIME_NONE or duration < 0:
      duration = None
    self.total = duration
    # section info
    self.sections = copy.deepcopy(sections)
    # info from previous run
    self.last = {}
    # -- normalize
    # may have open-ended section
    for i in range(len(self.sections)):
      if not self.sections[i]['end']:
        self.sections[i]['end'] = duration
    # -- initialize
    # duration determined by sections, if any
    if self.sections:
      try:
        self.duration = sum([x['end'] - x['start'] for x in self.sections])
      except: # may be None in there somewhere
        self.duration = None
    else:
      self.duration = self.total
    # most recent clocktime
    self.last['clock'] = time.time() * gst.SECOND
    self.last['cpu'] = os.times()[0] * gst.SECOND
    # most recently processed section index
    self.last['section'] = -1
    self.proc = { 'type': 'proc', 'clock': 0, 'pos': 0, 'initial': 0, 'cpu': 0 }
    self.drop = { 'type': 'drop', 'clock': 0, 'pos': 0, 'initial': 0, 'cpu': 0 }

  # returns (type, amount of stream time in this type, total amount in current section)
  # type can be 'proc' or 'drop'
  # current section can also refer to a skipped section
  # amounts can be None if unknown
  #
  # this is a bit convoluted but should also work if sections are not
  # in input time order
  def get_proc_info(self, position):
    sections = self.sections
    sec = None
    if not sections:
      return ('proc', position, position)
    proctype = None
    total = 0
    for i, section in enumerate(sections):
      if position >= section['start'] \
        and (position <= section['end'] or not section['end']):
        total = total + position - section['start']
        proctype = 'proc'
        if section['end']:
          sec = section['start'] - section['end']
        else:
          sec = None
        self.last['section'] = i
        break
      else:
        total = total + section['end'] - section['start']
    if not proctype:
      i = self.last['section']
      self.proc['pos'] = 0
      for j, section in enumerate(sections):
        if j <= i:
          self.proc['pos'] = self.proc['pos'] + section['end'] - section['start']
      proctype = 'drop'
      for i in range(len(sections)):
        if position < sections[i]['start']:
          if i:
            start = sections[i-1]['end']
          else:
            start = 0
          total = total + position - start
          sec = sections[i]['start'] - start
        else:
          total = total + sections[i]['start'] - sections[i]['end']
    return (proctype, total, sec)

  def update(self, position):
    self.position = position
    # -- normal operation
    now = time.time() * gst.SECOND
    nowcpu = os.times()[0] * gst.SECOND
    clocktime = (now - self.last['clock'])
    cputime = (nowcpu - self.last['cpu'])
    self.last['clock'] = now
    self.last['cpu'] = nowcpu
    proctype, total, current = self.get_proc_info(position)
    if proctype == 'proc':
      self.current = self.proc
      toupdate = [self.proc]
    else:
      self.current = self.drop
      # processed section must be updated in any event; to advance MT counter
      toupdate = [self.drop, self.proc]
    self.current['clock'] = self.current['clock'] + clocktime
    self.current['cpu'] = self.current['cpu'] + cputime
    self.current['pos'] = total
    # take into account (for speed) that first clock measurement
    # may not have happended at 0 MT position
    if not self.current['initial']:
      self.current['initial'] = total
      self.current['clock'] = 0
      self.current['cpu'] = 0
    for update in toupdate:
      if update['clock']:
        update['speed'] = float(update['pos'] - update['initial']) / update['clock']
      else:
        update['speed'] = None
      if update['cpu']:
        update['cpuspeed'] = float(update['pos'] - update['initial']) / update['cpu']
      else:
        update['cpuspeed'] = None
    # need MT in any event
    self.movietime = self.proc['pos']
# -- END Progress

# --
# provides runtime and progress info with a variety of callbacks and/or querying
class Monitor:

  def __init__(self, entrans, nonlin):
    # -- PRIVATE
    # nonlin we are monitoring
    self.nonlin = nonlin
    # configuration store
    self.config = entrans.config
    # settings can be obtained here
    self.entrans = entrans
    # did we get a signal (request) to stop
    self.got_signal = False
    # element used to query in the pipeline
    self.queryel = None
    # total duration
    self.duration = None
    # info on last snapshot
    self.last = dict()
    # info on progress so far
    self.progress = None
    # info on pipeline gathered by walk
    self.info = { }
    # tag info gathered from message
    self.tags = { }
    # output needs to provide a progress header
    self.print_progress = False

    bus = nonlin.pipeline.get_bus()
    bus.add_signal_watch()
    bus.connect('message', self.cb_message)

    self.nonlin.connect('started', self.cb_started)
    self.nonlin.connect('playing', self.cb_playing)

    if self.config.verbose:
      self.nonlin.pipeline.connect("deep-notify", self.cb_deep_notify)

  def cb_deep_notify(self, gstobject, object, property):
    name = property.name
    if object_match_prop(object, self.config.include, name) \
      or not object_match_prop(object, self.config.exclude, name):
      if property.flags & gobject.PARAM_READABLE:
        prop = object.get_property(name)
      else:
        prop = '[not readable]'
      print "Notify from ", to_str(object), ': ', name, ' = ', prop
      # make output as synchronous as possible
      sys.stdout.flush()

  def cb_message(self, bus, message):
    if message.structure:
      name = message.structure.get_name()
      struct = message.structure.to_string()
    else:
      struct, name = '', ''
    if self.config.messages \
      and (message_match(message, self.config.display_msg) \
        or not message_match(message, self.config.ignore_msg)):
      print "Got Message from element %s (%s): %s" % \
        (message.src.get_name (), message.type.first_value_nick, struct)
    if message.type == gst.MESSAGE_TAG \
       and (not object_match_prop(message.src, self.config.ignore_prop, 'tag') \
        or object_match_prop(message.src, self.config.display_prop, 'tag')):
      if not self.tags.has_key(message.src):
        self.tags[message.src] = []
      self.tags[message.src].append(message.structure)

    if message.type == gst.MESSAGE_ELEMENT:
      if struct and name in self.config.save:
        try:
          filename = self.config.save[name]['file'].replace('${n}', message.src.get_name())
          f = open(filename, 'a')
          if not self.config.save[name]['append']:
            f.truncate(0)
          f.write(str(struct) + "\n")
          f.close()
        except IOError, (errno, strerror):
          gst.error('Failed to save message ' + name + ' to ' + filename + ': ' + strerror)
          # do not try again
          del self.config.save[name]
    elif message.type == gst.MESSAGE_EOS:
      # avoid overwriting the (very likely present) status line
      print
      print "Got EOS from element ", to_str(message.src)
      self.nonlin.stop(True)
      self.entrans.stop()
    elif message.type == gst.MESSAGE_WARNING:
      error, debug = message.parse_warning()
      print "WARNING: from element " + to_str(message.src) + ":", error.message
      if debug:
        print "Additional debug info:"
        print debug
    elif message.type == gst.MESSAGE_ERROR:
      error, debug = message.parse_error()
      message.src.default_error(error, debug)
      self.nonlin.stop(True)
      self.entrans.stop()
    return True

  def cb_status(self):
    # do we need a header for good looks ?
    if self.print_progress and self.queryel:
      print "<<<< PROGRESS >>>>"
      if self.config.progress_real:
        timename = "realtime"
      else:
        timename = "cputime"
      print "[position] MT / Total (MT/%s = proc speed) ETA  (queued buffers) bitrate" % (timename)
      self.print_progress = False
    # get position, duration info
    try:
      pos, format = self.queryel.query_position(gst.FORMAT_TIME)
    except:
      pos = gst.CLOCK_TIME_NONE
      print "Failed to obtain progress info."
      return
    # update progress info
    self.progress.update(pos)
    # calculate eta based on proc speed
    if self.progress.duration and self.progress.proc['cpuspeed']:
      duration = self.progress.duration
      speed2 = self.progress.proc['speed']
      eta = (self.progress.duration - self.progress.movietime) / \
                speed2 / gst.SECOND
    else:
      duration = -1
      eta = -1
    # display current speed
    if self.progress.current['cpuspeed']:
      speed = self.progress.current['cpuspeed']
      speed2 = self.progress.current['speed']
      if self.config.progress_real:
        speed = speed2
    else:
      speed = -1
      speed2 = -1
    mt = self.progress.movietime / gst.SECOND
    # queue status
    queues = self.info['queue']['video']
    if self.info:
      qs = "|".join(["%2d" % (q.get_property('current-level-buffers')) \
                        for q in queues])
      if qs:
        qs = '(' + qs + ')'
    else:
      qs = ""
    if self.config.progress_fps:
      if self.nonlin.fps and speed != -1:
        fps = speed * self.nonlin.fps.num / self.nonlin.fps.denom
      else:
        fps = -1
    else:
      fps = ''
    # output bitrate
    rate = []
    if self.info:
      for sink, location in self.info['sink'].iteritems():
        # only try on reasonable sinks
        if location:
          # TODO needs filesink & co patching first
          #sink = sink.sink_pads().next()
          #try:
            #fpos, format = sink.query_position(gst.FORMAT_BYTES)
          #except:
            #gst.debug('Failed to query byte position of sink ' + to_str(sink))
            # falling back to direct call
          if os.path.isfile(location):
            fpos = os.path.getsize(location)
          else:
            fpos = 0
          if mt:
            rate.append(str(fpos * 8 / 1024 / mt ))
    if rate:
      rate = "|".join(rate) + ' kb/s'
    else:
      rate = ''
    # and display collected info
    if fps:
      if fps == -1:
        fps = "  N/A"
      else:
        fps = "%2.2f" % (fps)
      fps = fps + " fps "
    if speed == -1:
      speed = " N/A"
    else:
      speed = "%2.2f" % (speed)
    if eta == -1:
      eta = "    N/A"
    else:
      eta = get_time_as_str(eta * gst.SECOND, 0)
    if duration == -1:
      duration = "    N/A"
    else:
      duration = get_time_as_str(duration, 0)
    print "[%s: %s] %s / %s  %s(x %s) ETA: %s  %s %s" % \
        (self.progress.current['type'], get_time_as_str(self.progress.position),
        get_time_as_str(mt * gst.SECOND, 0),
        duration, fps, speed, eta, qs, rate),
    print "\r",
    sys.stdout.flush()
    return True

  def walk_pipeline(self, bin):
    result = { 'src': {}, 'sink': {}, 'caps': [], 'props': {}, \
                'queue': { 'video': [], 'audio': [], 'other': [] }, \
                'multiqueue': [] \
             }
    for element in bin_sorted_recursive(bin):
      # determine some useful location from the element (typically src or sink)
      location = None
      for loc in ['location', 'device']:
        if loc in [x.name for x in gobject.list_properties(element)]:
          location = element.get_property(loc)
      if element_is_src(element):
        result['src'][element] = location
      # output
      if element_is_sink(element):
        result['sink'][element] = location
      # caps
      for pad in element.pads():
        caps = pad.get_negotiated_caps()
        if caps:
          if caps not in result['caps']:
            result['caps'].append(caps)
      # non-default props
      props = []
      if not object_match_prop(element, self.config.ignore_prop, None):
        for pspec in gobject.list_properties(element):
          if pspec.flags & gobject.PARAM_READABLE and pspec.name != 'name':
            gst.log('Inspecting property %s.%s' % (to_str(element), pspec.name))
            if object_match_prop(element, self.config.display_prop, pspec.name) \
              or (not object_match_prop(element, self.config.ignore_prop, pspec.name)
                  and element.get_property(pspec.name) !=
                          clone_element(element).get_property(pspec.name)):
              props.append({ 'name': pspec.name,
                             'value': element.get_property(pspec.name) })
      if props or not object_match_prop(element, self.config.ignore_prop):
        result['props'][element] = props
      # queues
      res = result['queue']
      if gobject.type_name(element) in ['GstQueue', 'GstQueue2']:
        if element_is_video(element):
          res['video'].append(element)
        elif element_is_audio(element):
          res['audio'].append(element)
        else:
          res['other'].append(element)
      # multiqueues
      res = result['multiqueue']
      if gobject.type_name(element) == 'GstMultiQueue':
        res.append(element)
    return result

  def interrupt(self, signum, frame):
    print "Caught signal - exiting."
    # if we already tried to stop nicely, be more forceful
    if self.got_signal:
      self.nonlin.stop(True)
      self.entrans.stop()
    else:
      self.got_signal = True
      self.entrans.exitcode = 1
      self.nonlin.stop(False)

  def caps_to_str(self, caps):
    if self.config.short_caps:
      return caps_to_short_str(caps)
    else:
      return str(caps)

  def cb_started(self, object):
    # handle interrupt
    signal.signal(signal.SIGINT, self.interrupt)
    signal.signal(signal.SIGTERM, self.interrupt)
    # HACK now main loop should keep running
    self.entrans.on = True

    # get pipeline info
    walk = self.walk_pipeline(self.nonlin.pipeline)
    self.info = walk

    # provide for status info
    # look for a good element to query position
    # a dam in a video stream is generally best/safest
    for dam in self.nonlin.dams:
      if element_is_video(dam):
        self.queryel = dam
        break
    gst.debug("Dam used for querying " + str(self.queryel))
    # set fallback if none found
    if not self.queryel and self.nonlin.dams:
      self.queryel = self.nonlin.dams[0]
      gst.debug("Falling back to " + str(self.queryel) + " for querying")
    # there should be at least some queue in there
    # let's see if it already has caps
    if not self.queryel:
      if walk['queue']['video']:
        self.queryel = walk['queue']['video'][0]
      elif walk['queue']['audio']:
        self.queryel = walk['queue']['audio'][0]
      elif walk['queue']['other']:
        self.queryel = walk['queue']['other'][0]
    if not self.queryel:
        print "Unable to locate element to query.  No progress info can be provided."
    else:
      gst.debug("Element used for querying " + str(self.queryel))
      # get position, duration info
      try:
        pos, format = self.queryel.query_position(gst.FORMAT_TIME)
      except:
        pos = gst.CLOCK_TIME_NONE
      try:
        duration, format = self.queryel.query_duration(gst.FORMAT_TIME)
      except:
        duration = gst.CLOCK_TIME_NONE
      self.progress = Progress(self.nonlin.sections, duration)
      gobject.timeout_add(self.config.progress, self.cb_status)

    # display some pipeline info
    # input
    print "<<<< INPUT - OUTPUT >>>>"
    for src, location in walk['src'].iteritems():
      if location:
        location = '(' + str(location) + ')'
      else:
        location = ''
      print "Input:", to_str(src), location
    # output
    for src, location in walk['sink'].iteritems():
      if location:
        location = '(' + str(location) + ')'
      else:
        location = ''
      print "Output:", to_str(src), location
    # props
    print "<<<< NON-DEFAULT (OR SELECTED) PROPERTIES >>>>"
    # display in top sorted order
    for element in bin_sorted_recursive(self.nonlin.pipeline):
      if walk['props'].has_key(element) or self.tags.has_key(element):
        print "Element:", to_str(element)
      if walk['props'].has_key(element):
        for prop in walk['props'][element]:
          print "\t", prop['name'] +  ":", prop_to_str(prop['value'])
      if self.tags.has_key(element):
        for tag in self.tags[element]:
          print "\t", "tag:", tag.to_string()
    # caps
    print "<<<< PIPELINE CAPS >>>>"
    for caps in walk['caps']:
      print self.caps_to_str(caps)
    # on to playing and/or progress ...
    self.print_progress = True

  def cb_playing(self, object):
    print "<<<< Now reached PLAYING state >>>>"
    walk = self.walk_pipeline(self.nonlin.pipeline)
    # forcibly set some properties on queues
    # possibly overriding defaults set earlier
    self.config.set(walk['queue']['video'], {}, True)
    self.config.set(walk['queue']['audio'], {}, True)
    self.config.set(walk['queue']['other'], {}, True)
    self.config.set(walk['multiqueue'], {}, True)
    # some remaining info to display
    # caps
    # don't display if we had this already
    newcaps = []
    for caps in walk['caps']:
      if caps not in self.info['caps']:
        newcaps.append(caps)
    if newcaps:
      print "<<<< (MORE) PIPELINE CAPS >>>>"
      for caps in newcaps:
        print self.caps_to_str(caps)
    # queues
    print "<<<< QUEUES >>>>"
    print "Video: [max-kB|max-buffers|max-sec]"
    l = copy.copy(walk['queue']['video'])
    l.extend(walk['multiqueue'])
    for q in l:
      mkbytes = q.get_property('max-size-bytes') / 1024
      mbuffers = q.get_property('max-size-buffers')
      mtime = float(q.get_property('max-size-time')) / gst.SECOND
      pred = element_pred(q)
      post = element_succ(q)
      if len(pred) > 0:
        pred = to_str(pred[0]) + " - "
      else:
        pred = ""
      if len(post) > 0:
        post = " - " + to_str(post[0])
      else:
        post = ""
      print "%s%s [%d|%d|%.2f]%s" % (pred, to_str(q),
          mkbytes, mbuffers, mtime, post)
    # progress should follow
    self.print_progress = True
    # store this for future use
    self.info = walk
# -- END Monitor


# --
# monitors for timeouts between expected states,
# and tries to remedy or abort
# --
# consider it very 'friend'ly with Entrans below
class Timeout:

  STATE_NULL = 0
  STATE_BLOCKED = 1
  STATE_NO_MORE_PADS = 2
  STATE_STARTED = 3
  STATE_PLAYING = 4
  # do not wait for playing:
  # in cut mode, that might really take some time
  # in any case, the pipeline/caps walk might take time as well
  # also do not wait for started:
  # it might take some time in case of a second pass having to load stats
  STATE_OK = STATE_NO_MORE_PADS

  display = { STATE_NULL: 'null',
              STATE_BLOCKED: 'pad-blocked',
              STATE_NO_MORE_PADS: 'no-more-pads',
              STATE_STARTED: 'started', STATE_PLAYING: 'playing' }

  def __init__(self, entrans, nonlin, timeout):
    self.timeout = timeout
    # -- PRIVATE
    # nonlin we are monitoring
    self.nonlin = nonlin
    # configuration store
    self.config = entrans.config
    # settings can be obtained here
    self.entrans = entrans
    # id of the timeout source
    self.source = None
    # timeout between states
    self.timeout = timeout
    # last state
    self.state = self.STATE_NULL
    # avoid loop while trying to force things
    self.tried_no_more_pads = False
    # --
    self.start()

  def start(self):
    if not self.timeout:
      return
    self.nonlin.connect('blocked', self.cb_state, self.STATE_BLOCKED)
    self.nonlin.connect('started', self.cb_state, self.STATE_STARTED)
    self.nonlin.connect('playing', self.cb_state, self.STATE_PLAYING)
    decode = self.entrans.pipeline.get_by_name('decoder')
    if decode:
      decode.connect('no-more-pads', self.cb_state, self.STATE_NO_MORE_PADS)
    if self.entrans.raw:
      self.state = self.STATE_NO_MORE_PADS
    self.setup_timer()

  def setup_timer(self):
    if self.source:
      gobject.source_remove(self.source)
    # only bother if there is anything left to check
    if self.state < self.STATE_OK:
      self.source = gobject.timeout_add(self.timeout, self.cb_timeout)

  def cb_state(self, *arguments):
    state = arguments[-1]
    # order of pad blocking and no-more-pads is undetermined
    if state > self.state:
      gst.info('Timeout monitor reached state ' + self.display[state])
      self.state = state
      self.setup_timer()

  def cb_timeout(self):
    gst.debug('Timeout in state ' + self.display[self.state])
    # should only occur if we failed to reach a next state, check anyway
    if self.state >= self.STATE_OK:
      return False
    target_state = self.state + 1
    gst.error('Detected timeout trying to reach state ' + (self.display[target_state]))
    gst.error('See also --timeout option')
    if target_state == self.STATE_NO_MORE_PADS and not self.tried_no_more_pads:
      # try to remedy in hack-ish way, and continue timeout monitor
      gst.error('Trying to force state transition ...')
      self.entrans.cb_no_more_pads(None, None)
      self.tried_no_more_pads = True
      return True
    else: # terminate hard
      gst.error('Terminating ...')
      self.entrans.exitcode = 1
      self.entrans.stop()
      return False

# -- END Timeout

# --
# main application class, responsible for
# - pipeline construction
# - delegate option parsing
# - completing a decodebin based dynamic pipeline if requested
class Entrans:

  def __init__(self, argv):
    # -- PUBLIC
    # do we still need mainloop
    self.on = False
    # exitcode to return
    self.exitcode = 0
    # configuration
    self.config = None
    # -- PRIVATE
    # pipeline that may have to completed dynamically
    self.pipeline = None
    # non-linear being constructed
    self.nonlin = None
    # main loop to keep things alive
    self.loop = None
    # some options;
    # raw launch pipeline
    self.raw = None
    # dams seen so far
    self.dam_no = -1
    # whether dam plugin is available
    self.have_dam = False
    # probe info: (dam, info) pair;
    # info is dict with keys probe_id
    self.probe = { }
    # whether no-more-pads already received
    self.have_no_more_pads = False
    # pipeline fragments bookkeeping
    # bin: (stream number, bin launch line fragment)
    # no: no of streams seen so far
    # stream: (stream number, bin)
    self.pipes = { 'video': {'bin': {}, 'no': 0, 'stream': {} },
                   'audio': {'bin': {}, 'no': 0, 'stream': {} },
                   'other': {'bin': {}, 'no': 0, 'stream': {} }
                 }

  # link the given bin (already in pipeline) to the muxer
  def link_bin(self, bin):
    gst.log("Linking bin " + str(bin))
    binsrc = None
    mux = self.pipeline.get_by_name('muxer')
    if mux:
      for ghostpad in bin.src_pads():
        if ghostpad.get_direction() == gst.PAD_SRC and mux:
          muxpad = mux.get_compatible_pad(ghostpad)
          binsrc = ghostpad
    if binsrc and not muxpad:
      gst.debug("bin " + str(bin) + " not compatible with muxer")
      return False
    if binsrc:
      binsrc.link(muxpad)
      gst.debug("Linked " + str(binsrc) + " to " + str(muxpad))
    return True

  def cb_new_decode_pad(self, element, pad, no_more):

    # currently not used
    # custom way to look for unlinked pads in bin,
    # somewhat catering for the case where several sinkpads may be unlinked
    def bin_make_ghostpads(bin):
      # sink pads - needs to go first, otherwise interference from ghost pad
      pads = []
      for element in bin.recurse():
        pads.extend(element.sink_pads())
      pads = [p for p in pads if not p.get_peer()]
      if len(pads) == 1:
        bin.add_pad(gst.GhostPad("sink", pads[0]))
      else:
        # we look for a src pad whose downstream end does *not* have dangling src pad,
        # the one *with* dangling src pad is likely to connect to the muxer at the end
        # this allows for an in bin occurrence of a demuxer-like element
        for p in pads:
          endpad = pad_get_succ(p)[-1]
          print endpad
          if endpad.get_direction() == gst.PAD_SINK: # so no dangling src
            bin.add_pad(gst.GhostPad("sink", p))
      # src pads
      pads = []
      for element in bin.recurse():
        pads.extend(element.src_pads())
      pads = [p for p in pads if not p.get_peer()]
      if len(pads) > 1:
        gst.warning('More than 1 src pad detected in ' + to_str(bin) + ' ' + str(pads))
      else:
        bin.add_pad(gst.GhostPad("src", pads[0]))

    gst.debug("Found new decoded pad: " + str(pad) + ", last: " + str(no_more) + \
        ", caps: " + str(pad.get_negotiated_caps()))
    if self.have_no_more_pads:
      gst.warning("Already received no-more-pads; ignoring new pad.")
      return
    # if any linking, etc fails, exception will terminate things
    # user pipeline
    pipe = bin = None
    subst = { }
    dostamp = self.config.stamp
    # HACK: ignore current pads
    if pad_is_current(pad):
      gst.debug('Detected current_ pad')
    elif pad_is_video(pad):
      ptype = 'video'
      pipe, nos, subst = self.pipes[ptype], self.config.vn, 'vn'
    elif pad_is_audio(pad):
      ptype = 'audio'
      pipe, nos, subst = self.pipes[ptype], self.config.an, 'an'
    else: # subtitle or ??
      ptype = 'other'
      pipe, nos, subst = self.pipes[ptype], self.config.on, 'on'
    if pipe and pipe['bin']:
      pipe['no'] += 1
      stream_no, stream = pipe['no'], pipe['stream']
      if pipe['bin'].has_key(stream_no):
        bin = pipe['bin'][stream_no]
      elif pipe['bin'].has_key(0):
        bin = pipe['bin'][0]
      if bin and (not nos or pipe['no'] in nos):
        # FIXME perhaps do more/safer parsing some day
        bin = bin.replace('${' + subst + '}', str(stream_no))
        bin = gst.parse_bin_from_description(expand_caps(bin), True)
        subst = { subst: str(stream_no) }
      else:
        bin = None
        gst.debug('Filtered out new ' + ptype + ' decoded pad.')
    if not bin:
      # don't put fakesink, because this also requires dam (for eos)
      # muxer should ignore NOT_LINKED, but fail if all NOT_LINKED (which is ok)
      gst.debug('Ignoring pad.')
      return
    gst.debug('Trying to link pad type %s' % (ptype))
    # apply configuration to user part
    self.config.set(bin_sorted_recursive(bin), subst)
    # creation and config complete, now onto pipeline building
    if not bin.get_compatible_pad(pad):
      gst.debug("pad not compatible with bin")
      return
    # now we can go and create, add, link, ...
    # must add first (as it breaks links), then link and set state
    self.pipeline.add(bin)
    # try to link this already, if possible
    if (self.config.vn or self.config.an or self.config.on) and \
       not self.config.sync_link:
      self.nonlin.do_block = True
      stream[stream_no] = bin
    else:
      if not self.link_bin(bin):
        self.pipeline.remove(bin)
        return
    # some standard elements
    # decodebin already adds queues, so no need
    self.dam_no += 1
    if self.have_dam:
      dam = gst.element_factory_make('dam')
    else:
      dam = gst.element_factory_make('identity', 'dam' + str(self.dam_no))
      dam.set_property('silent', True)
    self.pipeline.add(dam)
    if dostamp:
      # use identity for re-sequencing so no additional dependency is introduced
      stamp = gst.element_factory_make('identity', 'stamp' + str(self.dam_no))
      stamp.set_property('silent', True)
      stamp.set_property('single-segment', True)
      self.pipeline.add(stamp)
    pad.link(dam.get_compatible_pad(pad))
    # make sure that there is queue following dam
    has_queue = False
    for element in bin.recurse():
      if gobject.type_name(element) == 'GstQueue':
        has_queue = True
    # insert one if not
    if has_queue:
      if dostamp:
        dam.link(stamp)
        stamp.link(bin)
      else:
        dam.link(bin)
    else:
      q = gst.element_factory_make('queue')
      self.pipeline.add(q)
      dam.link(q)
      if dostamp:
        q.link(stamp)
        stamp.link(bin)
      else:
        q.link(bin)
      q.set_state(gst.STATE_PAUSED)
    dam.set_state(gst.STATE_PAUSED)
    if dostamp:
      stamp.set_state(gst.STATE_PAUSED)
    bin.set_state(gst.STATE_PAUSED)
    # scan for language tags
    if self.config.at and format.find('audio/') == 0:
      self.probe[pad] = { }
      self.probe[pad]['buffer_id'] = pad.add_buffer_probe(self.cb_buffer_probe)
      self.probe[pad]['event_id'] = pad.add_event_probe(self.cb_event_probe)
      self.probe[pad]['request_pad_peer'] = list(bin.src_pads())
      gst.debug("Added probes on " + str(pad))
    # and inform of update
    self.nonlin.emit('updated')

  def remove_probe(self, pad):
    pad.remove_buffer_probe(self.probe[pad]['buffer_id'])
    pad.remove_buffer_probe(self.probe[pad]['event_id'])
    gst.debug("Removed probes on " + str(pad))

  def cb_event_probe(self, pad, event):
    def collect_pred(element, elements):
      if element not in elements:
        elements.append(element)
      for el in element_pred(element):
        collect_pred(el, elements)
    if event.type == gst.EVENT_TAG:
      taglist = event.parse_tag()
      if gst.TAG_LANGUAGE_CODE in taglist.keys():
        lang = taglist[gst.TAG_LANGUAGE_CODE]
        if [True for tag in self.config.at if re.search(tag, lang)]:
          gst.debug('Language tag on ' + str(pad) + ' matched')
          self.remove_probe(pad)
        else:
          gst.debug('Language tag on ' + str(pad) + ' did not match')
          self.remove_probe(pad)
          # unlink in front
          peer = pad.get_peer()
          pad.unlink(peer)
          mux = self.pipeline.get_by_name('muxer')
          # at muxer
          elements = []
          for srcpad in self.probe[pad]['request_pad_peer']:
            peer = srcpad.get_peer()
            srcpad.unlink(peer)
            mux.release_request_pad(peer)
            # collect upstream element up to the pad
            element = srcpad.get_parent_element()
            collect_pred(element, elements)
          # and get rid of some elements
          gst.log('Removing ' + str(elements) + ' from pipeline')
          for element in elements:
            self.pipeline.remove(element)
            element.set_state(gst.STATE_NULL)
          return False
    return True

  def cb_buffer_probe(self, pad, buf):
    # data is coming, hands off from now on
    self.remove_probe(pad)
    return True

  def link_bins(self):
    if self.config.sync_link or \
       not (self.config.vn or self.config.an or self.config.on):
      return
    l = [ [self.config.vn, self.pipes['video']['stream']], \
          [self.config.an, self.pipes['audio']['stream']], \
          [self.config.on, self.pipes['other']['stream']] ]
    for ll in l:
      nl, sl = ll[0], ll[1]
      if not sl:
        continue
      if not nl:
        nl = range(1, max(sl.keys()) + 1)
      for n in nl:
        if sl.has_key(n):
          if not self.link_bin(sl[n]):
            # minimal recovery
            self.pipeline.remove(sl[n])

  def cb_no_more_pads(self, element, mux):
    gst.debug("no-more-pads")
    if self.have_no_more_pads:
      gst.warning("Already received no-more-pads; ignoring.")
      return
    self.have_no_more_pads = True
    # caller may not have provided, try finding it
    if not mux:
      mux = self.pipeline.get_by_name('muxer')
    if mux:
      self.link_bins()
      if not element_pred(mux):
        for el in element_succ(mux):
          self.pipeline.remove(el)
          el.set_state(gst.STATE_NULL)
        self.pipeline.remove(mux)
        mux.set_state(gst.STATE_NULL)
      # unlock muxer state and set
      mux.set_locked_state(False)
      mux.set_state(gst.STATE_PAUSED)
    gst.debug("Signalling complete.")
    self.nonlin.emit('complete')

  def cb_queue_filled(self, object):
    gst.log("Suppressing overrun for " + to_str(object))
    object.stop_emission('overrun')

  # set properties of added element
  def cb_element_added(self, bin, element):
    if element.get_factory().get_name() == 'queue':
      # override decodebin's queue management with user settings
      force = True
      # and prevent further adjustment if so requested
      if self.config.block_overrun:
        element.connect('overrun', self.cb_queue_filled)
    else:
      force = False
    self.config.set([element], {}, force)

  # - need to exit hard if exception happens when invoked from native thread
  # - display trace guts depending on debug level
  def excepthook(self, t, value, tb):
    gst.error ('\n' + ''.join(traceback.format_tb (tb)))
    print ''.join(traceback.format_exception_only (t, value))
    self.exitcode = 2
    self.stop()

  # collect pipeline fragments --ptype into collector
  # multiple: whether or not --ptype:* is allowed
  def collect_pipeline(self, ptype, collector, multiple = True):
    l = len(ptype)
    collect = None
    for arg in self.config.args:
      n = -1
      if arg == '--' + ptype:
        n = 0
      elif arg[0:l + 3] == '--' + ptype + ':':
        try:
          if not multiple:
            raise
          n = (int) (arg[l + 3:])
        except:
          self.config.optparser.error("invalid argument: " + arg)
      if n >= 0:
        collect = collector[n] = []
      elif arg[0:2] == '--':
        collect = []
      else:
        if collect != None:
          collect.append(arg)
        else:
          self.config.optparser.error(arg + " does not belong to a launch fragment")
    # convert to text form
    for i in collector:
      collector[i] =  " ".join(collector[i])

  def start(self):

    sys.excepthook = self.excepthook

    self.config = Configuration()

    # modify plugin level properties
    self.config.set_plugins()

    # extract pipeline fragments out of pipeline and re-assemble
    self.raw = {}
    self.collect_pipeline('raw', self.raw, False)
    for pipe in self.pipes:
      self.collect_pipeline(pipe, self.pipes[pipe]['bin'])
    if self.raw:
      self.raw = self.raw[0]

    if not self.raw:
      # will we have dams ?
      try:
        gst.element_factory_make('dam')
      except gst.PluginNotFoundError:
        print "<<<< WARNING: dam plugin could not be found; full operation not available >>>>"
        self.have_dam = False
      else:
        self.have_dam = True
      if not self.config.inputfile:
        self.config.optparser.error("-i or --raw must be given.")
      self.pipeline = gst.element_factory_make('pipeline')
      # input
      if gst.uri_is_valid(self.config.inputfile):
        filesrc = gst.element_make_from_uri(gst.URI_SRC, self.config.inputfile)
      else:
        if self.config.inputfile == '-':
          filesrc = gst.element_factory_make('fdsrc')
          filesrc.set_property('fd', 0)
        else:
          filesrc = gst.element_factory_make('filesrc')
          filesrc.set_property('location', self.config.inputfile)
      # output
      if self.config.outputfile:
        if gst.uri_is_valid(self.config.outputfile):
          filesink = gst.element_make_from_uri(gst.URI_SINK, self.config.outputfile)
        else:
          if self.config.outputfile == '-':
            self.config.optparser.error('output to stdout not supported')
          else:
            filesink = gst.element_factory_make('filesink')
            filesink.set_property('location', self.config.outputfile)
        # mux
        if self.config.muxer:
          mux = gst.element_factory_make(self.config.muxer, 'muxer')
          if mux.get_factory().get_klass().find('Muxer') < 0:
            self.config.optparser.error('muxer element ' + self.config.muxer + ' is not Muxer')
        elif self.config.outputfile[-4:] == '.avi':
            mux = gst.element_factory_make('avimux', 'muxer')
        elif self.config.outputfile[-4:] in ['.mkv', '.mka']:
          mux = gst.element_factory_make('matroskamux', 'muxer')
        elif self.config.outputfile[-4:] in ['.ogm', '.ogg']:
          mux = gst.element_factory_make('oggmux', 'muxer')
        elif self.config.outputfile[-4:] == '.mov':
          mux = gst.element_factory_make('ffmux_mov', 'muxer')
        elif self.config.outputfile[-4:] in ['.mp4', '.m4a']:
          mux = gst.element_factory_make('ffmux_mp4', 'muxer')
        elif self.config.outputfile[-4:] == '.3gp':
          mux = gst.element_factory_make('ffmux_3gp', 'muxer')
        elif self.config.outputfile[-4:] == '.3g2':
          mux = gst.element_factory_make('ffmux_3g2', 'muxer')
        elif self.config.outputfile[-4:] == '.mpg':
          mux = gst.element_factory_make('ffmux_mpeg', 'muxer')
        elif self.config.outputfile[-3:] == '.ts':
          mux = gst.element_factory_make('ffmux_mpegts', 'muxer')
        elif self.config.outputfile[-4:] == '.flv':
          mux = gst.element_factory_make('ffmux_flv', 'muxer')
        # asf/wmv/wma works but not directly mapped here
        else:
          self.config.optparser.error('could not determine muxer for ' + self.config.outputfile)
        # put together
        self.pipeline.add(mux, filesink)
        mux.link(filesink)
        # mux should only change state if all dynamic stuff has completed
        mux.set_locked_state(True)
      else:
        mux = None
      # decode
      decode = gst.element_factory_make(self.config.decoder, 'decoder')
      decode.connect('new-decoded-pad', self.cb_new_decode_pad)
      decode.connect('no-more-pads', self.cb_no_more_pads, mux)
      decode.connect('element-added', self.cb_element_added)
      complete = False
      # put together
      self.pipeline.add(filesrc, decode)
      filesrc.link(decode)
    else:
      self.pipeline = gst.parse_launch(expand_caps(self.raw))
      complete = True
      self.have_dam = self.config.dam

    if not self.have_dam and self.config.seek in [NonLinPipeline.SECTIONS_CUT,
                                                  NonLinPipeline.SECTIONS_CUT_TIME]:
      self.config.optparser.error('Invalid section mode without using dam, perhaps use --dam')

    # apply configuration to existing part
    self.config.set(bin_sorted_recursive(self.pipeline, True))
    # should be complete enough to handle tags
    if self.config.tag:
      tagsetter = self.pipeline.get_by_interface(gst.TagSetter)
      if tagsetter:
        taglist = gst.TagList()
        for k, v in self.config.tag.iteritems():
          # type info only in more recent gst-python
          if hasattr(gst, 'tag_get_tag_type'):
            gtype = gst.tag_get_tag_type(k)
            if gtype == gobject.TYPE_INVALID:
              # this should not happen as tags have been validated earlier
              print "WARNING: tag %s appears invalid, skipping" % (k)
            else:
              try:
                taglist[k] = gobject_convert_value(v, gtype)
              except ValueError:
                print "WARNING: skipping tag %s; value %s is not valid" % (k, v)
          else:
            taglist[k] = v
        if not taglist.is_empty():
          tagsetter.merge_tags(taglist, gst.TAG_MERGE_PREPEND)
      else:
        print "<<<< WARNING: Could not find element to set tags. >>>"

    self.nonlin = NonLinPipeline(self.pipeline, self.have_dam, complete, self.config.sections,
        self.config.seek, self.config.accurate, self.config.fps)
    # do_block should really be the default,
    # since that allows setting muxer to PAUSED before it receives anything
    if self.config.timeout:
      self.nonlin.do_block = True
    timeout = Timeout(self, self.nonlin, self.config.timeout)
    monitor = Monitor(self, self.nonlin)

    self.nonlin.start()
    self.loop = gobject.MainLoop()
    try:
      self.loop.run()
    except KeyboardInterrupt:
      if not self.on:
        raise
    # on becomes True when signal handler is put in place
    while self.on:
      try:
        self.loop.run()
      except KeyboardInterrupt:
        pass

    self.stop()

  def stop(self):
    if self.loop and self.loop.is_running():
      self.on = False
      self.loop.quit()
    else:
      sys.exit(self.exitcode)
# -- END Entrans

if __name__ == '__main__':
    app = Entrans(sys.argv)
    app.start()
