const fs = require('fs')
const path = require('path')
const events = require('node:events')
const ffmpeg = require('ffmpeg-static')
const error = require('./src/error')
const parse = require('./src/parse')
const segments = require('./src/segments')
const merge = require('./src/merge')
const transmux = require('./src/transmux')
class M3U8 extends events {
/**
* Create an M3U8 Instance Downloader.
* @constructor
* @param {Object} opt - Options for instance
* @param {String} opt.streamUrl - The URL of the m3u8 playlist.
* @param {String} opt.outputFile - Path where the downloaded output file will be saved.
* @param {String} [opt.quality='highest'] - Quality of the stream to download (default: highest) (qualities: highest, medium and lowest)
* @param {String} [opt.mergedPath='require('os').tempdir()/m3u8dl/merged.ts'] - Path where merged ts files from segments are stored (default: cache + '/merged.ts').
* @param {String} [opt.cache='require('os').tempdir()/m3u8dl/'] - Path where temporary files are stored.
* @param {Number} [opt.concurrency=10] - Number of download threads (default: 10)
* @param {String} [opt.ffmpegPath=ffmpegStatic] - Custom path to ffmpeg executable. (default: ffmpeg-static)
* @param {Boolean} [opt.useFfmpegToMerge=false] - Use ffmpeg to merge segments.
* @param {Function} [opt.cb=function(event,data){}] - Callback function for events. (default: function(event, data){})
*/
constructor(opt) {
super()
let { streamUrl, outputFile, quality, mergedPath, cache, concurrency, ffmpegPath, useFfmpegToMerge, cb } = opt
let options = {
streamUrl,
output: outputFile,
quality: String(quality || 'highest').toLowerCase(),
mergedPath,
cache: cache || path.join(require('os').tmpdir(), 'm3u8dl'),
concurrency: concurrency || 10,
captions: [],
ffmpegPath: ffmpegPath || ffmpeg,
ffmpegMerge: useFfmpegToMerge || false,
cb: cb || function(){}
}
if(!options.streamUrl) throw new error('NO STREAM URL');
if(!options.output) throw new error('PLEASE PROVIDE AN OUTPUT PATH');
options.mergedPath = options.mergedPath || path.join(options.cache, 'merged.ts')
this._options = options
this.instance = this
this.oldEmit = this.emit
this.emit = function(one, ...args) {
this._options.cb(one, ...args)
return this.oldEmit(one, ...args)
}
}
/**
* Add a caption file.
* @function
* @param {string} uri - URI or Path of the caption
* @param {string} lang - Language of the caption
*/
addCaption(uri = null, lang = 'english') {
this._options.captions.push({
uri,
lang
})
}
/**
* Starts the download
* @function
*/
startDownload() {
let master = this
let captions = this._options.captions
master.emit('start')
return new Promise(async(resolve) => {
master.emit('parsing')
let options = master._options
let parsedSegments = await parse(options.streamUrl, options.quality, options.cache)
if(!Array.isArray(parsedSegments)) {
master.emit('error', parsedSegments)
return resolve(new error(parsedSegments))
}
master.emit('segments_download:build')
let data = await segments(parsedSegments, options.streamUrl, options.cache, options.concurrency, (event, data) => {
return master.emit(`segments_download:${event}`, data)
})
if(!data || typeof data !== 'object' || !data.totalSegments || !Array.isArray(data.segments)) {
master.emit('error', data)
return resolve(new error(data))
}
master.emit('merging:start')
let merged = await merge(data, options.mergedPath, options.ffmpegMerge, options.ffmpegPath)
if(merged !== 100) {
master.emit('error', merged)
return resolve(new error(merged))
}
master.emit('merging:end')
master.emit('conversion:start')
let to_mp4 = await transmux(options.mergedPath, options.output, options.ffmpegPath, captions, options.cache)
if(to_mp4 !== 100) {
master.emit('error', to_mp4)
return resolve(new error(to_mp4))
}
master.emit('conversion:end')
master.emit('end')
if(fs.existsSync(options.cache)) await require('fs/promises').rm(options.cache, { recursive: true, force: true })
return resolve(100)
})
}
}
module.exports = M3U8