| |
| |
| |
| |
|
|
| import ast |
| import math |
| import os |
| import shutil |
| import subprocess |
| from dataclasses import dataclass |
| from typing import Optional |
|
|
| import av |
| from app_conf import FFMPEG_NUM_THREADS |
| from dataclasses_json import dataclass_json |
|
|
| TRANSCODE_VERSION = 1 |
|
|
|
|
| @dataclass_json |
| @dataclass |
| class VideoMetadata: |
| duration_sec: Optional[float] |
| video_duration_sec: Optional[float] |
| container_duration_sec: Optional[float] |
| fps: Optional[float] |
| width: Optional[int] |
| height: Optional[int] |
| num_video_frames: int |
| num_video_streams: int |
| video_start_time: float |
|
|
|
|
| def transcode( |
| in_path: str, |
| out_path: str, |
| in_metadata: Optional[VideoMetadata], |
| seek_t: float, |
| duration_time_sec: float, |
| ): |
| codec = os.environ.get("VIDEO_ENCODE_CODEC", "libx264") |
| crf = int(os.environ.get("VIDEO_ENCODE_CRF", "23")) |
| fps = int(os.environ.get("VIDEO_ENCODE_FPS", "24")) |
| max_w = int(os.environ.get("VIDEO_ENCODE_MAX_WIDTH", "1280")) |
| max_h = int(os.environ.get("VIDEO_ENCODE_MAX_HEIGHT", "720")) |
| verbose = ast.literal_eval(os.environ.get("VIDEO_ENCODE_VERBOSE", "False")) |
|
|
| normalize_video( |
| in_path=in_path, |
| out_path=out_path, |
| max_w=max_w, |
| max_h=max_h, |
| seek_t=seek_t, |
| max_time=duration_time_sec, |
| in_metadata=in_metadata, |
| codec=codec, |
| crf=crf, |
| fps=fps, |
| verbose=verbose, |
| ) |
|
|
|
|
| def get_video_metadata(path: str) -> VideoMetadata: |
| with av.open(path) as cont: |
| num_video_streams = len(cont.streams.video) |
| width, height, fps = None, None, None |
| video_duration_sec = 0 |
| container_duration_sec = float((cont.duration or 0) / av.time_base) |
| video_start_time = 0.0 |
| rotation_deg = 0 |
| num_video_frames = 0 |
| if num_video_streams > 0: |
| video_stream = cont.streams.video[0] |
| assert video_stream.time_base is not None |
|
|
| |
| rotation_deg = video_stream.side_data.get("DISPLAYMATRIX", 0) |
| num_video_frames = video_stream.frames |
| video_start_time = float(video_stream.start_time * video_stream.time_base) |
| width, height = video_stream.width, video_stream.height |
| fps = float(video_stream.guessed_rate) |
| fps_avg = video_stream.average_rate |
| if video_stream.duration is not None: |
| video_duration_sec = float( |
| video_stream.duration * video_stream.time_base |
| ) |
| if fps is None: |
| fps = float(fps_avg) |
|
|
| if not math.isnan(rotation_deg) and int(rotation_deg) in ( |
| 90, |
| -90, |
| 270, |
| -270, |
| ): |
| width, height = height, width |
|
|
| duration_sec = max(container_duration_sec, video_duration_sec) |
|
|
| return VideoMetadata( |
| duration_sec=duration_sec, |
| container_duration_sec=container_duration_sec, |
| video_duration_sec=video_duration_sec, |
| video_start_time=video_start_time, |
| fps=fps, |
| width=width, |
| height=height, |
| num_video_streams=num_video_streams, |
| num_video_frames=num_video_frames, |
| ) |
|
|
|
|
| def normalize_video( |
| in_path: str, |
| out_path: str, |
| max_w: int, |
| max_h: int, |
| seek_t: float, |
| max_time: float, |
| in_metadata: Optional[VideoMetadata], |
| codec: str = "libx264", |
| crf: int = 23, |
| fps: int = 24, |
| verbose: bool = False, |
| ): |
| if in_metadata is None: |
| in_metadata = get_video_metadata(in_path) |
|
|
| assert in_metadata.num_video_streams > 0, "no video stream present" |
|
|
| w, h = in_metadata.width, in_metadata.height |
| assert w is not None, "width not available" |
| assert h is not None, "height not available" |
|
|
| |
| r = w / h |
| if r < 1: |
| h = min(720, h) |
| w = h * r |
| else: |
| w = min(1280, w) |
| h = w / r |
|
|
| |
| w = int(w) |
| h = int(h) |
| if w % 2 != 0: |
| w += 1 |
| if h % 2 != 0: |
| h += 1 |
|
|
| ffmpeg = shutil.which("ffmpeg") |
| cmd = [ |
| ffmpeg, |
| "-threads", |
| f"{FFMPEG_NUM_THREADS}", |
| "-ss", |
| f"{seek_t:.2f}", |
| "-t", |
| f"{max_time:.2f}", |
| "-i", |
| in_path, |
| "-threads", |
| f"{FFMPEG_NUM_THREADS}", |
| "-vf", |
| f"fps={fps},scale={w}:{h},setsar=1:1", |
| "-c:v", |
| codec, |
| "-crf", |
| f"{crf}", |
| "-pix_fmt", |
| "yuv420p", |
| "-threads", |
| f"{FFMPEG_NUM_THREADS}", |
| out_path, |
| "-y", |
| ] |
| if verbose: |
| print(" ".join(cmd)) |
|
|
| subprocess.call( |
| cmd, |
| stdout=None if verbose else subprocess.DEVNULL, |
| stderr=None if verbose else subprocess.DEVNULL, |
| ) |
|
|