Fixing a few things about the videoZ
This commit is contained in:
@@ -328,6 +328,9 @@ class VideoSource:
|
|||||||
float
|
float
|
||||||
] = None # Max duration to play (seconds). Default: until next slide or end of clip
|
] = None # Max duration to play (seconds). Default: until next slide or end of clip
|
||||||
skip: float = 0.0 # Skip this many seconds at start of video (seek point)
|
skip: float = 0.0 # Skip this many seconds at start of video (seek point)
|
||||||
|
loop: bool = False # If True, loop the [skip, skip+take] window to fill the display
|
||||||
|
# window (end_on). Without take, loops the whole clip from skip. Distinct from
|
||||||
|
# end_on="loop" (which loops to the render end); loop rides on any end_on.
|
||||||
zoom: float = (
|
zoom: float = (
|
||||||
1.0 # Scale factor for video (1.0 = fit to cutout height, >1 = enlarge)
|
1.0 # Scale factor for video (1.0 = fit to cutout height, >1 = enlarge)
|
||||||
)
|
)
|
||||||
@@ -454,6 +457,12 @@ class VideoEvent:
|
|||||||
# the render-plan listing can report the ACTUAL end rule (incl. inline overrides),
|
# the render-plan listing can report the ACTUAL end rule (incl. inline overrides),
|
||||||
# not just the videos.json default. Purely informational; end_time is authoritative.
|
# not just the videos.json default. Purely informational; end_time is authoritative.
|
||||||
end_on: str = ""
|
end_on: str = ""
|
||||||
|
# Resolved PER-OCCURRENCE playback controls (inline override > videos.json). The
|
||||||
|
# renderer reads these, not video_source.*, so the same handle can e.g. seek to a
|
||||||
|
# different point or loop in one place but not another.
|
||||||
|
skip: float = 0.0 # seek point into the source (seconds)
|
||||||
|
take: Optional[float] = None # display duration / loop period (seconds); None = window
|
||||||
|
loop: bool = False # loop the [skip, skip+take] window across the display window
|
||||||
# Resolved per-occurrence CSS-like cutout placement (see VideoSource).
|
# Resolved per-occurrence CSS-like cutout placement (see VideoSource).
|
||||||
object_fit: str = "cover"
|
object_fit: str = "cover"
|
||||||
object_position: str = "center"
|
object_position: str = "center"
|
||||||
|
|||||||
+21
-3
@@ -59,20 +59,35 @@ def _resolve_case_insensitive(path: Path) -> Path:
|
|||||||
# (transformer.resolve_video_presentation). Global params (skip/zoom/volume/…) are not
|
# (transformer.resolve_video_presentation). Global params (skip/zoom/volume/…) are not
|
||||||
# yet overridable inline; unknown keys are ignored.
|
# yet overridable inline; unknown keys are ignored.
|
||||||
_MARKER_OVERRIDE_KEYS = frozenset(
|
_MARKER_OVERRIDE_KEYS = frozenset(
|
||||||
{"cutout", "layer", "end_on", "take", "volume", "object-fit", "object-position"}
|
{"cutout", "layer", "end_on", "take", "skip", "loop", "volume",
|
||||||
|
"object-fit", "object-position"}
|
||||||
)
|
)
|
||||||
# Override keys that are numeric (coerced to float).
|
# Override keys that are numeric (coerced to float).
|
||||||
_MARKER_NUMERIC_KEYS = frozenset({"take", "volume"})
|
_MARKER_NUMERIC_KEYS = frozenset({"take", "skip", "volume"})
|
||||||
|
# Override keys that are booleans (true/false/1/0/yes/no).
|
||||||
|
_MARKER_BOOL_KEYS = frozenset({"loop"})
|
||||||
|
# Inline aliases → canonical override key. `duration` reads naturally in the
|
||||||
|
# manuscript but means the same as `take` (how long the clip plays / the loop
|
||||||
|
# period), so it maps to take and is stored/rendered as take.
|
||||||
|
_MARKER_KEY_ALIASES = {"duration": "take"}
|
||||||
|
|
||||||
|
|
||||||
def _coerce_marker_value(key: str, raw: str):
|
def _coerce_marker_value(key: str, raw: str):
|
||||||
"""Coerce an inline override value: numeric keys → float; everything else → string."""
|
"""Coerce an inline override value: numeric keys → float, bool keys → bool,
|
||||||
|
everything else → string. Returns None if a typed value can't be parsed."""
|
||||||
v = raw.strip().strip('"').strip("'")
|
v = raw.strip().strip('"').strip("'")
|
||||||
if key in _MARKER_NUMERIC_KEYS:
|
if key in _MARKER_NUMERIC_KEYS:
|
||||||
try:
|
try:
|
||||||
return float(v)
|
return float(v)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
if key in _MARKER_BOOL_KEYS:
|
||||||
|
lv = v.lower()
|
||||||
|
if lv in ("true", "1", "yes", "on"):
|
||||||
|
return True
|
||||||
|
if lv in ("false", "0", "no", "off"):
|
||||||
|
return False
|
||||||
|
return None
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
@@ -100,6 +115,7 @@ def parse_marker(raw: str) -> "tuple[str, Optional[dict]]":
|
|||||||
continue
|
continue
|
||||||
key, _, val = tok.partition("=")
|
key, _, val = tok.partition("=")
|
||||||
key = key.strip().lower()
|
key = key.strip().lower()
|
||||||
|
key = _MARKER_KEY_ALIASES.get(key, key) # duration → take, etc.
|
||||||
if key in _MARKER_OVERRIDE_KEYS:
|
if key in _MARKER_OVERRIDE_KEYS:
|
||||||
coerced = _coerce_marker_value(key, val)
|
coerced = _coerce_marker_value(key, val)
|
||||||
if coerced is not None:
|
if coerced is not None:
|
||||||
@@ -636,6 +652,7 @@ def parse_videos(
|
|||||||
output_file=video_data.get("output_file"),
|
output_file=video_data.get("output_file"),
|
||||||
take=take,
|
take=take,
|
||||||
skip=skip,
|
skip=skip,
|
||||||
|
loop=bool(video_data.get("loop", False)),
|
||||||
zoom=video_data.get("zoom", 1.0),
|
zoom=video_data.get("zoom", 1.0),
|
||||||
cutout=video_data.get("cutout"),
|
cutout=video_data.get("cutout"),
|
||||||
object_fit=video_data.get("object-fit", "cover"),
|
object_fit=video_data.get("object-fit", "cover"),
|
||||||
@@ -954,6 +971,7 @@ def resolve_missing_videos(
|
|||||||
output_file=entry.get("output_file"),
|
output_file=entry.get("output_file"),
|
||||||
take=entry.get("take"),
|
take=entry.get("take"),
|
||||||
skip=float(entry.get("skip", 0.0)),
|
skip=float(entry.get("skip", 0.0)),
|
||||||
|
loop=bool(entry.get("loop", False)),
|
||||||
zoom=float(entry.get("zoom", 1.0)),
|
zoom=float(entry.get("zoom", 1.0)),
|
||||||
cutout=entry.get("cutout"),
|
cutout=entry.get("cutout"),
|
||||||
always_visible=bool(entry.get("always_visible", False)),
|
always_visible=bool(entry.get("always_visible", False)),
|
||||||
|
|||||||
+86
-27
@@ -300,6 +300,63 @@ def _ci_resolve(path: Path) -> Path:
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _video_playback(event, fps: int) -> tuple[float, float, int]:
|
||||||
|
"""Per-occurrence playback geometry for a triggered video overlay.
|
||||||
|
|
||||||
|
Returns (skip, display_duration, loop_frames):
|
||||||
|
skip seek into the source — the chunk-seam override if present, else
|
||||||
|
the resolved per-occurrence skip (inline > videos.json).
|
||||||
|
display_duration how long the overlay is shown, in OUTPUT seconds. Normally the
|
||||||
|
clip's window (end-start), capped by `take` when set. With
|
||||||
|
loop=true, `take` is the loop PERIOD, not a display cap, so the
|
||||||
|
overlay fills the whole window.
|
||||||
|
loop_frames >0 → loop this many source frames (a filtergraph `loop` over the
|
||||||
|
[skip, skip+take] sub-window); 0 → no filtergraph loop. Only set
|
||||||
|
when loop=true AND take is given; whole-clip looping is handled by
|
||||||
|
the input-level -stream_loop auto-loop instead.
|
||||||
|
|
||||||
|
Single source of truth so the input builder and every overlay layer agree.
|
||||||
|
"""
|
||||||
|
# event.skip is the already-resolved per-occurrence value (inline > videos.json),
|
||||||
|
# so trust it verbatim — don't `or` it against video_source.skip, or an explicit
|
||||||
|
# skip=0 override (falsy) would wrongly fall back to the videos.json skip. The
|
||||||
|
# chunk-seam override, when present, wins over both.
|
||||||
|
if getattr(event, "skip_override", None) is not None:
|
||||||
|
skip = event.skip_override
|
||||||
|
elif hasattr(event, "skip"):
|
||||||
|
skip = event.skip or 0.0
|
||||||
|
else:
|
||||||
|
skip = event.video_source.skip or 0.0
|
||||||
|
window = event.end_time - event.start_time
|
||||||
|
take = getattr(event, "take", None)
|
||||||
|
if take is None:
|
||||||
|
take = event.video_source.take
|
||||||
|
loop = bool(getattr(event, "loop", False))
|
||||||
|
if loop:
|
||||||
|
display = window
|
||||||
|
loop_frames = int(round(take * fps)) if (take and take > 0) else 0
|
||||||
|
else:
|
||||||
|
display = window if take is None else min(window, take)
|
||||||
|
loop_frames = 0
|
||||||
|
return skip, display, loop_frames
|
||||||
|
|
||||||
|
|
||||||
|
def _trig_video_pts(event, fps: int, loop_frames: int) -> tuple[str, str]:
|
||||||
|
"""(loop_prefix, setpts_expr) for a triggered-video overlay source chain.
|
||||||
|
|
||||||
|
When loop_frames>0 the source is a `loop` filter repeating a `loop_frames`-frame
|
||||||
|
window forever; loop can leave non-monotonic PTS, so re-time from the frame index
|
||||||
|
(N/fps) plus the clip's output start. Otherwise use the normal PTS rebase+offset.
|
||||||
|
"""
|
||||||
|
start = event.start_time
|
||||||
|
if loop_frames > 0:
|
||||||
|
return (
|
||||||
|
f"loop=loop=-1:size={loop_frames}:start=0,",
|
||||||
|
f"setpts=N/({fps}*TB)+{start:.3f}/TB",
|
||||||
|
)
|
||||||
|
return "", f"setpts=PTS-STARTPTS+{start:.3f}/TB"
|
||||||
|
|
||||||
|
|
||||||
def _resolve_video_path(
|
def _resolve_video_path(
|
||||||
videos_dir: Path,
|
videos_dir: Path,
|
||||||
video_source: VideoSource,
|
video_source: VideoSource,
|
||||||
@@ -632,15 +689,8 @@ def build_ffmpeg_command(plan: RenderPlan, output_path: Path) -> list[str]:
|
|||||||
video_path = _resolve_video_path(
|
video_path = _resolve_video_path(
|
||||||
videos_dir, event.video_source, shared_assets_dir, project_path
|
videos_dir, event.video_source, shared_assets_dir, project_path
|
||||||
)
|
)
|
||||||
# Chunking v2 (docs/chunking_v2.md): a clip that began before this chunk
|
# Per-occurrence geometry (chunk-seam skip_override, per-event skip/take/loop).
|
||||||
# resumes mid-clip via skip_override. None today (v1) → the source's own skip.
|
skip, clip_duration, loop_frames = _video_playback(event, plan.config.fps)
|
||||||
skip = event.skip_override if getattr(event, "skip_override", None) is not None \
|
|
||||||
else (event.video_source.skip or 0.0)
|
|
||||||
|
|
||||||
# How long this clip needs to play in the output
|
|
||||||
clip_duration = event.end_time - event.start_time
|
|
||||||
if event.video_source.take is not None:
|
|
||||||
clip_duration = min(clip_duration, event.video_source.take)
|
|
||||||
|
|
||||||
# Loop the clip if the file is shorter than the display window.
|
# Loop the clip if the file is shorter than the display window.
|
||||||
# Don't loop pause-narration videos — they intentionally play once and stop.
|
# Don't loop pause-narration videos — they intentionally play once and stop.
|
||||||
@@ -654,6 +704,21 @@ def build_ffmpeg_command(plan: RenderPlan, output_path: Path) -> list[str]:
|
|||||||
if has_audio:
|
if has_audio:
|
||||||
video_events_with_audio.add(i)
|
video_events_with_audio.add(i)
|
||||||
|
|
||||||
|
if loop_frames > 0:
|
||||||
|
# Explicit loop=true of a bounded [skip, skip+take] sub-window: read ONLY
|
||||||
|
# that window here (a filtergraph `loop` filter repeats it — see the overlay
|
||||||
|
# layers). No -stream_loop; the filter does the repeating.
|
||||||
|
_take_secs = loop_frames / plan.config.fps
|
||||||
|
if skip > 0:
|
||||||
|
cmd.extend(["-ss", f"{skip:.3f}"])
|
||||||
|
probesize = "1000000" if has_audio else "1000"
|
||||||
|
cmd.extend(["-analyzeduration", "0", "-probesize", probesize])
|
||||||
|
cmd.extend(["-t", f"{_take_secs:.3f}"])
|
||||||
|
cmd.extend(["-i", str(video_path)])
|
||||||
|
video_inputs[i] = input_idx
|
||||||
|
input_idx += 1
|
||||||
|
continue
|
||||||
|
|
||||||
if needs_loop:
|
if needs_loop:
|
||||||
cmd.extend(["-stream_loop", "-1"])
|
cmd.extend(["-stream_loop", "-1"])
|
||||||
if skip > 0:
|
if skip > 0:
|
||||||
@@ -1164,20 +1229,18 @@ def build_filter_complex(
|
|||||||
event.cutout, width, height
|
event.cutout, width, height
|
||||||
)
|
)
|
||||||
|
|
||||||
duration = event.end_time - event.start_time
|
_skip, duration, _loop_frames = _video_playback(event, plan.config.fps)
|
||||||
if event.video_source.take is not None:
|
|
||||||
duration = min(duration, event.video_source.take)
|
|
||||||
effective_end = event.start_time + duration
|
effective_end = event.start_time + duration
|
||||||
|
_loop_pre, _pts = _trig_video_pts(event, plan.config.fps, _loop_frames)
|
||||||
|
|
||||||
zoom = event.video_source.zoom
|
zoom = event.video_source.zoom
|
||||||
zoomed_width = int(cut_width * zoom)
|
zoomed_width = int(cut_width * zoom)
|
||||||
zoomed_height = int(cut_height * zoom)
|
zoomed_height = int(cut_height * zoom)
|
||||||
|
|
||||||
video_label = f"tvb{i}"
|
video_label = f"tvb{i}"
|
||||||
start_pts = event.start_time
|
|
||||||
filters.append(
|
filters.append(
|
||||||
f"[{video_idx}:v]format=yuva444p10le,"
|
f"[{video_idx}:v]{_loop_pre}format=yuva444p10le,"
|
||||||
f"setpts=PTS-STARTPTS+{start_pts:.3f}/TB,"
|
f"{_pts},"
|
||||||
f"{_fit_filter(cut_width, cut_height, zoom, event.object_fit, event.object_position)},"
|
f"{_fit_filter(cut_width, cut_height, zoom, event.object_fit, event.object_position)},"
|
||||||
f"format=rgba[{video_label}]"
|
f"format=rgba[{video_label}]"
|
||||||
)
|
)
|
||||||
@@ -1264,20 +1327,18 @@ def build_filter_complex(
|
|||||||
event.cutout, width, height
|
event.cutout, width, height
|
||||||
)
|
)
|
||||||
|
|
||||||
duration = event.end_time - event.start_time
|
_skip, duration, _loop_frames = _video_playback(event, plan.config.fps)
|
||||||
if event.video_source.take is not None:
|
|
||||||
duration = min(duration, event.video_source.take)
|
|
||||||
effective_end = event.start_time + duration
|
effective_end = event.start_time + duration
|
||||||
|
_loop_pre, _pts = _trig_video_pts(event, plan.config.fps, _loop_frames)
|
||||||
|
|
||||||
zoom = event.video_source.zoom
|
zoom = event.video_source.zoom
|
||||||
zoomed_width = int(cut_width * zoom)
|
zoomed_width = int(cut_width * zoom)
|
||||||
zoomed_height = int(cut_height * zoom)
|
zoomed_height = int(cut_height * zoom)
|
||||||
|
|
||||||
video_label = f"tvm{i}"
|
video_label = f"tvm{i}"
|
||||||
start_pts = event.start_time
|
|
||||||
filters.append(
|
filters.append(
|
||||||
f"[{video_idx}:v]format=yuva444p10le,"
|
f"[{video_idx}:v]{_loop_pre}format=yuva444p10le,"
|
||||||
f"setpts=PTS-STARTPTS+{start_pts:.3f}/TB,"
|
f"{_pts},"
|
||||||
f"{_fit_filter(cut_width, cut_height, zoom, event.object_fit, event.object_position)},"
|
f"{_fit_filter(cut_width, cut_height, zoom, event.object_fit, event.object_position)},"
|
||||||
f"format=rgba[{video_label}]"
|
f"format=rgba[{video_label}]"
|
||||||
)
|
)
|
||||||
@@ -1320,20 +1381,18 @@ def build_filter_complex(
|
|||||||
event.cutout, width, height
|
event.cutout, width, height
|
||||||
)
|
)
|
||||||
|
|
||||||
duration = event.end_time - event.start_time
|
_skip, duration, _loop_frames = _video_playback(event, plan.config.fps)
|
||||||
if event.video_source.take is not None:
|
|
||||||
duration = min(duration, event.video_source.take)
|
|
||||||
effective_end = event.start_time + duration
|
effective_end = event.start_time + duration
|
||||||
|
_loop_pre, _pts = _trig_video_pts(event, plan.config.fps, _loop_frames)
|
||||||
|
|
||||||
zoom = event.video_source.zoom
|
zoom = event.video_source.zoom
|
||||||
zoomed_width = int(cut_width * zoom)
|
zoomed_width = int(cut_width * zoom)
|
||||||
zoomed_height = int(cut_height * zoom)
|
zoomed_height = int(cut_height * zoom)
|
||||||
|
|
||||||
video_label = f"tv{i}"
|
video_label = f"tv{i}"
|
||||||
start_pts = event.start_time
|
|
||||||
filters.append(
|
filters.append(
|
||||||
f"[{video_idx}:v]format=rgba,"
|
f"[{video_idx}:v]{_loop_pre}format=rgba,"
|
||||||
f"setpts=PTS-STARTPTS+{start_pts:.3f}/TB,"
|
f"{_pts},"
|
||||||
f"{_fit_filter(cut_width, cut_height, zoom, event.object_fit, event.object_position)}"
|
f"{_fit_filter(cut_width, cut_height, zoom, event.object_fit, event.object_position)}"
|
||||||
f"[{video_label}]"
|
f"[{video_label}]"
|
||||||
)
|
)
|
||||||
|
|||||||
+9
-1
@@ -32,7 +32,7 @@ from .transformer import MarkerTiming, resolve_video_presentation
|
|||||||
|
|
||||||
# Per-occurrence presentation fields ALWAYS materialized onto video events (atomic
|
# Per-occurrence presentation fields ALWAYS materialized onto video events (atomic
|
||||||
# events.json, GUI-ready). Round-tripped as overrides so a stored value drives render.
|
# events.json, GUI-ready). Round-tripped as overrides so a stored value drives render.
|
||||||
_PRESENTATION_KEYS = ("cutout", "layer", "end_on", "take", "object-fit", "object-position")
|
_PRESENTATION_KEYS = ("cutout", "layer", "end_on", "take", "skip", "loop", "object-fit", "object-position")
|
||||||
# `volume` is materialized SPARSELY — only when actually overridden (inline/GUI/manual),
|
# `volume` is materialized SPARSELY — only when actually overridden (inline/GUI/manual),
|
||||||
# so the videos.json value keeps flowing as the default and only a real override pins it.
|
# so the videos.json value keeps flowing as the default and only a real override pins it.
|
||||||
# Round-tripping still carries it whenever present in the event dict.
|
# Round-tripping still carries it whenever present in the event dict.
|
||||||
@@ -190,6 +190,14 @@ def derive_events(
|
|||||||
e["end_on"] = pres["end_on"]
|
e["end_on"] = pres["end_on"]
|
||||||
if pres["take"] is not None:
|
if pres["take"] is not None:
|
||||||
e["take"] = pres["take"]
|
e["take"] = pres["take"]
|
||||||
|
# skip/loop are sparse: materialized only when explicitly overridden
|
||||||
|
# inline (like volume), so a plain video keeps a clean events.json and
|
||||||
|
# the videos.json default flows via render's fallback. Keyed on override
|
||||||
|
# PRESENCE, not truthiness, so an explicit skip=0 / loop=false survives.
|
||||||
|
if t.overrides and "skip" in t.overrides:
|
||||||
|
e["skip"] = round(pres["skip"], 3)
|
||||||
|
if t.overrides and "loop" in t.overrides:
|
||||||
|
e["loop"] = bool(pres["loop"])
|
||||||
# volume is sparse: written only when actually overridden, so the
|
# volume is sparse: written only when actually overridden, so the
|
||||||
# videos.json default keeps flowing until someone pins it here.
|
# videos.json default keeps flowing until someone pins it here.
|
||||||
if t.overrides and "volume" in t.overrides:
|
if t.overrides and "volume" in t.overrides:
|
||||||
|
|||||||
+11
-1
@@ -95,6 +95,9 @@ def resolve_video_presentation(
|
|||||||
layer = overrides.get("layer") or impl_layer or video_source.layer
|
layer = overrides.get("layer") or impl_layer or video_source.layer
|
||||||
end_on = overrides.get("end_on") or video_source.end_on or default_end_on
|
end_on = overrides.get("end_on") or video_source.end_on or default_end_on
|
||||||
take = overrides["take"] if "take" in overrides else video_source.take
|
take = overrides["take"] if "take" in overrides else video_source.take
|
||||||
|
# Per-occurrence playback controls: inline override wins, else videos.json default.
|
||||||
|
skip = overrides["skip"] if "skip" in overrides else (video_source.skip or 0.0)
|
||||||
|
loop = overrides["loop"] if "loop" in overrides else bool(video_source.loop)
|
||||||
pause_narration = overrides.get(
|
pause_narration = overrides.get(
|
||||||
"pause_narration", video_source.pause_narration or 0.0
|
"pause_narration", video_source.pause_narration or 0.0
|
||||||
)
|
)
|
||||||
@@ -113,6 +116,8 @@ def resolve_video_presentation(
|
|||||||
"layer": layer,
|
"layer": layer,
|
||||||
"end_on": end_on,
|
"end_on": end_on,
|
||||||
"take": take,
|
"take": take,
|
||||||
|
"skip": float(skip or 0.0),
|
||||||
|
"loop": bool(loop),
|
||||||
"pause_narration": float(pause_narration or 0.0),
|
"pause_narration": float(pause_narration or 0.0),
|
||||||
"volume": float(volume if volume is not None else 1.0),
|
"volume": float(volume if volume is not None else 1.0),
|
||||||
"object_fit": object_fit,
|
"object_fit": object_fit,
|
||||||
@@ -1426,6 +1431,8 @@ def _extract_video_events(
|
|||||||
layer = pres["layer"]
|
layer = pres["layer"]
|
||||||
end_on = pres["end_on"]
|
end_on = pres["end_on"]
|
||||||
take = pres["take"]
|
take = pres["take"]
|
||||||
|
skip = pres["skip"]
|
||||||
|
loop = pres["loop"]
|
||||||
pause_narration = pres["pause_narration"]
|
pause_narration = pres["pause_narration"]
|
||||||
volume = pres["volume"]
|
volume = pres["volume"]
|
||||||
object_fit = pres["object_fit"]
|
object_fit = pres["object_fit"]
|
||||||
@@ -1504,7 +1511,7 @@ def _extract_video_events(
|
|||||||
skip_override = None
|
skip_override = None
|
||||||
if start_time < range_start:
|
if start_time < range_start:
|
||||||
into = range_start - start_time # elapsed since the clip started
|
into = range_start - start_time # elapsed since the clip started
|
||||||
base = video_source.skip or 0.0
|
base = skip # resolved per-occurrence skip
|
||||||
playable = (video_source.duration - base) if video_source.duration else None
|
playable = (video_source.duration - base) if video_source.duration else None
|
||||||
if playable and playable > 0 and into >= playable:
|
if playable and playable > 0 and into >= playable:
|
||||||
# the clip has looped by the window start → resume at the loop phase
|
# the clip has looped by the window start → resume at the loop phase
|
||||||
@@ -1525,6 +1532,9 @@ def _extract_video_events(
|
|||||||
cutout_name=cutout_name,
|
cutout_name=cutout_name,
|
||||||
layer=layer,
|
layer=layer,
|
||||||
end_on=end_on or "",
|
end_on=end_on or "",
|
||||||
|
skip=skip,
|
||||||
|
take=take,
|
||||||
|
loop=loop,
|
||||||
volume=volume,
|
volume=volume,
|
||||||
object_fit=object_fit,
|
object_fit=object_fit,
|
||||||
object_position=object_position,
|
object_position=object_position,
|
||||||
|
|||||||
Reference in New Issue
Block a user