Skip to content

Commit e2d62b5

Browse files
committed
probe: functionalize
1 parent 94665fc commit e2d62b5

File tree

5 files changed

+23
-47
lines changed

5 files changed

+23
-47
lines changed

README.md

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,18 @@ Examples of Python asyncio.subprocess with FFmpeg and also traditional synchrono
99
Both synchronous (traditional for loop) and asynchronous pipeline are demonstrated.
1010
They call FFprobe executable to return JSON formatted metadata.
1111

12-
### probe_sync.py
13-
14-
retrieve file metadata synchronously.
15-
16-
### probe_coroutine.py
17-
18-
retrieve file metadata in an asynchronous pipeline (asyncio generator) using Python `asyncio` coroutine event loop.
12+
* probe_sync.py: retrieve file metadata synchronously.
13+
* probe_coroutine.py: retrieve file metadata in asynchronous pipeline or list using Python `asyncio` coroutines.
1914

2015
## FFplay
2116

22-
Test ingasynchronous techniques with video playback makes some effects obvious.
17+
Testing asynchronous techniques with video playback makes some effects obvious.
2318
The FFplay asyncio example is more advanced than the FFprobe example.
2419
In the FFprobe example, the lazy asyncio generator produces metadata concurrently as fast as it's requested.
2520
There is no resource throttling in the FFprobe example, so the CPU could become overwhelmed with context switching.
2621

2722
The FFplay example in contrast is an example of a task using resource throttling via asyncio.Queue.
28-
The queueing could also be implemented for FFprobe style task if desired.
23+
The queuing could also be implemented for FFprobe style task if desired.
2924
However, the rationale employed is that the FFprobe task is overall lightweight, and thus other parts of the pipeline inherently limit resource utilization.
3025
If the FFprobe task was in an asyncio.gather() algorithm, resource utilization could get too high.
3126
Thus we have a "win-win" by using asyncio generator for FFprobe--the throttling comes implicitly from other parts of the pipeline.

play_coroutine.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010
p = ArgumentParser(description="Plays media files asynchronously with FFplay")
1111
p.add_argument("path", help="directory where media files are kept")
1212
p.add_argument(
13-
"-suffix",
14-
help="file suffixes of desired media file types",
15-
nargs="+",
13+
"-suffix", help="file suffixes of desired media file types", nargs="+",
1614
)
1715
P = p.parse_args()
1816

probe_coroutine.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
#!/usr/bin/env python
1+
#!/usr/bin/env python3
22
"""
3-
3x to 4x speedup over ffprobe_sync on Linux
4-
10% speedup on Windows over ffprobe_sync, even with virus monitor disabled.
3+
~ 4x speedup over ffprobe_sync
54
"""
65
import time
76
from argparse import ArgumentParser
@@ -14,19 +13,16 @@
1413
p = ArgumentParser(description="Get media metadata asynchronously with FFprobe")
1514
p.add_argument("path", help="directory where media files are kept")
1615
p.add_argument(
17-
"-suffix",
18-
help="file suffixes of desired media file types",
19-
nargs="+",
20-
default=[".mp3", ".mp4", ".avi", ".ogv", ".ogg"],
16+
"-suffix", help="file suffixes of desired media file types", nargs="+",
2117
)
2218
P = p.parse_args()
2319

2420
tic = time.monotonic()
25-
# emits results as each future is completed
21+
# %% emits results as each future is completed
2622
runner(probe.get_meta, P.path, P.suffix)
27-
print("ffprobe asyncio.as_completed: {:.3f} seconds".format(time.monotonic() - tic))
23+
print(f"ffprobe asyncio.as_completed: {time.monotonic() - tic:.3f} seconds")
2824

