132 lines
4.2 KiB
Python
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
|