Skip to content

adouz/hypertube-torrent-stream-explained

Repository files navigation

hypertube-torrent-stream-explained

this is an explanation on how to stream torrent videos with low level library torrent-stream on nodejs

References

the stream endpoint (torrentStream.js file)

const logger = require('./logger'); // ignore this, its just a fancy console.log
const torrentStream = require('torrent-stream');
const parseRange = require('range-parser');
const ffmpeg = require('fluent-ffmpeg');
const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path;

module.exports = (req, res) => {
    /**
     * its up to you on how you going to get the magnet link from client
     */
    var torrentId = 'magnet:?xt='+req.query.xt;
    logger.info(`magnet link ${torrentId}`);
    /**
     * lets create an engine of the torrent 
     * path: is where we are saving the file
     * trackers: is the trackers that come with the magnet link
     */
    const engine = torrentStream(torrentId, {
        path: '/Users/adouz/goinfre/',
        trackers: req.query.tr
    });
    /**
     * engine.on('ready', () =>{}) is event listener  that will trigger when the engine is ready
     */
    engine.on('ready', () => {
        let large = 0;
        for (let i = 0; i < engine.files.length; i++) {
            console.log('filename:', engine.files[i].name, 'length:', engine.files[i].length);
            /**
             * get largest file because is going to be the video file
             */
            if (engine.files[i].length > engine.files[large].length) large = i;
        }
        const videoFile = engine.files[large];
        /**
         * exetract file extension from videFile.name
         * we going to need this to know if we can stream the video directly 
         * or we will have to convert it first
         * most browser supported formats: (.ogg/.ogv) .mp4 .webm
         * other format you will have to convert it to a supported format
         */
        const re = /(?:\.([^.]+))?$/;
        const ext = re.exec(videoFile.name)[1];
        console.log('largest file:: filename:', videoFile.name, 'length:', videoFile.length, 'extension:', ext);
        /**
         * to start file download we call videoFile.select();
         */
        videoFile.select();
        /**
         * if its first time that we get request we will not have range header
         * so we just start streaming video for html5 video and he will send us back a request with range header
         * then we start string using the range header
         */
        if (req.headers.range){
            console.log(`range: ${req.headers.range}`);
            /**
             * we use parseRange to parse the range for us
             */
            const range = parseRange(videoFile.length, req.headers.range)[0];
            /**
             * if theres somthing wrong with range we will response with 415
             */
            if (range.type !== 'bytes' && range === -1 && range === -2) 
                return res.status(415).end(); //415 Unsupported Media Type
            console.log(range);
            /**
             * 206 status code is needed to tell the browser that the body containes the requested range of data
             */
            res.status(206);
            /**
             * and we set the necessary headers to describe our range of data
             */
            res.set({
                'Content-Range': `bytes ${range.start}-${range.end}/${videoFile.length}`,
                'Accept-Ranges': 'bytes',
                'Content-Length': (range.end - range.start) + 1,
            })
            /**
             * check if file is a supported video format
             */
            if (['mp4', 'webm', 'ogv', 'ogg'].includes(ext)) {
                /**
                 * and take the range.start and range.end pass it to createReadStream so it will return a stream file at the requested range
                 */
                let stream = videoFile.createReadStream({ start: range.start, end: range.end });
                /**
                 * set type of content we are streaming
                 */
                res.set({
                    'Content-Type': `video/${ext === 'ogv' ? 'ogg' : ext }`,
                })
                stream.pipe(res)
            }else if (ext === 'mkv'){
                /**
                 * this if the file is not a supported video format
                 * we will have to convert to a supported format like .webm
                 */
                let stream = videoFile.createReadStream({ start: range.start, end: range.end });
                res.set({
                    'Content-Type': `video/webm`,
                })
                /**
                 * converting mkv to webm using fluent-ffmpeg and streaming it
                 */
                ffmpeg(stream) //set stream file
                    .setFfmpegPath(ffmpegPath) // set path of ffmpeg
                    .format('webm')  //set output format to webm
                    .videoCodec('libvpx') //transcoding video using libvpx library (https://en.wikipedia.org/wiki/Libvpx)
                    .audioCodec('libvorbis') //transcoding audio using libvorbis library (https://en.wikipedia.org/wiki/Vorbis)
                    .on('progress', (p) => logger.info('[ffmpeg Processing] ' + p.frames + ' frames'))          
                    .on('start', (s) => logger.info(`[ffmpeg started] ${s}`))
                    .on('error', (e) => logger.info(`[ffmpeg error] ${e}`))
                    .pipe(res);

            }else return res.status(415).end(); //415 Unsupported Media Type
        }else{
            /**
             * this if the browser didnt send us the range header
             * we will just send a stream to it so he will send us range header and start streaming
             * we call videoFile.createReadStream() and it will create a readable stream file
             */
            logger.info(`firt request`);
            let stream = videoFile.createReadStream();
            res.status(200);
            stream.pipe(res);
        }
        
    })
    /**
     * those are other event listeners to just debug and you dont need them!
     */
    engine.on('download', (i) => { logger.info(`download ${i}`) });
    engine.on('upload', (i) => { logger.info(`upload ${i}`) });
    engine.on('torrent', (i) => { logger.info(`torrent ${i}`) });
};

to run server

> npm i
> npm run dev

to watch streamed torrent video

  • go to http://localhost:4000 to watch the stream
  • if you want to change magnet link go to index.html file and change src attribute on <source />

About

this is an explanation of how to stream torrent videos with low-level library torrent-stream on nodejs

Resources

Stars

Watchers

Forks