ffmpeg-python-helper 3.2.0__tar.gz → 4.0.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ffmpeg-python-helper
3
- Version: 3.2.0
3
+ Version: 4.0.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
@@ -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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ffmpeg-python-helper"
3
- version = "3.2.0"
3
+ version = "4.0.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.2.0"
3
+ version = "4.0.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 = [
@@ -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)