Trying to retrieve an mp3 file stored in AWS S3 and load it into my React client as a Blob...it's not working

Viewed 2773

I have a react web app that allows users to record mp3 files in the browser. These mp3 files are saved in an AWS S3 bucket and can be retrieved and loaded back into the react app during the user's next session.

Saving the file works just fine, but when I try to retrieve the file with getObject() and try to create an mp3 blob on the client-side, I get a small, unusable blob:

enter image description here

Here's the journey the recorded mp3 file goes on:

1) Saving to S3

In my Express/Node server, I receive the uploaded mp3 file and save to the S3 bucket:

//SAVE THE COMPLETED AUDIO TO S3
router.post("/", [auth, upload.array('audio', 12)], async (req, res) => {

    try {
        //get file
        const audioFile = req.files[0];

        //create object key
        const userId = req.user;
        const projectId = req.cookies.currentProject;
        const { sectionId } = req.body;
        const key = `${userId}/${projectId}/${sectionId}.mp3`;

        const fileStream = fs.createReadStream(audioFile.path)

        const uploadParams = {
            Bucket: bucketName,
            Body: fileStream,
            Key: key,
            ContentType: "audio/mp3" 
        }

        const result = await s3.upload(uploadParams).promise();
        
        res.send(result.key);

    } catch (error) {
        console.error(error);
        res.status(500).send();
    }
});

As far as I know, there are no problems at this stage. The file ends up in my S3 bucket with "type: mp3" and "Content-Type: audio/mp3".

2) Loading file from S3 Bucket

When the react app is loaded up, an HTTP GET Request is made in my Express/Node server to retrieve the mp3 file from the S3 Bucket

//LOAD A FILE FROM S3
router.get("/:sectionId", auth, async(req, res) => {    

    try {
        //create key from user/project/section IDs
        const sectionId = req.params.sectionId;
        const userId = req.user;
        const projectId = req.cookies.currentProject;
        const key = `${userId}/${projectId}/${sectionId}.mp3`;

        const downloadParams = {
            Key: key,
            Bucket: bucketName
        }

        s3.getObject(downloadParams, function (error, data) {
            if (error) {
                console.error(error);
                res.status(500).send();
            }
            res.send(data);
        });

    } catch (error) {
        console.error(error);
        res.status(500).send();
    }
    
});

The "data" returned here is as such: enter image description here

3) Making a Blob URL on the client

Finally, in the React client, I try to create an 'audio/mp3' blob from the returned array buffer

const loadAudio = async () => {
        
        const res = await api.loadAudio(activeSection.sectionId);
        
        const blob = new Blob([res.data.Body], {type: 'audio/mp3' });
        
        const url = URL.createObjectURL(blob);
                
        globalDispatch({ type: "setFullAudioURL", payload: url });
}

The created blob is severely undersized and appears to be completely unusable. Downloading the file results in a 'Failed - No file' error. enter image description here

I've been stuck on this for a couple of days now with no luck. I would seriously appreciate any advice you can give!

Thanks

EDIT 1

Just some additional info here: in the upload parameters, I set the Content-Type as audio/mp3 explicitly. This is because when not set, the Content-Type defaults to 'application/octet-stream'. Either way, I encounter the same issue with the same result.

EDIT 2 At the request of a commenter, here is the res.data available on the client-side after the call is complete:

enter image description here

3 Answers

Based on the output of res.data on the client, there are a couple of things that you'd need to do:

  • Replace uses of res.data.Body with res.data.Body.data (as the actual data array is in the data attribute of res.data.Body)
  • Pass a Uint8Array to the Blob constructor, as the existing array is of a larger type, which will create an invalid blob

Putting that together, you would end up replacing:

const blob = new Blob([res.data.Body], {type: 'audio/mp3' });

with:

const blob = new Blob([new Uint8Array(res.data.Body.data)], {type: 'audio/mp3' });

Having said all that, the underlying issue is that the NodeJS server is sending the content over as a JSON encoded serialisation of the response from S3, which is likely overkill for what you are doing. Instead, you can send the Buffer across directly, which would involve, on the server side, replacing:

res.send(data);

with:

res.set('Content-Type', 'audio/mp3');
res.send(data.Body);

and on the client side (likely in the loadAudio method) processing the response as a blob instead of JSON. If using the Fetch API then it could be as simple as:

const blob = await fetch(<URL>).then(x => x.blob());

Your server side code seems alright to me. I'm not super clear about the client-side approach. Do you load this into the blob into the HTML5 Audio player.

I have a few approaches, assuming you're trying to load this into an audio tag in the UI.

<audio controls src="data:audio/mpeg;base64,blahblahblah or html src" />

Assuming that the file you had uploaded to S3 is valid here are two approaches:

  1. Return the data as a base64 string instead of as a buffer directly from S3. You can do this in your server side by returning as
 const base64MP3 = data.Body.toString('base64');

You can then pass this in to the MP3 player in the src property and it will play the audio. Prefix it with data:audio/mpeg;base64

  1. Instead of returning the entire MP3 file, have your sectionID method return a presigned S3 URL. Essentially, this is a direct link to the object in S3 that is authorized for say 5 minutes.

Then you should be able to use this URL directly to stream the audio and set it as the src. Keep in mind that it will expire.

 const url = s3.getSignedUrl('getObject', {
        Bucket: myBucket,
        Key: myKey,
        Expires: signedUrlExpireSeconds
   });

You stated: "The created blob is severely undersized and appears to be completely unusable"

This appears to me that you have an encoding issue. Once you read the MP3 from the Amazon S3 bucket, you need to encode it properly so it functions in a web page.

I did a similar multimedia use case that involved MP4 and a Java app. That is, i wanted a MP4 obtained from a bucket to play in the web page - as shown in this example web app.

enter image description here

Once I read the byte stream from the S3 bucket, I had to encode it so it would play in a HTML Video tag. Here is a good reference to properly encode a MP3 file.

Related