From e7f61dca61a833e6155091995f68140dae5d8462 Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Mon, 28 Jan 2019 03:23:05 +0100 Subject: [PATCH] implement extract all streams #106 --- src/ffmpeg.js | 115 +++++++++++++++++++++++++++++++++++++---------- src/menu.js | 6 +++ src/renderer.jsx | 16 +++++++ 3 files changed, 113 insertions(+), 24 deletions(-) diff --git a/src/ffmpeg.js b/src/ffmpeg.js index 647e6ce3..06974ee8 100644 --- a/src/ffmpeg.js +++ b/src/ffmpeg.js @@ -4,7 +4,7 @@ const which = bluebird.promisify(require('which')); const path = require('path'); const fileType = require('file-type'); const readChunk = require('read-chunk'); -const _ = require('lodash'); +const flatMap = require('lodash/flatMap'); const readline = require('readline'); const moment = require('moment'); const stringToStream = require('string-to-stream'); @@ -29,6 +29,11 @@ function getFfmpegPath() { }); } +async function getFFprobePath() { + const ffmpegPath = await getFfmpegPath(); + return path.join(path.dirname(ffmpegPath), getWithExt('ffprobe')); +} + function handleProgress(process, cutDuration, onProgress) { const rl = readline.createInterface({ input: process.stderr }); rl.on('line', (line) => { @@ -176,34 +181,95 @@ function mapFormat(requestedFormat) { } function determineOutputFormat(ffprobeFormats, ft) { - if (_.includes(ffprobeFormats, ft.ext)) return ft.ext; + if (ffprobeFormats.includes(ft.ext)) return ft.ext; return ffprobeFormats[0] || undefined; } -function getFormat(filePath) { - return bluebird.try(() => { - console.log('getFormat', filePath); +async function getFormat(filePath) { + console.log('getFormat', filePath); - return getFfmpegPath() - .then(ffmpegPath => path.join(path.dirname(ffmpegPath), getWithExt('ffprobe'))) - .then(ffprobePath => execa(ffprobePath, [ - '-of', 'json', '-show_format', '-i', filePath, - ])) - .then((result) => { - const formatsStr = JSON.parse(result.stdout).format.format_name; - console.log('formats', formatsStr); - const formats = (formatsStr || '').split(','); + const ffprobePath = await getFFprobePath(); + const result = await execa(ffprobePath, [ + '-of', 'json', '-show_format', '-i', filePath, + ]); + const formatsStr = JSON.parse(result.stdout).format.format_name; + console.log('formats', formatsStr); + const formats = (formatsStr || '').split(','); - // ffprobe sometimes returns a list of formats, try to be a bit smarter about it. - return readChunk(filePath, 0, 4100) - .then((bytes) => { - const ft = fileType(bytes) || {}; - console.log(`fileType detected format ${JSON.stringify(ft)}`); - const assumedFormat = determineOutputFormat(formats, ft); - return mapFormat(assumedFormat); - }); - }); - }); + // ffprobe sometimes returns a list of formats, try to be a bit smarter about it. + const bytes = await readChunk(filePath, 0, 4100); + const ft = fileType(bytes) || {}; + console.log(`fileType detected format ${JSON.stringify(ft)}`); + const assumedFormat = determineOutputFormat(formats, ft); + return mapFormat(assumedFormat); +} + +async function getAllStreams(filePath) { + const ffprobePath = await getFFprobePath(); + const result = await execa(ffprobePath, [ + '-of', 'json', '-show_entries', 'stream', '-i', filePath, + ]); + + return JSON.parse(result.stdout); +} + +// https://stackoverflow.com/questions/32922226/extract-every-audio-and-subtitles-from-a-video-with-ffmpeg +async function extractAllStreams(filePath) { + const { streams } = await getAllStreams(filePath); + console.log('streams', streams); + + function mapCodecToOutputFormat(codec, type) { + const map = { + // See mapFormat + m4a: { ext: 'm4a', format: 'ipod' }, + aac: { ext: 'm4a', format: 'ipod' }, + + mp3: { ext: 'mp3', format: 'mp3' }, + opus: { ext: 'opus', format: 'opus' }, + vorbis: { ext: 'ogg', format: 'ogg' }, + h264: { ext: 'mp4', format: 'mp4' }, + eac3: { ext: 'eac3', format: 'eac3' }, + + subrip: { ext: 'srt', format: 'srt' }, + + // TODO add more + // TODO allow user to change? + }; + + if (map[codec]) return map[codec]; + if (type === 'video') return { ext: 'mkv', format: 'mkv' }; + if (type === 'audio') return { ext: 'mkv', format: 'mkv' }; + return undefined; + } + + const outStreams = streams.map((s, i) => ({ + i, + codec: s.codec_name, + type: s.codec_type, + format: mapCodecToOutputFormat(s.codec_name, s.codec_type), + })) + .filter(it => it); + + // console.log(outStreams); + + const streamArgs = flatMap(outStreams, ({ + i, codec, type, format: { format, ext }, + }) => [ + '-map', `0:${i}`, '-c', 'copy', '-f', format, '-y', `${filePath}-${i}-${type}-${codec}.${ext}`, + ]); + + const ffmpegArgs = [ + '-i', filePath, + ...streamArgs, + ]; + + console.log(ffmpegArgs); + + // TODO progress + const ffmpegPath = await getFfmpegPath(); + const process = execa(ffmpegPath, ffmpegArgs); + const result = await process; + console.log(result.stdout); } module.exports = { @@ -211,4 +277,5 @@ module.exports = { getFormat, html5ify, mergeFiles, + extractAllStreams, }; diff --git a/src/menu.js b/src/menu.js index 25a00fc6..42a814d5 100644 --- a/src/menu.js +++ b/src/menu.js @@ -34,6 +34,12 @@ module.exports = (app, mainWindow, newVersion) => { mainWindow.webContents.send('html5ify', true); }, }, + { + label: 'Extract all streams', + click() { + mainWindow.webContents.send('extract-all-streams', false); + }, + }, { label: 'Set custom start time offset', click() { diff --git a/src/renderer.jsx b/src/renderer.jsx index 4524ef3e..54af5348 100644 --- a/src/renderer.jsx +++ b/src/renderer.jsx @@ -182,6 +182,22 @@ class App extends React.Component { this.setState({ startTimeOffset }); }); + electron.ipcRenderer.on('extract-all-streams', async () => { + const { filePath } = this.state; + if (!filePath) return; + + try { + this.setState({ working: true }); + // TODO customOutDir ? + await ffmpeg.extractAllStreams(filePath); + this.setState({ working: false }); + } catch (err) { + errorToast('Failed to extract all streams'); + console.error('Failed to extract all streams', err); + this.setState({ working: false }); + } + }); + document.ondragover = ev => ev.preventDefault(); document.ondragend = document.ondragover;