Compare commits
9 Commits
68b2f8949b
...
f87f7fc8aa
| Author | SHA1 | Date | |
|---|---|---|---|
| f87f7fc8aa | |||
| e3fb14e9aa | |||
| 747d5296fa | |||
| dba3f21114 | |||
| 676a715187 | |||
| 71fab9def0 | |||
| 34c4521629 | |||
| a2ba71a2a6 | |||
| e57d9a0229 |
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"]
|
||||
}
|
||||
143
README.md
@@ -1,2 +1,145 @@
|
||||
# RadioPlayer
|
||||
|
||||
A lightweight, cross-platform radio player built with Tauri and Vanilla JavaScript. Features local playback and Google Cast integration.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following installed on your machine:
|
||||
|
||||
1. **Node.js**: [Download Node.js](https://nodejs.org/) (LTS version recommended).
|
||||
2. **Rust**: Install via [rustup.rs](https://rustup.rs/).
|
||||
3. **Visual Studio C++ Build Tools** (Windows only): Required for compiling Rust. Ensure "Desktop development with C++" is selected during installation.
|
||||
|
||||
## Installation
|
||||
|
||||
1. **Clone the repository**:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd RadioPlayer
|
||||
```
|
||||
|
||||
2. **Install dependencies**:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Verify Rust environment**:
|
||||
It's good practice to ensure your Rust environment is ready.
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo check
|
||||
cd ..
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
To start the application in development mode (with hot-reloading for frontend changes):
|
||||
|
||||
```bash
|
||||
npm run tauri dev
|
||||
```
|
||||
|
||||
This command will:
|
||||
1. Compile the Rust backend.
|
||||
2. Launch the application window.
|
||||
3. Watch for changes in `src/` and `src-tauri/`.
|
||||
|
||||
## Building for Production
|
||||
|
||||
To create an optimized, standalone executable for your operating system:
|
||||
|
||||
1. **Run the build command**:
|
||||
```bash
|
||||
npm run tauri build
|
||||
```
|
||||
|
||||
2. **Locate the artifacts**:
|
||||
After the build completes, the installers and executables will be found in:
|
||||
- **Windows**: `src-tauri/target/release/bundle/msi/` or `nsis/`
|
||||
- **macOS**: `src-tauri/target/release/bundle/dmg/` or `macos/`
|
||||
- **Linux**: `src-tauri/target/release/bundle/deb/` or `appimage/`
|
||||
|
||||
## Project Structure
|
||||
|
||||
* **`src/`**: Frontend source code (Vanilla HTML/CSS/JS).
|
||||
* `index.html`: The main entry point of the app.
|
||||
* `main.js`: Core logic, handles UI events and communication with the Tauri backend.
|
||||
* `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.
|
||||
* `tauri.conf.json`: Configuration for the Tauri app (window size, permissions, package info).
|
||||
|
||||
## Customization
|
||||
|
||||
### Adding Radio Stations
|
||||
To add new stations, edit the `src/stations.json` file. Add a new object to the array with a `name` and stream `url`:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "My New Station",
|
||||
"url": "https://stream-url.com/stream"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Adjusting Window Size
|
||||
To change the default window size, edit `src-tauri/tauri.conf.json`:
|
||||
|
||||
```json
|
||||
"windows": [
|
||||
{
|
||||
"width": 360, // Change width
|
||||
"height": 720, // Change height
|
||||
"resizable": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
* **Command `tauri` not found**: Ensure you are running commands via `npm run tauri ...` or global install `@tauri-apps/cli`.
|
||||
* **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.
|
||||
|
||||
## License
|
||||
|
||||
[Add License Information Here]
|
||||
|
||||
|
||||
## Release v0.1
|
||||
|
||||
Initial public preview (v0.1) — a minimal, working RadioPlayer experience:
|
||||
|
||||
- Custom CAF Receiver UI (HTML/CSS/JS) in `receiver/` with branded artwork and playback status.
|
||||
- Plays LIVE stream: `https://live.radio1.si/Radio1MB` (contentType: `audio/mpeg`, streamType: `LIVE`).
|
||||
- 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.
|
||||
|
||||
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 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.
|
||||
- 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.
|
||||
|
||||
|
||||
BIN
android/.gradle/8.9/checksums/checksums.lock
Normal file
BIN
android/.gradle/8.9/checksums/md5-checksums.bin
Normal file
BIN
android/.gradle/8.9/checksums/sha1-checksums.bin
Normal file
BIN
android/.gradle/8.9/executionHistory/executionHistory.lock
Normal file
BIN
android/.gradle/8.9/fileChanges/last-build.bin
Normal file
BIN
android/.gradle/8.9/fileHashes/fileHashes.lock
Normal file
0
android/.gradle/8.9/gc.properties
Normal file
BIN
android/.gradle/buildOutputCleanup/buildOutputCleanup.lock
Normal file
2
android/.gradle/buildOutputCleanup/cache.properties
Normal file
@@ -0,0 +1,2 @@
|
||||
#Wed Dec 31 09:53:51 CET 2025
|
||||
gradle.version=8.9
|
||||
0
android/.gradle/vcs-1/gc.properties
Normal file
BIN
app-icon.png
Normal file
|
After Width: | Height: | Size: 290 KiB |
256
layout2_plan.md
Normal file
@@ -0,0 +1,256 @@
|
||||
# Beautify “Connect to Device” Popup (Tauri + HTML)
|
||||
|
||||
## Goal
|
||||
|
||||
Redesign the **Connect to Device** popup/modal to match the app’s **glassmorphism + neon purple** style:
|
||||
|
||||
* Frosted glass modal
|
||||
* Blurred/dimmed overlay background
|
||||
* Clean device list with hover + selected states
|
||||
* Premium rounded corners and soft glow
|
||||
* Works smoothly inside **Tauri WebView** (desktop)
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
|
||||
* Modal opens/closes via existing app logic (do not change behavior).
|
||||
* Device list supports:
|
||||
|
||||
* Hover highlight
|
||||
* Single selected device (one at a time)
|
||||
* Click selects device (and triggers existing handler)
|
||||
* “Cancel” button closes modal (existing handler).
|
||||
* Include “This Computer (Local Playback)” option at top.
|
||||
|
||||
### Visual
|
||||
|
||||
* Overlay blur + dark tint behind modal.
|
||||
* Modal background: semi-transparent glass with blur.
|
||||
* No harsh borders; use subtle border + soft shadow.
|
||||
* Selected device: gradient accent + glow.
|
||||
* Smooth animation on open (scale + fade).
|
||||
* Desktop-friendly sizing (320–420px wide).
|
||||
|
||||
### Tauri Safety / Performance
|
||||
|
||||
* Keep CSS lightweight.
|
||||
* Avoid heavy SVG masks or expensive filters beyond `backdrop-filter: blur(...)`.
|
||||
* Ensure modal area is `-webkit-app-region: no-drag` if you use draggable window headers.
|
||||
|
||||
---
|
||||
|
||||
## Target Structure
|
||||
|
||||
### HTML (keep semantic + simple)
|
||||
|
||||
Use or adapt this structure in the renderer HTML:
|
||||
|
||||
```html
|
||||
<div id="deviceOverlay" class="overlay hidden" aria-hidden="true">
|
||||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="deviceTitle">
|
||||
<h2 id="deviceTitle">Connect to Device</h2>
|
||||
|
||||
<ul id="deviceList" class="device-list">
|
||||
<!-- Render device items here -->
|
||||
<!-- Example item:
|
||||
<li class="device local selected" data-device="local">
|
||||
<div class="device-main">This Computer</div>
|
||||
<div class="device-sub">Local Playback</div>
|
||||
</li>
|
||||
-->
|
||||
</ul>
|
||||
|
||||
<button id="deviceCancelBtn" class="btn cancel" type="button">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
* Use `hidden` class to toggle visibility.
|
||||
* Each `<li>` must have `data-device="<name-or-id>"` for click handling.
|
||||
* Selected item uses `.selected`.
|
||||
|
||||
---
|
||||
|
||||
## CSS (Glassmorphism Modal)
|
||||
|
||||
Add/merge this CSS into your stylesheet:
|
||||
|
||||
```css
|
||||
/* Overlay */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(20, 10, 35, 0.45);
|
||||
backdrop-filter: blur(14px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.hidden { display: none !important; }
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
width: min(420px, calc(100vw - 48px));
|
||||
padding: 22px;
|
||||
border-radius: 22px;
|
||||
background: rgba(30, 30, 40, 0.82);
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
box-shadow: 0 30px 80px rgba(0,0,0,0.6);
|
||||
color: #fff;
|
||||
animation: pop 0.22s ease;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
from { transform: scale(0.94); opacity: 0; }
|
||||
to { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
|
||||
.modal h2 {
|
||||
margin: 0 0 14px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* Device list */
|
||||
.device-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 18px;
|
||||
max-height: 360px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Device row */
|
||||
.device {
|
||||
padding: 12px 14px;
|
||||
border-radius: 14px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
background: rgba(255,255,255,0.05);
|
||||
transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.device:hover {
|
||||
background: rgba(255,255,255,0.10);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.device .device-main {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.device .device-sub {
|
||||
margin-top: 3px;
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Selected device */
|
||||
.device.selected {
|
||||
background: linear-gradient(135deg, #c77dff, #8b5cf6);
|
||||
box-shadow: 0 0 18px rgba(199,125,255,0.65);
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.device.selected .device-sub {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* Optional: disabled group devices */
|
||||
.device.disabled {
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Cancel button */
|
||||
.btn.cancel {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border-radius: 999px;
|
||||
border: none;
|
||||
background: #d16b7d;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.btn.cancel:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JavaScript Behavior (Minimal)
|
||||
|
||||
### Selection logic
|
||||
|
||||
* When a device row is clicked:
|
||||
|
||||
1. Remove `.selected` from all device `<li>`
|
||||
2. Add `.selected` to clicked `<li>`
|
||||
3. Call existing “connect/cast to device” handler with `data-device`
|
||||
|
||||
Pseudo-code (adapt to existing app code):
|
||||
|
||||
```js
|
||||
deviceList.addEventListener("click", (e) => {
|
||||
const item = e.target.closest(".device");
|
||||
if (!item) return;
|
||||
|
||||
deviceList.querySelectorAll(".device.selected")
|
||||
.forEach(el => el.classList.remove("selected"));
|
||||
|
||||
item.classList.add("selected");
|
||||
|
||||
const deviceName = item.dataset.device;
|
||||
// call existing connect logic:
|
||||
// connectToDevice(deviceName);
|
||||
});
|
||||
```
|
||||
|
||||
### Close modal
|
||||
|
||||
* Cancel button closes modal (existing function):
|
||||
|
||||
```js
|
||||
deviceCancelBtn.addEventListener("click", closeDeviceModal);
|
||||
```
|
||||
|
||||
### Optional: close on overlay click
|
||||
|
||||
* Only if app UX allows:
|
||||
|
||||
```js
|
||||
deviceOverlay.addEventListener("click", (e) => {
|
||||
if (e.target === deviceOverlay) closeDeviceModal();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
* Popup visually matches glassmorphism theme.
|
||||
* Overlay dims and blurs background.
|
||||
* Device list has hover + selected glow.
|
||||
* Exactly one device can be selected.
|
||||
* Cancel closes modal.
|
||||
* Runs smoothly in Tauri WebView without lag.
|
||||
|
||||
---
|
||||
|
||||
## Notes / Future Enhancements (Optional)
|
||||
|
||||
* Mark “Speaker Groups” as `.disabled` if casting library can’t support them.
|
||||
* Add small icons per device type (local vs cast).
|
||||
* Remember last selected device and preselect it on open.
|
||||
212
layout_plan.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Radio1 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.
|
||||
|
||||
The app is built with:
|
||||
|
||||
* **Tauri**
|
||||
* **HTML / CSS / Vanilla JS**
|
||||
* Desktop-first (Windows)
|
||||
|
||||
The UI must feel:
|
||||
|
||||
* Premium
|
||||
* Lightweight
|
||||
* Native
|
||||
* Smooth
|
||||
* Purple / neon themed
|
||||
|
||||
---
|
||||
|
||||
## Visual Reference
|
||||
|
||||
Style inspiration:
|
||||
|
||||
* Frosted glass cards
|
||||
* Soft purple / blue gradient background
|
||||
* Rounded corners everywhere
|
||||
* Subtle glow instead of hard borders
|
||||
* Floating controls
|
||||
|
||||
Target aesthetic keywords:
|
||||
|
||||
> glassmorphism, neon glow, frosted glass, modern music player, soft gradients
|
||||
|
||||
---
|
||||
|
||||
## Layout Structure
|
||||
|
||||
Single centered player card:
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ Radio1 Player │
|
||||
│ ● Playing / Ready │
|
||||
│ │
|
||||
│ [ Station Artwork / Logo ] │
|
||||
│ │
|
||||
│ Radio 1 MB │
|
||||
│ Live Stream │
|
||||
│ │
|
||||
│ ────────●──────── │
|
||||
│ │
|
||||
│ ⏮ ▶ / ⏸ ⏭ │
|
||||
│ │
|
||||
│ 🔊 ─────●──── 50% │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Required UI Elements
|
||||
|
||||
### Header
|
||||
|
||||
* Title: `Radio1 Player`
|
||||
* Status indicator:
|
||||
|
||||
* `● Ready`
|
||||
* `● Playing`
|
||||
* `● Casting to <Device>`
|
||||
|
||||
### Artwork Area
|
||||
|
||||
* Square (1:1)
|
||||
* Rounded corners (20–24px)
|
||||
* Gradient or station logo
|
||||
* Soft glow around container
|
||||
|
||||
### Metadata
|
||||
|
||||
* Station name (Radio 1 MB)
|
||||
* Subtitle (Live Stream / Casting Mode)
|
||||
|
||||
### Playback Controls
|
||||
|
||||
* Previous (optional, visual only)
|
||||
* Play / Pause (primary accent button)
|
||||
* Next (optional, visual only)
|
||||
|
||||
### Volume Control
|
||||
|
||||
* Speaker icon
|
||||
* Horizontal slider
|
||||
* Percentage text
|
||||
* * / – buttons optional
|
||||
|
||||
---
|
||||
|
||||
## Color Palette
|
||||
|
||||
```css
|
||||
--background-gradient: linear-gradient(135deg, #7b7fd8, #b57cf2);
|
||||
--glass-bg: rgba(255, 255, 255, 0.15);
|
||||
--glass-border: rgba(255, 255, 255, 0.25);
|
||||
--accent: #c77dff;
|
||||
--accent-glow: rgba(199, 125, 255, 0.7);
|
||||
--text-main: #ffffff;
|
||||
--text-muted: rgba(255, 255, 255, 0.65);
|
||||
--success: #7dffb3;
|
||||
--danger: #ff5f5f;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Styling Rules
|
||||
|
||||
* Use **glassmorphism**
|
||||
|
||||
* `backdrop-filter: blur(20px)`
|
||||
* Semi-transparent backgrounds
|
||||
* No hard borders
|
||||
* Use glow (`box-shadow`) instead of outlines
|
||||
* Large border radius (20–28px)
|
||||
* Smooth hover animations
|
||||
* No external UI libraries
|
||||
|
||||
---
|
||||
|
||||
## CSS Requirements
|
||||
|
||||
* Must work inside **Tauri WebView**
|
||||
* Avoid heavy filters or SVG masks
|
||||
* Use `accent-color` for sliders
|
||||
* Buttons should animate on hover (`scale`, `glow`)
|
||||
|
||||
---
|
||||
|
||||
## Tauri-Specific Enhancements
|
||||
|
||||
### Frameless Window
|
||||
|
||||
```ts
|
||||
appWindow.setDecorations(false);
|
||||
```
|
||||
|
||||
### Draggable Header
|
||||
|
||||
```css
|
||||
header {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
button, input {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## States to Support
|
||||
|
||||
### Playback States
|
||||
|
||||
* Ready
|
||||
* Playing locally
|
||||
* Casting to Google Speaker
|
||||
* Stopped
|
||||
|
||||
### UI Feedback
|
||||
|
||||
* Change accent glow when playing
|
||||
* Dim Play button when active
|
||||
* Update status text dynamically
|
||||
|
||||
---
|
||||
|
||||
## Accessibility & UX
|
||||
|
||||
* Buttons ≥ 44px
|
||||
* Clear contrast for text
|
||||
* Keyboard accessible controls
|
||||
* Volume slider supports mouse wheel
|
||||
|
||||
---
|
||||
|
||||
## Non-Goals
|
||||
|
||||
* No playlists
|
||||
* No track seeking (live stream)
|
||||
* No mobile layout
|
||||
* No heavy animations
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
* UI visually matches glassmorphism inspiration
|
||||
* Smooth performance in Tauri
|
||||
* Looks premium and modern
|
||||
* Clean HTML & CSS
|
||||
* Easy to extend later
|
||||
|
||||
---
|
||||
|
||||
## Instructions for Copilot Agent
|
||||
|
||||
* Generate **HTML + CSS only**
|
||||
* Keep JavaScript minimal
|
||||
* Prioritize visual fidelity
|
||||
* Avoid overengineering
|
||||
* Comment code where helpful
|
||||
240
package-lock.json
generated
Normal file
@@ -0,0 +1,240 @@
|
||||
{
|
||||
"name": "radio-tauri",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "radio-tauri",
|
||||
"version": "0.1.0",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
"rcedit": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.9.6.tgz",
|
||||
"integrity": "sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
"tauri": "tauri.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.9.6",
|
||||
"@tauri-apps/cli-darwin-x64": "2.9.6",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.9.6",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.9.6",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.9.6",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.9.6",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.9.6",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.9.6",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.9.6",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.9.6",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.9.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.9.6.tgz",
|
||||
"integrity": "sha512-gf5no6N9FCk1qMrti4lfwP77JHP5haASZgVbBgpZG7BUepB3fhiLCXGUK8LvuOjP36HivXewjg72LTnPDScnQQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.9.6.tgz",
|
||||
"integrity": "sha512-oWh74WmqbERwwrwcueJyY6HYhgCksUc6NT7WKeXyrlY/FPmNgdyQAgcLuTSkhRFuQ6zh4Np1HZpOqCTpeZBDcw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.9.6.tgz",
|
||||
"integrity": "sha512-/zde3bFroFsNXOHN204DC2qUxAcAanUjVXXSdEGmhwMUZeAQalNj5cz2Qli2elsRjKN/hVbZOJj0gQ5zaYUjSg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.9.6.tgz",
|
||||
"integrity": "sha512-pvbljdhp9VOo4RnID5ywSxgBs7qiylTPlK56cTk7InR3kYSTJKYMqv/4Q/4rGo/mG8cVppesKIeBMH42fw6wjg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.9.6.tgz",
|
||||
"integrity": "sha512-02TKUndpodXBCR0oP//6dZWGYcc22Upf2eP27NvC6z0DIqvkBBFziQUcvi2n6SrwTRL0yGgQjkm9K5NIn8s6jw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.9.6.tgz",
|
||||
"integrity": "sha512-fmp1hnulbqzl1GkXl4aTX9fV+ubHw2LqlLH1PE3BxZ11EQk+l/TmiEongjnxF0ie4kV8DQfDNJ1KGiIdWe1GvQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.9.6.tgz",
|
||||
"integrity": "sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.9.6.tgz",
|
||||
"integrity": "sha512-TOEuB8YCFZTWVDzsO2yW0+zGcoMiPPwcUgdnW1ODnmgfwccpnihDRoks+ABT1e3fHb1ol8QQWsHSCovb3o2ENQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.9.6.tgz",
|
||||
"integrity": "sha512-ujmDGMRc4qRLAnj8nNG26Rlz9klJ0I0jmZs2BPpmNNf0gM/rcVHhqbEkAaHPTBVIrtUdf7bGvQAD2pyIiUrBHQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.9.6.tgz",
|
||||
"integrity": "sha512-S4pT0yAJgFX8QRCyKA1iKjZ9Q/oPjCZf66A/VlG5Yw54Nnr88J1uBpmenINbXxzyhduWrIXBaUbEY1K80ZbpMg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.9.6.tgz",
|
||||
"integrity": "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/rcedit": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/rcedit/-/rcedit-1.1.2.tgz",
|
||||
"integrity": "sha512-z2ypB4gbINhI6wVe0JJMmdpmOpmNc4g90sE6/6JSuch5kYnjfz9CxvVPqqhShgR6GIkmtW3W2UlfiXhWljA0Fw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "radio-tauri",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
"build": "node tools/copy-binaries.js && tauri build && node tools/post-build-rcedit.js",
|
||||
"tauri": "node tools/copy-binaries.js && tauri"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
"rcedit": "^1.1.2"
|
||||
}
|
||||
}
|
||||
15
receiver/assets/logo.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 815 B |
27
receiver/index.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<!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>
|
||||
73
receiver/receiver.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/* 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' });
|
||||
});
|
||||
})();
|
||||
58
receiver/styles.css
Normal file
@@ -0,0 +1,58 @@
|
||||
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; }
|
||||
}
|
||||
215
sidecar/index.js
Normal file
@@ -0,0 +1,215 @@
|
||||
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();
|
||||
|
||||
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();
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
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) {
|
||||
if (activeClient) {
|
||||
try { activeClient.close(); } catch (e) { }
|
||||
}
|
||||
|
||||
activeClient = new Client();
|
||||
|
||||
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, /*didStopFirst*/ true));
|
||||
} else {
|
||||
activePlayer = player;
|
||||
loadMedia(url);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// If another app is running, stop it first to avoid NOT_ALLOWED.
|
||||
if (sessions.length > 0) {
|
||||
log('Non-media session detected, stopping before launch...');
|
||||
stopSessions(activeClient, sessions, () => launchPlayer(url, /*didStopFirst*/ true));
|
||||
} else {
|
||||
launchPlayer(url, /*didStopFirst*/ false);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
activeClient.on('error', (err) => {
|
||||
error(`Client error: ${err.message}`);
|
||||
try { activeClient.close(); } catch (e) { }
|
||||
activeClient = null;
|
||||
activePlayer = null;
|
||||
});
|
||||
}
|
||||
|
||||
function launchPlayer(url, didStopFirst) {
|
||||
if (!activeClient) return;
|
||||
|
||||
activeClient.launch(DefaultMediaReceiver, (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;
|
||||
}
|
||||
activePlayer = retryPlayer;
|
||||
loadMedia(url);
|
||||
});
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
error(details);
|
||||
try { error(`Launch error full: ${JSON.stringify(err)}`); } catch (e) { /* ignore */ }
|
||||
return;
|
||||
}
|
||||
activePlayer = player;
|
||||
loadMedia(url);
|
||||
});
|
||||
}
|
||||
|
||||
function loadMedia(url) {
|
||||
if (!activePlayer) return;
|
||||
|
||||
const media = {
|
||||
contentId: url,
|
||||
contentType: 'audio/mpeg',
|
||||
streamType: 'LIVE',
|
||||
metadata: {
|
||||
metadataType: 0,
|
||||
title: 'Radio 1'
|
||||
}
|
||||
};
|
||||
|
||||
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) { }
|
||||
log('Stopped playback');
|
||||
}
|
||||
if (activeClient) {
|
||||
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');
|
||||
190
sidecar/package-lock.json
generated
Normal file
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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
@@ -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"
|
||||
}
|
||||
}
|
||||
7
src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
5626
src-tauri/Cargo.lock
generated
Normal file
29
src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "radio-tauri"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "radio_tauri_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
rust_cast = "0.19.0"
|
||||
mdns-sd = "0.17.1"
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
tauri-plugin-shell = "2.3.3"
|
||||
|
||||
BIN
src-tauri/binaries/RadioPlayer-x86_64-pc-windows-msvc.exe
Normal file
BIN
src-tauri/binaries/radiocast-sidecar-x86_64-pc-windows-msvc.exe
Normal file
3
src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
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
|
||||
12
src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-close",
|
||||
"opener:default",
|
||||
"shell:default"
|
||||
]
|
||||
}
|
||||
29
src-tauri/check_log.txt
Normal file
@@ -0,0 +1,29 @@
|
||||
Checking radio-tauri v0.1.0 (D:\Sites\Work\Radio1\radio-tauri\src-tauri)
|
||||
warning: variable does not need to be mutable
|
||||
--> src\lib.rs:38:9
|
||||
|
|
||||
38 | let mut device = CastDevice::connect_without_host_verification(ip_addr.to_string(), 8009)
|
||||
| ----^^^^^^
|
||||
| |
|
||||
| help: remove this `mut`
|
||||
|
|
||||
= note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default
|
||||
|
||||
warning: variable does not need to be mutable
|
||||
--> src\lib.rs:75:9
|
||||
|
|
||||
75 | let mut device = CastDevice::connect_without_host_verification(ip_addr.to_string(), 8009)
|
||||
| ----^^^^^^
|
||||
| |
|
||||
| help: remove this `mut`
|
||||
|
||||
warning: variable does not need to be mutable
|
||||
--> src\lib.rs:99:9
|
||||
|
|
||||
99 | let mut device = CastDevice::connect_without_host_verification(ip_addr.to_string(), 8009)
|
||||
| ----^^^^^^
|
||||
| |
|
||||
| help: remove this `mut`
|
||||
|
||||
warning: `radio-tauri` (lib) generated 3 warnings (run `cargo fix --lib -p radio-tauri` to apply 3 suggestions)
|
||||
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.78s
|
||||
BIN
src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 158 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 516 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
175
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
|
||||
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
||||
use serde_json::json;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
struct SidecarState {
|
||||
child: Mutex<Option<CommandChild>>,
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
known_devices: Mutex<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn list_cast_devices(state: State<'_, AppState>) -> Result<Vec<String>, String> {
|
||||
let devices = state.known_devices.lock().unwrap();
|
||||
let mut list: Vec<String> = devices.keys().cloned().collect();
|
||||
list.sort();
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cast_play(
|
||||
app: AppHandle,
|
||||
state: State<'_, AppState>,
|
||||
sidecar_state: State<'_, SidecarState>,
|
||||
device_name: String,
|
||||
url: String,
|
||||
) -> Result<(), String> {
|
||||
let ip = {
|
||||
let devices = state.known_devices.lock().unwrap();
|
||||
devices
|
||||
.get(&device_name)
|
||||
.cloned()
|
||||
.ok_or("Device not found")?
|
||||
};
|
||||
|
||||
let mut lock = sidecar_state.child.lock().unwrap();
|
||||
|
||||
// Get or spawn child
|
||||
let child = if let Some(ref mut child) = *lock {
|
||||
child
|
||||
} else {
|
||||
println!("Spawning new sidecar...");
|
||||
let sidecar_command = app
|
||||
.shell()
|
||||
.sidecar("radiocast-sidecar")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let (mut rx, child) = sidecar_command.spawn().map_err(|e| e.to_string())?;
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line) => {
|
||||
println!("Sidecar: {}", String::from_utf8_lossy(&line))
|
||||
}
|
||||
CommandEvent::Stderr(line) => {
|
||||
eprintln!("Sidecar Error: {}", String::from_utf8_lossy(&line))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
*lock = Some(child);
|
||||
lock.as_mut().unwrap()
|
||||
};
|
||||
|
||||
let play_cmd = json!({
|
||||
"command": "play",
|
||||
"args": { "ip": ip, "url": url }
|
||||
});
|
||||
|
||||
child
|
||||
.write(format!("{}\n", play_cmd.to_string()).as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cast_stop(
|
||||
_app: AppHandle,
|
||||
sidecar_state: State<'_, SidecarState>,
|
||||
_device_name: String,
|
||||
) -> Result<(), String> {
|
||||
let mut lock = sidecar_state.child.lock().unwrap();
|
||||
if let Some(ref mut child) = *lock {
|
||||
let stop_cmd = json!({ "command": "stop", "args": {} });
|
||||
child
|
||||
.write(format!("{}\n", stop_cmd.to_string()).as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cast_set_volume(
|
||||
_app: AppHandle,
|
||||
sidecar_state: State<'_, SidecarState>,
|
||||
_device_name: String,
|
||||
volume: f32,
|
||||
) -> Result<(), String> {
|
||||
let mut lock = sidecar_state.child.lock().unwrap();
|
||||
if let Some(ref mut child) = *lock {
|
||||
let vol_cmd = json!({ "command": "volume", "args": { "level": volume } });
|
||||
child
|
||||
.write(format!("{}\n", vol_cmd.to_string()).as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.setup(|app| {
|
||||
app.manage(AppState {
|
||||
known_devices: Mutex::new(HashMap::new()),
|
||||
});
|
||||
app.manage(SidecarState {
|
||||
child: Mutex::new(None),
|
||||
});
|
||||
|
||||
let handle = app.handle().clone();
|
||||
thread::spawn(move || {
|
||||
let mdns = ServiceDaemon::new().expect("Failed to create daemon");
|
||||
let receiver = mdns
|
||||
.browse("_googlecast._tcp.local.")
|
||||
.expect("Failed to browse");
|
||||
while let Ok(event) = receiver.recv() {
|
||||
match event {
|
||||
ServiceEvent::ServiceResolved(info) => {
|
||||
let name = info
|
||||
.get_property_val_str("fn")
|
||||
.or_else(|| Some(info.get_fullname()))
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let addresses = info.get_addresses();
|
||||
let ip = addresses
|
||||
.iter()
|
||||
.find(|ip| ip.is_ipv4())
|
||||
.or_else(|| addresses.iter().next());
|
||||
|
||||
if let Some(ip) = ip {
|
||||
let state = handle.state::<AppState>();
|
||||
let mut devices = state.known_devices.lock().unwrap();
|
||||
let ip_str = ip.to_string();
|
||||
if !devices.contains_key(&name) {
|
||||
println!("Discovered Cast Device: {} at {}", name, ip_str);
|
||||
devices.insert(name, ip_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
list_cast_devices,
|
||||
cast_play,
|
||||
cast_stop,
|
||||
cast_set_volume
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
6
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
radio_tauri_lib::run()
|
||||
}
|
||||
39
src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "RadioPlayer",
|
||||
"version": "0.1.0",
|
||||
"identifier": "si.klevze.radioPlayer",
|
||||
"build": {
|
||||
"frontendDist": "../src"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"title": "RadioPlayer",
|
||||
"width": 360,
|
||||
"height": 720,
|
||||
"resizable": false,
|
||||
"decorations": false,
|
||||
"transparent": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"externalBin": [
|
||||
"binaries/RadioPlayer"
|
||||
],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
src/assets/appIcon.png
Normal file
|
After Width: | Height: | Size: 682 KiB |
BIN
src/assets/favicon_io.zip
Normal file
BIN
src/assets/favicon_io/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
src/assets/favicon_io/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
src/assets/favicon_io/app-icon.png
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
src/assets/favicon_io/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
src/assets/favicon_io/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 859 B |
BIN
src/assets/favicon_io/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/assets/favicon_io/icon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
src/assets/favicon_io/site.webmanifest
Normal file
@@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||