mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
Add fallback video thumbnailer for windows
This commit is contained in:
parent
e940d81d2a
commit
f6aa1eb1e8
6 changed files with 1105 additions and 6 deletions
828
Cargo.lock
generated
828
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [ "yazi-*" ]
|
members = [ "yazi-*", "fallback-thumbnailer"]
|
||||||
default-members = [ "yazi-fm", "yazi-cli" ]
|
default-members = [ "yazi-fm", "yazi-cli" ]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|
|
||||||
84
fallback-thumbnailer/build.rs
Normal file
84
fallback-thumbnailer/build.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
use std::env;
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::io::{Write};
|
||||||
|
use std::path::Path;
|
||||||
|
use zip::read::ZipArchive;
|
||||||
|
use reqwest::blocking::get;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let archive_name = "ffmpeg-n7.0.2-13-g51482627ca-win64-gpl-shared-7.0";
|
||||||
|
let zip_file_name = format!("{}.zip", archive_name);
|
||||||
|
|
||||||
|
let out_dir_string = env::var("OUT_DIR").expect("OUT_DIR not set");
|
||||||
|
let out_dir = Path::new(&out_dir_string);
|
||||||
|
|
||||||
|
let extract_path = out_dir.join(&archive_name);
|
||||||
|
|
||||||
|
if extract_path.exists() {
|
||||||
|
fs::remove_dir_all(&extract_path).expect("Failed to remove existing directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = format!("https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2024-09-02-12-48/{}", zip_file_name);
|
||||||
|
let zip_path = out_dir.join(&zip_file_name);
|
||||||
|
|
||||||
|
println!("Downloading from URL: {}", url);
|
||||||
|
println!("Saving to file: {}", zip_path.display());
|
||||||
|
|
||||||
|
let response = get(&url).expect("Failed to download the archive");
|
||||||
|
let mut file = File::create(&zip_path).expect("Failed to create archive file");
|
||||||
|
file.write_all(&response.bytes().expect("Failed to read response body")).expect("Failed to write to archive file");
|
||||||
|
|
||||||
|
println!("Extracting archive to: {}", extract_path.display());
|
||||||
|
|
||||||
|
let file = File::open(&zip_path).expect("Failed to open ZIP file");
|
||||||
|
let mut archive = ZipArchive::new(file).expect("Failed to read ZIP archive");
|
||||||
|
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut file = archive.by_index(i).expect("Failed to access file in ZIP archive");
|
||||||
|
let out_path = extract_path.join(file.name());
|
||||||
|
|
||||||
|
if (&*file.name()).ends_with('/') {
|
||||||
|
fs::create_dir_all(&out_path).expect("Failed to create directory");
|
||||||
|
} else {
|
||||||
|
if let Some(p) = out_path.parent() {
|
||||||
|
if !p.exists() {
|
||||||
|
fs::create_dir_all(p).expect("Failed to create directory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut outfile = File::create(&out_path).expect("Failed to create file");
|
||||||
|
std::io::copy(&mut file, &mut outfile).expect("Failed to copy contents");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let bin_dir = extract_path.join(archive_name).join("bin");
|
||||||
|
println!("Setting bin_dir: {}", bin_dir.display());
|
||||||
|
|
||||||
|
if bin_dir.exists() {
|
||||||
|
unsafe {
|
||||||
|
env::set_var("FFMPEG_DIR", bin_dir.to_str().expect("Failed to convert path to string"));
|
||||||
|
}
|
||||||
|
println!("FFMPEG_DIR is set to: {}", bin_dir.display());
|
||||||
|
} else {
|
||||||
|
panic!("No 'bin' directory found in the extracted contents");
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::remove_file(&zip_path).expect("Failed to remove zip file");
|
||||||
|
eprintln!("Removed archive file: {}", zip_path.display());
|
||||||
|
|
||||||
|
eprintln!("cargo:rustc-env=FFMPEG_DIR={}", env::var("FFMPEG_DIR").expect("Failed to get FFMPEG_DIR"));
|
||||||
|
|
||||||
|
for entry in fs::read_dir(bin_dir).expect("Failed to read bin_dir") {
|
||||||
|
let entry = entry.expect("Failed to get entry");
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if path.is_file() {
|
||||||
|
if let Some(extension) = path.extension() {
|
||||||
|
if extension == "dll" {
|
||||||
|
let dest_path = out_dir.parent().unwrap().parent().unwrap().parent().unwrap().join(entry.file_name());
|
||||||
|
println!("Copying {} to {}", path.display(), dest_path.display());
|
||||||
|
fs::copy(&path, &dest_path).expect("Failed to copy DLL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
fallback-thumbnailer/cargo.toml
Normal file
14
fallback-thumbnailer/cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "fallback-thumbnailer"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
reqwest = { version = "0.11", features = ["blocking"] }
|
||||||
|
sevenz-rust = "0.4"
|
||||||
|
zip = "0.6"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ffmpeg-next = "7.0.4"
|
||||||
|
image = "0.25.2"
|
||||||
|
clap = { version = "4.0", features = ["derive"] }
|
||||||
165
fallback-thumbnailer/src/main.rs
Normal file
165
fallback-thumbnailer/src/main.rs
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
use std::fs::File;
|
||||||
|
use clap::Parser;
|
||||||
|
use ffmpeg_next::{Error, format, Packet};
|
||||||
|
use ffmpeg_next::format::context::Input;
|
||||||
|
use ffmpeg_next::format::Pixel;
|
||||||
|
use ffmpeg_next::frame::Video;
|
||||||
|
use ffmpeg_next::media::Type;
|
||||||
|
use ffmpeg_next::rescale::TIME_BASE;
|
||||||
|
use ffmpeg_next::software::scaling::{Context, Flags};
|
||||||
|
use image::{ImageEncoder, Rgb, RgbImage};
|
||||||
|
use image::codecs::jpeg::JpegEncoder;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command()]
|
||||||
|
struct Args {
|
||||||
|
#[arg(short, long)]
|
||||||
|
input: String,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
output: String,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
quality: u8,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
time: u32,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
size: u32,
|
||||||
|
|
||||||
|
#[arg(short, long)]
|
||||||
|
codec: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Error> {
|
||||||
|
let args = Args::parse();
|
||||||
|
let i_temp = args.input;
|
||||||
|
let o_temp = args.output;
|
||||||
|
println!("input: {:?} {:?}", i_temp, o_temp);
|
||||||
|
|
||||||
|
ffmpeg_next::init()?;
|
||||||
|
let mut ictx = format::input(&i_temp)?;
|
||||||
|
let input = ictx
|
||||||
|
.streams()
|
||||||
|
.best(Type::Video)
|
||||||
|
.ok_or(Error::StreamNotFound)?;
|
||||||
|
let stream_index = input.index();
|
||||||
|
|
||||||
|
if input.duration() == 0 {
|
||||||
|
eprintln!("Error: The duration of the input file is 0.");
|
||||||
|
return Err(Error::InvalidData);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("duration: {:?}", input.duration());
|
||||||
|
|
||||||
|
let context_decoder = ffmpeg_next::codec::context::Context::from_parameters(input.parameters())?;
|
||||||
|
let mut decoder = context_decoder.decoder().video()?;
|
||||||
|
|
||||||
|
let scalar = create_scalar(args.size, &mut decoder)?;
|
||||||
|
|
||||||
|
let seek_percent = args.time as f64 * 0.01;
|
||||||
|
seek_to_position(&mut ictx, seek_percent)?;
|
||||||
|
|
||||||
|
let rgb_frame = get_frame(ictx, stream_index, decoder, scalar)?;
|
||||||
|
|
||||||
|
if rgb_frame != Video::empty() {
|
||||||
|
if let Err(e) = write_frame_to_jpeg(&rgb_frame, &o_temp, args.quality) {
|
||||||
|
eprintln!("Error writing file {:?}", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eprintln!("Could not find frame");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seek_to_position(ictx: &mut Input, seek_percent: f64) -> Result<(), Error> {
|
||||||
|
let seek_pos_in_seconds = ((ictx.duration() as f64 * seek_percent) / f64::from(TIME_BASE.denominator())) as i32;
|
||||||
|
let seek_pos = seek_pos_in_seconds * TIME_BASE.denominator();
|
||||||
|
ictx.seek(seek_pos as i64, ..seek_pos as i64)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_scalar(size: u32, decoder: &mut ffmpeg_next::codec::decoder::video::Video) -> Result<Context, Error> {
|
||||||
|
let w = decoder.width();
|
||||||
|
let p = size as f32 / w as f32;
|
||||||
|
let h = decoder.height();
|
||||||
|
let new_h = (h as f32 * p) as u32;
|
||||||
|
let scalar = Context::get(
|
||||||
|
decoder.format(),
|
||||||
|
decoder.width(),
|
||||||
|
decoder.height(),
|
||||||
|
Pixel::RGB24,
|
||||||
|
size,
|
||||||
|
new_h,
|
||||||
|
Flags::BILINEAR,
|
||||||
|
)?;
|
||||||
|
Ok(scalar)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_frame(mut ictx: Input,
|
||||||
|
stream_index: usize,
|
||||||
|
mut decoder: ffmpeg_next::codec::decoder::video::Video,
|
||||||
|
mut scaler: Context) -> Result<Video, Error> {
|
||||||
|
let mut rgb_frame = Video::empty();
|
||||||
|
for (stream, packet) in ictx.packets() {
|
||||||
|
if stream.index() == stream_index {
|
||||||
|
if let Ok(frame_decoded) = process_packet(&mut decoder, &mut scaler, &packet, &mut rgb_frame) {
|
||||||
|
if frame_decoded {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(rgb_frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_frame_to_jpeg(frame: &Video, filename: &str, quality: u8) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if frame.format() != Pixel::RGB24 {
|
||||||
|
return Err("Frame format is not RGB24".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = frame.width();
|
||||||
|
let height = frame.height();
|
||||||
|
|
||||||
|
let mut img_buffer = RgbImage::new(width, height);
|
||||||
|
|
||||||
|
let data = frame.data(0);
|
||||||
|
let line_size = frame.stride(0);
|
||||||
|
|
||||||
|
for y in 0..height {
|
||||||
|
for x in 0..width {
|
||||||
|
let offset = (y * line_size as u32 + x * 3) as usize;
|
||||||
|
let rgb = Rgb([data[offset], data[offset + 1], data[offset + 2]]);
|
||||||
|
img_buffer.put_pixel(x, y, rgb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = File::create(filename)?;
|
||||||
|
let encoder = JpegEncoder::new_with_quality(file, quality);
|
||||||
|
encoder.write_image(&img_buffer, width, height, image::ExtendedColorType::Rgb8)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_packet(
|
||||||
|
decoder: &mut ffmpeg_next::decoder::Video,
|
||||||
|
scaler: &mut Context,
|
||||||
|
packet: &Packet,
|
||||||
|
rgb_frame: &mut Video,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
decoder.send_packet(packet)?;
|
||||||
|
let mut decoded = Video::empty();
|
||||||
|
match decoder.receive_frame(&mut decoded) {
|
||||||
|
Ok(_) => {
|
||||||
|
scaler.run(&decoded, rgb_frame)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Failed to receive frame: {:?}", err);
|
||||||
|
Ok(false)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
local function is_command_available(cmd)
|
||||||
|
local child, code = Command(cmd):args({"--version"}):spawn()
|
||||||
|
if not child then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local status = child:wait()
|
||||||
|
return status and status.success
|
||||||
|
end
|
||||||
|
|
||||||
function M:peek()
|
function M:peek()
|
||||||
local start, cache = os.clock(), ya.file_cache(self)
|
local start, cache = os.clock(), ya.file_cache(self)
|
||||||
if not cache or self:preload() ~= 1 then
|
if not cache or self:preload() ~= 1 then
|
||||||
|
|
@ -33,7 +42,12 @@ function M:preload()
|
||||||
return 1
|
return 1
|
||||||
end
|
end
|
||||||
|
|
||||||
local child, code = Command("ffmpegthumbnailer"):args({
|
local cmd = "ffmpegthumbnailer"
|
||||||
|
if not is_command_available(cmd) then
|
||||||
|
cmd = "fallback-thumbnailer"
|
||||||
|
end
|
||||||
|
|
||||||
|
local child, code = Command(cmd):args({
|
||||||
"-q",
|
"-q",
|
||||||
"6",
|
"6",
|
||||||
"-c",
|
"-c",
|
||||||
|
|
@ -49,7 +63,7 @@ function M:preload()
|
||||||
}):spawn()
|
}):spawn()
|
||||||
|
|
||||||
if not child then
|
if not child then
|
||||||
ya.err("spawn `ffmpegthumbnailer` command returns " .. tostring(code))
|
ya.err("spawn `" .. cmd .. "` command returns " .. tostring(code))
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue