Files

132 lines
4.2 KiB
Python

from __future__ import annotations
from fractions import Fraction
from pathlib import Path
from tools.avisynth_render import AvisynthClipInfo
from tools.video_encode_output import AudioEncodeOptions
from tools.video_probe import VideoProbe
from tools.video_timeline import OutputTimeline
def normal_deleted_frame_count(timeline: OutputTimeline) -> int:
return missing_source_frames(timeline.consumed_source_frames)
def missing_source_frames_between_kept(timeline: OutputTimeline) -> int:
return missing_source_frames([frame.source_frame for frame in timeline.frames])
def missing_source_frames(source_frames: list[int]) -> int:
missing = 0
for previous, current in zip(source_frames, source_frames[1:]):
missing += max(0, current - previous - 1)
return missing
def should_preserve_video_timestamps(timeline: OutputTimeline) -> bool:
return timeline.timing_kind == "vfr" or normal_deleted_frame_count(timeline) > 0
def should_use_audio_segments(
timeline: OutputTimeline,
audio_options: AudioEncodeOptions,
) -> bool:
if audio_options.mode != "aac":
return False
if len(timeline.audio_segments) > 1:
return True
if not timeline.audio_segments:
return False
start, duration = timeline.audio_segments[0]
return (
abs(start - timeline.frames[0].start) > 0.001
or abs(duration - timeline.duration_seconds) > 0.001
)
def y4m_command_for_timeline(
avs_runner: Path,
timeline: OutputTimeline,
*,
force_timeline_rate: bool = False,
) -> list[str]:
if not timeline.dropped_frame_count and not force_timeline_rate:
return [str(avs_runner), "--y4m"]
fps = cfr_fps_for_timeline(timeline)
return [str(avs_runner), "--y4m-skip-drop", str(fps.numerator), str(fps.denominator)]
def cfr_fps_for_timeline(timeline: OutputTimeline) -> Fraction:
if timeline.duration_seconds <= 0:
raise RuntimeError("Cannot encode a zero-duration timeline")
return Fraction(len(timeline.frames) / timeline.duration_seconds).limit_denominator(1001000)
def timeline_frame_starts(timeline: OutputTimeline) -> list[float]:
starts: list[float] = []
current = 0.0
for frame in timeline.frames:
starts.append(current)
current += frame.duration
return starts
def avisynth_duration_differs(
clip_info: AvisynthClipInfo,
timeline: OutputTimeline,
) -> bool:
duration = clip_info.duration_seconds
if duration is None:
return False
return abs(duration - timeline.duration_seconds) > 0.05
def encoded_duration_seconds(
clip_info: AvisynthClipInfo | None,
timeline: OutputTimeline,
) -> float:
if clip_info is None or clip_info.duration_seconds is None:
return timeline.duration_seconds
return clip_info.duration_seconds
def has_avisynth_speed_changes(
timelines: dict[Path, OutputTimeline],
clip_infos: dict[Path, AvisynthClipInfo],
) -> bool:
for path, timeline in timelines.items():
clip_info = clip_infos.get(path)
if clip_info is not None and avisynth_duration_differs(clip_info, timeline):
return True
return False
def audio_tempo_factor(timeline: OutputTimeline, output_duration: float) -> float:
if output_duration <= 0:
raise RuntimeError("Encoded output duration must be positive")
return timeline.duration_seconds / output_duration
def audio_options_for_probe(
requested: AudioEncodeOptions,
probe: VideoProbe,
timeline: OutputTimeline,
output_duration: float,
*,
normal_deleted_frames: int = 0,
) -> AudioEncodeOptions:
if requested.mode == "none":
return requested
if probe.audio_stream_count <= 0:
print(" audio: source has no audio stream; output will be video-only")
return AudioEncodeOptions(mode="none")
if requested.mode == "copy" and normal_deleted_frames:
print(" audio: copy requested, but frame deletion needs audio cuts; output will be video-only")
return AudioEncodeOptions(mode="none")
if requested.mode == "copy" and abs(timeline.duration_seconds - output_duration) > 0.05:
print(" audio: copy requested, but speed-changed output needs audio re-encoding; output will be video-only")
return AudioEncodeOptions(mode="none")
return requested