Streaming mp4 video (mongodb gridfsbucket) in nextJS application

394 views Asked by At

I'm trying to stream a mp4 video file in my nextJS application. The file is stored in a mongodb GridFSBucket.

So, for this I'm using getServerSideProps(). Calling the page in chrome browser shows up a video player, but the video isn't playing (duration 0:00). I do see multiple log messages warn - You should not access 'res' after getServerSideProps resolves.. But I don't think that is the issue as it is only a warning.

Maybe someone can see a fundamental error in my code as I don't have clue how to debug the issue without any error messages.

[hash].tsx

export const getServerSideProps: GetServerSideProps = async ({
    res,
    query: { hash }
}) => {        
    const database = await mongodb()
    const Videos = database.collection('videos')

    const { fileId } = await Videos.findOne({ uid: hash })

    const Files = new GridFSBucket(database)
    const id = new ObjectId(fileId)

    const file: GridFSFile = await new Promise((resolve, reject) => {
        Files.find({
            _id: id
        }).toArray((err, files) => {
            if (err) reject(err)
            resolve(files[0])
        })
    })
    const { contentType } = file || {}

    res.writeHead('Content-Type', contentType) // contentType is "video/mp4"
    Files.openDownloadStream(id)
    .on('data', (chunk) => {
        res.write(chunk)
    })
    .on('end', () => {
        res.end()
    })
    .on('error', (err) => {
        throw err
    })

    return {
        props: {}
    }
}
1

There are 1 answers

11
VonC On BEST ANSWER

You can see in "How to Integrate MongoDB Into Your Next.js App / Example 2: Next.js pages with MongoDB" (from Ado Kukic and Kushagra Kesav) an example of using the getServerSideProps function.
It works well for data that you want to render on the server side and then send to the client as HTML.

However, your original question concerns streaming media files (specifically, an MP4 video) stored in MongoDB GridFS, which is a different use case than the MongoDB guide is addressing.

  • you are trying to stream binary data (a video file), and not querying and rendering textual data (like movie titles and plot summaries)
  • you are manipulating video files, which are often large and should be streamed to support seeking, buffering, and other media controls. Not textual data, which is typically small and can be easily passed as a prop from getServerSideProps().

When dealing with streaming media files, you would also need to properly set up HTTP headers like Content-Type, and possibly support HTTP range requests. This is not covered by the typical use case of getServerSideProps().


If you want to serve a video file from MongoDB GridFS, you should do so by setting up a dedicated API endpoint for that.

Create a new API route, for example, pages/api/videos/[hash].ts:

import { MongoClient, ObjectId, GridFSBucket } from 'mongodb';

export default async function handler(req, res) {
  if (req.method !== 'GET') {
    res.status(405).end(); //Method Not Allowed
    return;
  }

  const hash = req.query.hash;

  try {
    const database = await mongodb();  // Replace with your mongodb connection logic
    const Videos = database.collection('videos');

    const { fileId } = await Videos.findOne({ uid: hash });

    const Files = new GridFSBucket(database);
    const id = new ObjectId(fileId);

    const file = await new Promise((resolve, reject) => {
      Files.find({ _id: id }).toArray((err, files) => {
        if (err) reject(err);
        resolve(files[0]);
      });
    });
    
    const { contentType } = file || {};

    res.writeHead(200, { 'Content-Type': contentType });

    Files.openDownloadStream(id)
      .on('data', (chunk) => {
        res.write(chunk);
      })
      .on('end', () => {
        res.end();
      })
      .on('error', (err) => {
        res.status(500).json({ error: err.message });
      });

  } catch (error) {
    res.status(500).json({ error: 'Internal Server Error' });
  }
}

Then, in your Next.js application, you can use the HTML5 <video> tag to embed the video:

<video controls width="250">
  <source src="/api/videos/your-hash-goes-here" type="video/mp4">
  Your browser does not support the video tag.
</video>

