upload
This commit is contained in:
696
utils/modules/pycaption/scc/__init__.py
Normal file
696
utils/modules/pycaption/scc/__init__.py
Normal file
@@ -0,0 +1,696 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
3 types of SCC captions:
|
||||
Roll-Up
|
||||
Paint-On
|
||||
Pop-On
|
||||
|
||||
Commands:
|
||||
94ae - [ENM] - Erase Non-displayed(buffer) Memory
|
||||
942c - [EDM] - Erase Displayed Memory
|
||||
9420 - [RCL] - Resume Caption Loading
|
||||
9429 - [RDC] - Resume Direct Captioning
|
||||
|
||||
9425, 9426, 94a7 - [RU2], [RU3], [RU4] (roll up captions 2,3 or 4 rows)
|
||||
- these commands set the number of expected lines
|
||||
|
||||
94ad - (in CEA-608-E: 142d) - [CR] carriage return.
|
||||
- This actually rolls the captions up as many rows as specified by
|
||||
[RU1], [RU2], or [RU3]
|
||||
|
||||
80 - no-op char. Doesn't do anything, but must be used with other
|
||||
characters, to make a 2 byte word
|
||||
|
||||
97a1, 97a2, 9723 - [TO] move 1, 2 or 3 columns - Tab Over command
|
||||
- this moves the positioning 1, 2, or 3 columns to the right
|
||||
- Nothing regarding this is implemented.
|
||||
|
||||
942f - [EOC] - display the buffer on the screen - End Of Caption
|
||||
... - [PAC] - Preamble address code (can set positioning and style)
|
||||
- All the PACs are specified by the first and second byte combined
|
||||
from pycaption.scc.constants.PAC_BYTES_TO_POSITIONING_MAP
|
||||
|
||||
9429 - [RDC] - Resume Direct Captioning
|
||||
94a4 - (in CEA-608-E: 1424) - [DER] Delete to End of Row
|
||||
|
||||
|
||||
Pop-On:
|
||||
The commands should usually appear in this order. Not strict though, and
|
||||
the the commands don't have to necessarily be on the same row.
|
||||
|
||||
1. 94ae [ENM] (erase non displayed memory)
|
||||
2. 9420 [RCL] (resume caption loading => this command here means we're using Pop-On captions)
|
||||
2.1? [ENM] - if step 0 was skipped?
|
||||
3. [PAC] Positioning/ styling command (can position on columns divisible by 4)
|
||||
The control chars is called Preamble Address Code [PAC].
|
||||
4. If positioning needs to be on columns not divisible by 4, use a [TO] command
|
||||
5. text
|
||||
6. 942c [EDM] - optionally, erase the currently displayed caption
|
||||
7. 942f [EOC] display the caption
|
||||
|
||||
|
||||
Roll-Up:
|
||||
1. [RU2], [RU3] or [RU4] - sets Roll-Up style and depth
|
||||
- these set the Roll-Up style: (characteristic command)
|
||||
2. [CR] to roll the display up 1 row...lol?
|
||||
3. [PAC] - sets the indent of the base row
|
||||
|
||||
|
||||
Paint-On:
|
||||
1. [RDC] - sets the Paint-On style (characteristic command)
|
||||
2. [PAC]
|
||||
3. text
|
||||
4. [PAC]
|
||||
5. text or [DER]
|
||||
|
||||
There are some rules regarding the parity of the commands.
|
||||
|
||||
This resource:
|
||||
http://www.theneitherworld.com/mcpoodle/SCC_TOOLS/DOCS/SCC_FORMAT.HTML
|
||||
specifies that there are interpreters which only work if the commands have an
|
||||
odd parity. This however is not consistent, and we might not handle well
|
||||
these cases. Odd parity of a command means that, converting toe word into
|
||||
binary, should result in an odd number of '1's. The PAC commands obey this
|
||||
rule, but some do not. Some commands that do not are found in the COMMANDS
|
||||
dictionary. This is legacy logic, that I didn't know how to handle, and
|
||||
just carried over when implementing positioning.
|
||||
"""
|
||||
|
||||
import re
|
||||
import math
|
||||
import textwrap
|
||||
from copy import deepcopy
|
||||
|
||||
import six
|
||||
|
||||
from pycaption.base import (
|
||||
BaseReader, BaseWriter, CaptionSet, CaptionNode,
|
||||
)
|
||||
from pycaption.exceptions import CaptionReadNoCaptions, InvalidInputError
|
||||
from .constants import (
|
||||
HEADER, COMMANDS, SPECIAL_CHARS, EXTENDED_CHARS, CHARACTERS,
|
||||
MICROSECONDS_PER_CODEWORD, CHARACTER_TO_CODE,
|
||||
SPECIAL_OR_EXTENDED_CHAR_TO_CODE, PAC_BYTES_TO_POSITIONING_MAP,
|
||||
PAC_HIGH_BYTE_BY_ROW, PAC_LOW_BYTE_BY_ROW_RESTRICTED,
|
||||
)
|
||||
from .specialized_collections import (
|
||||
TimingCorrectingCaptionList, NotifyingDict, CaptionCreator,
|
||||
InstructionNodeCreator)
|
||||
from .state_machines import DefaultProvidingPositionTracker
|
||||
|
||||
|
||||
class NodeCreatorFactory(object):
|
||||
"""Will return instances of the given node_creator.
|
||||
|
||||
This is used as a means of creating new InstructionNodeCreator instances,
|
||||
because these need to share state beyond their garbage collection, but
|
||||
storing the information at the class level is not good either, because
|
||||
this information must be erased after the reader's .read() operation
|
||||
completes.
|
||||
"""
|
||||
def __init__(self, position_tracker,
|
||||
node_creator=InstructionNodeCreator):
|
||||
self.position_tracker = position_tracker
|
||||
self.node_creator = node_creator
|
||||
|
||||
def new_creator(self):
|
||||
"""Returns a new instance of self.node_creator, initialized with
|
||||
the same italics_tracker, and position_tracker
|
||||
"""
|
||||
return self.node_creator(position_tracker=self.position_tracker)
|
||||
|
||||
def from_list(self, roll_rows):
|
||||
"""Wraps the node_creator's method with the same name
|
||||
|
||||
:param roll_rows: list of node_creator instances
|
||||
|
||||
:return: a node_creator instance
|
||||
"""
|
||||
return self.node_creator.from_list(
|
||||
roll_rows,
|
||||
position_tracker=self.position_tracker
|
||||
)
|
||||
|
||||
|
||||
def get_corrected_end_time(caption):
|
||||
"""If the last caption was never explicitly ended, set its end time to
|
||||
start + 4 seconds
|
||||
|
||||
:param Caption caption: the last caption
|
||||
:rtype: int
|
||||
"""
|
||||
if caption.end:
|
||||
return caption.end
|
||||
|
||||
return caption.start + 4 * 1000 * 1000
|
||||
|
||||
|
||||
class SCCReader(BaseReader):
|
||||
"""Converts a given unicode string to a CaptionSet.
|
||||
|
||||
This can be then later used for converting into any other supported formats
|
||||
"""
|
||||
def __init__(self, *args, **kw):
|
||||
self.caption_stash = CaptionCreator()
|
||||
self.time_translator = _SccTimeTranslator()
|
||||
|
||||
self.node_creator_factory = NodeCreatorFactory(
|
||||
DefaultProvidingPositionTracker()
|
||||
)
|
||||
|
||||
self.last_command = ''
|
||||
|
||||
self.buffer_dict = NotifyingDict()
|
||||
|
||||
self.buffer_dict['pop'] = self.node_creator_factory.new_creator()
|
||||
self.buffer_dict['paint'] = self.node_creator_factory.new_creator()
|
||||
self.buffer_dict['roll'] = self.node_creator_factory.new_creator()
|
||||
|
||||
# Call this method when the active key changes
|
||||
self.buffer_dict.add_change_observer(self._flush_implicit_buffers)
|
||||
self.buffer_dict.set_active('pop')
|
||||
|
||||
self.roll_rows = []
|
||||
self.roll_rows_expected = 0
|
||||
self.simulate_roll_up = False
|
||||
|
||||
self.time = 0
|
||||
|
||||
def detect(self, content):
|
||||
"""Checks whether the given content is a proper SCC file
|
||||
|
||||
:type content: unicode
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
lines = content.splitlines()
|
||||
if lines[0] == HEADER:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def read(self, content, lang='en-US', simulate_roll_up=False, offset=0):
|
||||
"""Converts the unicode string into a CaptionSet
|
||||
|
||||
:type content: six.text_type
|
||||
:param content: The SCC content to be converted to a CaptionSet
|
||||
|
||||
:type lang: six.text_type
|
||||
:param lang: The language of the caption
|
||||
|
||||
:type simulate_roll_up: bool
|
||||
:param simulate_roll_up: If True, when converting to other formats,
|
||||
the resulting captions will contain all the rows that were visible
|
||||
on the screen when the captions were rolling up.
|
||||
|
||||
:type offset: int
|
||||
:param offset:
|
||||
|
||||
:rtype: CaptionSet
|
||||
"""
|
||||
if type(content) != six.text_type:
|
||||
raise InvalidInputError('The content is not a unicode string.')
|
||||
|
||||
self.simulate_roll_up = simulate_roll_up
|
||||
self.time_translator.offset = offset * 1000000
|
||||
# split lines
|
||||
lines = content.splitlines()
|
||||
|
||||
# loop through each line except the first
|
||||
for line in lines[1:]:
|
||||
self._translate_line(line)
|
||||
|
||||
self._flush_implicit_buffers()
|
||||
|
||||
captions = CaptionSet({lang: self.caption_stash.get_all()})
|
||||
|
||||
# check captions for incorrect lengths
|
||||
for cap in captions.get_captions(lang):
|
||||
# if there's an end time on a caption and the difference is
|
||||
# less than .05s kill it (this is likely caused by a standalone
|
||||
# EOC marker in the SCC file)
|
||||
if 0 < cap.end - cap.start < 50000:
|
||||
raise ValueError('unsupported length found in SCC input file: ' + str(cap))
|
||||
|
||||
if captions.is_empty():
|
||||
raise CaptionReadNoCaptions("empty caption file")
|
||||
else:
|
||||
last_caption = captions.get_captions(lang)[-1]
|
||||
last_caption.end = get_corrected_end_time(last_caption)
|
||||
|
||||
return captions
|
||||
|
||||
def _fix_last_timing(self, timing):
|
||||
"""HACK HACK: Certain Paint-On captions don't specify the 942f [EOC]
|
||||
(End Of Caption) command on the same line.
|
||||
If this is a 942f line, also simulate a 942c (Erase Displayed Memory)
|
||||
to properly set the timing on the last caption.
|
||||
|
||||
This method needs some serious attention, because it proves the timing
|
||||
calculation is not done well for Pop-On captions
|
||||
"""
|
||||
# Calculate the end time from the current line
|
||||
time_translator = _SccTimeTranslator()
|
||||
time_translator.start_at(timing)
|
||||
time_translator.offset = self.time_translator.offset
|
||||
|
||||
# But use the current time translator for the start time
|
||||
self.caption_stash.create_and_store(
|
||||
self.buffer, self.time_translator.get_time())
|
||||
|
||||
self.caption_stash.correct_last_timing(time_translator.get_time())
|
||||
self.buffer = self.node_creator_factory.node_creator()
|
||||
|
||||
def _flush_implicit_buffers(self, old_key=None, *args):
|
||||
"""Convert to Captions those buffers whose behavior is implicit.
|
||||
|
||||
The Paint-On buffer is explicit. New captions are created from it
|
||||
with the command 'End Of Caption' [EOC], '942f'
|
||||
|
||||
The other 2 buffers, Roll-Up and Paint-On we treat as "more" implicit,
|
||||
meaning that they can be displayed by a command on the next row.
|
||||
If they're on the last row however, or if the caption type is changing,
|
||||
we make sure to convert the buffers to text, so we don't lose any info.
|
||||
"""
|
||||
if old_key == 'pop':
|
||||
return
|
||||
|
||||
elif old_key is None or old_key == 'roll':
|
||||
if not self.buffer.is_empty():
|
||||
self._roll_up()
|
||||
|
||||
elif old_key is None or old_key == 'paint':
|
||||
# xxx - perhaps the self.buffer property is sufficient
|
||||
if not self.buffer_dict['paint'].is_empty():
|
||||
self.caption_stash.create_and_store(
|
||||
self.buffer_dict['paint'], self.time)
|
||||
|
||||
def _translate_line(self, line):
|
||||
# ignore blank lines
|
||||
if line.strip() == '':
|
||||
return
|
||||
|
||||
# split line in timestamp and words
|
||||
r = re.compile(r"([0-9:;]*)([\s\t]*)((.)*)")
|
||||
parts = r.findall(line.lower())
|
||||
|
||||
# XXX!!!!!! THESE 2 LINES ARE A HACK
|
||||
if parts[0][2].strip() == '942f':
|
||||
self._fix_last_timing(timing=parts[0][0])
|
||||
|
||||
self.time_translator.start_at(parts[0][0])
|
||||
|
||||
# loop through each word
|
||||
for word in parts[0][2].split(' '):
|
||||
# ignore empty results
|
||||
if word.strip() != '':
|
||||
self._translate_word(word)
|
||||
|
||||
def _translate_word(self, word):
|
||||
# count frames for timing
|
||||
self.time_translator.increment_frames()
|
||||
|
||||
# first check if word is a command
|
||||
# TODO - check that all the positioning commands are here, or use
|
||||
# some other strategy to determine if the word is a command.
|
||||
if word in COMMANDS or _is_pac_command(word):
|
||||
self._translate_command(word)
|
||||
|
||||
# second, check if word is a special character
|
||||
elif word in SPECIAL_CHARS:
|
||||
self._translate_special_char(word)
|
||||
|
||||
elif word in EXTENDED_CHARS:
|
||||
self._translate_extended_char(word)
|
||||
|
||||
# third, try to convert word into 2 characters
|
||||
else:
|
||||
self._translate_characters(word)
|
||||
|
||||
def _handle_double_command(self, word):
|
||||
# ensure we don't accidentally use the same command twice
|
||||
if word == self.last_command:
|
||||
self.last_command = ''
|
||||
return True
|
||||
else:
|
||||
self.last_command = word
|
||||
return False
|
||||
|
||||
def _translate_special_char(self, word):
|
||||
# XXX - this looks highly buggy. Why should special chars be ignored
|
||||
# when printed 2 times one after another?
|
||||
if self._handle_double_command(word):
|
||||
return
|
||||
|
||||
self.buffer.add_chars(SPECIAL_CHARS[word])
|
||||
|
||||
def _translate_extended_char(self, word):
|
||||
# XXX - this looks highly buggy. Why would a special char be ignored
|
||||
# if it's printed 2 times one after another?
|
||||
if self._handle_double_command(word):
|
||||
return
|
||||
|
||||
# add to buffer
|
||||
self.buffer.add_chars(EXTENDED_CHARS[word])
|
||||
|
||||
def _translate_command(self, word):
|
||||
if self._handle_double_command(word):
|
||||
return
|
||||
|
||||
# if command is pop_up
|
||||
if word == '9420':
|
||||
self.buffer_dict.set_active('pop')
|
||||
|
||||
# command is paint_on [Resume Direct Captioning]
|
||||
elif word == '9429':
|
||||
self.buffer_dict.set_active('paint')
|
||||
|
||||
self.roll_rows_expected = 1
|
||||
if not self.buffer.is_empty():
|
||||
self.caption_stash.create_and_store(
|
||||
self.buffer, self.time
|
||||
)
|
||||
self.buffer = self.node_creator_factory.new_creator()
|
||||
|
||||
self.time = self.time_translator.get_time()
|
||||
|
||||
# if command is roll_up 2, 3 or 4 rows
|
||||
elif word in ('9425', '9426', '94a7'):
|
||||
self.buffer_dict.set_active('roll')
|
||||
|
||||
# count how many lines are expected
|
||||
if word == '9425':
|
||||
self.roll_rows_expected = 2
|
||||
elif word == '9426':
|
||||
self.roll_rows_expected = 3
|
||||
elif word == '94a7':
|
||||
self.roll_rows_expected = 4
|
||||
|
||||
# if content is in the queue, turn it into a caption
|
||||
if not self.buffer.is_empty():
|
||||
self.caption_stash.create_and_store(
|
||||
self.buffer, self.time)
|
||||
self.buffer = self.node_creator_factory.new_creator()
|
||||
|
||||
# set rows to empty, configure start time for caption
|
||||
self.roll_rows = []
|
||||
self.time = self.time_translator.get_time()
|
||||
|
||||
# clear pop_on buffer
|
||||
elif word == '94ae':
|
||||
self.buffer = self.node_creator_factory.new_creator()
|
||||
|
||||
# display pop_on buffer [End Of Caption]
|
||||
elif word == '942f':
|
||||
self.time = self.time_translator.get_time()
|
||||
self.caption_stash.create_and_store(self.buffer, self.time)
|
||||
self.buffer = self.node_creator_factory.new_creator()
|
||||
|
||||
# roll up captions [Carriage Return]
|
||||
elif word == '94ad':
|
||||
# display roll-up buffer
|
||||
if not self.buffer.is_empty():
|
||||
self._roll_up()
|
||||
|
||||
# clear screen
|
||||
elif word == '942c':
|
||||
self.roll_rows = []
|
||||
|
||||
# XXX - The 942c command has nothing to do with paint-ons
|
||||
# This however is legacy code, and will break lots of tests if
|
||||
# the proper buffer (self.buffer) is used.
|
||||
# Most likely using `self.buffer` instead of the paint buffer
|
||||
# is the right thing to do, but this needs some further attention.
|
||||
if not self.buffer_dict['paint'].is_empty():
|
||||
self.caption_stash.create_and_store(
|
||||
self.buffer_dict['paint'], self.time)
|
||||
self.buffer = self.node_creator_factory.new_creator()
|
||||
|
||||
# attempt to add proper end time to last caption(s)
|
||||
self.caption_stash.correct_last_timing(
|
||||
self.time_translator.get_time())
|
||||
|
||||
# if command not one of the aforementioned, add to buffer
|
||||
else:
|
||||
self.buffer.interpret_command(word)
|
||||
|
||||
def _translate_characters(self, word):
|
||||
# split word into the 2 bytes
|
||||
byte1 = word[:2]
|
||||
byte2 = word[2:]
|
||||
|
||||
# check to see if the the bytes are recognized characters
|
||||
if byte1 not in CHARACTERS or byte2 not in CHARACTERS:
|
||||
return
|
||||
|
||||
self.buffer.add_chars(CHARACTERS[byte1], CHARACTERS[byte2])
|
||||
|
||||
@property
|
||||
def buffer(self):
|
||||
"""Returns the currently active buffer
|
||||
"""
|
||||
return self.buffer_dict.get_active()
|
||||
|
||||
@buffer.setter
|
||||
def buffer(self, value):
|
||||
"""Sets a new value to the active key
|
||||
|
||||
:param value: any object
|
||||
"""
|
||||
try:
|
||||
key = self.buffer_dict.active_key
|
||||
self.buffer_dict[key] = value
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def _roll_up(self):
|
||||
# We expect the active buffer to be the rol buffer
|
||||
if self.simulate_roll_up:
|
||||
if self.roll_rows_expected > 1:
|
||||
if len(self.roll_rows) >= self.roll_rows_expected:
|
||||
self.roll_rows.pop(0)
|
||||
|
||||
self.roll_rows.append(self.buffer)
|
||||
self.buffer = self.node_creator_factory.from_list(
|
||||
self.roll_rows)
|
||||
|
||||
# convert buffer and empty
|
||||
self.caption_stash.create_and_store(self.buffer, self.time)
|
||||
self.buffer = self.node_creator_factory.new_creator()
|
||||
|
||||
# configure time
|
||||
self.time = self.time_translator.get_time()
|
||||
|
||||
# try to insert the proper ending time for the previous caption
|
||||
self.caption_stash.correct_last_timing(self.time, force=True)
|
||||
|
||||
|
||||
class SCCWriter(BaseWriter):
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
super(SCCWriter, self).__init__(*args, **kw)
|
||||
|
||||
def write(self, caption_set):
|
||||
output = HEADER + '\n\n'
|
||||
|
||||
if caption_set.is_empty():
|
||||
return output
|
||||
|
||||
caption_set = deepcopy(caption_set)
|
||||
|
||||
# Only support one language.
|
||||
lang = list(caption_set.get_languages())[0]
|
||||
captions = caption_set.get_captions(lang)
|
||||
|
||||
# PASS 1: compute codes for each caption
|
||||
codes = [(self._text_to_code(caption), caption.start, caption.end)
|
||||
for caption in captions]
|
||||
|
||||
# PASS 2:
|
||||
# Advance start times so as to have time to write to the pop-on
|
||||
# buffer; possibly remove the previous clear-screen command
|
||||
for index, (code, start, end) in enumerate(codes):
|
||||
code_words = len(code) / 5 + 8
|
||||
code_time_microseconds = code_words * MICROSECONDS_PER_CODEWORD
|
||||
code_start = start - code_time_microseconds
|
||||
if index == 0:
|
||||
continue
|
||||
previous_code, previous_start, previous_end = codes[index-1]
|
||||
if previous_end + 3 * MICROSECONDS_PER_CODEWORD >= code_start:
|
||||
codes[index-1] = (previous_code, previous_start, None)
|
||||
codes[index] = (code, code_start, end)
|
||||
|
||||
# PASS 3:
|
||||
# Write captions.
|
||||
for (code, start, end) in codes:
|
||||
output += ('%s\t' % self._format_timestamp(start))
|
||||
output += '94ae 94ae 9420 9420 '
|
||||
output += code
|
||||
output += '942c 942c 942f 942f\n\n'
|
||||
if end is not None:
|
||||
output += '%s\t942c 942c\n\n' % self._format_timestamp(end)
|
||||
|
||||
return output
|
||||
|
||||
# Wrap lines at 32 chars
|
||||
@staticmethod
|
||||
def _layout_line(caption):
|
||||
def caption_node_to_text(caption_node):
|
||||
if caption_node.type_ == CaptionNode.TEXT:
|
||||
return six.text_type(caption_node.content)
|
||||
elif caption_node.type_ == CaptionNode.BREAK:
|
||||
return '\n'
|
||||
caption_text = ''.join(
|
||||
[caption_node_to_text(node) for node in caption.nodes])
|
||||
inner_lines = caption_text.split('\n')
|
||||
inner_lines_laid_out = [textwrap.fill(x, 32) for x in inner_lines]
|
||||
return '\n'.join(inner_lines_laid_out)
|
||||
|
||||
@staticmethod
|
||||
def _maybe_align(code):
|
||||
# Finish a half-word with a no-op so we can move to a full word
|
||||
if len(code) % 5 == 2:
|
||||
code += '80 '
|
||||
return code
|
||||
|
||||
@staticmethod
|
||||
def _maybe_space(code):
|
||||
if len(code) % 5 == 4:
|
||||
code += ' '
|
||||
return code
|
||||
|
||||
def _print_character(self, code, char):
|
||||
try:
|
||||
char_code = CHARACTER_TO_CODE[char]
|
||||
except KeyError:
|
||||
try:
|
||||
char_code = SPECIAL_OR_EXTENDED_CHAR_TO_CODE[char]
|
||||
except KeyError:
|
||||
char_code = '91b6' # Use £ as "unknown character" symbol
|
||||
|
||||
if len(char_code) == 2:
|
||||
return code + char_code
|
||||
elif len(char_code) == 4:
|
||||
return self._maybe_align(code) + char_code
|
||||
else:
|
||||
# This should not happen!
|
||||
return code
|
||||
|
||||
def _text_to_code(self, s):
|
||||
code = ''
|
||||
lines = self._layout_line(s).split('\n')
|
||||
for row, line in enumerate(lines):
|
||||
row += 16 - len(lines)
|
||||
# Move cursor to column 0 of the destination row
|
||||
for _ in range(2):
|
||||
code += ('%s%s ' % (PAC_HIGH_BYTE_BY_ROW[row],
|
||||
PAC_LOW_BYTE_BY_ROW_RESTRICTED[row]))
|
||||
# Print the line using the SCC encoding
|
||||
for char in line:
|
||||
code = self._print_character(code, char)
|
||||
code = self._maybe_space(code)
|
||||
code = self._maybe_align(code)
|
||||
return code
|
||||
|
||||
@staticmethod
|
||||
def _format_timestamp(microseconds):
|
||||
seconds_float = microseconds / 1000.0 / 1000.0
|
||||
# Convert to non-drop-frame timecode
|
||||
seconds_float *= 1000.0 / 1001.0
|
||||
hours = math.floor(seconds_float / 3600)
|
||||
seconds_float -= hours * 3600
|
||||
minutes = math.floor(seconds_float / 60)
|
||||
seconds_float -= minutes * 60
|
||||
seconds = math.floor(seconds_float)
|
||||
seconds_float -= seconds
|
||||
frames = math.floor(seconds_float * 30)
|
||||
return '%02d:%02d:%02d:%02d' % (hours, minutes, seconds, frames)
|
||||
|
||||
|
||||
class _SccTimeTranslator(object):
|
||||
"""Converts SCC time to microseconds, keeping track of frames passed
|
||||
"""
|
||||
def __init__(self):
|
||||
self._time = '00:00:00;00'
|
||||
|
||||
# microseconds. The offset from which we begin the time calculation
|
||||
self.offset = 0
|
||||
self._frames = 0
|
||||
|
||||
def get_time(self):
|
||||
"""Returns the time, in microseconds. Takes into account the number of
|
||||
frames passed, and the offset
|
||||
|
||||
:rtype: int
|
||||
"""
|
||||
return self._translate_time(
|
||||
self._time[:-2] + six.text_type(int(self._time[-2:]) + self._frames),
|
||||
self.offset
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _translate_time(stamp, offset):
|
||||
"""
|
||||
:param stamp:
|
||||
:type offset: int
|
||||
:param offset: Subtract this many microseconds from the calculated time
|
||||
Helpful for when the captions are off by some time interval.
|
||||
:rtype: int
|
||||
"""
|
||||
if ';' in stamp:
|
||||
# Drop-frame timebase runs at the same rate as wall clock
|
||||
seconds_per_timestamp_second = 1.0
|
||||
else:
|
||||
# Non-drop-frame timebase runs "slow"
|
||||
# 1 second of timecode is longer than an actual second (1.001s)
|
||||
seconds_per_timestamp_second = 1001.0 / 1000.0
|
||||
|
||||
time_split = stamp.replace(';', ':').split(':')
|
||||
|
||||
timestamp_seconds = (int(time_split[0]) * 3600 +
|
||||
int(time_split[1]) * 60 +
|
||||
int(time_split[2]) +
|
||||
int(time_split[3]) / 30.0)
|
||||
|
||||
seconds = timestamp_seconds * seconds_per_timestamp_second
|
||||
microseconds = seconds * 1000 * 1000 - offset
|
||||
|
||||
if microseconds < 0:
|
||||
microseconds = 0
|
||||
|
||||
return microseconds
|
||||
|
||||
def start_at(self, timespec):
|
||||
"""Reset the counter to the given time
|
||||
|
||||
:type timespec: unicode
|
||||
"""
|
||||
self._time = timespec
|
||||
self._frames = 0
|
||||
|
||||
def increment_frames(self):
|
||||
"""After a command was processed, we'd increment the number of frames
|
||||
"""
|
||||
self._frames += 1
|
||||
|
||||
|
||||
def _is_pac_command(word):
|
||||
"""Checks whether the given word is a Preamble Address Code [PAC] command
|
||||
|
||||
:type word: unicode
|
||||
:param word: 4 letter unicode command
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
if not word or len(word) != 4:
|
||||
return False
|
||||
|
||||
byte1, byte2 = word[:2], word[2:]
|
||||
|
||||
try:
|
||||
PAC_BYTES_TO_POSITIONING_MAP[byte1][byte2]
|
||||
except KeyError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
Reference in New Issue
Block a user