Skip to content

Commit

Permalink
stream improvements
Browse files Browse the repository at this point in the history
- manually copy disposition when concat (ffmpeg doesnt automatically)
- auto-convert any subtitle to mov_text when output is mp4 #418
  • Loading branch information
mifi committed Feb 24, 2022
1 parent 1e352a6 commit 3929627
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 24 deletions.
6 changes: 2 additions & 4 deletions src/StreamsSelector.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 });
Expand Down
25 changes: 17 additions & 8 deletions src/hooks/useFfmpegOperations.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -287,6 +295,7 @@ function useFfmpegOperations({ filePath, enableTransferTimestamps }) {
allFilesMeta: { [firstPath]: { streams } },
copyFileStreams: [{ path: firstPath, streamIds: streamIdsToCopy }],
outFormat,
manuallyCopyDisposition: true,
});

// Keep this similar to cutSingle()
Expand Down
38 changes: 32 additions & 6 deletions src/util/streams.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,51 @@ 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];
const stream = streams.find((s) => s.index === streamId);
args = [
...args,
'-map', `${fileIndex}:${streamId}`,
...getPerStreamQuirksFlags({ stream, outputIndex, outFormat }),
...getPerStreamQuirksFlags({ stream, outputIndex, outFormat, manuallyCopyDisposition }),
];
outputIndex += 1;
});
Expand Down
34 changes: 28 additions & 6 deletions src/util/streams.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
});

0 comments on commit 3929627

Please sign in to comment.