Cat Feeder  1.0.0
The Cat feeder project
Loading...
Searching...
No Matches
video_to_video.py
Go to the documentation of this file.
1r"""
2# +==== BEGIN CatFeeder =================+
3# LOGO:
4# ..............(..../\
5# ...............)..(.')
6# ..............(../..)
7# ...............\‍(__)|
8# Inspired by Joan Stark
9# source https://www.asciiart.eu/
10# animals/cats
11# /STOP
12# PROJECT: CatFeeder
13# FILE: image_to_image.py
14# CREATION DATE: 15-01-2026
15# LAST Modified: 1:37:6 17-01-2026
16# DESCRIPTION:
17# This is the backend server in charge of making the actual website work.
18# /STOP
19# COPYRIGHT: (c) Cat Feeder
20# PURPOSE: The file containing the code for converting bytes to a base of the user's choice.
21# // AR
22# +==== END CatFeeder =================+
23"""
24
25from io import BytesIO
26from typing import Optional
27import subprocess
28
29from display_tty import Disp, initialise_logger
30
31from .aliases import VIDEO_FORMAT_ALIASES
32from . import converters_constants as CONV_CONST
33
34from ..http_constants import DataTypes, MEDIA_TYPES
35
36from ...utils import CONST
37from ...core import FinalClass
38from ...fffamily import FFMPEGDownloader
39
40
41class VideoToVideo(metaclass=FinalClass):
42 """Class used to convert video from one format to another using ffmpeg.
43
44 Optimized to minimize I/O costs:
45 - Uses ffmpeg with pipes (stdin/stdout) for in-memory conversion
46 - No temporary files created - data flows through memory buffers
47 """
48
49 disp: Disp = initialise_logger(__qualname__, CONST.DEBUG)
50
51 _instance: Optional["VideoToVideo"] = None
52
53 _ffmpeg_ensured: bool = False
54
55 def __new__(cls) -> "VideoToVideo":
56 if cls._instance is None:
57 cls._instance = super(VideoToVideo, cls).__new__(cls)
58 return cls._instance
59
60 def __init__(self) -> None:
61 self.disp.log_debug("Initialising...")
62 self.disp.log_debug("Ensuring FFMPEG is available...")
63 self._ensure_ffmpeg()
64 self.disp.log_debug("FFMPEG is available.")
65 self.disp.log_debug("Initialised.")
66
67 def __call__(self, data: bytes, source_format: DataTypes) -> CONV_CONST.ConversionResult:
68 return self.video_to_video(data, source_format)
69
70 def _ensure_ffmpeg(self) -> None:
71 """Ensure that FFMPEG is downloaded and available."""
72 if self._ffmpeg_ensured:
73 return
74
75 self.disp.log_debug("Ensuring FFMPEG is available...")
76 FDI = FFMPEGDownloader(
77 cwd=str(CONV_CONST.FF_FAMILY_PATH),
78 success=CONV_CONST.SUCCESS,
79 error=CONV_CONST.ERROR,
80 debug=CONV_CONST.DEBUG
81 )
82 try:
83 FDI.main()
84 self._ffmpeg_ensured = True
85 except Exception as e:
86 self.disp.log_error(f"FFMPEG is not available: {e}")
87 raise RuntimeError(
88 "FFMPEG is required for video conversion but could not be ensured."
89 ) from e
90 self.disp.log_debug("FFMPEG is available.")
91
92 def get_video_extension(self, data_type: DataTypes) -> Optional[str]:
93 """
94 Get the file extension for a given video DataType.
95
96 Args:
97 data_type: The DataType to get extension for
98
99 Returns:
100 Extension string or None
101 """
102 return VIDEO_FORMAT_ALIASES.get(data_type)
103
105 self,
106 data: bytes,
107 source_format: DataTypes
108 ) -> tuple[Optional[DataTypes], Optional[str], Optional[str]]:
109 """
110 Validate conversion parameters and get destination format and extensions.
111
112 Args:
113 data: The video data to validate
114 source_format: The source video format
115
116 Returns:
117 Tuple of (destination_format, source_ext, dest_ext)
118 Returns (None, None, None) if validation fails
119 """
120 try:
121 destination_format = MEDIA_TYPES.get_conversion_target(
122 source_format)
123 except (AttributeError, NameError):
124 destination_format = None
125
126 if destination_format is None:
127 self.disp.log_debug(f"No conversion target for {source_format}")
128 return None, None, None
129
130 if source_format == destination_format:
131 self.disp.log_debug(
132 f"Source and destination formats are the same: {source_format}"
133 )
134 return destination_format, None, None
135
136 source_ext = self.get_video_extension(source_format)
137 dest_ext = self.get_video_extension(destination_format)
138
139 if not source_ext or not dest_ext:
140 self.disp.log_warning(
141 f"Unknown video extension for {source_format} -> {destination_format}"
142 )
143 return destination_format, None, None
144
145 return destination_format, source_ext, dest_ext
146
148 self,
149 data: bytes,
150 source_format: DataTypes,
151 destination_format: DataTypes,
152 source_ext: str,
153 dest_ext: str
154 ) -> Optional[bytes]:
155 """
156 Perform in-memory video conversion using ffmpeg with pipes.
157 MINIMAL I/O - data flows through stdin/stdout pipes, no temp files.
158
159 Args:
160 data: Source video data
161 source_format: Source video format
162 destination_format: Destination video format
163 source_ext: Source file extension
164 dest_ext: Destination file extension
165
166 Returns:
167 Converted video data as bytes, or None if conversion failed
168 """
169 self.disp.log_debug(
170 f"Converting video via pipes (minimal I/O): {source_format} -> {destination_format}"
171 )
172
173 try:
174 # Build ffmpeg command with pipes
175 # -i pipe:0 = read from stdin
176 # -f <format> pipe:1 = write to stdout
177 cmd = [
178 'ffmpeg',
179 '-i', 'pipe:0', # Read from stdin
180 '-f', source_ext, # Input format
181 '-c:v', 'libx264', # Video codec for MP4
182 '-c:a', 'aac', # Audio codec
183 '-movflags', '+faststart', # Optimize for streaming
184 '-f', dest_ext, # Output format
185 'pipe:1' # Write to stdout
186 ]
187
188 # Run ffmpeg with pipes
189 process = subprocess.Popen(
190 cmd,
191 stdin=subprocess.PIPE,
192 stdout=subprocess.PIPE,
193 stderr=subprocess.PIPE
194 )
195
196 # Send input data and get output
197 converted_data, stderr = process.communicate(input=data)
198
199 if process.returncode != 0:
200 self.disp.log_error(
201 f"ffmpeg conversion failed: {stderr.decode('utf-8', errors='replace')}"
202 )
203 return None
204
205 self.disp.log_debug(
206 f"Video conversion via pipes successful (minimal I/O cost)"
207 )
208 return converted_data
209
210 except Exception as e:
211 self.disp.log_error(f"ffmpeg pipe conversion failed: {e}")
212 return None
213
215 self,
216 data: bytes,
217 source_format: DataTypes,
218 destination_format: DataTypes,
219 result: Optional[bytes] = None
220 ) -> CONV_CONST.ConversionResult:
221 """Create a failed conversion result."""
222 return CONV_CONST.ConversionResult(
223 data=data,
224 converted=False,
225 from_type=source_format,
226 to_type=destination_format,
227 result=result
228 )
229
231 self,
232 data: bytes,
233 source_format: DataTypes,
234 destination_format: DataTypes,
235 converted_data: bytes
236 ) -> CONV_CONST.ConversionResult:
237 """Create a successful conversion result."""
238 return CONV_CONST.ConversionResult(
239 data=data,
240 converted=True,
241 from_type=source_format,
242 to_type=destination_format,
243 result=converted_data
244 )
245
247 self,
248 data: bytes,
249 source_format: DataTypes,
250 destination_format: Optional[DataTypes],
251 source_ext: Optional[str],
252 dest_ext: Optional[str]
253 ) -> Optional[CONV_CONST.ConversionResult]:
254 """Handle validation failures and return appropriate result."""
255 if destination_format is None:
256 return self._create_failed_result(
257 data, source_format, source_format, None
258 )
259
260 if source_ext is None or dest_ext is None:
261 result_data = data if source_format == destination_format else data
262 return self._create_failed_result(
263 data, source_format, destination_format, result_data
264 )
265
266 return None
267
268 def video_to_video(self, data: bytes, source_format: DataTypes) -> CONV_CONST.ConversionResult:
269 """
270 Convert video data from one format to another using ffmpeg.
271 Uses pipes for minimal I/O cost.
272
273 Args:
274 data (bytes): The video data to convert.
275 source_format (DataTypes): The original video format.
276
277 Returns:
278 ConversionResult: The converted video data in a contained dataclass.
279 """
280 # Validate conversion parameters
281 destination_format, source_ext, dest_ext = self._validate_conversion_params(
282 data, source_format
283 )
284
285 # Handle validation failures
286 validation_result = self._handle_validation_failure(
287 data, source_format, destination_format, source_ext, dest_ext
288 )
289 if validation_result is not None:
290 return validation_result
291
292 # Type guard: at this point we know these are not None due to validation
293 if destination_format is None or source_ext is None or dest_ext is None:
294 return self._create_failed_result(
295 data, source_format, source_format, None
296 )
297
298 # Perform conversion via ffmpeg pipes (minimal I/O cost)
299 converted_data = self._convert_with_ffmpeg_pipes(
300 data, source_format, destination_format, source_ext, dest_ext
301 )
302
303 if converted_data is None:
304 return self._create_failed_result(
305 data, source_format, destination_format, None
306 )
307
308 return self._create_success_result(
309 data, source_format, destination_format, converted_data
310 )
Optional[CONV_CONST.ConversionResult] _handle_validation_failure(self, bytes data, DataTypes source_format, Optional[DataTypes] destination_format, Optional[str] source_ext, Optional[str] dest_ext)
CONV_CONST.ConversionResult video_to_video(self, bytes data, DataTypes source_format)
tuple[Optional[DataTypes], Optional[str], Optional[str]] _validate_conversion_params(self, bytes data, DataTypes source_format)
Optional[str] get_video_extension(self, DataTypes data_type)
Optional[bytes] _convert_with_ffmpeg_pipes(self, bytes data, DataTypes source_format, DataTypes destination_format, str source_ext, str dest_ext)
CONV_CONST.ConversionResult _create_failed_result(self, bytes data, DataTypes source_format, DataTypes destination_format, Optional[bytes] result=None)
CONV_CONST.ConversionResult __call__(self, bytes data, DataTypes source_format)
CONV_CONST.ConversionResult _create_success_result(self, bytes data, DataTypes source_format, DataTypes destination_format, bytes converted_data)