29-
# approximately same wallclock time, but only gives results when all futures complete
25+
# %% approximately same wallclock time, but only gives results when all futures complete
3026
tic = time.monotonic()
3127
runner(probe.get_meta_gather, P.path, P.suffix)
32-
print("ffprobe asyncio.gather: {:.3f} seconds".format(time.monotonic() - tic))
28+
print(f"ffprobe asyncio.gather: {time.monotonic() - tic:.3f} seconds")

probe_sync.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,22 @@
1-
#!/usr/bin/env python
1+
#!/usr/bin/env python3
22
import time
3-
from pathlib import Path
43
from argparse import ArgumentParser
4+
55
import asyncioffmpeg.ffprobe as probe
6+
from asyncioffmpeg import get_videos
67

78

89
if __name__ == "__main__":
910
p = ArgumentParser(description="Get media metadata synchronously with FFprobe")
1011
p.add_argument("path", help="directory where media files are kept")
1112
p.add_argument(
12-
"-suffix",
13-
help="file suffixes of desired media file types",
14-
nargs="+",
15-
default=[".mp3", ".mp4", ".avi", ".ogv", ".ogg"],
13+
"-suffix", help="file suffixes of desired media file types", nargs="+",
1614
)
1715
P = p.parse_args()
1816

1917
tic = time.monotonic()
20-
path = Path(P.path).expanduser()
21-
if not path.is_dir():
22-
raise FileNotFoundError(f"{path} is not a directory")
23-
24-
flist = (f for f in path.iterdir() if f.is_file() and f.suffix in P.suffix)
25-
26-
for f in flist:
18+
for f in get_videos(P.path, P.suffix):
2719
meta = probe.ffprobe_sync(f)
2820
probe.print_meta(meta)
2921

30-
print("ffprobe sync: {:.3f} seconds".format(time.monotonic() - tic))
22+
print(f"ffprobe sync: {time.monotonic() - tic:.3f} seconds")

src/asyncioffmpeg/ffprobe.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,13 @@
1010
from pathlib import Path
1111
import shutil
1212

13+
from . import get_videos
14+
1315
FFPROBE = shutil.which("ffprobe")
1416
if not FFPROBE:
1517
raise ImportError("FFPROBE not found")
1618

1719

18-
def files2futures(path: Path, suffix: str) -> list:
19-
path = Path(path).expanduser()
20-
if not path.is_dir():
21-
raise NotADirectoryError(path)
22-
return [ffprobe(f) for f in path.iterdir() if f.is_file() and f.suffix in suffix]
23-
24-
2520
def print_meta(meta: typing.Dict[str, typing.Any]):
2621
fn = Path(meta["format"]["filename"])
2722
dur = float(meta["streams"][0]["duration"])
@@ -30,7 +25,7 @@ def print_meta(meta: typing.Dict[str, typing.Any]):
3025

3126
async def get_meta_gather(path: Path, suffix: str) -> typing.List[typing.Dict[str, typing.Any]]:
3227
""" for comparison with asyncio.as_completed"""
33-
futures = files2futures(path, suffix)
28+
futures = [ffprobe(f) for f in get_videos(path, suffix)]
3429
metas = await asyncio.gather(*futures)
3530
for meta in metas:
3631
print_meta(meta)
@@ -39,7 +34,7 @@ async def get_meta_gather(path: Path, suffix: str) -> typing.List[typing.Dict[st
3934

4035

4136
async def get_meta(path: Path, suffix: str) -> typing.List[typing.Dict[str, typing.Any]]:
42-
futures = files2futures(path, suffix)
37+
futures = [ffprobe(f) for f in get_videos(path, suffix)]
4338
metas = []
4439
for file in asyncio.as_completed(futures):
4540
meta = await file
@@ -83,7 +78,7 @@ def ffprobe_sync(file: Path) -> typing.Dict[str, typing.Any]:
8378
"-show_format",
8479
str(file),
8580
],
86-
universal_newlines=True,
81+
text=True,
8782
)
8883

8984
return json.loads(meta)

0 commit comments

Comments
 (0)