Add fallback video thumbnailer for windows

This commit is contained in:
Eric Nelson 2024-09-05 17:06:51 -07:00
parent e940d81d2a
commit f6aa1eb1e8
6 changed files with 1105 additions and 6 deletions

828
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -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]

View 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");
}
}
}
}
}

View 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"] }

View 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)
},
}
}

View file

@ -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