Skip to content

Commit ea6e50c

Browse files
committed
feat!: video spotter & using ffmpeg instead of ffmpegthumbnailer as previewer backend (#1928)
1 parent 22e7d57 commit ea6e50c

File tree

18 files changed

+203
-95
lines changed

18 files changed

+203
-95
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cspell.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +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"}
1+
{"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"}

yazi-boot/src/actions/debug.rs

+15-14
Original file line numberDiff line numberDiff line change
@@ -76,20 +76,21 @@ impl Actions {
7676

7777
writeln!(s, "\nDependencies")?;
7878
#[rustfmt::skip]
79-
writeln!(s, " file : {}", Self::process_output(env::var_os("YAZI_FILE_ONE").unwrap_or("file".into()), "--version"))?;
80-
writeln!(s, " ueberzugpp : {}", Self::process_output("ueberzugpp", "--version"))?;
81-
writeln!(s, " ffmpegthumbnailer: {}", Self::process_output("ffmpegthumbnailer", "-v"))?;
82-
writeln!(s, " pdftoppm : {}", Self::process_output("pdftoppm", "--help"))?;
83-
writeln!(s, " magick : {}", Self::process_output("magick", "--version"))?;
84-
writeln!(s, " fzf : {}", Self::process_output("fzf", "--version"))?;
85-
writeln!(s, " fd : {}", Self::process_output("fd", "--version"))?;
86-
writeln!(s, " fdfind : {}", Self::process_output("fdfind", "--version"))?;
87-
writeln!(s, " rg : {}", Self::process_output("rg", "--version"))?;
88-
writeln!(s, " chafa : {}", Self::process_output("chafa", "--version"))?;
89-
writeln!(s, " zoxide : {}", Self::process_output("zoxide", "--version"))?;
90-
writeln!(s, " 7z : {}", Self::process_output("7z", "i"))?;
91-
writeln!(s, " 7zz : {}", Self::process_output("7zz", "i"))?;
92-
writeln!(s, " jq : {}", Self::process_output("jq", "--version"))?;
79+
writeln!(s, " file : {}", Self::process_output(env::var_os("YAZI_FILE_ONE").unwrap_or("file".into()), "--version"))?;
80+
writeln!(s, " ueberzugpp : {}", Self::process_output("ueberzugpp", "--version"))?;
81+
#[rustfmt::skip]
82+
writeln!(s, " ffmpeg/ffprobe: {} / {}", Self::process_output("ffmpeg", "-version"), Self::process_output("ffprobe", "-version"))?;
83+
writeln!(s, " pdftoppm : {}", Self::process_output("pdftoppm", "--help"))?;
84+
writeln!(s, " magick : {}", Self::process_output("magick", "--version"))?;
85+
writeln!(s, " fzf : {}", Self::process_output("fzf", "--version"))?;
86+
#[rustfmt::skip]
87+
writeln!(s, " fd/fdfind : {} / {}", Self::process_output("fd", "--version"), Self::process_output("fdfind", "--version"))?;
88+
writeln!(s, " rg : {}", Self::process_output("rg", "--version"))?;
89+
writeln!(s, " chafa : {}", Self::process_output("chafa", "--version"))?;
90+
writeln!(s, " zoxide : {}", Self::process_output("zoxide", "--version"))?;
91+
#[rustfmt::skip]
92+
writeln!(s, " 7z/7zz : {} / {}", Self::process_output("7z", "i"), Self::process_output("7zz", "i"))?;
93+
writeln!(s, " jq : {}", Self::process_output("jq", "--version"))?;
9394

9495
writeln!(s, "\nClipboard")?;
9596
#[rustfmt::skip]

yazi-plugin/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ md-5 = { workspace = true }
3232
mlua = { workspace = true }
3333
parking_lot = { workspace = true }
3434
ratatui = { workspace = true }
35+
serde_json = { workspace = true }
3536
shell-words = { workspace = true }
3637
syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
3738
tokio = { workspace = true }

yazi-plugin/preset/plugins/file.lua

+30-47
Original file line numberDiff line numberDiff line change
@@ -16,56 +16,10 @@ end
1616

1717
function M:seek() end
1818

19-
-- TODO: remove this
20-
local hovered_mime = ya.sync(function()
21-
local h = cx.active.current.hovered
22-
if not h then
23-
return nil
24-
elseif h.cha.is_dir then
25-
return "inode/directory"
26-
else
27-
return h:mime()
28-
end
29-
end)
30-
3119
function M:spot(job)
32-
local mime = hovered_mime()
33-
if not mime then
34-
return
35-
end
36-
37-
local file = job.file
38-
local spotter = PLUGIN.spotter(file.url, mime)
39-
local previewer = PLUGIN.previewer(file.url, mime)
40-
local fetchers = PLUGIN.fetchers(file.url, mime)
41-
local preloaders = PLUGIN.preloaders(file.url, mime)
42-
43-
for i, v in ipairs(fetchers) do
44-
fetchers[i] = v.cmd
45-
end
46-
for i, v in ipairs(preloaders) do
47-
preloaders[i] = v.cmd
48-
end
49-
50-
local rows = {}
51-
local row = function(key, value)
52-
local h = type(value) == "table" and #value or 1
53-
rows[#rows + 1] = ui.Row({ key, value }):height(h)
54-
end
55-
56-
rows[#rows + 1] = ui.Row({ "Metadata", "" }):style(ui.Style():fg("red"))
57-
row(" Created:", file.cha.btime and os.date("%y/%m/%d %H:%M", math.floor(file.cha.btime)) or "-")
58-
row(" Modified:", file.cha.mtime and os.date("%y/%m/%d %H:%M", math.floor(file.cha.mtime)) or "-")
59-
row(" Mimetype:", mime)
60-
rows[#rows + 1] = ui.Row({ { "", "Plugins" }, "" }):height(2):style(ui.Style():fg("red"))
61-
row(" Spotter:", spotter and spotter.cmd or "-")
62-
row(" Previewer:", previewer and previewer.cmd or "-")
63-
row(" Fetchers:", #fetchers ~= 0 and fetchers or "-")
64-
row(" Preloaders:", #preloaders ~= 0 and preloaders or "-")
65-
6620
ya.spot_table(
6721
job,
68-
ui.Table(rows)
22+
ui.Table(self:spot_base(job))
6923
:area(ui.Pos { "center", w = 60, h = 20 })
7024
:row(1)
7125
:col(1)
@@ -75,4 +29,33 @@ function M:spot(job)
7529
)
7630
end
7731

32+
function M:spot_base(job)
33+
local url, cha = job.file.url, job.file.cha
34+
local spotter = PLUGIN.spotter(url, job.mime)
35+
local previewer = PLUGIN.previewer(url, job.mime)
36+
local fetchers = PLUGIN.fetchers(url, job.mime)
37+
local preloaders = PLUGIN.preloaders(url, job.mime)
38+
39+
for i, v in ipairs(fetchers) do
40+
fetchers[i] = v.cmd
41+
end
42+
for i, v in ipairs(preloaders) do
43+
preloaders[i] = v.cmd
44+
end
45+
46+
return {
47+
ui.Row({ "Base" }):style(ui.Style():fg("green")),
48+
ui.Row { " Created:", cha.btime and os.date("%y/%m/%d %H:%M", math.floor(cha.btime)) or "-" },
49+
ui.Row { " Modified:", cha.mtime and os.date("%y/%m/%d %H:%M", math.floor(cha.mtime)) or "-" },
50+
ui.Row { " Mimetype:", job.mime },
51+
ui.Row {},
52+
53+
ui.Row({ "Plugins" }):style(ui.Style():fg("green")),
54+
ui.Row { " Spotter:", spotter and spotter.cmd or "-" },
55+
ui.Row { " Previewer:", previewer and previewer.cmd or "-" },
56+
ui.Row { " Fetchers:", #fetchers ~= 0 and fetchers or "-" },
57+
ui.Row { " Preloaders:", #preloaders ~= 0 and preloaders or "-" },
58+
}
59+
end
60+
7861
return M

yazi-plugin/preset/plugins/image.lua

+15-14
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,30 @@ function M:preload()
2323
end
2424

2525
function M:spot(job)
26-
local info = ya.image_info(job.file.url)
27-
28-
local rows = {}
29-
local row = function(key, value)
30-
local h = type(value) == "table" and #value or 1
31-
rows[#rows + 1] = ui.Row({ key, value }):height(h)
32-
end
33-
34-
row("Format:", tostring(info.format))
35-
row("Width:", string.format("%dpx", info.w))
36-
row("Height:", string.format("%dpx", info.h))
37-
row("Color:", tostring(info.color))
26+
local rows = self:spot_base(job)
27+
rows[#rows + 1] = ui.Row {}
3828

3929
ya.spot_table(
4030
job,
41-
ui.Table(rows)
31+
ui.Table(ya.list_merge(rows, require("file"):spot_base(job)))
4232
:area(ui.Pos { "center", w = 60, h = 20 })
4333
:row(job.skip)
34+
:row(1)
4435
:col(1)
4536
:col_style(ui.Style():fg("blue"))
4637
:cell_style(ui.Style():fg("yellow"):reverse())
47-
:widths { ui.Constraint.Length(12), ui.Constraint.Fill(1) }
38+
:widths { ui.Constraint.Length(14), ui.Constraint.Fill(1) }
4839
)
4940
end
5041

42+
function M:spot_base(job)
43+
local info = ya.image_info(job.file.url)
44+
return {
45+
ui.Row({ "Image" }):style(ui.Style():fg("green")),
46+
ui.Row { " Format:", tostring(info.format) },
47+
ui.Row { " Size:", string.format("%dx%d", info.w, info.h) },
48+
ui.Row { " Color:", tostring(info.color) },
49+
}
50+
end
51+
5152
return M

yazi-plugin/preset/plugins/video.lua

+80-14
Original file line numberDiff line numberDiff line change
@@ -38,30 +38,96 @@ function M:preload()
3838
return 1
3939
end
4040

41-
local child, code = Command("ffmpegthumbnailer"):args({
42-
"-q",
43-
"6",
44-
"-c",
45-
"jpeg",
46-
"-i",
47-
tostring(self.file.url),
48-
"-o",
41+
local meta, err = self.list_meta(self.file.url, "format=duration")
42+
if not meta then
43+
ya.err(tostring(err))
44+
return 0
45+
end
46+
47+
local ss = tonumber(meta.format.duration) * percent / 100
48+
local qv = 31 - math.floor(PREVIEW.image_quality * 0.3)
49+
-- stylua: ignore
50+
local child, code = Command("ffmpeg"):args({
51+
"-v", "quiet",
52+
"-ss", ss,
53+
"-an", "-dn", "-sn",
54+
"-i", tostring(self.file.url),
55+
"-vframes", 1,
56+
"-q:v", qv,
57+
"-vf", string.format("scale=%d:-2:flags=fast_bilinear", PREVIEW.max_width),
58+
"-f", "image2",
4959
tostring(cache),
50-
"-t",
51-
tostring(percent),
52-
"-s",
53-
tostring(PREVIEW.max_width),
5460
}):spawn()
5561

5662
if not child then
57-
ya.err("spawn `ffmpegthumbnailer` command returns " .. tostring(code))
63+
ya.err("Spawn `ffmpeg` process returns " .. tostring(code))
5864
return 0
5965
end
6066

6167
local status = child:wait()
6268
return status and status.success and 1 or 2
6369
end
6470

65-
function M:spot(job) require("file"):spot(job) end
71+
function M:spot(job)
72+
local rows = self:spot_base(job)
73+
rows[#rows + 1] = ui.Row {}
74+
75+
ya.spot_table(
76+
job,
77+
ui.Table(ya.list_merge(rows, require("file"):spot_base(job)))
78+
:area(ui.Pos { "center", w = 60, h = 20 })
79+
:row(1)
80+
:col(1)
81+
:col_style(ui.Style():fg("blue"))
82+
:cell_style(ui.Style():fg("yellow"):reverse())
83+
:widths { ui.Constraint.Length(14), ui.Constraint.Fill(1) }
84+
)
85+
end
86+
87+
function M:spot_base(job)
88+
local meta, err = self.list_meta(job.file.url, "format=duration:stream=codec_name,codec_type,width,height")
89+
if not meta then
90+
ya.err(tostring(err))
91+
return {}
92+
end
93+
94+
local dur = meta.format.duration
95+
local rows = {
96+
ui.Row({ "Video" }):style(ui.Style():fg("green")),
97+
ui.Row { " Codec:", meta.streams[1].codec_name },
98+
ui.Row { " Duration:", string.format("%d:%02d", math.floor(dur / 60), math.floor(dur % 60)) },
99+
}
100+
101+
for i, s in ipairs(meta.streams) do
102+
if s.codec_type == "video" then
103+
rows[#rows + 1] = ui.Row { string.format(" Stream %d:", i), "video" }
104+
rows[#rows + 1] = ui.Row { " Codec:", s.codec_name }
105+
rows[#rows + 1] = ui.Row { " Size:", string.format("%dx%d", s.width, s.height) }
106+
elseif s.codec_type == "audio" then
107+
rows[#rows + 1] = ui.Row { string.format(" Stream %d:", i), "audio" }
108+
rows[#rows + 1] = ui.Row { " Codec:", s.codec_name }
109+
end
110+
end
111+
return rows
112+
end
113+
114+
function M.list_meta(url, entries)
115+
local output, err =
116+
Command("ffprobe"):args({ "-v", "quiet", "-show_entries", entries, "-of", "json=c=1", tostring(url) }):output()
117+
if not output then
118+
return nil, Err("Spawn `ffprobe` process returns %s", err)
119+
end
120+
121+
local t = ya.json_decode(output.stdout)
122+
if not t then
123+
return nil, Err("Failed to decode `ffprobe` output: %s", output.stdout)
124+
elseif type(t) ~= "table" then
125+
return nil, Err("Invalid `ffprobe` output: %s", output.stdout)
126+
end
127+
128+
t.format = t.format or {}
129+
t.streams = t.streams or {}
130+
return t
131+
end
66132

67133
return M

yazi-plugin/preset/setup.lua

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
os.setlocale("")
2-
package.path = BOOT.plugin_dir .. "/?.yazi/init.lua;" .. package.path
32

43
require("dds"):setup()
54
require("extract"):setup()

yazi-plugin/preset/ya.lua

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
table.unpack = table.unpack or unpack
2+
function Err(s, ...) return Error.custom(string.format(s, ...)) end
23

34
function ya.clamp(min, x, max)
45
if x < min then

yazi-plugin/src/error.rs

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use mlua::{Lua, MetaMethod, UserData, UserDataMethods};
2+
3+
pub enum Error {
4+
Serde(serde_json::Error),
5+
Custom(String),
6+
}
7+
8+
impl Error {
9+
pub fn install(lua: &Lua) -> mlua::Result<()> {
10+
let new = lua.create_function(|_, msg: String| Ok(Error::Custom(msg)))?;
11+
12+
lua.globals().raw_set("Error", lua.create_table_from([("custom", new)])?)
13+
}
14+
}
15+
16+
impl UserData for Error {
17+
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
18+
methods.add_meta_method(MetaMethod::ToString, |_, me, ()| {
19+
Ok(match me {
20+
Error::Serde(e) => e.to_string(),
21+
Error::Custom(s) => s.clone(),
22+
})
23+
});
24+
}
25+
}

yazi-plugin/src/isolate/isolate.rs

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub fn slim_lua(name: &str) -> mlua::Result<Lua> {
1818
crate::file::pour(&lua)?;
1919
crate::url::pour(&lua)?;
2020

21+
crate::Error::install(&lua)?;
2122
crate::loader::install_isolate(&lua)?;
2223
crate::process::install(&lua)?;
2324

yazi-plugin/src/isolate/peek.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ pub fn peek(
6262

6363
if let Err(e) = result {
6464
if !e.to_string().contains("Peek task cancelled") {
65-
error!("{e:?}");
65+
error!("{e}");
6666
}
6767
}
6868
});

yazi-plugin/src/isolate/spot.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ pub fn spot(
5858

5959
if let Err(e) = result {
6060
if !e.to_string().contains("Spot task cancelled") {
61-
error!("{e:?}");
61+
error!("{e}");
6262
}
6363
}
6464
});

yazi-plugin/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ yazi_macro::mod_pub!(
66
bindings, elements, external, file, fs, isolate, loader, process, pubsub, url, utils
77
);
88

9-
yazi_macro::mod_flat!(clipboard config lua runtime);
9+
yazi_macro::mod_flat!(clipboard config error lua runtime);
1010

1111
pub fn init() -> anyhow::Result<()> {
1212
CLIPBOARD.with(<_>::default);

yazi-plugin/src/lua.rs

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ fn stage_1(lua: &'static Lua) -> Result<()> {
2626
globals.raw_set("ya", crate::utils::compose(lua, false)?)?;
2727
globals.raw_set("ps", crate::pubsub::compose(lua)?)?;
2828

29+
crate::Error::install(lua)?;
2930
crate::bindings::Cha::install(lua)?;
3031
crate::loader::install(lua)?;
3132
crate::file::pour(lua)?;

0 commit comments

Comments
 (0)