Audio capture and streaming app for webOS 5/6
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>
This commit is contained in:
Executable
+92
@@ -0,0 +1,92 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user