sane-cast: support casting audio
This commit is contained in:
@@ -1079,7 +1079,8 @@ in
|
|||||||
sane-cast.sandbox.method = "bunpen";
|
sane-cast.sandbox.method = "bunpen";
|
||||||
sane-cast.sandbox.net = "clearnet";
|
sane-cast.sandbox.net = "clearnet";
|
||||||
sane-cast.sandbox.autodetectCliPaths = "existingFile";
|
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
|
sane-die-with-parent.sandbox.enable = false; #< it's a launcher; can't sandbox
|
||||||
|
|
||||||
|
@@ -7,6 +7,11 @@
|
|||||||
# based on: <repo:pipewire/pipewire:src/daemon/filter-chain/source-duplicate-FL.conf>
|
# based on: <repo:pipewire/pipewire:src/daemon/filter-chain/source-duplicate-FL.conf>
|
||||||
# but modified:
|
# but modified:
|
||||||
# - duplicate both FL *and* FR
|
# - 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 = [
|
context.modules = [
|
||||||
{ name = libpipewire-module-filter-chain
|
{ name = libpipewire-module-filter-chain
|
||||||
@@ -40,8 +45,8 @@ context.modules = [
|
|||||||
links = [
|
links = [
|
||||||
# we can only tee from nodes, not inputs so we need
|
# we can only tee from nodes, not inputs so we need
|
||||||
# to copy the inputs and then tee.
|
# to copy the inputs and then tee.
|
||||||
{ output = "copyIL:Out" input = "copyOL:In" }
|
# { output = "copyIL:Out" input = "copyOL:In" }
|
||||||
{ output = "copyIR:Out" input = "copyOR:In" }
|
# { output = "copyIR:Out" input = "copyOR:In" }
|
||||||
]
|
]
|
||||||
inputs = [ "copyIL:In" "copyIR:In" ]
|
inputs = [ "copyIL:In" "copyIR:In" ]
|
||||||
outputs = [ "copyOL:Out" "copyOR:Out" ]
|
outputs = [ "copyOL:Out" "copyOR:Out" ]
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{ static-nix-shell }:
|
{ static-nix-shell }:
|
||||||
static-nix-shell.mkPython3 {
|
static-nix-shell.mkPython3 {
|
||||||
pname = "sane-cast";
|
pname = "sane-cast";
|
||||||
pkgs = [ "go2tv" ];
|
pkgs = [ "blast-ugjka" "go2tv" ];
|
||||||
srcRoot = ./.;
|
srcRoot = ./.;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env nix-shell
|
#!/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 :
|
# vim: set filetype=python :
|
||||||
"""
|
"""
|
||||||
cast media (local video or audio files) to a device on the same network
|
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 argparse
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
@@ -125,6 +126,31 @@ class Go2TvDriver:
|
|||||||
|
|
||||||
os.execvp("go2tv", cli_args)
|
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: <https://stackoverflow.com/a/1267524>
|
||||||
|
"""
|
||||||
|
_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]:
|
def filter_devices(devices: list[Device], filter: str) -> list[Device]:
|
||||||
return [d for d in devices if d.matches(filter)]
|
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("--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("--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("--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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.verbose:
|
if args.verbose:
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
blast = BlastDriver()
|
||||||
go2tv = Go2TvDriver()
|
go2tv = Go2TvDriver()
|
||||||
devices = go2tv.scan_devices()
|
devices = go2tv.scan_devices()
|
||||||
|
|
||||||
@@ -197,7 +224,10 @@ def main():
|
|||||||
if dev is None or args.always_ask:
|
if dev is None or args.always_ask:
|
||||||
dev = ask_device(devices)
|
dev = ask_device(devices)
|
||||||
if dev:
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
Reference in New Issue
Block a user