ffmpeg-python-helper 3.2.0__tar.gz → 4.1.0__tar.gz
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-3.2.0 → ffmpeg_python_helper-4.1.0}/PKG-INFO +204 -4
- {ffmpeg_python_helper-3.2.0 → ffmpeg_python_helper-4.1.0}/README.md +203 -3
- {ffmpeg_python_helper-3.2.0 → ffmpeg_python_helper-4.1.0}/pyproject.toml +1 -1
- {ffmpeg_python_helper-3.2.0 → ffmpeg_python_helper-4.1.0}/pyproject.toml.orig +1 -1
- {ffmpeg_python_helper-3.2.0 → ffmpeg_python_helper-4.1.0}/src/ffmpeg_python_helper/__init__.py +3 -1
- ffmpeg_python_helper-4.1.0/src/ffmpeg_python_helper/async_ffmpeg_api.py +642 -0
- {ffmpeg_python_helper-3.2.0 → ffmpeg_python_helper-4.1.0}/src/ffmpeg_python_helper/ffmpeg_api.py +0 -0
- {ffmpeg_python_helper-3.2.0 → ffmpeg_python_helper-4.1.0}/src/ffmpeg_python_helper/ffprobe_api.py +0 -0
- {ffmpeg_python_helper-3.2.0 → ffmpeg_python_helper-4.1.0}/src/ffmpeg_python_helper/pipe_helper.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: ffmpeg-python-helper
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.1.0
|
|
4
4
|
Summary: A Python wrapper for FFMPEG that provides a simple, intuitive API for common video processing tasks including format conversion, GIF creation, and video trimming.
|
|
5
5
|
Author: marjon
|
|
6
6
|
Author-email: marjon <petmalu.marjon@gmail.com>
|
|
@@ -20,6 +20,7 @@ A Python wrapper for FFMPEG that provides a simple, intuitive API for common vid
|
|
|
20
20
|
- 🧠 **In-Memory Processing** - Process video/audio data directly from bytes without temporary files
|
|
21
21
|
- ✂️ **Video Trimming** - Trim videos with precise start time and duration control
|
|
22
22
|
- 🔍 **Video Metadata Analysis** - Extract video information and metadata using FFProbe
|
|
23
|
+
- ⚡ **Asynchronous Operations** - Non-blocking async API for responsive applications
|
|
23
24
|
- 🐍 **Pythonic API** - Clean, object-oriented interface with proper error handling
|
|
24
25
|
- 📁 **File Validation** - Automatic input file existence checking
|
|
25
26
|
|
|
@@ -103,6 +104,25 @@ stdout, stderr = ffprobe.execute("-v", "quiet", "-print_format", "json",
|
|
|
103
104
|
metadata = json.loads(stdout.decode())
|
|
104
105
|
print(f"Video duration: {metadata['format']['duration']} seconds")
|
|
105
106
|
print(f"Video dimensions: {metadata['streams'][0]['width']}x{metadata['streams'][0]['height']}")
|
|
107
|
+
|
|
108
|
+
# Use AsyncFFMPEG for non-blocking operations
|
|
109
|
+
import asyncio
|
|
110
|
+
from ffmpeg_python_helper import AsyncFFMPEG
|
|
111
|
+
|
|
112
|
+
async def process_video_async():
|
|
113
|
+
async_ffmpeg = AsyncFFMPEG()
|
|
114
|
+
print(f"AsyncFFMPEG executable found at: {async_ffmpeg.executable}")
|
|
115
|
+
|
|
116
|
+
# Convert video asynchronously
|
|
117
|
+
output = await async_ffmpeg.reformat("input.mp4", "output_async.avi")
|
|
118
|
+
print(f"Async conversion output: {output.decode()[:50]}...")
|
|
119
|
+
|
|
120
|
+
# Create GIF asynchronously
|
|
121
|
+
await async_ffmpeg.gif("video.mp4", "animation_async.gif", fps=15, scale=480)
|
|
122
|
+
print("Async GIF creation completed!")
|
|
123
|
+
|
|
124
|
+
# Run the async function
|
|
125
|
+
asyncio.run(process_video_async())
|
|
106
126
|
```
|
|
107
127
|
|
|
108
128
|
## API Reference
|
|
@@ -346,6 +366,156 @@ for video in videos:
|
|
|
346
366
|
print(f"{video}: Too long (> 5 seconds)")
|
|
347
367
|
```
|
|
348
368
|
|
|
369
|
+
### `AsyncFFMPEG` Class
|
|
370
|
+
|
|
371
|
+
An asynchronous Python wrapper for FFMPEG that provides a simple, intuitive API for common video processing tasks with non-blocking operations.
|
|
372
|
+
|
|
373
|
+
This class provides all the same functionality as the `FFMPEG` class but with asynchronous methods, allowing you to perform video processing operations without blocking your application. This is especially useful for web applications, GUI applications, or any scenario where you need to maintain responsiveness while performing video processing tasks.
|
|
374
|
+
|
|
375
|
+
#### Constructor
|
|
376
|
+
```python
|
|
377
|
+
AsyncFFMPEG()
|
|
378
|
+
```
|
|
379
|
+
Creates a new AsyncFFMPEG instance. Automatically searches for FFMPEG in the system PATH.
|
|
380
|
+
- **Raises**: `FileNotFoundError` if FFMPEG is not found in PATH
|
|
381
|
+
|
|
382
|
+
#### Properties
|
|
383
|
+
- `executable` (str): The path to the FFMPEG executable found in the system
|
|
384
|
+
|
|
385
|
+
#### Class Methods
|
|
386
|
+
```python
|
|
387
|
+
@classmethod
|
|
388
|
+
def api(cls) -> "AsyncFFMPEG"
|
|
389
|
+
```
|
|
390
|
+
Factory method that returns a new AsyncFFMPEG instance.
|
|
391
|
+
- **Returns**: `AsyncFFMPEG` instance
|
|
392
|
+
|
|
393
|
+
#### Instance Methods
|
|
394
|
+
|
|
395
|
+
All methods are asynchronous and must be awaited. The API mirrors the synchronous `FFMPEG` class but with `async`/`await` syntax.
|
|
396
|
+
|
|
397
|
+
##### `async execute(*args: str, input_data: bytes | None = None) -> tuple[bytes, bytes]`
|
|
398
|
+
Execute raw FFMPEG commands asynchronously with the given arguments.
|
|
399
|
+
|
|
400
|
+
**Parameters:**
|
|
401
|
+
- `*args` (str): FFMPEG command-line arguments
|
|
402
|
+
- `input_data` (bytes | None, optional): Optional bytes to send to FFMPEG's stdin
|
|
403
|
+
|
|
404
|
+
**Returns:**
|
|
405
|
+
- `tuple[bytes, bytes]`: A tuple containing (stdout, stderr) as bytes
|
|
406
|
+
|
|
407
|
+
**Raises:**
|
|
408
|
+
- `FileNotFoundError`: If FFMPEG executable is not found
|
|
409
|
+
- `RuntimeError`: If FFMPEG command returns a non-zero exit code
|
|
410
|
+
|
|
411
|
+
**Example:**
|
|
412
|
+
```python
|
|
413
|
+
# Must be called within an async context
|
|
414
|
+
stdout, stderr = await async_ffmpeg.execute("-version")
|
|
415
|
+
print(stdout.decode())
|
|
416
|
+
|
|
417
|
+
# Process data from memory asynchronously
|
|
418
|
+
video_data = b"...video bytes..."
|
|
419
|
+
stdout, stderr = await async_ffmpeg.execute("-i", "pipe:0", "-f", "null", "-", input_data=video_data)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
##### `async reformat(input_file: str, output_file: str) -> bytes`
|
|
423
|
+
Asynchronously convert a video file from one format to another.
|
|
424
|
+
|
|
425
|
+
**Parameters:**
|
|
426
|
+
- `input_file` (str): Path to the input video file
|
|
427
|
+
- `output_file` (str): Path for the output video file
|
|
428
|
+
|
|
429
|
+
**Returns:**
|
|
430
|
+
- `bytes`: FFMPEG output (stdout or stderr) as bytes
|
|
431
|
+
|
|
432
|
+
**Raises:**
|
|
433
|
+
- `FileNotFoundError`: If input file doesn't exist
|
|
434
|
+
|
|
435
|
+
**Example:**
|
|
436
|
+
```python
|
|
437
|
+
output = await async_ffmpeg.reformat("input.mov", "output.mp4")
|
|
438
|
+
print(output.decode())
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
##### `async gif(input_file: str, output_file: str, fps: int = 10, scale: int = 320) -> bytes`
|
|
442
|
+
Asynchronously convert a video file to an optimized GIF.
|
|
443
|
+
|
|
444
|
+
**Parameters:**
|
|
445
|
+
- `input_file` (str): Path to the input video file
|
|
446
|
+
- `output_file` (str): Path for the output GIF file
|
|
447
|
+
- `fps` (int, optional): Frames per second for the GIF (default: 10)
|
|
448
|
+
- `scale` (int, optional): Width of the GIF in pixels, height is auto-scaled (default: 320)
|
|
449
|
+
|
|
450
|
+
**Returns:**
|
|
451
|
+
- `bytes`: FFMPEG output (stdout or stderr) as bytes
|
|
452
|
+
|
|
453
|
+
**Raises:**
|
|
454
|
+
- `FileNotFoundError`: If input file doesn't exist
|
|
455
|
+
- `RuntimeError`: If GIF file was not created successfully
|
|
456
|
+
|
|
457
|
+
**Example:**
|
|
458
|
+
```python
|
|
459
|
+
output = await async_ffmpeg.gif("video.mp4", "output.gif", fps=15, scale=640)
|
|
460
|
+
print(output.decode())
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
##### `async gifs(input_byte: bytes, fps: int = 10, scale: int = 320) -> bytes`
|
|
464
|
+
Asynchronously convert video data from bytes to an optimized GIF (in-memory processing).
|
|
465
|
+
|
|
466
|
+
**Parameters:**
|
|
467
|
+
- `input_byte` (bytes): Video data as bytes to convert to GIF
|
|
468
|
+
- `fps` (int, optional): Frames per second for the GIF (default: 10)
|
|
469
|
+
- `scale` (int, optional): Width of the GIF in pixels, height is auto-scaled (default: 320)
|
|
470
|
+
|
|
471
|
+
**Returns:**
|
|
472
|
+
- `bytes`: The generated GIF data as bytes
|
|
473
|
+
|
|
474
|
+
**Raises:**
|
|
475
|
+
- `RuntimeError`: If GIF conversion fails
|
|
476
|
+
|
|
477
|
+
**Example:**
|
|
478
|
+
```python
|
|
479
|
+
# Read video data from a file
|
|
480
|
+
with open("video.mp4", "rb") as f:
|
|
481
|
+
video_data = f.read()
|
|
482
|
+
|
|
483
|
+
# Convert to GIF in memory asynchronously
|
|
484
|
+
gif_data = await async_ffmpeg.gifs(video_data, fps=15, scale=480)
|
|
485
|
+
|
|
486
|
+
# Save the GIF
|
|
487
|
+
with open("output.gif", "wb") as f:
|
|
488
|
+
f.write(gif_data)
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
##### `async trim(input_file: str, output_file: str, start: float = 0, duration: float | None = None) -> bytes`
|
|
492
|
+
Asynchronously trim a video file.
|
|
493
|
+
|
|
494
|
+
**Parameters:**
|
|
495
|
+
- `input_file` (str): Path to the input video file
|
|
496
|
+
- `output_file` (str): Path for the output trimmed video
|
|
497
|
+
- `start` (float, optional): Start time in seconds (default: 0)
|
|
498
|
+
- `duration` (float | None, optional): Duration in seconds, or None for remaining video (default: None)
|
|
499
|
+
|
|
500
|
+
**Returns:**
|
|
501
|
+
- `bytes`: FFMPEG output (stdout or stderr) as bytes
|
|
502
|
+
|
|
503
|
+
**Raises:**
|
|
504
|
+
- `FileNotFoundError`: If input file doesn't exist
|
|
505
|
+
- `ValueError`: If start is negative or duration is non-positive
|
|
506
|
+
- `RuntimeError`: If trimmed video was not created successfully
|
|
507
|
+
|
|
508
|
+
**Example:**
|
|
509
|
+
```python
|
|
510
|
+
# Trim from 5 seconds to 10 seconds (5-second clip) asynchronously
|
|
511
|
+
output = await async_ffmpeg.trim("video.mp4", "clip.mp4", start=5, duration=5)
|
|
512
|
+
print(output.decode())
|
|
513
|
+
|
|
514
|
+
# Trim from 10 seconds to the end of video asynchronously
|
|
515
|
+
output = await async_ffmpeg.trim("video.mp4", "ending.mp4", start=10)
|
|
516
|
+
print(output.decode())
|
|
517
|
+
```
|
|
518
|
+
|
|
349
519
|
## Advanced Usage
|
|
350
520
|
|
|
351
521
|
### Custom FFMPEG Commands
|
|
@@ -409,6 +579,37 @@ if audio_info['streams']:
|
|
|
409
579
|
print(f"Sample rate: {audio_info['streams'][0]['sample_rate']} Hz")
|
|
410
580
|
```
|
|
411
581
|
|
|
582
|
+
### Asynchronous Batch Processing with AsyncFFMPEG
|
|
583
|
+
AsyncFFMPEG is ideal for batch processing and web applications where you need to maintain responsiveness:
|
|
584
|
+
|
|
585
|
+
```python
|
|
586
|
+
import asyncio
|
|
587
|
+
from ffmpeg_python_helper import AsyncFFMPEG
|
|
588
|
+
|
|
589
|
+
async def process_videos_concurrently():
|
|
590
|
+
async_ffmpeg = AsyncFFMPEG()
|
|
591
|
+
videos = ["video1.mp4", "video2.mp4", "video3.mp4"]
|
|
592
|
+
|
|
593
|
+
# Process multiple videos concurrently
|
|
594
|
+
tasks = []
|
|
595
|
+
for video in videos:
|
|
596
|
+
task = async_ffmpeg.gif(video, f"{video}_async.gif", fps=12, scale=400)
|
|
597
|
+
tasks.append(task)
|
|
598
|
+
|
|
599
|
+
# Wait for all async tasks to complete
|
|
600
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
601
|
+
|
|
602
|
+
# Handle results
|
|
603
|
+
for video, result in zip(videos, results):
|
|
604
|
+
if isinstance(result, Exception):
|
|
605
|
+
print(f"Failed to process {video}: {result}")
|
|
606
|
+
else:
|
|
607
|
+
print(f"Successfully processed {video}: {len(result)} bytes output")
|
|
608
|
+
|
|
609
|
+
# Run concurrent processing
|
|
610
|
+
asyncio.run(process_videos_concurrently())
|
|
611
|
+
```
|
|
612
|
+
|
|
412
613
|
### Error Handling
|
|
413
614
|
```python
|
|
414
615
|
from ffmpeg_python_helper import FFMPEG
|
|
@@ -509,9 +710,8 @@ MIT License - see LICENSE file for details
|
|
|
509
710
|
|
|
510
711
|
## Support
|
|
511
712
|
|
|
512
|
-
- **Issues**: [GitHub Issues](https://github.com/
|
|
513
|
-
- **
|
|
514
|
-
- **Email**: marjongodito@gmanmi.com
|
|
713
|
+
- **Issues**: [GitHub Issues](https://github.com/nojram00/python-ffmpeg/issues)
|
|
714
|
+
- **Email**: marjongodito.0505@gmail.com
|
|
515
715
|
|
|
516
716
|
## Acknowledgments
|
|
517
717
|
|
|
@@ -11,6 +11,7 @@ A Python wrapper for FFMPEG that provides a simple, intuitive API for common vid
|
|
|
11
11
|
- 🧠 **In-Memory Processing** - Process video/audio data directly from bytes without temporary files
|
|
12
12
|
- ✂️ **Video Trimming** - Trim videos with precise start time and duration control
|
|
13
13
|
- 🔍 **Video Metadata Analysis** - Extract video information and metadata using FFProbe
|
|
14
|
+
- ⚡ **Asynchronous Operations** - Non-blocking async API for responsive applications
|
|
14
15
|
- 🐍 **Pythonic API** - Clean, object-oriented interface with proper error handling
|
|
15
16
|
- 📁 **File Validation** - Automatic input file existence checking
|
|
16
17
|
|
|
@@ -94,6 +95,25 @@ stdout, stderr = ffprobe.execute("-v", "quiet", "-print_format", "json",
|
|
|
94
95
|
metadata = json.loads(stdout.decode())
|
|
95
96
|
print(f"Video duration: {metadata['format']['duration']} seconds")
|
|
96
97
|
print(f"Video dimensions: {metadata['streams'][0]['width']}x{metadata['streams'][0]['height']}")
|
|
98
|
+
|
|
99
|
+
# Use AsyncFFMPEG for non-blocking operations
|
|
100
|
+
import asyncio
|
|
101
|
+
from ffmpeg_python_helper import AsyncFFMPEG
|
|
102
|
+
|
|
103
|
+
async def process_video_async():
|
|
104
|
+
async_ffmpeg = AsyncFFMPEG()
|
|
105
|
+
print(f"AsyncFFMPEG executable found at: {async_ffmpeg.executable}")
|
|
106
|
+
|
|
107
|
+
# Convert video asynchronously
|
|
108
|
+
output = await async_ffmpeg.reformat("input.mp4", "output_async.avi")
|
|
109
|
+
print(f"Async conversion output: {output.decode()[:50]}...")
|
|
110
|
+
|
|
111
|
+
# Create GIF asynchronously
|
|
112
|
+
await async_ffmpeg.gif("video.mp4", "animation_async.gif", fps=15, scale=480)
|
|
113
|
+
print("Async GIF creation completed!")
|
|
114
|
+
|
|
115
|
+
# Run the async function
|
|
116
|
+
asyncio.run(process_video_async())
|
|
97
117
|
```
|
|
98
118
|
|
|
99
119
|
## API Reference
|
|
@@ -337,6 +357,156 @@ for video in videos:
|
|
|
337
357
|
print(f"{video}: Too long (> 5 seconds)")
|
|
338
358
|
```
|
|
339
359
|
|
|
360
|
+
### `AsyncFFMPEG` Class
|
|
361
|
+
|
|
362
|
+
An asynchronous Python wrapper for FFMPEG that provides a simple, intuitive API for common video processing tasks with non-blocking operations.
|
|
363
|
+
|
|
364
|
+
This class provides all the same functionality as the `FFMPEG` class but with asynchronous methods, allowing you to perform video processing operations without blocking your application. This is especially useful for web applications, GUI applications, or any scenario where you need to maintain responsiveness while performing video processing tasks.
|
|
365
|
+
|
|
366
|
+
#### Constructor
|
|
367
|
+
```python
|
|
368
|
+
AsyncFFMPEG()
|
|
369
|
+
```
|
|
370
|
+
Creates a new AsyncFFMPEG instance. Automatically searches for FFMPEG in the system PATH.
|
|
371
|
+
- **Raises**: `FileNotFoundError` if FFMPEG is not found in PATH
|
|
372
|
+
|
|
373
|
+
#### Properties
|
|
374
|
+
- `executable` (str): The path to the FFMPEG executable found in the system
|
|
375
|
+
|
|
376
|
+
#### Class Methods
|
|
377
|
+
```python
|
|
378
|
+
@classmethod
|
|
379
|
+
def api(cls) -> "AsyncFFMPEG"
|
|
380
|
+
```
|
|
381
|
+
Factory method that returns a new AsyncFFMPEG instance.
|
|
382
|
+
- **Returns**: `AsyncFFMPEG` instance
|
|
383
|
+
|
|
384
|
+
#### Instance Methods
|
|
385
|
+
|
|
386
|
+
All methods are asynchronous and must be awaited. The API mirrors the synchronous `FFMPEG` class but with `async`/`await` syntax.
|
|
387
|
+
|
|
388
|
+
##### `async execute(*args: str, input_data: bytes | None = None) -> tuple[bytes, bytes]`
|
|
389
|
+
Execute raw FFMPEG commands asynchronously with the given arguments.
|
|
390
|
+
|
|
391
|
+
**Parameters:**
|
|
392
|
+
- `*args` (str): FFMPEG command-line arguments
|
|
393
|
+
- `input_data` (bytes | None, optional): Optional bytes to send to FFMPEG's stdin
|
|
394
|
+
|
|
395
|
+
**Returns:**
|
|
396
|
+
- `tuple[bytes, bytes]`: A tuple containing (stdout, stderr) as bytes
|
|
397
|
+
|
|
398
|
+
**Raises:**
|
|
399
|
+
- `FileNotFoundError`: If FFMPEG executable is not found
|
|
400
|
+
- `RuntimeError`: If FFMPEG command returns a non-zero exit code
|
|
401
|
+
|
|
402
|
+
**Example:**
|
|
403
|
+
```python
|
|
404
|
+
# Must be called within an async context
|
|
405
|
+
stdout, stderr = await async_ffmpeg.execute("-version")
|
|
406
|
+
print(stdout.decode())
|
|
407
|
+
|
|
408
|
+
# Process data from memory asynchronously
|
|
409
|
+
video_data = b"...video bytes..."
|
|
410
|
+
stdout, stderr = await async_ffmpeg.execute("-i", "pipe:0", "-f", "null", "-", input_data=video_data)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
##### `async reformat(input_file: str, output_file: str) -> bytes`
|
|
414
|
+
Asynchronously convert a video file from one format to another.
|
|
415
|
+
|
|
416
|
+
**Parameters:**
|
|
417
|
+
- `input_file` (str): Path to the input video file
|
|
418
|
+
- `output_file` (str): Path for the output video file
|
|
419
|
+
|
|
420
|
+
**Returns:**
|
|
421
|
+
- `bytes`: FFMPEG output (stdout or stderr) as bytes
|
|
422
|
+
|
|
423
|
+
**Raises:**
|
|
424
|
+
- `FileNotFoundError`: If input file doesn't exist
|
|
425
|
+
|
|
426
|
+
**Example:**
|
|
427
|
+
```python
|
|
428
|
+
output = await async_ffmpeg.reformat("input.mov", "output.mp4")
|
|
429
|
+
print(output.decode())
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
##### `async gif(input_file: str, output_file: str, fps: int = 10, scale: int = 320) -> bytes`
|
|
433
|
+
Asynchronously convert a video file to an optimized GIF.
|
|
434
|
+
|
|
435
|
+
**Parameters:**
|
|
436
|
+
- `input_file` (str): Path to the input video file
|
|
437
|
+
- `output_file` (str): Path for the output GIF file
|
|
438
|
+
- `fps` (int, optional): Frames per second for the GIF (default: 10)
|
|
439
|
+
- `scale` (int, optional): Width of the GIF in pixels, height is auto-scaled (default: 320)
|
|
440
|
+
|
|
441
|
+
**Returns:**
|
|
442
|
+
- `bytes`: FFMPEG output (stdout or stderr) as bytes
|
|
443
|
+
|
|
444
|
+
**Raises:**
|
|
445
|
+
- `FileNotFoundError`: If input file doesn't exist
|
|
446
|
+
- `RuntimeError`: If GIF file was not created successfully
|
|
447
|
+
|
|
448
|
+
**Example:**
|
|
449
|
+
```python
|
|
450
|
+
output = await async_ffmpeg.gif("video.mp4", "output.gif", fps=15, scale=640)
|
|
451
|
+
print(output.decode())
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
##### `async gifs(input_byte: bytes, fps: int = 10, scale: int = 320) -> bytes`
|
|
455
|
+
Asynchronously convert video data from bytes to an optimized GIF (in-memory processing).
|
|
456
|
+
|
|
457
|
+
**Parameters:**
|
|
458
|
+
- `input_byte` (bytes): Video data as bytes to convert to GIF
|
|
459
|
+
- `fps` (int, optional): Frames per second for the GIF (default: 10)
|
|
460
|
+
- `scale` (int, optional): Width of the GIF in pixels, height is auto-scaled (default: 320)
|
|
461
|
+
|
|
462
|
+
**Returns:**
|
|
463
|
+
- `bytes`: The generated GIF data as bytes
|
|
464
|
+
|
|
465
|
+
**Raises:**
|
|
466
|
+
- `RuntimeError`: If GIF conversion fails
|
|
467
|
+
|
|
468
|
+
**Example:**
|
|
469
|
+
```python
|
|
470
|
+
# Read video data from a file
|
|
471
|
+
with open("video.mp4", "rb") as f:
|
|
472
|
+
video_data = f.read()
|
|
473
|
+
|
|
474
|
+
# Convert to GIF in memory asynchronously
|
|
475
|
+
gif_data = await async_ffmpeg.gifs(video_data, fps=15, scale=480)
|
|
476
|
+
|
|
477
|
+
# Save the GIF
|
|
478
|
+
with open("output.gif", "wb") as f:
|
|
479
|
+
f.write(gif_data)
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
##### `async trim(input_file: str, output_file: str, start: float = 0, duration: float | None = None) -> bytes`
|
|
483
|
+
Asynchronously trim a video file.
|
|
484
|
+
|
|
485
|
+
**Parameters:**
|
|
486
|
+
- `input_file` (str): Path to the input video file
|
|
487
|
+
- `output_file` (str): Path for the output trimmed video
|
|
488
|
+
- `start` (float, optional): Start time in seconds (default: 0)
|
|
489
|
+
- `duration` (float | None, optional): Duration in seconds, or None for remaining video (default: None)
|
|
490
|
+
|
|
491
|
+
**Returns:**
|
|
492
|
+
- `bytes`: FFMPEG output (stdout or stderr) as bytes
|
|
493
|
+
|
|
494
|
+
**Raises:**
|
|
495
|
+
- `FileNotFoundError`: If input file doesn't exist
|
|
496
|
+
- `ValueError`: If start is negative or duration is non-positive
|
|
497
|
+
- `RuntimeError`: If trimmed video was not created successfully
|
|
498
|
+
|
|
499
|
+
**Example:**
|
|
500
|
+
```python
|
|
501
|
+
# Trim from 5 seconds to 10 seconds (5-second clip) asynchronously
|
|
502
|
+
output = await async_ffmpeg.trim("video.mp4", "clip.mp4", start=5, duration=5)
|
|
503
|
+
print(output.decode())
|
|
504
|
+
|
|
505
|
+
# Trim from 10 seconds to the end of video asynchronously
|
|
506
|
+
output = await async_ffmpeg.trim("video.mp4", "ending.mp4", start=10)
|
|
507
|
+
print(output.decode())
|
|
508
|
+
```
|
|
509
|
+
|
|
340
510
|
## Advanced Usage
|
|
341
511
|
|
|
342
512
|
### Custom FFMPEG Commands
|
|
@@ -400,6 +570,37 @@ if audio_info['streams']:
|
|
|
400
570
|
print(f"Sample rate: {audio_info['streams'][0]['sample_rate']} Hz")
|
|
401
571
|
```
|
|
402
572
|
|
|
573
|
+
### Asynchronous Batch Processing with AsyncFFMPEG
|
|
574
|
+
AsyncFFMPEG is ideal for batch processing and web applications where you need to maintain responsiveness:
|
|
575
|
+
|
|
576
|
+
```python
|
|
577
|
+
import asyncio
|
|
578
|
+
from ffmpeg_python_helper import AsyncFFMPEG
|
|
579
|
+
|
|
580
|
+
async def process_videos_concurrently():
|
|
581
|
+
async_ffmpeg = AsyncFFMPEG()
|
|
582
|
+
videos = ["video1.mp4", "video2.mp4", "video3.mp4"]
|
|
583
|
+
|
|
584
|
+
# Process multiple videos concurrently
|
|
585
|
+
tasks = []
|
|
586
|
+
for video in videos:
|
|
587
|
+
task = async_ffmpeg.gif(video, f"{video}_async.gif", fps=12, scale=400)
|
|
588
|
+
tasks.append(task)
|
|
589
|
+
|
|
590
|
+
# Wait for all async tasks to complete
|
|
591
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
592
|
+
|
|
593
|
+
# Handle results
|
|
594
|
+
for video, result in zip(videos, results):
|
|
595
|
+
if isinstance(result, Exception):
|
|
596
|
+
print(f"Failed to process {video}: {result}")
|
|
597
|
+
else:
|
|
598
|
+
print(f"Successfully processed {video}: {len(result)} bytes output")
|
|
599
|
+
|
|
600
|
+
# Run concurrent processing
|
|
601
|
+
asyncio.run(process_videos_concurrently())
|
|
602
|
+
```
|
|
603
|
+
|
|
403
604
|
### Error Handling
|
|
404
605
|
```python
|
|
405
606
|
from ffmpeg_python_helper import FFMPEG
|
|
@@ -500,9 +701,8 @@ MIT License - see LICENSE file for details
|
|
|
500
701
|
|
|
501
702
|
## Support
|
|
502
703
|
|
|
503
|
-
- **Issues**: [GitHub Issues](https://github.com/
|
|
504
|
-
- **
|
|
505
|
-
- **Email**: marjongodito@gmanmi.com
|
|
704
|
+
- **Issues**: [GitHub Issues](https://github.com/nojram00/python-ffmpeg/issues)
|
|
705
|
+
- **Email**: marjongodito.0505@gmail.com
|
|
506
706
|
|
|
507
707
|
## Acknowledgments
|
|
508
708
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ffmpeg-python-helper"
|
|
3
|
-
version = "
|
|
3
|
+
version = "4.1.0"
|
|
4
4
|
description = "A Python wrapper for FFMPEG that provides a simple, intuitive API for common video processing tasks including format conversion, GIF creation, and video trimming."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ffmpeg-python-helper"
|
|
3
|
-
version = "
|
|
3
|
+
version = "4.1.0"
|
|
4
4
|
description = "A Python wrapper for FFMPEG that provides a simple, intuitive API for common video processing tasks including format conversion, GIF creation, and video trimming."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
{ffmpeg_python_helper-3.2.0 → ffmpeg_python_helper-4.1.0}/src/ffmpeg_python_helper/__init__.py
RENAMED
|
@@ -38,11 +38,13 @@ For detailed API documentation, see:
|
|
|
38
38
|
from .ffmpeg_api import FFMPEG
|
|
39
39
|
from .pipe_helper import Pipe
|
|
40
40
|
from .ffprobe_api import FFProbe
|
|
41
|
+
from .async_ffmpeg_api import AsyncFFMPEG
|
|
41
42
|
|
|
42
43
|
__all__ = [
|
|
43
44
|
'FFMPEG',
|
|
44
45
|
'Pipe',
|
|
45
|
-
'FFProbe'
|
|
46
|
+
'FFProbe',
|
|
47
|
+
'AsyncFFMPEG'
|
|
46
48
|
]
|
|
47
49
|
|
|
48
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)
|
{ffmpeg_python_helper-3.2.0 → ffmpeg_python_helper-4.1.0}/src/ffmpeg_python_helper/ffmpeg_api.py
RENAMED
|
File without changes
|
{ffmpeg_python_helper-3.2.0 → ffmpeg_python_helper-4.1.0}/src/ffmpeg_python_helper/ffprobe_api.py
RENAMED
|
File without changes
|
{ffmpeg_python_helper-3.2.0 → ffmpeg_python_helper-4.1.0}/src/ffmpeg_python_helper/pipe_helper.py
RENAMED
|
File without changes
|