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]
|
||||
resolver = "2"
|
||||
members = [ "yazi-*" ]
|
||||
members = [ "yazi-*", "fallback-thumbnailer"]
|
||||
default-members = [ "yazi-fm", "yazi-cli" ]
|
||||
|
||||
[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 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()
|
||||
local start, cache = os.clock(), ya.file_cache(self)
|
||||
if not cache or self:preload() ~= 1 then
|
||||
|
|
@ -33,7 +42,12 @@ function M:preload()
|
|||
return 1
|
||||
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",
|
||||
"6",
|
||||
"-c",
|
||||
|
|
@ -49,7 +63,7 @@ function M:preload()
|
|||
}):spawn()
|
||||
|
||||
if not child then
|
||||
ya.err("spawn `ffmpegthumbnailer` command returns " .. tostring(code))
|
||||
ya.err("spawn `" .. cmd .. "` command returns " .. tostring(code))
|
||||
return 0
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue