Initial attempt to add --syncTiming flag

Add --syncTiming flag to attempt to detect timing differences in multi-dub downloads and sync them properly. Currently disabled by default since it's experimental and likely buggy. Potentially Resolved #471
This commit is contained in:
AnimeDL 2023-07-16 19:24:41 -07:00
parent 54d3a14bea
commit 485433cd74
8 changed files with 183 additions and 23 deletions

View file

@ -29,7 +29,8 @@ export type CrunchyDownloadOptions = {
defaultAudio: LanguageItem,
ccTag: string,
dlVideoOnce: boolean,
skipmux?: boolean
skipmux?: boolean,
syncTiming: boolean,
}
export type CurnchyMultiDownload = {
@ -52,7 +53,8 @@ export type CrunchyMuxOptions = {
mkvmergeOptions: string[],
defaultSub: LanguageItem,
defaultAudio: LanguageItem,
ccTag: string
ccTag: string,
syncTiming: boolean,
}
export type CrunchyEpMeta = {
@ -60,7 +62,9 @@ export type CrunchyEpMeta = {
mediaId: string,
lang?: LanguageItem,
playback?: string,
versions?: EpisodeVersion[] | null
versions?: EpisodeVersion[] | null,
isSubbed: boolean,
isDubbed: boolean,
}[],
seriesTitle: string,
seasonTitle: string,
@ -76,7 +80,8 @@ export type CrunchyEpMeta = {
export type DownloadedMedia = {
type: 'Video',
lang: LanguageItem,
path: string
path: string,
isPrimary?: boolean
} | ({
type: 'Subtitle',
cc: boolean

View file

@ -816,7 +816,9 @@ export default class Crunchy implements ServiceClass {
{
mediaId: item.id,
versions: null,
lang: langsData.languages.find(a => a.code == yargs.appArgv(this.cfg.cli).dubLang[0])
lang: langsData.languages.find(a => a.code == yargs.appArgv(this.cfg.cli).dubLang[0]),
isSubbed: item.is_subbed,
isDubbed: item.is_dubbed
}
],
seriesTitle: item.series_title,
@ -974,7 +976,9 @@ export default class Crunchy implements ServiceClass {
epMeta.data = [
{
mediaId: 'E:'+ item.id,
versions: item.episode_metadata.versions
versions: item.episode_metadata.versions,
isSubbed: item.episode_metadata.is_subbed,
isDubbed: item.episode_metadata.is_dubbed
}
];
epMeta.seriesTitle = item.episode_metadata.series_title;
@ -986,7 +990,9 @@ export default class Crunchy implements ServiceClass {
item.f_num = 'F:' + item.id;
epMeta.data = [
{
mediaId: 'M:'+ item.id
mediaId: 'M:'+ item.id,
isSubbed: item.movie_listing_metadata.is_subbed,
isDubbed: item.movie_listing_metadata.is_dubbed
}
];
epMeta.seriesTitle = item.title;
@ -997,7 +1003,9 @@ export default class Crunchy implements ServiceClass {
item.f_num = 'F:' + item.id;
epMeta.data = [
{
mediaId: 'M:'+ item.id
mediaId: 'M:'+ item.id,
isSubbed: item.movie_metadata.is_subbed,
isDubbed: item.movie_metadata.is_dubbed
}
];
epMeta.season = 0;
@ -1053,6 +1061,8 @@ export default class Crunchy implements ServiceClass {
//Make sure token is up to date
await this.refreshToken(true, true);
let currentVersion;
let isPrimary = mMeta.isSubbed;
const AuthHeaders = {
headers: {
Authorization: `Bearer ${this.token.access_token}`,
@ -1063,11 +1073,13 @@ export default class Crunchy implements ServiceClass {
//Get Media GUID
let mediaId = mMeta.mediaId;
if (mMeta.versions && mMeta.lang) {
mediaId = mMeta.versions.find(a => a.audio_locale == mMeta.lang?.cr_locale)?.media_guid as string;
if (!mediaId) {
currentVersion = mMeta.versions.find(a => a.audio_locale == mMeta.lang?.cr_locale);
if (!currentVersion?.media_guid) {
console.error('Selected language not found.');
return undefined;
continue;
}
isPrimary = currentVersion.original;
mediaId = currentVersion?.media_guid;
}
// If for whatever reason mediaId has a :, return the ID only
@ -1352,7 +1364,8 @@ export default class Crunchy implements ServiceClass {
files.push({
type: 'Video',
path: `${tsFile}.ts`,
lang: lang
lang: lang,
isPrimary: isPrimary
});
dlVideoOnce = true;
}
@ -1489,6 +1502,9 @@ export default class Crunchy implements ServiceClass {
// collect fonts info
// mergers
let isMuxed = false;
if (options.syncTiming) {
await merger.createDelays();
}
if (bin.MKVmerge) {
await merger.merge('mkvmerge', bin.MKVmerge);
isMuxed = true;
@ -1657,7 +1673,9 @@ export default class Crunchy implements ServiceClass {
data: [
{
mediaId: item.id,
versions: item.versions
versions: item.versions,
isSubbed: item.is_subbed,
isDubbed: item.is_dubbed
}
],
seriesTitle: itemE.items.find(a => !a.series_title.match(/\(\w+ Dub\)/))?.series_title ?? itemE.items[0].series_title.replace(/\(\w+ Dub\)/g, '').trimEnd(),

View file

@ -824,6 +824,9 @@ export default class Hidive implements ServiceClass {
// collect fonts info
// mergers
let isMuxed = false;
if (options.syncTiming) {
await merger.createDelays();
}
if (bin.MKVmerge) {
await merger.merge('mkvmerge', bin.MKVmerge);
isMuxed = true;

View file

@ -67,6 +67,7 @@ let argvC: {
removeBumpers: boolean;
originalFontSize: boolean;
keepAllVideos: boolean;
syncTiming: boolean;
};
export type ArgvType = typeof argvC;

View file

@ -419,6 +419,20 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
default: false
}
},
{
name: 'syncTiming',
group: 'mux',
describe: 'Attempts to sync timing for multi-dub downloads EXPERIMENTAL',
docDescribe: 'In enabled attempts to sync timing for multi-dub downloads.'
+ '\nNOTE: This is currently experimental and syncs audio and subtitles, though subtitles has a lot of guesswork'
+ '\nIf you find bugs with this, please report it in the discord or github',
service: ['crunchy','hidive'],
type: 'boolean',
usage: '',
default: {
default: false
}
},
{
name: 'skipmux',
describe: 'Skip muxing video, audio and subtitles',

View file

@ -6,16 +6,22 @@ import { LanguageItem } from './module.langsData';
import { AvailableMuxer } from './module.args';
import { exec } from './sei-helper-fixes';
import { console } from './log';
import ffprobe from 'ffprobe';
import ffprobeStatic from 'ffprobe-static';
export type MergerInput = {
path: string,
lang: LanguageItem
lang: LanguageItem,
duration?: number,
delay?: number,
isPrimary?: boolean,
}
export type SubtitleInput = {
language: LanguageItem,
file: string,
closedCaption?: boolean
closedCaption?: boolean,
delay?: number
}
export type Font = keyof typeof fontFamilies;
@ -58,6 +64,43 @@ class Merger {
this.options.videoTitle = this.options.videoTitle.replace(/"/g, '\'');
}
public async createDelays() {
//Don't bother scanning it if there is only 1 vna stream
if (this.options.videoAndAudio.length > 1) {
const vnas = this.options.videoAndAudio;
//get and set durations on each videoAndAudio Stream
for (const [vnaIndex, vna] of vnas.entries()) {
const streamInfo = await ffprobe(vna.path, { path: ffprobeStatic.path });
const videoInfo = streamInfo.streams.filter(stream => stream.codec_type == 'video');
vnas[vnaIndex].duration = videoInfo[0].duration;
}
//Sort videoAndAudio streams by duration (shortest first)
vnas.sort((a,b) => {
if (!a.duration || !b.duration) return -1;
return a.duration - b.duration;
});
//Set Delays
const shortestDuration = vnas[0].duration;
for (const [vnaIndex, vna] of vnas.entries()) {
//Don't calculate the shortestDuration track
if (vnaIndex == 0) continue;
if (vna.duration && shortestDuration) {
//Calculate the tracks delay
vna.delay = Math.ceil((vna.duration-shortestDuration) * 1000) / 1000;
//TODO: set primary language for audio so it can be used to determine which track needs the delay
//The above is a problem in the event that it isn't the dub that needs the delay, but rather the sub.
//Alternatively: Might not work: it could be checked if there are multiple of the same video language, and if there is
//more than 1 of the same video language, then do the subtitle delay on CC, else normal language.
const subtitles = this.options.subtitles.filter(sub => sub.language.code == vna.lang.code);
for (const [subIndex, sub] of subtitles.entries()) {
if (vna.isPrimary) subtitles[subIndex].delay = vna.delay;
else if (sub.closedCaption) subtitles[subIndex].delay = vna.delay;
}
}
}
}
}
public FFmpeg() : string {
const args: string[] = [];
const metaData: string[] = [];
@ -67,6 +110,11 @@ class Merger {
let hasVideo = false;
for (const vid of this.options.videoAndAudio) {
if (vid.delay && hasVideo) {
args.push(
`-itsoffset -${Math.ceil(vid.delay*1000)}ms`
);
}
args.push(`-i "${vid.path}"`);
if (!hasVideo || this.options.keepAllVideos) {
metaData.push(`-map ${index}:a -map ${index}:v`);
@ -101,6 +149,11 @@ class Merger {
for (const index in this.options.subtitles) {
const sub = this.options.subtitles[index];
if (sub.delay) {
args.push(
`-itsoffset -${Math.ceil(sub.delay*1000)}ms`
);
}
args.push(`-i "${sub.file}"`);
}
@ -164,6 +217,11 @@ class Merger {
for (const vid of this.options.videoAndAudio) {
const audioTrackNum = this.options.inverseTrackOrder ? '0' : '1';
const videoTrackNum = this.options.inverseTrackOrder ? '1' : '0';
if (vid.delay) {
args.push(
`--sync ${audioTrackNum}:-${Math.ceil(vid.delay*1000)}`
);
}
if (!hasVideo || this.options.keepAllVideos) {
args.push(
`--video-tracks ${videoTrackNum}`,
@ -213,6 +271,11 @@ class Merger {
if (this.options.subtitles.length > 0) {
for (const subObj of this.options.subtitles) {
if (subObj.delay) {
args.push(
`--sync 0:-${Math.ceil(subObj.delay*1000)}`
);
}
args.push('--track-name', `0:"${(subObj.language.language || subObj.language.name) + `${subObj.closedCaption === true ? ` ${this.options.ccTag}` : ''}`}"`);
args.push('--language', `0:"${subObj.language.code}"`);
//TODO: look into making Closed Caption default if it's the only sub of the default language downloaded

View file

@ -1,7 +1,7 @@
{
"name": "multi-downloader-nx",
"short_name": "aniDL",
"version": "4.3.0b8",
"version": "4.3.0",
"description": "Downloader for Crunchyroll, Funimation, or Hidive via CLI or GUI",
"keywords": [
"download",
@ -50,6 +50,8 @@
"dotenv": "^16.3.1",
"eslint-plugin-import": "^2.27.5",
"express": "^4.18.2",
"ffprobe": "^1.1.2",
"ffprobe-static": "^3.1.0",
"form-data": "^4.0.0",
"fs-extra": "^11.1.1",
"got": "^11.8.6",
@ -67,6 +69,8 @@
"devDependencies": {
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/ffprobe": "^1.1.4",
"@types/ffprobe-static": "^2.0.1",
"@types/fs-extra": "^11.0.1",
"@types/node": "^18.15.11",
"@types/ws": "^8.5.5",

View file

@ -29,6 +29,12 @@ dependencies:
express:
specifier: ^4.18.2
version: 4.18.2
ffprobe:
specifier: ^1.1.2
version: 1.1.2
ffprobe-static:
specifier: ^3.1.0
version: 3.1.0
form-data:
specifier: ^4.0.0
version: 4.0.0
@ -76,6 +82,12 @@ devDependencies:
'@types/express':
specifier: ^4.17.17
version: 4.17.17
'@types/ffprobe':
specifier: ^1.1.4
version: 1.1.4
'@types/ffprobe-static':
specifier: ^2.0.1
version: 2.0.1
'@types/fs-extra':
specifier: ^11.0.1
version: 11.0.1
@ -1845,6 +1857,14 @@ packages:
'@types/serve-static': 1.15.1
dev: true
/@types/ffprobe-static@2.0.1:
resolution: {integrity: sha512-V5CrKUfms0lBGSXliKmKzSFFZWgJusQks1YfjRI/+2dXFF+aK7qBAarCe/ryYHQI44jYQX7xtlgH0fCuJepuGQ==}
dev: true
/@types/ffprobe@1.1.4:
resolution: {integrity: sha512-gtfU+bD4FDoF1S2ybmIWEIz0K5ijeHpi+CgJUtXl3FTGnf+61HmsZksqDn8M9M9lRJU9SRyMLt5yrIwUSep4Uw==}
dev: true
/@types/fs-extra@11.0.1:
resolution: {integrity: sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==}
dependencies:
@ -2214,6 +2234,14 @@ packages:
url-toolkit: 2.2.5
dev: false
/JSONStream@1.3.5:
resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
hasBin: true
dependencies:
jsonparse: 1.3.1
through: 2.3.8
dev: false
/accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
@ -2449,7 +2477,6 @@ packages:
/base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
dev: true
/bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
@ -2457,7 +2484,6 @@ packages:
buffer: 5.7.1
inherits: 2.0.4
readable-stream: 3.6.2
dev: true
/body-parser@1.20.1:
resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==}
@ -2521,7 +2547,6 @@ packages:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
dev: true
/bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
@ -2822,6 +2847,12 @@ packages:
engines: {node: '>=10'}
dev: false
/deferential@1.0.0:
resolution: {integrity: sha512-QyFNvptDP8bypD6WK6ZOXFSBHN6CFLZmQ59QyvRGDvN9+DoX01mxw28QrJwSVPrrwnMWqHgTRiXybH6Y0cBbWw==}
dependencies:
native-promise-only: 0.8.1
dev: false
/define-lazy-prop@2.0.0:
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
engines: {node: '>=8'}
@ -3492,6 +3523,18 @@ packages:
dependencies:
reusify: 1.0.4
/ffprobe-static@3.1.0:
resolution: {integrity: sha512-Dvpa9uhVMOYivhHKWLGDoa512J751qN1WZAIO+Xw4L/mrUSPxS4DApzSUDbCFE/LUq2+xYnznEahTd63AqBSpA==}
dev: false
/ffprobe@1.1.2:
resolution: {integrity: sha512-a+oTbhyeM7Z8PRy+mpzmVUAnATZT7z4BO94HSKeqHupdmjiKZ1djzcZkyoyXA21zCOCG7oVRrsBMmvvtmzoz4g==}
dependencies:
JSONStream: 1.3.5
bl: 4.1.0
deferential: 1.0.0
dev: false
/file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@ -3825,7 +3868,6 @@ packages:
/ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: true
/ignore@5.2.4:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
@ -4103,6 +4145,11 @@ packages:
optionalDependencies:
graceful-fs: 4.2.11
/jsonparse@1.3.1:
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
engines: {'0': node >= 0.2.0}
dev: false
/jsx-ast-utils@3.3.3:
resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==}
engines: {node: '>=4.0'}
@ -4306,6 +4353,10 @@ packages:
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
dev: true
/native-promise-only@0.8.1:
resolution: {integrity: sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==}
dev: false
/natural-compare-lite@1.4.0:
resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==}
dev: true
@ -4705,7 +4756,6 @@ packages:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
dev: true
/regenerate-unicode-properties@10.1.0:
resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==}
@ -5015,7 +5065,6 @@ packages:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
dependencies:
safe-buffer: 5.2.1
dev: true
/strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
@ -5075,6 +5124,10 @@ packages:
/text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
/through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
dev: false
/to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'}
@ -5272,7 +5325,6 @@ packages:
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
/utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}