diff --git a/hosts/common/programs/assorted.nix b/hosts/common/programs/assorted.nix index 5eb7b70fd..4563f3a42 100644 --- a/hosts/common/programs/assorted.nix +++ b/hosts/common/programs/assorted.nix @@ -1079,7 +1079,8 @@ in sane-cast.sandbox.method = "bunpen"; sane-cast.sandbox.net = "clearnet"; sane-cast.sandbox.autodetectCliPaths = "existingFile"; - sane-cast.suggestedPrograms = [ "go2tv" ]; + sane-cast.sandbox.whitelistAudio = true; #< for blast audio casting + sane-cast.suggestedPrograms = [ "blast-ugjka" "go2tv" ]; sane-die-with-parent.sandbox.enable = false; #< it's a launcher; can't sandbox diff --git a/hosts/common/programs/pipewire/20-virtual.conf b/hosts/common/programs/pipewire/20-virtual.conf index c504cdb08..20f8d91c6 100644 --- a/hosts/common/programs/pipewire/20-virtual.conf +++ b/hosts/common/programs/pipewire/20-virtual.conf @@ -7,6 +7,11 @@ # based on: # but modified: # - duplicate both FL *and* FR +# - don't pipe inputs into outputs +# this effectively creates a filter who's output is always silence. +# blast still works because it grabs from the monitor, not the output. +# better would be to make a sink (a "virtual sink", a "null sink", or a "loopback"), which has no output at *all*, +# but i couldn't get that to actually work (e.g. it doesn't show up in PavuControl, or it does but still forwards audio) context.modules = [ { name = libpipewire-module-filter-chain @@ -40,8 +45,8 @@ context.modules = [ links = [ # we can only tee from nodes, not inputs so we need # to copy the inputs and then tee. - { output = "copyIL:Out" input = "copyOL:In" } - { output = "copyIR:Out" input = "copyOR:In" } + # { output = "copyIL:Out" input = "copyOL:In" } + # { output = "copyIR:Out" input = "copyOR:In" } ] inputs = [ "copyIL:In" "copyIR:In" ] outputs = [ "copyOL:Out" "copyOR:Out" ] diff --git a/pkgs/additional/sane-cast/default.nix b/pkgs/additional/sane-cast/default.nix index 7a118d27f..59095fa49 100644 --- a/pkgs/additional/sane-cast/default.nix +++ b/pkgs/additional/sane-cast/default.nix @@ -1,7 +1,7 @@ { static-nix-shell }: static-nix-shell.mkPython3 { pname = "sane-cast"; - pkgs = [ "go2tv" ]; + pkgs = [ "blast-ugjka" "go2tv" ]; srcRoot = ./.; } diff --git a/pkgs/additional/sane-cast/sane-cast b/pkgs/additional/sane-cast/sane-cast index e5260e2e8..d5d785aae 100755 --- a/pkgs/additional/sane-cast/sane-cast +++ b/pkgs/additional/sane-cast/sane-cast @@ -1,5 +1,5 @@ #!/usr/bin/env nix-shell -#!nix-shell -i python3 -p go2tv -p python3 +#!nix-shell -i python3 -p blast-ugjka -p go2tv -p python3 # vim: set filetype=python : """ cast media (local video or audio files) to a device on the same network @@ -13,6 +13,7 @@ from enum import Enum import argparse import logging import os +import socket import subprocess import tempfile @@ -125,6 +126,31 @@ class Go2TvDriver: os.execvp("go2tv", cli_args) +class BlastDriver: + def cast_to(self, dev: Device) -> None: + blast_args = [ + "blast", + # "blast.monitor" source will create a new output, or we can do that in pipewire config for better predictability. + "-source", "effect_input.virtual.monitor", + "-device", dev.model, + "-ip", get_ranked_ip_addrs()[0], + ] + if dev.compat != Compat.RenameToMp4: + blast_args += [ "-usewav" ]; + + os.execvp("blast", blast_args) + +def get_ranked_ip_addrs(): + """ + return the IP addresses most likely to be LAN addresses + based on: + """ + _name, _aliases, static_addrs = socket.gethostbyname_ex(socket.gethostname()) + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("1", 53)) + con_addr, _port = s.getsockname() + return sorted(set(static_addrs + [ con_addr ]), key=lambda a: (a.startswith("127"), a)) + def filter_devices(devices: list[Device], filter: str) -> list[Device]: return [d for d in devices if d.matches(filter)] @@ -179,13 +205,14 @@ def main(): parser.add_argument("--verbose", action="store_true", help="more logging") parser.add_argument("--always-ask", action="store_true", help="always ask which device to cast to, regardless how many are available") parser.add_argument("--device", help="filter devices based on if this string is contained in their name (case-insensitive)") - parser.add_argument("media", help="file or URL to send to the DLNA device") + parser.add_argument("media", nargs="?", help="file or URL to send to the DLNA device. empty to case just an audio stream") args = parser.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) + blast = BlastDriver() go2tv = Go2TvDriver() devices = go2tv.scan_devices() @@ -197,7 +224,10 @@ def main(): if dev is None or args.always_ask: dev = ask_device(devices) if dev: - go2tv.cast_to(dev, args.media) + if args.media: + go2tv.cast_to(dev, args.media) + else: + blast.cast_to(dev) if __name__ == "__main__": main()