mirror of
https://github.com/mifi/lossless-cut.git
synced 2024-11-26 12:12:39 +01:00
stream improvements
- manually copy disposition when concat (ffmpeg doesnt automatically) - auto-convert any subtitle to mov_text when output is mp4 #418
This commit is contained in:
parent
1e352a6d75
commit
392962729a
@ -11,6 +11,7 @@ import { askForMetadataKey, showJson5Dialog } from './dialogs';
|
||||
import { formatDuration } from './util/duration';
|
||||
import { getStreamFps } from './ffmpeg';
|
||||
import { deleteDispositionValue } from './util';
|
||||
import { getActiveDisposition } from './util/streams';
|
||||
|
||||
|
||||
const activeColor = '#429777';
|
||||
@ -130,10 +131,7 @@ const EditStreamDialog = memo(({ editingStream: { streamId: editingStreamId, pat
|
||||
const existingDispositionsObj = useMemo(() => (stream && stream.disposition) || {}, [stream]);
|
||||
const effectiveDisposition = useMemo(() => {
|
||||
if (customDisposition) return customDisposition;
|
||||
if (!existingDispositionsObj) return undefined;
|
||||
const existingActiveDispositionEntry = Object.entries(existingDispositionsObj).find(([, value]) => value === 1);
|
||||
if (!existingActiveDispositionEntry) return undefined;
|
||||
return existingActiveDispositionEntry[0]; // return the key
|
||||
return getActiveDisposition(existingDispositionsObj);
|
||||
}, [customDisposition, existingDispositionsObj]);
|
||||
|
||||
// console.log({ existingDispositionsObj, effectiveDisposition });
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import flatMap from 'lodash/flatMap';
|
||||
import flatMapDeep from 'lodash/flatMapDeep';
|
||||
import sum from 'lodash/sum';
|
||||
import pMap from 'p-map';
|
||||
|
||||
@ -125,15 +124,24 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps }) {
|
||||
return streamCount + copiedStreamIndex;
|
||||
}
|
||||
|
||||
const lessDeepMap = (root, fn) => flatMapDeep((
|
||||
Object.entries(root), ([path, streamsMap]) => (
|
||||
Object.entries(streamsMap || {}).map(([streamId, value]) => (
|
||||
fn(path, streamId, value)
|
||||
)))));
|
||||
function lessDeepMap(root, fn) {
|
||||
let ret = [];
|
||||
Object.entries(root).forEach(([path, streamsMap]) => (
|
||||
Object.entries(streamsMap || {}).forEach(([streamId, value]) => {
|
||||
ret = [...ret, ...fn(path, streamId, value)];
|
||||
})));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// The structure is deep! file -> stream -> key -> value Example: { 'file.mp4': { 0: { key: 'value' } } }
|
||||
const deepMap = (root, fn) => lessDeepMap(root, (path, streamId, tagsMap) => (
|
||||
Object.entries(tagsMap || {}).map(([key, value]) => fn(path, streamId, key, value))));
|
||||
const deepMap = (root, fn) => lessDeepMap(root, (path, streamId, tagsMap) => {
|
||||
let ret = [];
|
||||
Object.entries(tagsMap || {}).forEach(([key, value]) => {
|
||||
ret = [...ret, ...fn(path, streamId, key, value)];
|
||||
});
|
||||
return ret;
|
||||
});
|
||||
|
||||
const customTagsArgs = [
|
||||
// Main file metadata:
|
||||
@ -287,6 +295,7 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps }) {
|
||||
allFilesMeta: { [firstPath]: { streams } },
|
||||
copyFileStreams: [{ path: firstPath, streamIds: streamIdsToCopy }],
|
||||
outFormat,
|
||||
manuallyCopyDisposition: true,
|
||||
});
|
||||
|
||||
// Keep this similar to cutSingle()
|
||||
|
@ -6,17 +6,43 @@ export const defaultProcessedCodecTypes = [
|
||||
'attachment',
|
||||
];
|
||||
|
||||
function getPerStreamQuirksFlags({ stream, outputIndex, outFormat }) {
|
||||
if (['mov', 'mp4'].includes(outFormat) && stream.codec_tag === '0x0000' && stream.codec_name === 'hevc') {
|
||||
return [`-tag:${outputIndex}`, 'hvc1'];
|
||||
export function getActiveDisposition(disposition) {
|
||||
if (disposition == null) return undefined;
|
||||
const existingActiveDispositionEntry = Object.entries(disposition).find(([, value]) => value === 1);
|
||||
if (!existingActiveDispositionEntry) return undefined;
|
||||
return existingActiveDispositionEntry[0]; // return the key
|
||||
}
|
||||
|
||||
function getPerStreamQuirksFlags({ stream, outputIndex, outFormat, manuallyCopyDisposition = false }) {
|
||||
let args = [];
|
||||
if (['mov', 'mp4'].includes(outFormat)) {
|
||||
if (stream.codec_tag === '0x0000' && stream.codec_name === 'hevc') {
|
||||
args = [...args, `-tag:${outputIndex}`, 'hvc1'];
|
||||
}
|
||||
|
||||
// mp4/mov only supports mov_text, so convert it https://stackoverflow.com/a/17584272/6519037
|
||||
// https://github.com/mifi/lossless-cut/issues/418
|
||||
if (stream.codec_type === 'subtitle') {
|
||||
args = [...args, `-c:${outputIndex}`, 'mov_text'];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
|
||||
// when concat'ing, disposition doesn't seem to get automatically transferred by ffmpeg, so we must do it manually
|
||||
if (manuallyCopyDisposition && stream.disposition != null) {
|
||||
const activeDisposition = getActiveDisposition(stream.disposition);
|
||||
if (activeDisposition != null) {
|
||||
args = [...args, `-disposition:${outputIndex}`, String(activeDisposition)];
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function getMapStreamsArgs({ outFormat, allFilesMeta, copyFileStreams }) {
|
||||
export function getMapStreamsArgs({ outFormat, allFilesMeta, copyFileStreams, manuallyCopyDisposition }) {
|
||||
let args = [];
|
||||
let outputIndex = 0;
|
||||
|
||||
copyFileStreams.forEach(({ streamIds, path }, fileIndex) => {
|
||||
streamIds.forEach((streamId) => {
|
||||
const { streams } = allFilesMeta[path];
|
||||
@ -24,7 +50,7 @@ export function getMapStreamsArgs({ outFormat, allFilesMeta, copyFileStreams })
|
||||
args = [
|
||||
...args,
|
||||
'-map', `${fileIndex}:${streamId}`,
|
||||
...getPerStreamQuirksFlags({ stream, outputIndex, outFormat }),
|
||||
...getPerStreamQuirksFlags({ stream, outputIndex, outFormat, manuallyCopyDisposition }),
|
||||
];
|
||||
outputIndex += 1;
|
||||
});
|
||||
|
@ -8,24 +8,46 @@ const streams1 = [
|
||||
{ index: 4, codec_type: 'audio', codec_tag: '0x6134706d', codec_name: 'aac' },
|
||||
{ index: 5, codec_type: 'attachment', codec_tag: '0x0000', codec_name: 'ttf' },
|
||||
{ index: 6, codec_type: 'data', codec_tag: '0x64636d74' },
|
||||
{ index: 7, codec_type: 'subtitle', codec_tag: '0x0000', codec_name: 'subrip' },
|
||||
];
|
||||
|
||||
const path = '/path/file.mp4';
|
||||
const outFormat = 'mp4';
|
||||
|
||||
// Some files haven't got a valid video codec tag set, so change it to hvc1 (default by ffmpeg is hev1 which doesn't work in QuickTime)
|
||||
// https://github.com/mifi/lossless-cut/issues/1032
|
||||
// https://stackoverflow.com/questions/63468587/what-hevc-codec-tag-to-use-with-fmp4-hvc1-or-hev1
|
||||
// https://stackoverflow.com/questions/32152090/encode-h265-to-hvc1-codec
|
||||
test('getMapStreamsArgs, tag', () => {
|
||||
const path = '/path/file.mp4';
|
||||
const outFormat = 'mp4';
|
||||
|
||||
test('getMapStreamsArgs', () => {
|
||||
expect(getMapStreamsArgs({
|
||||
allFilesMeta: { [path]: { streams: streams1 } },
|
||||
copyFileStreams: [{ path, streamIds: streams1.map((stream) => stream.index) }],
|
||||
outFormat,
|
||||
})).toEqual(['-map', '0:0', '-map', '0:1', '-map', '0:2', '-map', '0:3', '-tag:3', 'hvc1', '-map', '0:4', '-map', '0:5', '-map', '0:6']);
|
||||
})).toEqual([
|
||||
'-map', '0:0',
|
||||
'-map', '0:1',
|
||||
'-map', '0:2',
|
||||
'-map', '0:3', '-tag:3', 'hvc1',
|
||||
'-map', '0:4',
|
||||
'-map', '0:5',
|
||||
'-map', '0:6',
|
||||
'-map', '0:7', '-c:7', 'mov_text',
|
||||
]);
|
||||
});
|
||||
|
||||
test('getMapStreamsArgs, disposition', () => {
|
||||
expect(getMapStreamsArgs({
|
||||
allFilesMeta: { [path]: { streams: streams1 } },
|
||||
copyFileStreams: [{ path, streamIds: [0] }],
|
||||
outFormat,
|
||||
manuallyCopyDisposition: true,
|
||||
})).toEqual([
|
||||
'-map', '0:0',
|
||||
'-disposition:0', 'attached_pic',
|
||||
]);
|
||||
});
|
||||
|
||||
test('getStreamIdsToCopy, includeAllStreams false', () => {
|
||||
const streamIdsToCopy = getStreamIdsToCopy({ streams: streams1, includeAllStreams: false });
|
||||
expect(streamIdsToCopy).toEqual([2, 1]);
|
||||
expect(streamIdsToCopy).toEqual([2, 1, 7]);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user