This commit is contained in:
2024-12-24 20:49:41 +03:00
parent f8febafacb
commit 2b3521544b
35 changed files with 2043 additions and 767 deletions
+53 -43
View File
@@ -4,12 +4,10 @@ import dataclasses
import json
import logging
import subprocess
import typing
from datetime import timedelta
from math import ceil
from pathlib import Path
logger = logging.getLogger(__name__)
@@ -26,11 +24,28 @@ class FfprobeResult:
container: str
ar: float
sample_ar: float
tags: typing.Dict[str, str]
tags: dict[str, str]
@property
def resolution(self) -> str:
return f'{self.width}x{self.height}'
@property
def has_metadata(self):
return any(k in self.tags for k in ['title', 'comment'])
@property
def hd_mode(self) -> str:
if 700 < self.height < 800:
return '720p'
if 900 < self.height < 1200:
return '1080p'
if 2000 < self.height < 2200:
return '4K'
raise ValueError('unknown HD mode')
def ffprobe(video: Path) -> FfprobeResult:
logger.debug("executing ffprobe")
def ffprobe(video_path: Path) -> FfprobeResult:
proc = subprocess.run(
args=[
"ffprobe",
@@ -40,7 +55,7 @@ def ffprobe(video: Path) -> FfprobeResult:
"-show_format",
"-print_format",
"json",
str(video),
str(video_path),
],
text=True,
stdout=subprocess.PIPE,
@@ -48,8 +63,8 @@ def ffprobe(video: Path) -> FfprobeResult:
)
try:
proc.check_returncode()
except subprocess.CalledProcessError:
logger.error("failed to run ffprobe", exc_info=True)
except subprocess.CalledProcessError as e:
logger.error(f"failed to run ffprobe. stderr={e.stderr}", exc_info=True)
raise
output: dict = json.loads(proc.stdout)
video_stream: dict = [s for s in output.get("streams", []) if s.get("codec_type") == "video"][0]
@@ -71,9 +86,6 @@ def ffprobe(video: Path) -> FfprobeResult:
except:
sample_ar = 1
logger.debug(f"dimensions={width}x{height} duration={duration_time}")
# total_frames = video_stream.get("nb_frames")
return FfprobeResult(
duration_sec=duration,
duration_human=str(duration_time),
@@ -83,19 +95,18 @@ def ffprobe(video: Path) -> FfprobeResult:
width=width,
bitrate=bitrate,
height=height,
container=video.suffix.lstrip(".").lower(),
container=video_path.suffix.lstrip(".").lower(),
ar=width / height,
sample_ar=sample_ar,
tags=tags,
# total_frames=total_frames,
)
def strip_metadata(video_path: Path, save_path: Path) -> Path:
media_info = ffprobe(video_path)
# check if the first video stream has hevc codec
if media_info["streams"][0]["codec_name"] == "hevc":
logger.info(f"Video codec is hevc, adding hvc1 tag")
if media_info.codec == "hevc":
logging.info(f"Video codec is hevc, adding hvc1 tag")
extra_args = ["-tag:v", "hvc1"]
else:
extra_args = []
@@ -106,7 +117,7 @@ def strip_metadata(video_path: Path, save_path: Path) -> Path:
'-i',
str(video_path),
'-c:v', 'copy',
'-movflags', '+faststart',
# '-movflags', '+faststart',
'-map_metadata', '-1',
*extra_args,
'-c:a', 'copy',
@@ -114,32 +125,22 @@ def strip_metadata(video_path: Path, save_path: Path) -> Path:
str(save_path),
]
# fmt: on
logger.info(f"calling ffmpeg with {args=}")
logging.info(f"calling ffmpeg with {args=}")
subprocess.run(args, check=True)
return save_path
def make_thumbnail_tile(
video: Path,
image_path: Path = None,
columns: int = 3,
interval_seconds: int = 60,
tile_width: int = 540,
skip_first_sec: int = 10,
skip_if_exists: bool = False,
) -> Path:
if not image_path:
image_path = video.parent / f"{video.stem}.thumbnail.jpg"
if image_path.is_file() and skip_if_exists:
logger.debug("thumbnail already exists")
return image_path
) -> bytes:
info = ffprobe(video)
min_frames = columns * 3
sec_per_frame = min(interval_seconds, ceil(info.duration_sec / min_frames))
rows = ceil(info.duration_sec // sec_per_frame / columns)
tile = f"{columns}x{rows}"
@@ -150,25 +151,32 @@ def make_thumbnail_tile(
# timestamp_filter = rf"drawtext=r=1:timecode='00\:00\:00\:00':fontsize=16:fontcolor=white:x=10:y=10:box=1:boxcolor=black@0.5"
# timestamp_filter = fr"drawtext=text='%{{(pts\\+{skip_first_sec})\:hms}}':fontsize=16:fontcolor=white:x=10:y=10:box=1:boxcolor=black@0.5"
proc = subprocess.run(
args = [
# fmt: off
args=[
"ffmpeg",
"-v", "error",
"-skip_frame", "nokey",
"-ss", f"{skip_first_sec}",
"-i", str(video.absolute()),
# "-vf", f"fps=1/{sec_per_frame},scale={scale},{timestamp_filter},tile={tile}",
"-vf", f"fps=1/{sec_per_frame},scale={scale},tile={tile}",
"-frames", "1",
"-y", str(image_path.absolute()),
],
"ffmpeg",
"-v", "error",
"-skip_frame", "nokey",
"-ss", f"{skip_first_sec}",
"-i", str(video.absolute()),
# "-vf", f"fps=1/{sec_per_frame},scale={scale},{timestamp_filter},tile={tile}",
"-vf", f"fps=1/{sec_per_frame},scale={scale},tile={tile}",
"-frames", "1",
"-c:v",
"jpeg2000",
"-q:v", "90",
"-f", "image2pipe",
"-",
# fmt: on
stdout=subprocess.DEVNULL,
]
logger.debug(f'calling ffmpeg with args={args}')
proc = subprocess.run(
args=args,
stdout=subprocess.PIPE,
timeout=2000,
# preexec_fn=os.setpgrp,
)
proc.check_returncode()
return image_path
return bytes(proc.stdout)
if __name__ == "__main__":
@@ -177,9 +185,11 @@ if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
for it in sys.argv[1:]:
video = Path(it)
image_path = video.parent / f"{video.stem}.thumbnail.jpg"
if not video.exists():
continue
logger.info("creating thumbnail for %s", video)
make_thumbnail_tile(video)
image_jpeg = make_thumbnail_tile(video)
image_path.write_bytes(image_jpeg)