Compare commits

...

39 commits
1.11.0 ... main

Author SHA1 Message Date
87f98a586e
Update install.ps1 2025-07-07 21:01:01 +02:00
74b5f09442
bump version to 1.14.1
All checks were successful
Upload release / build (push) Successful in 4m36s
2025-07-07 00:24:42 +02:00
c28e4559ca
missing "k" 2025-07-07 00:24:21 +02:00
4c2c5f419a
bump version to 1.14.0
All checks were successful
Upload release / build (push) Successful in 6m48s
2025-07-06 23:54:49 +02:00
4267038983
fmt 2025-07-06 23:53:45 +02:00
c1168d1d58
Merge pull request 'feat: audio compression' (#30) from audio-compression into main
Reviewed-on: #30
2025-07-06 23:52:12 +02:00
010610bd5c
audio compression 2025-07-06 23:48:49 +02:00
6c9cd5deb4
update dependencies 2025-07-06 23:43:14 +02:00
604d7a0268
bump to 1.13.0
All checks were successful
Upload release / build (push) Successful in 5m20s
2025-05-19 23:05:51 +02:00
e91aed0020
format 2025-05-19 23:05:35 +02:00
13a8ef86cb
use local install when available (#28) 2025-05-19 23:04:11 +02:00
ac60082b59
update dependencies 2025-05-19 22:44:06 +02:00
c11f2d1d32
bump version to 1.12.3
All checks were successful
Upload release / build (push) Successful in 4m5s
2025-04-30 23:07:58 +02:00
c1a172d5d7
experimental fix of hw acc on Linux 2025-04-30 23:07:38 +02:00
44f6b0d48b
bump to 1.12.2
All checks were successful
Upload release / build (push) Successful in 4m19s
2025-04-30 21:40:44 +02:00
112770ec68
Update renderer.ts 2025-04-30 21:39:10 +02:00
f6feddd4c3
useless async 2025-04-30 21:38:31 +02:00
397c71f9da
accept more fullpath file, when possible 2025-04-30 21:28:18 +02:00
812a216902
correctly use the icon in linux systems 2025-04-30 21:01:45 +02:00
56936cda9f
fmt 2025-04-30 20:39:50 +02:00
77c0edb15a
Update README.md 2025-04-30 19:47:06 +02:00
1f0d18de51
bump version to 1.12.1
All checks were successful
Upload release / build (push) Successful in 3m54s
2025-04-30 19:08:13 +02:00
06851a2c3c
Update dependencies 2025-04-30 19:07:42 +02:00
02a2059552
Add icon as PNG for Linux builds 2025-04-30 19:05:34 +02:00
e5e8f002d8
bump version to 1.12.0
All checks were successful
Upload release / build (push) Successful in 4m40s
2025-04-25 00:13:31 +02:00
b63a38a21b
Merge pull request 'chore: release linux version' (#27) from linux-port into main
Reviewed-on: #27
2025-04-24 23:57:53 +02:00
41b59d65b0
correctly attach assets
Some checks failed
Upload release / build (push) Has been cancelled
2025-04-24 21:33:10 +02:00
2e1179aa27
fix workflow 2025-04-24 21:11:21 +02:00
0907480386
allow build of linux version 2025-04-24 20:45:47 +02:00
01f0d2702f
clean up 2025-04-24 20:45:33 +02:00
2f8e02a29d
bump dependencies 2025-04-24 20:22:40 +02:00
0010aa0a33
bump dependencie to 1.11.3
All checks were successful
Upload release / build (push) Successful in 2m31s
2024-11-13 02:02:12 +01:00
74b5be4e3b
add amd support 2024-11-13 02:02:01 +01:00
e1aad3fb1e
update dependencies 2024-11-13 01:42:08 +01:00
f590f77bca
bump version to 1.11.2
All checks were successful
Upload release / build (push) Successful in 1m45s
2024-10-16 01:08:36 +02:00
ecb03ded2a
add extra room 2024-10-16 01:08:17 +02:00
21b39e80bf
update dependencies 2024-10-16 01:01:59 +02:00
55b94d0021
bump version to 1.11.1
All checks were successful
Upload release / build (push) Successful in 1m54s
2024-10-01 16:55:12 +02:00
4f34191d2f
don't crash when trying to compress an enormous file 2024-10-01 16:55:06 +02:00
11 changed files with 509 additions and 963 deletions

View file

@ -7,22 +7,35 @@ on:
jobs:
build:
runs-on: docker
container:
image: node
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install dependencies
run: |
npm install --platform=win32 &&
apt-get update &&
apt-get install -y zip
- name: Build the app
run: npm run make -- --platform=win32
- name: Build the app for Windows
run: |
npm install --platform=win32 &&
npm run make -- --platform=win32
- name: Clear the node packages
run: rm -rf node_modules/ package-lock.json
- name: Build the app for Linux
run: |
npm install --platform=linux &&
npm run make -- --platform=linux
- name: Create release
uses: akkuman/gitea-release-action@v1
with:
token: ${{ secrets.TOKEN }}
files: out/make/zip/win32/x64/dsr-win32-x64-${{ github.ref_name }}.zip
files: out/make/zip/**/*.zip
draft: true

View file

@ -2,8 +2,6 @@
Tool for sharing video to Discord.
> This tool was primarily made for video captured by NVidia Shadowplay.
## Download/Install/Update
2 choices :
@ -15,20 +13,27 @@ Tool for sharing video to Discord.
irm https://git.mylloon.fr/Anri/dsr/raw/branch/main/install.ps1 | iex
```
### Linux
Install from AUR: [dsr](https://aur.archlinux.org/packages/dsr)
## Available flags
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) |
| `/h265` | Enable the H.265 CPU encoder (slow compression) |
| | |
| ------------- | ------------------------------------------------------ |
| `/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) |
> NVenc support is experimental, but faster than CPU counterparts.
> NVidia and AMD hardware accelerators support is experimental, but faster
> than CPU counterparts.
## More info
@ -38,6 +43,7 @@ You can add thoses flags in the `Target` field of your Windows shortcut.
- [x] Defaults to H.264 CPU encoder
- [x] If already under the limit, the file won't be compressed
- [x] NVenc support
- [x] AMD cards acceleration support
- [x] Nitro suppport
- [x] Merge 2 audio files into one track when recorded with system audio and microphone
split up, while keeping the original ones (with conveniant metadata)

BIN
image/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -7,12 +7,13 @@ param (
$path = "$env:LOCALAPPDATA\DSR"
$update = Test-Path -Path $path\*
$iwa = "-UserAgent 'confOS'"
# Download
$releases = "https://git.mylloon.fr/api/v1/repos/Anri/dsr/releases/latest"
$link = (Invoke-WebRequest $releases | ConvertFrom-Json)[0].assets.browser_download_url
$link = (Invoke-WebRequest $iwa $releases | ConvertFrom-Json)[0].assets.browser_download_url
$archive = "$env:TEMP\dsr.zip"
Invoke-WebRequest -Uri $link -OutFile $archive
Invoke-WebRequest $iwa -Uri $link -OutFile $archive
Remove-Item "$path" -Recurse -ErrorAction SilentlyContinue
# Close running DSR

1144
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "dsr",
"version": "1.11.0",
"version": "1.14.1",
"description": "Discord Video Sharing",
"main": "./dist/main.js",
"scripts": {
@ -18,15 +18,15 @@
"author": "Mylloon",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@electron-forge/maker-zip": "^7.4",
"@electron-forge/maker-zip": "^7.8",
"ffmpeg-static": "^5.2",
"ffprobe-static": "^3.1",
"typescript": "^5.6"
"typescript": "^5.8"
},
"devDependencies": {
"@electron-forge/cli": "^7.4",
"@electron-forge/cli": "^7.8",
"@types/ffprobe-static": "^2.0",
"electron": "^32.1"
"electron": "^37.2"
},
"config": {
"forge": {
@ -40,7 +40,8 @@
{
"name": "@electron-forge/maker-zip",
"platforms": [
"win32"
"win32",
"linux"
]
}
]

View file

@ -1,15 +1,9 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<meta
http-equiv="X-Content-Security-Policy"
content="default-src 'self'; script-src 'self'"
/>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
<meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
<link rel="stylesheet" type="text/css" href="../css/style.css" />

View file

@ -13,7 +13,15 @@ import {
import path = require("path");
import ffmpeg = require("ffmpeg-static");
const ffmpegPath = `${ffmpeg}`.replace("app.asar", "app.asar.unpacked");
let ffmpegPath;
try {
ffmpegPath = "ffmpeg";
require("child_process").execSync(`${ffmpegPath} -version`, {
stdio: "ignore",
});
} catch {
ffmpegPath = `${ffmpeg}`.replace("app.asar", "app.asar.unpacked");
}
let error = false;
@ -35,12 +43,14 @@ const registerError = (win: BrowserWindow, err: string) => {
printAndDevTool(win, err);
};
const onWindows = process.platform === "win32";
/** Create a new window */
const createWindow = () => {
const win = new BrowserWindow({
width: 600,
height: 340,
icon: "./image/icon.ico",
icon: "./image/icon." + (onWindows ? "ico" : "png"),
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, "preload.js"),
@ -53,7 +63,7 @@ const createWindow = () => {
};
// For notification on Windows
if (process.platform === "win32") {
if (onWindows) {
app.setAppUserModelId(app.name);
}
@ -101,7 +111,7 @@ app.whenReady().then(() => {
-i "${file}" \
-filter_complex "[0:a]amerge=inputs=2[a]" -ac 2 -map 0:v -map "[a]" \
-c:v copy \
"${tmpFile}"`
"${tmpFile}"`,
);
outFile = getNewFilename(file, "(merged audio) ");
@ -115,7 +125,7 @@ app.whenReady().then(() => {
-map 0 -map 1:a -c:v copy \
-disposition:a 0 -disposition:a:0 default \
${metadataAudio} \
"${outFile}"`
"${outFile}"`,
).catch((e) => registerError(win, e));
// Delete the temporary video file
@ -144,75 +154,91 @@ app.whenReady().then(() => {
};
};
/** Reduce size of a file */
const reduceSize = async (
file: string,
bitrate: number,
audioTracks: number[]
) => {
const audioBitrate = Math.ceil(
audioTracks.reduce((sum, current) => current + sum, 0)
);
let videoBitrate = bitrate - audioBitrate;
/** Reduce size of a file
* Returns an empty string in case of failing
*/
const reduceSize = async (file: string, bitrate: number, audioTracks: number[]) => {
const audioBitratePerTrack = 128; // kbps
const finalFile = getNewFilename(file, "Compressed - ");
// Calculate audio bitrate
const audioBitrate = audioTracks.length * audioBitratePerTrack;
const videoBitrate = bitrate - audioBitrate;
let finalFile;
// Trash the output, depends on the platform
const nul = process.platform === "win32" ? "NUL" : "/dev/null";
if (videoBitrate > 0) {
finalFile = getNewFilename(file, "Compressed - ");
// Mapping of tracks for FFMPEG, adding 1 for the video stream
const mappingTracks = Array(audioTracks.length + 1)
.fill("-map 0:")
.map((str, index) => {
return str + index;
})
.join(" ");
// Trash the output, depends on the platform
const nul = onWindows ? "NUL" : "/dev/null";
let codec = "libx264";
let hwAcc = "";
// Mapping of tracks for FFMPEG, adding 1 for the video stream
const mappingTracks = Array(audioTracks.length + 1)
.fill("-map 0:")
.map((str, index) => {
return str + index;
})
.join(" ");
const argv = process.argv;
if (argv.includes("/nvenc_h264")) {
// Use NVenc H.264
codec = "h264_nvenc";
hwAcc = "-hwaccel cuda";
let codec = "libx264";
let hwAcc = "";
const argv = process.argv;
if (argv.includes("/nvenc_h264")) {
// Use NVenc H.264
codec = "h264_nvenc";
hwAcc = onWindows ? "-hwaccel cuda" : "-hwaccel cuda -hwaccel_output_format cuda";
}
if (argv.includes("/amd_h264")) {
// Use AMF H.264
codec = onWindows ? "h264_amf" : "h264_vaapi";
hwAcc = onWindows ? "-hwaccel d3d11va" : "-hwaccel vaapi -hwaccel_output_format vaapi";
}
if (argv.includes("/nvenc_h265")) {
// Use NVenc H.265
codec = "hevc_nvenc";
hwAcc = onWindows ? "-hwaccel cuda" : "-hwaccel cuda -hwaccel_output_format cuda";
}
if (argv.includes("/amd_h265")) {
// Use AMF H.265
codec = onWindows ? "hevc_amf" : "hevc_vaapi";
hwAcc = onWindows ? "-hwaccel d3d11va" : "-hwaccel vaapi -hwaccel_output_format vaapi";
}
if (argv.includes("/h265")) {
// Use H.265 encoder
codec = "libx265";
}
// Compress the video with AAC audio compression
// Add metadata to audio's track
await execute(
`"${ffmpegPath}" -y ${hwAcc} \
-i "${file}" \
-c:v ${codec} -b:v ${videoBitrate}k -pass 1 -an -f mp4 \
${nul} \
&& \
"${ffmpegPath}" -y ${hwAcc} \
-i "${file}" \
-c:v ${codec} -b:v ${videoBitrate}k -pass 2 -c:a aac -b:a ${audioBitratePerTrack}k \
${mappingTracks} -f mp4 \
-profile:v main \
${audioTracks.length === metadataAudioSize ? metadataAudio : ""} \
${shareOpt} \
"${finalFile}"`,
).catch((e) => registerError(win, e));
// Delete the 2 pass temporary files
deleteTwoPassFiles(process.cwd());
} else {
finalFile = "";
}
if (argv.includes("/nvenc_h265")) {
// Use NVenc H.265
codec = "hevc_nvenc";
hwAcc = "-hwaccel cuda";
}
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}" \
-c:v ${codec} -b:v ${videoBitrate}k -pass 1 -an -f mp4 \
${nul} \
&& \
"${ffmpegPath}" -y ${hwAcc} \
-i "${file}" \
-c:v ${codec} -b:v ${videoBitrate}k -pass 2 -c:a copy \
${mappingTracks} -f mp4 \
-profile:v main \
${audioTracks.length === metadataAudioSize ? metadataAudio : ""} \
${shareOpt} \
"${finalFile}"`
).catch((e) => registerError(win, e));
// Delete the old video file
deleteFile(file);
// Delete the 2 pass temporary files
deleteTwoPassFiles(process.cwd());
return finalFile;
};
@ -227,7 +253,7 @@ app.whenReady().then(() => {
-map 0 -codec copy \
${shareOpt} \
${nbTracks === metadataAudioSize ? metadataAudio : ""} \
"${finalFile}"`
"${finalFile}"`,
).catch((e) => registerError(win, e));
// Delete the old video file
@ -238,17 +264,16 @@ app.whenReady().then(() => {
/* Context bridge */
ipcMain.handle("argv", () => process.argv);
ipcMain.handle("cwd", process.cwd);
ipcMain.handle("allowedExtensions", () => moviesFilter);
ipcMain.handle("getFilename", (_, filepath: string) => getFilename(filepath));
ipcMain.handle("askFiles", () => askFiles());
ipcMain.handle("mergeAudio", (_, file: string) => mergeAudio(file));
ipcMain.handle(
"reduceSize",
(_, file: string, bitrate: number, audioTracks: number[]) =>
reduceSize(file, bitrate, audioTracks)
ipcMain.handle("reduceSize", (_, file: string, bitrate: number, audioTracks: number[]) =>
reduceSize(file, bitrate, audioTracks),
);
ipcMain.handle("moveMetadata", (_, file: string, nbTracks: number) =>
moveMetadata(file, nbTracks)
moveMetadata(file, nbTracks),
);
ipcMain.handle("exit", () => (error ? {} : app.quit()));
ipcMain.handle("confirmation", (_, text: string) => confirmation(text));

View file

@ -8,9 +8,9 @@ ipcRenderer.on("error", (_, err) => {
/* Context bridge */
contextBridge.exposeInMainWorld("internals", {
argv: () => ipcRenderer.invoke("argv"),
cwd: () => ipcRenderer.invoke("cwd"),
allowedExtensions: () => ipcRenderer.invoke("allowedExtensions"),
getFilename: (filepath: string) =>
ipcRenderer.invoke("getFilename", filepath),
getFilename: (filepath: string) => ipcRenderer.invoke("getFilename", filepath),
askFiles: () => ipcRenderer.invoke("askFiles"),
mergeAudio: (file: string) => ipcRenderer.invoke("mergeAudio", file),
reduceSize: (file: string, bitrate: number, audioTracks: number[]) =>

View file

@ -1,6 +1,7 @@
/** Context bridge types */
let internals: {
argv: () => Promise<string[]>;
cwd: () => Promise<string>;
allowedExtensions: () => Promise<{
extensions: string[];
}>;
@ -13,11 +14,7 @@ let internals: {
size: number;
audioTracks: number[];
}>;
reduceSize: (
file: string,
bitrate: number,
audioTracks: number[]
) => Promise<string>;
reduceSize: (file: string, bitrate: number, audioTracks: number[]) => Promise<string>;
moveMetadata: (file: string, nbTracks: number) => Promise<string>;
confirmation: (text: string) => Promise<void>;
};
@ -25,9 +22,17 @@ let internals: {
/** Search for files */
const getFiles = async () => {
const allowedExtensions = (await internals.allowedExtensions()).extensions;
const currentDir = await internals.cwd();
const argvFiles = (await internals.argv())
.slice(1)
.filter((element) => !element.startsWith("/"));
.filter((element) => {
if (element.startsWith("/")) {
// Slash either fullpath files, or commands args
return element.startsWith(currentDir);
}
return true;
})
.map((element) => element.split("/").pop());
if (argvFiles.length > 0) {
const files = argvFiles;
@ -35,9 +40,7 @@ const getFiles = async () => {
// Exit if a file isn't supported in the list
if (
files.filter((file) =>
allowedExtensions.some((ext) =>
file.toLowerCase().endsWith(ext.toLowerCase())
)
allowedExtensions.some((ext) => file.toLowerCase().endsWith(ext.toLowerCase())),
).length !== files.length
) {
await internals.exit();
@ -75,11 +78,7 @@ enum Mode {
}
/** Update the message to the user */
const updateMessage = (
message: string,
load: boolean = false,
mode: Mode = Mode.Write
) => {
const updateMessage = (message: string, load: boolean = false, mode: Mode = Mode.Write) => {
switch (mode) {
case Mode.Write:
document.getElementById("message").innerText = message;
@ -92,9 +91,7 @@ const updateMessage = (
default:
break;
}
document.getElementById("load").style.visibility = load
? "visible"
: "hidden";
document.getElementById("load").style.visibility = load ? "visible" : "hidden";
};
/** Main function */
@ -103,20 +100,16 @@ const main = async () => {
updateMessage("Récupération des fichiers...");
const files = await getFiles();
let processedFiles = "";
let numberOfUncompressableFiles = 0;
// Iterate over all the retrieved files
for (const [idx, file] of files.entries()) {
const counter = `${idx + 1}/${files.length}`;
const filename = await internals.getFilename(file);
updateMessage(
`${counter} - Mélange des pistes audios de ${filename}...`,
true
);
updateMessage(`${counter} - Mélange des pistes audios de ${filename}...`, true);
const newFile = await internals.mergeAudio(file);
let finalTitle = newFile.title;
updateMessage(
`${counter} - Taille calculée : ~${Math.round(newFile.size)}Mio`
);
updateMessage(`${counter} - Taille calculée : ~${Math.round(newFile.size)}Mio`);
// Compress video if needed
if (newFile.size > maxSizeDiscord) {
@ -128,34 +121,39 @@ const main = async () => {
updateMessage(
`\nFichier trop lourd, compression en cours... (bitrate total = ${bitrate}kbps)`,
true,
Mode.Append
Mode.Append,
);
// Compress the video and change the title to the new one
finalTitle = await internals.reduceSize(
newFile.title,
bitrate,
newFile.audioTracks
);
finalTitle = await internals.reduceSize(newFile.title, bitrate, newFile.audioTracks);
} else {
updateMessage(`\nPréparation pour le partage...`, true, Mode.Append);
// Move the metadata to make it playable before everything is downloaded
finalTitle = await internals.moveMetadata(
newFile.title,
newFile.audioTracks.length
);
finalTitle = await internals.moveMetadata(newFile.title, newFile.audioTracks.length);
}
// Append title to the list of processed files
processedFiles += `\n- ${finalTitle}`;
updateMessage(`Fichier ${counter} traités.`);
if (finalTitle.length > 0) {
processedFiles += `\n- ${finalTitle}`;
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
await internals.confirmation(
`${files.length} fichiers traités : ${processedFiles}`
`${files.length} fichiers traités : ${processedFiles}` + errorMessage,
);
await internals.exit();
};

View file

@ -25,9 +25,7 @@ export const getVideoDuration = (file: string) => {
export const getNumberOfAudioTracks = (file: string): number[] => {
const command = `"${ffprobePath}" -v error -show_entries stream=bit_rate -select_streams a -of json "${file}"`;
const result = child_process.execSync(command, { encoding: "utf8" });
return JSON.parse(result).streams.map(
(v: { bit_rate: string }) => Number(v.bit_rate) / 1000
);
return JSON.parse(result).streams.map((v: { bit_rate: string }) => Number(v.bit_rate) / 1000);
};
/** Print an error to the console and open the dev tool panel */
@ -37,9 +35,7 @@ export const printAndDevTool = (win: BrowserWindow, err: string) => {
};
/** Run a command asynchronously */
export const execute = (
command: string
): Promise<{ stdout: string; stderr: string }> => {
export const execute = (command: string): Promise<{ stdout: string; stderr: string }> => {
return new Promise((resolve, reject) => {
const process = child_process.exec(command, (error, stdout, stderr) => {
if (error) {