fixed cast
This commit is contained in:
140
sidecar/index.js
Normal file
140
sidecar/index.js
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
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 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}`);
|
||||||
|
|
||||||
|
// 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...');
|
||||||
|
launchPlayer(url);
|
||||||
|
} else {
|
||||||
|
activePlayer = player;
|
||||||
|
loadMedia(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
launchPlayer(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
activeClient.on('error', (err) => {
|
||||||
|
error(`Client error: ${err.message}`);
|
||||||
|
try { activeClient.close(); } catch (e) { }
|
||||||
|
activeClient = null;
|
||||||
|
activePlayer = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchPlayer(url) {
|
||||||
|
if (!activeClient) return;
|
||||||
|
|
||||||
|
activeClient.launch(DefaultMediaReceiver, (err, player) => {
|
||||||
|
if (err) {
|
||||||
|
// If launch fails with NOT_ALLOWED, it sometimes means we MUST join or something else is occupying it
|
||||||
|
return error(`Launch error: ${err.message}`);
|
||||||
|
}
|
||||||
|
activePlayer = player;
|
||||||
|
loadMedia(url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadMedia(url) {
|
||||||
|
if (!activePlayer) return;
|
||||||
|
|
||||||
|
const media = {
|
||||||
|
contentId: url,
|
||||||
|
contentType: 'audio/mp3',
|
||||||
|
streamType: 'LIVE'
|
||||||
|
};
|
||||||
|
|
||||||
|
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
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
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src-tauri/Cargo.lock
generated
73
src-tauri/Cargo.lock
generated
@@ -846,6 +846,15 @@ version = "1.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "endi"
|
name = "endi"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -2441,6 +2450,16 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "os_pipe"
|
||||||
|
version = "1.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pango"
|
name = "pango"
|
||||||
version = "0.18.3"
|
version = "0.18.3"
|
||||||
@@ -2894,6 +2913,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
|
"tauri-plugin-shell",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3531,12 +3551,44 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shared_child"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"sigchld",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sigchld"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"os_pipe",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.8"
|
version = "1.4.8"
|
||||||
@@ -3980,6 +4032,27 @@ dependencies = [
|
|||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-shell"
|
||||||
|
version = "2.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c374b6db45f2a8a304f0273a15080d98c70cde86178855fc24653ba657a1144c"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
"log",
|
||||||
|
"open",
|
||||||
|
"os_pipe",
|
||||||
|
"regex",
|
||||||
|
"schemars 0.8.22",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"shared_child",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.9.2"
|
version = "2.9.2"
|
||||||
|
|||||||
@@ -25,4 +25,5 @@ serde_json = "1"
|
|||||||
rust_cast = "0.19.0"
|
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"
|
||||||
|
|
||||||
|
|||||||
BIN
src-tauri/binaries/radiocast-sidecar-x86_64-pc-windows-msvc.exe
Normal file
BIN
src-tauri/binaries/radiocast-sidecar-x86_64-pc-windows-msvc.exe
Normal file
Binary file not shown.
15
src-tauri/build_log.txt
Normal file
15
src-tauri/build_log.txt
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
Compiling radio-tauri v0.1.0 (D:\Sites\Work\RadioCast\src-tauri)
|
||||||
|
error[E0599]: no method named `clone` found for struct `CommandChild` in the current scope
|
||||||
|
--> src\lib.rs:33:36
|
||||||
|
|
|
||||||
|
33 | let returned_child = child.clone();
|
||||||
|
| ^^^^^ method not found in `CommandChild`
|
||||||
|
|
||||||
|
error[E0599]: no method named `clone` found for struct `CommandChild` in the current scope
|
||||||
|
--> src\lib.rs:58:32
|
||||||
|
|
|
||||||
|
58 | let returned_child = child.clone();
|
||||||
|
| ^^^^^ method not found in `CommandChild`
|
||||||
|
|
||||||
|
For more information about this error, try `rustc --explain E0599`.
|
||||||
|
error: could not compile `radio-tauri` (lib) due to 2 previous errors
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"core:window:allow-close",
|
"core:window:allow-close",
|
||||||
"opener:default"
|
"opener:default",
|
||||||
|
"shell:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::IpAddr;
|
use std::sync::Mutex;
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
||||||
use rust_cast::channels::media::{Media, StreamType};
|
use serde_json::json;
|
||||||
use rust_cast::channels::receiver::CastDeviceApp;
|
use tauri::{AppHandle, Manager, State};
|
||||||
use rust_cast::CastDevice;
|
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||||
use tauri::State;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
|
struct SidecarState {
|
||||||
|
child: Mutex<Option<CommandChild>>,
|
||||||
|
}
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
known_devices: Mutex<HashMap<String, String>>,
|
known_devices: Mutex<HashMap<String, String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn list_cast_devices(state: State<'_, Arc<AppState>>) -> Result<Vec<String>, String> {
|
async fn list_cast_devices(state: State<'_, AppState>) -> Result<Vec<String>, String> {
|
||||||
let devices = state.known_devices.lock().unwrap();
|
let devices = state.known_devices.lock().unwrap();
|
||||||
let mut list: Vec<String> = devices.keys().cloned().collect();
|
let mut list: Vec<String> = devices.keys().cloned().collect();
|
||||||
list.sort();
|
list.sort();
|
||||||
@@ -24,7 +26,9 @@ async fn list_cast_devices(state: State<'_, Arc<AppState>>) -> Result<Vec<String
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cast_play(
|
async fn cast_play(
|
||||||
state: State<'_, Arc<AppState>>,
|
app: AppHandle,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
sidecar_state: State<'_, SidecarState>,
|
||||||
device_name: String,
|
device_name: String,
|
||||||
url: String,
|
url: String,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
@@ -36,167 +40,118 @@ async fn cast_play(
|
|||||||
.ok_or("Device not found")?
|
.ok_or("Device not found")?
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("Connecting to {} ({})", device_name, ip);
|
let mut lock = sidecar_state.child.lock().unwrap();
|
||||||
|
|
||||||
// Run connection logic
|
// Get or spawn child
|
||||||
let ip_addr = IpAddr::from_str(&ip).map_err(|e| e.to_string())?;
|
let child = if let Some(ref mut child) = *lock {
|
||||||
|
child
|
||||||
// Connect to port 8009
|
|
||||||
let device = CastDevice::connect_without_host_verification(ip_addr.to_string(), 8009)
|
|
||||||
.map_err(|e| format!("Failed to connect: {:?}", e))?;
|
|
||||||
|
|
||||||
device
|
|
||||||
.connection
|
|
||||||
.connect("receiver-0")
|
|
||||||
.map_err(|e| format!("Failed to connect receiver: {:?}", e))?;
|
|
||||||
|
|
||||||
// Check if Default Media Receiver is already running
|
|
||||||
let app = CastDeviceApp::DefaultMediaReceiver;
|
|
||||||
let status = device
|
|
||||||
.receiver
|
|
||||||
.get_status()
|
|
||||||
.map_err(|e| format!("Failed to get status: {:?}", e))?;
|
|
||||||
|
|
||||||
// Determine if we need to launch or if we can use existing
|
|
||||||
let application = status.applications.iter().find(|a| a.app_id == "CC1AD845"); // Default Media Receiver ID
|
|
||||||
|
|
||||||
let (transport_id, session_id) = if let Some(app_instance) = application {
|
|
||||||
println!(
|
|
||||||
"App already running, joining session {}",
|
|
||||||
app_instance.session_id
|
|
||||||
);
|
|
||||||
(
|
|
||||||
app_instance.transport_id.clone(),
|
|
||||||
app_instance.session_id.clone(),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
println!("Launching app...");
|
println!("Spawning new sidecar...");
|
||||||
let app_instance = device
|
let sidecar_command = app
|
||||||
.receiver
|
.shell()
|
||||||
.launch_app(&app)
|
.sidecar("radiocast-sidecar")
|
||||||
.map_err(|e| format!("Failed to launch app: {:?}", e))?;
|
.map_err(|e| e.to_string())?;
|
||||||
(app_instance.transport_id, app_instance.session_id)
|
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()
|
||||||
};
|
};
|
||||||
|
|
||||||
device
|
let play_cmd = json!({
|
||||||
.connection
|
"command": "play",
|
||||||
.connect(&transport_id)
|
"args": { "ip": ip, "url": url }
|
||||||
.map_err(|e| format!("Failed to connect transport: {:?}", e))?;
|
});
|
||||||
|
|
||||||
// Load Media
|
child
|
||||||
let media = Media {
|
.write(format!("{}\n", play_cmd.to_string()).as_bytes())
|
||||||
content_id: url,
|
.map_err(|e| e.to_string())?;
|
||||||
stream_type: StreamType::Live, // Live stream
|
|
||||||
content_type: "audio/mp3".to_string(),
|
|
||||||
metadata: None,
|
|
||||||
duration: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
device
|
|
||||||
.media
|
|
||||||
.load(&transport_id, &session_id, &media)
|
|
||||||
.map_err(|e| format!("Failed to load media: {:?}", e))?;
|
|
||||||
|
|
||||||
println!("Playing on {}", device_name);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cast_stop(state: State<'_, Arc<AppState>>, device_name: String) -> Result<(), String> {
|
async fn cast_stop(
|
||||||
let ip = {
|
_app: AppHandle,
|
||||||
let devices = state.known_devices.lock().unwrap();
|
sidecar_state: State<'_, SidecarState>,
|
||||||
devices
|
_device_name: String,
|
||||||
.get(&device_name)
|
) -> Result<(), String> {
|
||||||
.cloned()
|
let mut lock = sidecar_state.child.lock().unwrap();
|
||||||
.ok_or("Device not found")?
|
if let Some(ref mut child) = *lock {
|
||||||
};
|
let stop_cmd = json!({ "command": "stop", "args": {} });
|
||||||
|
child
|
||||||
let ip_addr = IpAddr::from_str(&ip).map_err(|e| e.to_string())?;
|
.write(format!("{}\n", stop_cmd.to_string()).as_bytes())
|
||||||
let device = CastDevice::connect_without_host_verification(ip_addr.to_string(), 8009)
|
.map_err(|e| e.to_string())?;
|
||||||
.map_err(|e| format!("Failed to connect: {:?}", e))?;
|
|
||||||
|
|
||||||
device.connection.connect("receiver-0").unwrap();
|
|
||||||
let status = device
|
|
||||||
.receiver
|
|
||||||
.get_status()
|
|
||||||
.map_err(|e| format!("{:?}", e))?;
|
|
||||||
|
|
||||||
if let Some(app) = status.applications.first() {
|
|
||||||
let _transport_id = &app.transport_id;
|
|
||||||
// device.connection.connect(transport_id).unwrap();
|
|
||||||
|
|
||||||
device
|
|
||||||
.receiver
|
|
||||||
.stop_app(app.session_id.as_str())
|
|
||||||
.map_err(|e| format!("{:?}", e))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cast_set_volume(
|
async fn cast_set_volume(
|
||||||
state: State<'_, Arc<AppState>>,
|
_app: AppHandle,
|
||||||
device_name: String,
|
sidecar_state: State<'_, SidecarState>,
|
||||||
|
_device_name: String,
|
||||||
volume: f32,
|
volume: f32,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let ip = {
|
let mut lock = sidecar_state.child.lock().unwrap();
|
||||||
let devices = state.known_devices.lock().unwrap();
|
if let Some(ref mut child) = *lock {
|
||||||
devices
|
let vol_cmd = json!({ "command": "volume", "args": { "level": volume } });
|
||||||
.get(&device_name)
|
child
|
||||||
.cloned()
|
.write(format!("{}\n", vol_cmd.to_string()).as_bytes())
|
||||||
.ok_or("Device not found")?
|
.map_err(|e| e.to_string())?;
|
||||||
};
|
}
|
||||||
|
|
||||||
let ip_addr = IpAddr::from_str(&ip).map_err(|e| e.to_string())?;
|
|
||||||
let device = CastDevice::connect_without_host_verification(ip_addr.to_string(), 8009)
|
|
||||||
.map_err(|e| format!("Failed to connect: {:?}", e))?;
|
|
||||||
|
|
||||||
device.connection.connect("receiver-0").unwrap();
|
|
||||||
|
|
||||||
// Volume is on the receiver struct
|
|
||||||
let vol = rust_cast::channels::receiver::Volume {
|
|
||||||
level: Some(volume),
|
|
||||||
muted: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
device
|
|
||||||
.receiver
|
|
||||||
.set_volume(vol)
|
|
||||||
.map_err(|e| format!("{:?}", e))?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let app_state = Arc::new(AppState {
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_shell::init())
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.setup(|app| {
|
||||||
|
app.manage(AppState {
|
||||||
known_devices: Mutex::new(HashMap::new()),
|
known_devices: Mutex::new(HashMap::new()),
|
||||||
});
|
});
|
||||||
|
app.manage(SidecarState {
|
||||||
|
child: Mutex::new(None),
|
||||||
|
});
|
||||||
|
|
||||||
let state_clone = app_state.clone();
|
let handle = app.handle().clone();
|
||||||
|
|
||||||
// Start Discovery Thread
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mdns = ServiceDaemon::new().expect("Failed to create daemon");
|
let mdns = ServiceDaemon::new().expect("Failed to create daemon");
|
||||||
// Google Cast service
|
|
||||||
let receiver = mdns
|
let receiver = mdns
|
||||||
.browse("_googlecast._tcp.local.")
|
.browse("_googlecast._tcp.local.")
|
||||||
.expect("Failed to browse");
|
.expect("Failed to browse");
|
||||||
|
|
||||||
while let Ok(event) = receiver.recv() {
|
while let Ok(event) = receiver.recv() {
|
||||||
match event {
|
match event {
|
||||||
ServiceEvent::ServiceResolved(info) => {
|
ServiceEvent::ServiceResolved(info) => {
|
||||||
// Try to get "fn" property for Friendly Name
|
|
||||||
let name = info
|
let name = info
|
||||||
.get_property_val_str("fn")
|
.get_property_val_str("fn")
|
||||||
.or_else(|| Some(info.get_fullname()))
|
.or_else(|| Some(info.get_fullname()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string();
|
.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) = info.get_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();
|
let ip_str = ip.to_string();
|
||||||
let mut devices = state_clone.known_devices.lock().unwrap();
|
|
||||||
if !devices.contains_key(&name) {
|
if !devices.contains_key(&name) {
|
||||||
println!("Discovered Cast Device: {} at {}", name, ip_str);
|
println!("Discovered Cast Device: {} at {}", name, ip_str);
|
||||||
devices.insert(name, ip_str);
|
devices.insert(name, ip_str);
|
||||||
@@ -207,10 +162,8 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
tauri::Builder::default()
|
})
|
||||||
.plugin(tauri_plugin_opener::init())
|
|
||||||
.manage(app_state)
|
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
list_cast_devices,
|
list_cast_devices,
|
||||||
cast_play,
|
cast_play,
|
||||||
|
|||||||
@@ -25,6 +25,9 @@
|
|||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
|
"externalBin": [
|
||||||
|
"binaries/radiocast-sidecar"
|
||||||
|
],
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
|
|||||||
Reference in New Issue
Block a user