Compare commits

..

No commits in common. "main" and "1.9.3" have entirely different histories.
main ... 1.9.3

7 changed files with 530 additions and 356 deletions

View file

@ -1,10 +1,10 @@
# Discord Video Sharing [![status-badge](https://git.mylloon.fr/Anri/dsr/badges/workflows/release.yml/badge.svg)](https://git.mylloon.fr/Anri/dsr/actions?workflow=release.yml) # Discord Video Sharing ![status-badge](https://git.mylloon.fr/Anri/dsr/badges/workflows/release.yml/badge.svg)
Tool for sharing video to Discord. Tool for sharing video to Discord.
> This tool was primarily made for video captured by NVidia Shadowplay. > This tool was primarily made for video captured by NVidia Shadowplay.
## Download/Install/Update ## Download/Install
2 choices : 2 choices :
@ -15,40 +15,22 @@ Tool for sharing video to Discord.
irm https://git.mylloon.fr/Anri/dsr/raw/branch/main/install.ps1 | iex irm https://git.mylloon.fr/Anri/dsr/raw/branch/main/install.ps1 | iex
``` ```
## Available flags > - If you have Discord Nitro: add `/nitro` flag when running DSR.
> - If you have an NVidia GPU with NVenc: add `/nvenc` flag when running DSR.
You can add thoses flags in the `Target` field of your Windows shortcut.
| | |
| ------------- | ------------------------------------------------------ |
| `/nitro` | Increase the file limit to 500Mo |
| `/nitrobasic` | Increase the file limit to 50Mo |
| | |
| `/nvenc_h264` | Enable NVenc with H.264 encoder (NVidia GPU required) |
| `/nvenc_h265` | Enable NVenc with H.265 encoder (NVidia GPU required) |
| `/amd_h264` | Enable AMF using DX11 with H.264 encoder (for AMD GPU) |
| `/amd_h265` | Enable AMF using DX11 with H.265 encoder (for AMD GPU) |
| `/h265` | Enable the H.265 CPU encoder (slow compression) |
> NVidia and AMD hardware accelerators support is experimental, but faster
> than CPU counterparts.
## More info ## More info
- [x] KISS interface - [x] KISS interface
- [x] Support drag&drop into the icon - [x] Support drag&drop into the icon
- [x] Keep the video under discord limitation - [x] Keep the video under 25mb (discord limitation)
- [x] Defaults to H.264 CPU encoder
- [x] If already under the limit, the file won't be compressed
- [x] NVenc support - [x] NVenc support
- [x] AMD cards acceleration support - [x] If already under the limit, the file won't be compressed
- [x] Nitro suppport - [x] Nitro suppport via `/nitro` flag
- [x] Merge 2 audio files into one track when recorded with system audio and microphone - [x] Merge audio files into one track when recorded with system audio and microphone
split up, while keeping the original ones (with conveniant metadata) split up, while keeping the original ones (with conveninant metadata)
- [x] Works also with file with only one or more than 2 audio track, by doing - [x] Works also with file with only one audio track
nothing
- [x] Support multiples files at once - [x] Support multiples files at once
- [x] Always optimize for video streaming - [x] Optimize for video streaming
## Package the app for Windows ## Package the app for Windows

