Compare commits

..

1 commit

Author SHA1 Message Date
4e8a3fd179
Create release.yml 2023-09-24 19:48:58 +02:00
12 changed files with 249 additions and 325 deletions

47
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,47 @@
name: Release
permissions:
contents: write
on:
push:
tags:
- v[0-9]+.*
jobs:
create-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: taiki-e/create-gh-release-action@v1
with:
# (required) GitHub token for creating GitHub Releases.
token: ${{ secrets.GITHUB_TOKEN }}
upload-assets:
strategy:
matrix:
include:
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
- target: aarch64-apple-darwin
os: macos-latest
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
- target: x86_64-apple-darwin
os: macos-latest
# Universal macOS binary is supported as universal-apple-darwin.
- target: universal-apple-darwin
os: macos-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: taiki-e/upload-rust-binary-action@v1
with:
# (required) Comma-separated list of binary names (non-extension portion of filename) to build and upload.
# Note that glob pattern is not supported yet.
bin: ...
# (optional) Target triple, default is host triple.
target: ${{ matrix.target }}
# (required) GitHub token for uploading assets to GitHub Releases.
token: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View file

@ -1,5 +1,4 @@
/target /target
*.log
*.webp *.webp
*.webm *.webm
*.ogg *.ogg

104
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 3
[[package]] [[package]]
name = "addr2line" name = "addr2line"
@ -71,6 +71,12 @@ version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.69" version = "0.3.69"
@ -87,10 +93,16 @@ dependencies = [
] ]
[[package]] [[package]]
name = "bytes" name = "bitflags"
version = "1.6.0" version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bytes"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
[[package]] [[package]]
name = "cc" name = "cc"
@ -186,9 +198,9 @@ checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.9" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]] [[package]]
name = "indicatif" name = "indicatif"
@ -224,6 +236,16 @@ version = "0.2.148"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b"
[[package]]
name = "lock_api"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.6.3" version = "2.6.3"
@ -252,7 +274,7 @@ dependencies = [
[[package]] [[package]]
name = "n-mb" name = "n-mb"
version = "1.1.3" version = "1.0.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@ -286,6 +308,29 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets 0.48.5",
]
[[package]] [[package]]
name = "pbr" name = "pbr"
version = "1.1.1" version = "1.1.1"
@ -311,28 +356,43 @@ checksum = "31114a898e107c51bb1609ffaf55a0e011cf6a4d7f1170d0015a165082c0338b"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.81" version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.36" version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "redox_syscall"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.23" version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.1" version = "1.4.1"
@ -342,6 +402,22 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "smallvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
[[package]]
name = "socket2"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
dependencies = [
"libc",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
@ -350,9 +426,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.60" version = "2.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -370,8 +446,10 @@ dependencies = [
"libc", "libc",
"mio", "mio",
"num_cpus", "num_cpus",
"parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2",
"tokio-macros", "tokio-macros",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]

View file

@ -1,7 +1,7 @@
[package] [package]
name = "n-mb" name = "n-mb"
authors = ["Djkáťo <djkatovfx@gmail.com>"] authors = ["Djkáťo <djkatovfx@gmail.com>"]
version = "1.1.3" version = "1.0.0"
edition = "2021" edition = "2021"
description = "Simple ffmpeg wrapper to parse files to the most efficient formats within a set size" description = "Simple ffmpeg wrapper to parse files to the most efficient formats within a set size"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
@ -10,30 +10,13 @@ repository = "https://github.com/djkato/n-mb"
readme = "README.md" readme = "README.md"
keywords = ["media", "ffmpeg", "cli"] keywords = ["media", "ffmpeg", "cli"]
categories = ["command-line-utilities"] categories = ["command-line-utilities"]
exclude = ["*.mp4", "*.mp3", "*.jpg", "*.ogg", "*.webp", "*.webm", "*.log"]
[dependencies] [dependencies]
anyhow = "1.0.75" anyhow = "1.0.75"
clap = { version = "4.4.4", features = ["cargo"] } clap = { version = "4.4.4", features = ["cargo"] }
indicatif = "0.17.7" indicatif = "0.17.7"
pbr = "1.1.1" pbr = "1.1.1"
tokio = { version = "1.32.0", features = [ tokio = { version = "1.32.0", features = ["full"] }
"macros",
"rt-multi-thread",
"process",
"io-std",
"sync",
"time",
"io-util",
] }
[[bin]] [[bin]]
name = "nmb" name="nmb"
path = "src/main.rs"
[profile.release]
opt-level = 'z' # Optimize for size
lto = true # Enable link-time optimization
codegen-units = 1 # Reduce number of codegen units to increase optimizations
panic = 'abort' # Abort on panic
# strip = true # Strip symbols from binary*

View file

@ -1 +0,0 @@
ko_fi: djkato

View file

@ -1,26 +1,15 @@
<a href='https://ko-fi.com/A0A8Q3SVZ' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi4.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>
# Automatically converts any media file and makes sure its under your limit! # Automatically converts any media file and makes sure its under your limit!
For all those who want to post memes that are just too big and surpass the 25mb free upload limit on discord, this is the app for you! For all those who want to post memes that are just too big and surpass the 25mb free upload limit on discord, this is the app for you!
![nbm usage](https://github.com/djkato/n-mb/assets/25299243/b2531d88-5de1-465f-9bef-d0ad225f06b4) ## This program outputs to following formats:
## This program outputs to following formats:
- audio codec: opus .ogg - audio codec: opus .ogg
- video codec: vp9 + opus .webm - video codec: vp9 + opus .webm
- image codec: vp8 .webp (for gifs too) - image codec: vp8 .webp (for gifs too)
## ~~How to install Binary(Windows, Linux):~~ ## How to install(Windows, Linux, MacOS):
**Releases are no longer being kept uptodate, please follow [Source installation](#how-to-install-from-sourcewindows-linux-macos)**
1. ~~Download binary from [Releases](https://github.com/djkato/n-mb/releases), put into $PATH~~
2. ~~get ffmpeg for your platform [here](https://ffmpeg.org/download.html), put into $PATH~~
3. ~~execute anywhere using the `nmb --size/-s <SIZE IN MB> --codec/-c <WEBM/HEVC> --files/-f=<FILE 1>,<FILE 2> . . .` command!~~
## How to install From Source(Windows, Linux, MacOS):
1. get rustup (cargo, rustc etc) from [here](https://www.rust-lang.org/tools/install) 1. get rustup (cargo, rustc etc) from [here](https://www.rust-lang.org/tools/install)
2. get ffmpeg for your platform [here](https://ffmpeg.org/download.html), put into $PATH 2. get ffmpeg for your platform [here](https://ffmpeg.org/download.html), put into $PATH
3. run `cargo install n-mb` in your favourite terminal 3. run `cargo install n-mb` in your favourite terminal
4. execute anywhere using the `nmb --size/-s <SIZE IN MB> --codec/-c <WEBM/HEVC> --files/-f=<FILE 1>,<FILE 2> . . .` command! 4. execute anywhere using the `nmb --size/-s <SIZE IN MB> --files/-f=<FILE 1>,<FILE 2> . . .` command!
<sub>Thanks for an amazing read on how to optimize vp9 for file sizes deterenkelt, I recommend this read: https://codeberg.org/deterenkelt/Nadeshiko/wiki/Researches%E2%80%89%E2%80%93%E2%80%89VP9-and-overshooting</sub> <sub>Thanks for an amazing read on how to optimize vp9 for file sizes deterenkelt, I recommend this read: https://codeberg.org/deterenkelt/Nadeshiko/wiki/Researches%E2%80%89%E2%80%93%E2%80%89VP9-and-overshooting</sub>

View file

@ -1,2 +0,0 @@
[toolchain]
channel = "nightly"

View file

@ -2,11 +2,9 @@ use anyhow::{bail, Context};
use pbr::{Pipe, ProgressBar}; use pbr::{Pipe, ProgressBar};
use std::{path::PathBuf, process::Stdio}; use std::{path::PathBuf, process::Stdio};
use tokio::{ use tokio::{
io::{BufReader, Lines}, io::{AsyncBufReadExt, BufReader, Lines},
process::{Child, ChildStdout, Command}, process::{Child, ChildStderr, ChildStdout, Command},
}; };
use crate::VideoCodec;
const MAX_OPUS_BITRATE: f32 = 256.; //kbits const MAX_OPUS_BITRATE: f32 = 256.; //kbits
const MIN_OPUS_BITRATE: f32 = 50.; //kbits const MIN_OPUS_BITRATE: f32 = 50.; //kbits
@ -25,21 +23,10 @@ pub struct FFMPEGCommand {
pub progress_bar: Option<ProgressBar<Pipe>>, pub progress_bar: Option<ProgressBar<Pipe>>,
} }
struct MediaData {
resolution: Option<(u16, u16)>,
duration: f32,
old_kbit_rate: Option<u32>,
}
impl FFMPEGCommand { impl FFMPEGCommand {
pub async fn new( pub async fn new(media_type: MediaType, path: &PathBuf, size: u16) -> anyhow::Result<Self> {
media_type: MediaType,
path: &PathBuf,
size: u16,
codec: VideoCodec,
) -> anyhow::Result<Self> {
match media_type { match media_type {
MediaType::Video => Self::create_video(path, size, codec).await, MediaType::Video => Self::create_video(path, size).await,
MediaType::Audio => Self::create_audio(path, size).await, MediaType::Audio => Self::create_audio(path, size).await,
MediaType::Image => Self::create_image(path, size), MediaType::Image => Self::create_image(path, size),
MediaType::AnimatedImage => Self::create_animated_image(path), MediaType::AnimatedImage => Self::create_animated_image(path),
@ -48,29 +35,11 @@ impl FFMPEGCommand {
async fn create_audio(path: &PathBuf, size: u16) -> anyhow::Result<Self> { async fn create_audio(path: &PathBuf, size: u16) -> anyhow::Result<Self> {
let ffprobe_out = parse_ffprobe(path).await?; let ffprobe_out = parse_ffprobe(path).await?;
let duration = ffprobe_out.duration; let duration = ffprobe_out.0.context("Duration missing")?;
let max_kbit_rate = match ffprobe_out.old_kbit_rate {
None => MAX_OPUS_BITRATE, let bitrate = (size as f32 * 1000. / duration) * 0.95;
Some(r) => { let bitrate = bitrate.clamp(MIN_OPUS_BITRATE, MAX_OPUS_BITRATE) as u16;
if (r as f32) < MAX_OPUS_BITRATE {
r as f32
} else {
MAX_OPUS_BITRATE
}
}
};
let bitrate = (size as f32 * 1000. / duration) * 0.85;
let bitrate = bitrate.clamp(MIN_OPUS_BITRATE, max_kbit_rate) as u16;
/*
println!(
"{} * {} ~= {} (actually is {})",
duration,
bitrate,
size * 1000,
duration * bitrate as f32
);
*/
let mut new_path = path.clone(); let mut new_path = path.clone();
new_path.set_extension("ogg"); new_path.set_extension("ogg");
@ -107,18 +76,18 @@ impl FFMPEGCommand {
}) })
} }
async fn create_video(path: &PathBuf, size: u16, codec: VideoCodec) -> anyhow::Result<Self> { async fn create_video(path: &PathBuf, size: u16) -> anyhow::Result<Self> {
let ffprobe_out = parse_ffprobe(path).await?; let ffprobe_out = parse_ffprobe(path).await?;
let duration = ffprobe_out.duration; let duration = ffprobe_out.0.context("Duration missing")?;
let resolution = ffprobe_out.resolution.context("Missing resolution")?; let resolution = ffprobe_out.1.context("Missing resolution")?;
let mut overflown_audio_bitrate = None; let mut overflown_audio_bitrate = None;
let mut audio_bitrate = size as f32 * 180. / duration; let mut audio_bitrate = (size as f32 * 1000. / duration) * 0.95 * 0.1;
let mut video_bitrate = size as f32 * 780. / duration; let mut video_bitrate = (size as f32 * 1000. / duration) * 0.95 * 0.9;
if audio_bitrate < MIN_OPUS_BITRATE { if audio_bitrate < MIN_OPUS_BITRATE {
overflown_audio_bitrate = Some(audio_bitrate - MIN_OPUS_BITRATE); overflown_audio_bitrate = Some(MIN_OPUS_BITRATE - audio_bitrate);
audio_bitrate = MIN_OPUS_BITRATE; audio_bitrate = MIN_OPUS_BITRATE;
} }
if audio_bitrate > MAX_OPUS_BITRATE { if audio_bitrate > MAX_OPUS_BITRATE {
@ -127,16 +96,7 @@ impl FFMPEGCommand {
} }
if let Some(overflow) = overflown_audio_bitrate { if let Some(overflow) = overflown_audio_bitrate {
/* video_bitrate = video_bitrate + overflow;
println!(
"-b:v:{}\n-b:a:{} (ovw: {})\nsum:{}/{}",
video_bitrate,
audio_bitrate,
overflow,
video_bitrate + audio_bitrate,
size
);*/
video_bitrate += overflow;
} }
let mut height = resolution.1; let mut height = resolution.1;
@ -152,56 +112,25 @@ impl FFMPEGCommand {
let old_path_str = path.as_os_str().to_str().context("missing or bad path")?; let old_path_str = path.as_os_str().to_str().context("missing or bad path")?;
let mut new_path = path.clone(); let mut new_path = path.clone();
new_path.set_extension("webm");
let scale_arg = format!("scale=-1:{height}"); let scale_arg = format!("scale=-1:{height}");
let bitrate_arg = format!("{}k", video_bitrate as u16); let bitrate_arg = format!("{video_bitrate}k");
let minrate_arg = format!("{}k", (video_bitrate * 0.5) as u16); let minrate_arg = format!("{}k", (video_bitrate * 0.5) as u16);
let maxrate_arg = format!("{}k", (video_bitrate * 1.45) as u16); let maxrate_arg = format!("{}k", (video_bitrate * 1.45) as u16);
let ba_arg = format!("{}k", audio_bitrate as u16); let ba_arg = format!("{audio_bitrate}k");
let mut passlogfile = path.clone();
passlogfile.set_extension("");
let mut command = Command::new("ffmpeg"); let mut command = Command::new("ffmpeg");
let mut command2 = Command::new("ffmpeg"); let mut command2 = Command::new("ffmpeg");
command.args(["-progress", "-", "-nostats", "-stats_period", "50ms"]); command.args(["-progress", "-", "-nostats", "-stats_period", "50ms"]);
command2.args(["-progress", "-", "-nostats", "-stats_period", "50ms"]); command2.args(["-progress", "-", "-nostats", "-stats_period", "50ms"]);
let video_codec;
let audio_codec;
match codec {
VideoCodec::WEBM => {
video_codec = "libvpx-vp9";
audio_codec = "libopus";
new_path.set_extension("webm");
new_path.set_file_name(
"minified_".to_owned() + new_path.file_name().unwrap().to_str().unwrap(),
)
}
VideoCodec::HEVC => {
video_codec = "libx265";
audio_codec = "aac";
new_path.set_extension("mp4");
new_path.set_file_name(
"minified_".to_owned() + new_path.file_name().unwrap().to_str().unwrap(),
)
}
};
/*
println!(
"{} * ({}+{}) ~= {} (actually is {})",
duration,
video_bitrate,
audio_bitrate,
size,
(duration * ((video_bitrate + audio_bitrate) / 1000.)) as f32
);
*/
let pass = [ let pass = [
"-y", "-y",
"-i", "-i",
old_path_str, old_path_str,
"-vcodec", "-vcodec",
video_codec, "libvpx-vp9",
"-acodec", "-acodec",
audio_codec, "libopus",
"-vf", "-vf",
&scale_arg, &scale_arg,
"-deadline", "-deadline",
@ -234,25 +163,12 @@ impl FFMPEGCommand {
"6", "6",
"-qmax", "-qmax",
"60", "60",
"-qmin",
"1",
"-g", "-g",
"240", "240",
"-passlogfile",
passlogfile
.as_os_str()
.to_str()
.context("missing or bad path")?,
]; ];
command.args(pass); command.args(pass);
command.args(["-pass", "1", "-f", "webm"]);
command.args([
"-pass",
"1",
"-f",
new_path.extension().unwrap().to_str().unwrap(),
]);
if cfg!(windows) { if cfg!(windows) {
command.arg("NUL"); command.arg("NUL");
} else { } else {
@ -267,10 +183,7 @@ impl FFMPEGCommand {
.to_str() .to_str()
.context("missing or bad path")?, .context("missing or bad path")?,
]); ]);
#[cfg(debug_assertions)]
dbg!(&command);
#[cfg(debug_assertions)]
dbg!(&command2);
Ok(FFMPEGCommand { Ok(FFMPEGCommand {
file_name: path.file_name().unwrap().to_str().unwrap().to_owned(), file_name: path.file_name().unwrap().to_str().unwrap().to_owned(),
resolution: None, resolution: None,
@ -322,7 +235,7 @@ impl FFMPEGCommand {
progress_bar: None, progress_bar: None,
}) })
} }
fn create_animated_image(_path: &PathBuf) -> anyhow::Result<Self> { fn create_animated_image(path: &PathBuf) -> anyhow::Result<Self> {
bail!("") bail!("")
} }
} }
@ -342,20 +255,8 @@ pub enum EncodingStatus {
NotStarted, NotStarted,
} }
async fn parse_ffprobe(path: &PathBuf) -> anyhow::Result<MediaData> { async fn parse_ffprobe(path: &PathBuf) -> anyhow::Result<(Option<f32>, Option<(u16, u16)>)> {
let args = [
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=width,height,duration,bit_rate",
"-of",
"csv=s=,:p=0",
];
let ffprobe = Command::new("ffprobe") let ffprobe = Command::new("ffprobe")
.args(args)
.arg(path) .arg(path)
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.output() .output()
@ -364,96 +265,72 @@ async fn parse_ffprobe(path: &PathBuf) -> anyhow::Result<MediaData> {
.status .status
.exit_ok() .exit_ok()
.context("Failed to run ffprobe. Make sure ffprobe is installed and file exists")?; .context("Failed to run ffprobe. Make sure ffprobe is installed and file exists")?;
let ffprobe_output = std::str::from_utf8(&ffprobe.stderr)?;
let text = std::str::from_utf8(&ffprobe.stdout)?; let mut duration = None;
let mut resolution = None;
let mem = text.split(',').collect::<Vec<_>>(); let text = ffprobe_output;
if text.contains("Duration") {
let width = mem.first().and_then(|v| v.parse::<u16>().ok()); duration = Some(parse_duration(text)?);
let height = mem.get(1).and_then(|v| v.parse::<u16>().ok()); }
if text.contains("Stream") {
let duration = match mem.get(2).and_then(|v| v.parse::<f32>().ok()) { resolution = Some(parse_resolution(text)?);
Some(d) => d, }
None => { return Ok((duration, resolution));
let metadata = get_attribute_from_meta("duration", path)
.await
.context("can't find duration anywhere")?;
let res = metadata.parse::<f32>();
//see if metadatat had seconds directly
if let Ok(d) = res {
d
} else {
// try to convert 00:00:00:00 to 0.000s
if !metadata.contains(":") {
return Err(anyhow::anyhow!("can't find duration of media anywhere"));
} else {
let mut res = 0.;
let mut iter = metadata.split(':').rev();
let secs = iter.next().map(|n| n.parse::<f32>().ok()).flatten();
let mins = iter
.next()
.map(|n| n.parse::<f32>().ok().map(|m| m * 60.))
.flatten();
let hrs = iter
.next()
.map(|n| n.parse::<f32>().ok().map(|h| h * 3600.))
.flatten();
let days = iter
.next()
.map(|n| n.parse::<f32>().ok().map(|d| d * 24. * 3600.))
.flatten();
if let Some(s) = secs {
res = res + s
};
if let Some(m) = mins {
res = res + m
};
if let Some(h) = hrs {
res = res + h
};
if let Some(d) = days {
res = res + d
};
res
}
}
}
};
dbg!(&duration);
let old_kbit_rate = mem
.get(3)
.and_then(|v| v.parse::<u32>().ok().map(|v| v / 1000));
let resolution = width.zip(height);
Ok(MediaData {
duration,
resolution,
old_kbit_rate,
})
} }
async fn get_attribute_from_meta(attr: &str, path: &PathBuf) -> Option<String> { fn parse_duration(text: &str) -> anyhow::Result<f32> {
let ffprobe = Command::new("ffprobe") let text = text[text.find("Duration").unwrap()..].to_owned();
.args([ let dur_text = text[text
"-v", .find(":")
"error", .context("something wrong with the ffprobe output")?
"-select_streams", + 2
"v:0", ..text
"-show_entries", .find(",")
&format!("stream={attr}:stream_args={attr}"), .context("something wrong with the ffprobe output")?]
"-of", .to_owned();
"csv=s=,:p=0", let durs_text: Vec<&str> = dur_text.split(":").collect();
]) let mut durs_text_iter = durs_text.into_iter();
.arg(path) let h = durs_text_iter
.stderr(Stdio::piped()) .next()
.output() .context("something wrong with the ffprobe output")?
.await .parse::<f32>()?;
.ok()?; let m = durs_text_iter
ffprobe.status.exit_ok().ok()?; .next()
.context("something wrong with the ffprobe output")?
std::str::from_utf8(&ffprobe.stdout) .parse::<f32>()?;
.ok() let s = durs_text_iter
.map(|v| v.to_string()) .next()
.context("something wrong with the ffprobe output")?
.parse::<f32>()?;
Ok(h * 60. * 60. + m * 60. + s)
}
fn parse_resolution(text: &str) -> anyhow::Result<(u16, u16)> {
let text = text[text.find("Stream").unwrap()..].to_owned();
let sar_i = text
.find("[SAR ")
.context("something wrong with the ffprobe output")?
- 1;
let rb_b4_sar_i = text[..sar_i]
.rfind(",")
.context("something wrong with the ffprobe output")?
+ 1;
let res_text = text[rb_b4_sar_i..sar_i].to_owned();
let res_text = res_text.trim().to_owned();
let width = res_text[..res_text
.find("x")
.context("something wrong with ffprobe output")?]
.to_owned()
.parse::<u16>()?;
let height = res_text[res_text
.find("x")
.context("something wrong with ffprobe output")?
+ 1..]
.to_owned()
.parse::<u16>()?;
return Ok((width, height));
} }

View file

@ -14,31 +14,6 @@ use crate::encoder::EncodingStatus;
mod encoder; mod encoder;
mod ui; mod ui;
#[derive(Debug, Clone)]
pub enum VideoCodec {
WEBM,
HEVC,
}
impl std::fmt::Display for VideoCodec {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::WEBM => write!(f, "WEBM"),
Self::HEVC => write!(f, "HEVC"),
}
}
}
impl VideoCodec {
pub fn from_string(string: &str) -> Option<Self> {
match string.to_lowercase().as_str() {
"webm" => Some(Self::WEBM),
"hevc" => Some(Self::HEVC),
_ => None,
}
}
}
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
let args = command!() let args = command!()
@ -49,11 +24,6 @@ async fn main() -> anyhow::Result<()> {
.default_value("25") .default_value("25")
.value_parser(value_parser!(u16)) .value_parser(value_parser!(u16))
) )
.arg(
arg!(-c --codec <CODEC> "Choose video codec between `HEVC` (H.265) and `WEBM` (vp9).")
.required(false)
.default_value("WEBM")
)
.arg( .arg(
arg!(-f --files <FILES> "Comma separated files to convert. EG: -f=<FILE>,<FILE>") arg!(-f --files <FILES> "Comma separated files to convert. EG: -f=<FILE>,<FILE>")
.required(true) .required(true)
@ -63,17 +33,12 @@ async fn main() -> anyhow::Result<()> {
).get_matches(); ).get_matches();
let size = args let size = args
.get_one::<u16>("size") .get_one::<u16>("size")
.expect("Default value dissapeared from rate") .expect("Default value dissapeared from rate");
* 8;
let files = args let files = args
.get_many::<PathBuf>("files") .get_many::<PathBuf>("files")
.context("No files specified")? .context("No files specified")?
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let binding = "webm".to_owned();
let codec = args.get_one::<String>("codec").unwrap_or(&binding);
let codec = VideoCodec::from_string(codec).unwrap_or(VideoCodec::WEBM);
let commands: Arc<Mutex<Vec<FFMPEGCommand>>> = Arc::new(Mutex::new(vec![])); let commands: Arc<Mutex<Vec<FFMPEGCommand>>> = Arc::new(Mutex::new(vec![]));
{ {
let mut commands_mut = commands.try_lock().unwrap(); let mut commands_mut = commands.try_lock().unwrap();
@ -90,33 +55,32 @@ async fn main() -> anyhow::Result<()> {
.as_str() .as_str()
{ {
"webm" | "mp4" | "mov" | "avi" | "mpeg" | "mkv" => { "webm" | "mp4" | "mov" | "avi" | "mpeg" | "mkv" => {
command = command = FFMPEGCommand::new(MediaType::Video, file, size.clone()).await?;
FFMPEGCommand::new(MediaType::Video, file, size, codec.clone()).await?;
} }
"mp3" | "wav" | "ogg" | "opus" | "flac" | "aiff" => { "mp3" | "wav" | "ogg" | "opus" | "flac" | "aiff" => {
command = command = FFMPEGCommand::new(MediaType::Audio, file, size.clone()).await?;
FFMPEGCommand::new(MediaType::Audio, file, size, codec.clone()).await?;
} }
"jpg" | "png" | "webp" | "exr" | "jpeg" | "tiff" | "bpm" | "raw" | "tif" => { "jpg" | "png" | "webp" | "exr" | "jpeg" | "tiff" | "bpm" | "raw" | "tif" => {
command = command = FFMPEGCommand::new(MediaType::Image, file, size.clone()).await?;
FFMPEGCommand::new(MediaType::Image, file, size, codec.clone()).await?;
} }
"gif" => { "gif" => {
command = command =
FFMPEGCommand::new(MediaType::AnimatedImage, file, size, codec.clone()) FFMPEGCommand::new(MediaType::AnimatedImage, file, size.clone()).await?;
.await?;
} }
_ => break, _ => break,
} }
//dbg!(&command.command.0);
command.command.0.stdout(Stdio::piped()); command.command.0.stdout(Stdio::piped());
command.command.0.stderr(Stdio::piped()); command.command.0.stderr(Stdio::null());
command.command.0.stdin(Stdio::null()); command.command.0.stdin(Stdio::null());
command.command.0.kill_on_drop(true);
if command.media_type == MediaType::Video { if command.media_type == MediaType::Video {
let mut pass2 = command.command.1.unwrap(); let mut pass2 = command.command.1.unwrap();
pass2.stdout(Stdio::piped()); pass2.stdout(Stdio::piped());
pass2.stderr(Stdio::piped()); pass2.stderr(Stdio::null());
pass2.stdin(Stdio::null()); pass2.stdin(Stdio::null());
pass2.kill_on_drop(true);
command.command.1 = Some(pass2) command.command.1 = Some(pass2)
} }
@ -150,17 +114,15 @@ async fn main() -> anyhow::Result<()> {
use std::time::Duration; use std::time::Duration;
use tokio::time::interval; use tokio::time::interval;
let commands_ref = commands.clone(); let commands_ref = commands.clone();
let mut intv = interval(Duration::from_millis(10)); let mut intv = interval(Duration::from_millis(50));
command_spawns.push(tokio::spawn(async move { command_spawns.push(tokio::spawn(async move {
intv.tick().await; intv.tick().await;
'line: while let Ok(Some(line)) = buff_reader.1.next_line().await { 'line: while let Ok(Some(line)) = buff_reader.1.next_line().await {
#[cfg(debug_assertions)]
dbg!(&line);
if let Some(time_start) = line.find("out_time=") { if let Some(time_start) = line.find("out_time=") {
let time: Vec<String> = line[time_start + 10..] let time: Vec<String> = line[time_start + 10..]
.split(':') .split(":")
.map(|s| s.to_owned()) .map(|s| s.to_owned())
.collect(); .collect();
@ -170,13 +132,9 @@ async fn main() -> anyhow::Result<()> {
if let Ok(number) = part.parse::<f32>() { if let Ok(number) = part.parse::<f32>() {
parsed_time.push(number) parsed_time.push(number)
} else { } else {
// parsed_time.push(0.); break 'line;
// break 'line;
} }
} }
if parsed_time.len() == 0 {
parsed_time.append(&mut vec![0., 0., 0.]);
}
let time = parsed_time[0] * 3600. + parsed_time[1] * 60. + parsed_time[2]; let time = parsed_time[0] * 3600. + parsed_time[1] * 60. + parsed_time[2];
let mut command = commands_ref.lock().await; let mut command = commands_ref.lock().await;
@ -186,8 +144,6 @@ async fn main() -> anyhow::Result<()> {
command.progressed_time = time; command.progressed_time = time;
} }
if let Some(progress_i) = line.find("progress=") { if let Some(progress_i) = line.find("progress=") {
#[cfg(debug_assertions)]
println!("found progress!, {}", &line[progress_i + 9..]);
let mut command = commands_ref.lock().await; let mut command = commands_ref.lock().await;
let command = command.get_mut(buff_reader.0).unwrap(); let command = command.get_mut(buff_reader.0).unwrap();
@ -211,9 +167,7 @@ async fn main() -> anyhow::Result<()> {
command.passed_pass_1 = true; command.passed_pass_1 = true;
} }
}, },
_ => { _ => command.status = EncodingStatus::Finished,
command.status = EncodingStatus::Finished;
}
}, },
"continue" => command.status = EncodingStatus::InProgress, "continue" => command.status = EncodingStatus::InProgress,
_ => (), _ => (),

View file

@ -8,7 +8,7 @@ use tokio::time::interval;
pub async fn display(commands: Arc<Mutex<Vec<FFMPEGCommand>>>) { pub async fn display(commands: Arc<Mutex<Vec<FFMPEGCommand>>>) {
let mb = MultiProgress::new(); let mb = MultiProgress::new();
let sty = ProgressStyle::with_template( let sty = ProgressStyle::with_template(
"{spinner:.blue} {msg} [{elapsed_precise}/{eta_precise}(eta)] {bar:40.cyan/blue} {pos:>7}/{len:7} (ms)", "{spinner:.blue} {msg} [{elapsed_precise}] {bar:40.cyan/blue} {pos:>7}/{len:7} (ms)",
) )
.unwrap() .unwrap()
.tick_strings(&[ .tick_strings(&[

BIN
test.avi

Binary file not shown.

BIN
test.mkv

Binary file not shown.