Search code examples
pythonomxplayer

Playing a MP3 from a Python process's memory via omxplayer, without writing to disk


The following code receives an MP3, writes it to disk and plays it using OMXPlayer. I want to eliminate the need to write the MP3 to disk before playing it.

song = response.content
file = open("temp.mp3", "wb")
file.write(song)
file.close()
response.close()
play_song_subprocess = subprocess.call(['omxplayer', '-o', 'local', '--vol', '-500', 'temp.mp3'])

How can I eliminate the file.write()? I'm looking to do something like this:

song = response.content
play_song_subprocess = subprocess.call(['omxplayer', '-o', 'local', '--vol', '-500', song])

But this causes the following error: embedded null byte


Solution

  • Backstory For Readers

    Established in chat and comments:

    • cat temp.mp3 | omxplayer -o local --vol -500 /dev/stdin causes a segfault.
    • omxplayer -o local --vol -500 /dev/fd/3 3< <(cat temp.mp3) works correctly.

    Thus, we can pass a MP3's data in... but not on stdin (which omxplayer uses for controls: pausing, early exiting, etc).


    Approach 1: Using A Shell For File Descriptor Wrangling

    This is equivalent to "Approach 3", but instead of using very new and modern Python functionality to do the FD wrangling in-process, it launches a copy of /bin/sh to do the work (and consequently will work with much older Python releases).

    play_from_stdin_sh = '''
    exec 3<&0                                     # copy stdin to FD 3
    exec </dev/tty || exec </dev/null             # make stdin now be /dev/tty or /dev/null
    exec omxplayer -o local --vol -500 /dev/fd/3  # play data from FD 3
    '''
    p = subprocess.Popen(['sh', '-c', play_from_stdin_sh], stdin=subprocess.POPEN)
    p.communicate(song)  # passes data in "song" as stdin to the copy of sh
    

    Because omxplayer expects to use stdin to get instructions from its user, we need to use a different file descriptor for passing in its contents. Thus, while we have the Python interpreter pass content on stdin, we then have a shell copy stdin to FD 3 and replace the original stdin with a handle or either /dev/tty or /dev/null before invoking omxplayer.


    Approach 2: Using A Named Pipe

    There's a little bit of a question as to whether this is cheating on the "no writing to disk" constraint. It doesn't write any of the MP3 data to disk, but it does create a filesystem object that both processes can open as a way to connect to each other, even though the data written to that object flows directly between the processes, without being written to disk.

    import tempfile, os, os.path, shutil, subprocess
    
    fifo_dir = None
    try:
      fifo_dir = tempfile.mkdtemp('mp3-fifodir')
      fifo_name = os.path.join(fifo_dir, 'fifo.mp3')
      os.mkfifo(fifo_name)
      # now, we start omxplayer, and tell it to read from the FIFO
      # as long as it opens it in read mode, it should just hang until something opens
      # ...the write side of the FIFO, writes content to it, and then closes it.
      p = subprocess.Popen(['omxplayer', '-o', 'local', '--vol', '-500', fifo_name])
      # this doesn't actually write content to a file on disk! instead, it's written directly
      # ...to the omxplayer process's handle on the other side of the FIFO.
      fifo_fd = open(fifo_name, 'w')
      fifo_fd.write(song)
      fifo_fd.close()
      p.wait()
    finally:
      shutil.rmtree(fifo_dir)
    

    Approach 3: Using A preexec_fn In Python

    We can implement the file descriptor wrangling that Approach 1 used a shell for in native Python using the Popen object's preexec_fn argument. Consider:

    import os, subprocess
    
    def move_stdin():
      os.dup2(0, 3)                       # copy our stdin -- FD 0 -- to FD 3
      try:
        newstdin = open('/dev/tty', 'r')  # open /dev/tty...
        os.dup2(newstdin.fileno(), 0)     # ...and attach it to FD 0.
      except IOError:
        newstdin = open('/dev/null', 'r') # Couldn't do that? Open /dev/null...
        os.dup2(newstdin.fileno(), 0)     # ...and attach it to FD 0.
    
    p = subprocess.Popen(['omxplayer', '-o', 'local', '--vol', '-500', '/dev/fd/3'],
                         stdin=subprocess.PIPE, preexec_fn=move_stdin, pass_fds=[0,1,2,3])
    p.communicate(song)