ffmpeg-python-helper 3.1.0__py3-none-any.whl → 4.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ffmpeg_python_helper/__init__.py +5 -1
- ffmpeg_python_helper/async_ffmpeg_api.py +642 -0
- ffmpeg_python_helper/ffmpeg_api.py +61 -4
- ffmpeg_python_helper/ffprobe_api.py +169 -0
- ffmpeg_python_helper-4.0.0.dist-info/METADATA +720 -0
- ffmpeg_python_helper-4.0.0.dist-info/RECORD +9 -0
- ffmpeg_python_helper-3.1.0.dist-info/METADATA +0 -358
- ffmpeg_python_helper-3.1.0.dist-info/RECORD +0 -7
- {ffmpeg_python_helper-3.1.0.dist-info → ffmpeg_python_helper-4.0.0.dist-info}/WHEEL +0 -0
- {ffmpeg_python_helper-3.1.0.dist-info → ffmpeg_python_helper-4.0.0.dist-info}/entry_points.txt +0 -0
ffmpeg_python_helper/__init__.py
CHANGED
|
@@ -37,10 +37,14 @@ For detailed API documentation, see:
|
|
|
37
37
|
|
|
38
38
|
from .ffmpeg_api import FFMPEG
|
|
39
39
|
from .pipe_helper import Pipe
|
|
40
|
+
from .ffprobe_api import FFProbe
|
|
41
|
+
from .async_ffmpeg_api import AsyncFFMPEG
|
|
40
42
|
|
|
41
43
|
__all__ = [
|
|
42
44
|
'FFMPEG',
|
|
43
|
-
'Pipe'
|
|
45
|
+
'Pipe',
|
|
46
|
+
'FFProbe',
|
|
47
|
+
'AsyncFFMPEG'
|
|
44
48
|
]
|
|
45
49
|
|
|
46
50
|
__version__ = "0.1.0"
|
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AsyncFFMPEG:
|
|
6
|
+
"""
|
|
7
|
+
An asynchronous Python wrapper for FFMPEG that provides a simple, intuitive API for
|
|
8
|
+
common video processing tasks with non-blocking operations.
|
|
9
|
+
|
|
10
|
+
This class automatically detects FFMPEG installation in the system PATH
|
|
11
|
+
and provides asynchronous methods for video conversion, GIF creation, video trimming,
|
|
12
|
+
and audio extraction. It supports both file-based operations and
|
|
13
|
+
in-memory data processing. All methods are asynchronous and must be awaited.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
>>> import asyncio
|
|
17
|
+
>>> from ffmpeg_python_helper import AsyncFFMPEG
|
|
18
|
+
>>>
|
|
19
|
+
>>> async def process_video():
|
|
20
|
+
... async_ffmpeg = AsyncFFMPEG()
|
|
21
|
+
... print(f"AsyncFFMPEG executable found at: {async_ffmpeg.executable}")
|
|
22
|
+
...
|
|
23
|
+
... # Convert video asynchronously
|
|
24
|
+
... output = await async_ffmpeg.reformat("input.mp4", "output.avi")
|
|
25
|
+
... print(f"Conversion output: {output.decode()[:50]}...")
|
|
26
|
+
...
|
|
27
|
+
... # Create GIF asynchronously
|
|
28
|
+
... await async_ffmpeg.gif("video.mp4", "animation.gif", fps=15, scale=480)
|
|
29
|
+
... print("GIF creation completed!")
|
|
30
|
+
...
|
|
31
|
+
... # In-memory processing asynchronously
|
|
32
|
+
... with open("video.mp4", "rb") as f:
|
|
33
|
+
... video_data = f.read()
|
|
34
|
+
... gif_data = await async_ffmpeg.gifs(video_data, fps=15, scale=480)
|
|
35
|
+
... trimmed_data = await async_ffmpeg.trims(video_data, start=0, duration=30)
|
|
36
|
+
... audio_data = await async_ffmpeg.extract_audios(video_data, output_format="m4a")
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
executable (str): The path to the FFMPEG executable found in the system PATH.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Initialize a new AsyncFFMPEG instance.
|
|
45
|
+
|
|
46
|
+
Automatically searches for FFMPEG in the system PATH.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
FileNotFoundError: If FFMPEG is not found in the system PATH.
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
>>> try:
|
|
53
|
+
... async_ffmpeg = AsyncFFMPEG()
|
|
54
|
+
... print(f"AsyncFFMPEG found at: {async_ffmpeg.executable}")
|
|
55
|
+
... except FileNotFoundError as e:
|
|
56
|
+
... print(f"FFMPEG not found: {e}")
|
|
57
|
+
"""
|
|
58
|
+
import shutil
|
|
59
|
+
self.executable = shutil.which('ffmpeg')
|
|
60
|
+
|
|
61
|
+
if self.executable is None:
|
|
62
|
+
raise FileNotFoundError("""
|
|
63
|
+
FFmpeg is not installed or could not be found in PATH.
|
|
64
|
+
Please install FFmpeg and make sure it is available
|
|
65
|
+
in your system PATH.
|
|
66
|
+
""")
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def api(cls) -> "AsyncFFMPEG":
|
|
70
|
+
"""
|
|
71
|
+
Factory method that returns a new AsyncFFMPEG instance.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
AsyncFFMPEG: A new instance of the AsyncFFMPEG class.
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
>>> async_ffmpeg = AsyncFFMPEG.api()
|
|
78
|
+
>>> # Must be called within an async context
|
|
79
|
+
>>> # stdout, stderr = await async_ffmpeg.execute("-version")
|
|
80
|
+
"""
|
|
81
|
+
return cls()
|
|
82
|
+
|
|
83
|
+
async def create_process(self, *args : str):
|
|
84
|
+
"""
|
|
85
|
+
Create an asynchronous FFMPEG subprocess with the given arguments.
|
|
86
|
+
|
|
87
|
+
This is a low-level method that creates an asyncio subprocess for FFMPEG execution.
|
|
88
|
+
It's primarily used internally by the `execute` method but can be used directly
|
|
89
|
+
for advanced use cases where you need fine-grained control over the subprocess.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
*args: FFMPEG command-line arguments as strings (excluding the ffmpeg executable itself).
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
asyncio.subprocess.Process: An asyncio subprocess instance, or None if FFMPEG executable
|
|
96
|
+
is not found.
|
|
97
|
+
|
|
98
|
+
Note:
|
|
99
|
+
This method returns the subprocess object, allowing you to manage stdin/stdout/stderr
|
|
100
|
+
communication manually. For most use cases, the `execute` method is recommended as it
|
|
101
|
+
handles communication and error checking automatically.
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
>>> # Create a subprocess for FFMPEG version check
|
|
105
|
+
>>> process = await async_ffmpeg.create_process("-version")
|
|
106
|
+
>>> if process:
|
|
107
|
+
... stdout, stderr = await process.communicate()
|
|
108
|
+
... print(stdout.decode())
|
|
109
|
+
... else:
|
|
110
|
+
... print("FFMPEG not found")
|
|
111
|
+
|
|
112
|
+
>>> # Create a subprocess for video conversion
|
|
113
|
+
>>> process = await async_ffmpeg.create_process("-i", "input.mp4", "output.avi")
|
|
114
|
+
>>> if process:
|
|
115
|
+
... await process.communicate()
|
|
116
|
+
... print("Conversion completed")
|
|
117
|
+
"""
|
|
118
|
+
process = None
|
|
119
|
+
|
|
120
|
+
if self.executable:
|
|
121
|
+
_args = [
|
|
122
|
+
self.executable, *args
|
|
123
|
+
]
|
|
124
|
+
process = await asyncio.subprocess.create_subprocess_exec(
|
|
125
|
+
*_args,
|
|
126
|
+
stdin=asyncio.subprocess.PIPE,
|
|
127
|
+
stdout=asyncio.subprocess.PIPE,
|
|
128
|
+
stderr=asyncio.subprocess.PIPE,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return process
|
|
132
|
+
|
|
133
|
+
async def execute(self, *args: str, input_data: bytes | None = None) -> tuple[bytes, bytes]:
|
|
134
|
+
"""
|
|
135
|
+
Execute raw FFMPEG commands asynchronously with the given arguments.
|
|
136
|
+
|
|
137
|
+
This method allows you to run any FFMPEG command directly asynchronously,
|
|
138
|
+
providing maximum flexibility for operations not covered by the built-in methods.
|
|
139
|
+
All operations are non-blocking.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
*args: FFMPEG command-line arguments as strings.
|
|
143
|
+
input_data: Optional bytes to send to FFMPEG's stdin. Useful for
|
|
144
|
+
piping data directly to FFMPEG without intermediate files.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
tuple[bytes, bytes]: A tuple containing (stdout, stderr) from FFMPEG
|
|
148
|
+
as bytes objects.
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
FileNotFoundError: If FFMPEG executable is not found.
|
|
152
|
+
RuntimeError: If FFMPEG command returns a non-zero exit code.
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
>>> # Must be called within an async context
|
|
156
|
+
>>> stdout, stderr = await async_ffmpeg.execute("-version")
|
|
157
|
+
>>> print(stdout.decode())
|
|
158
|
+
|
|
159
|
+
>>> # Extract audio from video asynchronously
|
|
160
|
+
>>> await async_ffmpeg.execute("-i", "video.mp4", "-q:a", "0", "-map", "a", "audio.mp3")
|
|
161
|
+
|
|
162
|
+
>>> # Add watermark to video asynchronously
|
|
163
|
+
>>> await async_ffmpeg.execute("-i", "video.mp4", "-i", "watermark.png",
|
|
164
|
+
... "-filter_complex", "overlay=10:10", "output.mp4")
|
|
165
|
+
|
|
166
|
+
>>> # Process data from memory asynchronously
|
|
167
|
+
>>> video_data = b"...video bytes..."
|
|
168
|
+
>>> stdout, stderr = await async_ffmpeg.execute("-i", "pipe:0", "-f", "null", "-",
|
|
169
|
+
... input_data=video_data)
|
|
170
|
+
"""
|
|
171
|
+
_args = [
|
|
172
|
+
self.executable, *args
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
process = await self.create_process(*args)
|
|
176
|
+
if process is not None:
|
|
177
|
+
stdout, stderr = await process.communicate(input_data)
|
|
178
|
+
|
|
179
|
+
if process.returncode != 0:
|
|
180
|
+
raise RuntimeError(
|
|
181
|
+
stderr.decode(errors="replace")
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return stdout, stderr
|
|
185
|
+
|
|
186
|
+
raise FileNotFoundError("""
|
|
187
|
+
No ffmpeg executable found.
|
|
188
|
+
|
|
189
|
+
Please Install ffmpeg first.
|
|
190
|
+
""")
|
|
191
|
+
|
|
192
|
+
async def reformat(self, input_file: str, output_file: str) -> bytes:
|
|
193
|
+
"""
|
|
194
|
+
Asynchronously convert a video file from one format to another.
|
|
195
|
+
|
|
196
|
+
This method performs a simple format conversion without
|
|
197
|
+
modifying video quality or other parameters.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
input_file: Path to the input video file.
|
|
201
|
+
output_file: Path for the output video file.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
bytes: FFMPEG output (stdout or stderr) as bytes.
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
FileNotFoundError: If input file doesn't exist.
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
>>> output = await async_ffmpeg.reformat("input.mov", "output.mp4")
|
|
211
|
+
>>> print(output.decode())
|
|
212
|
+
>>>
|
|
213
|
+
>>> output = await async_ffmpeg.reformat("video.avi", "video.mkv")
|
|
214
|
+
>>> print(output.decode())
|
|
215
|
+
"""
|
|
216
|
+
if not Path(input_file).exists():
|
|
217
|
+
raise FileNotFoundError(f"""
|
|
218
|
+
Input file {input_file} did not found .
|
|
219
|
+
|
|
220
|
+
Please check filename and directory if correct.
|
|
221
|
+
""")
|
|
222
|
+
|
|
223
|
+
stdout, stderr = await self.execute("-i", input_file, output_file)
|
|
224
|
+
|
|
225
|
+
if stdout:
|
|
226
|
+
return stdout
|
|
227
|
+
else:
|
|
228
|
+
return stderr
|
|
229
|
+
|
|
230
|
+
async def gifs(self,
|
|
231
|
+
input_bytes: bytes,
|
|
232
|
+
fps: int = 10,
|
|
233
|
+
scale: int = 320) -> bytes:
|
|
234
|
+
"""
|
|
235
|
+
Asynchronously convert video data from bytes to an optimized GIF.
|
|
236
|
+
|
|
237
|
+
Creates a high-quality GIF from in-memory video data using FFMPEG's
|
|
238
|
+
palette optimization. This method is useful when you have video data
|
|
239
|
+
in memory and want to avoid writing temporary files.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
input_byte: Video data as bytes to convert to GIF.
|
|
243
|
+
fps: Frames per second for the GIF. Defaults to 10.
|
|
244
|
+
scale: Width of the GIF in pixels. Height is auto-scaled
|
|
245
|
+
to maintain aspect ratio. Defaults to 320.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
bytes: The generated GIF data as bytes.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
RuntimeError: If GIF conversion fails.
|
|
252
|
+
|
|
253
|
+
Example:
|
|
254
|
+
>>> # Read video data from a file
|
|
255
|
+
>>> with open("video.mp4", "rb") as f:
|
|
256
|
+
... video_data = f.read()
|
|
257
|
+
>>>
|
|
258
|
+
>>> # Convert to GIF in memory asynchronously
|
|
259
|
+
>>> gif_data = await async_ffmpeg.gifs(video_data, fps=15, scale=480)
|
|
260
|
+
>>>
|
|
261
|
+
>>> # Save the GIF
|
|
262
|
+
>>> with open("output.gif", "wb") as f:
|
|
263
|
+
... f.write(gif_data)
|
|
264
|
+
|
|
265
|
+
>>> # Process video from network or database asynchronously
|
|
266
|
+
>>> # video_bytes = download_video_from_url(url)
|
|
267
|
+
>>> # gif_bytes = await async_ffmpeg.gifs(video_bytes, scale=320)
|
|
268
|
+
"""
|
|
269
|
+
filter_graph: str = (
|
|
270
|
+
f"fps={fps},"
|
|
271
|
+
f"scale={scale}:-1:flags=lanczos,"
|
|
272
|
+
"split[s0][s1];"
|
|
273
|
+
"[s0]palettegen[p];"
|
|
274
|
+
"[s1][p]paletteuse"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
stdout, stderr = await self.execute(
|
|
278
|
+
"-y",
|
|
279
|
+
"-i", "pipe:0",
|
|
280
|
+
"-vf", filter_graph,
|
|
281
|
+
"-f", "gif",
|
|
282
|
+
"pipe:1",
|
|
283
|
+
input_data=input_bytes
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return stdout
|
|
287
|
+
|
|
288
|
+
async def gif(self,
|
|
289
|
+
input_file: str,
|
|
290
|
+
output_file: str,
|
|
291
|
+
fps: int = 10,
|
|
292
|
+
scale: int = 320
|
|
293
|
+
) -> None:
|
|
294
|
+
"""
|
|
295
|
+
Convert a video file to an optimized GIF.
|
|
296
|
+
|
|
297
|
+
Creates a high-quality GIF using FFMPEG's palette optimization
|
|
298
|
+
for better color reproduction and smaller file sizes.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
input_file: Path to the input video file.
|
|
302
|
+
output_file: Path for the output GIF file.
|
|
303
|
+
fps: Frames per second for the GIF. Defaults to 10.
|
|
304
|
+
scale: Width of the GIF in pixels. Height is auto-scaled
|
|
305
|
+
to maintain aspect ratio. Defaults to 320.
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
FileNotFoundError: If input file doesn't exist.
|
|
309
|
+
RuntimeError: If GIF file was not created successfully.
|
|
310
|
+
|
|
311
|
+
Example:
|
|
312
|
+
>>> # Create a standard GIF
|
|
313
|
+
>>> ffmpeg.gif("video.mp4", "output.gif")
|
|
314
|
+
|
|
315
|
+
>>> # Create a higher quality GIF with custom settings
|
|
316
|
+
>>> ffmpeg.gif("video.mp4", "output.gif",
|
|
317
|
+
... fps=15, scale=640)
|
|
318
|
+
|
|
319
|
+
>>> # Create a small thumbnail GIF
|
|
320
|
+
>>> ffmpeg.gif("video.mp4", "thumbnail.gif",
|
|
321
|
+
... fps=5, scale=160)
|
|
322
|
+
"""
|
|
323
|
+
input_path = Path(input_file)
|
|
324
|
+
output_path = Path(output_file)
|
|
325
|
+
|
|
326
|
+
if not input_path.exists():
|
|
327
|
+
raise FileNotFoundError(
|
|
328
|
+
f"Input file {input_file} was not found."
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
filter_graph: str = (
|
|
332
|
+
f"fps={fps},"
|
|
333
|
+
f"scale={scale}:-1:flags=lanczos,"
|
|
334
|
+
"split[s0][s1];"
|
|
335
|
+
"[s0]palettegen[p];"
|
|
336
|
+
"[s1][p]paletteuse"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
await self.execute(
|
|
340
|
+
"-y",
|
|
341
|
+
"-i", str(input_path),
|
|
342
|
+
"-vf", filter_graph,
|
|
343
|
+
str(output_path),
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if not output_path.exists():
|
|
347
|
+
raise RuntimeError(
|
|
348
|
+
f"GIF was not created: {output_file}"
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
async def trim(
|
|
352
|
+
self,
|
|
353
|
+
input_file: str,
|
|
354
|
+
output_file: str,
|
|
355
|
+
start: float = 0,
|
|
356
|
+
duration: float | None = None,
|
|
357
|
+
) -> None:
|
|
358
|
+
"""
|
|
359
|
+
Trim a video file.
|
|
360
|
+
|
|
361
|
+
Extracts a segment from a video file starting at a specified time
|
|
362
|
+
and optionally ending after a specified duration.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
input_file: Path to the input video file.
|
|
366
|
+
output_file: Path for the output trimmed video.
|
|
367
|
+
start: Start time in seconds. Defaults to 0.
|
|
368
|
+
duration: Duration in seconds, or None for remaining video.
|
|
369
|
+
Defaults to None.
|
|
370
|
+
|
|
371
|
+
Raises:
|
|
372
|
+
FileNotFoundError: If input file doesn't exist.
|
|
373
|
+
ValueError: If start is negative or duration is non-positive.
|
|
374
|
+
RuntimeError: If trimmed video was not created successfully.
|
|
375
|
+
|
|
376
|
+
Example:
|
|
377
|
+
>>> # Trim from 5 seconds to 10 seconds (5-second clip)
|
|
378
|
+
>>> ffmpeg.trim("video.mp4", "clip.mp4",
|
|
379
|
+
... start=5, duration=5)
|
|
380
|
+
|
|
381
|
+
>>> # Trim from 10 seconds to the end of video
|
|
382
|
+
>>> ffmpeg.trim("video.mp4", "ending.mp4",
|
|
383
|
+
... start=10)
|
|
384
|
+
|
|
385
|
+
>>> # Trim first 30 seconds of video
|
|
386
|
+
>>> ffmpeg.trim("video.mp4", "intro.mp4",
|
|
387
|
+
... start=0, duration=30)
|
|
388
|
+
|
|
389
|
+
>>> # Trim with floating point precision
|
|
390
|
+
>>> ffmpeg.trim("video.mp4", "precise.mp4",
|
|
391
|
+
... start=2.5, duration=3.75)
|
|
392
|
+
"""
|
|
393
|
+
input_path = Path(input_file)
|
|
394
|
+
output_path = Path(output_file)
|
|
395
|
+
|
|
396
|
+
if not input_path.exists():
|
|
397
|
+
raise FileNotFoundError(
|
|
398
|
+
f"Input file {input_file} was not found."
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
if start < 0:
|
|
402
|
+
raise ValueError("start must be greater than or equal to 0")
|
|
403
|
+
|
|
404
|
+
if duration is not None and duration <= 0:
|
|
405
|
+
raise ValueError("duration must be greater than 0")
|
|
406
|
+
|
|
407
|
+
args = [
|
|
408
|
+
"-y",
|
|
409
|
+
"-ss", str(start),
|
|
410
|
+
"-i", str(input_path),
|
|
411
|
+
]
|
|
412
|
+
|
|
413
|
+
if duration is not None:
|
|
414
|
+
args.extend(["-t", str(duration)])
|
|
415
|
+
|
|
416
|
+
args.append(str(output_path))
|
|
417
|
+
|
|
418
|
+
stdout, stderr = await self.execute(*args)
|
|
419
|
+
|
|
420
|
+
if not output_path.exists():
|
|
421
|
+
raise RuntimeError(
|
|
422
|
+
f"Trimmed video was not created: {output_file}"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
async def trims(self, input_bytes: bytes,
|
|
426
|
+
start: float = 0,
|
|
427
|
+
duration: float | None = None,
|
|
428
|
+
format_type: str = "mp4",
|
|
429
|
+
v_encoder : str = "libx264",
|
|
430
|
+
a_encoder : str = "aac"
|
|
431
|
+
) -> bytes:
|
|
432
|
+
"""
|
|
433
|
+
Trim video data from bytes (in-memory processing).
|
|
434
|
+
|
|
435
|
+
Extracts a segment from in-memory video data starting at a specified time
|
|
436
|
+
and optionally ending after a specified duration. This method is useful
|
|
437
|
+
when you have video data in memory and want to avoid writing temporary files.
|
|
438
|
+
|
|
439
|
+
Note: For MP4 format, this method uses libx264 video codec and AAC audio codec
|
|
440
|
+
with fragmented MP4 output for better streaming compatibility.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
input_bytes: Video data as bytes to trim.
|
|
444
|
+
start: Start time in seconds. Defaults to 0.
|
|
445
|
+
duration: Duration in seconds, or None for remaining video.
|
|
446
|
+
Defaults to None.
|
|
447
|
+
format_type: Output format (e.g., 'mp4', 'avi', 'mov'). Defaults to 'mp4'.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
bytes: The trimmed video data as bytes.
|
|
451
|
+
|
|
452
|
+
Raises:
|
|
453
|
+
RuntimeError: If video trimming fails.
|
|
454
|
+
|
|
455
|
+
Example:
|
|
456
|
+
>>> # Read video data from a file
|
|
457
|
+
>>> with open("video.mp4", "rb") as f:
|
|
458
|
+
... video_data = f.read()
|
|
459
|
+
>>>
|
|
460
|
+
>>> # Trim first 30 seconds in memory
|
|
461
|
+
>>> trimmed_data = ffmpeg.trims(video_data, start=0, duration=30)
|
|
462
|
+
>>>
|
|
463
|
+
>>> # Save the trimmed video
|
|
464
|
+
>>> with open("intro.mp4", "wb") as f:
|
|
465
|
+
... f.write(trimmed_data)
|
|
466
|
+
>>>
|
|
467
|
+
>>> # Trim with different format
|
|
468
|
+
>>> webm_data = ffmpeg.trims(video_data, start=10, duration=5, format_type="webm")
|
|
469
|
+
>>> with open("clip.webm", "wb") as f:
|
|
470
|
+
... f.write(webm_data)
|
|
471
|
+
>>>
|
|
472
|
+
>>> # Trim with specific codec settings
|
|
473
|
+
>>> # For non-MP4 formats, FFMPEG will use default codecs
|
|
474
|
+
>>> avi_data = ffmpeg.trims(video_data, start=5, duration=10, format_type="avi")
|
|
475
|
+
"""
|
|
476
|
+
args = [
|
|
477
|
+
"-y",
|
|
478
|
+
"-i", "pipe:0",
|
|
479
|
+
"-ss", str(start)
|
|
480
|
+
]
|
|
481
|
+
|
|
482
|
+
if duration is not None:
|
|
483
|
+
args.extend(["-t", str(duration)])
|
|
484
|
+
|
|
485
|
+
if format_type == 'mp4':
|
|
486
|
+
args.extend([
|
|
487
|
+
"-c:v", v_encoder,
|
|
488
|
+
"-pix_fmt", "yuv420p",
|
|
489
|
+
"-c:a", a_encoder,
|
|
490
|
+
"-movflags", "frag_keyframe+empty_moov"
|
|
491
|
+
])
|
|
492
|
+
|
|
493
|
+
args.extend([
|
|
494
|
+
"-f", format_type,
|
|
495
|
+
"pipe:1"
|
|
496
|
+
])
|
|
497
|
+
|
|
498
|
+
stdout, stderr = await self.execute(*args, input_data=input_bytes)
|
|
499
|
+
|
|
500
|
+
return stdout
|
|
501
|
+
|
|
502
|
+
async def extract_audio(self, input_file: str, output_file: str) -> None:
|
|
503
|
+
"""
|
|
504
|
+
Extract audio from a video file.
|
|
505
|
+
|
|
506
|
+
Extracts the audio track from a video file without re-encoding,
|
|
507
|
+
preserving the original audio quality. The output file extension
|
|
508
|
+
should match the audio codec (e.g., .m4a for AAC, .mp3 for MP3,
|
|
509
|
+
.ogg for Vorbis).
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
input_file: Path to the input video file.
|
|
513
|
+
output_file: Path for the output audio file.
|
|
514
|
+
|
|
515
|
+
Raises:
|
|
516
|
+
FileNotFoundError: If input file doesn't exist.
|
|
517
|
+
RuntimeError: If audio extraction fails.
|
|
518
|
+
|
|
519
|
+
Example:
|
|
520
|
+
>>> # Extract audio from MP4 video
|
|
521
|
+
>>> ffmpeg.extract_audio("video.mp4", "audio.m4a")
|
|
522
|
+
>>>
|
|
523
|
+
>>> # Extract audio and convert to MP3
|
|
524
|
+
>>> ffmpeg.execute("-i", "video.mp4", "-q:a", "0", "-map", "a", "audio.mp3")
|
|
525
|
+
>>>
|
|
526
|
+
>>> # Extract audio from multiple formats
|
|
527
|
+
>>> ffmpeg.extract_audio("movie.mkv", "audio.m4a")
|
|
528
|
+
>>> ffmpeg.extract_audio("clip.avi", "audio.mp3")
|
|
529
|
+
"""
|
|
530
|
+
input_path = Path(input_file)
|
|
531
|
+
output_path = Path(output_file)
|
|
532
|
+
|
|
533
|
+
if not input_path.exists():
|
|
534
|
+
raise FileNotFoundError(
|
|
535
|
+
f"Input file {input_file} was not found."
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
await self.execute(
|
|
539
|
+
"-i", str(input_path), # Specifies the input video file.
|
|
540
|
+
"-vn", # Disables the video stream (drops the visual data).
|
|
541
|
+
"-c:a", "copy", # Copies the audio track as-is without re-encoding.
|
|
542
|
+
str(output_path) # Output file. Extension should match audio codec.
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
async def extract_audios(self, input_bytes: bytes, output_format: str = "m4a") -> bytes:
|
|
546
|
+
"""
|
|
547
|
+
Extract audio from video data (in-memory processing).
|
|
548
|
+
|
|
549
|
+
Extracts the audio track from in-memory video data without re-encoding,
|
|
550
|
+
preserving the original audio quality. This method is useful when you
|
|
551
|
+
have video data in memory and want to avoid writing temporary files.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
input_bytes: Video data as bytes to extract audio from.
|
|
555
|
+
output_format: Audio output format (e.g., 'm4a', 'mp3', 'ogg', 'wav').
|
|
556
|
+
Defaults to 'm4a'.
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
bytes: The extracted audio data as bytes.
|
|
560
|
+
|
|
561
|
+
Raises:
|
|
562
|
+
RuntimeError: If audio extraction fails.
|
|
563
|
+
|
|
564
|
+
Example:
|
|
565
|
+
>>> # Read video data from a file
|
|
566
|
+
>>> with open("video.mp4", "rb") as f:
|
|
567
|
+
... video_data = f.read()
|
|
568
|
+
>>>
|
|
569
|
+
>>> # Extract audio in memory
|
|
570
|
+
>>> audio_data = ffmpeg.extract_audios(video_data, output_format="m4a")
|
|
571
|
+
>>>
|
|
572
|
+
>>> # Save the extracted audio
|
|
573
|
+
>>> with open("audio.m4a", "wb") as f:
|
|
574
|
+
... f.write(audio_data)
|
|
575
|
+
>>>
|
|
576
|
+
>>> # Extract audio in different formats
|
|
577
|
+
>>> mp3_data = ffmpeg.extract_audios(video_data, output_format="mp3")
|
|
578
|
+
>>> ogg_data = ffmpeg.extract_audios(video_data, output_format="ogg")
|
|
579
|
+
"""
|
|
580
|
+
args = [
|
|
581
|
+
"-i", "pipe:0",
|
|
582
|
+
"-vn",
|
|
583
|
+
"-c:a", "copy",
|
|
584
|
+
"-f", output_format,
|
|
585
|
+
"pipe:1"
|
|
586
|
+
]
|
|
587
|
+
|
|
588
|
+
return (await self.execute(*args, input_data=input_bytes))[0]
|
|
589
|
+
|
|
590
|
+
async def compress_file(self,
|
|
591
|
+
input_file : str,
|
|
592
|
+
output_file : str,
|
|
593
|
+
crf: int = 28,
|
|
594
|
+
v_encoder : str = "libx264",
|
|
595
|
+
a_encoder : str = "aac" ,
|
|
596
|
+
px_format : str = "yuv420p"):
|
|
597
|
+
|
|
598
|
+
args = [
|
|
599
|
+
"-i", input_file,
|
|
600
|
+
"-c:v", v_encoder,
|
|
601
|
+
"-crf", str(crf),
|
|
602
|
+
"-pix_fmt", px_format,
|
|
603
|
+
"-c:a", a_encoder,
|
|
604
|
+
"-movflags", "faststart",
|
|
605
|
+
output_file,
|
|
606
|
+
]
|
|
607
|
+
|
|
608
|
+
await self.execute(*args)
|
|
609
|
+
|
|
610
|
+
async def compress_byte(self,
|
|
611
|
+
input_data : bytes,
|
|
612
|
+
crf: int = 28,
|
|
613
|
+
v_encoder : str = "libx264",
|
|
614
|
+
a_encoder : str = "aac" ,
|
|
615
|
+
px_format : str = "yuv420p"
|
|
616
|
+
):
|
|
617
|
+
args = [
|
|
618
|
+
"-i", "pipe:0",
|
|
619
|
+
"-c:v", v_encoder,
|
|
620
|
+
"-crf", str(crf),
|
|
621
|
+
"-pix_fmt", px_format,
|
|
622
|
+
"-c:a", a_encoder,
|
|
623
|
+
"-movflags", "frag_keyframe+empty_moov",
|
|
624
|
+
"-f", "mp4",
|
|
625
|
+
"pipe:1"
|
|
626
|
+
]
|
|
627
|
+
|
|
628
|
+
return (await self.execute(*args, input_data=input_data))[0]
|
|
629
|
+
|
|
630
|
+
async def verify(self, input_file : str) -> tuple[bool, str]:
|
|
631
|
+
args = [
|
|
632
|
+
"-v", "error",
|
|
633
|
+
"-i", input_file,
|
|
634
|
+
"-f", "null",
|
|
635
|
+
"-"
|
|
636
|
+
]
|
|
637
|
+
|
|
638
|
+
try:
|
|
639
|
+
await self.execute(*args)
|
|
640
|
+
return True, ""
|
|
641
|
+
except RuntimeError as e:
|
|
642
|
+
return False, str(e)
|