3

I am currently trying to serve MP3 Files using Python. The problem is that I can only play the MP3 once. Afterwards media controls stop responding and I need to reload entirely the page to be able to listen again to the MP3. (tested in Chrome)

Problem: running the script below, and entering http://127.0.0.1/test.mp3 on my browser will return an MP3 files which can be replayed only if I refresh the page

Notes:

  • Saving the page as HTML and loading it directly with Chrome (without Python server) would make the problem disappear.

  • Serving the file with Apache would solve the problem, but this is overkilled: I want to make the script very easy to use and not require installing Apache.

Here is the code I use:

import string
import os
import urllib
import socket

# Setup web server import string,cgi,time
import string,cgi,time
from os import curdir, sep
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import hashlib

class MyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        try:
            # serve mp3 files
            if self.path.endswith(".mp3"):
                print curdir + sep + self.path
                f = open(curdir + sep + self.path, 'rb')
                st = os.fstat( f.fileno() )
                length = st.st_size
                data = f.read()
                md5 = hashlib.md5()
                md5.update(data)
                md5_key = self.headers.getheader('If-None-Match')
                if md5_key:
                  if md5_key[1:-1] == md5.hexdigest():
                    self.send_response(304)
                    self.send_header('ETag', '"{0}"'.format(md5.hexdigest()))
                    self.send_header('Keep-Alive', 'timeout=5, max=100')
                    self.end_headers()
                    return

                self.send_response(200)
                self.send_header('Content-type',    'audio/mpeg')
                self.send_header('Content-Length', length )
                self.send_header('ETag', '"{0}"'.format(md5.hexdigest()))
                self.send_header('Accept-Ranges', 'bytes')
                self.send_header('Last-Modified', time.strftime("%a %d %b %Y %H:%M:%S GMT",time.localtime(os.path.getmtime('test.mp3'))))
                self.end_headers()
                self.wfile.write(data)
                f.close()
            return
        except IOError:
           self.send_error(404,'File Not Found: %s' % self.path)

from SocketServer import ThreadingMixIn
class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    pass

if __name__ == "__main__":
    try:
       server = ThreadedHTTPServer(('', 80), MyHandler)
       print 'started httpserver...'
       server.serve_forever()
    except KeyboardInterrupt:
       print '^C received, shutting down server'
       server.socket.close()
Mapad
  • 8,407
  • 5
  • 41
  • 40

2 Answers2

3

BaseServer is single-threaded, you should use either ForkingMixIn or ThreadingMixIn to support multiple connections.

For example replace line:

server = HTTPServer(('', 80), MyHandler)

with

from SocketServer import ThreadingMixIn

class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    pass

server = ThreadedHTTPServer(('', 80), MyHandler)
vartec
  • 131,205
  • 36
  • 218
  • 244
  • @Mike: fair enough, but the problem with vanilla `HTTPServer` is it cannot even handle two connections at the same time. I.e. cannot serve MP3, until previous connection was closed. – vartec Apr 18 '11 at 12:52
  • thanks... actually that's why I deleted my comment after I wrote it. I realized why you posted that. – Mike Pennington Apr 18 '11 at 12:58
  • 1
    Still this is not what makes it not working. I've updated my script to use multithreaded server and I get the same problem. I don't need multiple threads to serve one file! – Mapad Apr 18 '11 at 16:09
2

EDIT: I wrote much of this before I realized Mapadd only planned to use this in a lab. WSGI probably is not required for his use case.

If you are willing to run this as a wsgi app (which I would recommend over vanilla CGI for any real scalability), you can use the script I have included below.

I took the liberty of modifying your source... this works with the assumptions above.. btw, you should spend some time checking that your html is reasonably compliant... this will help ensure that you get better cross-browser compatibility... the original didn't have <head> or <body> tags... mine (below) is strictly prototype html, and could be improved.

To run this, you just run the python executable in your shell and surf to the ipaddress of the machine on 8080. If you were doing this for a production website, we should be using lighttpd or apache for serving files, but since this is simply for lab use, the embedded wsgi reference server should be fine. Substitute the WSGIServer line at the bottom of the file if you want to run in apache or lighttpd.

Save as mp3.py

from webob import Request
import re
import os
import sys

####
#### Run with:
#### twistd -n web --port 8080 --wsgi mp3.mp3_app

_MP3DIV = """<div id="musicHere"></div>"""

_MP3EMBED = """<embed src="mp3/" loop="true" autoplay="false" width="145" height="60"></embed>"""

_HTML = '''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><html><head></head><body> Hello %s %s</body></html> ''' % (_MP3DIV, _MP3EMBED)

def mp3_html(environ, start_response):
    """This function will be mounted on "/" and refer the browser to the mp3 serving URL."""

    start_response('200 OK', [('Content-Type', 'text/html')])
    return [_HTML]

def mp3_serve(environ, start_response):
    """Serve the MP3, one chunk at a time with a generator"""
    file_path = "/file/path/to/test.mp3"
    mimetype = "application/x-mplayer2"
    size = os.path.getsize(file_path)
    headers = [
        ("Content-type", mimetype),
        ("Content-length", str(size)),
    ]
    start_response("200 OK", headers)
    return send_file(file_path, size)

def send_file(file_path, size):
    BLOCK_SIZE = 4096
    fh = open(file_path, 'r')
    while True:
        block = fh.read(BLOCK_SIZE)
        if not block:
            fh.close()
            break
        yield block

def _not_found(environ,start_response):
    """Called if no URL matches."""
    start_response('404 NOT FOUND', [('Content-Type', 'text/plain')])
    return ['Not Found']

