1
0
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:
Mikael Finstad 2022-02-24 15:32:11 +08:00
parent 1e352a6d75
commit 392962729a
No known key found for this signature in database
GPG Key ID: 25AB36E3E81CBC26
4 changed files with 79 additions and 24 deletions

View File

@ -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 });

View File

@ -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()

View File

@ -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;
});

View File

@ -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]);
});