Compare commits
26 Commits
master
...
bbb767cd20
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
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
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
54
README.md
54
README.md
@@ -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):
|
To start the application in development mode (with hot-reloading for frontend changes):
|
||||||
|
|
||||||
```bash
|
```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:
|
This command will:
|
||||||
@@ -50,7 +56,7 @@ To create an optimized, standalone executable for your operating system:
|
|||||||
|
|
||||||
1. **Run the build command**:
|
1. **Run the build command**:
|
||||||
```bash
|
```bash
|
||||||
npm run tauri build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Locate the artifacts**:
|
2. **Locate the artifacts**:
|
||||||
@@ -67,7 +73,9 @@ To create an optimized, standalone executable for your operating system:
|
|||||||
* `styles.css`: Application styling.
|
* `styles.css`: Application styling.
|
||||||
* `stations.json`: Configuration file for available radio streams.
|
* `stations.json`: Configuration file for available radio streams.
|
||||||
* **`src-tauri/`**: Rust backend code.
|
* **`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).
|
* `tauri.conf.json`: Configuration for the Tauri app (window size, permissions, package info).
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
@@ -103,18 +111,46 @@ 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.
|
* **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.
|
* **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
|
## License
|
||||||
|
|
||||||
[Add License Information Here]
|
[Add License Information Here]
|
||||||
|
|
||||||
|
|
||||||
## Release v0.1
|
## Release v0.2
|
||||||
|
|
||||||
Initial public preview (v0.1) — a minimal, working RadioPlayer experience:
|
Public beta (v0.2) — updates since v0.1:
|
||||||
|
|
||||||
- Custom CAF Receiver UI (HTML/CSS/JS) in `receiver/` with branded artwork and playback status.
|
- **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.
|
||||||
- Plays LIVE stream: `https://live.radio1.si/Radio1MB` (contentType: `audio/mpeg`, streamType: `LIVE`).
|
- **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.
|
||||||
- Desktop sidecar (`sidecar/index.js`) launches the Default Media Receiver and sends LOAD commands; launch flow now retries if the device reports `NOT_ALLOWED` by stopping existing sessions first.
|
- **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:
|
Included receiver files:
|
||||||
|
|
||||||
@@ -140,6 +176,6 @@ npx http-server receiver -p 8443 -S -C localhost.pem -K localhost-key.pem
|
|||||||
|
|
||||||
Sidecar / troubleshoot
|
Sidecar / troubleshoot
|
||||||
|
|
||||||
- If a Cast launch fails with `NOT_ALLOWED`, the sidecar will now 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.
|
- 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.
|
- 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.
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Radio1 Player – Glassmorphism UI Redesign (Tauri + HTML)
|
# Radio Player – Glassmorphism UI Redesign (Tauri + HTML)
|
||||||
|
|
||||||
## Objective
|
## 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:
|
The app is built with:
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ Single centered player card:
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌──────────────────────────────┐
|
┌──────────────────────────────┐
|
||||||
│ Radio1 Player │
|
│ Radio Player │
|
||||||
│ ● Playing / Ready │
|
│ ● Playing / Ready │
|
||||||
│ │
|
│ │
|
||||||
│ [ Station Artwork / Logo ] │
|
│ [ Station Artwork / Logo ] │
|
||||||
@@ -52,7 +52,7 @@ Single centered player card:
|
|||||||
│ │
|
│ │
|
||||||
│ ────────●──────── │
|
│ ────────●──────── │
|
||||||
│ │
|
│ │
|
||||||
│ ⏮ ▶ / ⏸ ⏭ │
|
│ ⏮ ▶ / ⏸ ⏭ │
|
||||||
│ │
|
│ │
|
||||||
│ 🔊 ─────●──── 50% │
|
│ 🔊 ─────●──── 50% │
|
||||||
└──────────────────────────────┘
|
└──────────────────────────────┘
|
||||||
@@ -64,7 +64,7 @@ Single centered player card:
|
|||||||
|
|
||||||
### Header
|
### Header
|
||||||
|
|
||||||
* Title: `Radio1 Player`
|
* Title: `RadioPlayer`
|
||||||
* Status indicator:
|
* Status indicator:
|
||||||
|
|
||||||
* `● Ready`
|
* `● Ready`
|
||||||
|
|||||||
1548
package-lock.json
generated
1548
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -1,15 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "radio-tauri",
|
"name": "radio-tauri",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
"build": "node tools/copy-binaries.js && tauri build && node tools/post-build-rcedit.js",
|
"dev:native": "node tools/copy-binaries.js && node tools/copy-ffmpeg.js && tauri dev",
|
||||||
"tauri": "node tools/copy-binaries.js && tauri"
|
"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": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"npx": "^3.0.0",
|
||||||
"rcedit": "^1.1.2"
|
"rcedit": "^1.1.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
|
|
||||||
<stop offset="0" stop-color="#7b7fd8"/>
|
|
||||||
<stop offset="1" stop-color="#b57cf2"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect width="100%" height="100%" rx="24" fill="url(#g)" />
|
|
||||||
<g fill="white" transform="translate(32,32)">
|
|
||||||
<circle cx="48" cy="48" r="28" fill="rgba(255,255,255,0.15)" />
|
|
||||||
<path d="M24 48c6-10 16-16 24-16v8c-6 0-14 4-18 12s-2 12 0 12 6-2 10-6c4-4 10-6 14-6v8c-6 0-14 4-18 12s-2 12 0 12" stroke="white" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round" opacity="0.95" />
|
|
||||||
<text x="96" y="98" font-family="sans-serif" font-size="18" fill="white" opacity="0.95">Radio</text>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 815 B |
@@ -1,27 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
||||||
<title>Radio Player</title>
|
|
||||||
|
|
||||||
<!-- Google Cast Receiver SDK -->
|
|
||||||
<script src="https://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="styles.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<h1>Radio Player</h1>
|
|
||||||
<p id="status">Ready</p>
|
|
||||||
|
|
||||||
<div id="artwork">
|
|
||||||
<img src="assets/logo.svg" alt="Radio Player" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p id="station">Radio 1 – Live Stream</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="receiver.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
/* Receiver for "Radio Player" using CAF Receiver SDK */
|
|
||||||
(function () {
|
|
||||||
const STREAM_URL = 'https://live.radio1.si/Radio1MB';
|
|
||||||
|
|
||||||
function $(id) { return document.getElementById(id); }
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const context = cast.framework.CastReceiverContext.getInstance();
|
|
||||||
const playerManager = context.getPlayerManager();
|
|
||||||
const statusEl = $('status');
|
|
||||||
const stationEl = $('station');
|
|
||||||
|
|
||||||
// Intercept LOAD to enforce correct metadata for LIVE audio
|
|
||||||
playerManager.setMessageInterceptor(
|
|
||||||
cast.framework.messages.MessageType.LOAD,
|
|
||||||
(request) => {
|
|
||||||
if (!request || !request.media) return request;
|
|
||||||
|
|
||||||
request.media.contentId = request.media.contentId || STREAM_URL;
|
|
||||||
request.media.contentType = 'audio/mpeg';
|
|
||||||
request.media.streamType = cast.framework.messages.StreamType.LIVE;
|
|
||||||
|
|
||||||
request.media.metadata = request.media.metadata || {};
|
|
||||||
request.media.metadata.title = request.media.metadata.title || 'Radio 1';
|
|
||||||
request.media.metadata.images = request.media.metadata.images || [{ url: 'assets/logo.svg' }];
|
|
||||||
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update UI on player state changes
|
|
||||||
playerManager.addEventListener(
|
|
||||||
cast.framework.events.EventType.PLAYER_STATE_CHANGED,
|
|
||||||
() => {
|
|
||||||
const state = playerManager.getPlayerState();
|
|
||||||
switch (state) {
|
|
||||||
case cast.framework.messages.PlayerState.PLAYING:
|
|
||||||
statusEl.textContent = 'Playing';
|
|
||||||
break;
|
|
||||||
case cast.framework.messages.PlayerState.PAUSED:
|
|
||||||
statusEl.textContent = 'Paused';
|
|
||||||
break;
|
|
||||||
case cast.framework.messages.PlayerState.IDLE:
|
|
||||||
statusEl.textContent = 'Stopped';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
statusEl.textContent = state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// When a new media is loaded, reflect metadata (station name, artwork)
|
|
||||||
playerManager.addEventListener(cast.framework.events.EventType.LOAD, (event) => {
|
|
||||||
const media = event && event.data && event.data.media;
|
|
||||||
if (media && media.metadata) {
|
|
||||||
if (media.metadata.title) stationEl.textContent = media.metadata.title;
|
|
||||||
if (media.metadata.images && media.metadata.images[0] && media.metadata.images[0].url) {
|
|
||||||
const img = document.querySelector('#artwork img');
|
|
||||||
img.src = media.metadata.images[0].url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Optional: reflect volume in title attribute
|
|
||||||
playerManager.addEventListener(cast.framework.events.EventType.VOLUME_CHANGED, (evt) => {
|
|
||||||
const level = evt && evt.data && typeof evt.data.level === 'number' ? evt.data.level : null;
|
|
||||||
if (level !== null) statusEl.title = `Volume: ${Math.round(level * 100)}%`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start the cast receiver context
|
|
||||||
context.start({ statusText: 'Radio Player Ready' });
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
html, body {
|
|
||||||
margin: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(135deg, #7b7fd8, #b57cf2);
|
|
||||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-align: center;
|
|
||||||
padding: 24px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
#artwork {
|
|
||||||
width: 240px;
|
|
||||||
height: 240px;
|
|
||||||
margin: 20px 0;
|
|
||||||
border-radius: 24px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: rgba(0,0,0,0.1);
|
|
||||||
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#artwork img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
#status {
|
|
||||||
font-size: 18px;
|
|
||||||
opacity: 0.95;
|
|
||||||
margin: 6px 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#station {
|
|
||||||
font-size: 16px;
|
|
||||||
opacity: 0.85;
|
|
||||||
margin: 6px 0 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 20px;
|
|
||||||
margin: 0 0 6px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
#artwork { width: 160px; height: 160px; }
|
|
||||||
h1 { font-size: 18px; }
|
|
||||||
}
|
|
||||||
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 {}
|
||||||
|
}
|
||||||
@@ -175,7 +175,7 @@ function loadMedia(url) {
|
|||||||
streamType: 'LIVE',
|
streamType: 'LIVE',
|
||||||
metadata: {
|
metadata: {
|
||||||
metadataType: 0,
|
metadataType: 0,
|
||||||
title: 'Radio 1'
|
title: 'RadioPlayer'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
3
sidecar/package-lock.json
generated
3
sidecar/package-lock.json
generated
@@ -9,6 +9,9 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"castv2-client": "^1.2.0"
|
"castv2-client": "^1.2.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"radiocast-sidecar": "index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@protobufjs/aspromise": {
|
"node_modules/@protobufjs/aspromise": {
|
||||||
|
|||||||
748
src-tauri/Cargo.lock
generated
748
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "radio-tauri"
|
name = "radio-tauri"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
default-run = "radio-tauri"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@@ -26,4 +27,8 @@ rust_cast = "0.19.0"
|
|||||||
mdns-sd = "0.17.1"
|
mdns-sd = "0.17.1"
|
||||||
tokio = { version = "1.48.0", features = ["full"] }
|
tokio = { version = "1.48.0", features = ["full"] }
|
||||||
tauri-plugin-shell = "2.3.3"
|
tauri-plugin-shell = "2.3.3"
|
||||||
|
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
|
||||||
|
base64 = "0.22"
|
||||||
|
cpal = "0.15"
|
||||||
|
ringbuf = "0.3"
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
warning: variable does not need to be mutable
|
||||||
--> src\lib.rs:38:9
|
--> 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.
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