mxivideo/python_core/models/ffmpeg_tasks/models.py

303 lines
10 KiB
Python

import json
from typing import Union, Any, Optional, List, Dict
from loguru import logger
from pydantic import BaseModel, Field, computed_field, field_validator, model_validator, ConfigDict, HttpUrl
from pydantic.json_schema import JsonSchemaValue
from datetime import timedelta, datetime
class TimeDelta(timedelta):
@classmethod
def from_timedelta(cls, delta: timedelta) -> "TimeDelta":
return cls(days=delta.days, seconds=delta.seconds, microseconds=delta.microseconds)
@classmethod
def from_format_string(cls, format_string: str) -> "TimeDelta":
formated_time = datetime.strptime(format_string, "%H:%M:%S.%f")
return cls(hours=formated_time.hour, minutes=formated_time.minute, seconds=formated_time.second,
microseconds=formated_time.microsecond)
def toFormatStr(self) -> str:
return (datetime(year=2000, month=1, day=1) + self).strftime("%H:%M:%S.%f")[:-3]
class FFMpegSliceSegment(BaseModel):
start: TimeDelta = Field(
description="视频切割的开始时间点秒数, 可为浮点小数(精确到小数点后3位,毫秒级)或者为标准格式的时间戳")
end: TimeDelta = Field(
description="视频切割的结束时间点秒数, 可为浮点小数(精确到小数点后3位,毫秒级)或者标准格式的时间戳")
@computed_field
@property
def duration(self) -> TimeDelta:
return self.end - self.start
@field_validator('start', mode='before')
@classmethod
def parse_start(cls, v: Union[float, str, TimeDelta]):
if isinstance(v, float):
if v < 0.0:
raise ValueError("开始时间点不能小于0")
return TimeDelta(seconds=v)
elif isinstance(v, int):
if v < 0:
raise ValueError("开始时间点不能小于0")
return TimeDelta(seconds=v)
elif isinstance(v, str):
timedelta = TimeDelta.from_format_string(v)
if timedelta.total_seconds() < 0.0:
raise ValueError("开始时间点不能小于0")
return timedelta
elif isinstance(v, TimeDelta):
if v.total_seconds() < 0.0:
raise ValueError("开始时间点不能小于0")
return v
else:
raise TypeError(v)
@field_validator('end', mode='before')
@classmethod
def parse_end(cls, v: Union[float, TimeDelta]):
if isinstance(v, float):
if v < 0.0:
raise ValueError("结束时间点不能小于0")
return TimeDelta(seconds=v)
elif isinstance(v, int):
if v < 0:
raise ValueError("结束时间点不能小于0")
return TimeDelta(seconds=v)
elif isinstance(v, str):
timedelta = TimeDelta.from_format_string(v)
if timedelta.total_seconds() < 0.0:
raise ValueError("结束时间点不能小于0")
return timedelta
elif isinstance(v, TimeDelta):
if v.total_seconds() < 0.0:
raise ValueError("结束时间点不能小于0")
return v
else:
raise TypeError(v)
@model_validator(mode='after')
def validate_end_after_start(self) -> 'FFMpegSliceSegment':
if self.end <= self.start:
raise ValueError("end time must be greater than start time")
return self
@classmethod
def __get_pydantic_json_schema__(cls, core_schema: Any, handler: Any) -> JsonSchemaValue:
# Override the schema to represent it as a string
return {
"type": "object",
"properties": {
"start": {
"type": "number",
"examples": [5, 10.5, '00:00:10.500'],
},
"end": {
"type": "number",
"examples": [8, 12.5, '00:00:12.500'],
}
},
"required": [
"start",
"end"
]
}
model_config = {
"arbitrary_types_allowed": True
}
class FFMPEGSliceOptions(BaseModel):
limit_size: Optional[int] = Field(default=None, description="不超过指定文件(字节)大小, 默认为空不限制输出大小")
bit_rate: Optional[int] = Field(default=None, description="指定输出视频的比特率, 不能与limit_size同时设置")
crf: int = Field(default=16, description="输出视频的质量")
fps: int = Field(default=30, description="输出视频的FPS")
width: int = Field(default=None, description="输出视频的宽(像素)")
height: int = Field(default=None, description="输出视频的高(像素)")
@computed_field(description="解析为字符表达式的文件大小")
@property
def pretty_limit_size(self) -> Optional[str]:
if self.limit_size is not None:
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if self.limit_size < 1024.0:
return f"{self.limit_size:.2f}{unit}"
self.limit_size /= 1024.0
return f"{self.limit_size:.2f}PB"
else:
return None
@computed_field(description="解析为字符表达式的比特率")
@property
def pretty_bit_rate(self) -> Optional[str]:
if self.bit_rate is not None:
return f"{self.bit_rate}k"
class MediaStream(BaseModel):
duration: float = Field(0, description="时长")
codec_name: str
tags: Optional[Any] = Field(None)
model_config = ConfigDict(extra='allow')
class HLSMediaVideoStream(BaseModel):
stream_type: str = "video"
codec_name: str
codec_type: str
width: int
height: int
avg_frame_rate: str
tags: Optional[Any] = Field(None)
duration: Optional[float] = Field(None)
@computed_field
@property
def video_frame_rate(self) -> float:
numerator, denominator = map(int, self.avg_frame_rate.split('/'))
if denominator != 0:
return numerator / denominator
return 0
class HLSMediaAudioStream(BaseModel):
stream_type: str = "audio"
sample_rate: str
channels: int
channel_layout: str
start_time: str
tags: Optional[Any] = Field(None)
class AudioStream(MediaStream):
stream_type: str = Field("audio")
codec_type: str
sample_rate: str
channels: int
tags: Optional[Any] = Field(None)
model_config = ConfigDict(extra='allow')
class VideoStream(MediaStream):
stream_type: str = "video"
width: int
height: int
bit_rate: int
avg_frame_rate: str
@computed_field
@property
def video_bitrate(self) -> str:
return str(int(self.bit_rate / 1000)) + 'k'
@computed_field
@property
def video_frame_rate(self) -> float:
numerator, denominator = map(int, self.avg_frame_rate.split('/'))
if denominator != 0:
return numerator / denominator
return 0
model_config = ConfigDict(extra='allow')
class ImageStream(MediaStream):
stream_type: str = "image"
width: int
height: int
model_config = ConfigDict(extra='allow')
class SubtitleStreamTags(BaseModel):
language: str = Field(description="内嵌字幕的语言")
model_config = ConfigDict(extra='allow')
class SubtitleStream(MediaStream):
stream_type: str = "subtitle"
tags: SubtitleStreamTags
model_config = ConfigDict(extra='allow')
class VideoFormat(BaseModel):
filename: str
format_name: str
start_time: float = Field(0, description="起始时间")
size: int
bit_rate: Optional[int] = Field(None, description="文件比特率")
duration: float = Field(3600 * 12, description="文件时长")
class VideoMetadata(BaseModel):
streams: List[
Union[ImageStream, AudioStream, VideoStream, HLSMediaAudioStream, HLSMediaVideoStream, SubtitleStream]] = Field(
description="媒体包含的数据轨道")
format: Optional[VideoFormat] = Field(None)
@field_validator('streams', mode='before')
def parse_streams(cls, value):
streams = []
if isinstance(value, List):
for stream in value:
if isinstance(stream, Dict):
logger.info(f"Parsing stream : {json.dumps(stream, ensure_ascii=False)}")
if stream.get("codec_type") == 'audio':
if stream.get("duration") is None:
logger.info("Parsing audio stream")
hls_audio = HLSMediaAudioStream.model_validate(stream)
streams.append(hls_audio)
else:
logger.info("Parsing hls audio stream")
audio = AudioStream.model_validate(stream)
streams.append(audio)
elif stream.get("codec_type") == 'video':
if stream.get("codec_name") in ("gif", "png", "mjpg", "mjpeg", "webp"):
logger.info("Parsing image stream")
image = ImageStream.model_validate(stream)
streams.append(image)
else:
if stream.get("duration") is None:
logger.info("Parsing hls video stream")
hls_video = HLSMediaVideoStream.model_validate(stream)
streams.append(hls_video)
else:
logger.info("Parsing video stream")
video = VideoStream.model_validate(stream)
streams.append(video)
elif stream.get("codec_type") == 'subtitle':
logger.info("Parsing subtitle stream")
subtitle_stream = SubtitleStream.model_validate(stream)
streams.append(subtitle_stream)
return streams
else:
raise TypeError
model_config = ConfigDict(extra='allow')
class SentryTransactionHeader(BaseModel):
x_trace_id: Optional[str] = Field(description="Sentry Transaction ID", default=None)
x_baggage: Optional[str] = Field(description="Sentry Transaction baggage", default=None)
class SentryTransactionInfo(BaseModel):
x_trace_id: str = Field(description="Sentry Transaction ID")
x_baggage: str = Field(description="Sentry Transaction baggage")
class FFMPEGResult(BaseModel):
urn: str = Field(description="FFMPEG任务结果urn")
content_length: int = Field(description="媒体资源文件字节大小(Byte)")
metadata: VideoMetadata = Field(description="媒体元数据")