Captures the TV's audio and sends it out over several transports. The
primary one is HyperHDR: RTP/L16 to a host-side loopback device, since
HyperHDR has no network audio input of its own. A second route renders
the spectrum on the TV and sends FlatBuffers images to port 19400
instead, for setups where touching the host's sound config is not an
option.
native/ the service: capture backends (PulseAudio, ALSA, exec,
test tone, all dlopen-based), DSP, and one file per sink
frontend/ D-pad driven UI at a fixed 1920x1080
servicefiles/ native service manifest plus the boot script
host/ RTP receiver and the loopback installer for the HyperHDR
machine
tools/ build/package, asset generation, Homebrew Channel
manifest, on-TV probe
test/ host-side suites: FlatBuffers and RTP verified against
real decoders, the engine end to end, the page in jsdom
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
93 lines
3.1 KiB
Python
Executable File
93 lines
3.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Writes the Homebrew Channel manifest for a built ipk.
|
|
|
|
The Homebrew Channel installs apps from a manifest that points at the ipk and
|
|
carries its hash. Publishing means putting this file somewhere stable (a GitHub
|
|
release asset, or the repository itself) and submitting the URL to
|
|
webosbrew/repo.
|
|
|
|
python3 tools/make-manifest.py \\
|
|
--base-url https://github.com/you/lgtv-audio-cap/releases/download/v1.0.0
|
|
|
|
Defaults to the newest ipk in out/ and writes out/manifest.json.
|
|
"""
|
|
|
|
import argparse
|
|
import glob
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
ROOT = os.path.join(HERE, os.pardir)
|
|
|
|
SOURCE_URL = "https://github.com/webosbrew/lgtv-audio-cap"
|
|
|
|
|
|
def newest_ipk(directory):
|
|
matches = sorted(glob.glob(os.path.join(directory, "*.ipk")),
|
|
key=os.path.getmtime, reverse=True)
|
|
return matches[0] if matches else None
|
|
|
|
|
|
def sha256(path):
|
|
digest = hashlib.sha256()
|
|
with open(path, "rb") as fh:
|
|
for block in iter(lambda: fh.read(1 << 20), b""):
|
|
digest.update(block)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
|
parser.add_argument("--ipk", default=None, help="path to the ipk")
|
|
parser.add_argument("--base-url", default=None,
|
|
help="URL the ipk and icon will be served from")
|
|
parser.add_argument("--source-url", default=SOURCE_URL,
|
|
help="project page shown in the Homebrew Channel")
|
|
parser.add_argument("--out", default=os.path.join(ROOT, "out", "manifest.json"))
|
|
args = parser.parse_args()
|
|
|
|
appinfo = json.load(open(os.path.join(ROOT, "frontend", "appinfo.json")))
|
|
|
|
ipk = args.ipk or newest_ipk(os.path.join(ROOT, "out"))
|
|
if not ipk or not os.path.exists(ipk):
|
|
print("no ipk found; run ./tools/build.sh first", file=sys.stderr)
|
|
return 1
|
|
|
|
name = os.path.basename(ipk)
|
|
base = (args.base_url or "").rstrip("/")
|
|
|
|
manifest = {
|
|
"id": appinfo["id"],
|
|
"version": appinfo["version"],
|
|
"type": appinfo.get("type", "web"),
|
|
"title": appinfo.get("title", appinfo["id"]),
|
|
"appDescription": appinfo.get("appDescription", ""),
|
|
"iconUri": base + "/icon.png" if base else "icon.png",
|
|
"sourceUrl": args.source_url,
|
|
# The service reads the TV's audio devices, which are root-only. The
|
|
# Homebrew Channel uses this to elevate the service at install time.
|
|
"rootRequired": True,
|
|
"ipkUrl": base + "/" + name if base else name,
|
|
"ipkHash": {"sha256": sha256(ipk)},
|
|
"ipkSize": os.path.getsize(ipk),
|
|
}
|
|
|
|
os.makedirs(os.path.dirname(args.out), exist_ok=True)
|
|
with open(args.out, "w") as fh:
|
|
json.dump(manifest, fh, indent=2)
|
|
fh.write("\n")
|
|
|
|
print(json.dumps(manifest, indent=2))
|
|
print("\nwrote %s" % os.path.relpath(args.out, ROOT), file=sys.stderr)
|
|
if not base:
|
|
print("note: no --base-url given, so ipkUrl and iconUri are relative",
|
|
file=sys.stderr)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|