I'm streaming audio (Internet radio) from an HTTP(S) endpoint, and wanting to capture 30 seconds of it (it's streaming in real time, and will continue sending data for as long as the connection is open).
import httpclient
import asyncdispatch
from portaudio as p import nil
const
URL = "http://icecast.maxxwave.co.uk/lcr_aac"
TIMEOUT_SECONDS = 30
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36"
OUTPUT_AAC = "output.aac"
proc check(err: p.TError|p.TErrorCode) =
if cast[p.TErrorCode](err) != p.NoError:
raise newException(Exception, $p.GetErrorText(err))
proc requestAACWithTimeout(client: AsyncHttpClient, url: string, timeout: int = TIMEOUT_SECONDS) {.async.} =
let resp = await client.get(url)
let outFile = open(OUTPUT_AAC, fmWrite)
defer:
outFile.close()
client.close()
let timeout = sleepAsync(timeout * 1_000)
while not timeout.finished():
let (hasData, bodyChunk) = await resp.bodyStream.read()
if hasData:
outFile.write(bodyChunk)
resp.bodyStream.complete()
# Any lingering data
let bodyChunk = await resp.bodyStream.readAll()
if bodyChunk.len > 0:
outFile.write(bodyChunk)
proc main() =
check p.Initialize()
defer:
check p.Terminate()
let client = newAsyncHttpClient(userAgent = USER_AGENT)
try:
waitFor client.requestAACWithTimeout(URL)
except Exception as e:
echo "Error getting request"
raise e
echo "Ran successfully"
# TODO: Convert from AAC to WAV
when isMainModule:
main()
The above works, but is a little awkward - I'm reading from the AsyncResponse stream until a timeout future completes, and then writing any remaining elements to the file. I'm wondering if there's a better of doing this. (It would also be nice if I could write the end of the AAC stream cleanly (mplayer and ffmpeg complain about 'missing end element'), but that's not essential for my purposes - I just want to sample a small snippet of audio regularly).
In reading the HTTP Client timeouts section, the timeout seems to be for connection and so:
I can't really think of a better way to do this, the way your doing it looks good.
I'm not sure why you're closing the bodyStream though, I'm surprised that doesn't cause issues. The HTTP client is only supposed to close it.
Btw, beware of 0.19.0, you might run into a regression if you use it: https://github.com/nim-lang/Nim/issues/8994
Also, in the future I will likely change the way future streams work. I'm still not sure how exactly though, this issue tracks that: https://github.com/nim-lang/Nim/issues/7126. Feedback welcome.
I probably am getting corrupted data at the end of stream, and just not noticing because ffmpeg does a good enough job at handling bad inputs. :( I'll take a closer look (and not close bodyStream unnecessarily).
That's good information to know about issue 8994. I'll take a look at issue 7126 - that's also good to know for this use case.