Search code examples
mp3chunked-encodinglamechunkedtransfer-encoding

Transfer-encoding: chunked and MP3/Lame


I have a PHP webservice that returns an mp3 HTTP response. It works, but when I turn on Chrome's network throttling in DevTools, it returns only part of the response:

        $stream_start1 = Psr7\stream_for(fopen('./sounds/ping.mp3', 'r'));
        $stream_start2 = Psr7\stream_for(fopen('./sounds/ping.mp3', 'r'));
        $stream_start3 = Psr7\stream_for(fopen('./sounds/ping.mp3', 'r'));
        $stream_start4 = Psr7\stream_for(fopen('./sounds/ping.mp3', 'r'));
        $stream_start5 = Psr7\stream_for(fopen('./sounds/ping.mp3', 'r'));
        $stream_start6 = Psr7\stream_for(fopen('./sounds/ping.mp3', 'r'));
        $stream_start7 = Psr7\stream_for(fopen('./sounds/ping.mp3', 'r'));
        $stream_start8 = Psr7\stream_for(fopen('./sounds/ping.mp3', 'r'));
        $stream_start9 = Psr7\stream_for(fopen('./sounds/ping.mp3', 'r'));
        $stream_start10 = Psr7\stream_for(fopen('./sounds/ping.mp3', 'r'));
        $stream_start11 = Psr7\stream_for(fopen('./sounds/ping.mp3', 'r'));
        $stream_start12 = Psr7\stream_for(fopen('./sounds/ping.mp3', 'r'));
        $stream_start13 = Psr7\stream_for(fopen('./sounds/ping.mp3', 'r'));

        return new Psr7\AppendStream([$stream_start1 , $stream_start2 , $stream_start3 , $stream_start4 , $stream_start5 , $stream_start6 , $stream_start7 , $stream_start8 , $stream_start9 , $stream_start10, $stream_start11, $stream_start12, $stream_start13]);

With the above code, with dev throttling on, I am getting back 7 pings instead of 13.

In reality the real code is getting a stream from a 3rd party service and and sandwiching it between two files:

 return new Psr7\AppendStream([file1Stream, 3rdPartyStream, file2Stream])

So, I don't necessarily know the length in order to set Content-Length, and so it's using Transfer-Encoding: chunked.

When I look in dev tools at the shortened responses of the requests, I consistently see them ending around what looks like some LAME metadata or added silence:

e¨Deš•†š¹‡ÌC`̹3†'2 CBR9S¨Âöe­i´g¡ÿ–W©^¥ÔzWI}I8¸¶vv,¸¼Ù£J*ÙÙû±W•'X&+X±£@È ·bbÂìýbŸâÿâŸëLAME3.100UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUÿóbÄ”AIÖtUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU

What I'm wondering is if it's possible that the browser is confusing some of this LAME info (or something else) for the end of the chunk sequence due to delays in receiving chunks. Is it possible that mixed up in the audio binary is something resembling an "empty chunk" indicating a last chunk? Is there a way to "escape" or encode the data to avoid potential sequences like this? Through compression, maybe?

My other option is to read the 3rd party stream (in the middle of the sandwich) all the way into memory before forwarding it. That way I can calculate the length and set the Content-Length, so that's plan B.

UPDATE: I have tried setting Content-Length on my little 13-file example there but I still get 7 pings as a response. The length I've calculated with:

$length = strlen($stream_start0->getContents()) * 13 

Which is 122252. But the file content that is returned is around 65529 bytes long (hex editor says 65536...?). Anyway, it's too short. Also, the Transfer-Encoding header is gone, which means I guess it's not being chunked anymore?

UPDATE2: It's also possible the problem is that I am concatenating mp3 files raw. I think technically things get shady when you do that, especially if the audio contains ID3 tags and such, though in the ping files used above, there shouldn't be any ID3 tags. However, in our stg environment where I observe this issue w/o dev tools, the cutoffs don't always happen on the breaks between files.


Solution

  • So all of the above was a red herring. It turned out the problem was I copied some blog post with JavaScript code that was calling response.getReader().read() only once, and thus only getting the first chunk of the response.

    Apparently it's meant to be invoked in a loop until the end of the stream is reached like most stream APIs, but I missed it.

    The solution was using response.blob(), which reads to the end of the stream and creates the Blob I need in one go.

    I thought I had validated everything I copied, but I guess not. But I learned a few things, so there's that.

    Docs on reader.read()
    Docs on response.blob()