martastain

CasparCG subtitles

Oct 25th, 2013
534
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 6.14 KB | None | 0 0
  1. # -*- coding: utf-8 -*-
  2.  
  3. import telnetlib, sys, os
  4. from xml.etree import ElementTree as ET
  5. from time import *
  6.  
  7.  
  8. try:
  9.  import cairo
  10. except ImportError:
  11.  print "PyCairo is not installed. Exiting"
  12.  sys.exit(1)
  13.  
  14.  
  15.  
  16.  
  17. class CG():
  18.  SAFET = 54  
  19.  SAFEL = 96
  20.  SAFER = 1920 - SAFET
  21.  SAFEB = 1080 - SAFEL
  22.  
  23.  def __init__(self,width=1920,height=1080):
  24.   self.w = width
  25.   self.h = height
  26.   self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.w, self.h)
  27.   self.ctx     = cairo.Context (self.surface)
  28.   self.ctx.set_antialias(cairo.ANTIALIAS_GRAY)
  29.  
  30.  def subtitle(self,titles):
  31.   self.ctx.set_font_size(46)
  32.   self.ctx.select_font_face('Arial', cairo.FONT_SLANT_NORMAL,cairo.FONT_WEIGHT_BOLD)
  33.  
  34.   for i,text in enumerate(titles[::-1]):
  35.    tx, ty, tw, th, dx, dy = self.ctx.text_extents(text)
  36.    self.ctx.move_to((self.w/2)-(tw/2),self.SAFEB-(i*50))
  37.    
  38.    self.ctx.text_path (text);
  39.    self.ctx.set_source_rgb (.9, .9, .9)
  40.    self.ctx.fill_preserve()
  41.    self.ctx.set_source_rgb (.1, .1, .1)
  42.    self.ctx.set_line_width (2)
  43.    self.ctx.stroke()
  44.    
  45.    #self.ctx.show_text(text)
  46.    #self.ctx.stroke()
  47.  
  48.  def save(self, fname):
  49.   self.surface.write_to_png(fname)
  50.  
  51.  
  52.  
  53. #
  54. # This CasparServer class is a rip-off from our automation system,
  55. # stripped to bare minimum. Original one has multiple channels support,
  56. # MAM integration, playlists.... So this one is quite messy. sorry...
  57. #
  58. # Video feed is expected in 1-1
  59. #
  60.  
  61. class CasparServer():
  62.  def __init__(self):
  63.   self.Connect()
  64.   self.fpos = False
  65.   self.fdur = False
  66.   self.pos  = False
  67.   self.dur  = False
  68.   self.current_file = False
  69.   self.current_item = False
  70.   self.id_channel = 1
  71.   self.video_layer = 1
  72.  
  73.  def Connect(self):
  74.   self.cmd_conn = telnetlib.Telnet("localhost",5250)
  75.  
  76.  def Query(self,q):
  77.   conn = self.cmd_conn
  78.   try:
  79.    conn.write("%s\r\n"%q)
  80.    result = conn.read_until("\r\n").strip()
  81.   except:
  82.    return (False,"Connection failed.")
  83.   if not result:
  84.    return (False,"Query failed")
  85.   try:
  86.    if result[0:3] == "202":
  87.     return (True,result)
  88.    elif result[0:3] in ["201","200"]:
  89.     result = conn.read_until("\r\n").strip()
  90.     return (True,result)
  91.    elif int(result[0:1]) > 3:
  92.     return (False,result)
  93.   except:
  94.    return (False,result)
  95.   return (False,result)
  96.  
  97.  
  98.  def UpdateStat(self):
  99.   stat, res = self.Query("INFO %s"%self.id_channel)
  100.   request_time = time()
  101.   if not stat: return False
  102.   try:    xstat = ET.XML(res)
  103.   except:
  104.    print "XStat fail"
  105.    return False
  106.   else:
  107.    self.xstat = xstat
  108.  
  109.   try:
  110.    for layer in self.xstat.find("stage").find("layers").findall("layer"):
  111.     if int(layer.find("index").text) != self.video_layer: continue
  112.     else:
  113.      video_layer = layer
  114.      break
  115.    else:
  116.     raise Exception
  117.   except:
  118.    print "No video layer"
  119.    
  120.   try:
  121.    fg_proc = video_layer.find("foreground").find("producer")
  122.    if fg_proc.find("type").text == "image-producer":
  123.     self.fpos = self.fdur = self.pos = self.dur = 0
  124.    else:
  125.     self.fpos = int(fg_proc.find("file-frame-number").text)
  126.     self.fdur = int(fg_proc.find("file-nb-frames").text)
  127.     self.pos  = int(fg_proc.find("frame-number").text)
  128.     self.dur  = int(fg_proc.find("nb-frames").text)
  129.    self.current_file = os.path.splitext(fg_proc.find("filename").text)[0].split("\\")[-1]
  130.    self.current_item = self.current_file
  131.   except:
  132.    self.current_file = False
  133.  
  134.  
  135.  
  136. ########################################################################
  137.  
  138.  
  139.  
  140. class CGPlugin():
  141.  def __init__(self, channel):
  142.   self.channel = channel
  143.   self.current_item = False
  144.   self.tasks = []
  145.   self.OnCreate()
  146.  
  147.  def OnCreate(self):
  148.   pass
  149.  
  150.  def OnInit(self):
  151.   pass
  152.  
  153.  def Work(self):  
  154.   if self.channel.current_item != self.current_item:
  155.    self.current_item = self.channel.current_item
  156.    self.OnInit()
  157.   if not self.tasks: return
  158.   if self.tasks[0](): del self.tasks[0]
  159.  
  160.  
  161. ########################################################################
  162.  
  163.  
  164.  
  165. class Subtitler(CGPlugin):
  166.  SUBS_DIR  = "subs"
  167.  MEDIA_DIR = "c:\\hfn\\opt\\Caspar\\media\\"
  168.  SUBS_OFFSET = -10
  169.  
  170.  def OnCreate(self):
  171.   self.channel_string = "%s-2" % self.channel.id_channel
  172.  
  173.  def OnInit(self):
  174.   self.subs = []
  175.   self.tasks = []
  176.  
  177.   if self.LoadSubs():
  178.    self.next_start = 0
  179.    self.next_stop  = 0
  180.    self.tasks = [self.MakeSub,self.ShowSub]
  181.  
  182.  
  183.  def ParseTC(self,tcode,tbase=25):
  184.   hh,mm,ss = tcode.strip().split(":")
  185.   hh = int(hh)
  186.   mm = int(mm)
  187.   ss = float(ss.replace(",","."))
  188.   return (hh*3600*tbase) + (mm*60*tbase) + (ss*tbase)
  189.  
  190.  
  191.  def LoadSubs(self):
  192.   sub_file = os.path.join(self.SUBS_DIR,self.current_item+".srt")
  193.   if os.path.exists(sub_file):
  194.    for block in open(sub_file).read().split("\n\n"):
  195.     try:
  196.      block = block.split("\n")
  197.      tcodes = block[1]
  198.      lines = block[2:]
  199.     except:
  200.      print block
  201.      continue
  202.     start, stop = tcodes.split("-->")
  203.     start = self.ParseTC(start)
  204.     stop  = self.ParseTC(stop)
  205.     self.subs.append((start,stop,lines))
  206.    return True
  207.   else:
  208.    return False
  209.  
  210.  
  211.  def MakeSub(self):
  212.   try:    
  213.    self.next_start,self.next_stop, titles = self.subs.pop(0)
  214.   except:
  215.    self.next_start = self.next_stop = False
  216.    return False  
  217.  
  218.   print "Rendering subtitle:", titles
  219.   c = CG()
  220.   c.subtitle(titles)
  221.   c.save(os.path.join(self.MEDIA_DIR,"subtitle.png"))
  222.   return True
  223.  
  224.  
  225.  def ShowSub(self):
  226.   if self.channel.fpos > self.next_start + self.SUBS_OFFSET:
  227.    self.channel.Query("PLAY %s subtitle"%self.channel_string)
  228.    self.current_stop = self.next_stop
  229.    self.MakeSub()
  230.    self.tasks.append(self.HideSub)
  231.    return True
  232.   else:
  233.    return False
  234.    
  235.    
  236.  def HideSub(self):
  237.   if self.channel.fpos > self.current_stop + self.SUBS_OFFSET:
  238.    self.channel.Query("CLEAR %s"%self.channel_string)
  239.    if self.next_start:
  240.     self.tasks.append(self.ShowSub)
  241.    return True
  242.  
  243.  
  244.  
  245.  
  246. ########################################################################
  247.  
  248.  
  249. if __name__ == "__main__":
  250.  csp = CasparServer()
  251.  plg = Subtitler(csp)
  252.  
  253.  csp.Query("CLEAR 1-1")
  254.  csp.Query("CLEAR 1-2")
  255.  sleep(.1)
  256.  csp.Query("PLAY 1-1 tears_of_steel")
  257.  
  258.  while True:
  259.   sleep(.2)
  260.   csp.UpdateStat()
  261.   plg.Work()
Advertisement
Add Comment
Please, Sign In to add comment