def mp3_app(environ,start_response):
    """
    The main WSGI application. Dispatch the current request to
    the functions andd store the regular expression
    captures in the WSGI environment as  `mp3app.url_args` so that
    the functions from above can access the url placeholders.

    If nothing matches call the `not_found` function.
    """
    # map urls to functions
    urls = [
        (r'^$', mp3_html),
        (r'mp3/?$', mp3_serve),
    ]
    path = environ.get('PATH_INFO', '').lstrip('/')
    for regex, callback in urls:
        match = re.search(regex, path)
        if match is not None:
            # assign http environment variables...
            environ['mp3app.url_args'] = match.groups()
            return callback(environ, start_response)
    return _not_found(environ, start_response)

Run from the bash shell with: twistd -n web --port 8080 --wsgi mp3.mp3_app from the directory where you saved mp3.py (or just put mp3.py somewhere in $PYTHONPATH).

Now surf to the external ip (i.e. http://some.ip.local:8080/) and it will serve the mp3 directly.

I tried running your original app as it was posted, and could not get it to source the mp3, it barked at me with an error in linux...

Mike Pennington
  • 41,899
  • 19
  • 136
  • 174
  • I did test it again with Python 2.6 and Python 2.5 under windows and everything works. This is strange. I used HTML5 – Mapad Apr 14 '11 at 17:24
  • Are you saying the wsgi threw an error? If so, what was the error? The wsgi works for me under debian linux with `lighttpd` and python 2.5.2 – Mike Pennington Apr 14 '11 at 17:27
  • @Mapad, this works with any html... you just modify what the methods return... FYI, I set it up so you have to surf to `http://url.local.com/mp3/`, but you could change that directory to be anything. BTW, I have used flowplayer before... it is a great way to serve music or video if you're willing to require that clients have flash. – Mike Pennington Apr 14 '11 at 17:35
  • sorry for the confusion: I was talking about my script not yours. I've tested mine and it worked under windows. Though I got a problem under Python 2.5 on Mac OS. I will test your script as soon as I can, but this will require configuration of a server. I've already tested serving MP3 under Apache and it worked. So I imagine your script will work as well, but I wish I could use the Python built-in web server. This script is meant to be distributed to a lot of people in order to make them share MP3 files very easily to do audio evaluations. I wish I could reduce dependencies as much as possible – Mapad Apr 14 '11 at 17:37
  • @Mapad... when you say share audio files... do you mean serve them over the internet directly off their PC? If so, is this only within one single company, or could it go across corporate boundaries? Also keep in mind that many corporate IT depts lock down PCs very tightly, and that could limit your ability to run a local webserver on any random corporate laptop – Mike Pennington Apr 14 '11 at 17:41
  • The use case is the following: in a research lab I'd like that anyone can share on the intranet their Mp3 files for audio evaluations. Since everyone has python installed it would be simpler if there would be no additional dependencies to make it simple to install. – Mapad Apr 14 '11 at 17:44
  • @Mapad, I had a different deployment scenario in mind when I suggested using wsgi... If no one else provides a solution, I will take a look at this after work... you certainly could solve several deployment problems by wrapping this in a binary packager, but as you say this is a limited use case, so let's see if we can make the python HTTPServer work. – Mike Pennington Apr 14 '11 at 17:58
  • Thanks. The python HTTPServer must do something differently than other servers. But I really don't know what it is. I've been stuck with this for quite a long time. – Mapad Apr 14 '11 at 18:01
  • Ok, I'm in a meeting but bored so I found this... [wsgiref.simpleserver](http://docs.python.org/release/2.5.2/lib/module-wsgiref.simpleserver.html). I suspect we can make it work with this module... – Mike Pennington Apr 14 '11 at 18:11
  • Interesting. I suspect the same bugs in wsgiref since it is based on BaseHTTPServer, but it is worth trying – Mapad Apr 14 '11 at 18:26
  • @Mapad, keep in mind the error I saw came because the original script opened and closed a filehandle to push the mp3 to the client... the wgsi version I povided just serves it as a normal file and lets the http server open it. – Mike Pennington Apr 14 '11 at 18:38
  • @Mapad, thanks for your patience... try this out, I just modified to use the built-in http with wsgi. – Mike Pennington Apr 15 '11 at 15:35
  • @Mike: Thanks for the update, but the file test.mp3 is not served with your mechanism. Doing "http://127.0.0.1:8080/test.mp3" would not work, or simply display some html. The problem IS about the way MP3 files are served: WSGI is not going to change anything to my problem. I just need to reproduce the response done by Apache or other servers when they are requested MP3 files. – Mapad Apr 18 '11 at 10:59
  • @Mapadd, btw, you needed to surf to `http://127.0.0.1/mp3/` in the old version of the code, that is why you didn't see anything; I rewrote so you only have to surf to `http://127.0.0.1/` to get the mp3. Be sure to look at how I'm routing URLs with `urls` in `mp3_app()`; there is a lot of customization you can do like this. – Mike Pennington Apr 18 '11 at 12:04
  • Thanks for your support. But it's a fact: you don't serve the mp3 file when using the built-in python WSGI implementation! You rely on lighttpd. 127.0.0.1:8080/ doesn't give any mp3 back. It is probably working on your side because of caching. I also get the same behavior with the script I've provided in my question: if I serve 'test.mp3' with Apache, access it, stop the server, and start the Python built-in server everything works. If I empty the cache it stops working. Conclusion: Apache and lighttpd are doing something special I wish I knew about – Mapad Apr 18 '11 at 16:01