Skip to content

Commit 822a00c

Browse files
committed
Add play_stream.py example
1 parent f2f97b8 commit 822a00c

File tree

2 files changed

+132
-0
lines changed

2 files changed

+132
-0
lines changed

doc/examples.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,13 @@ Play a Very Long Sound File
1414

1515
.. literalinclude:: ../examples/play_long_file.py
1616

17+
Play a Web Stream
18+
-----------------
19+
20+
:download:`play_stream.py <../examples/play_stream.py>`
21+
22+
.. literalinclude:: ../examples/play_stream.py
23+
1724
Play a Sine Signal
1825
------------------
1926

examples/play_stream.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env python3
2+
"""Play a web stream.
3+
4+
ffmpeg-python (https://github.com/kkroening/ffmpeg-python) has to be installed.
5+
6+
If you don't know a stream URL, try http://icecast.spc.org:8000/longplayer
7+
(see https://longplayer.org/ for a description).
8+
9+
"""
10+
import argparse
11+
import queue
12+
import sys
13+
14+
import ffmpeg
15+
import sounddevice as sd
16+
17+
18+
def int_or_str(text):
19+
"""Helper function for argument parsing."""
20+
try:
21+
return int(text)
22+
except ValueError:
23+
return text
24+
25+
26+
parser = argparse.ArgumentParser(add_help=False)
27+
parser.add_argument(
28+
'-l', '--list-devices', action='store_true',
29+
help='show list of audio devices and exit')
30+
args, remaining = parser.parse_known_args()
31+
if args.list_devices:
32+
print(sd.query_devices())
33+
parser.exit(0)
34+
parser = argparse.ArgumentParser(
35+
description=__doc__,
36+
formatter_class=argparse.RawDescriptionHelpFormatter,
37+
parents=[parser])
38+
parser.add_argument(
39+
'url', metavar='URL',
40+
help='stream URL')
41+
parser.add_argument(
42+
'-d', '--device', type=int_or_str,
43+
help='output device (numeric ID or substring)')
44+
parser.add_argument(
45+
'-b', '--blocksize', type=int, default=1024,
46+
help='block size (default: %(default)s)')
47+
parser.add_argument(
48+
'-q', '--buffersize', type=int, default=20,
49+
help='number of blocks used for buffering (default: %(default)s)')
50+
args = parser.parse_args(remaining)
51+
if args.blocksize == 0:
52+
parser.error('blocksize must not be zero')
53+
if args.buffersize < 1:
54+
parser.error('buffersize must be at least 1')
55+
56+
q = queue.Queue(maxsize=args.buffersize)
57+
58+
print('Getting stream information ...')
59+
60+
try:
61+
info = ffmpeg.probe(args.url)
62+
except ffmpeg.Error as e:
63+
sys.stderr.buffer.write(e.stderr)
64+
parser.exit(e)
65+
66+
streams = info.get('streams', [])
67+
if len(streams) != 1:
68+
parser.exit('There must be exactly one stream available')
69+
70+
stream = streams[0]
71+
72+
if stream.get('codec_type') != 'audio':
73+
parser.exit('The stream must be an audio stream')
74+
75+
channels = stream['channels']
76+
samplerate = float(stream['sample_rate'])
77+
78+
79+
def callback(outdata, frames, time, status):
80+
assert frames == args.blocksize
81+
if status.output_underflow:
82+
print('Output underflow: increase blocksize?', file=sys.stderr)
83+
raise sd.CallbackAbort
84+
assert not status
85+
try:
86+
data = q.get_nowait()
87+
except queue.Empty:
88+
print('Buffer is empty: increase buffersize?', file=sys.stderr)
89+
raise sd.CallbackAbort
90+
assert len(data) == len(outdata)
91+
outdata[:] = data
92+
93+
94+
try:
95+
print('Opening stream ...')
96+
process = ffmpeg.input(
97+
args.url
98+
).output(
99+
'pipe:',
100+
format='f32le',
101+
acodec='pcm_f32le',
102+
ac=channels,
103+
ar=samplerate,
104+
loglevel='quiet',
105+
).run_async(pipe_stdout=True)
106+
stream = sd.RawOutputStream(
107+
samplerate=samplerate, blocksize=args.blocksize,
108+
device=args.device, channels=channels, dtype='float32',
109+
callback=callback)
110+
read_size = args.blocksize * channels * stream.samplesize
111+
print('Buffering ...')
112+
for _ in range(args.buffersize):
113+
q.put_nowait(process.stdout.read(read_size))
114+
print('Starting Playback ...')
115+
with stream:
116+
timeout = args.blocksize * args.buffersize / samplerate
117+
while True:
118+
q.put(process.stdout.read(read_size), timeout=timeout)
119+
except KeyboardInterrupt:
120+
parser.exit('\nInterrupted by user')
121+
except queue.Full:
122+
# A timeout occurred, i.e. there was an error in the callback
123+
parser.exit(1)
124+
except Exception as e:
125+
parser.exit(type(e).__name__ + ': ' + str(e))

0 commit comments

Comments
 (0)