657
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "dsr", "name": "dsr",
"version": "1.11.3", "version": "1.9.3",
"description": "Discord Video Sharing", "description": "Discord Video Sharing",
"main": "./dist/main.js", "main": "./dist/main.js",
"scripts": { "scripts": {
@ -18,15 +18,16 @@
"author": "Mylloon", "author": "Mylloon",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@electron-forge/maker-zip": "^7.5", "@electron-forge/maker-zip": "^7.4",
"ffmpeg-static": "^5.2", "ffmpeg-static": "^5.2",
"ffprobe-static": "^3.1", "ffprobe-static": "^3.1",
"typescript": "^5.6" "terminate": "^2.8",
"typescript": "^5.5"
}, },
"devDependencies": { "devDependencies": {
"@electron-forge/cli": "^7.5", "@electron-forge/cli": "^7.4",
"@types/ffprobe-static": "^2.0", "@types/ffprobe-static": "^2.0",
"electron": "^33.2" "electron": "^32.0"
}, },
"config": { "config": {
"forge": { "forge": {

View file

@ -15,6 +15,8 @@ import path = require("path");
import ffmpeg = require("ffmpeg-static"); import ffmpeg = require("ffmpeg-static");
const ffmpegPath = `${ffmpeg}`.replace("app.asar", "app.asar.unpacked"); const ffmpegPath = `${ffmpeg}`.replace("app.asar", "app.asar.unpacked");
const kill = require("terminate");
let error = false; let error = false;
const moviesFilter = { const moviesFilter = {
@ -22,7 +24,6 @@ const moviesFilter = {
extensions: ["mp4", "mkv"], extensions: ["mp4", "mkv"],
}; };
const metadataAudioSize = 3;
const metadataAudio = `-metadata:s:a:0 title="System sounds and microphone" \ const metadataAudio = `-metadata:s:a:0 title="System sounds and microphone" \
-metadata:s:a:1 title="System sounds" \ -metadata:s:a:1 title="System sounds" \
-metadata:s:a:2 title="Microphone"`; -metadata:s:a:2 title="Microphone"`;
@ -90,9 +91,9 @@ app.whenReady().then(() => {
const tmpFile = getNewFilename(file, "TMP_"); const tmpFile = getNewFilename(file, "TMP_");
let outFile; let outFile;
let audioTracks = getNumberOfAudioTracks(file); const nbTracks = getNumberOfAudioTracks(file);
switch (audioTracks.length) { switch (nbTracks) {
case 2: case 2:
// Merge 2 audio // Merge 2 audio
// See: https://trac.ffmpeg.org/wiki/AudioChannelManipulation#a2stereostereo // See: https://trac.ffmpeg.org/wiki/AudioChannelManipulation#a2stereostereo
@ -121,8 +122,6 @@ app.whenReady().then(() => {
// Delete the temporary video file // Delete the temporary video file
deleteFile(tmpFile); deleteFile(tmpFile);
audioTracks = getNumberOfAudioTracks(outFile);
break; break;
default: default:
// Other cases: no merge needed // Other cases: no merge needed
@ -140,75 +139,49 @@ app.whenReady().then(() => {
title: outFile, title: outFile,
size: stats.size / 1024 / 1024, size: stats.size / 1024 / 1024,
duration, duration,
audioTracks, nbTracks,
}; };
}; };
/** Reduce size of a file /** Reduce size of a file */
* Returns an empty string in case of failing
*/
const reduceSize = async ( const reduceSize = async (
file: string, file: string,
bitrate: number, bitrate: number,
audioTracks: number[] nbTracks: number
) => { ) => {
const audioBitrate = Math.ceil( const audioBitrate = 500; // keep some room
audioTracks.reduce((sum, current) => current + sum, 50) // initial value > 0 for extra room let videoBitrate = bitrate - audioBitrate;
);
const videoBitrate = bitrate - audioBitrate;
let finalFile;
if (videoBitrate > 0) { const finalFile = getNewFilename(file, "Compressed - ");
finalFile = getNewFilename(file, "Compressed - ");
// Trash the output, depends on the platform // Trash the output, depends on the platform
const nul = process.platform === "win32" ? "NUL" : "/dev/null"; const nul = process.platform === "win32" ? "NUL" : "/dev/null";
// Mapping of tracks for FFMPEG, adding 1 for the video stream // Mapping of tracks for FFMPEG, adding 1 for the video stream
const mappingTracks = Array(audioTracks.length + 1) const mappingTracks = Array(nbTracks + 1)
.fill("-map 0:") .fill("-map 0:")
.map((str, index) => { .map((str, index) => {
return str + index; return str + index;
}) })
.join(" "); .join(" ");
let codec = "libx264"; let codec = "libx264";
let hwAcc = ""; let hwAcc = "";
const argv = process.argv; const argv = process.argv;
if (argv.includes("/nvenc_h264")) { if (argv.includes("/nvenc")) {
// Use NVenc H.264 // Use NVenc
codec = "h264_nvenc"; codec = "h264_nvenc";
hwAcc = "-hwaccel cuda"; hwAcc = "-hwaccel cuda";
}
if (argv.includes("/amd_h264")) { // Increase video bitrate
// Use AMF H.264 videoBitrate = Math.floor(videoBitrate * 1.7);
codec = "h264_amf"; }
hwAcc = "-hwaccel d3d11va";
}
if (argv.includes("/nvenc_h265")) { // Compress the video
// Use NVenc H.265 // Add metadata to audio's track
codec = "hevc_nvenc"; await execute(
hwAcc = "-hwaccel cuda"; `"${ffmpegPath}" -y ${hwAcc} \
}
if (argv.includes("/amd_h265")) {
// Use AMF H.265
codec = "hevc_amf";
hwAcc = "-hwaccel d3d11va";
}
if (argv.includes("/h265")) {
// Use H.265 encoder
codec = "libx265";
}
// Compress the video
// Add metadata to audio's track
await execute(
`"${ffmpegPath}" -y ${hwAcc} \
-i "${file}" \ -i "${file}" \
-c:v ${codec} -b:v ${videoBitrate}k -pass 1 -an -f mp4 \ -c:v ${codec} -b:v ${videoBitrate}k -pass 1 -an -f mp4 \
${nul} \ ${nul} \
@ -217,26 +190,22 @@ app.whenReady().then(() => {
-i "${file}" \ -i "${file}" \
-c:v ${codec} -b:v ${videoBitrate}k -pass 2 -c:a copy \ -c:v ${codec} -b:v ${videoBitrate}k -pass 2 -c:a copy \
${mappingTracks} -f mp4 \ ${mappingTracks} -f mp4 \
-profile:v main \ ${metadataAudio} \
${audioTracks.length === metadataAudioSize ? metadataAudio : ""} \
${shareOpt} \ ${shareOpt} \
"${finalFile}"` "${finalFile}"`
).catch((e) => registerError(win, e)); ).catch((e) => registerError(win, e));
// Delete the 2 pass temporary files
deleteTwoPassFiles(process.cwd());
} else {
finalFile = "";
}
// Delete the old video file // Delete the old video file
deleteFile(file); deleteFile(file);
// Delete the 2 pass temporary files
deleteTwoPassFiles(process.cwd());
return finalFile; return finalFile;
}; };
/** Move metadata at the begenning of the file */ /** Move metadata at the begenning of the file */
const moveMetadata = async (file: string, nbTracks: number) => { const moveMetadata = async (file: string) => {
const finalFile = getNewFilename(file, "Broadcastable - "); const finalFile = getNewFilename(file, "Broadcastable - ");
// Optimize for streaming // Optimize for streaming
@ -245,7 +214,6 @@ app.whenReady().then(() => {
-i "${file}" \ -i "${file}" \
-map 0 -codec copy \ -map 0 -codec copy \
${shareOpt} \ ${shareOpt} \
${nbTracks === metadataAudioSize ? metadataAudio : ""} \
"${finalFile}"` "${finalFile}"`
).catch((e) => registerError(win, e)); ).catch((e) => registerError(win, e));
@ -263,19 +231,17 @@ app.whenReady().then(() => {
ipcMain.handle("mergeAudio", (_, file: string) => mergeAudio(file)); ipcMain.handle("mergeAudio", (_, file: string) => mergeAudio(file));
ipcMain.handle( ipcMain.handle(
"reduceSize", "reduceSize",
(_, file: string, bitrate: number, audioTracks: number[]) => (_, file: string, bitrate: number, nbTracks: number) =>
reduceSize(file, bitrate, audioTracks) reduceSize(file, bitrate, nbTracks)
);
ipcMain.handle("moveMetadata", (_, file: string, nbTracks: number) =>
moveMetadata(file, nbTracks)
); );
ipcMain.handle("moveMetadata", (_, file: string) => moveMetadata(file));
ipcMain.handle("exit", () => (error ? {} : app.quit())); ipcMain.handle("exit", () => (error ? {} : app.quit()));
ipcMain.handle("confirmation", (_, text: string) => confirmation(text)); ipcMain.handle("confirmation", (_, text: string) => confirmation(text));
}); });
app.on("window-all-closed", () => { app.on("window-all-closed", () => {
processes.forEach((process) => { processes.forEach((process) => {
process.stdin.write("q"); kill(process.pid);
}); });
app.quit(); app.quit();

View file

@ -13,10 +13,9 @@ contextBridge.exposeInMainWorld("internals", {
ipcRenderer.invoke("getFilename", filepath), ipcRenderer.invoke("getFilename", filepath),
askFiles: () => ipcRenderer.invoke("askFiles"), askFiles: () => ipcRenderer.invoke("askFiles"),
mergeAudio: (file: string) => ipcRenderer.invoke("mergeAudio", file), mergeAudio: (file: string) => ipcRenderer.invoke("mergeAudio", file),
reduceSize: (file: string, bitrate: number, audioTracks: number[]) => reduceSize: (file: string, bitrate: number, nbTracks: number) =>
ipcRenderer.invoke("reduceSize", file, bitrate, audioTracks), ipcRenderer.invoke("reduceSize", file, bitrate, nbTracks),
moveMetadata: (file: string, nbTracks: number) => moveMetadata: (file: string) => ipcRenderer.invoke("moveMetadata", file),
ipcRenderer.invoke("moveMetadata", file, nbTracks),
exit: () => ipcRenderer.invoke("exit"), exit: () => ipcRenderer.invoke("exit"),
confirmation: (text: string) => ipcRenderer.invoke("confirmation", text), confirmation: (text: string) => ipcRenderer.invoke("confirmation", text),
}); });

View file

@ -11,14 +11,14 @@ let internals: {
title: string; title: string;
duration: number; duration: number;
size: number; size: number;
audioTracks: number[]; nbTracks: number;
}>; }>;
reduceSize: ( reduceSize: (
file: string, file: string,
bitrate: number, bitrate: number,
audioTracks: number[] nbTracks: number
) => Promise<string>; ) => Promise<string>;
moveMetadata: (file: string, nbTracks: number) => Promise<string>; moveMetadata: (file: string) => Promise<string>;
confirmation: (text: string) => Promise<void>; confirmation: (text: string) => Promise<void>;
}; };
@ -56,16 +56,13 @@ const getFiles = async () => {
/** Returns maximum allowed size for files in MB */ /** Returns maximum allowed size for files in MB */
const fetchMaxSize = async () => { const fetchMaxSize = async () => {
const argv = await internals.argv(); const argv = await internals.argv();
if (argv.includes("/nitrobasic")) { if (argv.includes("/nitro")) {
// Nitro Basic user
return 50;
} else if (argv.includes("/nitro")) {
// Nitro user // Nitro user
return 500; return 500;
} else {
// Free user
return 10;
} }
// Free user
return 25;
}; };
/** Either replace the message, or add some info */ /** Either replace the message, or add some info */
@ -103,7 +100,6 @@ const main = async () => {
updateMessage("Récupération des fichiers..."); updateMessage("Récupération des fichiers...");
const files = await getFiles(); const files = await getFiles();
let processedFiles = ""; let processedFiles = "";
let numberOfUncompressableFiles = 0;
// Iterate over all the retrieved files // Iterate over all the retrieved files
for (const [idx, file] of files.entries()) { for (const [idx, file] of files.entries()) {
@ -136,39 +132,24 @@ const main = async () => {
finalTitle = await internals.reduceSize( finalTitle = await internals.reduceSize(
newFile.title, newFile.title,
bitrate, bitrate,
newFile.audioTracks newFile.nbTracks
); );
} else { } else {
updateMessage(`\nPréparation pour le partage...`, true, Mode.Append); updateMessage(`\nPréparation pour le partage...`, true, Mode.Append);
// Move the metadata to make it playable before everything is downloaded // Move the metadata to make it playable before everything is downloaded
finalTitle = await internals.moveMetadata( finalTitle = await internals.moveMetadata(newFile.title);
newFile.title,
newFile.audioTracks.length
);
} }
// Append title to the list of processed files // Append title to the list of processed files
if (finalTitle.length > 0) { processedFiles += `\n- ${finalTitle}`;
processedFiles += `\n- ${finalTitle}`; updateMessage(`Fichier ${counter} traités.`);
updateMessage(`Fichier ${counter} traités.`);
} else {
processedFiles += `\n- ${file} [incompressable]`;
updateMessage(`Fichier ${counter} trop large pour être compressé.`);
numberOfUncompressableFiles++;
}
}
let errorMessage = "";
if (numberOfUncompressableFiles > 0) {
errorMessage += `\nNombre de fichier incompressable : ${numberOfUncompressableFiles}.`;
} }
// Send confirmation to the user that we're done // Send confirmation to the user that we're done
await internals.confirmation( await internals.confirmation(
`${files.length} fichiers traités : ${processedFiles}` + errorMessage `${files.length} fichiers traités : ${processedFiles}`
); );
await internals.exit(); await internals.exit();
}; };

View file

@ -22,12 +22,10 @@ export const getVideoDuration = (file: string) => {
}; };
/** Return the number of audio tracks */ /** Return the number of audio tracks */
export const getNumberOfAudioTracks = (file: string): number[] => { export const getNumberOfAudioTracks = (file: string) => {
const command = `"${ffprobePath}" -v error -show_entries stream=bit_rate -select_streams a -of json "${file}"`; const command = `"${ffprobePath}" -v error -show_entries stream=index -select_streams a -of json "${file}"`;
const result = child_process.execSync(command, { encoding: "utf8" }); const result = child_process.execSync(command, { encoding: "utf8" });
return JSON.parse(result).streams.map( return JSON.parse(result).streams.length;
(v: { bit_rate: string }) => Number(v.bit_rate) / 1000
);
}; };
/** Print an error to the console and open the dev tool panel */ /** Print an error to the console and open the dev tool panel */