Compare commits
5 Commits
master
...
wip-mpv-sy
Author | SHA1 | Date |
---|---|---|
Colin | 72c7287445 | |
Colin | b715fd346f | |
Colin | 1b9b0ac0f6 | |
Colin | 79c4e2c405 | |
Colin | 17a3f90825 |
4
TODO.md
4
TODO.md
|
@ -1,10 +1,14 @@
|
|||
## BUGS
|
||||
- Signal restart loop drains battery
|
||||
- decrease s6 restart time?
|
||||
- mpv `player-mode=pseudo-gui` swallows all loggin
|
||||
- ringer (i.e. dino incoming call) doesn't prevent moby from sleeping
|
||||
- sway mouse/kb hotplug doesn't work
|
||||
- `nix` operations from lappy hang when `desko` is unreachable
|
||||
- could at least direct the cache to `http://desko-hn:5001`
|
||||
|
||||
## REFACTORING:
|
||||
- REMOVE DEPRECATED `crypt` from sftpgo_auth_hook
|
||||
- consolidate ~/dev and ~/ref
|
||||
- ~/dev becomes a link to ~/ref/cat/mine
|
||||
- fold hosts/common/home/ssh.nix -> hosts/common/users/colin.nix
|
||||
|
|
|
@ -91,48 +91,22 @@ let
|
|||
local last_up = cursor.last_event['primary_up'] or { time = 0 }
|
||||
if cursor.hover_raw or last_down.time >= last_up.time then cursor:move(mouse.x, mouse.y) end"
|
||||
|
||||
### patch so that the volume control corresponds to `ao-volume`, i.e. the system-wide volume.
|
||||
### patch so that uosc volume control is routed to sane-sysvol.
|
||||
### this is particularly nice for moby, because it avoids the awkwardness that system volume
|
||||
### is hard to adjust while screen is on.
|
||||
### note that only under alsa (`-ao=alsa`) does `ao-volume` actually correspond to system volume.
|
||||
### previously i used ao-volume instead of sane-sysvol: but that forced `ao=alsa`
|
||||
### and came with heavy perf penalties (especially when adjusting the volume)
|
||||
substituteInPlace src/uosc/main.lua \
|
||||
--replace-fail \
|
||||
"mp.observe_property('volume', 'number', create_state_setter('volume'))" \
|
||||
"mp.observe_property('volume', 'number', update_ao_volume)"
|
||||
"mp.observe_property('volume'" \
|
||||
"mp.observe_property('user-data/sane-sysvol/volume'"
|
||||
substituteInPlace src/uosc/elements/Volume.lua \
|
||||
--replace-fail "mp.commandv('set', 'volume'" "mp.commandv('set', 'ao-volume'" \
|
||||
--replace-fail "mp.set_property_native('volume'" "mp.set_property('ao-volume'"
|
||||
|
||||
# `ao-volume` isn't actually an observable property.
|
||||
# as of 2024/03/02, they *may* be working on that:
|
||||
# - <https://github.com/mpv-player/mpv/pull/13604#issuecomment-1971665736>
|
||||
# in the meantime, just query the volume every tick (i.e. frame).
|
||||
# alternative is mpv's JSON IPC feature, where i could notify its socket whenever pipewire volume changes.
|
||||
cat <<EOF >> src/uosc/main.lua
|
||||
function update_ao_volume(_, vol)
|
||||
if vol == nil then
|
||||
-- vol will be nil if called manually, instead of via observe_property
|
||||
vol = mp.get_property('ao-volume')
|
||||
end
|
||||
if vol == nil then
|
||||
vol = 0
|
||||
else
|
||||
vol = tonumber(vol)
|
||||
end
|
||||
|
||||
if vol ~= state.volume then
|
||||
set_state('volume', vol)
|
||||
request_render()
|
||||
end
|
||||
end
|
||||
-- tick seems to occur on every redraw (even when volume is hidden).
|
||||
-- in practice: for every new frame of the source, or whenever the cursor is moved.
|
||||
mp.register_event('tick', update_ao_volume)
|
||||
-- if paused and cursor isn't moving, then `tick` isn't called. fallback to a timer.
|
||||
mp.add_periodic_timer(2, update_ao_volume)
|
||||
-- invoke immediately to ensure state.volume is non-nil
|
||||
update_ao_volume()
|
||||
EOF
|
||||
--replace-fail \
|
||||
"mp.commandv('set', 'volume'" \
|
||||
"mp.set_property_native('user-data/sane-sysvol/volume'" \
|
||||
--replace-fail \
|
||||
"mp.set_property_native('volume'" \
|
||||
"mp.set_property_native('user-data/sane-sysvol/volume'"
|
||||
'';
|
||||
});
|
||||
mpv-unwrapped = pkgs.mpv-unwrapped.overrideAttrs (upstream: {
|
||||
|
@ -148,7 +122,7 @@ let
|
|||
in
|
||||
{
|
||||
sane.programs.mpv = {
|
||||
packageUnwrapped = pkgs.wrapMpv mpv-unwrapped {
|
||||
packageUnwrapped = pkgs.wrapMpv (mpv-unwrapped.override { lua = pkgs.luajit; }) {
|
||||
scripts = [
|
||||
pkgs.mpvScripts.mpris
|
||||
pkgs.mpvScripts.mpv-playlistmanager
|
||||
|
@ -216,7 +190,9 @@ in
|
|||
# for `watch_later`
|
||||
".local/state/mpv"
|
||||
];
|
||||
fs.".config/mpv/scripts/sane/main.lua".symlink.target = ./sane-main.lua;
|
||||
fs.".config/mpv/scripts/sane-cast/main.lua".symlink.target = ./sane-cast-main.lua;
|
||||
fs.".config/mpv/scripts/sane-sysvol/main.lua".symlink.target = ./sane-sysvol/main.lua;
|
||||
fs.".config/mpv/scripts/sane-sysvol/non_blocking_popen.lua".symlink.target = ./sane-sysvol/non_blocking_popen.lua;
|
||||
fs.".config/mpv/input.conf".symlink.target = ./input.conf;
|
||||
fs.".config/mpv/mpv.conf".symlink.target = ./mpv.conf;
|
||||
fs.".config/mpv/script-opts/osc.conf".symlink.target = ./osc.conf;
|
||||
|
|
|
@ -31,7 +31,7 @@ ctrl+s async screenshot #! Utils > Screenshot
|
|||
alt+i script-binding uosc/keybinds #! Utils > Key bindings
|
||||
O script-binding uosc/show-in-directory #! Utils > Show in directory
|
||||
# script-binding uosc/open-config-directory #! Utils > Open config directory
|
||||
ctrl+r script-binding sane/blast #! Audiocast
|
||||
ctrl+t script-binding sane/go2tv-video #! Cast
|
||||
# script-binding sane/go2tv-stream #! Cast (...) > Stream
|
||||
# script-binding sane/go2tv-gui #! Cast (...) > GUI
|
||||
ctrl+r script-binding sane-cast/blast #! Audiocast
|
||||
ctrl+t script-binding sane-cast/go2tv-video #! Cast
|
||||
# script-binding sane-cast/go2tv-stream #! Cast (...) > Stream
|
||||
# script-binding sane-cast/go2tv-gui #! Cast (...) > GUI
|
||||
|
|
|
@ -0,0 +1,240 @@
|
|||
msg = require('mp.msg')
|
||||
msg.trace('sane-sysvol: load: begin')
|
||||
|
||||
non_blocking_popen = require("non_blocking_popen")
|
||||
|
||||
RD_SIZE = 4096
|
||||
|
||||
function startswith(superstring, substring)
|
||||
return superstring:sub(1, substring:len()) == substring
|
||||
end
|
||||
function strip_prefix(superstring, substring)
|
||||
return superstring:sub(substring:len())
|
||||
end
|
||||
|
||||
function ltrim(s)
|
||||
-- remove all leading whitespace from `s`
|
||||
local i = 1
|
||||
while s:sub(i, i) == " " or s:sub(i, i) == "\t" do
|
||||
i = i + 1
|
||||
end
|
||||
return s:sub(i)
|
||||
end
|
||||
|
||||
function subprocess(args)
|
||||
mp.command_native({
|
||||
name = "subprocess",
|
||||
args = args,
|
||||
-- these arguments below probably don't matter: copied from sane-cast
|
||||
detach = false,
|
||||
capture_stdout = false,
|
||||
capture_stderr = false,
|
||||
passthrough_stdin = false,
|
||||
playback_only = false,
|
||||
})
|
||||
end
|
||||
|
||||
function sysvol_new()
|
||||
return {
|
||||
-- sysvol is pipewire-native volume
|
||||
-- it's the cube of the equivalent 0-100% value represented inside mpv
|
||||
sysvol = nil,
|
||||
change_sysvol = function(self, mpv_vol)
|
||||
-- called when mpv wants to set the system-wide volume
|
||||
if mpv_vol == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local old_mpv_vol = nil
|
||||
if self.sysvol ~= nil then
|
||||
old_mpv_vol = 100 * self.sysvol^(1/3)
|
||||
end
|
||||
if old_mpv_vol ~= nil and math.floor(old_mpv_vol) == math.floor(mpv_vol) then
|
||||
return
|
||||
end
|
||||
|
||||
local volstr = tostring(mpv_vol) .. "%"
|
||||
msg.debug("setting system-wide volume:", volstr)
|
||||
self.sysvol = (0.01*mpv_vol)^3
|
||||
subprocess({
|
||||
"wpctl",
|
||||
"set-volume",
|
||||
"@DEFAULT_AUDIO_SINK@",
|
||||
volstr
|
||||
})
|
||||
end,
|
||||
on_sysvol_change = function(self, sysvol)
|
||||
if sysvol == nil then
|
||||
return
|
||||
end
|
||||
-- called when the pipewire system volume is changed (either by us, or an external application)
|
||||
local new_mpv_vol = 100 * sysvol^(1/3)
|
||||
local old_mpv_vol = nil
|
||||
if self.sysvol ~= nil then
|
||||
old_mpv_vol = 100 * self.sysvol^(1/3)
|
||||
end
|
||||
|
||||
if old_mpv_vol ~= nil and math.abs(new_mpv_vol - old_mpv_vol) < 1.0 then
|
||||
msg.debug("NOT announcing volume change to mpv (because it was what triggered the change):", old_mpv_vol, new_mpv_vol)
|
||||
return
|
||||
end
|
||||
|
||||
self.sysvol = sysvol
|
||||
msg.debug("announcing volume change to mpv:", old_mpv_vol, new_mpv_vol)
|
||||
mp.set_property_native("user-data/sane-sysvol/volume", new_mpv_vol)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
function pwmon_parser_new()
|
||||
return {
|
||||
-- volume: pipewire-native volume. usually 0.0 - 1.0, but can go higher (e.g. 3.25)
|
||||
-- `wpctl get-volume` and this volume are related, in that the volume reported by
|
||||
-- wpctl is the cube-root of this one.
|
||||
volume = {}, -- object-id (number) -> volume (number)
|
||||
mute = {}, -- object-id (number) -> mute (bool)
|
||||
last_audio_device_id = nil, -- TODO: might not actually be necessary
|
||||
|
||||
-- parser state:
|
||||
in_changed = false,
|
||||
changed_id = nil,
|
||||
in_device = false,
|
||||
in_direction = false,
|
||||
in_output = false,
|
||||
in_vol = false,
|
||||
in_mute = false,
|
||||
|
||||
feed_line = function(self, line)
|
||||
line = ltrim(line)
|
||||
if startswith(line, "changed:") then
|
||||
self.in_changed = true
|
||||
self.changed_id = nil
|
||||
self.in_device = false
|
||||
self.in_direction = false
|
||||
self.in_output = false
|
||||
self.in_vol = false
|
||||
self.in_mute = false
|
||||
self.in_properties = false
|
||||
elseif startswith(line, "added:") or startswith(line, "removed:") then
|
||||
self.in_changed = false
|
||||
self.changed_id = nil
|
||||
self.in_device = false
|
||||
self.in_direction = false
|
||||
self.in_output = false
|
||||
self.in_vol = false
|
||||
self.in_mute = false
|
||||
self.in_properties = false
|
||||
elseif startswith(line, "id: ") and self.in_changed then
|
||||
if self.changed_id == nil then
|
||||
self.changed_id = tonumber(strip_prefix(line, "id: "))
|
||||
msg.debug("changed_id:", self.changed_id)
|
||||
end
|
||||
elseif startswith(line, "type: ") and self.in_changed then
|
||||
self.in_device = startswith(line, "type: PipeWire:Interface:Device")
|
||||
msg.trace("parsed type:", line, self.in_device)
|
||||
elseif startswith(line, "Prop: ") and self.in_changed and self.in_device then
|
||||
self.in_direction = startswith(line, "Prop: key Spa:Pod:Object:Param:Route:direction")
|
||||
if self.in_direction then
|
||||
self.in_output = false
|
||||
end
|
||||
-- which of the *Volumes params we read is unclear.
|
||||
-- alternative to this is to just detect the change, and then cal wpctl get-volume @DEFAULT_AUDIO_SINK@
|
||||
self.in_vol = startswith(line, "Prop: key Spa:Pod:Object:Param:Props:channelVolumes")
|
||||
self.in_mute = startswith(line, "Prop: key Spa:Pod:Object:Param:Props:softMute")
|
||||
msg.trace("parsed `Prop:`", line, self.in_vol)
|
||||
elseif line:find("Spa:Enum:Direction:Output", 1, true) and self.in_direction then
|
||||
self.in_output = true
|
||||
elseif startswith(line, "Float ") and self.in_changed and self.in_device and self.in_output and self.in_vol then
|
||||
value = tonumber(strip_prefix(line, "Float "))
|
||||
self:feed_volume(value)
|
||||
elseif startswith(line, "Bool ") and self.in_changed and self.in_device and self.in_output and self.in_mute then
|
||||
value = tonumber(strip_prefix(line, "Bool ")) == "true"
|
||||
self:feed_mute(value)
|
||||
elseif startswith(line, "properties:") and self.in_changed and self.in_device then
|
||||
self.in_properties = true
|
||||
elseif line == 'media.class = "Audio/Device"' and self.in_changed and self.in_device and self.in_properties then
|
||||
self.last_audio_device_id = self.changed_id
|
||||
msg.debug("last_audio_device_id:", self.changed_id)
|
||||
end
|
||||
end,
|
||||
|
||||
feed_volume = function(self, vol)
|
||||
msg.debug("volume:", self.changed_id, vol)
|
||||
self.volume[self.changed_id] = vol
|
||||
end,
|
||||
feed_mute = function(self, mute)
|
||||
msg.debug("mute:", self.changed_id, mute)
|
||||
self.mute[self.changed_id] = mute
|
||||
end,
|
||||
get_effective_volume = function(self, id)
|
||||
if id == nil then
|
||||
id = self.last_audio_device_id
|
||||
end
|
||||
|
||||
if self.mute[id] then
|
||||
return 0
|
||||
else
|
||||
return self.volume[id]
|
||||
end
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
function pwmon_new()
|
||||
return {
|
||||
-- non_blocking_popen handle for the pw-mon process
|
||||
-- which can be periodically read and parsed to detect volume changes
|
||||
handle = non_blocking_popen.non_blocking_popen("pw-mon", RD_SIZE),
|
||||
stdout_unparsed = "",
|
||||
pwmon_parser = pwmon_parser_new(),
|
||||
service = function(self)
|
||||
-- do a single non-blocking read, and parse the result
|
||||
-- in the *rare* case in which more than RD_SIZE data is ready, we service that remaining data on the next call
|
||||
local buf, res = self.handle:read(RD_SIZE)
|
||||
if res == "closed" then
|
||||
msg.debug("pw-mon unexpectedly closed!")
|
||||
end
|
||||
if buf ~= nil then
|
||||
self.stdout_unparsed = self.stdout_unparsed .. buf
|
||||
self:consume_stdout()
|
||||
end
|
||||
end,
|
||||
consume_stdout = function(self)
|
||||
local idx_newline, next_newline = 0, 0
|
||||
while next_newline ~= nil do
|
||||
next_newline = self.stdout_unparsed:find("\n", idx_newline + 1, true)
|
||||
if next_newline ~= nil then
|
||||
self:ingest_line(self.stdout_unparsed:sub(idx_newline + 1, next_newline - 1))
|
||||
idx_newline = next_newline
|
||||
end
|
||||
end
|
||||
self.stdout_unparsed = self.stdout_unparsed:sub(idx_newline + 1)
|
||||
end,
|
||||
ingest_line = function(self, line)
|
||||
msg.trace("pw-mon:", line)
|
||||
local old_vol = self.pwmon_parser:get_effective_volume()
|
||||
self.pwmon_parser:feed_line(line)
|
||||
local new_vol = self.pwmon_parser:get_effective_volume()
|
||||
if new_vol ~= old_vol then
|
||||
msg.debug("pipewire volume change:", old_vol, new_vol)
|
||||
mp.set_property_native("user-data/sane-sysvol/pw-mon-volume", new_vol)
|
||||
end
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
mp.set_property_native("user-data/sane-sysvol/volume", 0)
|
||||
|
||||
local sysvol = sysvol_new()
|
||||
mp.observe_property("user-data/sane-sysvol/volume", "native", function(_, val)
|
||||
sysvol:change_sysvol(val)
|
||||
end)
|
||||
mp.observe_property("user-data/sane-sysvol/pw-mon-volume", "native", function(_, val)
|
||||
sysvol:on_sysvol_change(val)
|
||||
end)
|
||||
|
||||
local pwmon = pwmon_new()
|
||||
mp.register_event('tick', function() pwmon:service() end)
|
||||
|
||||
msg.trace("sane-sysvol: load: complete")
|
|
@ -0,0 +1,80 @@
|
|||
-- source: <https://gist.github.com/max1220/c19ccd4d90ed32d41b879eba727cbcbd>
|
||||
-- requires: luajit
|
||||
--
|
||||
-- Implements a basic binding for popen that allows non-blocking reads
|
||||
-- returned "file" table only supports :read(with an optional size argument, no mode etc.) and :close
|
||||
local function non_blocking_popen(cmd, read_buffer_size)
|
||||
local ffi = require("ffi")
|
||||
|
||||
-- C functions that we need
|
||||
ffi.cdef([[
|
||||
void* popen(const char* cmd, const char* mode);
|
||||
int pclose(void* stream);
|
||||
int fileno(void* stream);
|
||||
int fcntl(int fd, int cmd, int arg);
|
||||
int *__errno_location ();
|
||||
ssize_t read(int fd, void* buf, size_t count);
|
||||
]])
|
||||
|
||||
-- you can compile a simple C programm to find these values(Or look in the headers)
|
||||
local F_SETFL = 4
|
||||
local O_NONBLOCK = 2048
|
||||
local EAGAIN = 11
|
||||
|
||||
-- this "array" holds the errno variable
|
||||
local _errno = ffi.C.__errno_location()
|
||||
|
||||
-- the buffer for reading from the process
|
||||
local read_buffer_size = tonumber(read_buffer_size) or 2048
|
||||
local read_buffer = ffi.new('uint8_t[?]',read_buffer_size)
|
||||
|
||||
-- get a FILE* for our command
|
||||
local file = assert(ffi.C.popen(cmd, "r"))
|
||||
|
||||
-- turn the FILE* to a fd(int) for fcntl
|
||||
local fd = ffi.C.fileno(file)
|
||||
|
||||
-- set non-blocking mode for read
|
||||
assert(ffi.C.fcntl(fd, F_SETFL, O_NONBLOCK)==0, "fcntl failed")
|
||||
|
||||
-- close the process, prevent reading, allow garbage colletion
|
||||
function file_close(self)
|
||||
ffi.C.pclose(file)
|
||||
self.read_buffer = nil
|
||||
read_buffer = nil
|
||||
self.read = function() return nil, "closed"end
|
||||
end
|
||||
|
||||
-- read up to size bytes from the process. Returns data(string) and number of bytes read if successfull,
|
||||
-- nil, "EAGAIN" if there is no data aviable, and
|
||||
-- nil, "closed" if the process has ended
|
||||
local read = ffi.C.read
|
||||
function file_read(self, size)
|
||||
local _size = math.min(read_buffer_size, size)
|
||||
while true do
|
||||
local nbytes = read(fd,read_buffer,_size)
|
||||
if nbytes > 0 then
|
||||
local data = ffi.string(read_buffer, nbytes)
|
||||
return data, nbytes
|
||||
elseif (nbytes == -1) and (_errno[0] == EAGAIN) then
|
||||
return nil, "EAGAIN"
|
||||
else
|
||||
file_close(self)
|
||||
return nil, "closed"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
_fd = fd,
|
||||
_file = file,
|
||||
_read_buffer = read_buffer,
|
||||
_read_buffer_size = read_buffer_size,
|
||||
read = file_read,
|
||||
close = file_close
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
non_blocking_popen = non_blocking_popen
|
||||
}
|
Loading…
Reference in New Issue