feat!: video spotter & using ffmpeg instead of ffmpegthumbnailer as previewer backend (#1928)

This commit is contained in:
三咲雅 · Misaki Masa 2024-11-21 20:55:50 +08:00 committed by sxyazi
parent 22e7d579d2
commit 428c922703
No known key found for this signature in database
18 changed files with 204 additions and 96 deletions

1
Cargo.lock generated
View File

@ -3550,6 +3550,7 @@ dependencies = [
"mlua",
"parking_lot",
"ratatui",
"serde_json",
"shell-words",
"syntect",
"tokio",

View File

@ -1 +1 @@
{"flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp"," Überzug"," Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","Sysinfo"],"language":"en","version":"0.2"}
{"language":"en","flagWords":[],"words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp"," Überzug"," Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","Sysinfo","ffprobe","vframes"],"version":"0.2"}

View File

@ -76,20 +76,21 @@ impl Actions {
writeln!(s, "\nDependencies")?;
#[rustfmt::skip]
writeln!(s, " file : {}", Self::process_output(env::var_os("YAZI_FILE_ONE").unwrap_or("file".into()), "--version"))?;
writeln!(s, " ueberzugpp : {}", Self::process_output("ueberzugpp", "--version"))?;
writeln!(s, " ffmpegthumbnailer: {}", Self::process_output("ffmpegthumbnailer", "-v"))?;
writeln!(s, " pdftoppm : {}", Self::process_output("pdftoppm", "--help"))?;
writeln!(s, " magick : {}", Self::process_output("magick", "--version"))?;
writeln!(s, " fzf : {}", Self::process_output("fzf", "--version"))?;
writeln!(s, " fd : {}", Self::process_output("fd", "--version"))?;
writeln!(s, " fdfind : {}", Self::process_output("fdfind", "--version"))?;
writeln!(s, " rg : {}", Self::process_output("rg", "--version"))?;
writeln!(s, " chafa : {}", Self::process_output("chafa", "--version"))?;
writeln!(s, " zoxide : {}", Self::process_output("zoxide", "--version"))?;
writeln!(s, " 7z : {}", Self::process_output("7z", "i"))?;
writeln!(s, " 7zz : {}", Self::process_output("7zz", "i"))?;
writeln!(s, " jq : {}", Self::process_output("jq", "--version"))?;
writeln!(s, " file : {}", Self::process_output(env::var_os("YAZI_FILE_ONE").unwrap_or("file".into()), "--version"))?;
writeln!(s, " ueberzugpp : {}", Self::process_output("ueberzugpp", "--version"))?;
#[rustfmt::skip]
writeln!(s, " ffmpeg/ffprobe: {} / {}", Self::process_output("ffmpeg", "-version"), Self::process_output("ffprobe", "-version"))?;
writeln!(s, " pdftoppm : {}", Self::process_output("pdftoppm", "--help"))?;
writeln!(s, " magick : {}", Self::process_output("magick", "--version"))?;
writeln!(s, " fzf : {}", Self::process_output("fzf", "--version"))?;
#[rustfmt::skip]
writeln!(s, " fd/fdfind : {} / {}", Self::process_output("fd", "--version"), Self::process_output("fdfind", "--version"))?;
writeln!(s, " rg : {}", Self::process_output("rg", "--version"))?;
writeln!(s, " chafa : {}", Self::process_output("chafa", "--version"))?;
writeln!(s, " zoxide : {}", Self::process_output("zoxide", "--version"))?;
#[rustfmt::skip]
writeln!(s, " 7z/7zz : {} / {}", Self::process_output("7z", "i"), Self::process_output("7zz", "i"))?;
writeln!(s, " jq : {}", Self::process_output("jq", "--version"))?;
writeln!(s, "\nClipboard")?;
#[rustfmt::skip]

View File

@ -32,6 +32,7 @@ md-5 = { workspace = true }
mlua = { workspace = true }
parking_lot = { workspace = true }
ratatui = { workspace = true }
serde_json = { workspace = true }
shell-words = { workspace = true }
syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
tokio = { workspace = true }

View File

@ -16,56 +16,10 @@ end
function M:seek() end
-- TODO: remove this
local hovered_mime = ya.sync(function()
local h = cx.active.current.hovered
if not h then
return nil
elseif h.cha.is_dir then
return "inode/directory"
else
return h:mime()
end
end)
function M:spot(job)
local mime = hovered_mime()
if not mime then
return
end
local file = job.file
local spotter = PLUGIN.spotter(file.url, mime)
local previewer = PLUGIN.previewer(file.url, mime)
local fetchers = PLUGIN.fetchers(file.url, mime)
local preloaders = PLUGIN.preloaders(file.url, mime)
for i, v in ipairs(fetchers) do
fetchers[i] = v.cmd
end
for i, v in ipairs(preloaders) do
preloaders[i] = v.cmd
end
local rows = {}
local row = function(key, value)
local h = type(value) == "table" and #value or 1
rows[#rows + 1] = ui.Row({ key, value }):height(h)
end
rows[#rows + 1] = ui.Row({ "Metadata", "" }):style(ui.Style():fg("red"))
row(" Created:", file.cha.btime and os.date("%y/%m/%d %H:%M", math.floor(file.cha.btime)) or "-")
row(" Modified:", file.cha.mtime and os.date("%y/%m/%d %H:%M", math.floor(file.cha.mtime)) or "-")
row(" Mimetype:", mime)
rows[#rows + 1] = ui.Row({ { "", "Plugins" }, "" }):height(2):style(ui.Style():fg("red"))
row(" Spotter:", spotter and spotter.cmd or "-")
row(" Previewer:", previewer and previewer.cmd or "-")
row(" Fetchers:", #fetchers ~= 0 and fetchers or "-")
row(" Preloaders:", #preloaders ~= 0 and preloaders or "-")
ya.spot_table(
job,
ui.Table(rows)
ui.Table(self:spot_base(job))
:area(ui.Pos { "center", w = 60, h = 20 })
:row(1)
:col(1)
@ -75,4 +29,33 @@ function M:spot(job)
)
end
function M:spot_base(job)
local url, cha = job.file.url, job.file.cha
local spotter = PLUGIN.spotter(url, job.mime)
local previewer = PLUGIN.previewer(url, job.mime)
local fetchers = PLUGIN.fetchers(url, job.mime)
local preloaders = PLUGIN.preloaders(url, job.mime)
for i, v in ipairs(fetchers) do
fetchers[i] = v.cmd
end
for i, v in ipairs(preloaders) do
preloaders[i] = v.cmd
end
return {
ui.Row({ "Base" }):style(ui.Style():fg("green")),
ui.Row { " Created:", cha.btime and os.date("%y/%m/%d %H:%M", math.floor(cha.btime)) or "-" },
ui.Row { " Modified:", cha.mtime and os.date("%y/%m/%d %H:%M", math.floor(cha.mtime)) or "-" },
ui.Row { " Mimetype:", job.mime },
ui.Row {},
ui.Row({ "Plugins" }):style(ui.Style():fg("green")),
ui.Row { " Spotter:", spotter and spotter.cmd or "-" },
ui.Row { " Previewer:", previewer and previewer.cmd or "-" },
ui.Row { " Fetchers:", #fetchers ~= 0 and fetchers or "-" },
ui.Row { " Preloaders:", #preloaders ~= 0 and preloaders or "-" },
}
end
return M

View File

@ -23,29 +23,30 @@ function M:preload()
end
function M:spot(job)
local info = ya.image_info(job.file.url)
local rows = {}
local row = function(key, value)
local h = type(value) == "table" and #value or 1
rows[#rows + 1] = ui.Row({ key, value }):height(h)
end
row("Format:", tostring(info.format))
row("Width:", string.format("%dpx", info.w))
row("Height:", string.format("%dpx", info.h))
row("Color:", tostring(info.color))
local rows = self:spot_base(job)
rows[#rows + 1] = ui.Row {}
ya.spot_table(
job,
ui.Table(rows)
ui.Table(ya.list_merge(rows, require("file"):spot_base(job)))
:area(ui.Pos { "center", w = 60, h = 20 })
:row(job.skip)
:row(1)
:col(1)
:col_style(ui.Style():fg("blue"))
:cell_style(ui.Style():fg("yellow"):reverse())
:widths { ui.Constraint.Length(12), ui.Constraint.Fill(1) }
:widths { ui.Constraint.Length(14), ui.Constraint.Fill(1) }
)
end
function M:spot_base(job)
local info = ya.image_info(job.file.url)
return {
ui.Row({ "Image" }):style(ui.Style():fg("green")),
ui.Row { " Format:", tostring(info.format) },
ui.Row { " Size:", string.format("%dx%d", info.w, info.h) },
ui.Row { " Color:", tostring(info.color) },
}
end
return M

View File

@ -38,23 +38,29 @@ function M:preload()
return 1
end
local child, code = Command("ffmpegthumbnailer"):args({
"-q",
"6",
"-c",
"jpeg",
"-i",
tostring(self.file.url),
"-o",
tostring(cache),
"-t",
tostring(percent),
"-s",
tostring(PREVIEW.max_width),
local meta, err = self.list_meta(self.file.url, "format=duration")
if not meta then
ya.err(tostring(err))
return 0
end
local ss = math.floor(meta.format.duration * percent / 100)
local qv = 31 - math.floor(PREVIEW.image_quality * 0.3)
-- stylua: ignore
local child, code = Command("ffmpeg"):args({
"-v", "quiet",
"-skip_frame", "nokey", "-ss", ss,
"-an", "-sn", "-dn",
"-i", tostring(self.file.url),
"-vframes", 1,
"-q:v", qv,
"-vf", string.format("scale=%d:-2:flags=fast_bilinear", PREVIEW.max_width),
"-f", "image2",
"-y", tostring(cache),
}):spawn()
if not child then
ya.err("spawn `ffmpegthumbnailer` command returns " .. tostring(code))
ya.err("Spawn `ffmpeg` process returns " .. tostring(code))
return 0
end
@ -62,6 +68,66 @@ function M:preload()
return status and status.success and 1 or 2
end
function M:spot(job) require("file"):spot(job) end
function M:spot(job)
local rows = self:spot_base(job)
rows[#rows + 1] = ui.Row {}
ya.spot_table(
job,
ui.Table(ya.list_merge(rows, require("file"):spot_base(job)))
:area(ui.Pos { "center", w = 60, h = 20 })
:row(1)
:col(1)
:col_style(ui.Style():fg("blue"))
:cell_style(ui.Style():fg("yellow"):reverse())
:widths { ui.Constraint.Length(14), ui.Constraint.Fill(1) }
)
end
function M:spot_base(job)
local meta, err = self.list_meta(job.file.url, "format=duration:stream=codec_name,codec_type,width,height")
if not meta then
ya.err(tostring(err))
return {}
end
local dur = meta.format.duration
local rows = {
ui.Row({ "Video" }):style(ui.Style():fg("green")),
ui.Row { " Codec:", meta.streams[1].codec_name },
ui.Row { " Duration:", string.format("%d:%02d", math.floor(dur / 60), math.floor(dur % 60)) },
}
for i, s in ipairs(meta.streams) do
if s.codec_type == "video" then
rows[#rows + 1] = ui.Row { string.format(" Stream %d:", i), "video" }
rows[#rows + 1] = ui.Row { " Codec:", s.codec_name }
rows[#rows + 1] = ui.Row { " Size:", string.format("%dx%d", s.width, s.height) }
elseif s.codec_type == "audio" then
rows[#rows + 1] = ui.Row { string.format(" Stream %d:", i), "audio" }
rows[#rows + 1] = ui.Row { " Codec:", s.codec_name }
end
end
return rows
end
function M.list_meta(url, entries)
local output, err =
Command("ffprobe"):args({ "-v", "quiet", "-show_entries", entries, "-of", "json=c=1", tostring(url) }):output()
if not output then
return nil, Err("Spawn `ffprobe` process returns %s", err)
end
local t = ya.json_decode(output.stdout)
if not t then
return nil, Err("Failed to decode `ffprobe` output: %s", output.stdout)
elseif type(t) ~= "table" then
return nil, Err("Invalid `ffprobe` output: %s", output.stdout)
end
t.format = t.format or {}
t.streams = t.streams or {}
return t
end
return M

View File

@ -1,5 +1,4 @@
os.setlocale("")
package.path = BOOT.plugin_dir .. "/?.yazi/init.lua;" .. package.path
require("dds"):setup()
require("extract"):setup()

View File

@ -1,4 +1,5 @@
table.unpack = table.unpack or unpack
function Err(s, ...) return Error.custom(string.format(s, ...)) end
function ya.clamp(min, x, max)
if x < min then

25
yazi-plugin/src/error.rs Normal file
View File

@ -0,0 +1,25 @@
use mlua::{Lua, MetaMethod, UserData, UserDataMethods};
pub enum Error {
Serde(serde_json::Error),
Custom(String),
}
impl Error {
pub fn install(lua: &Lua) -> mlua::Result<()> {
let new = lua.create_function(|_, msg: String| Ok(Error::Custom(msg)))?;
lua.globals().raw_set("Error", lua.create_table_from([("custom", new)])?)
}
}
impl UserData for Error {
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(MetaMethod::ToString, |_, me, ()| {
Ok(match me {
Error::Serde(e) => e.to_string(),
Error::Custom(s) => s.clone(),
})
});
}
}

View File

@ -18,6 +18,7 @@ pub fn slim_lua(name: &str) -> mlua::Result<Lua> {
crate::file::pour(&lua)?;
crate::url::pour(&lua)?;
crate::Error::install(&lua)?;
crate::loader::install_isolate(&lua)?;
crate::process::install(&lua)?;

View File

@ -62,7 +62,7 @@ pub fn peek(
if let Err(e) = result {
if !e.to_string().contains("Peek task cancelled") {
error!("{e:?}");
error!("{e}");
}
}
});

View File

@ -58,7 +58,7 @@ pub fn spot(
if let Err(e) = result {
if !e.to_string().contains("Spot task cancelled") {
error!("{e:?}");
error!("{e}");
}
}
});

View File

@ -6,7 +6,7 @@ yazi_macro::mod_pub!(
bindings, elements, external, file, fs, isolate, loader, process, pubsub, url, utils
);
yazi_macro::mod_flat!(clipboard config lua runtime);
yazi_macro::mod_flat!(clipboard config error lua runtime);
pub fn init() -> anyhow::Result<()> {
CLIPBOARD.with(<_>::default);

View File

@ -26,6 +26,7 @@ fn stage_1(lua: &'static Lua) -> Result<()> {
globals.raw_set("ya", crate::utils::compose(lua, false)?)?;
globals.raw_set("ps", crate::pubsub::compose(lua)?)?;
crate::Error::install(lua)?;
crate::bindings::Cha::install(lua)?;
crate::loader::install(lua)?;
crate::file::pour(lua)?;

View File

@ -0,0 +1,24 @@
use mlua::{Function, IntoLuaMulti, Lua, LuaSerdeExt, Value};
use super::Utils;
use crate::Error;
impl Utils {
pub(super) fn json_encode(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, value: Value| async move {
match serde_json::to_string(&value) {
Ok(s) => (s, Value::Nil).into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Serde(e)).into_lua_multi(&lua),
}
})
}
pub(super) fn json_decode(lua: &Lua) -> mlua::Result<Function> {
lua.create_async_function(|lua, s: mlua::String| async move {
match serde_json::from_slice::<serde_json::Value>(&s.as_bytes()) {
Ok(v) => (lua.to_value(&v)?, Value::Nil).into_lua_multi(&lua),
Err(e) => (Value::Nil, Error::Serde(e)).into_lua_multi(&lua),
}
})
}
}

View File

@ -1,5 +1,5 @@
#![allow(clippy::module_inception)]
yazi_macro::mod_flat!(
app cache call image layer log preview spot sync target text time user utils
app cache call image json layer log preview spot sync target text time user utils
);

View File

@ -23,6 +23,10 @@ pub fn compose(lua: &Lua, isolate: bool) -> mlua::Result<Table> {
b"image_show" => Utils::image_show(lua)?,
b"image_precache" => Utils::image_precache(lua)?,
// JSON
b"json_encode" => Utils::json_encode(lua)?,
b"json_decode" => Utils::json_decode(lua)?,
// Layout
b"which" => Utils::which(lua)?,
b"input" => Utils::input(lua)?,