My goal is to extract a single frame from a large video without downloading the entire video file.
In practice, the video file is huge (4GB) and I don’t want to waste that much bandwidth if I am interested in only a 44kb single frame.
The program should take two inputs:
- An URL linking to an a fragmented mp4 file.
- A timestamp between 0 and the end of the video duration (for example, 12.45 seconds).
The program should output the frame of the video as .png
at that particular timestamp.
Here is my attempt with Node:
import path from "path";
// import ffmpegPath from "ffmpeg-static";
import axios from "axios";
import ffmpeg from "fluent-ffmpeg";
import fs from "fs";
// Set FFmpeg path
// ffmpeg.setFfmpegPath(ffmpegPath);
// URL of the video and desired timestamp (in seconds)
const videoUrl = 'https://raw.githubusercontent.com/tolotrasamuel/assets/main/videoplayback.mp4';
const timestamp = 30; // Example: 30 seconds into the video
// Function to get the byte range based on timestamp
const getByteRangeForTimestamp = async (url, timestamp) => {
const response = await axios.head(url);
const contentLength = response.headers['content-length'];
console.log('Content-Length:', contentLength);
// This part is a rough estimate, as the actual byte range will depend on the encoding
const estimatedBytesPerSecond = contentLength / 60; // Assuming a 1-minute video
const startByte = Math.floor(estimatedBytesPerSecond * timestamp);
const endByte = startByte + Math.floor(estimatedBytesPerSecond*10); // 1 second range
return { startByte, endByte };
};
// Download the specific 1-second segment
const downloadSegment = async (url, startByte, endByte, outputPath) => {
console.log(`Downloading bytes ${startByte}-${endByte}...`);
const response = await axios.get(url, {
responseType: 'arraybuffer',
headers: {
Range: `bytes=${startByte}-${endByte}`,
},
});
console.log('Segment downloaded!');
fs.writeFileSync(outputPath, response.data);
};
// Extract frame from the segment
const extractFrame = (videoPath, timestamp, outputFramePath) => {
ffmpeg(videoPath)
.seekInput(timestamp)
.frames(1)
.output(outputFramePath)
.on('end', () => {
console.log(`Frame extracted to: ${outputFramePath}`);
// Optionally, delete the temporary video file
fs.unlinkSync(videoPath);
})
.on('error', (err) => {
console.error('Error extracting frame:', err);
})
.run();
};
const __dirname = import.meta.dirname;
(async () => {
try {
const { startByte, endByte } = await getByteRangeForTimestamp(videoUrl, timestamp);
const tmpVideoPath = path.resolve(__dirname, 'temp_video.mp4');
const outputFramePath = path.resolve(__dirname, `frame_${timestamp}.png`);
await downloadSegment(videoUrl, startByte, endByte, tmpVideoPath);
extractFrame(tmpVideoPath, timestamp, outputFramePath);
} catch (err) {
console.error('Error:', err);
}
})();
The problem here is that the temp_video.mp4
seems not to be playable and ffmpeg
says:
Invalid data found when processing input
I can use any programming language but Javascript (Node), Python or Dart would be preferred.
Update
I was able to modify the obviously not working estimation of the byte with this code.
const getByteRangeForTimestamp = async (url, timestamp) => {
// Use ffprobe to get the offset and size of the frame at the given timestamp
const command = `ffprobe -v error -select_streams v:0 -show_entries packet=pos,size,dts_time -read_intervals ${timestamp}%+1 -of csv=p=0 ${url}`;
console.log('Running command:', command);
const { stdout } = await execPromise(command);
// Parse the output
const [ dts_time, size,offset] = stdout.trim().split("n")[0].split(',');
const [ dts_time2, size2,offset2] = stdout.trim().split("n")[1].split(',');
const startByte = parseInt(offset);
const endByte = startByte + parseInt(size) +parseInt(size2) - 1;
console.log(`Frame at timestamp ${timestamp}s is located at bytes ${startByte}-${endByte} (size: ${size} bytes)`);
return { startByte, endByte };
};
However, I suspect that this still download the entire file.
2