snapshot
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user