Skip to content

Commit 001de25

Browse files
authored
Fix YouTube downloading on Linux (#450)
* Limit resolution to 1080p * Update `yt-dlp` options for improved reliability * Apply full HTTP headers to FFmpeg * Disable AdvantageScope installations on Linux due to DNS issues
1 parent 4ea2419 commit 001de25

File tree

2 files changed

+48
-29
lines changed

2 files changed

+48
-29
lines changed

docs/docs/tab-reference/video.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ YouTube and TBA video download may failed unexpectedly due to changes on YouTube
2323
:::
2424

2525
:::info
26-
AdvantageScope requires [FFmpeg](https://ffmpeg.org) to process video files. If a valid copy of FFmpeg is not found on your system's PATH, AdvantageScope will prompt to download FFmpeg from the internet when loading a video for the first time.
26+
AdvantageScope requires [FFmpeg](https://ffmpeg.org) to process video files. If a valid copy of FFmpeg is not found on your system's PATH, AdvantageScope will prompt to download FFmpeg from the internet when loading a video for the first time. Automatic FFmpeg installation is only supported on Windows and macOS; Linux users may need to manually install FFmpeg and add it to the system PATH.
2727
:::
2828

2929
## Navigating the Video

src/main/electron/VideoProcessor.ts

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,6 @@ export class VideoProcessor {
3232
url: "https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-darwin-arm64",
3333
sha256: "a90e3db6a3fd35f6074b013f948b1aa45b31c6375489d39e572bea3f18336584"
3434
},
35-
"linux-x64": {
36-
url: "https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-linux-x64",
37-
sha256: "ed652b2f32e0851d1946894fb8333f5b677c1b2ce6b9d187910a67f8b99da028"
38-
},
39-
"linux-arm64": {
40-
url: "https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-linux-arm64",
41-
sha256: "237800b37bb65a81ad47871c6c8b7c45c0a3ca62a5b3f9d2a7a9a2dd9a338271"
42-
},
43-
"linux-armv7l": {
44-
url: "https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-linux-arm",
45-
sha256: "1a9ddc19d0e071b6e1ff6f8f34dc05ec6dd4d8f3e79a649f5a9ec0e8c929c4cb"
46-
},
4735
"win-x64": {
4836
url: "https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-win32-x64",
4937
sha256: "e9fd5e711debab9d680955fc1e38a2c1160fd280b144476cc3f62bc43ef49db1"
@@ -202,13 +190,14 @@ export class VideoProcessor {
202190

203191
private static async getFFmpegPath(window: BrowserWindow): Promise<string> {
204192
// Check for AdvantageScope install (user data folder)
193+
// Not compatible with Linux due to DNS issues, force a system install
205194
let ffmpegPath: string;
206195
if (process.platform === "win32") {
207196
ffmpegPath = path.join(app.getPath("userData"), "ffmpeg.exe");
208197
} else {
209198
ffmpegPath = path.join(app.getPath("userData"), "ffmpeg");
210199
}
211-
if (fs.existsSync(ffmpegPath)) {
200+
if (process.platform !== "linux" && fs.existsSync(ffmpegPath)) {
212201
return ffmpegPath;
213202
}
214203

@@ -232,7 +221,21 @@ export class VideoProcessor {
232221
return "ffmpeg";
233222
}
234223

235-
// Download FFmpeg
224+
// Exit immediately on Linux
225+
if (process.platform === "linux") {
226+
await dialog.showMessageBox(window, {
227+
type: "question",
228+
title: "Alert",
229+
message: "FFmpeg Installation Required",
230+
detail: "FFmpeg is required to process videos files. Please install FFmpeg and add to the PATH.",
231+
buttons: ["OK"],
232+
defaultId: 0,
233+
icon: WINDOW_ICON
234+
});
235+
throw "Failed";
236+
}
237+
238+
// Download FFmpeg on Windows and macOS
236239
let ffmpegDownloadResponse = await dialog.showMessageBox(window, {
237240
type: "question",
238241
title: "Alert",
@@ -282,7 +285,7 @@ export class VideoProcessor {
282285
menuCoordinates: null | [number, number],
283286
dataCallback: (data: any) => void
284287
) {
285-
let loadPath = async (videoPath: string, videoCache: string) => {
288+
let loadPath = async (videoPath: string, videoCache: string, httpHeaders?: any) => {
286289
// Find FFmpeg path
287290
let ffmpegPath: string;
288291
try {
@@ -304,15 +307,27 @@ export class VideoProcessor {
304307

305308
// Start ffmpeg
306309
if (uuid in VideoProcessor.processes) VideoProcessor.processes[uuid].kill();
307-
let ffmpeg = spawn(ffmpegPath, [
310+
311+
const ffmpegArgs: string[] = [];
312+
ffmpegArgs.push("-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "5");
313+
if (httpHeaders) {
314+
let headersStr = "";
315+
for (const [key, value] of Object.entries(httpHeaders)) {
316+
headersStr += `${key}: ${value}\r\n`;
317+
}
318+
ffmpegArgs.push("-headers", headersStr);
319+
}
320+
ffmpegArgs.push(
308321
"-i",
309322
videoPath,
310323
"-vf",
311324
"scale=1920:-2,setsar=1:1", // Limit to 1920px width
312325
"-q:v",
313326
"2",
314327
path.join(cachePath, "%08d.jpg")
315-
]);
328+
);
329+
330+
let ffmpeg = spawn(ffmpegPath, ffmpegArgs);
316331
VideoProcessor.processes[uuid] = ffmpeg;
317332
let running = true;
318333
let fullOutput = "";
@@ -482,7 +497,7 @@ export class VideoProcessor {
482497
} else if (code === 1) {
483498
if (videoCache === VIDEO_CACHE && fullOutput.includes("No space left on device")) {
484499
fs.rmSync(cachePath, { recursive: true });
485-
loadPath(videoPath, VIDEO_CACHE_FALLBACK);
500+
loadPath(videoPath, VIDEO_CACHE_FALLBACK, httpHeaders);
486501
} else {
487502
sendError();
488503
}
@@ -510,7 +525,7 @@ export class VideoProcessor {
510525
});
511526
} else {
512527
this.getDirectUrlFromYouTubeUrl(clipboardText, window)
513-
.then((path) => loadPath(path, VIDEO_CACHE))
528+
.then((result) => loadPath(result.url, VIDEO_CACHE, result.httpHeaders))
514529
.catch((silent) => {
515530
dataCallback({ uuid: uuid, error: true });
516531
if (silent === true) return;
@@ -529,7 +544,7 @@ export class VideoProcessor {
529544
this.getYouTubeUrlFromMatchInfo(matchInfo!, window, menuCoordinates!)
530545
.then((url) => {
531546
this.getDirectUrlFromYouTubeUrl(url, window)
532-
.then((path) => loadPath(path, VIDEO_CACHE))
547+
.then((result) => loadPath(result.url, VIDEO_CACHE, result.httpHeaders))
533548
.catch((silent) => {
534549
dataCallback({ uuid: uuid, error: true });
535550
if (silent === true) return;
@@ -587,8 +602,11 @@ export class VideoProcessor {
587602
});
588603
}
589604

590-
/** Gets the direct download URL based on a YouTube URL using youtube-dl-exec */
591-
private static getDirectUrlFromYouTubeUrl(youTubeUrl: string, window: BrowserWindow): Promise<string> {
605+
/** Gets the direct download URL and User Agent based on a YouTube URL using youtube-dl-exec */
606+
private static getDirectUrlFromYouTubeUrl(
607+
youTubeUrl: string,
608+
window: BrowserWindow
609+
): Promise<{ url: string; httpHeaders: any }> {
592610
return new Promise(async (resolve, reject) => {
593611
try {
594612
const videoId = VideoProcessor.getVideoId(youTubeUrl);
@@ -605,22 +623,23 @@ export class VideoProcessor {
605623
dumpSingleJson: true,
606624
noWarnings: true,
607625
noCheckCertificates: true,
608-
preferFreeFormats: true,
609-
youtubeSkipDashManifest: true
626+
forceIpv4: true,
627+
format: "best",
628+
extractorArgs: "youtube:player_client=android"
610629
},
611630
window
612631
);
613632

614633
// Filter formats
615634
let formats = output.formats || [];
616-
// Filter for formats that have video codec (not 'none')
617-
formats = formats.filter((f: any) => f.vcodec && f.vcodec !== "none");
635+
// Filter for formats that have a video codec and are not too large
636+
formats = formats.filter((f: any) => f.vcodec && f.vcodec !== "none" && f.height <= 1080);
618637
// Sort by quality (height/resolution) descending
619638
formats.sort((a: any, b: any) => (b.height || 0) - (a.height || 0));
620639

621-
// Find best url
640+
// Find best url and User Agent
622641
if (formats.length > 0) {
623-
resolve(formats[0].url);
642+
resolve({ url: formats[0].url, httpHeaders: output.http_headers });
624643
} else {
625644
reject();
626645
}

0 commit comments

Comments
 (0)