Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,15 @@ If `frame_rate` is:

Default: `"2997DF"`

#### start_tc

`"start_tc" : null | "HH:MM:SS:FF" | "HH;MM;SS;FF"`

If not `null`, specifies the starting timecode for the SCC file. The timecode
must be consistent with the value of the `frame_rate` parameter.

Default: `null`

### LCD filter configuration (`"lcd"`)

#### Description
Expand Down
6 changes: 6 additions & 0 deletions src/main/python/ttconv/scc/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

from ttconv.config import ModuleConfiguration
from ttconv.style_properties import TextAlignType
from ttconv.time_code import SmpteTimeCode


class TextAlignment(Enum):
Expand Down Expand Up @@ -130,6 +131,11 @@ class SccWriterConfiguration(ModuleConfiguration):
metadata={"decoder": SCCFrameRate.from_value}
)

start_tc: typing.Optional[str] = field(
default=None,
metadata={"decoder": lambda y: str(y) if y is not None else None}
)

@classmethod
def name(cls):
return "scc_writer"
19 changes: 15 additions & 4 deletions src/main/python/ttconv/scc/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,10 @@ def insert(self, other: _Chunk):
def __len__(self):
return len(self._octet_buffer)

def to_string(self, fps : Fraction, is_df: bool):
def to_string(self, fps : Fraction, is_df: bool, start_offset: int) -> str:
if fps not in (FPS_29_97, FPS_30):
raise ValueError(f"Frame rate {fps} out-of-range")

if fps != FPS_29_97 and is_df:
raise ValueError("Frame rate must be fractional if drop frame is true")

Expand All @@ -229,7 +229,7 @@ def _octet2hex(octet):
if len(self._octet_buffer) % 2 == 1:
packets.append(_octet2hex(self._octet_buffer[-1]) + _octet2hex(0))

return str(SmpteTimeCode.from_frames(self.get_begin(), fps, is_df)) + "\t" + " ".join(packets)
return str(SmpteTimeCode.from_frames(self.get_begin() + start_offset, fps, is_df)) + "\t" + " ".join(packets)

MAX_LINEWIDTH = 32

Expand Down Expand Up @@ -409,4 +409,15 @@ def _isd_progress(progress: float):
edm_chunk.set_begin(int(caption.get_end() * config.frame_rate.fps))
chunks.append(edm_chunk)

return "Scenarist_SCC V1.0\n\n" + "\n\n".join(map(lambda e: e.to_string(config.frame_rate.fps, config.frame_rate.df), chunks))
start_offset = 0
if config.start_tc is not None:
start_tc = SmpteTimeCode.parse(config.start_tc, config.frame_rate.fps)
if start_tc.is_drop_frame() != config.frame_rate.df:
raise RuntimeError("The drop-frame status of the specified start_timecode does not match the drop-frame status of the specified frame_rate")
start_offset = start_tc.to_frames()

for chunk in chunks:
if start_offset + chunk.get_begin() < 0:
raise RuntimeError("The SCC stream would start earlier than the specified start timecode")

return "Scenarist_SCC V1.0\n\n" + "\n\n".join(map(lambda e: e.to_string(config.frame_rate.fps, config.frame_rate.df, start_offset), chunks))
3 changes: 3 additions & 0 deletions src/main/python/ttconv/time_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,9 @@ def from_frames(nb_frames: Union[int, Fraction], frame_rate: Fraction, is_df: Op
if frame_rate is None:
raise ValueError("Cannot compute time code from frames without frame rate")

if nb_frames is None or nb_frames < 0:
raise ValueError("Number of frames must not None and must not be less than zero")

if is_df is None:
drop_frame: bool = frame_rate.denominator == 1001
else:
Expand Down
33 changes: 33 additions & 0 deletions src/test/python/test_scc_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,39 @@ def test_basic_30FPS(self):
scc_from_model = scc_writer.from_model(model, config)
self.assertEqual(scc_from_model, expected_scc)

def test_zero_start(self):
ttml_doc_str = """<?xml version="1.0" encoding="UTF-8"?>
<tt xml:lang="en" xmlns="http://www.w3.org/ns/ttml"
xmlns:ttp="http://www.w3.org/ns/ttml#parameter"
ttp:frameRate="30">
<body>
<div>
<p begin="0f" end="90f">Hello</p>
</div>
</body>
</tt>"""

model = imsc_reader.to_model(et.ElementTree(et.fromstring(ttml_doc_str)))

with self.assertRaises(RuntimeError):
scc_writer.from_model(model)

scc_from_model = scc_writer.from_model(model, SccWriterConfiguration(start_tc="01:00:00;00"))
expected_scc="""Scenarist_SCC V1.0

00:59:59;21 9420 9420 94ae 94ae 9440 9440 c8e5 ecec ef80 942f 942f

01:00:02;29 942c 942c"""
self.assertEqual(scc_from_model, expected_scc)

scc_from_model = scc_writer.from_model(model, SccWriterConfiguration(frame_rate=SCCFrameRate.FPS_30_NDF, start_tc="01:00:00:00"))
expected_scc="""Scenarist_SCC V1.0

00:59:59:21 9420 9420 94ae 94ae 9440 9440 c8e5 ecec ef80 942f 942f

01:00:03:00 942c 942c"""
self.assertEqual(scc_from_model, expected_scc)

def test_multi_regions(self):
expected_scc="""Scenarist_SCC V1.0

Expand Down