Add video duration handling for chat rendering and merging

This commit is contained in:
MaddoScientisto 2026-02-10 08:04:08 +01:00
commit cdef8cf9bb
4 changed files with 126 additions and 19 deletions

View file

@ -120,7 +120,8 @@ class ContentDownloader:
print(f'{Fore.RED}✗ Chat download failed: {str(e)}{Style.RESET_ALL}')
return False
def render_chat(self, json_path: str, video_path: str, output_args: str) -> bool:
def render_chat(self, json_path: str, video_path: str, output_args: str,
video_duration: Optional[float] = None) -> bool:
"""
Render chat JSON as a video.
@ -128,6 +129,7 @@ class ContentDownloader:
json_path: Path to chat JSON file
video_path: Path to save rendered chat video
output_args: FFmpeg output arguments for encoding
video_duration: Optional video duration in seconds to trim chat to match
Returns:
bool: True if succeeded, False otherwise
@ -164,6 +166,13 @@ class ContentDownloader:
'--collision', 'Rename'
]
# Trim chat to match video duration if provided
if video_duration is not None and video_duration > 0:
# Format duration as seconds with 1 decimal place
duration_str = f'{video_duration:.1f}s'
chat_settings.extend(['-e', duration_str])
print(f'{Fore.CYAN} Trimming chat to match video duration: {duration_str}{Style.RESET_ALL}')
# Add output args using = syntax to avoid parsing issues
if output_args:
chat_settings.append(f'--output-args={output_args}')
@ -188,7 +197,8 @@ class ContentDownloader:
return False
def download_and_render_chat(self, vod_info: Dict[str, Any], json_path: str,
video_path: str, output_args: str) -> bool:
video_path: str, output_args: str,
video_duration: Optional[float] = None) -> bool:
"""
Download chat logs and render them as video.
@ -197,6 +207,7 @@ class ContentDownloader:
json_path: Path to save chat JSON
video_path: Path to save rendered chat video
output_args: FFmpeg output arguments for encoding
video_duration: Optional video duration in seconds to trim chat to match
Returns:
bool: True if succeeded, False otherwise
@ -213,8 +224,8 @@ class ContentDownloader:
if not self.download_chat_json(vod_id, json_path):
return False
# Render chat video
return self.render_chat(json_path, video_path, output_args)
# Render chat video with optional duration trimming
return self.render_chat(json_path, video_path, output_args, video_duration=video_duration)
def start_live_chat_download(self, vod_id: str, json_path: str) -> Optional[subprocess.Popen]:
"""

View file

@ -191,6 +191,7 @@ class StreamProcessor:
return False
print(f'{Fore.YELLOW}Merging video and chat ({layout})...{Style.RESET_ALL}')
print(f'{Fore.CYAN} This may take several minutes depending on video length{Style.RESET_ALL}')
try:
if layout == 'overlay':
@ -222,6 +223,7 @@ class StreamProcessor:
if self.hwaccel_type and self.hwaccel_type != 'none':
encoder = get_hwaccel_encoder(self.hwaccel_type)
cmd.extend(['-c:v', encoder])
print(f'{Fore.CYAN} Using hardware encoder: {encoder}{Style.RESET_ALL}')
if 'nvenc' in encoder:
cmd.extend(['-preset', 'p4', '-cq', '18'])
@ -232,6 +234,7 @@ class StreamProcessor:
else:
cmd.extend(['-preset', 'medium', '-crf', '18'])
else:
print(f'{Fore.CYAN} Using software encoder: libx264{Style.RESET_ALL}')
cmd.extend(['-c:v', 'libx264', '-preset', 'medium', '-crf', '18'])
# Audio codec
@ -244,19 +247,20 @@ class StreamProcessor:
cmd.append(output_path)
# Run FFmpeg
if self.ffmpeg_progress:
result = subprocess.call(cmd)
else:
result = subprocess.call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
# Run FFmpeg with visible output to show progress
print(f'{Fore.CYAN} Processing...{Style.RESET_ALL}')
result = subprocess.call(cmd)
if result == 0:
print(f'{Fore.GREEN}✓ Video and chat merged successfully{Style.RESET_ALL}')
return True
else:
print(f'{Fore.RED}✗ Merge failed with exit code: {result}{Style.RESET_ALL}')
print(f'{Fore.YELLOW} Check FFmpeg output above for details{Style.RESET_ALL}')
return False
except Exception as e:
print(f'{Fore.RED}✗ Merge failed: {str(e)}{Style.RESET_ALL}')
import traceback
traceback.print_exc()
return False

View file

@ -98,6 +98,47 @@ def get_unique_filename(filepath: str) -> str:
counter += 1
def get_video_duration(video_path: str, ffmpeg_path: str) -> Optional[float]:
"""
Get the duration of a video file in seconds using FFmpeg.
Args:
video_path: Path to the video file
ffmpeg_path: Path to FFmpeg executable
Returns:
float: Duration in seconds, or None if failed
"""
if not os.path.exists(video_path):
return None
try:
# Use ffprobe (comes with ffmpeg) to get duration
ffprobe_path = ffmpeg_path.replace('ffmpeg', 'ffprobe')
cmd = [
ffprobe_path,
'-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
video_path
]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0 and result.stdout.strip():
return float(result.stdout.strip())
except Exception:
pass
return None
def verify_streamlink() -> bool:
"""
Verify that streamlink is available.