By following this approach, you allow the browser to handle the video streaming, and you isolate the media serving logic from your page data fetching logic, which is a better fit for Next.js.


I do see the player, which cannot open the stream. Timer is set to 0:00. Shouldn't http://localhost:3002/api/videos/RUaYN already stream the file? I also added some console.log() in the [hash].tsx, and I see, the file data is read correctly (return video/mp4 content type), it returns the chunks, and it returns something at the end. That's why I'm assuming the MongoDB connection and the [hash].tsx logic is correct...

If your API route is reading the file from MongoDB GridFS, streaming the chunks, and reaching the end without errors, but the video player still is not working, then the issue could be at the HTTP or client layer. So make sure the HTTP response headers are set correctly. Specifically, verify that Content-Type is set to video/mp4. If you are using a different MIME type for your video, adjust the header and the <source> tag accordingly.

And check at the browser console for errors or warnings. In particular, use the Network tab of your browser's developer tools to inspect the HTTP request and response. Check the status code, headers, and size of the content. Make sure the request is actually hitting your /api/videos/[hash] endpoint.
Verify that the video is in the correct format (MP4) and codec that the HTML5 video player can understand. A mismatch here can cause playback to fail even if the server-side logic is correct.

In your server-side code, add error handling and logging to catch any exceptions or errors that might occur. That will give you more information about what could be going wrong. Currently, you throw an error on a stream error event but do not handle it.

On the client side, add event listeners for error, loadeddata, canplay, etc., on the <video> element to track its state and catch potential errors. For example:

<video controls width="600"
    onError={(e) => {
        console.error("Video error:", e);
    }}
    onLoadedData={() => {
        console.log("Video data loaded");
    }}>
    <source src={`/api/videos/${videoHash}`} type="video/mp4" />
    Your browser does not support the video tag.
</video>

That gave me the hint... I tried another example mp4 file - which worked. So there must be a problem with the video file itself. When I take a look at the file properties: The file which is working has codecs MPEG-4 AAC, H.264, my file has MPEG-4 Video.

OK, the issue seems to lie in the video codecs. Codecs are algorithms used to encode and decode (or compress and decompress) digital data streams. In the context of video files, not all codecs are supported universally across all browsers and media players.

The H.264 (often packaged within MP4 files) and AAC codecs are widely supported and are standard for HTML5 video playback. On the other hand, MPEG-4 Video may not be as universally supported, leading to the issue you are experiencing.

You might need to re-encode: Use a video conversion tool to re-encode your video with H.264 video codec and AAC audio codec. Tools like FFmpeg can be quite handy for this purpose.

ffmpeg -i input.mp4 -c:v libx264 -c:a aac -strict experimental output.mp4

You can also use media information tools like ffprobe to inspect the details of your video file. It will give you insights into what codecs are being used, the video dimensions, etc. You can compare this with the file that works to understand the differences.


Is it possible to read the file from MongoDB bucket (stream), re-encode it with ffmpeg, and store it back to the db in Node.js?

Because I would like to create a function to re-encode failing video files in the application itself.

It should be possible to read a video file from MongoDB's GridFS, re-encode it using FFmpeg, and then store it back in GridFS, all within a Node.js application.

You would typically use the gridfs-stream package for MongoDB and the fluent-ffmpeg package for FFmpeg in Node.js.

const mongodb = require('mongodb');
const { GridFSBucket } = require('mongodb');
const ffmpeg = require('fluent-ffmpeg');

