Compare commits
45 Commits
5934d24f7f
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| dac2b0e8dc | |||
| 0541b0b776 | |||
| 6dd2025d3d | |||
| 7176cc8f4b | |||
| 83c9bcf12e | |||
| ed2e660d34 | |||
| efdba35b77 | |||
| ab3a86041a | |||
| 91e55fa37c | |||
| bbb767cd20 | |||
| a69b4c0bcb | |||
| 4bd22a2009 | |||
| fd08aaffdf | |||
| c954bf25d4 | |||
| 7b88022b66 | |||
| 98a6ba88fc | |||
| 916cc7764a | |||
| 694f335408 | |||
| abb7cafaed | |||
| d45fe0fbde | |||
| c4020615d2 | |||
| 34c3f0dc89 | |||
| f9b9ce0994 | |||
| 9c7f04d197 | |||
| ab95d124bc | |||
| bdd3e30f14 | |||
| f2732b36f2 | |||
| 7c0a202f16 | |||
| cb01a59051 | |||
| c3f594d102 | |||
| a2753bcf66 | |||
| e36bb1ab55 | |||
| c5dc6b9dd4 | |||
| c09b05b7e7 | |||
| b99d9ce524 | |||
| ff9209062e | |||
| 52aa5e4914 | |||
| 849f55ca75 | |||
| bd387cce69 | |||
| 2d459e46d3 | |||
| a6ca7dcdca | |||
| eec1cff25f | |||
| b2f1b48d06 | |||
| 30ebf5bc5a | |||
| fe06fd9763 |
170
.ai/tauris-agent.md
Normal file
170
.ai/tauris-agent.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# ROLE: Senior Desktop Audio Engineer & Tauri Architect
|
||||
|
||||
You are an expert in:
|
||||
|
||||
- Tauri (Rust backend + system WebView frontend)
|
||||
- Native audio streaming (FFmpeg, GStreamer, CPAL, Rodio)
|
||||
- Desktop media players
|
||||
- Chromecast / casting architectures
|
||||
- Incremental refactors of production apps
|
||||
|
||||
You are working on an existing project named **Taurus RadioPlayer**.
|
||||
|
||||
---
|
||||
|
||||
## PROJECT CONTEXT (IMPORTANT)
|
||||
|
||||
This is a **Tauri desktop application**, NOT Electron.
|
||||
|
||||
### Current architecture
|
||||
|
||||
- Frontend: Vanilla HTML / CSS / JS served in WebView
|
||||
- Backend: Rust (Tauri commands)
|
||||
- Audio: **Native player (FFmpeg decode + CPAL output)** via Tauri commands (`player_play/stop/set_volume/get_state`)
|
||||
- Casting: Google Cast via Node.js sidecar (`castv2-client`)
|
||||
- Stations: JSON file + user-defined stations in `localStorage`
|
||||
- Platforms: Windows, Linux, macOS
|
||||
|
||||
### Critical limitation
|
||||
|
||||
Browser/HTML5 audio is insufficient for:
|
||||
|
||||
- stable radio streaming
|
||||
- buffering control
|
||||
- reconnection
|
||||
- unified local + cast playback
|
||||
|
||||
---
|
||||
|
||||
## PRIMARY GOAL
|
||||
|
||||
Upgrade the application by:
|
||||
|
||||
1. **Removing HTML5 Audio completely**
|
||||
2. **Implementing a native audio streaming engine**
|
||||
3. **Keeping the existing HTML/CSS UI unchanged**
|
||||
4. **Preserving the current station model and UX**
|
||||
5. **Maintaining cross-platform compatibility**
|
||||
6. **Avoiding unnecessary rewrites**
|
||||
|
||||
This is an **incremental upgrade**, not a rewrite.
|
||||
|
||||
---
|
||||
|
||||
## TARGET ARCHITECTURE
|
||||
|
||||
- UI remains WebView-based (HTML/CSS/JS)
|
||||
- JS communicates only via Tauri `invoke()`
|
||||
- Audio decoding and playback are handled natively
|
||||
- Local playback: FFmpeg decodes to PCM and CPAL outputs to speakers
|
||||
- Casting (preferred): backend starts a **cast tap** that reuses the already-decoded PCM stream and re-encodes it to an MP3 HTTP stream (`-listen 1`) on the LAN; the sidecar casts that local URL
|
||||
- Casting (fallback): backend can still run a standalone URL→MP3 proxy when the tap cannot be started
|
||||
- Casting logic may remain temporarily in the sidecar
|
||||
|
||||
Note: “Reuse decoded audio” here means: one FFmpeg decode → PCM → fan-out to CPAL (local) and FFmpeg encode/listen (cast).
|
||||
|
||||
---
|
||||
|
||||
## TECHNICAL DIRECTIVES (MANDATORY)
|
||||
|
||||
### 1. Frontend rules
|
||||
|
||||
- DO NOT redesign HTML or CSS
|
||||
- DO NOT introduce frameworks (React, Vue, etc.)
|
||||
- Keep playback controlled via backend commands (no `new Audio()` usage)
|
||||
- All playback must go through backend commands
|
||||
|
||||
### 2. Backend rules
|
||||
|
||||
- Prefer **Rust-native solutions**
|
||||
- Acceptable audio stacks:
|
||||
- FFmpeg + CPAL / Rodio
|
||||
- GStreamer (if justified)
|
||||
- Implement commands such as:
|
||||
- `player_play(url)`
|
||||
- `player_stop()`
|
||||
- `player_set_volume(volume)`
|
||||
- `player_get_state()`
|
||||
- Handle:
|
||||
- buffering
|
||||
- reconnect on stream drop
|
||||
- clean shutdown
|
||||
- thread safety
|
||||
|
||||
### 3. Casting rules
|
||||
|
||||
- Do not break existing Chromecast support
|
||||
- Prefer reusing backend-controlled audio where possible (e.g., Cast via local proxy instead of sending station URL directly)
|
||||
- Do not introduce browser-based casting
|
||||
- Sidecar removal is OPTIONAL, not required now
|
||||
|
||||
---
|
||||
|
||||
## MIGRATION STRATEGY (VERY IMPORTANT)
|
||||
|
||||
You must:
|
||||
|
||||
- Work in **small, safe steps**
|
||||
- Clearly explain what files change and why
|
||||
- Never delete working functionality without replacement
|
||||
- Prefer additive refactors over destructive ones
|
||||
|
||||
Each response should:
|
||||
|
||||
1. Explain intent
|
||||
2. Show concrete code
|
||||
3. State which file is modified
|
||||
4. Preserve compatibility
|
||||
|
||||
---
|
||||
|
||||
## WHAT YOU SHOULD PRODUCE
|
||||
|
||||
You may generate:
|
||||
|
||||
- Rust code (Tauri commands, audio engine)
|
||||
- JS changes (invoke-based playback)
|
||||
- Architecture explanations
|
||||
- Migration steps
|
||||
- TODO lists
|
||||
- Warnings about pitfalls
|
||||
|
||||
You MUST NOT:
|
||||
|
||||
- Suggest Electron or Flutter
|
||||
- Suggest full rewrites
|
||||
- Ignore existing sidecar or station model
|
||||
- Break the current UX
|
||||
|
||||
---
|
||||
|
||||
## ENGINEERING PHILOSOPHY
|
||||
|
||||
This app should evolve into:
|
||||
|
||||
> “A native audio engine with a web UI shell”
|
||||
|
||||
The WebView is a **control surface**, not a media engine.
|
||||
|
||||
---
|
||||
|
||||
## COMMUNICATION STYLE
|
||||
|
||||
- Be precise
|
||||
- Be pragmatic
|
||||
- Be production-oriented
|
||||
- Prefer correctness over novelty
|
||||
- Assume this is a real app with users
|
||||
|
||||
---
|
||||
|
||||
## FIRST TASK WHEN STARTING
|
||||
|
||||
Begin by:
|
||||
|
||||
1. Identifying all HTML5 Audio usage
|
||||
2. Proposing the native audio engine design
|
||||
3. Defining the minimal command interface
|
||||
4. Planning the replacement step-by-step
|
||||
|
||||
Do NOT write all code at once.
|
||||
47
.github/FFMPEG_GUIDE.md
vendored
Normal file
47
.github/FFMPEG_GUIDE.md
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# FFmpeg CI Guide
|
||||
|
||||
This file describes how to provide a vetted FFmpeg build to the CI workflow and how the workflow expects archive layouts.
|
||||
|
||||
## Secrets (recommended)
|
||||
- `FFMPEG_URL` — primary URL the workflow will download. Use a stable URL to a signed/hosted FFmpeg build.
|
||||
- `FFMPEG_URL_LINUX` — optional override for Linux runners.
|
||||
- `FFMPEG_URL_WINDOWS` — optional override for Windows runners.
|
||||
- `FFMPEG_URL_MACOS` — optional override for macOS runners.
|
||||
|
||||
If per-OS secrets are present, they take precedence over `FFMPEG_URL`.
|
||||
|
||||
## Recommended FFmpeg sources
|
||||
- Use official static builds from a trusted provider (example):
|
||||
- Windows (ffmpeg.exe): https://www.gyan.dev/ffmpeg/builds/
|
||||
- Linux (static): https://johnvansickle.com/ffmpeg/
|
||||
- macOS (static): https://evermeet.cx/ffmpeg/
|
||||
|
||||
Prefer hosting a copy in your own artifact store (S3, GitHub Releases) so you control the binary used in CI.
|
||||
|
||||
## Expected archive layouts
|
||||
The workflow will attempt to extract common archive formats. Recommended layouts:
|
||||
|
||||
- Zip containing `ffmpeg.exe` at the archive root
|
||||
- Example: `ffmpeg-2025-01-01.zip` -> `ffmpeg.exe` (root)
|
||||
|
||||
- Tar.gz or tar.xz containing an `ffmpeg` binary at the archive root or inside a single top-level folder
|
||||
- Example: `ffmpeg-2025/ffmpeg` or `ffmpeg`
|
||||
|
||||
- Raw binary: a direct link to the `ffmpeg` executable is also supported (the workflow will make it executable).
|
||||
|
||||
If your archive nests the binary deep inside several folders, consider publishing a trimmed archive that places `ffmpeg` at the root for easier CI extraction.
|
||||
|
||||
## Verifying locally
|
||||
To test the workflow steps locally, download your chosen archive and ensure running the binary prints version information:
|
||||
|
||||
```bash
|
||||
# on Linux/macOS
|
||||
./ffmpeg -version
|
||||
|
||||
# on Windows (PowerShell)
|
||||
.\ffmpeg.exe -version
|
||||
```
|
||||
|
||||
## Notes for maintainers
|
||||
- If you need the workflow to handle a custom archive layout, I can update the extraction step (`.github/workflows/ffmpeg-preflight.yml`) to locate the binary path inside the archive and move it to `src-tauri/resources/ffmpeg(.exe)`.
|
||||
- After adding secrets, open a PR to trigger the workflow and verify the `FFmpeg preflight OK` message in the CI logs.
|
||||
129
.github/workflows/ffmpeg-preflight.yml
vendored
Normal file
129
.github/workflows/ffmpeg-preflight.yml
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
name: FFmpeg Preflight and Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
|
||||
env:
|
||||
# Provide a fallback URL via repository secret `FFMPEG_URL_{OS}` or `FFMPEG_URL`.
|
||||
FFMPEG_URL: ${{ secrets.FFMPEG_URL }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/gh-actions-rs@stable
|
||||
|
||||
- name: Determine OS-specific ffmpeg URL
|
||||
id: ffmpeg-url
|
||||
shell: bash
|
||||
run: |
|
||||
echo "RUNNER_OS=${RUNNER_OS}"
|
||||
if [[ "${RUNNER_OS}" == "Windows" ]]; then
|
||||
echo "url=${{ secrets.FFMPEG_URL_WINDOWS || secrets.FFMPEG_URL }}" >> $GITHUB_OUTPUT
|
||||
elif [[ "${RUNNER_OS}" == "macOS" ]]; then
|
||||
echo "url=${{ secrets.FFMPEG_URL_MACOS || secrets.FFMPEG_URL }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "url=${{ secrets.FFMPEG_URL_LINUX || secrets.FFMPEG_URL }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create resources dir
|
||||
run: mkdir -p src-tauri/resources
|
||||
|
||||
- name: Download and install FFmpeg into resources
|
||||
if: steps.ffmpeg-url.outputs.url != ''
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
URL="${{ steps.ffmpeg-url.outputs.url }}"
|
||||
echo "Downloading ffmpeg from: $URL"
|
||||
FNAME="${RUNNER_TEMP}/ffmpeg_bundle"
|
||||
if [[ "${RUNNER_OS}" == "Windows" ]]; then
|
||||
powershell -Command "(New-Object Net.WebClient).DownloadFile('$URL', '$FNAME.zip')"
|
||||
powershell -Command "Expand-Archive -Path '$FNAME.zip' -DestinationPath '${{ github.workspace }}\\src-tauri\\resources'"
|
||||
else
|
||||
curl -sL "$URL" -o "$FNAME"
|
||||
# Attempt to extract common archive formats
|
||||
if file "$FNAME" | grep -q 'Zip archive'; then
|
||||
unzip -q "$FNAME" -d src-tauri/resources
|
||||
elif file "$FNAME" | grep -q 'gzip compressed data'; then
|
||||
tar -xzf "$FNAME" -C src-tauri/resources
|
||||
elif file "$FNAME" | grep -q 'XZ compressed'; then
|
||||
tar -xJf "$FNAME" -C src-tauri/resources
|
||||
else
|
||||
# Assume raw binary
|
||||
mv "$FNAME" src-tauri/resources/ffmpeg
|
||||
chmod +x src-tauri/resources/ffmpeg
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: List resources
|
||||
run: ls -la src-tauri/resources || true
|
||||
|
||||
- name: Locate ffmpeg binary (Linux/macOS)
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Try to find an ffmpeg executable anywhere under resources
|
||||
BINPATH=$(find src-tauri/resources -type f -iname ffmpeg -print -quit || true)
|
||||
if [ -z "$BINPATH" ]; then
|
||||
BINPATH=$(find src-tauri/resources -type f -iname 'ffmpeg*' -print -quit || true)
|
||||
fi
|
||||
if [ -n "$BINPATH" ]; then
|
||||
echo "Found ffmpeg at $BINPATH"
|
||||
cp "$BINPATH" src-tauri/resources/ffmpeg
|
||||
chmod +x src-tauri/resources/ffmpeg
|
||||
else
|
||||
echo "ffmpeg binary not found in resources"
|
||||
ls -R src-tauri/resources || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Locate ffmpeg binary (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$found = Get-ChildItem -Path src-tauri/resources -Recurse -Filter ffmpeg.exe -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $found) {
|
||||
$found = Get-ChildItem -Path src-tauri/resources -Recurse -Filter '*ffmpeg*' -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
}
|
||||
if ($found) {
|
||||
Write-Host "Found ffmpeg at $($found.FullName)"
|
||||
Copy-Item $found.FullName -Destination 'src-tauri\resources\ffmpeg.exe' -Force
|
||||
} else {
|
||||
Write-Host "ffmpeg not found in src-tauri/resources"
|
||||
Get-ChildItem src-tauri\resources -Recurse | Format-List
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Install npm deps
|
||||
run: npm ci
|
||||
|
||||
- name: Copy project FFmpeg helpers
|
||||
run: node tools/copy-ffmpeg.js || true
|
||||
|
||||
- name: Build Rust and run ffmpeg preflight check
|
||||
working-directory: src-tauri
|
||||
run: |
|
||||
set -e
|
||||
cargo build --release
|
||||
cargo run --release --bin check_ffmpeg
|
||||
|
||||
- name: Optional frontend build
|
||||
run: npm run build --if-present || true
|
||||
34
.gitignore
vendored
34
.gitignore
vendored
@@ -1,3 +1,37 @@
|
||||
# Node
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
package-lock.json
|
||||
|
||||
# Tauri / Rust
|
||||
/target/
|
||||
/src-tauri/binaries/
|
||||
/src-tauri/target/
|
||||
|
||||
# Local build artifacts
|
||||
/dist/
|
||||
/build/
|
||||
|
||||
# FFmpeg / downloaded binaries
|
||||
/ffmpeg/bin/
|
||||
|
||||
# Editor / OS files
|
||||
.vscode/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs and temp
|
||||
*.log
|
||||
*.tmp
|
||||
|
||||
# Generated by tools
|
||||
/tools/*.cache
|
||||
|
||||
# Misc
|
||||
*.tgz
|
||||
.env
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
83
README.md
83
README.md
@@ -1,4 +1,4 @@
|
||||
# RadioCast
|
||||
# RadioPlayer
|
||||
|
||||
A lightweight, cross-platform radio player built with Tauri and Vanilla JavaScript. Features local playback and Google Cast integration.
|
||||
|
||||
@@ -15,7 +15,7 @@ Before you begin, ensure you have the following installed on your machine:
|
||||
1. **Clone the repository**:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd RadioCast
|
||||
cd RadioPlayer
|
||||
```
|
||||
|
||||
2. **Install dependencies**:
|
||||
@@ -36,7 +36,13 @@ Before you begin, ensure you have the following installed on your machine:
|
||||
To start the application in development mode (with hot-reloading for frontend changes):
|
||||
|
||||
```bash
|
||||
npm run tauri dev
|
||||
npm run dev
|
||||
```
|
||||
|
||||
If you want FFmpeg to be bundled into `src-tauri/resources/` for local/native playback during dev, use:
|
||||
|
||||
```bash
|
||||
npm run dev:native
|
||||
```
|
||||
|
||||
This command will:
|
||||
@@ -50,7 +56,7 @@ To create an optimized, standalone executable for your operating system:
|
||||
|
||||
1. **Run the build command**:
|
||||
```bash
|
||||
npm run tauri build
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. **Locate the artifacts**:
|
||||
@@ -67,7 +73,9 @@ To create an optimized, standalone executable for your operating system:
|
||||
* `styles.css`: Application styling.
|
||||
* `stations.json`: Configuration file for available radio streams.
|
||||
* **`src-tauri/`**: Rust backend code.
|
||||
* `src/main.rs`: The entry point for the Rust process. Handles Google Cast discovery and playback logic.
|
||||
* `src/lib.rs`: Tauri command layer (native player commands, Cast commands, utility HTTP helpers).
|
||||
* `src/player.rs`: Native audio engine (FFmpeg decode → PCM ring buffer → CPAL output).
|
||||
* `src/main.rs`: Rust entry point (wires the Tauri app; most command logic lives in `lib.rs`).
|
||||
* `tauri.conf.json`: Configuration for the Tauri app (window size, permissions, package info).
|
||||
|
||||
## Customization
|
||||
@@ -103,6 +111,71 @@ To change the default window size, edit `src-tauri/tauri.conf.json`:
|
||||
* **WebView2 Error (Windows)**: If the app doesn't start on Windows, ensure the [Microsoft Edge WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/) is installed.
|
||||
* **Build Failures**: Try running `cargo update` inside the `src-tauri` folder to update Rust dependencies.
|
||||
|
||||
## FFmpeg (Optional) for Native Playback
|
||||
|
||||
Local/native playback uses an external **FFmpeg** binary to decode radio streams.
|
||||
|
||||
### How the app finds FFmpeg
|
||||
|
||||
At runtime it searches in this order:
|
||||
|
||||
1. `RADIOPLAYER_FFMPEG` environment variable (absolute or relative path)
|
||||
2. Next to the application executable (Windows: `ffmpeg.exe`, macOS/Linux: `ffmpeg`)
|
||||
3. Common bundle resource folders relative to the executable:
|
||||
- `resources/ffmpeg(.exe)`
|
||||
- `Resources/ffmpeg(.exe)`
|
||||
- `../resources/ffmpeg(.exe)`
|
||||
- `../Resources/ffmpeg(.exe)`
|
||||
4. Your system `PATH`
|
||||
|
||||
### Optional: download FFmpeg automatically (Windows)
|
||||
|
||||
This is **opt-in** (it is not run automatically during build/run). It downloads a prebuilt FFmpeg zip and extracts `ffmpeg.exe` into `tools/ffmpeg/bin/ffmpeg.exe`.
|
||||
|
||||
```bash
|
||||
npm run ffmpeg:download
|
||||
```
|
||||
|
||||
Then run `npm run dev:native` (or `npm run build`) to copy FFmpeg into `src-tauri/resources/` for bundling.
|
||||
|
||||
## License
|
||||
|
||||
[Add License Information Here]
|
||||
|
||||
|
||||
## Release v0.2
|
||||
|
||||
Public beta (v0.2) — updates since v0.1:
|
||||
|
||||
- **Android build support:** Project includes Android build scripts and Gradle wrappers. See [scripts/build-android.sh](scripts/build-android.sh) and [build-android.ps1](build-android.ps1). Prebuilt native helper binaries are available in `src-tauri/binaries/` for convenience.
|
||||
- **Web receiver & webapp:** The `receiver/` folder contains a Custom CAF Receiver UI (HTML/CSS/JS) and the `webapp/` folder provides a standalone web distribution for hosting the app in browsers or PWAs.
|
||||
- **Sidecar improvements:** `sidecar/index.js` now retries launches when devices return `NOT_ALLOWED` by attempting to stop existing sessions before retrying. Check sidecar logs for `Launch NOT_ALLOWED` messages and retry attempts.
|
||||
- **LIVE stream:** The app continues to support the LIVE stream `https://live.radio1.si/Radio1MB` (contentType: `audio/mpeg`, streamType: `LIVE`).
|
||||
|
||||
Included receiver files:
|
||||
|
||||
- `receiver/index.html`
|
||||
- `receiver/receiver.js` (CAF Receiver initialization + LOAD interceptor for LIVE metadata)
|
||||
- `receiver/styles.css`
|
||||
- `receiver/assets/logo.svg`
|
||||
|
||||
Quick testing notes
|
||||
|
||||
- The receiver must be served over HTTPS for Cast devices to load it. For quick local testing you can use `mkcert` + a static HTTPS server:
|
||||
|
||||
```bash
|
||||
# create local certs
|
||||
mkcert -install
|
||||
mkcert localhost
|
||||
|
||||
# serve the receiver folder over HTTPS
|
||||
npx http-server receiver -p 8443 -S -C localhost.pem -K localhost-key.pem
|
||||
```
|
||||
|
||||
- Use the Default Media Receiver App ID while developing, or register a Custom Receiver App in the Cast Developer Console and point its URL to your hosted `index.html` for production.
|
||||
|
||||
Sidecar / troubleshoot
|
||||
|
||||
- If a Cast launch fails with `NOT_ALLOWED`, the sidecar will attempt to stop any existing sessions on the device and retry the launch (best-effort). Check sidecar logs for `Launch NOT_ALLOWED` and subsequent retry attempts.
|
||||
- Note: the sidecar uses `castv2-client` (not the official Google sender SDK). Group/stereo behavior may vary across device types — for full sender capabilities consider adding an official sender implementation.
|
||||
|
||||
|
||||
343
TECHNICAL_DOCUMENTATION.md
Normal file
343
TECHNICAL_DOCUMENTATION.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# RadioPlayer — Technical Documentation (Tauri + Desktop)
|
||||
|
||||
This document describes the desktop (Tauri) application architecture, build pipeline, backend commands, and how the UI maps to that backend.
|
||||
|
||||
## High-level architecture
|
||||
|
||||
- **Frontend (WebView)**: Vanilla HTML/CSS/JS in [src/index.html](src/index.html), [src/main.js](src/main.js), [src/styles.css](src/styles.css)
|
||||
- **Tauri host (Rust)**: Command layer + device discovery in [src-tauri/src/lib.rs](src-tauri/src/lib.rs)
|
||||
- **Native audio engine (Rust)**: FFmpeg decode + CPAL output in [src-tauri/src/player.rs](src-tauri/src/player.rs)
|
||||
- **Cast sidecar (Node executable)**: Google Cast control via `castv2-client` in [sidecar/index.js](sidecar/index.js)
|
||||
- **Packaging utilities**:
|
||||
- Sidecar binary copy/rename step: [tools/copy-binaries.js](tools/copy-binaries.js)
|
||||
- Windows EXE icon patch: [tools/post-build-rcedit.js](tools/post-build-rcedit.js)
|
||||
- Optional FFmpeg bundling helper: [tools/copy-ffmpeg.js](tools/copy-ffmpeg.js) (see [tools/ffmpeg/README.md](tools/ffmpeg/README.md))
|
||||
|
||||
Data flow:
|
||||
|
||||
1. UI actions call JS functions in `main.js`.
|
||||
2. JS calls Tauri commands via `window.__TAURI__.core.invoke()` (for both local playback and casting).
|
||||
3. In **Local mode**, Rust spawns FFmpeg and plays decoded PCM via CPAL.
|
||||
4. In **Cast mode**, the Rust backend discovers Cast devices via mDNS and stores `{ deviceName -> ip }`.
|
||||
5. On `cast_play/stop/volume`, Rust spawns (or reuses) a **sidecar process**, then sends newline-delimited JSON commands to the sidecar stdin.
|
||||
|
||||
## Running and building
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js (project uses ESM at the root; see [package.json](package.json))
|
||||
- Rust toolchain (via rustup)
|
||||
- Platform build tools (Windows: Visual Studio C++ Build Tools)
|
||||
- Tauri prerequisites (WebView2 runtime on Windows)
|
||||
|
||||
### Dev
|
||||
|
||||
From repo root:
|
||||
|
||||
- `npm install`
|
||||
- `npm run dev`
|
||||
|
||||
This runs `tauri dev` (see [package.json](package.json)).
|
||||
|
||||
### Production build (Windows MSI/NSIS, etc.)
|
||||
|
||||
From repo root:
|
||||
|
||||
- `npm run build`
|
||||
|
||||
What it does (see [package.json](package.json)):
|
||||
|
||||
1. `node tools/copy-binaries.js` — ensures the expected bundled binary name exists.
|
||||
2. `tauri build` — builds the Rust host and generates platform bundles.
|
||||
3. `node tools/post-build-rcedit.js` — patches the Windows EXE icon using the locally installed `rcedit` binary.
|
||||
|
||||
Artifacts typically land under:
|
||||
|
||||
- `src-tauri/target/release/bundle/`
|
||||
|
||||
### Building the sidecar
|
||||
|
||||
The sidecar is built separately using `pkg` (see [sidecar/package.json](sidecar/package.json)):
|
||||
|
||||
- `cd sidecar`
|
||||
- `npm install`
|
||||
- `npm run build`
|
||||
|
||||
This outputs:
|
||||
|
||||
- `src-tauri/binaries/radiocast-sidecar-x86_64-pc-windows-msvc.exe`
|
||||
|
||||
## Tauri configuration
|
||||
|
||||
### App config
|
||||
|
||||
Defined in [src-tauri/tauri.conf.json](src-tauri/tauri.conf.json):
|
||||
|
||||
- **build.frontendDist**: `../src`
|
||||
- The desktop app serves the static files in `src/`.
|
||||
- **window**:
|
||||
- `width: 360`, `height: 720`, `resizable: false`
|
||||
- `decorations: false`, `transparent: true` (frameless / custom UI)
|
||||
- **security.csp**: `null` (CSP disabled)
|
||||
- **bundle.targets**: `"all"`
|
||||
- **bundle.externalBin**: includes external binaries shipped with the bundle.
|
||||
|
||||
### Capabilities and permissions
|
||||
|
||||
Defined in [src-tauri/capabilities/default.json](src-tauri/capabilities/default.json):
|
||||
|
||||
- `core:default`
|
||||
- `core:window:allow-close` (allows JS to call window close)
|
||||
- `opener:default`
|
||||
- `shell:default` (required for spawning the sidecar)
|
||||
|
||||
## Rust backend (Tauri commands)
|
||||
|
||||
All commands are in [src-tauri/src/lib.rs](src-tauri/src/lib.rs) and registered via `invoke_handler`.
|
||||
|
||||
### Shared state
|
||||
|
||||
- `AppState.known_devices: HashMap<String, String>`
|
||||
- maps **device name** → **IP string**
|
||||
- `SidecarState.child: Option<CommandChild>`
|
||||
- stores a single long-lived sidecar child process
|
||||
|
||||
### mDNS discovery
|
||||
|
||||
In `.setup()` the backend spawns a thread that browses:
|
||||
|
||||
- `_googlecast._tcp.local.`
|
||||
|
||||
When a device is resolved:
|
||||
|
||||
- Name is taken from the `fn` TXT record if present, otherwise `fullname`.
|
||||
- First IPv4 address is preferred.
|
||||
- New devices are inserted into `known_devices` and logged.
|
||||
|
||||
### Commands
|
||||
|
||||
### Native player commands (local playback)
|
||||
|
||||
Local playback is handled by the Rust engine in [src-tauri/src/player.rs](src-tauri/src/player.rs). The UI controls it using these commands:
|
||||
|
||||
#### `player_play(url: String) -> Result<(), String>`
|
||||
|
||||
- Starts native playback of the provided stream URL.
|
||||
- Internally spawns FFmpeg to decode into `s16le` PCM and feeds a ring buffer consumed by a CPAL output stream.
|
||||
- Reports `buffering` → `playing` based on buffer fill/underrun.
|
||||
|
||||
#### `player_stop() -> Result<(), String>`
|
||||
|
||||
- Stops the native pipeline and updates state.
|
||||
|
||||
#### `player_set_volume(volume: f32) -> Result<(), String>`
|
||||
|
||||
- Sets volume in range `[0, 1]`.
|
||||
|
||||
#### `player_get_state() -> Result<PlayerState, String>`
|
||||
|
||||
- Returns `{ status, url, volume, error }`.
|
||||
- Used by the UI to keep status text and play/stop button in sync.
|
||||
|
||||
#### `list_cast_devices() -> Result<Vec<String>, String>`
|
||||
|
||||
- Returns the sorted list of discovered Cast device names.
|
||||
- Used by the UI when opening the Cast picker overlay.
|
||||
|
||||
#### `cast_play(device_name: String, url: String) -> Result<(), String>`
|
||||
|
||||
- Resolves `device_name` → `ip` from `known_devices`.
|
||||
- Spawns the sidecar if it doesn’t exist yet:
|
||||
- `app.shell().sidecar("radiocast-sidecar")`
|
||||
- Sidecar stdout/stderr are forwarded to the Rust process logs.
|
||||
- Writes a JSON line to the sidecar stdin:
|
||||
|
||||
```json
|
||||
{ "command": "play", "args": { "ip": "<ip>", "url": "<streamUrl>" } }
|
||||
```
|
||||
|
||||
#### `cast_stop(device_name: String) -> Result<(), String>`
|
||||
|
||||
- If the sidecar process exists, writes:
|
||||
|
||||
```json
|
||||
{ "command": "stop", "args": {} }
|
||||
```
|
||||
|
||||
#### `cast_set_volume(device_name: String, volume: f32) -> Result<(), String>`
|
||||
|
||||
- If the sidecar process exists, writes:
|
||||
|
||||
```json
|
||||
{ "command": "volume", "args": { "level": 0.0 } }
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `volume` is passed from the UI in the range `[0, 1]`.
|
||||
|
||||
#### `fetch_url(url: String) -> Result<String, String>`
|
||||
|
||||
- Performs a server-side HTTP GET using `reqwest`.
|
||||
- Returns response body as text.
|
||||
- Used by the UI to bypass browser CORS limitations when calling 3rd-party endpoints.
|
||||
|
||||
## Sidecar protocol and behavior
|
||||
|
||||
Implementation: [sidecar/index.js](sidecar/index.js)
|
||||
|
||||
### Input protocol (stdin)
|
||||
|
||||
The sidecar reads **newline-delimited JSON objects**:
|
||||
|
||||
- `{"command":"play","args":{"ip":"...","url":"..."}}`
|
||||
- `{"command":"stop","args":{}}`
|
||||
- `{"command":"volume","args":{"level":0.5}}`
|
||||
|
||||
### Output protocol (stdout/stderr)
|
||||
|
||||
Logs are JSON objects:
|
||||
|
||||
- `{"type":"log","message":"..."}` to stdout
|
||||
- `{"type":"error","message":"..."}` to stderr
|
||||
|
||||
### Cast launch logic
|
||||
|
||||
- Connects to the device IP.
|
||||
- Reads existing sessions via `getSessions()`.
|
||||
- If Default Media Receiver (`appId === "CC1AD845"`) exists, tries to join.
|
||||
- If other sessions exist, attempts to stop them to avoid `NOT_ALLOWED`.
|
||||
- On `NOT_ALLOWED` launch, retries once after stopping sessions (best-effort).
|
||||
|
||||
## Frontend behavior
|
||||
|
||||
### Station data model
|
||||
|
||||
Stations are loaded from [src/stations.json](src/stations.json) and normalized in [src/main.js](src/main.js) into:
|
||||
|
||||
```js
|
||||
{ id, name, url, logo, enabled, raw }
|
||||
```
|
||||
|
||||
Normalization rules (important for `stations.json` format compatibility):
|
||||
|
||||
- `name`: `title || id || name || "Unknown"`
|
||||
- `url`: `liveAudio || liveVideo || liveStream || url || ""`
|
||||
- `logo`: `logo || poster || ""`
|
||||
- Stations with `enabled === false` or without a URL are filtered out.
|
||||
|
||||
User-defined stations are stored in `localStorage` under `userStations` and appended after file stations.
|
||||
|
||||
The last selected station is stored under `localStorage.lastStationId`.
|
||||
|
||||
### Playback modes
|
||||
|
||||
State is tracked in JS:
|
||||
|
||||
- `currentMode`: `"local"` or `"cast"`
|
||||
- `currentCastDevice`: string or `null`
|
||||
- `isPlaying`: boolean
|
||||
|
||||
#### Local mode
|
||||
|
||||
- Uses backend invokes: `player_play`, `player_stop`, `player_set_volume`.
|
||||
- The UI polls `player_get_state` to reflect `buffering/playing/stopped/error`.
|
||||
|
||||
#### Cast mode
|
||||
|
||||
- Uses backend invokes: `cast_play`, `cast_stop`, `cast_set_volume`.
|
||||
|
||||
### Current song (“Now Playing”) polling
|
||||
|
||||
- For the currently selected station only, the app polls a station endpoint every 10s.
|
||||
- It prefers `raw.currentSong`, otherwise uses `raw.lastSongs`.
|
||||
- Remote URLs are fetched via the Tauri backend `fetch_url` to bypass CORS.
|
||||
- If the provider returns timing fields (`playTimeStart*`, `playTimeLength*`), the UI schedules a single refresh near song end.
|
||||
|
||||
### Overlays
|
||||
|
||||
The element [src/index.html](src/index.html) `#cast-overlay` is reused for two different overlays:
|
||||
|
||||
- Cast device picker (`openCastOverlay()`)
|
||||
- Station grid chooser (`openStationsOverlay()`)
|
||||
|
||||
The content is switched by:
|
||||
|
||||
- Toggling the `stations-grid` class on `#device-list`
|
||||
- Replacing `#device-list` contents dynamically
|
||||
|
||||
## UI controls (button-by-button)
|
||||
|
||||
All UI IDs below are in [src/index.html](src/index.html) and are wired in [src/main.js](src/main.js).
|
||||
|
||||
### Window / header
|
||||
|
||||
- `#close-btn`
|
||||
- Calls `getCurrentWindow().close()` (requires `core:window:allow-close`).
|
||||
- `#cast-toggle-btn`
|
||||
- Opens the Cast overlay and lists discovered devices (`invoke('list_cast_devices')`).
|
||||
- `#edit-stations-btn`
|
||||
- Opens the Stations Editor overlay (user stations stored in `localStorage.userStations`).
|
||||
|
||||
Note:
|
||||
|
||||
- `#cast-toggle-btn` and `#edit-stations-btn` appear twice in the HTML header. Duplicate IDs are invalid HTML and only the first element returned by `getElementById()` will be wired.
|
||||
|
||||
### Coverflow (station carousel inside artwork)
|
||||
|
||||
- `#artwork-prev`
|
||||
- Selects previous station via `setStationByIndex()`.
|
||||
- `#artwork-next`
|
||||
- Selects next station via `setStationByIndex()`.
|
||||
- `#artwork-coverflow` (drag/wheel area)
|
||||
- Pointer drag changes station when movement exceeds a threshold.
|
||||
- Wheel scroll changes station with a short debounce.
|
||||
- Coverflow card click
|
||||
- Selects that station.
|
||||
- Coverflow card double-click (on the selected station)
|
||||
- Opens the station grid overlay.
|
||||
|
||||
### Transport controls
|
||||
|
||||
- `#play-btn`
|
||||
- Toggles play/stop (`togglePlay()`):
|
||||
- Local mode: `invoke('player_play')` / `invoke('player_stop')`.
|
||||
- Cast mode: `invoke('cast_play')` / `invoke('cast_stop')`.
|
||||
- `#prev-btn`
|
||||
- Previous station (`playPrev()` → `setStationByIndex()`).
|
||||
- `#next-btn`
|
||||
- Next station (`playNext()` → `setStationByIndex()`).
|
||||
|
||||
### Volume
|
||||
|
||||
- `#volume-slider`
|
||||
- Local: `invoke('player_set_volume')`.
|
||||
- Cast: `invoke('cast_set_volume')`.
|
||||
- Persists `localStorage.volume`.
|
||||
- `#mute-btn`
|
||||
- Present in the UI but currently not wired to a handler in `main.js`.
|
||||
|
||||
### Cast overlay
|
||||
|
||||
- `#close-overlay`
|
||||
- Closes the overlay (`closeCastOverlay()`).
|
||||
|
||||
### Stations editor overlay
|
||||
|
||||
- `#editor-close-btn`
|
||||
- Closes the editor overlay.
|
||||
- `#add-station-form` submit
|
||||
- Adds/updates a station in `localStorage.userStations`.
|
||||
- Triggers a full station reload (`loadStations()`).
|
||||
|
||||
## Service worker / PWA pieces
|
||||
|
||||
- Service worker file: [src/sw.js](src/sw.js)
|
||||
- Caches core app assets for offline-ish behavior.
|
||||
- Web manifest: [src/manifest.json](src/manifest.json)
|
||||
- Name/icons/theme for installable PWA (primarily relevant for the web build; harmless in Tauri).
|
||||
|
||||
## Known sharp edges / notes
|
||||
|
||||
- **Duplicate IDs in HTML header**: only one of the duplicates will receive JS event listeners.
|
||||
- **Sidecar bundling name**: the build pipeline copies `radiocast-sidecar-...` to `RadioPlayer-...` (see [tools/copy-binaries.js](tools/copy-binaries.js)); ensure the bundled binary name matches what `shell.sidecar("radiocast-sidecar")` expects for your target.
|
||||
BIN
app-icon.png
Normal file
BIN
app-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 290 KiB |
18
cast-receiver/README.md
Normal file
18
cast-receiver/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Radio Player - Custom Cast Receiver
|
||||
|
||||
This folder contains a minimal Google Cast Web Receiver that displays a purple gradient background, station artwork, title and subtitle. It accepts `customData` hints sent from the sender (your app) for `backgroundImage`, `backgroundGradient` and `appName`.
|
||||
|
||||
Hosting requirements
|
||||
- The receiver must be served over HTTPS and be publicly accessible.
|
||||
- Recommended: host under GitHub Pages (`gh-pages` branch or `/docs` folder) or any static host (Netlify, Vercel, S3 + CloudFront).
|
||||
|
||||
Registering with Google Cast Console
|
||||
1. Go to the Cast SDK Developer Console and create a new Application.
|
||||
2. Choose "Custom Receiver" and provide the public HTTPS URL to `index.html` (e.g. `https://example.com/cast-receiver/index.html`).
|
||||
3. Note the generated Application ID.
|
||||
|
||||
Sender changes
|
||||
- After obtaining the Application ID, update your sender (sidecar) to launch that app ID instead of the DefaultMediaReceiver. The sidecar already supports passing `metadata.appId` when launching.
|
||||
|
||||
Testing locally
|
||||
- You can serve this folder locally during development, but Chromecast devices require public HTTPS endpoints to use a registered app.
|
||||
23
cast-receiver/index.html
Normal file
23
cast-receiver/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Radio Player Receiver</title>
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="bg" class="bg"></div>
|
||||
<div id="app" class="app">
|
||||
<div class="artwork"><img id="art" alt="Artwork"></div>
|
||||
<div class="meta">
|
||||
<div id="appName" class="app-name">Radio Player</div>
|
||||
<h1 id="title">Radio Player</h1>
|
||||
<h2 id="subtitle"></h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
|
||||
<script src="receiver.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
50
cast-receiver/receiver.js
Normal file
50
cast-receiver/receiver.js
Normal file
@@ -0,0 +1,50 @@
|
||||
// Minimal CAF receiver that applies customData theming and shows media metadata.
|
||||
const context = cast.framework.CastReceiverContext.getInstance();
|
||||
const playerManager = context.getPlayerManager();
|
||||
|
||||
function applyBranding(customData, metadata) {
|
||||
try {
|
||||
const bgEl = document.getElementById('bg');
|
||||
const art = document.getElementById('art');
|
||||
const title = document.getElementById('title');
|
||||
const subtitle = document.getElementById('subtitle');
|
||||
const appName = document.getElementById('appName');
|
||||
|
||||
if (customData) {
|
||||
if (customData.backgroundImage) {
|
||||
bgEl.style.backgroundImage = `url(${customData.backgroundImage})`;
|
||||
bgEl.style.backgroundSize = 'cover';
|
||||
bgEl.style.backgroundPosition = 'center';
|
||||
} else if (customData.backgroundGradient) {
|
||||
bgEl.style.background = customData.backgroundGradient;
|
||||
}
|
||||
if (customData.appName) appName.textContent = customData.appName;
|
||||
}
|
||||
|
||||
if (metadata) {
|
||||
if (metadata.title) title.textContent = metadata.title;
|
||||
const sub = metadata.subtitle || metadata.artist || '';
|
||||
subtitle.textContent = sub;
|
||||
if (metadata.images && metadata.images.length) {
|
||||
art.src = metadata.images[0].url || '';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// swallow UI errors
|
||||
console.warn('Branding apply failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
playerManager.setMessageInterceptor(cast.framework.messages.MessageType.LOAD, (request) => {
|
||||
const media = request.media || {};
|
||||
const customData = media.customData || {};
|
||||
applyBranding(customData, media.metadata || {});
|
||||
return request;
|
||||
});
|
||||
|
||||
playerManager.addEventListener(cast.framework.events.EventType.MEDIA_STATUS, () => {
|
||||
const media = playerManager.getMediaInformation();
|
||||
if (media) applyBranding(media.customData || {}, media.metadata || {});
|
||||
});
|
||||
|
||||
context.start();
|
||||
11
cast-receiver/styles.css
Normal file
11
cast-receiver/styles.css
Normal file
@@ -0,0 +1,11 @@
|
||||
:root{--primary:#6a0dad;--accent:#b36cf3}
|
||||
html,body{height:100%;margin:0;font-family:Inter,system-ui,Arial,Helvetica,sans-serif}
|
||||
body{background:linear-gradient(135deg,var(--primary),var(--accent));color:#fff}
|
||||
.bg{position:fixed;inset:0;background-size:cover;background-position:center;filter:blur(10px) saturate(120%);opacity:0.9}
|
||||
.app{position:relative;z-index:2;display:flex;align-items:center;gap:24px;padding:48px}
|
||||
.artwork{width:320px;height:320px;flex:0 0 320px;background:rgba(255,255,255,0.06);display:flex;align-items:center;justify-content:center;border-radius:8px;overflow:hidden}
|
||||
.artwork img{width:100%;height:100%;object-fit:cover}
|
||||
.meta{display:flex;flex-direction:column}
|
||||
.app-name{font-weight:600;opacity:0.9}
|
||||
h1{margin:6px 0 0 0;font-size:28px}
|
||||
h2{margin:6px 0 0 0;font-size:18px;opacity:0.9}
|
||||
@@ -1,8 +1,8 @@
|
||||
# Radio1 Player – Glassmorphism UI Redesign (Tauri + HTML)
|
||||
# Radio Player – Glassmorphism UI Redesign (Tauri + HTML)
|
||||
|
||||
## Objective
|
||||
|
||||
Redesign the **Radio1 Player** UI to match a **modern glassmorphism style** inspired by high-end music player apps.
|
||||
Redesign the **Radio Player** UI to match a **modern glassmorphism style** inspired by high-end music player apps.
|
||||
|
||||
The app is built with:
|
||||
|
||||
@@ -42,7 +42,7 @@ Single centered player card:
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ Radio1 Player │
|
||||
│ Radio Player │
|
||||
│ ● Playing / Ready │
|
||||
│ │
|
||||
│ [ Station Artwork / Logo ] │
|
||||
@@ -64,7 +64,7 @@ Single centered player card:
|
||||
|
||||
### Header
|
||||
|
||||
* Title: `Radio1 Player`
|
||||
* Title: `RadioPlayer`
|
||||
* Status indicator:
|
||||
|
||||
* `● Ready`
|
||||
|
||||
1558
package-lock.json
generated
1558
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@@ -1,14 +1,22 @@
|
||||
{
|
||||
"name": "radio-tauri",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
"build": "tauri build",
|
||||
"tauri": "tauri"
|
||||
"build:sidecar": "npm --prefix sidecar install && npm --prefix sidecar run build",
|
||||
"dev": "npm run build:sidecar && node tools/copy-binaries.js && node tools/copy-ffmpeg.js && tauri dev",
|
||||
"dev:native": "node tools/copy-binaries.js && node tools/copy-ffmpeg.js && tauri dev",
|
||||
"ffmpeg:download": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/download-ffmpeg.ps1",
|
||||
"version:sync": "node tools/sync-version.js",
|
||||
"build": "node tools/sync-version.js && node tools/copy-binaries.js && node tools/copy-ffmpeg.js && node tools/write-build-flag.js set && tauri build && node tools/post-build-rcedit.js && node tools/write-build-flag.js clear",
|
||||
"build:devlike": "node tools/sync-version.js && node tools/copy-binaries.js && node tools/copy-ffmpeg.js && node tools/write-build-flag.js set --debug && cross-env RADIO_DEBUG_DEVTOOLS=1 tauri build && node tools/post-build-rcedit.js && node tools/write-build-flag.js clear",
|
||||
"tauri": "node tools/copy-binaries.js && node tools/copy-ffmpeg.js && tauri"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2"
|
||||
"@tauri-apps/cli": "^2",
|
||||
"cross-env": "^7.0.3",
|
||||
"npx": "^3.0.0",
|
||||
"rcedit": "^1.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
71
scripts/download-ffmpeg.ps1
Normal file
71
scripts/download-ffmpeg.ps1
Normal file
@@ -0,0 +1,71 @@
|
||||
param(
|
||||
[string]$Url = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip",
|
||||
[string]$OutDir = "tools/ffmpeg/bin",
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$isWindows = $env:OS -eq 'Windows_NT'
|
||||
if (-not $isWindows) {
|
||||
Write-Host "This script is intended for Windows (ffmpeg.exe)." -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
||||
$outDirAbs = (Resolve-Path (Join-Path $repoRoot $OutDir) -ErrorAction SilentlyContinue)
|
||||
if (-not $outDirAbs) {
|
||||
$outDirAbs = Join-Path $repoRoot $OutDir
|
||||
New-Item -ItemType Directory -Force -Path $outDirAbs | Out-Null
|
||||
} else {
|
||||
$outDirAbs = $outDirAbs.Path
|
||||
}
|
||||
|
||||
$ffmpegDest = Join-Path $outDirAbs "ffmpeg.exe"
|
||||
|
||||
# If already present, do nothing.
|
||||
if (Test-Path $ffmpegDest) {
|
||||
Write-Host "FFmpeg already present: $ffmpegDest"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host "Dry run:" -ForegroundColor Cyan
|
||||
Write-Host " Would download: $Url"
|
||||
Write-Host " Would install to: $ffmpegDest"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "About to download a prebuilt FFmpeg package:" -ForegroundColor Cyan
|
||||
Write-Host " $Url"
|
||||
Write-Host "You are responsible for reviewing the FFmpeg license/compliance for your use case." -ForegroundColor Yellow
|
||||
|
||||
$tempRoot = Join-Path $env:TEMP ("radioplayer-ffmpeg-" + [guid]::NewGuid().ToString("N"))
|
||||
$zipPath = Join-Path $tempRoot "ffmpeg.zip"
|
||||
$extractDir = Join-Path $tempRoot "extract"
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $extractDir | Out-Null
|
||||
|
||||
try {
|
||||
Write-Host "Downloading..." -ForegroundColor Cyan
|
||||
Invoke-WebRequest -Uri $Url -OutFile $zipPath -UseBasicParsing
|
||||
|
||||
Write-Host "Extracting..." -ForegroundColor Cyan
|
||||
Expand-Archive -Path $zipPath -DestinationPath $extractDir -Force
|
||||
|
||||
$candidate = Get-ChildItem -Path $extractDir -Recurse -Filter "ffmpeg.exe" | Where-Object {
|
||||
$_.FullName -match "\\bin\\ffmpeg\.exe$"
|
||||
} | Select-Object -First 1
|
||||
|
||||
if (-not $candidate) {
|
||||
throw "Could not find ffmpeg.exe under extracted content. The archive layout may have changed."
|
||||
}
|
||||
|
||||
Copy-Item -Force -Path $candidate.FullName -Destination $ffmpegDest
|
||||
|
||||
Write-Host "Installed FFmpeg to: $ffmpegDest" -ForegroundColor Green
|
||||
Write-Host "Next: run 'node tools/copy-ffmpeg.js' (or 'npm run dev:native' / 'npm run build') to bundle it into src-tauri/resources/." -ForegroundColor Green
|
||||
} finally {
|
||||
try { Remove-Item -Recurse -Force -Path $tempRoot -ErrorAction SilentlyContinue } catch {}
|
||||
}
|
||||
259
sidecar/index.js
Normal file
259
sidecar/index.js
Normal file
@@ -0,0 +1,259 @@
|
||||
const { Client, DefaultMediaReceiver } = require('castv2-client');
|
||||
const readline = require('readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
terminal: false
|
||||
});
|
||||
|
||||
let activeClient = null;
|
||||
let activePlayer = null;
|
||||
|
||||
function isNotAllowedError(err) {
|
||||
if (!err) return false;
|
||||
const msg = (err.message || String(err)).toUpperCase();
|
||||
return msg.includes('NOT_ALLOWED') || msg.includes('NOT ALLOWED');
|
||||
}
|
||||
|
||||
function stopSessions(client, sessions, cb) {
|
||||
if (!client || !sessions || sessions.length === 0) return cb();
|
||||
|
||||
const remaining = sessions.slice();
|
||||
const stopNext = () => {
|
||||
const session = remaining.shift();
|
||||
if (!session) return cb();
|
||||
|
||||
try {
|
||||
client.stop(session, (err) => {
|
||||
if (err) {
|
||||
log(`Stop session failed (${session.appId || 'unknown app'}): ${err.message || String(err)}`);
|
||||
} else {
|
||||
log(`Stopped session (${session.appId || 'unknown app'})`);
|
||||
}
|
||||
// Continue regardless; best-effort.
|
||||
stopNext();
|
||||
});
|
||||
} catch (err) {
|
||||
// Some devices/library versions may throw synchronously; just log and continue.
|
||||
log(`Stop session threw (${session.appId || 'unknown app'}): ${err.message || String(err)}`);
|
||||
stopNext();
|
||||
}
|
||||
};
|
||||
|
||||
stopNext();
|
||||
}
|
||||
|
||||
function log(msg) {
|
||||
console.log(JSON.stringify({ type: 'log', message: msg }));
|
||||
}
|
||||
|
||||
function error(msg) {
|
||||
console.error(JSON.stringify({ type: 'error', message: msg }));
|
||||
}
|
||||
|
||||
rl.on('line', (line) => {
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
const { command, args } = data;
|
||||
|
||||
switch (command) {
|
||||
case 'play':
|
||||
play(args.ip, args.url, args.metadata);
|
||||
break;
|
||||
case 'stop':
|
||||
stop();
|
||||
break;
|
||||
case 'volume':
|
||||
setVolume(args.level);
|
||||
break;
|
||||
default:
|
||||
error(`Unknown command: ${command}`);
|
||||
}
|
||||
} catch (e) {
|
||||
error(`Failed to parse line: ${e.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
function play(ip, url, metadata) {
|
||||
if (activeClient) {
|
||||
try { activeClient.removeAllListeners(); } catch (e) { }
|
||||
try { activeClient.close(); } catch (e) { }
|
||||
}
|
||||
|
||||
activeClient = new Client();
|
||||
// Increase max listeners for this client instance to avoid Node warnings
|
||||
try { if (typeof activeClient.setMaxListeners === 'function') activeClient.setMaxListeners(50); } catch (e) {}
|
||||
activeClient._playMetadata = metadata || {};
|
||||
|
||||
activeClient.connect(ip, () => {
|
||||
log(`Connected to ${ip}`);
|
||||
|
||||
// First, check if DefaultMediaReceiver is already running
|
||||
activeClient.getSessions((err, sessions) => {
|
||||
if (err) return error(`GetSessions error: ${err.message}`);
|
||||
|
||||
// Log sessions for debugging (appId/sessionId if available)
|
||||
try {
|
||||
const sessInfo = sessions.map(s => ({ appId: s.appId, sessionId: s.sessionId, displayName: s.displayName }));
|
||||
log(`Sessions: ${JSON.stringify(sessInfo)}`);
|
||||
} catch (e) {
|
||||
log('Sessions: (unable to stringify)');
|
||||
}
|
||||
|
||||
// DefaultMediaReceiver App ID is CC1AD845
|
||||
const session = sessions.find(s => s.appId === 'CC1AD845');
|
||||
|
||||
if (session) {
|
||||
log('Session already running, joining...');
|
||||
activeClient.join(session, DefaultMediaReceiver, (err, player) => {
|
||||
if (err) {
|
||||
log('Join failed, attempting launch...');
|
||||
log(`Join error: ${err && err.message ? err.message : String(err)}`);
|
||||
// Join can fail if the session is stale; stop it and retry launch.
|
||||
stopSessions(activeClient, [session], () => launchPlayer(url, activeClient._playMetadata, /*didStopFirst*/ true));
|
||||
} else {
|
||||
// Clean up previous player listeners before replacing
|
||||
try { if (activePlayer && typeof activePlayer.removeAllListeners === 'function') activePlayer.removeAllListeners(); } catch (e) {}
|
||||
activePlayer = player;
|
||||
try { if (typeof activePlayer.setMaxListeners === 'function') activePlayer.setMaxListeners(50); } catch (e) {}
|
||||
loadMedia(url, activeClient._playMetadata);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Backdrop or other non-media session present: skip stopping to avoid platform sender crash, just launch.
|
||||
if (sessions.length > 0) {
|
||||
log('Non-media session detected; skipping stop and launching DefaultMediaReceiver...');
|
||||
}
|
||||
launchPlayer(url, activeClient._playMetadata, /*didStopFirst*/ false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
activeClient.on('error', (err) => {
|
||||
error(`Client error: ${err.message}`);
|
||||
try { activeClient.close(); } catch (e) { }
|
||||
activeClient = null;
|
||||
activePlayer = null;
|
||||
});
|
||||
}
|
||||
|
||||
function launchPlayer(url, metadata, didStopFirst) {
|
||||
if (!activeClient) return;
|
||||
|
||||
const launchApp = (metadata && metadata.appId) ? metadata.appId : DefaultMediaReceiver;
|
||||
activeClient.launch(launchApp, (err, player) => {
|
||||
if (err) {
|
||||
const details = `Launch error: ${err && err.message ? err.message : String(err)}${err && err.code ? ` (code: ${err.code})` : ''}`;
|
||||
// If launch fails with NOT_ALLOWED, the device may be busy with another app/session.
|
||||
// Best-effort: stop existing sessions once, then retry launch.
|
||||
if (!didStopFirst && isNotAllowedError(err)) {
|
||||
log('Launch NOT_ALLOWED; attempting to stop existing sessions and retry...');
|
||||
activeClient.getSessions((sessErr, sessions) => {
|
||||
if (sessErr) {
|
||||
error(`${details} | GetSessions error: ${sessErr.message || String(sessErr)}`);
|
||||
return;
|
||||
}
|
||||
stopSessions(activeClient, sessions, () => {
|
||||
activeClient.launch(DefaultMediaReceiver, (retryErr, retryPlayer) => {
|
||||
if (retryErr) {
|
||||
const retryDetails = `Launch retry error: ${retryErr && retryErr.message ? retryErr.message : String(retryErr)}${retryErr && retryErr.code ? ` (code: ${retryErr.code})` : ''}`;
|
||||
error(retryDetails);
|
||||
try { error(`Launch retry error full: ${JSON.stringify(retryErr)}`); } catch (e) { /* ignore */ }
|
||||
return;
|
||||
}
|
||||
try { if (activePlayer && typeof activePlayer.removeAllListeners === 'function') activePlayer.removeAllListeners(); } catch (e) {}
|
||||
activePlayer = retryPlayer;
|
||||
try { if (typeof activePlayer.setMaxListeners === 'function') activePlayer.setMaxListeners(50); } catch (e) {}
|
||||
loadMedia(url, metadata);
|
||||
});
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
error(details);
|
||||
try { error(`Launch error full: ${JSON.stringify(err)}`); } catch (e) { /* ignore */ }
|
||||
return;
|
||||
}
|
||||
try { if (activePlayer && typeof activePlayer.removeAllListeners === 'function') activePlayer.removeAllListeners(); } catch (e) {}
|
||||
activePlayer = player;
|
||||
try { if (typeof activePlayer.setMaxListeners === 'function') activePlayer.setMaxListeners(50); } catch (e) {}
|
||||
loadMedia(url, metadata);
|
||||
});
|
||||
}
|
||||
|
||||
function loadMedia(url, metadata) {
|
||||
if (!activePlayer) return;
|
||||
|
||||
const meta = metadata || {};
|
||||
// Build a richer metadata payload. Many receivers only honor specific
|
||||
// fields; we set both Music metadata and generic hints via `customData`.
|
||||
const media = {
|
||||
contentId: url,
|
||||
contentType: 'audio/mpeg',
|
||||
streamType: 'LIVE',
|
||||
metadata: {
|
||||
// Use MusicTrack metadata (common on audio receivers) but include
|
||||
// a subtitle field in case receivers surface it.
|
||||
metadataType: 3, // MusicTrackMediaMetadata
|
||||
title: meta.title || 'Radio Station',
|
||||
albumName: 'Radio Player',
|
||||
artist: meta.artist || meta.subtitle || meta.station || '',
|
||||
subtitle: meta.subtitle || '',
|
||||
images: (meta.image ? [
|
||||
{ url: meta.image },
|
||||
// also include a large hint for receivers that prefer big artwork
|
||||
{ url: meta.image, width: 1920, height: 1080 }
|
||||
] : [])
|
||||
},
|
||||
// Many receivers ignore `customData`, but some Styled receivers will
|
||||
// use it. Include background and theming hints here.
|
||||
customData: {
|
||||
appName: meta.appName || 'Radio Player',
|
||||
backgroundImage: meta.backgroundImage || meta.image || undefined,
|
||||
backgroundGradient: meta.bgGradient || '#6a0dad',
|
||||
themeHint: {
|
||||
primary: '#6a0dad',
|
||||
accent: '#b36cf3'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Ensure we don't accumulate 'status' listeners across loads
|
||||
try { if (activePlayer && typeof activePlayer.removeAllListeners === 'function') activePlayer.removeAllListeners('status'); } catch (e) {}
|
||||
activePlayer.load(media, { autoplay: true }, (err, status) => {
|
||||
if (err) return error(`Load error: ${err.message}`);
|
||||
log('Media loaded, playing...');
|
||||
});
|
||||
|
||||
activePlayer.on('status', (status) => {
|
||||
// Optional: track status
|
||||
});
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (activePlayer) {
|
||||
try { activePlayer.stop(); } catch (e) { }
|
||||
try { if (typeof activePlayer.removeAllListeners === 'function') activePlayer.removeAllListeners(); } catch (e) {}
|
||||
log('Stopped playback');
|
||||
}
|
||||
if (activeClient) {
|
||||
try { if (typeof activeClient.removeAllListeners === 'function') activeClient.removeAllListeners(); } catch (e) {}
|
||||
try { activeClient.close(); } catch (e) { }
|
||||
activeClient = null;
|
||||
activePlayer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function setVolume(level) {
|
||||
if (activeClient && activePlayer) {
|
||||
activeClient.setVolume({ level }, (err, status) => {
|
||||
if (err) return error(`Volume error: ${err.message}`);
|
||||
log(`Volume set to ${level}`);
|
||||
});
|
||||
} else {
|
||||
log('Volume command ignored: Player not initialized');
|
||||
}
|
||||
}
|
||||
|
||||
log('Sidecar initialized and waiting for commands');
|
||||
193
sidecar/package-lock.json
generated
Normal file
193
sidecar/package-lock.json
generated
Normal file
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"name": "radiocast-sidecar",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "radiocast-sidecar",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"castv2-client": "^1.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"radiocast-sidecar": "index.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@types/long": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
|
||||
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
|
||||
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/castv2": {
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/castv2/-/castv2-0.1.10.tgz",
|
||||
"integrity": "sha512-3QWevHrjT22KdF08Y2a217IYCDQDP7vEJaY4n0lPBeC5UBYbMFMadDfVTsaQwq7wqsEgYUHElPGm3EO1ey+TNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"protobufjs": "^6.8.8"
|
||||
}
|
||||
},
|
||||
"node_modules/castv2-client": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/castv2-client/-/castv2-client-1.2.0.tgz",
|
||||
"integrity": "sha512-2diOsC0vSSxa3QEOgoGBy9fZRHzNXatHz464Kje2OpwQ7GM5vulyrD0gLFOQ1P4rgLAFsYiSGQl4gK402nEEuA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"castv2": "~0.1.4",
|
||||
"debug": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/castv2/node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/castv2/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "6.11.4",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz",
|
||||
"integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/long": "^4.0.1",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbjs": "bin/pbjs",
|
||||
"pbts": "bin/pbts"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
17
sidecar/package.json
Normal file
17
sidecar/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "radiocast-sidecar",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"bin": "index.js",
|
||||
"dependencies": {
|
||||
"castv2-client": "^1.2.0"
|
||||
},
|
||||
"pkg": {
|
||||
"assets": [
|
||||
"node_modules/castv2/lib/*.proto"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pkg . --targets node18-win-x64 --output ../src-tauri/binaries/radiocast-sidecar-x86_64-pc-windows-msvc.exe"
|
||||
}
|
||||
}
|
||||
1541
src-tauri/Cargo.lock
generated
1541
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
||||
[package]
|
||||
name = "radio-tauri"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
default-run = "radio-tauri"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -24,5 +25,17 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
rust_cast = "0.19.0"
|
||||
mdns-sd = "0.17.1"
|
||||
agnostic-mdns = { version = "0.4", features = ["tokio"], optional = true }
|
||||
async-channel = "2.5.0"
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
tauri-plugin-shell = "2.3.3"
|
||||
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
|
||||
base64 = "0.22"
|
||||
cpal = "0.15"
|
||||
ringbuf = "0.3"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
|
||||
|
||||
[features]
|
||||
use_agnostic_mdns = ["agnostic-mdns"]
|
||||
|
||||
|
||||
15
src-tauri/build_log.txt
Normal file
15
src-tauri/build_log.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
Compiling radio-tauri v0.1.0 (D:\Sites\Work\RadioCast\src-tauri)
|
||||
error[E0599]: no method named `clone` found for struct `CommandChild` in the current scope
|
||||
--> src\lib.rs:33:36
|
||||
|
|
||||
33 | let returned_child = child.clone();
|
||||
| ^^^^^ method not found in `CommandChild`
|
||||
|
||||
error[E0599]: no method named `clone` found for struct `CommandChild` in the current scope
|
||||
--> src\lib.rs:58:32
|
||||
|
|
||||
58 | let returned_child = child.clone();
|
||||
| ^^^^^ method not found in `CommandChild`
|
||||
|
||||
For more information about this error, try `rustc --explain E0599`.
|
||||
error: could not compile `radio-tauri` (lib) due to 2 previous errors
|
||||
@@ -6,6 +6,7 @@
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-close",
|
||||
"opener:default"
|
||||
"opener:default",
|
||||
"shell:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Checking radio-tauri v0.1.0 (D:\Sites\Work\Radio1\radio-tauri\src-tauri)
|
||||
Checking radio-tauri v0.1.0 (D:\Sites\Work\RadioPlayer\radio-tauri\src-tauri)
|
||||
warning: variable does not need to be mutable
|
||||
--> src\lib.rs:38:9
|
||||
|
|
||||
|
||||
12
src-tauri/gen/android/.editorconfig
Normal file
12
src-tauri/gen/android/.editorconfig
Normal file
@@ -0,0 +1,12 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = false
|
||||
insert_final_newline = false
|
||||
19
src-tauri/gen/android/.gitignore
vendored
Normal file
19
src-tauri/gen/android/.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
key.properties
|
||||
|
||||
/.tauri
|
||||
/tauri.settings.gradle
|
||||
6
src-tauri/gen/android/app/.gitignore
vendored
Normal file
6
src-tauri/gen/android/app/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/src/main/java/si/klevze/radioPlayer/generated
|
||||
/src/main/jniLibs/**/*.so
|
||||
/src/main/assets/tauri.conf.json
|
||||
/tauri.build.gradle.kts
|
||||
/proguard-tauri.pro
|
||||
/tauri.properties
|
||||
64
src-tauri/gen/android/app/build.gradle.kts
Normal file
64
src-tauri/gen/android/app/build.gradle.kts
Normal file
@@ -0,0 +1,64 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
val tauriProperties = Properties().apply {
|
||||
val propFile = file("tauri.properties")
|
||||
if (propFile.exists()) {
|
||||
propFile.inputStream().use { load(it) }
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 36
|
||||
namespace = "si.klevze.radioPlayer"
|
||||
defaultConfig {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "false"
|
||||
applicationId = "si.klevze.radioPlayer"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
|
||||
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
|
||||
}
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
manifestPlaceholders["usesCleartextTraffic"] = "true"
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
isMinifyEnabled = false
|
||||
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86/*.so")
|
||||
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
|
||||
}
|
||||
}
|
||||
getByName("release") {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
*fileTree(".") { include("**/*.pro") }
|
||||
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
|
||||
.toList().toTypedArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
|
||||
implementation("androidx.webkit:webkit:1.14.0")
|
||||
implementation("androidx.appcompat:appcompat:1.7.1")
|
||||
implementation("androidx.activity:activity-ktx:1.10.1")
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.4")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
|
||||
}
|
||||
21
src-tauri/gen/android/app/proguard-rules.pro
vendored
Normal file
21
src-tauri/gen/android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
37
src-tauri/gen/android/app/src/main/AndroidManifest.xml
Normal file
37
src-tauri/gen/android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<!-- AndroidTV support -->
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.radio_tauri"
|
||||
android:usesCleartextTraffic="${usesCleartextTraffic}">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/main_activity_title"
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<!-- AndroidTV support -->
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
Binary file not shown.
Binary file not shown.
225
src-tauri/gen/android/app/src/main/assets/builder-debug.yml
Normal file
225
src-tauri/gen/android/app/src/main/assets/builder-debug.yml
Normal file
@@ -0,0 +1,225 @@
|
||||
x64:
|
||||
firstOrDefaultFilePatterns:
|
||||
- '!**/node_modules'
|
||||
- '!build{,/**/*}'
|
||||
- '!dist{,/**/*}'
|
||||
- electron/**/*
|
||||
- src/**/*
|
||||
- receiver/**/*
|
||||
- package.json
|
||||
- '!**/*.{iml,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,suo,xproj,cc,d.ts,mk,a,o,forge-meta,pdb}'
|
||||
- '!**/._*'
|
||||
- '!**/electron-builder.{yaml,yml,json,json5,toml,ts}'
|
||||
- '!**/{.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,.DS_Store,thumbs.db,.gitignore,.gitkeep,.gitattributes,.npmignore,.idea,.vs,.flowconfig,.jshintrc,.eslintrc,.circleci,.yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,appveyor.yml,.travis.yml,circle.yml,.nyc_output,.husky,.github,electron-builder.env}'
|
||||
- '!.yarn{,/**/*}'
|
||||
- '!.editorconfig'
|
||||
- '!.yarnrc.yml'
|
||||
nodeModuleFilePatterns:
|
||||
- '**/*'
|
||||
- electron/**/*
|
||||
- src/**/*
|
||||
- receiver/**/*
|
||||
- package.json
|
||||
nsis:
|
||||
script: |-
|
||||
!include "D:\Sites\Work\RadioCast\node_modules\app-builder-lib\templates\nsis\include\StdUtils.nsh"
|
||||
!addincludedir "D:\Sites\Work\RadioCast\node_modules\app-builder-lib\templates\nsis\include"
|
||||
!macro _isUpdated _a _b _t _f
|
||||
${StdUtils.TestParameter} $R9 "updated"
|
||||
StrCmp "$R9" "true" `${_t}` `${_f}`
|
||||
!macroend
|
||||
!define isUpdated `"" isUpdated ""`
|
||||
|
||||
!macro _isForceRun _a _b _t _f
|
||||
${StdUtils.TestParameter} $R9 "force-run"
|
||||
StrCmp "$R9" "true" `${_t}` `${_f}`
|
||||
!macroend
|
||||
!define isForceRun `"" isForceRun ""`
|
||||
|
||||
!macro _isKeepShortcuts _a _b _t _f
|
||||
${StdUtils.TestParameter} $R9 "keep-shortcuts"
|
||||
StrCmp "$R9" "true" `${_t}` `${_f}`
|
||||
!macroend
|
||||
!define isKeepShortcuts `"" isKeepShortcuts ""`
|
||||
|
||||
!macro _isNoDesktopShortcut _a _b _t _f
|
||||
${StdUtils.TestParameter} $R9 "no-desktop-shortcut"
|
||||
StrCmp "$R9" "true" `${_t}` `${_f}`
|
||||
!macroend
|
||||
!define isNoDesktopShortcut `"" isNoDesktopShortcut ""`
|
||||
|
||||
!macro _isDeleteAppData _a _b _t _f
|
||||
${StdUtils.TestParameter} $R9 "delete-app-data"
|
||||
StrCmp "$R9" "true" `${_t}` `${_f}`
|
||||
!macroend
|
||||
!define isDeleteAppData `"" isDeleteAppData ""`
|
||||
|
||||
!macro _isForAllUsers _a _b _t _f
|
||||
${StdUtils.TestParameter} $R9 "allusers"
|
||||
StrCmp "$R9" "true" `${_t}` `${_f}`
|
||||
!macroend
|
||||
!define isForAllUsers `"" isForAllUsers ""`
|
||||
|
||||
!macro _isForCurrentUser _a _b _t _f
|
||||
${StdUtils.TestParameter} $R9 "currentuser"
|
||||
StrCmp "$R9" "true" `${_t}` `${_f}`
|
||||
!macroend
|
||||
!define isForCurrentUser `"" isForCurrentUser ""`
|
||||
|
||||
!macro addLangs
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
!insertmacro MUI_LANGUAGE "German"
|
||||
!insertmacro MUI_LANGUAGE "French"
|
||||
!insertmacro MUI_LANGUAGE "SpanishInternational"
|
||||
!insertmacro MUI_LANGUAGE "SimpChinese"
|
||||
!insertmacro MUI_LANGUAGE "TradChinese"
|
||||
!insertmacro MUI_LANGUAGE "Japanese"
|
||||
!insertmacro MUI_LANGUAGE "Korean"
|
||||
!insertmacro MUI_LANGUAGE "Italian"
|
||||
!insertmacro MUI_LANGUAGE "Dutch"
|
||||
!insertmacro MUI_LANGUAGE "Danish"
|
||||
!insertmacro MUI_LANGUAGE "Swedish"
|
||||
!insertmacro MUI_LANGUAGE "Norwegian"
|
||||
!insertmacro MUI_LANGUAGE "Finnish"
|
||||
!insertmacro MUI_LANGUAGE "Russian"
|
||||
!insertmacro MUI_LANGUAGE "Portuguese"
|
||||
!insertmacro MUI_LANGUAGE "PortugueseBR"
|
||||
!insertmacro MUI_LANGUAGE "Polish"
|
||||
!insertmacro MUI_LANGUAGE "Ukrainian"
|
||||
!insertmacro MUI_LANGUAGE "Czech"
|
||||
!insertmacro MUI_LANGUAGE "Slovak"
|
||||
!insertmacro MUI_LANGUAGE "Hungarian"
|
||||
!insertmacro MUI_LANGUAGE "Arabic"
|
||||
!insertmacro MUI_LANGUAGE "Turkish"
|
||||
!insertmacro MUI_LANGUAGE "Thai"
|
||||
!insertmacro MUI_LANGUAGE "Vietnamese"
|
||||
!macroend
|
||||
|
||||
!include "C:\Users\Gregor\AppData\Local\Temp\t-6x2nSt\0-messages.nsh"
|
||||
!addplugindir /x86-unicode "C:\Users\Gregor\AppData\Local\electron-builder\Cache\nsis\nsis-resources-3.4.1\plugins\x86-unicode"
|
||||
|
||||
Var newStartMenuLink
|
||||
Var oldStartMenuLink
|
||||
Var newDesktopLink
|
||||
Var oldDesktopLink
|
||||
Var oldShortcutName
|
||||
Var oldMenuDirectory
|
||||
|
||||
!include "common.nsh"
|
||||
!include "MUI2.nsh"
|
||||
!include "multiUser.nsh"
|
||||
!include "allowOnlyOneInstallerInstance.nsh"
|
||||
|
||||
!ifdef INSTALL_MODE_PER_ALL_USERS
|
||||
!ifdef BUILD_UNINSTALLER
|
||||
RequestExecutionLevel user
|
||||
!else
|
||||
RequestExecutionLevel admin
|
||||
!endif
|
||||
!else
|
||||
RequestExecutionLevel user
|
||||
!endif
|
||||
|
||||
!ifdef BUILD_UNINSTALLER
|
||||
SilentInstall silent
|
||||
!else
|
||||
Var appExe
|
||||
Var launchLink
|
||||
!endif
|
||||
|
||||
!ifdef ONE_CLICK
|
||||
!include "oneClick.nsh"
|
||||
!else
|
||||
!include "assistedInstaller.nsh"
|
||||
!endif
|
||||
|
||||
!insertmacro addLangs
|
||||
|
||||
!ifmacrodef customHeader
|
||||
!insertmacro customHeader
|
||||
!endif
|
||||
|
||||
Function .onInit
|
||||
Call setInstallSectionSpaceRequired
|
||||
|
||||
SetOutPath $INSTDIR
|
||||
${LogSet} on
|
||||
|
||||
!ifmacrodef preInit
|
||||
!insertmacro preInit
|
||||
!endif
|
||||
|
||||
!ifdef DISPLAY_LANG_SELECTOR
|
||||
!insertmacro MUI_LANGDLL_DISPLAY
|
||||
!endif
|
||||
|
||||
!ifdef BUILD_UNINSTALLER
|
||||
WriteUninstaller "${UNINSTALLER_OUT_FILE}"
|
||||
!insertmacro quitSuccess
|
||||
!else
|
||||
!insertmacro check64BitAndSetRegView
|
||||
|
||||
!ifdef ONE_CLICK
|
||||
!insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE
|
||||
!else
|
||||
${IfNot} ${UAC_IsInnerInstance}
|
||||
!insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
!insertmacro initMultiUser
|
||||
|
||||
!ifmacrodef customInit
|
||||
!insertmacro customInit
|
||||
!endif
|
||||
|
||||
!ifmacrodef addLicenseFiles
|
||||
InitPluginsDir
|
||||
!insertmacro addLicenseFiles
|
||||
!endif
|
||||
!endif
|
||||
FunctionEnd
|
||||
|
||||
!ifndef BUILD_UNINSTALLER
|
||||
!include "installUtil.nsh"
|
||||
!endif
|
||||
|
||||
Section "install" INSTALL_SECTION_ID
|
||||
!ifndef BUILD_UNINSTALLER
|
||||
# If we're running a silent upgrade of a per-machine installation, elevate so extracting the new app will succeed.
|
||||
# For a non-silent install, the elevation will be triggered when the install mode is selected in the UI,
|
||||
# but that won't be executed when silent.
|
||||
!ifndef INSTALL_MODE_PER_ALL_USERS
|
||||
!ifndef ONE_CLICK
|
||||
${if} $hasPerMachineInstallation == "1" # set in onInit by initMultiUser
|
||||
${andIf} ${Silent}
|
||||
${ifNot} ${UAC_IsAdmin}
|
||||
ShowWindow $HWNDPARENT ${SW_HIDE}
|
||||
!insertmacro UAC_RunElevated
|
||||
${Switch} $0
|
||||
${Case} 0
|
||||
${Break}
|
||||
${Case} 1223 ;user aborted
|
||||
${Break}
|
||||
${Default}
|
||||
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Unable to elevate, error $0"
|
||||
${Break}
|
||||
${EndSwitch}
|
||||
Quit
|
||||
${else}
|
||||
!insertmacro setInstallModePerAllUsers
|
||||
${endIf}
|
||||
${endIf}
|
||||
!endif
|
||||
!endif
|
||||
!include "installSection.nsh"
|
||||
!endif
|
||||
SectionEnd
|
||||
|
||||
Function setInstallSectionSpaceRequired
|
||||
!insertmacro setSpaceRequired ${INSTALL_SECTION_ID}
|
||||
FunctionEnd
|
||||
|
||||
!ifdef BUILD_UNINSTALLER
|
||||
!include "uninstaller.nsh"
|
||||
!endif
|
||||
@@ -0,0 +1,25 @@
|
||||
directories:
|
||||
output: dist
|
||||
buildResources: build
|
||||
appId: si.klevze.radioPlayer
|
||||
productName: RadioPlayer
|
||||
files:
|
||||
- filter:
|
||||
- electron/**/*
|
||||
- src/**/*
|
||||
- receiver/**/*
|
||||
- package.json
|
||||
win:
|
||||
target:
|
||||
- nsis
|
||||
signAndEditExecutable: false
|
||||
icon: src-tauri/icons/icon.ico
|
||||
mac:
|
||||
target:
|
||||
- dmg
|
||||
icon: src-tauri/icons/icon.icns
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
icon: src-tauri/icons
|
||||
electronVersion: 30.5.1
|
||||
@@ -0,0 +1,21 @@
|
||||
Copyright (c) Electron contributors
|
||||
Copyright (c) 2013-2020 GitHub Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user