// Establish MongoDB connection
mongodb.MongoClient.connect('mongodb://localhost:27017/testDB', async function(err, client) {
  if (err) {
    console.error(err);
    return;
  }

  const db = client.db('testDB');

  // Initialize GridFS
  const bucket = new GridFSBucket(db, {
    bucketName: 'videos'
  });

  const fileId = new mongodb.ObjectId('your-object-id-here'); // Replace with your object ID
  const fileName =  are-encoded-video.mp4'; // New name for the re-encoded file

  // Read the existing video stream from GridFS
  const readStream = bucket.openDownloadStream(fileId);

  // Create write stream to GridFS for the re-encoded video
  const uploadStream = bucket.openUploadStream(fileName);
  const newFileId = uploadStream.id;

  // Set up FFmpeg to read from the GridFS readStream
  ffmpeg(readStream)
    .outputFormat('mp4') // You can specify the output format
    .videoCodec('libx264')
    .audioCodec('aac')
    .on('end', () => {
      console.log( are-encoding succeeded');
      // Optionally, delete the old file from GridFS
      bucket.delete(fileId, (err) => {
        if (err) {
          console.error('Error deleting old file:', err);
        } else {
          console.log('Old file deleted');
        }
      });
    })
    .on('error', (err) => {
      console.error( are-encoding failed:', err);
    })
    .pipe(uploadStream); // Pipe the re-encoded video back to GridFS
});

Do Replace:

  • 'your-object-id-here' with the Object ID of the video you want to re-encode.
  • are-encoded-video.mp4' with whatever new name you want to give to the re-encoded file.
  • 'videos' with the name of your GridFS bucket.
  • 'testDB' with the name of your MongoDB database.

That example has basic error handling. In a production scenario, you would likely want to add more robust error handling. You will need to have FFmpeg installed on the machine where this code will run.

By incorporating this function into your application, you can programmatically re-encode videos that fail to play due to codec issues.


Unfortunately, I do get the error re-encoding failed: Error: ffmpeg exited with code 1: Conversion failed!. How do I find out why it failed? This error message is not very helpful.

The error message you received, Error: ffmpeg exited with code 1: Conversion failed!, is indeed quite generic and does not provide much information about what went wrong during the re-encoding process. To get more information, you can capture the standard error (stderr) stream from FFmpeg. The fluent-ffmpeg package allows you to do this through its event handlers.

Here is a modified code snippet that captures stderr output:

ffmpeg(readStream)
  .outputFormat('mp4')
  .videoCodec('libx264')
  .audioCodec('aac')
  .on('stderr', function(stderrLine) {
    console.log('Stderr output: ' + stderrLine);
  })
  .on('end', function() {
    console.log( are-encoding succeeded');
  })
  .on('error', function(err, stdout, stderr) {
    console.error( are-encoding failed:', err);
    console.error('ffmpeg stdout:', stdout);
    console.error('ffmpeg stderr:', stderr);
  })
  .pipe(uploadStream);

The .on('stderr', function(stderrLine) {...}) event handler should capture each line of stderr output.
The .on('error', function(err, stdout, stderr) {...}) event handler captures additional information if an error occurs during the re-encoding process.

These outputs may provide you with more information about why FFmpeg is failing to re-encode the video, which could range from unsupported codecs to incorrect FFmpeg settings.

By reviewing the stderr logs, you should be able to get a better understanding of what is causing the conversion to fail. With that information, you may be able to adjust your FFmpeg options accordingly to resolve the issue.


This is the error I get: [mp4 @ 0x12a7041d0] muxer does not support non seekable output [out#0/mp4 @ 0x600003bb4000] Could not write header (incorrect codec parameters ?): Invalid argument

FFmpeg is having issues with the output format or codec settings while trying to write to a non-seekable stream. GridFS streams, in their nature, are non-seekable.

Try and first write the re-encoded video to an intermediate temporary file, and then upload that file to GridFS.

ffmpeg(readStream)
  .outputFormat('mp4')
  .videoCodec('libx264')
  .audioCodec('aac')
  .on('end', function() {
    // Read the intermediate file and upload it to GridFS here.
  })
  .on('error', function(err) {
    console.error('Re-encoding failed:', err);
  })
  .save('intermediate-file.mp4');  // Save to a temporary file first

After saving the file, you can then read this intermediate file and upload it back to GridFS. (and delete the intermediate file once the upload is complete).